modelcontextprotocol / modelcontextprotocol/typescript-sdk

streamable HTTP server: no duplicate-in-flight request-id guard — concurrent POSTs with the same JSON-RPC id cross-wire responses

Open
#2,433 3 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug fix proposed P2 ready for work v1 v2
Dominant language
TypeScript
Stars
13.4k
Forks
2.2k
Avg merge
3d 15h
Merged PRs (30d)
4

Description

Summary

The streamable-HTTP server transport routes responses to their originating POST via a map keyed by JSON-RPC request id with no duplicate-in-flight guard. Two concurrent POSTs on one session carrying the same request id will cross-wire: the mapping for the first request is silently overwritten, so its response is delivered to (or dropped on) the wrong HTTP response stream.

This is a code-level finding on main, filed as the TypeScript sibling of modelcontextprotocol/python-sdk#3060, where the identical pattern was reproduced end-to-end and observed in production: with 12 concurrent tools/call POSTs all using id: 1 on one session, 1 request received a different request's entire response envelope and 11 hung indefinitely; a unique-id control burst was 12/12 clean. The python issue includes the full reproducer and the production incident details.

Why this matters in practice

Duplicate in-flight ids violate the spec, but a major production client — claude.ai's custom-connector MCP client sends every request with id: 1, and pools one transport session across a user's conversations — so any stateful server with slow tools and concurrent calls will hit this. The failure mode is silent delivery of one request's data to another request, which callers cannot detect without embedding sentinels.

Code path (packages/server/src/server/streamableHttp.ts, main)
  • POST handling registers each incoming request as this._requestToStreamMapping.set(message.id, streamId) (both the JSON-response branch and the SSE branch, ~L801 and ~L858). There is no check whether message.id is already mapped to a live stream — a concurrent duplicate silently overwrites the previous entry.
  • send() routes a response by requestId = message.id through _requestToStreamMapping to a stream — i.e., to whichever POST registered last.
  • The transport does guard stream-id conflicts for GET resumption ('Conflict: Stream already has an active connection', ~L530), but there is no analogous guard for duplicate request ids on POST.
Suggested fix

On POST, if message.id already exists in _requestToStreamMapping (with a live stream), reject with JSON-RPC -32600 rather than overwriting — or queue the duplicate until the first completes. Either prevents cross-request data delivery while surfacing the client's protocol violation.

The client-side id-reuse behavior is also being reported to Anthropic (claude.ai).

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Read packages/server/src/server/streamableHttp.ts, especially POST registration around ~L801 and ~L858 and the send() routing through _requestToStreamMapping. Trace how a duplicate in-flight id is handled, then ensure it cannot overwrite the existing mapping; done means concurrent duplicate-id POSTs no longer cross-wire or leave responses hanging, with the chosen JSON-RPC error or queue behavior applied consistently.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
api, backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.