# Sage X3 MCP Server — Read-Only Data Access Layer ## TL;DR > **Quick Summary**: Build a TypeScript MCP server that gives AI agents structured, read-only access to Sage X3 ERP data (V12 on-premise) via 9 universal tools spanning REST (SData 2.0) and SOAP APIs across all 6 modules (Sales, Purchasing, Financials, Stock, Manufacturing, Common Data). > > **Deliverables**: > - MCP server with 9 tools: sage_health, sage_query, sage_read, sage_search, sage_list_entities, sage_describe_entity, sage_soap_read, sage_soap_query, sage_get_context > - REST client (SData 2.0) with Basic auth, pagination, error handling > - SOAP client with pool support, XML/JSON response handling > - Dual transport: stdio (default) + Streamable HTTP (optional) > - Full TDD test suite with vitest > - Environment variable-based configuration with validation > > **Estimated Effort**: Medium-Large > **Parallel Execution**: YES — 5 waves, max 5 concurrent tasks > **Critical Path**: Task 1 → Task 4 → Task 7 → Task 10 → Task 12 → Final --- ## Context ### Original Request Build an MCP server for Sage X3 ERP to democratize Sage knowledge — any company employee can ask an AI agent questions about their Sage X3 data (invoices, orders, customers, stock, etc.) without needing a Sage expert. The MCP server acts purely as a structured data access layer; intelligence/RAG/knowledge is a separate future project. ### Interview Summary **Key Discussions**: - **Sage X3 Version**: V12 on-premise - **APIs Available**: REST (SData 2.0) + SOAP web services — no GraphQL currently - **Modules Used**: ALL 6 — Sales, Purchasing, Financials, Stock, Manufacturing, Common Data - **Access Mode**: Read-only (safe, diagnostic) — no write operations - **Authentication**: Basic Auth (dedicated web service user:password) - **SOAP Status**: Connection pools configured, key objects already published - **Scale**: Small (1-5 concurrent users) - **Tool Design**: Universal tools (~9 generic tools) rather than entity-specific (avoids 100+ tool token explosion) - **Tests**: TDD approach with vitest - **Configuration**: Environment variables - **MCP Client**: Any MCP-compatible client (Claude Code, Opencode, etc.) — transport-agnostic - **Scope**: MCP interface ONLY — no RAG, no docs, no business logic engine **Research Findings**: - Sage X3 REST uses representations + classes + facets ($query, $details) via Syracuse server - SOAP uses RPC/encoded style with CAdxCallContext (codeLang, poolAlias, requestConfig) - SOAP always returns HTTP 200 — must check `` field (1=success, 0=error) - Field names are X3 internal codes (BPCNUM, BPCNAM, SIVTYP) — cryptic without context - REST pagination: cursor-based via `$links.$next`, default 20 records/page - SOAP has data volume licensing limits (WSSIZELIMIT per period) - `adxwss.optreturn=JSON` in SOAP requestConfig may allow JSON responses (avoid XML parsing) - MCP SDK v1.27.1 stable — use `server.registerTool()` with Zod schemas - Tool annotations (readOnlyHint, destructiveHint, idempotentHint) signal safety to AI - Token economics: 40 tools ≈ 43K tokens; 100 tools ≈ 129K tokens — universal tools are critical - Research files saved to `.sisyphus/research/sage-x3-api-landscape.md` and `.sisyphus/research/mcp-server-architecture.md` ### Metis Review **Identified Gaps** (addressed): - SDK version: Use v1.27.1 stable (`@modelcontextprotocol/sdk`), NOT v2 pre-alpha split packages - SOAP WSDL spike needed: `soap` npm package may struggle with X3's RPC/encoded WSDL — must validate before building SOAP client - `adxwss.optreturn=JSON` reliability unknown — need `fast-xml-parser` as fallback - Tool annotations required: `readOnlyHint: true` on every tool - Response size cap needed: hard limit of 200 records, default 20 - Error messages need AI-oriented hints (not just technical errors) - Self-signed SSL certificates on-premise: add `SAGE_X3_REJECT_UNAUTHORIZED` env var - Logging: `console.error()` only — NEVER `console.log()` (corrupts stdio JSON-RPC) - SOAP CAdxCallContext: `codeUser`/`password` fields empty in V12 (auth at HTTP level) --- ## Work Objectives ### Core Objective Build a TypeScript MCP server that provides 9 universal, read-only tools for querying any Sage X3 business object across all modules via REST and SOAP APIs. ### Concrete Deliverables - `src/index.ts` — MCP server entry point with dual transport (stdio/HTTP) - `src/config/` — Environment variable loading and validation - `src/clients/rest-client.ts` — SData 2.0 REST client - `src/clients/soap-client.ts` — SOAP client with pool support - `src/tools/*.ts` — 9 tool implementations - `src/types/` — Shared TypeScript interfaces - `src/utils/` — Error handling, logging, response formatting - `vitest.config.ts` + `src/**/__tests__/*.test.ts` — Full test suite - `.env.example` — Documented environment variable template - `package.json`, `tsconfig.json` — Project configuration ### Definition of Done - [ ] `npx tsc --noEmit` → exit 0 (zero TypeScript errors) - [ ] `npx vitest run` → all tests pass, 0 failures - [ ] `echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | node dist/index.js` → lists 9 tools - [ ] Each tool has `readOnlyHint: true` annotation in tools/list response - [ ] REST client only uses HTTP GET — no POST/PUT/DELETE methods exist - [ ] SOAP client only exposes read/query/getDescription — no save/delete/modify/run methods exist - [ ] Missing env vars at startup → stderr error + exit 1 - [ ] All tool errors return `{ isError: true, content: [...] }` with AI-oriented hints ### Must Have - 9 MCP tools as specified - REST client with SData 2.0 query/read/pagination - SOAP client with read/query/getDescription - Basic Auth for X3 authentication - Dual transport: stdio (default) + HTTP (optional via `MCP_TRANSPORT=http`) - Environment variable configuration with startup validation - Response size cap (max 200 records per query) - Pagination metadata in query responses (hasMore, nextUrl, returned) - Error responses with classification + AI hints - TDD test suite ### Must NOT Have (Guardrails) - **NO write operations**: REST client has no POST/PUT/DELETE. SOAP client has no save/delete/modify/run/actionObject/insertLines/deleteLines - **NO data transformation**: Return X3 data as-received — no field renaming, no aggregation, no calculated fields - **NO MCP Resources or Prompts**: Tools only for v1 - **NO tools beyond the 9**: Any additional tool = explicit scope change - **NO caching layer**: Every call hits X3 directly (singleton SOAP client is connection reuse, not caching) - **NO `console.log()`**: Use `console.error()` for all logging — `console.log` corrupts stdio JSON-RPC - **NO pretty-printed JSON in responses**: Minified JSON to save AI context tokens - **NO multi-endpoint support**: Single X3 instance only - **NO GraphQL/OAuth2**: Basic Auth + REST + SOAP only - **NO Docker/deployment orchestration**: A basic Dockerfile at most if time permits - **NO `forceSoap12Headers: true`**: X3 uses SOAP 1.1 --- ## Verification Strategy (MANDATORY) > **ZERO HUMAN INTERVENTION** — ALL verification is agent-executed. No exceptions. ### Test Decision - **Infrastructure exists**: NO (greenfield) - **Automated tests**: YES — TDD (RED → GREEN → REFACTOR) - **Framework**: vitest (TypeScript-native, ESM-friendly, fast) - **Setup included**: Task 1 installs vitest + configures vitest.config.ts ### QA Policy Every task MUST include agent-executed QA scenarios. Evidence saved to `.sisyphus/evidence/task-{N}-{scenario-slug}.{ext}`. - **MCP Server**: Use Bash (piped JSON-RPC via stdio) — send JSON-RPC, validate response - **HTTP Transport**: Use Bash (curl) — POST to endpoint, assert response - **TypeScript**: Use Bash (tsc --noEmit) — verify compilation - **Tests**: Use Bash (vitest run) — verify all tests pass --- ## Execution Strategy ### Parallel Execution Waves ``` Wave 1 (Start Immediately — foundation): ├── Task 1: Project scaffolding + dependencies + build config [quick] ├── Task 2: Shared types, config module, error utilities [quick] └── Task 3: SOAP WSDL spike (validate soap lib against X3) [deep] Wave 2 (After Wave 1 — API clients + server skeleton): ├── Task 4: REST client (SData 2.0) + tests [unspecified-high] ├── Task 5: SOAP client (based on spike results) + tests [deep] └── Task 6: MCP server skeleton + sage_health tool + tests [unspecified-high] Wave 3 (After Wave 2 — all tools, MAX PARALLEL): ├── Task 7: REST tools: sage_query + sage_read + sage_search (TDD) [unspecified-high] ├── Task 8: REST tools: sage_list_entities + sage_get_context (TDD) [unspecified-high] └── Task 9: SOAP tools: sage_soap_read + sage_soap_query + sage_describe_entity (TDD) [unspecified-high] Wave 4 (After Wave 3 — integration): ├── Task 10: Complete tool registration + HTTP transport + dual entry point [unspecified-high] └── Task 11: Integration test suite + .env.example [deep] Wave FINAL (After ALL tasks — independent review, 4 parallel): ├── Task F1: Plan compliance audit (oracle) ├── Task F2: Code quality review (unspecified-high) ├── Task F3: Real manual QA (unspecified-high) └── Task F4: Scope fidelity check (deep) Critical Path: Task 1 → Task 4 → Task 7 → Task 10 → Task 11 → F1-F4 Parallel Speedup: ~60% faster than sequential Max Concurrent: 3 (Waves 1, 3) ``` ### Dependency Matrix | Task | Depends On | Blocks | Wave | |------|-----------|--------|------| | 1 | — | 2, 3, 4, 5, 6 | 1 | | 2 | 1 | 4, 5, 6, 7, 8, 9 | 1 | | 3 | 1 | 5 | 1 | | 4 | 1, 2 | 7, 8 | 2 | | 5 | 1, 2, 3 | 9 | 2 | | 6 | 1, 2 | 7, 8, 9, 10 | 2 | | 7 | 4, 6 | 10 | 3 | | 8 | 4, 6 | 10 | 3 | | 9 | 5, 6 | 10 | 3 | | 10 | 6, 7, 8, 9 | 11 | 4 | | 11 | 10 | F1-F4 | 4 | ### Agent Dispatch Summary - **Wave 1**: 3 tasks — T1 → `quick`, T2 → `quick`, T3 → `deep` - **Wave 2**: 3 tasks — T4 → `unspecified-high`, T5 → `deep`, T6 → `unspecified-high` - **Wave 3**: 3 tasks — T7 → `unspecified-high`, T8 → `unspecified-high`, T9 → `unspecified-high` - **Wave 4**: 2 tasks — T10 → `unspecified-high`, T11 → `deep` - **FINAL**: 4 tasks — F1 → `oracle`, F2 → `unspecified-high`, F3 → `unspecified-high`, F4 → `deep` ### IMPORTANT NOTE FOR ALL AGENTS When using `background_task` or similar sub-agent functions, **timeouts are in MILLISECONDS not seconds**. - 60 seconds = 60000 ms - 120 seconds = 120000 ms - Default timeout: 120000 ms (2 minutes) --- ## TODOs - [x] 1. Project Scaffolding + Dependencies + Build Config **What to do**: - Initialize npm project: `npm init -y` - Install production deps: `@modelcontextprotocol/sdk@^1.27.1`, `zod`, `soap`, `fast-xml-parser` - Install dev deps: `vitest`, `typescript`, `@types/node`, `tsx` - Create `tsconfig.json` with strict mode, ES2022 target, NodeNext module resolution, `outDir: "dist"` - Create `vitest.config.ts` with TypeScript support - Create directory structure: `src/`, `src/tools/`, `src/clients/`, `src/config/`, `src/types/`, `src/utils/`, `src/tools/__tests__/`, `src/clients/__tests__/` - Create `.gitignore` (node_modules, dist, .env, *.js in src) - Add npm scripts: `build` (tsc), `start` (node dist/index.js), `dev` (tsx src/index.ts), `test` (vitest run), `test:watch` (vitest) - Create empty `src/index.ts` with a placeholder comment **Must NOT do**: - Do NOT write any business logic or tool code - Do NOT install Express or any HTTP framework (we use SDK's built-in transport) - Do NOT add Docker files **Recommended Agent Profile**: - **Category**: `quick` - **Skills**: [] **Parallelization**: - **Can Run In Parallel**: YES - **Parallel Group**: Wave 1 (with Tasks 2, 3) - **Blocks**: Tasks 2, 3, 4, 5, 6 - **Blocked By**: None (can start immediately) **References**: - `@modelcontextprotocol/sdk` npm page — for exact package name and version - `.sisyphus/research/mcp-server-architecture.md` — MCP SDK import patterns and version info - Example: `import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'` - Example: `import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'` **Acceptance Criteria**: - [ ] `npm install` completes without errors - [ ] `npx tsc --noEmit` on empty src/index.ts exits 0 - [ ] `npx vitest run` exits 0 (no tests yet, but config is valid) - [ ] Directory structure exists: src/tools/, src/clients/, src/config/, src/types/, src/utils/ **QA Scenarios (MANDATORY)**: ``` Scenario: Project builds successfully Tool: Bash Steps: 1. Run `npm install` in project root 2. Run `npx tsc --noEmit` 3. Run `npx vitest run` Expected Result: All three commands exit with code 0 Evidence: .sisyphus/evidence/task-1-build.txt Scenario: Dependencies installed correctly Tool: Bash Steps: 1. Run `node -e "require('@modelcontextprotocol/sdk/server/mcp.js')"` 2. Run `node -e "require('zod')"` 3. Run `node -e "require('soap')"` 4. Run `node -e "require('fast-xml-parser')"` Expected Result: All four commands exit 0 (no MODULE_NOT_FOUND) Evidence: .sisyphus/evidence/task-1-deps.txt ``` **Commit**: YES - Message: `feat(scaffold): initialize project with TypeScript, vitest, MCP SDK` - Files: `package.json`, `tsconfig.json`, `vitest.config.ts`, `.gitignore`, `src/index.ts` - Pre-commit: `npx tsc --noEmit` - [x] 2. Shared Types, Config Module, Error Utilities **What to do**: - Create `src/types/sage.ts` — TypeScript interfaces for: - `SageConfig` (url, user, password, endpoint, poolAlias, language, rejectUnauthorized) - `RestQueryOptions` (entity, representation, where, orderBy, count, nextUrl, select) - `RestQueryResult` (records: unknown[], pagination: { returned, hasMore, nextUrl? }) - `RestDetailResult` (record: unknown) - `SoapCallContext` (codeLang, poolAlias, poolId, requestConfig) - `SoapReadOptions` (objectName, publicName, key: Record) - `SoapQueryOptions` (objectName, publicName, listSize, inputXml?) - `SoapResult` (status: number, data: unknown, messages: SoapMessage[], technicalInfos: SoapTechInfo) - `SoapMessage` (type: number, message: string) - `ToolResponse` (records?, record?, pagination?, error?, hint?) - `HealthStatus` (rest: { status, latencyMs, endpoint }, soap: { status, latencyMs, poolAlias }) - Create `src/types/index.ts` — barrel export - Create `src/config/index.ts` — Load and validate env vars: - Required: `SAGE_X3_URL`, `SAGE_X3_USER`, `SAGE_X3_PASSWORD`, `SAGE_X3_ENDPOINT` - Optional: `SAGE_X3_POOL_ALIAS` (default: "SEED"), `SAGE_X3_LANGUAGE` (default: "ENG"), `MCP_TRANSPORT` (default: "stdio"), `MCP_HTTP_PORT` (default: "3000"), `SAGE_X3_REJECT_UNAUTHORIZED` (default: "true") - On missing required var: `console.error("FATAL: Missing required environment variable: VAR_NAME")` → `process.exit(1)` - Return typed `SageConfig` object - Create `src/utils/errors.ts` — Error utilities: - `formatToolError(error: unknown, hint?: string): CallToolResult` — returns `{ isError: true, content: [{ type: 'text', text: '...\n\nHint: ...' }] }` - `classifyError(error: unknown): 'auth_error' | 'timeout' | 'not_found' | 'connection_error' | 'x3_error' | 'unknown'` - `getErrorHint(classification: string): string` — AI-oriented hint per error type - Create `src/utils/response.ts` — Response formatting: - `formatQueryResponse(records: unknown[], pagination: object): CallToolResult` - `formatReadResponse(record: unknown): CallToolResult` - All responses use `JSON.stringify(data)` (minified, NOT pretty-printed) - Create `src/utils/index.ts` — barrel export - Write tests for config validation and error formatting **Must NOT do**: - Do NOT create the actual REST or SOAP client implementations - Do NOT register any MCP tools - Do NOT add `console.log()` anywhere — use `console.error()` only **Recommended Agent Profile**: - **Category**: `quick` - **Skills**: [] **Parallelization**: - **Can Run In Parallel**: YES - **Parallel Group**: Wave 1 (with Tasks 1, 3) - **Blocks**: Tasks 4, 5, 6, 7, 8, 9 - **Blocked By**: Task 1 (needs package.json + dependencies installed) **References**: - `.sisyphus/research/sage-x3-api-landscape.md` — Sage X3 REST URL patterns, SOAP callContext structure, business object codes - `.sisyphus/research/mcp-server-architecture.md` — MCP tool response patterns (`{ content: [{ type: 'text', text: ... }], isError: true }`) - Sage X3 REST URL pattern: `http://SERVER:PORT/api1/x3/erp/ENDPOINT/CLASS?representation=REPR.$query` - Sage X3 SOAP callContext: `codeLang`, `poolAlias`, `poolId`, `requestConfig` - MCP tool error pattern: `{ isError: true, content: [{ type: 'text', text: 'Error message\n\nHint: suggestion' }] }` **Acceptance Criteria**: - [ ] `npx tsc --noEmit` exits 0 — all types compile - [ ] `npx vitest run src/config` — config validation tests pass - [ ] `npx vitest run src/utils` — error/response utility tests pass - [ ] Config throws + exits on missing `SAGE_X3_URL` - [ ] `formatToolError` returns `{ isError: true, content: [...] }` with hint text **QA Scenarios (MANDATORY)**: ``` Scenario: Config validates required environment variables Tool: Bash Steps: 1. Run `SAGE_X3_USER=x SAGE_X3_PASSWORD=x SAGE_X3_ENDPOINT=x npx tsx src/config/index.ts` (missing SAGE_X3_URL) 2. Check stderr contains "FATAL: Missing required environment variable: SAGE_X3_URL" 3. Check exit code is 1 Expected Result: Process exits with code 1 and clear error on stderr Evidence: .sisyphus/evidence/task-2-config-validation.txt Scenario: Error formatter produces AI-friendly errors Tool: Bash Steps: 1. Run vitest for error utility tests 2. Verify formatToolError returns isError: true 3. Verify hint text is included Expected Result: All utility tests pass Evidence: .sisyphus/evidence/task-2-utils-tests.txt ``` **Commit**: YES - Message: `feat(core): add shared types, config validation, and error utilities` - Files: `src/types/*.ts`, `src/config/index.ts`, `src/utils/*.ts`, `src/**/__tests__/*.test.ts` - Pre-commit: `npx vitest run` - [x] 3. SOAP WSDL Spike — Validate soap Library Against X3 **What to do**: - Create `spike/soap-spike.ts` — standalone proof-of-concept script - The spike MUST validate these critical questions: 1. Can the `soap` npm package parse X3's WSDL at `/soap-wsdl/syracuse/collaboration/syracuse/CAdxWebServiceXmlCC?wsdl`? 2. Can it construct a valid `getDescription` call with `CAdxCallContext`? 3. Does `adxwss.optreturn=JSON` in requestConfig make SOAP return JSON instead of XML? 4. What format does `resultXml` come back in — parsed object or raw string? 5. Does Basic Auth work at the HTTP level (not in CAdxCallContext fields)? - Create `spike/soap-spike-results.md` — document findings with concrete examples of: - Working SOAP request envelope - Response structure (parsed vs raw) - Whether JSON mode works - Any WSDL parsing errors or workarounds needed - If `soap` library FAILS: document the failure and recommend the fallback approach (raw HTTP POST with XML templates + `fast-xml-parser` for response parsing) - If `soap` library WORKS: document the exact API calls and patterns to use - NOTE: This spike requires access to the actual X3 Syracuse server. If unavailable, create the spike script with mock data and document what needs to be tested against real X3. **Must NOT do**: - Do NOT build the full SOAP client — this is a spike only - Do NOT build any MCP tools - Do NOT spend more than a few hours — quick validation only **Recommended Agent Profile**: - **Category**: `deep` - **Skills**: [] **Parallelization**: - **Can Run In Parallel**: YES - **Parallel Group**: Wave 1 (with Tasks 1, 2) - **Blocks**: Task 5 (SOAP client depends on spike results) - **Blocked By**: Task 1 (needs soap + fast-xml-parser installed) **References**: - `.sisyphus/research/sage-x3-api-landscape.md` — SOAP WSDL URL, callContext structure, requestConfig options - SOAP WSDL URL: `http://SERVER:PORT/soap-wsdl/syracuse/collaboration/syracuse/CAdxWebServiceXmlCC?wsdl` - SOAP endpoint URL: `http://SERVER:PORT/soap-generic/syracuse/collaboration/syracuse/CAdxWebServiceXmlCC` - CAdxCallContext: `{ codeLang: "ENG", poolAlias: "POOL", poolId: "", requestConfig: "adxwss.optreturn=JSON&adxwss.beautify=true" }` - X3 uses RPC/encoded SOAP 1.1 (NOT document/literal, NOT SOAP 1.2) - V12 uses HTTP-level Basic Auth — codeUser/password fields in callContext should be empty **Acceptance Criteria**: - [ ] `spike/soap-spike.ts` exists and is executable - [ ] `spike/soap-spike-results.md` documents all 5 validation questions with answers - [ ] Clear recommendation: use `soap` library OR use raw XML approach - [ ] If soap works: document exact API patterns for read/query/getDescription - [ ] If soap fails: document exact failure + working raw XML fallback **QA Scenarios (MANDATORY)**: ``` Scenario: Spike script runs without crashing Tool: Bash Steps: 1. Set required env vars (SAGE_X3_URL, SAGE_X3_USER, SAGE_X3_PASSWORD, SAGE_X3_ENDPOINT, SAGE_X3_POOL_ALIAS) 2. Run `npx tsx spike/soap-spike.ts` 3. Check output for WSDL parsing results Expected Result: Script completes (success or documented failure) — does not crash with unhandled error Evidence: .sisyphus/evidence/task-3-soap-spike.txt Scenario: Results document is complete Tool: Bash Steps: 1. Check spike/soap-spike-results.md exists 2. Verify it contains answers to all 5 validation questions 3. Verify it contains a clear recommendation Expected Result: All 5 questions answered, recommendation present Evidence: .sisyphus/evidence/task-3-results-check.txt ``` **Commit**: YES - Message: `spike(soap): validate SOAP library against X3 WSDL` - Files: `spike/soap-spike.ts`, `spike/soap-spike-results.md` - Pre-commit: `npx tsc --noEmit` - [x] 4. REST Client (SData 2.0) + Tests **What to do**: - Create `src/clients/rest-client.ts` — SData 2.0 REST client with: - Constructor takes `SageConfig` (from src/types/) - `async query(options: RestQueryOptions): Promise` — paginated query - Builds URL: `${config.url}/api1/x3/erp/${config.endpoint}/${options.entity}?representation=${options.representation || options.entity}.$query` - Adds query params: `&where=${options.where}`, `&orderBy=${options.orderBy}`, `&count=${Math.min(options.count || 20, 200)}` - If `options.nextUrl` is provided, use it directly (cursor-based pagination) - Hard cap: `count` cannot exceed 200 - Sends `Authorization: Basic ${Buffer.from(config.user + ':' + config.password).toString('base64')}` - Returns `{ records: response.$resources, pagination: { returned: response.$resources.length, hasMore: !!response.$links?.$next, nextUrl: response.$links?.$next?.$url } }` - `async read(entity: string, key: string, representation?: string): Promise` — single record - URL: `${config.url}/api1/x3/erp/${config.endpoint}/${entity}('${key}')?representation=${representation || entity}.$details` - `async listEntities(): Promise` — list available endpoints - URL: `${config.url}/api1/x3/erp/${config.endpoint}` or list representations - `async healthCheck(): Promise<{ status: string, latencyMs: number }>` — test connectivity - Private `async get(url: string): Promise` — core HTTP GET with: - Basic Auth header - `Accept: application/json` header - 15-second timeout via `AbortSignal.timeout(15000)` - SSL/TLS: if `config.rejectUnauthorized === false`, set `NODE_TLS_REJECT_UNAUTHORIZED=0` - Error handling: detect non-JSON responses (HTML login page redirect), classify errors - **CRITICAL**: The REST client MUST NOT have ANY method that sends POST, PUT, DELETE, or PATCH - The class should only have a private `.get()` method for HTTP - Use native `fetch` (Node 18+) — no axios/got dependency - Create `src/clients/__tests__/rest-client.test.ts` — TDD tests with mocked fetch: - Test query returns paginated results - Test read returns single record - Test pagination cursor handling (hasMore=true/false) - Test auth header is sent correctly - Test 401 error handling - Test timeout handling - Test empty results - Test count cap at 200 **Must NOT do**: - Do NOT add POST, PUT, DELETE, PATCH methods — read-only enforcement - Do NOT add caching - Do NOT pretty-print JSON responses **Recommended Agent Profile**: - **Category**: `unspecified-high` - **Skills**: [] **Parallelization**: - **Can Run In Parallel**: YES - **Parallel Group**: Wave 2 (with Tasks 5, 6) - **Blocks**: Tasks 7, 8 - **Blocked By**: Tasks 1, 2 **References**: - `.sisyphus/research/sage-x3-api-landscape.md` — Full REST URL patterns, query params, pagination structure - REST query URL: `http://SERVER:PORT/api1/x3/erp/ENDPOINT/CLASS?representation=REPR.$query&where=COND&orderBy=FIELD&count=N` - REST read URL: `http://SERVER:PORT/api1/x3/erp/ENDPOINT/CLASS('KEY')?representation=REPR.$details` - Pagination: `response.$links.$next.$url` for cursor-based next page - Response envelope: `{ "$itemsPerPage": 20, "$resources": [...], "$links": { "$next": { "$url": "..." } } }` - SData query examples: `where=BPCNAM eq 'ACME'`, `where=left(BPCNAM,4) eq 'Test'`, `orderBy=BPCNAM desc` - Auth: `Authorization: Basic ${Buffer.from('user:password').toString('base64')}` **Acceptance Criteria**: - [ ] `npx vitest run src/clients/__tests__/rest-client.test.ts` — all tests pass - [ ] REST client class has ONLY a private `.get()` method for HTTP (no POST/PUT/DELETE) - [ ] Query enforces max 200 records cap - [ ] Pagination returns `{ hasMore, nextUrl, returned }` - [ ] 15-second timeout on all requests - [ ] Auth header sent on every request **QA Scenarios (MANDATORY)**: ``` Scenario: REST client query returns paginated data Tool: Bash Steps: 1. Run vitest for rest-client tests 2. Verify query test returns records array + pagination object 3. Verify count > 200 is capped to 200 Expected Result: All REST client tests pass Evidence: .sisyphus/evidence/task-4-rest-client-tests.txt Scenario: REST client has no write methods Tool: Bash (grep) Steps: 1. Search src/clients/rest-client.ts for POST, PUT, DELETE, PATCH 2. Verify zero matches Expected Result: No write HTTP methods found in REST client Evidence: .sisyphus/evidence/task-4-readonly-check.txt ``` **Commit**: YES - Message: `feat(rest): add SData 2.0 REST client with pagination and auth` - Files: `src/clients/rest-client.ts`, `src/clients/__tests__/rest-client.test.ts` - Pre-commit: `npx vitest run src/clients` - [x] 5. SOAP Client + Tests **What to do**: - Read `spike/soap-spike-results.md` FIRST to understand which approach to use - **If `soap` library works** (spike found it compatible with X3 WSDL): - Create `src/clients/soap-client.ts` using the `soap` npm package - Create SOAP client from WSDL URL: `soap.createClientAsync(wsdlUrl, { forceSoap12Headers: false })` - Set Basic Auth: `client.setSecurity(new soap.BasicAuthSecurity(user, password))` - Singleton pattern: create client once, reuse across calls - **If `soap` library fails** (spike found issues): - Create `src/clients/soap-client.ts` using raw HTTP POST + XML templates - Use `fast-xml-parser` to parse XML responses - Build SOAP envelopes from templates (see references for exact XML structure) - **Regardless of approach, implement these methods**: - `async read(options: SoapReadOptions): Promise` — read single record - Calls SOAP `read` operation with objectXml containing key fields - Sets requestConfig: `adxwss.optreturn=JSON&adxwss.beautify=false` - `async query(options: SoapQueryOptions): Promise` — list records - Calls SOAP `query` operation with listSize and optional inputXml - `async getDescription(publicName: string): Promise` — get field definitions - Calls SOAP `getDescription` operation - Returns field names, types, lengths, labels (C_ENG field in response) - `async healthCheck(): Promise<{ status: string, latencyMs: number }>` — test connectivity - Private helper to build `CAdxCallContext`: `{ codeLang: config.language, poolAlias: config.poolAlias, poolId: "", requestConfig: "adxwss.optreturn=JSON&adxwss.beautify=false" }` - **CRITICAL**: The SOAP client MUST NOT expose save, delete, modify, run, actionObject, insertLines, or deleteLines operations - Handle SOAP-specific error patterns: - HTTP 200 with `0` = business error → parse `` array - HTTP 401 = auth failure - HTTP 500 "No Web services accepted" = pool children not configured - Timeout: 30s for read, 60s for query - Use 30-second timeout for read operations, 60-second timeout for query operations - Create `src/clients/__tests__/soap-client.test.ts` — TDD tests with mocked SOAP responses **Must NOT do**: - Do NOT expose save, delete, modify, run, actionObject, insertLines, or deleteLines - Do NOT use `forceSoap12Headers: true` — X3 is SOAP 1.1 - Do NOT pretty-print JSON responses - Do NOT cache responses **Recommended Agent Profile**: - **Category**: `deep` - **Skills**: [] **Parallelization**: - **Can Run In Parallel**: YES - **Parallel Group**: Wave 2 (with Tasks 4, 6) - **Blocks**: Task 9 - **Blocked By**: Tasks 1, 2, 3 (SOAP spike results are critical input) **References**: - `spike/soap-spike-results.md` — **READ THIS FIRST** — spike findings determine implementation approach - `.sisyphus/research/sage-x3-api-landscape.md` — SOAP WSDL URL, callContext, request/response formats - SOAP endpoint: `http://SERVER:PORT/soap-generic/syracuse/collaboration/syracuse/CAdxWebServiceXmlCC` - WSDL: `http://SERVER:PORT/soap-wsdl/syracuse/collaboration/syracuse/CAdxWebServiceXmlCC?wsdl` - CAdxCallContext XML: `ENGPOOLadxwss.optreturn=JSON` - SOAP response: `1.........` - SIH getDescription response (example in research file): returns `` — C_ENG is the English label - V12 auth: Basic Auth at HTTP level, NOT in CAdxCallContext (codeUser/password fields empty) **Acceptance Criteria**: - [ ] `npx vitest run src/clients/__tests__/soap-client.test.ts` — all tests pass - [ ] SOAP client has ONLY read/query/getDescription/healthCheck methods (no write ops) - [ ] Uses approach validated by SOAP spike results - [ ] Handles status=0 (business error) correctly - [ ] 30s timeout for read, 60s timeout for query - [ ] CAdxCallContext has empty codeUser/password **QA Scenarios (MANDATORY)**: ``` Scenario: SOAP client tests pass Tool: Bash Steps: 1. Run `npx vitest run src/clients/__tests__/soap-client.test.ts` 2. Verify all tests pass Expected Result: All SOAP client tests pass Evidence: .sisyphus/evidence/task-5-soap-client-tests.txt Scenario: SOAP client has no write methods Tool: Bash (grep) Steps: 1. Search src/clients/soap-client.ts for 'save', 'delete', 'modify', 'run(' (as method names) 2. Verify zero matches for write operation methods Expected Result: No write SOAP operations found Evidence: .sisyphus/evidence/task-5-readonly-check.txt ``` **Commit**: YES - Message: `feat(soap): add SOAP client with read/query/getDescription` - Files: `src/clients/soap-client.ts`, `src/clients/__tests__/soap-client.test.ts` - Pre-commit: `npx vitest run src/clients` - [x] 6. MCP Server Skeleton + sage_health Tool + Tests **What to do**: - Create `src/server.ts` — MCP server setup: - Import `McpServer` from `@modelcontextprotocol/sdk/server/mcp.js` - Create server: `new McpServer({ name: 'sage-x3-mcp', version: '1.0.0' }, { capabilities: { logging: {} } })` - Export function `createServer(config: SageConfig): McpServer` — creates server and registers tools - This file will be imported by the entry point and by tests - Create `src/tools/sage-health.ts` — First tool implementation: - Tool name: `sage_health` - Description (≤50 words): `Check connectivity to Sage X3 REST and SOAP APIs. Returns connection status, latency, and configuration details.` - Input schema: `z.object({})` (no parameters) - Annotations: `{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }` - Handler: calls `restClient.healthCheck()` and `soapClient.healthCheck()` - Returns structured health status with REST status, SOAP status, latency, endpoint info - On error: returns `{ isError: false }` (health check reports errors as data, doesn't fail itself) - Create `src/index.ts` — Entry point: - Load config from env vars - Create REST client and SOAP client - Create MCP server with `createServer()` - Default transport: stdio (`StdioServerTransport`) - Connect: `await server.connect(transport)` - NO `console.log()` anywhere - Register tool using: `server.registerTool('sage_health', { title: 'Sage X3 Health Check', description: '...', inputSchema: z.object({}), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true } }, handler)` - Create `src/tools/__tests__/sage-health.test.ts` — TDD tests with mocked clients - Verify the server starts and responds to `tools/list` via stdio **Must NOT do**: - Do NOT register any other tools yet (just sage_health) - Do NOT add HTTP transport yet (Task 10) - Do NOT use `console.log()` — use `console.error()` only - Do NOT register Resources or Prompts **Recommended Agent Profile**: - **Category**: `unspecified-high` - **Skills**: [] **Parallelization**: - **Can Run In Parallel**: YES - **Parallel Group**: Wave 2 (with Tasks 4, 5) - **Blocks**: Tasks 7, 8, 9, 10 - **Blocked By**: Tasks 1, 2 **References**: - `.sisyphus/research/mcp-server-architecture.md` — MCP SDK server setup patterns, tool registration - MCP SDK import: `import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'` - Transport import: `import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'` - Tool registration: `server.registerTool('name', { title, description, inputSchema: z.object({...}), annotations: {...} }, handler)` - Tool handler return: `{ content: [{ type: 'text', text: JSON.stringify(data) }] }` - Error return: `{ content: [{ type: 'text', text: errorMsg }], isError: true }` - NEVER use `console.log()` in stdio mode — corrupts JSON-RPC channel **Acceptance Criteria**: - [ ] `npx tsc --noEmit` exits 0 - [ ] `npx vitest run src/tools/__tests__/sage-health.test.ts` passes - [ ] `echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | npx tsx src/index.ts` returns JSON-RPC with sage_health tool - [ ] sage_health has `readOnlyHint: true` in tools/list response - [ ] No `console.log` in any file (grep check) **QA Scenarios (MANDATORY)**: ``` Scenario: MCP server lists sage_health tool Tool: Bash Steps: 1. Set env vars: SAGE_X3_URL=http://localhost:8124 SAGE_X3_USER=test SAGE_X3_PASSWORD=test SAGE_X3_ENDPOINT=TEST 2. Run: echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' | npx tsx src/index.ts 2>/dev/null 3. Parse JSON-RPC response for id=2 4. Verify response contains tool named "sage_health" 5. Verify tool has annotations.readOnlyHint === true Expected Result: sage_health tool listed with correct annotations Evidence: .sisyphus/evidence/task-6-tools-list.txt Scenario: No console.log in codebase Tool: Bash (grep) Steps: 1. Search all src/**/*.ts files for 'console.log' 2. Verify zero matches Expected Result: Zero instances of console.log Evidence: .sisyphus/evidence/task-6-no-console-log.txt ``` **Commit**: YES - Message: `feat(server): add MCP server skeleton with sage_health tool` - Files: `src/server.ts`, `src/tools/sage-health.ts`, `src/index.ts`, `src/tools/__tests__/sage-health.test.ts` - Pre-commit: `npx vitest run && npx tsc --noEmit` - [x] 7. REST Tools: sage_query + sage_read + sage_search (TDD) **What to do**: - Create `src/tools/sage-query.ts` — `sage_query` tool: - Description (≤50 words): `Query Sage X3 business objects via REST. Returns paginated records. Use entity names like BPCUSTOMER, SINVOICE, SORDER, PORDER, ITMMASTER, STOCK.` - Input schema: ``` z.object({ entity: z.string().describe("X3 class name, e.g. BPCUSTOMER, SINVOICE, SORDER"), representation: z.string().optional().describe("X3 representation. Defaults to {entity}"), where: z.string().optional().describe("SData filter, e.g. BPCNAM eq 'ACME'"), orderBy: z.string().optional().describe("Sort field, e.g. CREDAT desc"), count: z.number().min(1).max(200).optional().describe("Records per page, max 200. Default 20"), nextUrl: z.string().optional().describe("Pagination cursor from previous response"), select: z.string().optional().describe("Comma-separated fields to return, e.g. BPCNUM,BPCNAM") }) ``` - Annotations: `{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }` - Handler: calls `restClient.query(options)` → `formatQueryResponse(result.records, result.pagination)` - Create `src/tools/sage-read.ts` — `sage_read` tool: - Description (≤50 words): `Read a single Sage X3 record by its primary key. Returns full record details. Example: entity=SINVOICE, key=INV001.` - Input schema: ``` z.object({ entity: z.string().describe("X3 class name, e.g. SINVOICE, BPCUSTOMER"), key: z.string().describe("Primary key value, e.g. INV001, CUST0001"), representation: z.string().optional().describe("X3 representation. Defaults to {entity}") }) ``` - Annotations: `{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }` - Handler: calls `restClient.read(entity, key, representation)` → `formatReadResponse(result.record)` - Create `src/tools/sage-search.ts` — `sage_search` tool: - Description (≤50 words): `Search Sage X3 records with flexible text matching. Builds SData where clauses from a search term across common fields.` - Input schema: ``` z.object({ entity: z.string().describe("X3 class name, e.g. BPCUSTOMER"), searchTerm: z.string().describe("Text to search for across key fields"), searchFields: z.array(z.string()).optional().describe("Fields to search in, e.g. ['BPCNUM','BPCNAM']"), count: z.number().min(1).max(200).optional().describe("Max results, default 20") }) ``` - Annotations: `{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }` - Handler: builds SData `where` clause from searchTerm + searchFields using `contains()` function or `left()` syntax. Calls `restClient.query()` with the constructed filter. If `searchFields` not provided, search entity name + "NUM" and entity name + "NAM" fields (common X3 naming convention). - Create TDD tests for all 3 tools in `src/tools/__tests__/`: - `sage-query.test.ts` — test pagination, filtering, count cap, empty results - `sage-read.test.ts` — test single record read, not found (404), key formatting - `sage-search.test.ts` — test search term → where clause construction, multi-field search, empty results - All tests mock the REST client (no real HTTP calls) **Must NOT do**: - Do NOT add POST/PUT/DELETE capability - Do NOT transform or rename X3 field names — return data as-received - Do NOT add any caching - Do NOT hard-code entity-specific logic (all entities work through same generic code) **Recommended Agent Profile**: - **Category**: `unspecified-high` - Reason: Multi-file tool implementation with TDD requires focused work but not deep algorithmic complexity - **Skills**: [] - **Skills Evaluated but Omitted**: - `playwright`: No browser UI involved - `frontend-ui-ux`: No frontend work **Parallelization**: - **Can Run In Parallel**: YES - **Parallel Group**: Wave 3 (with Tasks 8, 9) - **Blocks**: Task 10 - **Blocked By**: Tasks 4 (REST client), 6 (server skeleton with tool registration pattern) **References**: **Pattern References**: - `src/tools/sage-health.ts` — Follow this exact pattern for tool registration: `server.registerTool('name', { title, description, inputSchema, annotations }, handler)` - `src/utils/response.ts:formatQueryResponse()` — Use for query/search tool response formatting - `src/utils/response.ts:formatReadResponse()` — Use for read tool response formatting - `src/utils/errors.ts:formatToolError()` — Use for ALL error handling in tool handlers **API/Type References**: - `src/clients/rest-client.ts:query()` — REST client query method signature and return type - `src/clients/rest-client.ts:read()` — REST client read method signature and return type - `src/types/sage.ts:RestQueryOptions` — Input type for REST client query - `src/types/sage.ts:RestQueryResult` — Return type with records[] + pagination **Test References**: - `src/tools/__tests__/sage-health.test.ts` — Test structure and client mocking patterns for tool tests **External References**: - `.sisyphus/research/sage-x3-api-landscape.md` — SData query syntax: `where=BPCNAM eq 'ACME'`, `where=left(BPCNAM,4) eq 'Test'`, `where=contains(BPCNAM,'test')`, `orderBy=BPCNAM desc` - Key entities: BPCUSTOMER (customers), SINVOICE/SIH (sales invoices), SORDER/SOH (sales orders), PORDER/POH (purchase orders), ITMMASTER (products), STOCK (inventory) **Acceptance Criteria**: - [ ] `npx vitest run src/tools/__tests__/sage-query.test.ts` — all tests pass - [ ] `npx vitest run src/tools/__tests__/sage-read.test.ts` — all tests pass - [ ] `npx vitest run src/tools/__tests__/sage-search.test.ts` — all tests pass - [ ] `npx tsc --noEmit` exits 0 - [ ] All 3 tools have `readOnlyHint: true` annotation - [ ] sage_query enforces max 200 count - [ ] sage_search builds where clause from search term (not hardcoded entity logic) **QA Scenarios (MANDATORY)**: ``` Scenario: sage_query returns paginated results Tool: Bash Steps: 1. Run `npx vitest run src/tools/__tests__/sage-query.test.ts` 2. Verify test for pagination: response includes { records: [...], pagination: { returned: N, hasMore: true/false, nextUrl: "..." } } 3. Verify test for count > 200: tool caps count at 200 Expected Result: All sage_query tests pass Evidence: .sisyphus/evidence/task-7-sage-query-tests.txt Scenario: sage_read handles not-found gracefully Tool: Bash Steps: 1. Run `npx vitest run src/tools/__tests__/sage-read.test.ts` 2. Verify test for 404: returns isError: true with hint "Record not found..." Expected Result: All sage_read tests pass, including error cases Evidence: .sisyphus/evidence/task-7-sage-read-tests.txt Scenario: sage_search constructs valid where clauses Tool: Bash Steps: 1. Run `npx vitest run src/tools/__tests__/sage-search.test.ts` 2. Verify test: searchTerm="ACME" with fields=["BPCNUM","BPCNAM"] generates where clause with contains() or similar 3. Verify test: no searchFields provided → falls back to entity+NUM and entity+NAM pattern Expected Result: All sage_search tests pass Evidence: .sisyphus/evidence/task-7-sage-search-tests.txt ``` **Commit**: YES - Message: `feat(tools): add REST query tools (sage_query, sage_read, sage_search)` - Files: `src/tools/sage-query.ts`, `src/tools/sage-read.ts`, `src/tools/sage-search.ts`, `src/tools/__tests__/sage-query.test.ts`, `src/tools/__tests__/sage-read.test.ts`, `src/tools/__tests__/sage-search.test.ts` - Pre-commit: `npx vitest run src/tools` - [x] 8. REST Discovery Tools: sage_list_entities + sage_get_context (TDD) **What to do**: - Create `src/tools/sage-list-entities.ts` — `sage_list_entities` tool: - Description (≤50 words): `List available Sage X3 REST entity types (classes) on the configured endpoint. Use this to discover what data you can query.` - Input schema: ``` z.object({}) ``` - Annotations: `{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }` - Handler: calls `restClient.listEntities()` → returns list of available class names on the endpoint - The REST endpoint `${config.url}/api1/x3/erp/${config.endpoint}` should return a list of available classes/representations. Parse and return them. - Create `src/tools/sage-get-context.ts` — `sage_get_context` tool: - Description (≤50 words): `Get field names and metadata for a Sage X3 entity via REST. Returns available fields, their types, and sample structure.` - Input schema: ``` z.object({ entity: z.string().describe("X3 class name, e.g. BPCUSTOMER, SINVOICE"), representation: z.string().optional().describe("X3 representation. Defaults to {entity}") }) ``` - Annotations: `{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }` - Handler: calls `restClient.query({ entity, representation, count: 1 })` to fetch 1 record, then extracts field names/keys from the returned record to show the entity's structure. If REST supports `$prototype` or `$schema` endpoints, prefer those. Otherwise, a 1-record sample is a pragmatic approach. - Returns: `{ entity, fields: string[], sampleRecord: object | null }` - Create TDD tests in `src/tools/__tests__/`: - `sage-list-entities.test.ts` — test entity listing, empty response, error handling - `sage-get-context.test.ts` — test field extraction from sample record, empty entity, error handling - All tests mock the REST client **Must NOT do**: - Do NOT hardcode entity lists — always query from X3 - Do NOT transform field names or add labels (that's the AI's job with sage_describe_entity via SOAP) - Do NOT cache entity lists **Recommended Agent Profile**: - **Category**: `unspecified-high` - Reason: Discovery tools require careful REST response parsing and schema extraction - **Skills**: [] - **Skills Evaluated but Omitted**: - `playwright`: No browser work **Parallelization**: - **Can Run In Parallel**: YES - **Parallel Group**: Wave 3 (with Tasks 7, 9) - **Blocks**: Task 10 - **Blocked By**: Tasks 4 (REST client), 6 (server skeleton) **References**: **Pattern References**: - `src/tools/sage-health.ts` — Tool registration and handler pattern - `src/tools/sage-query.ts` (from Task 7) — REST tool pattern (same wave, but can reference the design) **API/Type References**: - `src/clients/rest-client.ts:listEntities()` — Method that queries the endpoint root for available classes - `src/clients/rest-client.ts:query()` — Used by sage_get_context to fetch a 1-record sample **Test References**: - `src/tools/__tests__/sage-health.test.ts` — Test mocking patterns **External References**: - `.sisyphus/research/sage-x3-api-landscape.md` — REST endpoint root returns list of classes/representations - SData convention: `GET /api1/x3/erp/ENDPOINT` lists available resources **Acceptance Criteria**: - [ ] `npx vitest run src/tools/__tests__/sage-list-entities.test.ts` — all tests pass - [ ] `npx vitest run src/tools/__tests__/sage-get-context.test.ts` — all tests pass - [ ] `npx tsc --noEmit` exits 0 - [ ] Both tools have `readOnlyHint: true` annotation - [ ] sage_list_entities requires no input parameters - [ ] sage_get_context returns field names from real X3 data (not hardcoded) **QA Scenarios (MANDATORY)**: ``` Scenario: sage_list_entities returns available classes Tool: Bash Steps: 1. Run `npx vitest run src/tools/__tests__/sage-list-entities.test.ts` 2. Verify test: tool returns array of entity names (strings) 3. Verify test: empty endpoint returns empty array (not error) Expected Result: All sage_list_entities tests pass Evidence: .sisyphus/evidence/task-8-list-entities-tests.txt Scenario: sage_get_context extracts fields from sample Tool: Bash Steps: 1. Run `npx vitest run src/tools/__tests__/sage-get-context.test.ts` 2. Verify test: given a sample BPCUSTOMER record with fields {BPCNUM, BPCNAM, CRY}, returns fields: ["BPCNUM","BPCNAM","CRY"] 3. Verify test: entity with no records returns fields: [] and sampleRecord: null Expected Result: All sage_get_context tests pass Evidence: .sisyphus/evidence/task-8-get-context-tests.txt Scenario: Error handling on REST failure Tool: Bash Steps: 1. Run vitest for both tool tests 2. Verify tests include error scenarios: REST client throws → tool returns isError: true with AI hint Expected Result: Error scenarios tested and passing Evidence: .sisyphus/evidence/task-8-error-handling-tests.txt ``` **Commit**: YES - Message: `feat(tools): add REST discovery tools (sage_list_entities, sage_get_context)` - Files: `src/tools/sage-list-entities.ts`, `src/tools/sage-get-context.ts`, `src/tools/__tests__/sage-list-entities.test.ts`, `src/tools/__tests__/sage-get-context.test.ts` - Pre-commit: `npx vitest run src/tools` - [x] 9. SOAP Tools: sage_soap_read + sage_soap_query + sage_describe_entity (TDD) **What to do**: - Create `src/tools/sage-soap-read.ts` — `sage_soap_read` tool: - Description (≤50 words): `Read a single Sage X3 record via SOAP by its key fields. Use for objects not available via REST, or when you need SOAP-specific data.` - Input schema: ``` z.object({ publicName: z.string().describe("SOAP publication name, e.g. SIH, SOH, WSBPC, WITM"), key: z.record(z.string(), z.string()).describe("Key field(s) as object, e.g. {NUM: 'INV001'}") }) ``` - Annotations: `{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }` - Handler: calls `soapClient.read({ publicName, key })` → formats result with `formatReadResponse()`. Checks `result.status === 1` for success, returns error with messages if status === 0. - Create `src/tools/sage-soap-query.ts` — `sage_soap_query` tool: - Description (≤50 words): `Query Sage X3 records via SOAP. Returns a list of records matching criteria. Use for bulk data retrieval via SOAP pools.` - Input schema: ``` z.object({ publicName: z.string().describe("SOAP publication name, e.g. SIH, SOH, WSBPC"), listSize: z.number().min(1).max(200).optional().describe("Max records to return, default 20, max 200"), inputXml: z.string().optional().describe("Optional XML filter criteria for the query") }) ``` - Annotations: `{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }` - Handler: calls `soapClient.query({ publicName, listSize, inputXml })` → formats result. Enforces listSize cap at 200. - Create `src/tools/sage-describe-entity.ts` — `sage_describe_entity` tool: - Description (≤50 words): `Get field definitions for a Sage X3 SOAP object. Returns field names, types, lengths, and English labels. Essential for understanding X3 field codes.` - Input schema: ``` z.object({ publicName: z.string().describe("SOAP publication name, e.g. SIH, SOH, WSBPC, WITM") }) ``` - Annotations: `{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }` - Handler: calls `soapClient.getDescription(publicName)` → parses response to extract field definitions. Returns array of `{ name, type, length, label }` where label comes from the C_ENG attribute (English field label). This is the key tool for translating cryptic X3 field codes (BPCNUM, SIVTYP) into human-readable labels. - Create TDD tests in `src/tools/__tests__/`: - `sage-soap-read.test.ts` — test successful read (status=1), business error (status=0 with messages), key formatting - `sage-soap-query.test.ts` — test query results, listSize cap, empty results - `sage-describe-entity.test.ts` — test field definition parsing: NAM→name, TYP→type, C_ENG→label - All tests mock the SOAP client **Must NOT do**: - Do NOT expose save, delete, modify, run, actionObject, insertLines, or deleteLines SOAP operations - Do NOT transform X3 data (return as-received from SOAP client) - Do NOT hardcode entity-specific parsing logic - Do NOT cache SOAP responses or field definitions **Recommended Agent Profile**: - **Category**: `unspecified-high` - Reason: SOAP tool implementation with XML/JSON response handling requires careful work - **Skills**: [] - **Skills Evaluated but Omitted**: - `playwright`: No browser work **Parallelization**: - **Can Run In Parallel**: YES - **Parallel Group**: Wave 3 (with Tasks 7, 8) - **Blocks**: Task 10 - **Blocked By**: Tasks 5 (SOAP client), 6 (server skeleton) **References**: **Pattern References**: - `src/tools/sage-health.ts` — Tool registration and handler pattern - `src/tools/sage-query.ts` (Task 7) — Similar tool structure for reference **API/Type References**: - `src/clients/soap-client.ts:read()` — SOAP read method signature (takes SoapReadOptions) - `src/clients/soap-client.ts:query()` — SOAP query method signature (takes SoapQueryOptions) - `src/clients/soap-client.ts:getDescription()` — SOAP getDescription method (takes publicName string) - `src/types/sage.ts:SoapResult` — Return type: `{ status, data, messages, technicalInfos }` - `src/types/sage.ts:SoapReadOptions` — `{ objectName?, publicName, key: Record }` - `src/types/sage.ts:SoapQueryOptions` — `{ objectName?, publicName, listSize?, inputXml? }` **Test References**: - `src/tools/__tests__/sage-health.test.ts` — Test mocking patterns - `src/clients/__tests__/soap-client.test.ts` — SOAP response mocking structure **External References**: - `.sisyphus/research/sage-x3-api-landscape.md` — SOAP response format: `1......` - getDescription response contains: `` — parse NAM, TYP, LEN, C_ENG attributes - SOAP public names: SIH (Sales Invoice), SOH (Sales Order), WSBPC (Customer), WSBPS (Supplier), WITM (Product) **Acceptance Criteria**: - [ ] `npx vitest run src/tools/__tests__/sage-soap-read.test.ts` — all tests pass - [ ] `npx vitest run src/tools/__tests__/sage-soap-query.test.ts` — all tests pass - [ ] `npx vitest run src/tools/__tests__/sage-describe-entity.test.ts` — all tests pass - [ ] `npx tsc --noEmit` exits 0 - [ ] All 3 tools have `readOnlyHint: true` annotation - [ ] sage_soap_query enforces listSize max 200 - [ ] sage_describe_entity extracts C_ENG (English label) from field definitions - [ ] No SOAP write operations accessible through any tool **QA Scenarios (MANDATORY)**: ``` Scenario: sage_soap_read handles business errors Tool: Bash Steps: 1. Run `npx vitest run src/tools/__tests__/sage-soap-read.test.ts` 2. Verify test: status=1 (success) → returns record data 3. Verify test: status=0 (error) → returns isError: true with messages from SOAP response Expected Result: All sage_soap_read tests pass including error handling Evidence: .sisyphus/evidence/task-9-soap-read-tests.txt Scenario: sage_soap_query caps listSize Tool: Bash Steps: 1. Run `npx vitest run src/tools/__tests__/sage-soap-query.test.ts` 2. Verify test: listSize > 200 is capped to 200 3. Verify test: default listSize is 20 Expected Result: All sage_soap_query tests pass Evidence: .sisyphus/evidence/task-9-soap-query-tests.txt Scenario: sage_describe_entity parses field definitions Tool: Bash Steps: 1. Run `npx vitest run src/tools/__tests__/sage-describe-entity.test.ts` 2. Verify test: given getDescription response with , tool returns { name: "NUM", type: "Char", length: "20", label: "Invoice no." } 3. Verify test: empty description returns empty fields array Expected Result: All sage_describe_entity tests pass Evidence: .sisyphus/evidence/task-9-describe-entity-tests.txt ``` **Commit**: YES - Message: `feat(tools): add SOAP tools (sage_soap_read, sage_soap_query, sage_describe_entity)` - Files: `src/tools/sage-soap-read.ts`, `src/tools/sage-soap-query.ts`, `src/tools/sage-describe-entity.ts`, `src/tools/__tests__/sage-soap-read.test.ts`, `src/tools/__tests__/sage-soap-query.test.ts`, `src/tools/__tests__/sage-describe-entity.test.ts` - Pre-commit: `npx vitest run src/tools` - [x] 10. Complete Tool Registration + HTTP Transport + Dual Entry Point **What to do**: - Update `src/server.ts` — register ALL 9 tools: - Import all 8 tool modules (sage_health is already registered from Task 6) - Register: sage_query, sage_read, sage_search, sage_list_entities, sage_get_context, sage_soap_read, sage_soap_query, sage_describe_entity - Each tool must receive the REST client and/or SOAP client via closure or dependency injection - Verify all 9 tools appear in `tools/list` response - Update `src/index.ts` — add HTTP transport support: - Check `MCP_TRANSPORT` env var (default: "stdio") - If `MCP_TRANSPORT=stdio`: use `StdioServerTransport` (already implemented in Task 6) - If `MCP_TRANSPORT=http`: use `StreamableHTTPServerTransport` from `@modelcontextprotocol/sdk/server/streamableHttp.js` - Create HTTP server on `MCP_HTTP_PORT` (default 3000) - Route: `POST /mcp` for JSON-RPC messages - Route: `GET /mcp` for SSE stream (if client requests) - Route: `DELETE /mcp` for session termination - Handle session initialization and session IDs - Add graceful shutdown (SIGINT/SIGTERM → close server + transport) - Invalid `MCP_TRANSPORT` value → `console.error("FATAL: Invalid MCP_TRANSPORT: must be 'stdio' or 'http'")` → `process.exit(1)` - Log startup info to stderr: `console.error(\`Sage X3 MCP server started (${transport} transport)\`)` - Create `src/tools/index.ts` — barrel export for all tool registration functions - Ensure `npm run build` produces working `dist/index.js` - Add npm script: `"start:http": "MCP_TRANSPORT=http node dist/index.js"` **Must NOT do**: - Do NOT add Express or any HTTP framework — use SDK's built-in `StreamableHTTPServerTransport` with Node's `http.createServer` - Do NOT add authentication/CORS for the MCP HTTP endpoint (this is for local/internal use) - Do NOT register Resources or Prompts - Do NOT use `console.log()` anywhere **Recommended Agent Profile**: - **Category**: `unspecified-high` - Reason: Integration of all tool modules + HTTP transport wiring requires careful coordination - **Skills**: [] - **Skills Evaluated but Omitted**: - `playwright`: No browser work - `frontend-ui-ux`: No UI work **Parallelization**: - **Can Run In Parallel**: NO - **Parallel Group**: Wave 4 (sequential after Wave 3) - **Blocks**: Task 11 - **Blocked By**: Tasks 6, 7, 8, 9 (all tools must exist before full registration) **References**: **Pattern References**: - `src/server.ts` (from Task 6) — Existing server setup with sage_health registration. Extend this pattern for all 9 tools. - `src/index.ts` (from Task 6) — Existing stdio entry point. Add HTTP transport branch. **API/Type References**: - `src/tools/sage-query.ts`, `src/tools/sage-read.ts`, etc. — Each tool module exports a registration function - `src/config/index.ts` — Config includes `MCP_TRANSPORT` and `MCP_HTTP_PORT` **External References**: - `.sisyphus/research/mcp-server-architecture.md` — MCP SDK transport patterns - MCP SDK HTTP transport: `import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'` - SDK example for HTTP: create `http.createServer()`, route POST/GET/DELETE `/mcp` to transport - Session management: `StreamableHTTPServerTransport` handles session IDs automatically **Acceptance Criteria**: - [ ] `npx tsc --noEmit` exits 0 - [ ] `npm run build` produces `dist/index.js` without errors - [ ] `echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' | SAGE_X3_URL=http://localhost SAGE_X3_USER=x SAGE_X3_PASSWORD=x SAGE_X3_ENDPOINT=X3 node dist/index.js 2>/dev/null` → returns exactly 9 tools - [ ] All 9 tools have `readOnlyHint: true` in annotations - [ ] Invalid `MCP_TRANSPORT=invalid` → stderr error + exit 1 - [ ] No `console.log` in any source file **QA Scenarios (MANDATORY)**: ``` Scenario: All 9 tools registered via stdio Tool: Bash Steps: 1. Build: npm run build 2. Set env vars: SAGE_X3_URL=http://localhost:8124 SAGE_X3_USER=test SAGE_X3_PASSWORD=test SAGE_X3_ENDPOINT=TEST 3. Send initialize + tools/list via stdin pipe to dist/index.js 4. Parse JSON-RPC response for tools/list (id=2) 5. Count tools — must be exactly 9 6. Verify tool names: sage_health, sage_query, sage_read, sage_search, sage_list_entities, sage_get_context, sage_soap_read, sage_soap_query, sage_describe_entity 7. Verify each tool has annotations.readOnlyHint === true Expected Result: Exactly 9 tools listed, all with readOnlyHint: true Evidence: .sisyphus/evidence/task-10-tools-list-9.txt Scenario: HTTP transport starts and accepts requests Tool: Bash Steps: 1. Start server in background: MCP_TRANSPORT=http MCP_HTTP_PORT=3456 SAGE_X3_URL=http://localhost SAGE_X3_USER=x SAGE_X3_PASSWORD=x SAGE_X3_ENDPOINT=X3 node dist/index.js & 2. Wait 2 seconds for startup 3. Send POST to http://localhost:3456/mcp with JSON-RPC initialize request: curl -s -X POST http://localhost:3456/mcp -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' 4. Verify response contains "serverInfo" and "capabilities" 5. Kill background server Expected Result: HTTP transport responds to initialize request Failure Indicators: curl returns connection refused, empty response, or non-JSON Evidence: .sisyphus/evidence/task-10-http-transport.txt Scenario: Invalid MCP_TRANSPORT exits with error Tool: Bash Steps: 1. Run: MCP_TRANSPORT=invalid SAGE_X3_URL=http://localhost SAGE_X3_USER=x SAGE_X3_PASSWORD=x SAGE_X3_ENDPOINT=X3 node dist/index.js 2>&1 2. Check stderr contains "FATAL" and "Invalid MCP_TRANSPORT" 3. Check exit code is 1 Expected Result: Process exits with code 1 and clear error message Evidence: .sisyphus/evidence/task-10-invalid-transport.txt ``` **Commit**: YES - Message: `feat(transport): register all 9 tools and add HTTP transport` - Files: `src/server.ts`, `src/index.ts`, `src/tools/index.ts`, `package.json` (script update) - Pre-commit: `npx vitest run && npx tsc --noEmit` - [x] 11. Integration Test Suite + .env.example + Final Polish **What to do**: - Create `src/__tests__/integration.test.ts` — end-to-end integration tests: - Import `createServer` from `src/server.ts` - Create server with mock config - Mock REST and SOAP clients at the client level (not at HTTP/SOAP transport level) - Test: `tools/list` returns exactly 9 tools with correct names - Test: `tools/list` — all tools have `readOnlyHint: true` annotation - Test: `tools/call` sage_health → returns health status - Test: `tools/call` sage_query with valid params → returns paginated results - Test: `tools/call` sage_read with valid params → returns single record - Test: `tools/call` sage_search → returns search results - Test: `tools/call` sage_list_entities → returns entity list - Test: `tools/call` sage_get_context → returns field names - Test: `tools/call` sage_soap_read → returns SOAP record - Test: `tools/call` sage_soap_query → returns SOAP results - Test: `tools/call` sage_describe_entity → returns field definitions with labels - Test: `tools/call` with invalid tool name → returns error - Test: `tools/call` sage_query with missing required param → returns validation error - Test: Error propagation — REST client throws → tool returns isError with hint - Use MCP SDK's in-memory client/server transport for programmatic testing (if available), or test via direct tool handler invocation - Create `.env.example` — documented template: ``` # Sage X3 Connection (REQUIRED) SAGE_X3_URL=http://your-x3-server:8124 SAGE_X3_USER=your_webservice_user SAGE_X3_PASSWORD=your_password SAGE_X3_ENDPOINT=X3V12 # Sage X3 SOAP Settings (OPTIONAL) SAGE_X3_POOL_ALIAS=SEED SAGE_X3_LANGUAGE=ENG # MCP Transport (OPTIONAL) MCP_TRANSPORT=stdio MCP_HTTP_PORT=3000 # TLS (OPTIONAL — set to false for self-signed certificates) SAGE_X3_REJECT_UNAUTHORIZED=true ``` - Create/update `README.md` with: - Project description (1 paragraph) - Quick start: install, configure .env, run - Tool list (all 9 with 1-line descriptions) - Configuration reference (all env vars with defaults) - Usage examples with Claude Desktop / Opencode config snippets - Development: build, test, dev commands - Final polish: - Verify `package.json` has correct `"main"`, `"types"`, `"files"` fields for distribution - Ensure `npm run build && npm run start` works end-to-end - Run full test suite: `npx vitest run` — all tests pass - Run type check: `npx tsc --noEmit` — zero errors **Must NOT do**: - Do NOT add Docker files or CI/CD pipelines - Do NOT add real E2E tests against a live X3 instance (all integration tests use mocks) - Do NOT add monitoring, metrics, or observability tooling - Do NOT commit any real credentials or .env file **Recommended Agent Profile**: - **Category**: `deep` - Reason: Integration testing requires understanding the full tool-client-server chain and verifying all 9 tools work through the MCP protocol layer - **Skills**: [] - **Skills Evaluated but Omitted**: - `playwright`: No browser work - `git-master`: Git operations not the focus **Parallelization**: - **Can Run In Parallel**: NO - **Parallel Group**: Wave 4 (after Task 10) - **Blocks**: Final Verification (F1-F4) - **Blocked By**: Task 10 (all tools must be registered and server must be fully wired) **References**: **Pattern References**: - `src/server.ts` — createServer function that registers all tools - `src/tools/__tests__/sage-health.test.ts` — Client mocking patterns - All tool test files — Individual tool handler test patterns **API/Type References**: - `src/types/sage.ts` — All interfaces needed for mock data - `src/config/index.ts` — SageConfig type for mock config creation **Test References**: - `src/clients/__tests__/rest-client.test.ts` — REST client mocking - `src/clients/__tests__/soap-client.test.ts` — SOAP client mocking **External References**: - `.sisyphus/research/mcp-server-architecture.md` — MCP testing with `@modelcontextprotocol/inspector` - MCP SDK in-memory transport: check if SDK provides `InMemoryTransport` or `createClientServerPair()` for programmatic testing - If no in-memory transport: test by importing tool handler functions directly and calling with mock clients **Acceptance Criteria**: - [ ] `npx vitest run` — ALL tests pass (unit + integration), 0 failures - [ ] `npx tsc --noEmit` exits 0 - [ ] `.env.example` exists with all documented env vars - [ ] `README.md` exists with quickstart, tool list, config reference - [ ] Integration tests verify all 9 tools callable through server - [ ] Integration tests verify error handling propagation - [ ] Integration tests verify input validation (missing required params) - [ ] `npm run build` produces clean dist/ **QA Scenarios (MANDATORY)**: ``` Scenario: Full test suite passes Tool: Bash Steps: 1. Run `npx vitest run --reporter=verbose` 2. Verify all test files pass: config, utils, rest-client, soap-client, all 9 tool tests, integration 3. Count total test cases — should be at least 40+ Expected Result: All tests pass, zero failures Evidence: .sisyphus/evidence/task-11-full-test-suite.txt Scenario: Built server lists all 9 tools Tool: Bash Steps: 1. Run `npm run build` 2. Set env vars: SAGE_X3_URL=http://localhost:8124 SAGE_X3_USER=test SAGE_X3_PASSWORD=test SAGE_X3_ENDPOINT=TEST 3. Pipe initialize + tools/list JSON-RPC to `node dist/index.js` 4. Parse tools/list response 5. Verify exactly these 9 tool names: sage_health, sage_query, sage_read, sage_search, sage_list_entities, sage_get_context, sage_soap_read, sage_soap_query, sage_describe_entity 6. Verify each has readOnlyHint: true Expected Result: 9 tools, all read-only annotated Evidence: .sisyphus/evidence/task-11-built-server-tools.txt Scenario: .env.example is complete Tool: Bash Steps: 1. Read .env.example 2. Verify contains: SAGE_X3_URL, SAGE_X3_USER, SAGE_X3_PASSWORD, SAGE_X3_ENDPOINT, SAGE_X3_POOL_ALIAS, SAGE_X3_LANGUAGE, MCP_TRANSPORT, MCP_HTTP_PORT, SAGE_X3_REJECT_UNAUTHORIZED 3. Verify each has a comment explaining its purpose 4. Verify no real credentials are present Expected Result: All 9 env vars documented with comments Evidence: .sisyphus/evidence/task-11-env-example.txt Scenario: README has required sections Tool: Bash Steps: 1. Read README.md 2. Verify contains: project description, quickstart, tool list (9 tools), configuration reference, usage examples 3. Verify tool list matches exactly the 9 tool names Expected Result: README complete with all required sections Evidence: .sisyphus/evidence/task-11-readme.txt ``` **Commit**: YES - Message: `test(integration): add integration test suite, .env.example, and README` - Files: `src/__tests__/integration.test.ts`, `.env.example`, `README.md`, `package.json` - Pre-commit: `npx vitest run && npx tsc --noEmit` --- ## Final Verification Wave (MANDATORY — after ALL implementation tasks) > 4 review agents run in PARALLEL. ALL must APPROVE. Rejection → fix → re-run. - [ ] F1. **Plan Compliance Audit** — `oracle` Read the plan end-to-end. For each "Must Have": verify implementation exists (read file, pipe JSON-RPC, run command). For each "Must NOT Have": search codebase for forbidden patterns (POST/PUT/DELETE in REST client, save/delete in SOAP client, console.log anywhere, Resources/Prompts registration) — reject with file:line if found. Check evidence files exist in .sisyphus/evidence/. Compare deliverables against plan. Output: `Must Have [N/N] | Must NOT Have [N/N] | Tasks [N/N] | VERDICT: APPROVE/REJECT` - [ ] F2. **Code Quality Review** — `unspecified-high` Run `tsc --noEmit` + `vitest run`. Review all source files for: `as any`/`@ts-ignore`, empty catches, `console.log` (FORBIDDEN), commented-out code, unused imports. Check AI slop: excessive comments, over-abstraction, generic names (data/result/item/temp). Verify all tools have `readOnlyHint: true` annotation. Verify REST client has NO POST/PUT/DELETE methods. Verify SOAP client has NO save/delete/modify/run methods. Output: `Build [PASS/FAIL] | Tests [N pass/N fail] | Files [N clean/N issues] | VERDICT` - [ ] F3. **Real Manual QA** — `unspecified-high` Start the MCP server with `echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' | node dist/index.js` and verify initialization succeeds. Then test `tools/list` returns exactly 9 tools. Then test each tool with mock-compatible parameters. Save evidence to `.sisyphus/evidence/final-qa/`. Test with `MCP_TRANSPORT=http` as well. Output: `Scenarios [N/N pass] | Tools [9/9 registered] | Transports [2/2] | VERDICT` - [ ] F4. **Scope Fidelity Check** — `deep` For each task: read "What to do", read actual diff (git log/diff). Verify 1:1 — everything in spec was built (no missing), nothing beyond spec was built (no creep). Check "Must NOT do" compliance: no write operations, no Resources/Prompts, no data transformation, no caching, no `console.log`. Flag unaccounted changes. Verify exactly 9 tools (not more, not less). Output: `Tasks [N/N compliant] | Tool Count [9/9] | Guardrails [N/N clean] | VERDICT` --- ## Commit Strategy - **Wave 1**: `feat(scaffold): initialize project with TypeScript, vitest, MCP SDK` — package.json, tsconfig.json, vitest.config.ts, src/ structure - **Wave 1**: `feat(core): add shared types, config validation, and error utilities` — src/types/, src/config/, src/utils/ - **Wave 1**: `spike(soap): validate SOAP library against X3 WSDL` — spike/ directory - **Wave 2**: `feat(rest): add SData 2.0 REST client with pagination and auth` — src/clients/rest-client.ts + tests - **Wave 2**: `feat(soap): add SOAP client with read/query/getDescription` — src/clients/soap-client.ts + tests - **Wave 2**: `feat(server): add MCP server skeleton with sage_health tool` — src/index.ts, src/tools/sage-health.ts + tests - **Wave 3**: `feat(tools): add REST query tools (sage_query, sage_read, sage_search)` — src/tools/ + tests - **Wave 3**: `feat(tools): add REST discovery tools (sage_list_entities, sage_get_context)` — src/tools/ + tests - **Wave 3**: `feat(tools): add SOAP tools (sage_soap_read, sage_soap_query, sage_describe_entity)` — src/tools/ + tests - **Wave 4**: `feat(transport): add HTTP transport and unified entry point` — src/index.ts updates - **Wave 4**: `test(integration): add integration test suite and .env.example` — tests + docs --- ## Success Criteria ### Verification Commands ```bash npx tsc --noEmit # Expected: exit 0, zero errors npx vitest run # Expected: all tests pass echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | node dist/index.js # Expected: 9 tools listed # Each tool has readOnlyHint: true annotation # REST client only has GET methods (no POST/PUT/DELETE) # SOAP client only has read/query/getDescription (no save/delete/modify/run) # Missing env vars → stderr error + exit 1 ``` ### Final Checklist - [ ] All "Must Have" present (9 tools, 2 clients, 2 transports, TDD suite, env config) - [ ] All "Must NOT Have" absent (no writes, no console.log, no Resources/Prompts, no data transforms) - [ ] All tests pass - [ ] TypeScript compiles cleanly - [ ] Both transports work (stdio + HTTP) - [ ] Tool annotations present (readOnlyHint: true on all 9 tools)