feat(005): MCP stdio server exposing the governance verbs as tools#4
Merged
Conversation
`stagecraft mcp` runs a hand-rolled JSON-RPC 2.0 server over
newline-delimited stdio, exposing the spec 004 verbs as nine MCP tools
so a coding agent (Claude Code first) operates under Stagecraft
governance natively. Not a privileged side door: each tool calls the
identical verb request from stagecraft_cli::verbs (the endpoint and body
knowledge now lives once, in `*_request` functions both faces share),
and the tool result is the spec 004 §5.2 {ok,data|error} envelope
verbatim, so platform attestation/record ids are carried through for
agent transcripts to cite.
Guards pass through to the agent unchanged: stamp_new requires an
explicit posture and fleet_remove a confirm_name, both rejected as
JSON-RPC invalid-params (-32602) when missing, empty, or malformed;
neither login nor install-url --open is exposed, so an agent can never
trigger a browser flow. An unauthenticated (or unreadable-credential)
server still starts and answers every tool call with a structured error
naming `stagecraft login`. `stagecraft mcp --print-config` prints the
.mcp.json install snippet.
The transport is deliberately robust: one malformed stdio line (invalid
UTF-8, invalid JSON, a non-request object) is answered with the right
JSON-RPC error and skipped rather than aborting the server, and one
current-thread runtime is reused across the sequential request loop.
Transport is hand-rolled (no rmcp) per the spec 005 §1 amendment: small,
dependency-free, rustls-only, and it keeps the envelope the literal tool
result with no reshaping.
Spec 005 stays implementation: in-progress: the §2 live check needs a
running control plane (Stagecraft's tenants/factory/fleet services are
still pending) plus a Claude Code round-trip. A dated §4 Status note
records exactly what remains; all mock-testable acceptance holds.
Covered by 98 tests (85 unit + 13 integration): scripted stdio
initialize -> tools/list -> tools/call round-trips against a mock plane,
guard rejections, the unauthenticated tool-result, record-id
passthrough, transport-robustness paths, and the print-config snippet.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements spec 005:
stagecraft mcpruns a Model Context Protocol server overstdio, exposing the spec 004 governance verbs as nine MCP tools so a coding
agent (Claude Code first) operates under Stagecraft governance natively, with
the same auth, the same guards, and the same JSON shapes as the CLI face.
request. The endpoint and body knowledge now lives once, in
*_requestfunctions in
stagecraft_cli::verbsthat both faces share; the MCP face onlychooses which to call. The tool result is the spec 004 §5.2
{ok,data|error}envelope verbatim, so platform attestation/record ids in the payload are
carried through for agent transcripts to cite.
stamp_newrequires an explicitpostureand
fleet_removeaconfirm_name; both are rejected as JSON-RPCinvalid-params (-32602) when missing, empty, or malformed. Neither
loginnorinstall-url --openis exposed, so an agent can never trigger a browser flow.(or an unreadable one) and answers every tool call with a structured
{ok:false,error:{kind:"unauthenticated"}}result namingstagecraft login.a non-request object) is answered with the right JSON-RPC error and skipped,
never aborting the server; one current-thread runtime is reused across the
sequential request loop.
stagecraft mcp --print-configprints the.mcp.jsoninstall snippet.Transport decision (spec 005 §1 amendment): hand-rolled JSON-RPC 2.0 over
newline-delimited stdio, not the rmcp SDK. The framing is small,
dependency-free, keeps the tree rustls-only, and lets the envelope be the
literal tool result with no reshaping. Recorded in the spec.
Spec 005 stays
implementation: in-progress(same shape as 003 and 004):the §2 live check needs a running control plane (Stagecraft's tenants/factory/
fleet services are all still
implementation: pending) plus a Claude Codeclaude mcp addround-trip. A dated §4 Status note records exactly whatremains; all mock-testable acceptance holds and drops the live transcript in
with no code change once a plane exists.
Testing
cargo fmt --check,cargo clippy --all-targets -- -D warnings: clean.cargo test: 98 pass (85 unit + 13 integration). New coverage: a scriptedin-process stdio client drives
initialize -> tools/list -> tools/callround-trips against a mock-backed verb layer (tenants list, stamp launch with
posture forwarding, fleet remove with confirm-name forwarding), guard
rejections for the two guarded tools, the unauthenticated tool-result,
record-id passthrough, and the transport-robustness paths (invalid UTF-8 line
survived + next request served, invalid-request vs parse-error codes, explicit
null id as a request, unknown-method, notification, shutdown). Two new
integration tests drive the real binary:
mcp --print-configand aninitializeround-trip over real stdio.line no longer kills the server (byte-oriented read), a single reused runtime
replaces per-call runtime construction, and a corrupt credentials store
degrades to an unauthenticated start instead of failing to boot. Each was
reproduced against the release binary and locked with a test.
cargo build --release, then drove the release binary end-to-end over stdio:initialize(serverInfo + echoed protocolVersion),tools/list(9 tools),tools/call tenants_listwith a base URL but no credential (structuredunauthenticatedresult namingstagecraft login),stamp_newwithoutposture (-32602),
ping, clean shutdown on EOF. Verified strict framing (onemessage per physical line, no embedded newlines) and that the text content
mirrors the structured envelope.
spec-spine compile,index(stagecraft_cli::mcpresolves, 0 diagnostics),
lint --fail-on-warn(0/0/0),index checkfresh.Spec-Drift-Waiver: this PR implements spec 005 (the MCP face) but necessarily
edits files under the spec 002 crate scaffold: src/mcp.rs (new), src/cli.rs,
src/commands.rs, src/error.rs, src/main.rs, src/verbs/mod.rs, src/verbs/
tenants.rs, src/verbs/stamp.rs, src/verbs/fleet.rs. The crate's
[package.metadata.spec-spine] key couples the whole crate to spec 002, so every
feature spec's code lands in 002 territory; the new logic lives in the
spec-005-owned stagecraft_cli::mcp module, while the verb-layer edits factor the
shared spec-004 request path both faces call, and the src/error.rs edit only
annotates the now-unconstructed not-implemented taxonomy arm (all verbs are
implemented). Waiving rather than editing the completed 002 spec, per the
precedent set by the spec 003 and 004 PRs. Approved by the repo owner as the
standing pattern.