Skip to content

feat(mcp): upgrade rmcp to 3.0.1 and serve MCP 2026-07-28 - #177

Merged
sunerpy merged 7 commits into
mainfrom
feat/rmcp-3.0.1-upgrade
Jul 31, 2026
Merged

feat(mcp): upgrade rmcp to 3.0.1 and serve MCP 2026-07-28#177
sunerpy merged 7 commits into
mainfrom
feat/rmcp-3.0.1-upgrade

Conversation

@sunerpy

@sunerpy sunerpy commented Jul 31, 2026

Copy link
Copy Markdown
Owner

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_tool returns the #[non_exhaustive] CallToolResponse enum instead of CallToolResult. We only ever build Complete, via .into() — never match the enum, so a future variant cannot break us.
  • with_stateful_modewith_legacy_session_mode. The value stays false, but the semantics narrowed: it governs legacy revisions only, because 2026-07-28 is stateless unconditionally per SEP-2567.
  • A get_info() comment claiming the protocol version is "forced" was wrong.

What the upgrade actually changed

That last comment is the point. negotiate_protocol_version echoes 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:

client requests negotiated resultType HTTP session
2024-11-05 2024-11-05 absent no header (our config)
2025-03-26 2025-03-26 absent no header (our config)
2025-06-18 2025-06-18 absent no header (our config)
2025-11-25 2025-11-25 absent no header (our config)
2026-07-28 2026-07-28 "complete" no header (per spec)

resultType is 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.rs and tests/rmcp_http.rs now pin the version-echo table across all five revisions, resultType presence at 2026-07-28 against its absence at the four legacy ones, 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 / Mcp-Name is 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 Task or InputRequired. The docs now also distinguish that from the request surface, which is wider: several rmcp defaults we do not override answer successfully, including discover and the three list methods.

Verification

  • make ci green: 128 suites / 3071 passed / 0 failed
  • Golden byte-stability: reference/ untouched; the 15 MCP parity fixtures pass unmodified
  • Coverage measured at 94.42% line / 94.12% region / 93.41% function (informational gate); AGENTS.md's stale ~72% baseline corrected in the process
  • Final Verification Wave: F1 plan compliance (momus), F2 code quality (oracle, four passes), F3 independent hands-on QA, F4 scope fidelity — all APPROVE
  • F3 verified all five revisions on both transports against the real binary, plus nine adversarial probes; F2 closed four real defects including a three-vs-five known-version error in both the docs and the tests

Note for the release

rmcp types appear in codegraph-mcp's public API (build_http_config's return type, the ServerHandler impl), 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, and codegraph-cli only uses the rmcp-free serve_stdio_rmcp / serve_http signatures.

sunerpy added 7 commits July 31, 2026 18:39
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

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Impacted file tree graph

@@           Coverage Diff           @@
##             main     #177   +/-   ##
=======================================
  Coverage   95.07%   95.07%           
=======================================
  Files         136      136           
  Lines       70128    70129    +1     
=======================================
+ Hits        66673    66674    +1     
  Misses       3455     3455           
Files with missing lines Coverage Δ
crates/codegraph-mcp/src/rmcp_handler.rs 92.67% <100.00%> (+0.01%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@sunerpy
sunerpy merged commit e2604a5 into main Jul 31, 2026
7 checks passed
@sunerpy
sunerpy deleted the feat/rmcp-3.0.1-upgrade branch July 31, 2026 14:24
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