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