Five MCP TypeScript SDK security advisories landed on the same day (2026-10-20). Most of them need someone malicious in the loop. This one doesn't — and that's what makes it the scariest of the bunch.
The bug
@modelcontextprotocol/server versions 2.0.0 through 2.2.0 (fixed in 2.3.0) have a flaw in how they route responses when multiple clients share one transport / McpServer instance over HTTP.
Here's the mechanism, stripped down:
- Every MCP client numbers its own JSON-RPC requests starting from 0. That's per-client, and it's fine on its own.
- The server, however, tracked in-flight requests by the bare request id — one shared table for everybody.
- So client A sends request
"0", client B sends request"0", and when the tool results come back, the lookup by bare id hands one client's tool result to the other client.
No attacker. No crafted payload. Just two normal clients making normal calls at overlapping times, and suddenly tenant A's tool output shows up in tenant B's session.
The part that stings: the official README's hono / express / fastify / node adapter examples used exactly the unsafe pattern — a single shared server instance across HTTP requests. Copy-paste the docs, get the leak for free.
What I built
mcp-idprobe — a small, dependency-free Python tool that proves whether a running MCP server has this leak pattern. It:
- Opens two independent sessions against your Streamable HTTP endpoint (
initialize→notifications/initialized). -
Interleaves
tools/callrequests from both clients, each numbering its own requests from"0"— the same way real clients do. - Plants a unique secret in every request's arguments.
- Collects each session's responses and attributes them by secret. If client B ever receives client A's secret, that's a crossed response — VULNERABLE, with the crossed response saved as evidence. Otherwise, PASS.
pip install mcp-idprobe
mcp-idprobe --url http://127.0.0.1:8000/mcp --out-dir ./evidence
Real output from a vulnerable test server:
[runtime probe]
target: http://127.0.0.1:8000/mcp
verdict: VULNERABLE
detail: 2 crossed response(s): a client received another client's tool result
-- CVE-2026-111770 leak pattern confirmed.
LEAK: session-A -> session-B (response id '0', secret 'idprobe-A-38fb4c28-r0')
LEAK: session-A -> session-B (response id '1', secret 'idprobe-A-21572f4e-r1')
It also ships a heuristic static scan for the code shapes behind the CVE — a module-scope new McpServer(...) reused across HTTP handlers, a shared StreamableHTTPServerTransport, pending-request tables keyed by bare id with no session scoping, and @modelcontextprotocol/server pinned to 2.0.0–2.2.0 in package.json:
mcp-idprobe --scan ./my-mcp-server/src --out-dir ./evidence
Combine both: mcp-idprobe --url ... --scan ... --out-dir ./evidence gives you evidence.json (machine-readable) plus summary.txt (human-readable), with exit code 2 when the pattern is found — CI-friendly.
How I made the tests deterministic
Cross-tenant races are timing-dependent by nature, which is terrible for a test suite. The repo ships two localhost fixtures: a vulnerable server (one shared pending dict keyed by bare id — the CVE pattern) whose worker completes jobs after a fixed delay, so both clients' colliding registrations always land before routing happens; and a fixed server with per-session id namespaces (the 2.3.0 behavior). The probe must flag the first every single run and pass the second. 30 tests, stdlib only, all green.
How this differs from my mcp-audit
I also maintain mcp-audit, which is a local static attack-surface scanner — it reads your code and config on disk and lists where an MCP deployment could be attacked. mcp-idprobe is the other side of that coin: a runtime probe for one specific, real CVE. It opens real sessions, generates real cross-tenant traffic, and gives you a deterministic pass/fail with captured evidence. They're complementary — audit to find where to look, probe to prove whether the leak is actually there.
Honest scope
This proves the pattern exists; it's not an exploit. It only ever reads responses delivered to its own two sessions, and it's meant for servers you own or are authorized to test (localhost is the intended target). The static scan is heuristic — it flags suspicious code shapes, and a flagged file deserves a human look plus a live probe run before anyone calls it vulnerable.
The fix, if you find it
Upgrade @modelcontextprotocol/server to >= 2.3.0, and regardless of version: never share one McpServer/transport instance across HTTP clients. Create a fresh server + transport per request or session, and namespace in-flight request ids per session.
Repo: https://github.com/hahahahahahahahah6/mcp-idprobe — MIT, stdlib-only, PRs welcome.









