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).
Proposed
Section titled “Proposed”sdlc mcp startstarts a stdio MCP server (a bare-noundefineServicehas no way to make its noun alone runstart, matchingsdlc dashboard start’s existing convention — settled during implementation in favor of baresdlc mcp/sdlc mcp serve). It walkslistOps(), keeps only ops that (a) are NOT excluded by themcp:config’s allow/deny list and (b) are read-only by default —mutating: falseon the descriptor, or explicitly allowlisted in config whenmutatingis absent/true— converts each surviving op’s Zodinputschema to JSON Schema, and registers one MCP tool per op whose handler callsinvokeOp(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 atsdlc mcp startas the launch command.- A new
mcp:section inSdlcConfigSchemacarries the allow/deny list of op paths (space-joined, e.g."task next"), deliberately small — Phase 5’s later fullsdlc.yamlmeta-schema redesign (.meta()annotations) can fold it in without rework.
Approach
Section titled “Approach”- Add an optional
mutating?: booleanfield toOpDescriptorinsolutions/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. - 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-typepreview-id,dashboard list, and siblings found by the same pattern) — and addmutating: falseto each one’sdefineOp/defineCreateOpcall site. - Add the
mcp:section toSdlcConfigSchemainsolutions/ontological/lib/config/load.ts: anallow/denypair 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. - Add
solutions/ontological/package.json’s@modelcontextprotocol/sdkdependency (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) tosolutions/ontological/package.json. - Create
solutions/ontological/lib/services/mcp/mcp-service.tsexportingdefineService({ path: ['mcp'], summary, lifecycle: { start } }), modeled ondashboard-service.ts’s structure but WITHOUT its detach/pidfile machinery:startruns in the foreground, builds an MCPServer/McpServer, filterslistOps()by themutatingdefault + themcp:config allow/deny list, registers one tool per surviving op (JSON-Schema-converted input, handler callinginvokeOp), and connects overStdioServerTransport. - 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-runabsent) the per-harness MCP client registration block pointing atsdlc mcpas the launch command, for--harness claude|codex|cursor. - Write the test suite (see Acceptance criteria) against a fixture corpus built in a tmpdir, never the real repo corpus.
- Run
sdlc docs generate --check(viasolutions/ontological/cli/sdlc.ts) to confirm the auto-discovered service and op need no manual doc wiring.
| Area | Note |
|---|---|
solutions/ontological/lib/registry.ts | New 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.ts | New mcp: allow/deny config section |
solutions/ontological/package.json | New @modelcontextprotocol/sdk (+ JSON Schema conversion) dependency |
solutions/ontological/lib/model/entities/*/ops/*.ts | mutating: false added to the curated read-only op set’s defineOp calls |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1:
sdlc mcp startdriven over stdio (in-process via the MCP SDK’s Client + InMemoryTransport inserver.test.ts) answers an MCPinitialize+tools/listrequest, andtools/callon amutating: falseop (task next) against a fixture corpus built in a tmpdir returns the same result as invoking that op directly viainvokeOpon the same fixture (CLI/MCP parity test). - AC-2: An op with no
mutatingfield set does NOT appear intools/listwhen themcp:config has no explicit allowlist entry for it, and DOES appear once explicitly allowlisted — proven both as aselectMcpToolsunit case and at the realtools/listwiring level (task_nextvstask_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 actualtools/listwire output. - AC-4:
sdlc mcp register --harness claude --dry-runprints a valid.mcp.json-shaped block namingsdlc mcp startas the launch command, without writing any file; omitting--dry-runwrites it (merge-preserving existing keys, idempotent on re-run). - AC-5:
sdlc docs generate --checkpasses with no diff after this change (the new service and op are picked up bydiscoverOps()with zero manual wiring insolutions/ontological/cli/sdlc.tsorsolutions/ontological/lib/services/docs/list-ops.ts) — reconfirmed clean at close-out.
Out of scope
Section titled “Out of scope”- Phase 5’s full
sdlc.yamlmeta-schema redesign (.meta()annotations). - Phase 6’s host/apply integration — this task ships
sdlc mcp registeras a standalone command; wiring it intoapplyis a later phase. - Any change to
solutions/ontological/lib/config/workflows.tsorsolutions/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).
Dependencies
Section titled “Dependencies”- none