Files
sage-mcp-server/.sisyphus/plans/sage-mcp-server.md

1402 lines
75 KiB
Markdown

# 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 `<status>` 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<string, string>)
- `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<RestQueryResult>` — 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<RestDetailResult>` — single record
- URL: `${config.url}/api1/x3/erp/${config.endpoint}/${entity}('${key}')?representation=${representation || entity}.$details`
- `async listEntities(): Promise<string[]>` — 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<unknown>` — 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<SoapResult>` — 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<SoapResult>` — list records
- Calls SOAP `query` operation with listSize and optional inputXml
- `async getDescription(publicName: string): Promise<SoapResult>` — 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 `<status>0</status>` = business error → parse `<messages>` 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: `<callContext><codeLang>ENG</codeLang><poolAlias>POOL</poolAlias><poolId></poolId><requestConfig>adxwss.optreturn=JSON</requestConfig></callContext>`
- SOAP response: `<CAdxResultXml><status>1</status><resultXml>...</resultXml><messages>...</messages><technicalInfos>...</technicalInfos></CAdxResultXml>`
- SIH getDescription response (example in research file): returns `<FLD NAM="NUM" TYP="Char" C_ENG="Invoice no."/>` — 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<string, string> }`
- `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: `<CAdxResultXml><status>1</status><resultXml>...</resultXml><messages>...</messages></CAdxResultXml>`
- getDescription response contains: `<FLD NAM="NUM" TYP="Char" LEN="20" C_ENG="Invoice no."/>` — 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 <FLD NAM="NUM" TYP="Char" LEN="20" C_ENG="Invoice no."/>, 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)