Skip to content

[v2] StreamableHTTPClientTransport standby GET/SSE stream reconnects forever when the server gracefully idle-closes it ( maxRetries  never trips) #2682

Description

@Erli-ms

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):

  1. _handleSseStream handles a graceful server-side disconnect by calling
    this._scheduleReconnection(options, 0) — the attempt count is hardcoded 0.
  2. _scheduleReconnection only advances the counter inside the failed-attempt
    .catch() (this._scheduleReconnection(options, attemptCount + 1)).
  3. So a successful reconnect that the server then idle-closes again re-enters
    _handleSseStream and schedules with 0 once more.
  4. attemptCount therefore never grows past 0 across repeated idle-close/reopen
    cycles; maxRetries and reconnectionDelayGrowFactor are effectively dead
    for this case, and the transport loops at initialReconnectionDelay (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

Metadata

Metadata

Assignees

No one assigned

    Labels

    v1Issues / PRs related to v1.xv2Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions