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

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=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

  • 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
  • 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
  • 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
  • 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
  • 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.mdREAD 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
  • 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
  • 7. REST Tools: sage_query + sage_read + sage_search (TDD)

    What to do:

    • Create src/tools/sage-query.tssage_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.tssage_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.tssage_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
  • 8. REST Discovery Tools: sage_list_entities + sage_get_context (TDD)

    What to do:

    • Create src/tools/sage-list-entities.tssage_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.tssage_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
  • 9. SOAP Tools: sage_soap_read + sage_soap_query + sage_describe_entity (TDD)

    What to do:

    • Create src/tools/sage-soap-read.tssage_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.tssage_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.tssage_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
  • 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
  • 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 Auditoracle 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 Reviewunspecified-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 QAunspecified-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 Checkdeep 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

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)