Skip to content

Commit ce40fb8

Browse files
committed
fix(transport): gate the session id on a per-request protocol version
A request carrying `_meta.protocolVersion` replaces the negotiated version for itself and everything after it, but the id was not reconsulted. A legacy session that moved to 2026-07-28 that way kept echoing its Mcp-Session-Id, starting with the very request that moved it. Gate above the POST built from that request, so it is the first one to go without the id rather than the last one to carry it. The cleanup record is built earlier from the id the server sent, so the shutdown DELETE is unaffected.
1 parent d36ce25 commit ce40fb8

1 file changed

Lines changed: 7 additions & 2 deletions

File tree

‎crates/rmcp/src/transport/streamable_http_client.rs‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,8 @@ fn request_version_headers(
9696
/// [`ProtocolVersion::STANDARD_HEADERS`], so at that version an `Mcp-Session-Id` and a GET
9797
/// stream are both artifacts of a pre-`2026-07-28` server shape. A legacy-shaped handshake
9898
/// can still answer with a session id while negotiating that version; the id is dropped
99-
/// rather than echoed, which also leaves every `spawn_common_stream` call site — all three
100-
/// of which are guarded on a session id being present — with no stream to open.
99+
/// rather than echoed, which also leaves every `spawn_common_stream` call site — each
100+
/// guarded on a session id being present — with no stream to open.
101101
///
102102
/// Dropping is deliberate rather than fatal: refusing to start would break clients against
103103
/// servers that work today, and the receive-side enforcement added for SEP-2260 still
@@ -1597,6 +1597,11 @@ impl<C: StreamableHttpClient> Worker for StreamableHttpClientWorker<C> {
15971597
);
15981598
if inline_version.is_some() {
15991599
negotiated_version = request_version.clone();
1600+
// A per-request version replaces the negotiated one, so it can move
1601+
// the transport onto a version that has no sessions. Gate here, above
1602+
// the POST built below, so that request is the first one to go without
1603+
// the id rather than the last one to carry it.
1604+
session_id = session_id_for_version(session_id, &negotiated_version);
16001605
if let Ok(value) = HeaderValue::from_str(request_version.as_str()) {
16011606
protocol_headers
16021607
.insert(HeaderName::from_static("mcp-protocol-version"), value);

0 commit comments

Comments
 (0)