mcp-idprobe
A runtime probe for MCP cross-tenant response leaking via JSON-RPC id collision — CVE-2026-111770 (@modelcontextprotocol/server 2.0.0–2.2.0, fixed in 2.3.0). Stdlib-only Python, no dependencies.
The bug, in one paragraph
The MCP TypeScript SDK numbers each client's JSON-RPC requests from 0. In the affected versions, when several clients share one transport / McpServer instance over HTTP (exactly what the official README's hono/express/fastify/node adapter examples did), the server tracks in-flight requests by the bare request id. Two clients both have a request "0" in flight; when results complete, the lookup by bare id hands one client's tool result to the other client. No attacker is needed — ordinary overlapping traffic leaks across tenants.
What this tool does
Against a running MCP server over Streamable HTTP, mcp-idprobe:
- Opens two independent client sessions (
initialize→notifications/initialized). - Interleaves
tools/callrequests from both clients, each client numbering its own requests from"0"— mirroring the per-client counters from the CVE. - Every request carries a per-client secret token in its arguments.
- Collects each session's responses and attributes them by secret. If any client receives the other client's secret →
VULNERABLE, with the crossed response saved as evidence. Otherwise →PASS.
It also ships a heuristic static scan of server source (.ts/.js/.py, plus package.json SDK version) for the anti-pattern behind the CVE: a module-scope new McpServer(...) / StreamableHTTPServerTransport reused across HTTP handlers, and pending-request tables keyed by bare id with no session scoping.
Output is a machine-readable evidence.json plus a human summary.txt.
Differentiation: mcp-idprobe vs mcp-audit
My mcp-audit repo is a local static attack-surface scanner: it reads code and configuration on disk and lists where an MCP deployment could be attacked. mcp-idprobe is the opposite side of the same coin: a runtime probe for one specific, real CVE pattern. It talks to a live server, creates real cross-tenant traffic, and gives a deterministic pass/fail with captured evidence of responses actually crossing sessions. The two are complementary — run mcp-audit to find where to look, run mcp-idprobe to prove whether the leak is really there.
Install
pip install mcp-idprobe
Or from source (no dependencies, Python ≥ 3.9):
git clone https://github.com/hahahahahahahahah6/mcp-idprobe
cd mcp-idprobe
python -m mcp_idprobe --help
Usage
# Probe a live Streamable HTTP endpoint (two sessions, interleaved)
mcp-idprobe --url http://127.0.0.1:8000/mcp --out-dir ./evidence
# ...and also statically scan the server's source tree
mcp-idprobe --url http://127.0.0.1:8000/mcp --scan ./my-mcp-server/src --out-dir ./evidence
# Static scan only
mcp-idprobe --scan ./my-mcp-server/src --out-dir ./evidence
# JSON evidence to stdout (for CI)
mcp-idprobe --url http://127.0.0.1:8000/mcp --json
Exit codes: 0 = PASS / no pattern found, 2 = VULNERABLE pattern found, 1 = error.
Options: --rounds N (interleaved rounds per session, default 2), --settle SECONDS (wait before polling, default 1.0), --tool NAME (tool to call, default echo), --timeout SECONDS.
Example output (vulnerable server):
mcp-idprobe 0.1.0 -- CVE-2026-111770 evidence report
[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')
note: 2 request(s) never reached their owner (consistent with misrouting).
Remediation: upgrade @modelcontextprotocol/server to >= 2.3.0; never share one
McpServer/transport instance across HTTP clients -- create a fresh server +
transport per request/session, and namespace in-flight request ids per session.
Honest scope
- This proves the leak pattern exists; it is not an exploit. It only reads responses addressed to its own two sessions.
- It targets Streamable HTTP servers. Responses delivered inline on the POST body (plain JSON or SSE) are parsed; servers that queue responses expose them via a GET poll with the
Mcp-Session-Idheader — if your server delivers results some other way, point the probe at it and check whether attribution holds. - The static scan is heuristic: it flags code shapes (shared instance, bare-id maps), not runtime behavior. A flagged file deserves a look, not a verdict — confirm with the live probe.
- Test against servers you own or are authorized to test. Localhost is the intended target.
How the probe stays deterministic
The bundled test fixtures (mcp_idprobe.fixtures) include a fake vulnerable server — one shared pending dict keyed by bare id, like the CVE — and a fixed server with per-session id namespaces (the 2.3.0 behavior). The vulnerable fixture's worker completes jobs after a fixed delay so both clients' colliding registrations land before routing; the probe must flag it every run, and must pass the fixed one. The test suite asserts exactly that.
Development
python -m unittest discover -s tests -t . # 30 tests, stdlib only
License
MIT — see LICENSE.
Metadata
Release files for mcp-idprobe 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| mcp_idprobe-0.1.0.tar.gz | 21.3 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| mcp_idprobe-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 39.9 kB
Release files / mcp_idprobe-0.1.0.tar.gz
| Download URL | mcp_idprobe-0.1.0.tar.gz |
|---|---|
| Size | 21.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
38c6edafd39ee18e91b44289ccb559565a9314f36c4b7cd5dbb1b3a7c11463d8
|
|
BLAKE2b-256 checksum How to use checksums |
4a9f766bce13c74f1bc511bbc3439c9e85c3519599ba6f253cf2c6f3777fd64f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|
Release files / mcp_idprobe-0.1.0-py3-none-any.whl
| Download URL | mcp_idprobe-0.1.0-py3-none-any.whl |
|---|---|
| Size | 18.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2d949ccdb6b40fe1cb891a60637537327f4e0cfd70ac7cf7b9a7a9cf04ecd91a
|
|
BLAKE2b-256 checksum How to use checksums |
34986927fe58cc3cf3e37774388a0b0554a1c71567f51a775901482d96ecabc1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|
Security for mcp-idprobe 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.