75 KiB
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=JSONin 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.mdand.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:
soapnpm package may struggle with X3's RPC/encoded WSDL — must validate before building SOAP client adxwss.optreturn=JSONreliability unknown — needfast-xml-parseras fallback- Tool annotations required:
readOnlyHint: trueon 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_UNAUTHORIZEDenv var - Logging:
console.error()only — NEVERconsole.log()(corrupts stdio JSON-RPC) - SOAP CAdxCallContext:
codeUser/passwordfields 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 validationsrc/clients/rest-client.ts— SData 2.0 REST clientsrc/clients/soap-client.ts— SOAP client with pool supportsrc/tools/*.ts— 9 tool implementationssrc/types/— Shared TypeScript interfacessrc/utils/— Error handling, logging, response formattingvitest.config.ts+src/**/__tests__/*.test.ts— Full test suite.env.example— Documented environment variable templatepackage.json,tsconfig.json— Project configuration
Definition of Done
npx tsc --noEmit→ exit 0 (zero TypeScript errors)npx vitest run→ all tests pass, 0 failuresecho '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | node dist/index.js→ lists 9 tools- Each tool has
readOnlyHint: trueannotation 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(): Useconsole.error()for all logging —console.logcorrupts 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
-
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.jsonwith strict mode, ES2022 target, NodeNext module resolution,outDir: "dist" - Create
vitest.config.tswith 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.tswith 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/sdknpm 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 installcompletes without errorsnpx tsc --noEmiton empty src/index.ts exits 0npx vitest runexits 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.txtCommit: 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
- Initialize npm project:
-
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
SageConfigobject
- Required:
- 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): CallToolResultformatReadResponse(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 — useconsole.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 --noEmitexits 0 — all types compilenpx vitest run src/config— config validation tests passnpx vitest run src/utils— error/response utility tests pass- Config throws + exits on missing
SAGE_X3_URL formatToolErrorreturns{ 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.txtCommit: 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
- Create
-
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:
- Can the
soapnpm package parse X3's WSDL at/soap-wsdl/syracuse/collaboration/syracuse/CAdxWebServiceXmlCC?wsdl? - Can it construct a valid
getDescriptioncall withCAdxCallContext? - Does
adxwss.optreturn=JSONin requestConfig make SOAP return JSON instead of XML? - What format does
resultXmlcome back in — parsed object or raw string? - Does Basic Auth work at the HTTP level (not in CAdxCallContext fields)?
- Can the
- 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
soaplibrary FAILS: document the failure and recommend the fallback approach (raw HTTP POST with XML templates +fast-xml-parserfor response parsing) - If
soaplibrary 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.tsexists and is executablespike/soap-spike-results.mddocuments all 5 validation questions with answers- Clear recommendation: use
soaplibrary 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.txtCommit: 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
- Create
-
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.nextUrlis provided, use it directly (cursor-based pagination) - Hard cap:
countcannot 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 } }
- Builds 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
- URL:
async listEntities(): Promise<string[]>— list available endpoints- URL:
${config.url}/api1/x3/erp/${config.endpoint}or list representations
- URL:
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/jsonheader- 15-second timeout via
AbortSignal.timeout(15000) - SSL/TLS: if
config.rejectUnauthorized === false, setNODE_TLS_REJECT_UNAUTHORIZED=0 - Error handling: detect non-JSON responses (HTML login page redirect), classify errors
- Constructor takes
- 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.$urlfor 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.txtCommit: 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
- Create
-
5. SOAP Client + Tests
What to do:
- Read
spike/soap-spike-results.mdFIRST to understand which approach to use - If
soaplibrary works (spike found it compatible with X3 WSDL):- Create
src/clients/soap-client.tsusing thesoapnpm 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
- Create
- If
soaplibrary fails (spike found issues):- Create
src/clients/soap-client.tsusing raw HTTP POST + XML templates - Use
fast-xml-parserto parse XML responses - Build SOAP envelopes from templates (see references for exact XML structure)
- Create
- Regardless of approach, implement these methods:
async read(options: SoapReadOptions): Promise<SoapResult>— read single record- Calls SOAP
readoperation with objectXml containing key fields - Sets requestConfig:
adxwss.optreturn=JSON&adxwss.beautify=false
- Calls SOAP
async query(options: SoapQueryOptions): Promise<SoapResult>— list records- Calls SOAP
queryoperation with listSize and optional inputXml
- Calls SOAP
async getDescription(publicName: string): Promise<SoapResult>— get field definitions- Calls SOAP
getDescriptionoperation - Returns field names, types, lengths, labels (C_ENG field in response)
- Calls SOAP
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
- HTTP 200 with
- 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.txtCommit: 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
- Read
-
6. MCP Server Skeleton + sage_health Tool + Tests
What to do:
- Create
src/server.ts— MCP server setup:- Import
McpServerfrom@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
- Import
- 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()andsoapClient.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)
- Tool name:
- 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/listvia 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()— useconsole.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 --noEmitexits 0npx vitest run src/tools/__tests__/sage-health.test.tspassesecho '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | npx tsx src/index.tsreturns JSON-RPC with sage_health tool- sage_health has
readOnlyHint: truein tools/list response - No
console.login 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.txtCommit: 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
- Create
-
7. REST Tools: sage_query + sage_read + sage_search (TDD)
What to do:
- Create
src/tools/sage-query.ts—sage_querytool:- 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)
- Description (≤50 words):
- Create
src/tools/sage-read.ts—sage_readtool:- 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)
- Description (≤50 words):
- Create
src/tools/sage-search.ts—sage_searchtool:- 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
whereclause from searchTerm + searchFields usingcontains()function orleft()syntax. CallsrestClient.query()with the constructed filter. IfsearchFieldsnot provided, search entity name + "NUM" and entity name + "NAM" fields (common X3 naming convention).
- Description (≤50 words):
- Create TDD tests for all 3 tools in
src/tools/__tests__/:sage-query.test.ts— test pagination, filtering, count cap, empty resultssage-read.test.ts— test single record read, not found (404), key formattingsage-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 involvedfrontend-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 formattingsrc/utils/response.ts:formatReadResponse()— Use for read tool response formattingsrc/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 typesrc/clients/rest-client.ts:read()— REST client read method signature and return typesrc/types/sage.ts:RestQueryOptions— Input type for REST client querysrc/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 passnpx vitest run src/tools/__tests__/sage-read.test.ts— all tests passnpx vitest run src/tools/__tests__/sage-search.test.ts— all tests passnpx tsc --noEmitexits 0- All 3 tools have
readOnlyHint: trueannotation - 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.txtCommit: 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
- Create
-
8. REST Discovery Tools: sage_list_entities + sage_get_context (TDD)
What to do:
- Create
src/tools/sage-list-entities.ts—sage_list_entitiestool:- 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.
- Description (≤50 words):
- Create
src/tools/sage-get-context.ts—sage_get_contexttool:- 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$prototypeor$schemaendpoints, prefer those. Otherwise, a 1-record sample is a pragmatic approach. - Returns:
{ entity, fields: string[], sampleRecord: object | null }
- Description (≤50 words):
- Create TDD tests in
src/tools/__tests__/:sage-list-entities.test.ts— test entity listing, empty response, error handlingsage-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 patternsrc/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 classessrc/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/ENDPOINTlists available resources
Acceptance Criteria:
npx vitest run src/tools/__tests__/sage-list-entities.test.ts— all tests passnpx vitest run src/tools/__tests__/sage-get-context.test.ts— all tests passnpx tsc --noEmitexits 0- Both tools have
readOnlyHint: trueannotation - 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.txtCommit: 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
- Create
-
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_readtool:- 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 withformatReadResponse(). Checksresult.status === 1for success, returns error with messages if status === 0.
- Description (≤50 words):
- Create
src/tools/sage-soap-query.ts—sage_soap_querytool:- 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.
- Description (≤50 words):
- Create
src/tools/sage-describe-entity.ts—sage_describe_entitytool:- 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.
- Description (≤50 words):
- Create TDD tests in
src/tools/__tests__/:sage-soap-read.test.ts— test successful read (status=1), business error (status=0 with messages), key formattingsage-soap-query.test.ts— test query results, listSize cap, empty resultssage-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 patternsrc/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 patternssrc/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 passnpx vitest run src/tools/__tests__/sage-soap-query.test.ts— all tests passnpx vitest run src/tools/__tests__/sage-describe-entity.test.ts— all tests passnpx tsc --noEmitexits 0- All 3 tools have
readOnlyHint: trueannotation - 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.txtCommit: 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
- Create
-
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/listresponse
- Update
src/index.ts— add HTTP transport support:- Check
MCP_TRANSPORTenv var (default: "stdio") - If
MCP_TRANSPORT=stdio: useStdioServerTransport(already implemented in Task 6) - If
MCP_TRANSPORT=http: useStreamableHTTPServerTransportfrom@modelcontextprotocol/sdk/server/streamableHttp.js- Create HTTP server on
MCP_HTTP_PORT(default 3000) - Route:
POST /mcpfor JSON-RPC messages - Route:
GET /mcpfor SSE stream (if client requests) - Route:
DELETE /mcpfor session termination - Handle session initialization and session IDs
- Add graceful shutdown (SIGINT/SIGTERM → close server + transport)
- Create HTTP server on
- Invalid
MCP_TRANSPORTvalue →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)`)`
- Check
- Create
src/tools/index.ts— barrel export for all tool registration functions - Ensure
npm run buildproduces workingdist/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
StreamableHTTPServerTransportwith Node'shttp.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 workfrontend-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 functionsrc/config/index.ts— Config includesMCP_TRANSPORTandMCP_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/mcpto transport - Session management:
StreamableHTTPServerTransporthandles session IDs automatically
Acceptance Criteria:
npx tsc --noEmitexits 0npm run buildproducesdist/index.jswithout errorsecho '{"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: truein annotations - Invalid
MCP_TRANSPORT=invalid→ stderr error + exit 1 - No
console.login 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.txtCommit: 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
- Update
-
11. Integration Test Suite + .env.example + Final Polish
What to do:
- Create
src/__tests__/integration.test.ts— end-to-end integration tests:- Import
createServerfromsrc/server.ts - Create server with mock config
- Mock REST and SOAP clients at the client level (not at HTTP/SOAP transport level)
- Test:
tools/listreturns exactly 9 tools with correct names - Test:
tools/list— all tools havereadOnlyHint: trueannotation - Test:
tools/callsage_health → returns health status - Test:
tools/callsage_query with valid params → returns paginated results - Test:
tools/callsage_read with valid params → returns single record - Test:
tools/callsage_search → returns search results - Test:
tools/callsage_list_entities → returns entity list - Test:
tools/callsage_get_context → returns field names - Test:
tools/callsage_soap_read → returns SOAP record - Test:
tools/callsage_soap_query → returns SOAP results - Test:
tools/callsage_describe_entity → returns field definitions with labels - Test:
tools/callwith invalid tool name → returns error - Test:
tools/callsage_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
- Import
- 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.mdwith:- 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.jsonhas correct"main","types","files"fields for distribution - Ensure
npm run build && npm run startworks end-to-end - Run full test suite:
npx vitest run— all tests pass - Run type check:
npx tsc --noEmit— zero errors
- Verify
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 workgit-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 toolssrc/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 datasrc/config/index.ts— SageConfig type for mock config creation
Test References:
src/clients/__tests__/rest-client.test.ts— REST client mockingsrc/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
InMemoryTransportorcreateClientServerPair()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 failuresnpx tsc --noEmitexits 0.env.exampleexists with all documented env varsREADME.mdexists 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 buildproduces 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.txtCommit: 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
- Create
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 —
oracleRead 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-highRuntsc --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 havereadOnlyHint: trueannotation. 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-highStart the MCP server withecho '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' | node dist/index.jsand verify initialization succeeds. Then testtools/listreturns exactly 9 tools. Then test each tool with mock-compatible parameters. Save evidence to.sisyphus/evidence/final-qa/. Test withMCP_TRANSPORT=httpas well. Output:Scenarios [N/N pass] | Tools [9/9 registered] | Transports [2/2] | VERDICT -
F4. Scope Fidelity Check —
deepFor 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, noconsole.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
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)