Skip to content

T-VJ13-sdlc-mcp-stdio-server-tools-derived-from-the-op-registry

State: closed/done · Impact: high · Complexity: large

sdlc has ~30 deterministic ops behind defineOp/defineService (solutions/ontological/lib/registry.ts), each with a Zod input/output contract, but the only way to invoke one is the generated CLI or a direct invokeOp call from TypeScript. There is no way for an MCP-speaking agent host (Claude Code, Codex, Cursor) to call sdlc’s ops as tools. This task builds sdlc mcp, a stdio MCP server that derives its tool set from the existing op registry — no hand-written tool schemas — gated by a safe default (read-only/query ops exposed by default, mutating ops opt-in via config) — plus sdlc mcp register to wire the server into a harness’s own MCP client config.

solutions/ontological/lib/registry.ts exports OpDescriptor (path, summary, Zod input/output, optional cli hints, streaming, hidden), defineOp/ defineService, listOps(), and invokeOp(path, rawInput, ctx). discoverOps() walks the tree for modules whose default export is an OpDescriptor — this is how solutions/ontological/cli/sdlc.ts and solutions/ontological/lib/services/docs/list-ops.ts both build their surfaces without any manual registration list. solutions/ontological/lib/services/dashboard/dashboard-service.ts is the existing model for a defineService with lifecycle leaves (start/stop/list) backed by a long-lived process — but dashboard’s start detaches to a pidfile-tracked background process, which an MCP stdio server must NOT do (the host process that launches sdlc mcp owns its lifecycle by holding stdio open).

OpDescriptor has no notion of “mutating vs read-only” today — every op is equally invocable from the CLI, because the CLI is always operated by a human or an already-trusted agent. An MCP server exposed to a tool-calling loop needs a narrower, opt-in-for-writes default.

solutions/ontological/lib/config/load.ts’s SdlcConfigSchema is a flat z.object of per-feature sections, each .strict(), each field .describe()’d and .default()’d, validated via SdlcConfigSchema.safeParse with an all-defaults fallback. There is no mcp: section yet.

No @modelcontextprotocol/sdk dependency exists anywhere in the repo today (solutions/ontological/package.json has no matching entry).

  • sdlc mcp start starts a stdio MCP server (a bare-noun defineService has no way to make its noun alone run start, matching sdlc dashboard start’s existing convention — settled during implementation in favor of bare sdlc mcp/sdlc mcp serve). It walks listOps(), keeps only ops that (a) are NOT excluded by the mcp: config’s allow/deny list and (b) are read-only by default — mutating: false on the descriptor, or explicitly allowlisted in config when mutating is absent/true — converts each surviving op’s Zod input schema to JSON Schema, and registers one MCP tool per op whose handler calls invokeOp(op.path, args, ctx) and returns the op’s output.
  • sdlc mcp register --harness <claude|codex|cursor> [--dry-run] prints (or writes, without --dry-run) the per-harness MCP client registration (Claude Code .mcp.json, Codex .codex/config.toml, Cursor .cursor/mcp.json) pointing at sdlc mcp start as the launch command.
  • A new mcp: section in SdlcConfigSchema carries the allow/deny list of op paths (space-joined, e.g. "task next"), deliberately small — Phase 5’s later full sdlc.yaml meta-schema redesign (.meta() annotations) can fold it in without rework.
  1. Add an optional mutating?: boolean field to OpDescriptor in solutions/ontological/lib/registry.ts. Absence is treated as “unknown — treat as mutating” (excluded from the MCP server’s default read-only tool set unless explicitly allowlisted in config). Document the default-deny rationale in the field’s docstring.
  2. Survey the ops tree for the full set of clearly read-only ops — the list/check/coverage/graph/probe/gap-report/preview-id/inflight-style ops (task next, task inflight, task probe-state, task list-unmerged, roadmap check, capability coverage, capability graph, every per-type preview-id, dashboard list, and siblings found by the same pattern) — and add mutating: false to each one’s defineOp/defineCreateOp call site.
  3. Add the mcp: section to SdlcConfigSchema in solutions/ontological/lib/config/load.ts: an allow/deny pair of string arrays (space-joined op-path strings, e.g. "task next"), .strict(), .prefault({}), per-field .describe() and .default([]), styled like every neighboring config block.
  4. Add solutions/ontological/package.json’s @modelcontextprotocol/sdk dependency (the official MCP TypeScript SDK) and a Zod-to-JSON-Schema conversion dependency (or a small hand-rolled converter if the op input shapes don’t need a full library) to solutions/ontological/package.json.
  5. Create solutions/ontological/lib/services/mcp/mcp-service.ts exporting defineService({ path: ['mcp'], summary, lifecycle: { start } }), modeled on dashboard-service.ts’s structure but WITHOUT its detach/pidfile machinery: start runs in the foreground, builds an MCP Server/McpServer, filters listOps() by the mutating default + the mcp: config allow/deny list, registers one tool per surviving op (JSON-Schema-converted input, handler calling invokeOp), and connects over StdioServerTransport.
  6. Add solutions/ontological/lib/services/mcp/register.ts, a second op (defineOp({ path: ['mcp', 'register'], ... })) in the same directory that prints (default) or writes (--dry-run absent) the per-harness MCP client registration block pointing at sdlc mcp as the launch command, for --harness claude|codex|cursor.
  7. Write the test suite (see Acceptance criteria) against a fixture corpus built in a tmpdir, never the real repo corpus.
  8. Run sdlc docs generate --check (via solutions/ontological/cli/sdlc.ts) to confirm the auto-discovered service and op need no manual doc wiring.
