Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/bound-standby-sse-reconnects.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@modelcontextprotocol/client': patch
---

Bound standby SSE reconnects when the server rapidly idle-closes the stream. `StreamableHTTPClientTransport` reset its reconnection attempt count to `0` every time a stream ended, so a server that gracefully idle-closes the standby GET/SSE stream immediately after every reconnect (spec-compliant behavior) kept the client reconnecting forever at `initialReconnectionDelay` — `maxRetries` never tripped, and every cycle re-ran the authenticated fetch path.

The attempt count now persists across connect-then-close cycles that make no progress: after `maxRetries` consecutive fruitless reconnects the transport stops and surfaces `onerror` ("Maximum reconnection attempts exceeded"), exactly as it already did for reconnects that fail outright. A stream counts as having made progress — and resets the count — when it delivers a message or stays open for at least `maxReconnectionDelay`, so healthy sessions whose idle standby stream is periodically closed by the server or an intermediary keep reconnecting indefinitely as before.

Also fixed in the same path: a reconnected stream that ended before any event arrived no longer drops the `Last-Event-ID` resumption token it was opened with — the next attempt resumes from the same token instead of silently starting a fresh stream.
17 changes: 12 additions & 5 deletions docs/migration/upgrade-to-v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -1511,11 +1511,18 @@ rewrite required unless noted.
no `notifications/cancelled` goes on the wire — the spec forbids cancelling
`initialize`, and v1 sent one anyway. v1 tests asserting that notification need
re-baselining.
- **Also unchanged: SSE reconnection exhaustion.** `StreamableHTTPClientTransport`'s
standalone GET-stream reconnection behavior and its exhaustion signal carry over from
v1: when retries run out, the transport emits `onerror` with a plain `Error` whose
message is `Maximum reconnection attempts (N) exceeded.` — there is no typed error
class for this condition, so monitors that match the message text keep working.
- **Changed: SSE reconnection exhaustion accounting.** The exhaustion _signal_ carries
over from v1: when retries run out, `StreamableHTTPClientTransport` emits `onerror`
with a plain `Error` whose message is `Maximum reconnection attempts (N) exceeded.` —
there is no typed error class for this condition, so monitors that match the message
text keep working. The _accounting_ changed: v1 reset the retry counter every time a
stream closed, so a server that gracefully idle-closed the standby GET stream
immediately after each reconnect kept the client reconnecting forever and
`maxRetries` never fired. v2 counts consecutive reconnects that make no progress —
no message delivered and the connection lasted less than `maxReconnectionDelay` — so
such loops now stop with the error above after `maxRetries` attempts. Streams that
deliver a message or stay open at least `maxReconnectionDelay` reset the counter and
reconnect indefinitely, exactly as in v1.
- **Also unchanged: elicitation response validation.** `elicitInput`'s local validation
of elicitation responses against `requestedSchema`, the resulting `-32602` error
message wording (`Elicitation response content does not match requested schema: …`),
Expand Down
Loading
Loading