feat(mcp): upgrade rmcp to 3.0.1 and serve MCP 2026-07-28 - #177
Merged
Conversation
rmcp 3.0.1 implements the stable MCP 2026-07-28 specification. The server-side migration cost three lines: call_tool returns the non-exhaustive CallToolResponse enum instead of CallToolResult, of which we only ever build Complete; with_stateful_mode became with_legacy_session_mode; and the get_info comment claiming the protocol version is forced was wrong. That comment is the substantive part. negotiate_protocol_version echoes back any version the client asks for that rmcp knows, consulting the configured value only for an unrecognized request, so with_protocol_version was never a ceiling. The upgrade therefore starts serving 2026-07-28 by default rather than by choice, and the honest change is to say so instead of implying we pin 2024-11-05. with_legacy_session_mode also narrowed: it governs legacy revisions only, because 2026-07-28 is stateless unconditionally per SEP-2567. The list_roots deprecation note is refreshed rather than removed: SEP-2577 still deprecates it in 3.0.1 and still offers no replacement.
The structural goldens compare only named fields, so a top-level resultType is invisible to them — which is why the fifteen fixtures survived the 3.0.1 upgrade untouched, and equally why the new wire shape needed its own assertions. Lock the version-echo table across all three known revisions, the presence of resultType at 2026-07-28 against its absence at 2024-11-05 and 2025-11-25, the absence of Mcp-Session-Id on the stateless 2026 HTTP path, and SEP-2243 header validation in both directions: matching headers reach the tool, while a missing MCP-Protocol-Version or a mismatched Mcp-Method or Mcp-Name is rejected with HTTP 400 and JSON-RPC -32020. 2025-11-25 had no coverage anywhere before this.
Both files claimed the handshake returns a fixed 2024-11-05, which negotiation contradicts. Replace that with the three revisions actually served and what changes at each, and state plainly that the configured version is only an unknown-request fallback. Also correct the session-id claim the first draft got wrong: the tables promised Mcp-Session-Id for legacy revisions, but this server passes with_legacy_session_mode(false), so it never sends one at any revision. The two absences have different causes worth distinguishing — legacy is our configuration and reversible, 2026-07-28 is mandated by SEP-2567 and is not. Finally, state that the MCP goldens are structural rather than byte-stable. That is why resultType could not drift them and why it needed an explicit test. The extraction goldens are a separate contract and genuinely are byte-stable.
KNOWN_VERSIONS holds five revisions, not three: 2025-03-26 and 2025-06-18 sit between the two I had documented. negotiate_protocol_version echoes any of them, so telling a reader that only three are known and anything else falls back to 2024-11-05 was a false statement about our own wire behavior, and the version tables had the same blind spot. Both tests now cover all five; the two added revisions strip resultType like the other pre-2026 peers, verified by running them rather than assuming. Discovery was listed as not implemented, which is wrong. rmcp's default discover answers with supported_protocol_versions() plus get_info(), and we do not override it, so a client can call it and get a real result. Same for complete, which returns an empty default. Move both out of the not-implemented list and say what actually happens, keeping Tasks, MRTR and subscriptions there, where it is true: we never construct Task or InputRequired, accepted_subscription_filter keeps its None default, and subscribe, unsubscribe and set_level all return method-not-found. Advertising only the tools capability and having a literally tools-only request surface are different things, and the docs now distinguish them.
The claim that two request-level defaults remain callable was wrong: six answer successfully. Besides discover and complete, the three list methods return empty results rather than method-not-found, so probing for prompts or resources yields a success with nothing in it; and ping succeeds on the legacy revisions while being method-not-found at 2026-07-28. This is the third incomplete enumeration of rmcp defaults this round — first discovery was wrongly filed as unimplemented, then three list methods were missed, now ping. The pattern is that a claim about what a macro-generated trait does not implement has to be checked against the generated defaults, not against our own code, since we override none of them. Behavior is unchanged. Returning an empty list is spec-conformant, so overriding those methods to refuse would be a change nobody asked for.
The sentence had been corrected four times in one round — discovery was wrongly filed as unimplemented, then three list methods were missed, then ping, then initialize itself. Each fix bumped a total that the next reading falsified, because an exact count of a macro-generated trait's defaults goes stale the moment upstream adds one. State the scope instead: beyond the mandatory initialize handshake, these inherited defaults also answer. A future addition then makes the list incomplete rather than false. Naming initialize as an inherited default also connects two facts the sections stated separately: its default applies negotiate_protocol_version on top of our get_info(), which is precisely why negotiation is automatic and why the version we pass there is only a fallback.
The coverage bullet claimed a ~72% baseline and named three files as the biggest gaps, all four of which are now false: measured coverage is 94.42% line, 94.12% region, 93.41% function, and the named files sit at 95.16%, 97.12%, 94.26% and 91.11% — the last two having been described as 0%-covered. Record the measured figures with the commit they came from, since a bare number in a doc becomes folklore once nobody knows how old it is. Also state where the gap actually lives now: it moved category, from resolver and watch internals to the per-language extractors, which hold 7 of the 10 lowest-covered files. The 95% aspiration, the informational non-blocking gate, the three make targets and the bench exclusion are unchanged — this corrects a measurement, not a policy.
Codecov Report✅ All modified and coverable lines are covered by tests. @@ Coverage Diff @@
## main #177 +/- ##
=======================================
Coverage 95.07% 95.07%
=======================================
Files 136 136
Lines 70128 70129 +1
=======================================
+ Hits 66673 66674 +1
Misses 3455 3455
🚀 New features to boost your workflow:
|
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
rmcp 3.0.1 implements the stable MCP 2026-07-28 specification. The server-side migration cost three lines; the substantive work was discovering what the upgrade actually changes on the wire and testing it.
The migration (3 edits)
call_toolreturns the#[non_exhaustive]CallToolResponseenum instead ofCallToolResult. We only ever buildComplete, via.into()— nevermatchthe enum, so a future variant cannot break us.with_stateful_mode→with_legacy_session_mode. The value staysfalse, but the semantics narrowed: it governs legacy revisions only, because 2026-07-28 is stateless unconditionally per SEP-2567.get_info()comment claiming the protocol version is "forced" was wrong.What the upgrade actually changed
That last comment is the point.
negotiate_protocol_versionechoes back any version the client requests that rmcp knows, consulting the configured value only for an unrecognized request. So.with_protocol_version(V_2024_11_05)was never a ceiling — the moment 3.0.1 landed, this server began serving five revisions, by SDK default rather than by choice:resultType"complete"resultTypeis absent for pre-2026 peers because upstream #1038 strips it for legacy peers — which is exactly why the 15 golden fixtures survived the upgrade untouched, and equally why the new wire shape needed its own assertions.New coverage
tests/rmcp_l3.rsandtests/rmcp_http.rsnow pin the version-echo table across all five revisions,resultTypepresence at 2026-07-28 against its absence at the four legacy ones, the absence ofMcp-Session-Idon the stateless 2026 HTTP path, and SEP-2243 header validation in both directions — matching headers reach the tool, while a missingMCP-Protocol-Versionor a mismatchedMcp-Method/Mcp-Nameis rejected with HTTP 400 and JSON-RPC-32020. 2025-11-25 had no coverage anywhere before this.Not implemented, deliberately
Tasks (SEP-2663), MRTR / elicitation-in-tool, and subscriptions. We advertise only the tools capability and never construct
TaskorInputRequired. The docs now also distinguish that from the request surface, which is wider: several rmcp defaults we do not override answer successfully, includingdiscoverand the three list methods.Verification
make cigreen: 128 suites / 3071 passed / 0 failedreference/untouched; the 15 MCP parity fixtures pass unmodifiedAGENTS.md's stale ~72% baseline corrected in the processNote for the release
rmcptypes appear incodegraph-mcp's public API (build_http_config's return type, theServerHandlerimpl), so the 2.x → 3.x jump is breaking for anyone consuming that module with their own rmcp 2.x. Contained in practice: the project ships via GitHub Releases rather than crates.io, andcodegraph-clionly uses the rmcp-freeserve_stdio_rmcp/serve_httpsignatures.