modelcontextprotocol / modelcontextprotocol/typescript-sdk
[v2] Legacy Streamable HTTP reconnects continue after the client request times out
Nobody has claimed this yet.
- 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:
-
A default v2 Client negotiates protocol version 2025-11-25.
-
The client sends a request with a short timeout.
-
The POST response starts an SSE stream, provides an event ID and retry interval, and closes.
-
The transport begins resuming the stream through GET requests carrying Last-Event-ID.
-
The request timeout expires.
-
The request promise rejects with Request timed out, and the client sends notifications/cancelled.
-
Despite that settlement, the transport continues issuing resumed GET requests.
-
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:
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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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