DEV Community

Kaushikreddy Lingi
Kaushikreddy Lingi

Posted on AI-assisted

Two sessions sent the same request id. My MCP server swapped their results.

I audited my own Rust MCP server against the spec, line by line. One row of the audit was a bug I would not have guessed: two clients that happened to use the same JSON-RPC request id received each other's results.

That is not a protocol nitpick. It is the server answering session A with session B's data.

How I found it

I did not trust my own README. I pinned the MCP 2025-11-25 spec at one commit, turned every MUST and SHOULD into a row, and gave each row a verdict backed by one of four kinds of evidence: a test in the repo, a scenario from the official conformance suite, a black-box probe, or a replay of a real session from MCP Inspector or Claude Code.

Result, from the official suite's 2025-11-25 requirements:

Before After
Checks 48 passed, 13 failed 62 passed, 10 failed
Failures that were spec violations 2 0
Non-conformant rows in my matrix 20 1 (deliberate)

The 10 checks that still fail are features the SDK does not implement (completion, subscriptions, audio content, sampling, elicitation). I list them as "not implemented", not as defects.

The ones that surprised me

  • Same id, two sessions, swapped results. Over HTTP, responses were routed by request id alone. Fixed by scoping routing to the session; there is a regression test for it.
  • Ping waited for a running tool call. The spec says ping must be answered promptly. In my probe it took 1.5 seconds because it queued behind a slow tool. After the fix it answers immediately.
  • Cancellation did nothing until it was too late. The cancel notification was read only after the request finished, so the request ran to completion and was answered anyway.
  • A panicking tool was answered with id: 0. The client could not match the error to its request.
  • Tool failures were protocol errors. A tool that failed returned JSON-RPC -32603 instead of a result with isError: true, so a model never got to see and recover from the error.
  • The server bound to 0.0.0.0 by default and ignored Origin. Now it binds to localhost and checks Host and Origin, which closes the DNS-rebinding hole.

One thing I deliberately left "wrong"

The spec says a missing resource SHOULD return -32002. I left my -32602. The next revision standardizes on -32602 and the official suite already checks for it, so "fixing" it would only get reverted. The matrix says so in the row instead of hiding it.

What I will not claim

An earlier README said "high-performance" and implied completeness. I removed both, because I had no benchmark behind the first and the matrix disproves the second. The audit document is more useful to a reader than the adjectives were.

Try it

The full matrix, with a file:line citation for every requirement, is in docs/CONFORMANCE.md: https://github.com/Lingikaushikreddy/mcp-rust-sdk

The fixes are in an open draft PR on the repo, so check the branch rather than assuming they are on main.

If you maintain an MCP server in any language, how many of these would your server fail? I would like to know which ones are most common.

Top comments (1)

Collapse
 
dhruonweb profile image
Dhruv Malaviya •

Pinning the spec at one commit and turning every MUST into a row with evidence is the methodology worth copying, and it's what makes this credible rather than a list of impressions.

The tool-failure bug is the most consequential of the six. Returning -32603 instead of a result with isError: true doesn't just mislabel the outcome — it removes the model's ability to see and recover from it. A protocol error ends the exchange; a tool error is information.

And binding to 0.0.0.0 while ignoring Origin is the DNS-rebinding shape again. A localhost-only service that answers any Host header isn't localhost-only.

The ping-queued-behind-a-slow-tool result is a good argument for answering control messages off the work queue entirely.