modelcontextprotocol / modelcontextprotocol/typescript-sdk

Zombie Task Collision in StreamableHTTPServerTransport

Open
#1,186 3 comments 0 reactions 1 assignee View on GitHub

@felixweinberger is already working on this.

Since Nov 28, 2025.

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

Description

Describe the bug
The StreamableHTTPServerTransport in the MCP SDK contains a critical architectural flaw that causes Zombie Task Collisions.

When a client disconnects while a long-running task is active, the transport does not cancel the task nor clean up its Request ID mapping properly. If a new client connection (or the same client reconnecting) reuses the same Request ID (e.g., id: 1), the old "Zombie Task" can complete and send its result to the new connection. This overwrites the mapping and causes the legitimate new request to fail or hang indefinitely.

To Reproduce
Steps to reproduce the behavior:

  1. Start an MCP server with a tool that simulates a delay (e.g., 5 seconds).
  2. Client A connects and sends a call_tool request with id: 1.
  3. Client A immediately disconnects (closes connection) before the tool completes.
  4. Wait 1 second (the tool is still running as a "Zombie Task").
  5. Client B connects and sends a different request, but also uses id: 1.
  6. Observe that when the Zombie Task finishes, it sends a response to Client B (unexpectedly), and Client B's actual request subsequently hangs or times out because its internal mapping was deleted by the Zombie Task's completion.

Can check on this repo: https://github.com/Miaoxingren/modelcontextprotocol-issue

Expected behavior

  • When a connection closes, all associated Request ID mappings (_requestToStreamMapping) should be immediately invalidated or cleaned up for that specific session.
  • Ideally, running tasks associated with the closed connection should be cancelled.
  • A new connection reusing id: 1 should be completely isolated from any previous id: 1 used in a different (now closed) connection.

Logs
If you enable debug logging in StreamableHTTPServerTransport, you can observe:

  1. _requestToStreamMapping still containing id: 1 after Client A disconnects.
  2. The Zombie Task attempting to write to the stream associated with Client B.

Additional context

  • Root Cause: Shared mutable state in StreamableHTTPServerTransport. The _requestToStreamMapping map is global and assumes Request IDs are unique across all time/connections, which is false for JSON-RPC clients (which reset IDs on reconnect).
  • Affected Version: @modelcontextprotocol/sdk <= 1.0.1 (and current main).
  • Impact: Production environments see random timeouts/hangs, especially with frequent reconnects or page refreshes.

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.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.