Skip to content

feat(005): MCP stdio server exposing the governance verbs as tools#4

Merged
bartekus merged 1 commit into
mainfrom
005-mcp-server
Jul 15, 2026
Merged

feat(005): MCP stdio server exposing the governance verbs as tools#4
bartekus merged 1 commit into
mainfrom
005-mcp-server

Conversation

@bartekus

Copy link
Copy Markdown
Contributor

Summary

Implements spec 005: stagecraft mcp runs a Model Context Protocol server over
stdio, 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.

  • Not a privileged side door. Each tool calls the identical spec 004 verb
    request. The endpoint and body knowledge now lives once, in *_request
    functions in stagecraft_cli::verbs that both faces share; the MCP face only
    chooses 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.
  • Guards pass through unchanged. stamp_new requires an explicit posture
    and fleet_remove a confirm_name; both are 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.
  • Unauthenticated is graceful. The server still starts with no credential
    (or an unreadable one) and answers every tool call with a structured
    {ok:false,error:{kind:"unauthenticated"}} result naming stagecraft login.
  • Robust transport. One malformed stdio line (invalid UTF-8, invalid JSON,
    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-config prints the .mcp.json install 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 Code
claude mcp add round-trip. A dated §4 Status note records exactly what
remains; 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 scripted
    in-process stdio client drives initialize -> tools/list -> tools/call
    round-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-config and an
    initialize round-trip over real stdio.
  • An independent review pass drove hardening before merge: a bad-UTF-8 stdin
    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_list with a base URL but no credential (structured
    unauthenticated result naming stagecraft login), stamp_new without
    posture (-32602), ping, clean shutdown on EOF. Verified strict framing (one
    message per physical line, no embedded newlines) and that the text content
    mirrors the structured envelope.
  • Spine gates green: spec-spine compile, index (stagecraft_cli::mcp
    resolves, 0 diagnostics), lint --fail-on-warn (0/0/0), index check fresh.

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.

`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.
@bartekus
bartekus merged commit aa56f42 into main Jul 15, 2026
2 checks passed
@bartekus
bartekus deleted the 005-mcp-server branch July 15, 2026 12:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant