MCP Spec 2026-07-28 - #56
Merged
Merged
Conversation
Deep-dive on adopting the MCP 2026-07-28 spec in CALM. Assesses each SEP against CALM's real deployment (single-node stdio/daemon + opt-in HTTP, single SQLite, SINGLE_WRITER) rather than adopting wholesale. Key findings: - rmcp is pinned "2" (2.2.0, LATEST=2025-11-25); full 2026-07-28 lives in the rmcp 3.x line (3.0.0/3.1.0) which the pin excludes. - Highest-value + deadline item: migrate the write-path human-veto gate from deprecated elicitation (SEP-2577) to MRTR/input_required (SEP-2322). Fixes a real cross-transport safety hole (gate silently disables on stateless connections) and survives the 12-month deprecation window. - Most of the spec (stateless core, header routing, OAuth hardening) is low-value for a single-node tool; deferred to a hosted/multi-user future. - Structured output + explicit model-visible handles are already in place. Proposes a 4-phase roadmap (P0 de-risk spike, P1 rmcp 2->3 behavior- preserving, P2 MRTR gate migration, P3 cacheable lists, P4 deferred). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiYMb2ZWUn8TzViFV4rJC6
…e 1) Phase 1 of docs/plans/2026-08-04-mcp-2026-07-28-upgrade-plan.md. Live- verified in a scratch spike first (rmcp 3.1.0 + axum 0.8 + schemars 1 + transport-streamable-http-server all resolve to a single dependency tree each, no OTEL-style split-core), then applied here. rmcp 3.x wraps ServerHandler::call_tool/get_prompt results in new CallToolResponse/GetPromptResponse enums (SEP-2322 MRTR support) and adds required cache-hint fields to ListToolsResult/ListPromptsResult (SEP-2549). Individual #[tool] methods are unaffected -- the IntoCallToolResult blanket impl absorbs the wrapping in the macro-generated router; only the hand-written ServerHandler dispatch in tools.rs needed updating. Explicitly caps CalmServer::supported_protocol_versions() below 2026-07-28: rmcp defaults that list to every version it knows, but a peer negotiating 2026-07-28 is served statelessly over Streamable-HTTP regardless of legacy_session_mode (SEP-2567) -- which would silently drop the per-connection state this server leans on today (toolset narrowing, the hub-edit human-veto declined-answer cache) before Phase 2 gives that gate an MRTR-based path that survives statelessness. Verified end-to-end with a new daemon_integration test: a client offering 2026-07-28 still gets a successful initialize, gracefully downgraded to 2025-11-25. Verified: full workspace build clean (default + --all-features), clippy clean, calm-core (957) + calm-server (308/310, 2 pre-existing unrelated failures confirmed via git-stash baseline) + calm-cli (all suites, including http_guard under --features http) tests green. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiYMb2ZWUn8TzViFV4rJC6
…07-28 Phase 2) Phase 2 of docs/plans/2026-08-04-mcp-2026-07-28-upgrade-plan.md. The write-safety human-veto gate (edit_lines/edit_symbol) previously had exactly one mechanism -- a live server-initiated elicitation/create round-trip over an open back-channel (peer.elicit_with_timeout). That mechanism cannot work at all over a genuinely stateless connection, which is what any peer negotiating protocol 2026-07-28 gets served over (SEP-2567) regardless of session-manager configuration -- so the gate would have silently had no working mechanism for such a peer. Adds SEP-2322 Multi Round-Trip Requests as a second mechanism, selected per-call by elicit_setup() based on the peer's negotiated protocol version: pre-2026-07-28 peers get the exact same legacy round-trip as before (byte-identical code path, unchanged); 2026-07-28+ peers get an InputRequiredResult instead of a held-open await, retry the same tools/call with inputResponses once a human answers, and hub_mrtr_decide verifies the answer from a self-contained, HMAC-sealed requestState (rmcp::model::RequestStateCodec, keyed by the same load_or_create_mac_key project_memory already uses) bound to (tool, path, fingerprint) -- so a stale or forged approval fails closed regardless of whether the ask and the retry land on the same server instance. Both mechanisms converge on the same fail-closed decision: only an explicit approve:true ever lets the write proceed. decide_mrtr_answer is map_elicit_outcome's sibling for the MRTR answer shape, mirroring its table 1:1. Mechanics, verified against the real rmcp 3.1.0 source (not release notes): - ToolCallContext::new discards input_responses/request_state before any #[tool] method sees them, so the dispatcher (CalmServer::call_tool) is the only place that can forward a retry's answer -- via RequestContext::extensions, rmcp's own typed pass-through. - The #[tool] macro can only auto-derive output_schema from a literal Json<T> return type; edit_lines_tool/edit_symbol_tool now return HubEditToolResult<T> (Done(Json<T>) | NeedsApproval(InputRequiredResult)) instead, so both carry an explicit output_schema = schema_for_output:: <T>() attribute reproducing exactly what the macro used to derive -- confirmed unchanged by tool_schemas_match_committed_snapshots. - request-state feature added to the rmcp dependency (pulls hmac 0.13/sha2 0.11 as new transitive deps, distinct majors from this workspace's own 0.12/0.10 pins -- verified safe, not an OTEL-style two-cores problem: RequestStateCodec is self-contained, this code never constructs an Hmac/Sha2 value itself). Lifts the Phase 1 protocol-version cap: supported_protocol_versions() now returns rmcp's full KNOWN_VERSIONS again. The cap existed for exactly the gap this closes (the gate's only mechanism not surviving statelessness); the decision no longer depends on any per-connection state, so it is correct regardless of instance affinity. set_toolset narrowing and the declined-answer cache remain per-connection conveniences (not safety guarantees) -- general stateless-HTTP readiness for those stays Phase 4. Verified: full workspace build clean (default + --all-features), clippy clean. 7 new unit tests (approve/decline/tamper/forged-state/already- declined/full-round-trip) plus all 12 existing elicitation tests green, proving the legacy path is untouched. New daemon_integration end-to-end test proves 2026-07-28 now negotiates for real over the actual daemon; existing 2024-11-05 negotiation (used by every other test in that file) still holds. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CiYMb2ZWUn8TzViFV4rJC6
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.
No description provided.