AreaNote
solutions/ontological/lib/registry.tsNew mutating?: boolean field on OpDescriptor
solutions/ontological/lib/services/mcp/New mcp-service.ts (the stdio server) and register.ts (the register op)
solutions/ontological/lib/config/load.tsNew mcp: allow/deny config section
solutions/ontological/package.jsonNew @modelcontextprotocol/sdk (+ JSON Schema conversion) dependency
solutions/ontological/lib/model/entities/*/ops/*.tsmutating: false added to the curated read-only op set’s defineOp calls
  • AC-1: sdlc mcp start driven over stdio (in-process via the MCP SDK’s Client + InMemoryTransport in server.test.ts) answers an MCP initialize + tools/list request, and tools/call on a mutating: false op (task next) against a fixture corpus built in a tmpdir returns the same result as invoking that op directly via invokeOp on the same fixture (CLI/MCP parity test).
  • AC-2: An op with no mutating field set does NOT appear in tools/list when the mcp: config has no explicit allowlist entry for it, and DOES appear once explicitly allowlisted — proven both as a selectMcpTools unit case and at the real tools/list wiring level (task_next vs task_create).
  • AC-3: The JSON Schema generated for task next’s non-trivial Zod input (string arrays, booleans, a nullable-number field) is asserted field-by-field against the actual tools/list wire output.
  • AC-4: sdlc mcp register --harness claude --dry-run prints a valid .mcp.json-shaped block naming sdlc mcp start as the launch command, without writing any file; omitting --dry-run writes it (merge-preserving existing keys, idempotent on re-run).
  • AC-5: sdlc docs generate --check passes with no diff after this change (the new service and op are picked up by discoverOps() with zero manual wiring in solutions/ontological/cli/sdlc.ts or solutions/ontological/lib/services/docs/list-ops.ts) — reconfirmed clean at close-out.
  • Phase 5’s full sdlc.yaml meta-schema redesign (.meta() annotations).
  • Phase 6’s host/apply integration — this task ships sdlc mcp register as a standalone command; wiring it into apply is a later phase.
  • Any change to solutions/ontological/lib/config/workflows.ts or solutions/ontological/lib/services/session/.
  • Orchestrate and PR-review changes (Phase 2 territory).
  • Anything under solutions/ontological/skills/, solutions/ontological/plugin/, or the gates/harness/docs-site surfaces (Phase 3 territory).
  • none

← Back to Tasks