You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(client): handle connection close during the version negotiation probe
A stdio server that exits on the unrecognized server/discover probe (the
rmcp shape: terminate on any pre-initialize request) connected fine under
mode 'legacy' but hard-failed under mode 'auto'. Classify that close as a
legacy signal, symmetric with the stdio probe-timeout rule.
StdioClientTransport owns the restart: an internal capability respawns the
server after the child died, with generation-scoped child event handlers,
the read buffer cleared on child exit, and refusal after a local close();
with stderr 'pipe' the aggregate stream spans restarts and ends exactly
once, in close(). The probe layer checks for the capability and asks the
transport to restart before running the plain initialize fallback; a
transport without it, or whose restart refuses or fails, rejects with the
existing typed negotiation error. On HTTP a mid-probe close keeps
surfacing as the same typed connect error as any probe transport failure.
Handle a connection that closes during the `server/discover` version-negotiation probe.
7
+
8
+
On stdio, a server process that exits on the unrecognized probe is a legacy signal — symmetric with the stdio probe-timeout rule. Servers built on SDKs that terminate on any pre-`initialize` request (the official Rust SDK, rmcp, is the prominent example) previously connected fine under `mode: 'legacy'` but hard-failed under `mode: 'auto'`; `StdioClientTransport` now restarts itself (respawning the server from its retained parameters) and the client completes the plain `initialize` handshake there — the respawned process sees bytes identical to a plain legacy connect. A transport that was closed locally, or that cannot restart, rejects with a typed negotiation error instead: a deliberately terminated server is never resurrected. On HTTP-class transports a mid-probe close keeps surfacing as the same typed connect error as any other probe transport failure — an ambiguous drop is never era evidence.
9
+
10
+
`StdioClientTransport` restart mechanics: child events are generation-scoped — `start()` opens a generation and `close()` retires it, so a dead child's late events can touch neither a successor nor a closed transport — the read buffer is cleared on child exit, and with `stderr: 'pipe'` the stream spans restarts: it ends exactly once, on `close()`, not on a child's exit, and a `start()` after `close()` begins a fresh stream.
Copy file name to clipboardExpand all lines: docs/protocol-versions.md
+2Lines changed: 2 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -101,6 +101,8 @@ const cli = new Client(
101
101
102
102
A probe timeout is transport-aware. On stdio a silent server is a legacy server, so `connect()` falls back to `initialize` on the same stream; on HTTP silence is an outage, so `connect()` rejects with `SdkError(RequestTimeout)` instead of misreporting a dead server as legacy. One browser exception: an opaque CORS `TypeError` during the probe falls back to the legacy era, because deployed 2025 servers commonly have allow-lists that predate the 2026 headers.
103
103
104
+
A connection that closes mid-probe follows the same transport split. On stdio a server process that exits on the unrecognized probe is a legacy server — stdio servers built on SDKs that terminate on any pre-`initialize` request behave exactly this way (the official Rust SDK, rmcp, is the prominent example) — so the transport restarts itself, respawning the server, and `connect()` falls back to `initialize` there; the respawned process sees the same bytes a `mode: 'legacy'` connect would have sent. On HTTP a mid-probe close is ambiguous — a proxy drop or a crash, never era evidence — so `connect()` rejects with the same typed `SdkError(EraNegotiationFailed)` as any probe transport failure. A transport closed locally during the probe is never restarted.
105
+
104
106
The client's `supportedProtocolVersions` option shapes the probe: its 2026+ entries are the versions the probe offers, and the legacy fallback stays available only while the list keeps a pre-2026 entry. A list with no pre-2026 entry removes the fallback — against a 2025-only server, `connect()` rejects with `SdkError(EraNegotiationFailed)`.
0 commit comments