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 child that exits on the unrecognized server/discover probe is a
legacy signal, symmetric with the stdio probe-timeout rule: respawn the
child once via the transport's public close()/start() and run the plain
initialize handshake there. The respawn is gated on a close-provenance
stamp (internal well-known symbol, implemented by StdioClientTransport):
a transport the caller closed mid-probe, or one that does not report
provenance, rejects with the typed connect error instead, and HTTP-class
mid-probe closes keep rejecting typed. StdioClientTransport.start()
clears the read buffer, and close() detaches the child's stderr pipe and
ends the aggregate stream once teardown completes, so a dead predecessor
cannot corrupt the next child's first frame, stderr readers of a closed
transport always see end, and a restarted life begins on a fresh stream.
Handle a connection that closes during the `server/discover` version-negotiation probe. On stdio, a child that exits on the unrecognized probe is a legacy signal (symmetric with the stdio probe-timeout rule): under `versionNegotiation: { mode: 'auto' }` the client now respawns it once via the transport's own `close()`/`start()` and completes the plain `initialize` handshake there — stdio servers built on SDKs that terminate on any pre-`initialize` request (the official Rust SDK, rmcp) previously hard-failed under `'auto'`. The respawn happens only for transports that positively report close provenance (an internal well-known-symbol stamp implemented by `StdioClientTransport`): a transport the caller closed mid-probe, or a custom transport that does not track provenance, rejects with the typed connect error instead — and on HTTP-class transports a mid-probe close keeps rejecting with the typed connect error. `StdioClientTransport.start()` now also clears the read buffer, and `close()` detaches the child's stderr pipe and ends the aggregate stream once teardown completes, so a dead predecessor cannot corrupt the next child's first frame, `stderr` readers of a closed transport always see `end`, and a restarted life begins on a fresh stream (re-read the `stderr` getter).
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 child that exits on the unrecognized probe is a legacy server — stdio servers built on SDKs that terminate on any pre-`initialize` request (the official Rust SDK, rmcp) behave exactly this way — so under `'auto'` the client respawns the child once and falls back to `initialize` there: pre-connect `stderr` subscribers should re-read that getter after `connect()` resolves, and a pre-set `onclose` observer will see the probe child's close. The respawn is limited to transports that positively report close provenance (the SDK's own `StdioClientTransport` does): a transport the caller closed during the probe, or a custom stdio-shaped transport that does not track it, is never respawned — and on HTTP a mid-probe close is ambiguous, never era evidence — `connect()` rejects with the same typed `SdkError(EraNegotiationFailed)` as any probe transport failure.
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