Skip to main content

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:

  1. Opens two independent client sessions (initialize → notifications/initialized).
  2. Interleaves tools/call requests from both clients, each client numbering its own requests from "0" — mirroring the per-client counters from the CVE.
  3. Every request carries a per-client secret token in its arguments.
  4. 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-Id header — 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)

Source distribution for mcp-idprobe 0.1.0
File Size Uploaded Provenance
mcp_idprobe-0.1.0.tar.gz 21.3 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for mcp-idprobe 0.1.0
File Interpreter ABI Platform
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.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page