modelcontextprotocol / modelcontextprotocol/typescript-sdk
[v2] StreamableHTTPClientTransport standby GET/SSE stream reconnects forever when the server gracefully idle-closes it ( maxRetries never trips)
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?
StreamableHTTPClientTransport's standby GET/SSE stream reconnects forever
(~once per second, indefinitely) when the upstream server gracefully idle-closes
the stream — which is spec-compliant server behavior, not an error.
maxRetries (default 2) never trips because the reconnection attempt counter is
reset to 0 on every graceful disconnect instead of persisting for the stream's
lifetime.
Root cause (packages/client/src/client/streamableHttp.ts, main):
_handleSseStreamhandles a graceful server-side disconnect by calling
this._scheduleReconnection(options, 0)— the attempt count is hardcoded0._scheduleReconnectiononly advances the counter inside the failed-attempt
.catch()(this._scheduleReconnection(options, attemptCount + 1)).- So a successful reconnect that the server then idle-closes again re-enters
_handleSseStreamand schedules with0once more. attemptCounttherefore never grows past 0 across repeated idle-close/reopen
cycles;maxRetriesandreconnectionDelayGrowFactorare effectively dead
for this case, and the transport loops atinitialReconnectionDelay(1000ms)
for the life of the process.
Any upstream MCP server whose transport idle-closes the standby SSE/GET stream
triggers this. Each reconnect re-runs the authenticated-fetch path, so it also
produces continuous auth/token churn for no functional benefit.
Reproduced on @modelcontextprotocol/sdk@1.29.0; confirmed still present on main / V2 SDK.
What did you expect?
The attempt counter should persist per-stream-lifetime so that repeated
graceful-close/reopen cycles are bounded by maxRetries. After maxRetries
consecutive idle-close reconnects with no useful traffic, the transport should
stop reconnecting and surface onerror ("Maximum reconnection attempts exceeded")
rather than reconnecting indefinitely.
Equivalently: a successful-connect-then-graceful-idle-close should not be treated
as identical to a brand-new first connection for retry-accounting purposes.
Code to reproduce
Point a `StreamableHTTPClientTransport` at any MCP server that opens the standby
GET/SSE stream and then gracefully closes it when idle (spec-compliant), and
observe the reconnect loop:
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const transport = new StreamableHTTPClientTransport(new URL("https://your-mcp-server/mcp"));
const client = new Client({ name: "repro", version: "1.0.0" });
transport.onerror = (e) => console.log("onerror:", e.message);
transport.onclose = () => console.log("onclose");
await client.connect(transport);
// Leave the process idle. The server gracefully closes the standby GET/SSE
// stream when idle; the SDK reconnects at ~initialReconnectionDelay (1000ms)
// and repeats forever. `maxRetries` (default 2) is never reached and
// `onerror`/`onclose` are never called, because each successful reconnect
// resets the attempt count to 0 via _scheduleReconnection(options, 0).
SDK version
No response
Area
Client
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
Start in packages/client/src/client/streamableHttp.ts, reading _handleSseStream and _scheduleReconnection together. Run the provided reproduction against a server that gracefully idle-closes the standby GET/SSE stream. Done means repeated graceful-close/reconnect cycles advance the per-stream attempt count, stop at maxRetries, and surface the documented onerror instead of reconnecting indefinitely.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- api, networking
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100