modelcontextprotocol / modelcontextprotocol/typescript-sdk

[v2] Legacy Streamable HTTP reconnects continue after the client request times out

Open
#2,615 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

v1 v2
Dominant language
TypeScript
Stars
13.4k
Forks
2.2k
Avg merge
3d 15h
Merged PRs (30d)
4

Description

What happened?

In @modelcontextprotocol/client@2.0.0, a request using the legacy 2025-11-25 Streamable HTTP path can continue issuing resumable GET requests after the request has already timed out.

The sequence is:

  1. A default v2 Client negotiates protocol version 2025-11-25.

  2. The client sends a request with a short timeout.

  3. The POST response starts an SSE stream, provides an event ID and retry interval, and closes.

  4. The transport begins resuming the stream through GET requests carrying Last-Event-ID.

  5. The request timeout expires.

  6. The request promise rejects with Request timed out, and the client sends notifications/cancelled.

  7. Despite that settlement, the transport continues issuing resumed GET requests.

  8. If a later resumed GET contains the original JSON-RPC response, the client reports Received a response for an unknown message ID.

This reproduced both against the current main source revision and the published @modelcontextprotocol/client@2.0.0 package on Node 20, 22, and 24.

The behavior appears to result from split lifecycle ownership:

  • the protocol layer owns request timeout, cancellation and response-handler cleanup;

  • the transport owns the SSE reconnect chain;

  • legacy requests do not give that reconnect chain a request-scoped abort signal.

This is separate from #2098. That issue concerns failed reconnect exhaustion leaving the standalone response channel unusable, whereas this report concerns a reconnect chain remaining active after its originating request has already settled through timeout.

Runnable published-package reproducer:

https://github.com/teamleaderleo/fieldwork/tree/main/campaigns/0004-mcp-streamable-http-reconnect/lanes/L02-reconnect-budget-terminal-state/probe

Published-package Node 20/22/24 run:

https://github.com/teamleaderleo/fieldwork/actions/runs/30479661274

Source-tree Node 20/22/24 run:

https://github.com/teamleaderleo/typescript-sdk/actions/runs/30479313714

The compliant 2026-07-28 Streamable HTTP path didn't reproduce this mechanism because it does not use the legacy GET/Last-Event-ID resumption path.

What did you expect?

When a request settles through timeout or caller cancellation, any reconnect chain owned by that request should stop.

A response racing with local cancellation should either be ignored or handled according to an explicitly documented cancellation-race policy, rather than being treated as a generic response for an unknown message ID.

I am filing this before proposing an implementation because the appropriate ownership boundary requires maintainer guidance. Possible approaches include giving legacy request streams a request-lifecycle abort signal, adding explicit per-request reconnect disposal, or providing a transport teardown hook when the protocol layer settles a request.

Code to reproduce

See the runnable published-package reproducer linked above.

SDK version

@modelcontextprotocol/client@2.0.0 Source revision also tested: modelcontextprotocol/typescript-sdk@cc4b41617ce3601b1290d67216ea0b194a3cd9ac

Area

Transports

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

Run the linked published-package reproducer and compare it with the source revision on the legacy 2025-11-25 Streamable HTTP path. Trace how timeout or caller cancellation interacts with the GET/Last-Event-ID reconnect chain and response handling; done means request-owned reconnects stop after settlement and late responses do not produce unknown-message errors.

Written by the indexing model from the issue text.

Assessment

Tech stack
node.js, typescript
Domain
api, networking
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.