The MCP tools
@putnami/sdd contributes five read-only Model Context Protocol tools to
putnami mcp. Four answer the feature/spec questions extracted from core. The
fifth gives an agent one exact architecture domain from the same ARC/DARC
evaluator used by the interactive commands — never from a second parser or
graph.
| Tool | Answers | Arguments |
|---|---|---|
sdd.list_features |
The compact authored-feature catalog | query, projects, impacted, baseline |
sdd.feature_context |
One feature's scoped design facts | feature (required) |
sdd.list_specs |
The durable spec catalog | projects, impacted, baseline |
sdd.spec_context |
One complete spec document | feature (required) |
sdd.architecture_context |
One domain's complete declaration, touching relationships, findings, coverage, and diagnostics | domain (required, exact id) |
All five are annotated read-only and carry the putnami.dev/contract meta
(access: "read", readOnly: true, supportsDryRun: false). None of them
writes. Creating a spec stays a putnami specs init decision made in a terminal
(ADR 0007).
The rename is breaking, and there is no alias
Before #3214 these tools were core tools named list_features,
feature_context, list_specs, and spec_context. Those names are gone.
The core server refuses them with JSON-RPC error -32602.
The dot is not a style choice. An extension tool name without a dot is dropped by the CLI's extension-tool validator, and a name core owns wins the collision against an extension — so an alias would have been either invisible or ambiguous, depending on which of the two rules hit it first.
What you have to update:
- Agent configuration and prompts that name the old tools.
- Nothing in the workspace: the tool names are advertised live by the MCP server at initialization, not copied into a generated file.
- Any tooling that asserts on
tools/listoutput.
Calling them
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | putnami mcp
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"sdd.list_specs","arguments":{"projects":["@putnami/cli"]}}}' | putnami mcp
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"sdd.architecture_context","arguments":{"domain":"billing"}}}' | putnami mcpputnami mcp prepares the extension runtime on the first call, not at
startup: registration answers the executable-presence probe from the manifest
declaration, so no extension code runs while the session is being set up.
How a tool learns what the workspace contains
An MCP tool call carries no job context. Before #3214 Task 7, a
ToolCallRequest carried a workspaceRoot and nothing else — no membership, no
selection. A root-only handler is not merely limited, it is confidently wrong:
measured over this repository's parity fixture, sdd.list_features returned
{ "selection": { "mode": "all", "scoped": false, "projects": [] },
"manifests": 0, "features": [] }with exit zero, over a workspace with two authored features.
The extension may not re-derive any of it — no directory scan, no putnami
subprocess — so the request contract grew instead, additively:
| The tool needs | The request member that carries it |
|---|---|
| Every workspace project, with its resolved version, type and direct edges | workspaceProjects |
A sibling's authored putnami.json (bin, featureAuthority, options) |
workspaceProjects[].config, raw |
| What the caller asked for and what it resolved to | selection, byte-identical to the job wire's |
A tool opts in by declaring "workspaceSelection": true in its manifest
descriptor. The declaration is opt-in rather than implicit because projects,
impacted, and baseline are the CLI's vocabulary and not every extension's: a
third-party tool that means something else by projects must not have its
arguments read as project selectors.
workspaceProjects[].config is the member that is closed for tools and
still open for jobs. A tool reports on other projects by construction, so a
sibling's authored facts are part of its answer — sdd.feature_context reads a
project's bin to mint its command nodes, and without the member it would
report a project with no commands and say so confidently. A job acts on its own
project, so the same member is a follow-up there rather than a blocker.
A request with no resolved selection is refused, never defaulted to "all".
A wrong answer an agent cannot tell from a right one is worse than an error.
Architecture context is worktree-only
sdd.architecture_context requires the exact domain minted by a
putnami.architecture.json; it does not guess from a path, owner, or final id
segment. Its typed report contains:
- the complete
DomainView, including owned concepts, exports and imports; - declared inbound edges (facts flowing into the domain) and outbound edges (facts it offers to consumers);
- every observed edge touching the domain;
- relevant ARC/DARC findings, detector coverage, ratchet summary, and sorted structural diagnostics.
The handler calls BuildArchitectureWorktreeInspectionResult, which calls the
same EvaluateWorkspace and bounded inspection projection as interactive
architecture inspect. The difference is deliberate: the agent tool never
resolves Git history or an adoption baseline. baseline.compared therefore
stays false. Interactive validate/snapshot/inspect remain baseline-aware and
unchanged.
Failures close at the boundary that discovered them. Missing or unknown arguments and unresolved workspace selection fail before evaluation and carry only an error. A structurally invalid repository, a blocking architecture finding, or an unknown exact domain fails after evaluation and carries the typed report first, then the error. An agent can therefore distinguish “the tool did not run” from “the evaluator ran and rejected this worktree.”
Parity with the tools they replace
tooling/cli/internal/cli/sdd_mcp_parity_test.go drives one real mcp.Server
built by production's own constructor and compares content blocks byte for byte
across seventeen calls for the four extracted tools — every narrowing their
schemas declare and every failure mode. Since the core tools were removed,
those answers are compared against recorded fixtures captured from core
before the deletion, with no -update flag; see 05-parity.md.
Architecture context was added after that oracle was deleted, so its tests pin
the shared evaluator, full typed projection, worktree-only boundary, and
report-plus-error failure envelope directly.
Two implementation details are load-bearing and are the opposite of the interactive path's:
- No key-order round trip. The interactive path re-encodes its payload
through
map[string]any, because the CLI captures a built-in command's stdout and Go sorts map keys.handleToolsCalldoes the opposite — it hands the handler's return value straight tojson.MarshalIndent— so a report struct encodes in declaration order and applying the interactive adaptation here would break parity. Same extraction, opposite rule, one wire apart. - A failure may carry its report. A core tool that fails with a value
attached writes two content blocks, the value then the message. The SDK's
mcp.Servereproduces that split; argument guards return a nil payload, and engine calls return the report beside the error.