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
PR #1055 makes the client reject SEP-2260-restricted requests that arrive on the standalone GET stream: the streamable HTTP transport marks inbound requests with an InboundStreamOrigin, the service layer turns that into a PeerRequestAssociation, and RoleClient::enforce_peer_request_association answers -32602 for Unassociated.
Rejecting is the right behavior for a server that sends such a request. But at protocol 2026-07-28 the client shouldn't have the stream in the first place. SEP-2567 removes sessions and the standalone GET endpoint at that version, so an Mcp-Session-Id and a GET stream are both artifacts of a pre-2026-07-28 server shape. Keeping them open at 2026-07-28 means maintaining an inbound path whose only purpose is to be refused.
The modern path already gets this right. A server/discover startup sets session_id to None unconditionally (streamable_http_client.rs, ~L892), and there's a debug_assert! that discover bootstrap creates no session state (~L1054). What's left is the legacy-shaped paths, where a session id survives regardless of the version that was ultimately negotiated. spawn_common_stream has three call sites and all three are guarded only by if let Some(session_id), never by negotiated_version:
Fallback initialize after server/discover fails (~L1382, on the initialized notification). Same shape, reached through ClientLifecycleMode::Auto.
Session re-establishment after an expired-session 404 (~L1218). Re-spawns on the new session id without reconsulting the version.
Work: gate session tracking and spawn_common_stream on negotiated_version < ProtocolVersion::STANDARD_HEADERS, so that at 2026-07-28 and later no session id is retained and no GET stream is opened. PeerRequestAssociation::Unassociated then becomes unreachable over streamable HTTP rather than merely rejected, and the enforcement from #1055 stays as the defense for anything that still slips through.
Two open questions worth settling in the same change:
What to do when a server returns a session id and negotiates 2026-07-28. Dropping the id silently is the least disruptive, but the server plainly expects it echoed back, so failing startup may be more honest than half-speaking its dialect.
Whether session cleanup (SessionCleanupInfo, the DELETE on shutdown) should also be skipped, or kept so a session the server did create still gets torn down.
This also bears on #863 (expose the negotiated Mcp-Session-Id): if there is deliberately no session at 2026-07-28, the accessor is a legacy-only affordance, and that's probably worth saying there either way.
Not a conformance gap — the receive-side check is a SHOULD and #1055 covers the scenarios — so this is hardening rather than a fix.
Follow-up to #1033 / PR #1055.
PR #1055 makes the client reject SEP-2260-restricted requests that arrive on the standalone GET stream: the streamable HTTP transport marks inbound requests with an
InboundStreamOrigin, the service layer turns that into aPeerRequestAssociation, andRoleClient::enforce_peer_request_associationanswers-32602forUnassociated.Rejecting is the right behavior for a server that sends such a request. But at protocol
2026-07-28the client shouldn't have the stream in the first place. SEP-2567 removes sessions and the standalone GET endpoint at that version, so anMcp-Session-Idand a GET stream are both artifacts of a pre-2026-07-28server shape. Keeping them open at2026-07-28means maintaining an inbound path whose only purpose is to be refused.The modern path already gets this right. A
server/discoverstartup setssession_idtoNoneunconditionally (streamable_http_client.rs, ~L892), and there's adebug_assert!that discover bootstrap creates no session state (~L1054). What's left is the legacy-shaped paths, where a session id survives regardless of the version that was ultimately negotiated.spawn_common_streamhas three call sites and all three are guarded only byif let Some(session_id), never bynegotiated_version:initializewith both a session id andprotocolVersion: 2026-07-28gets a GET stream. This is the shape the e2e test in feat: SEP-2260 stream-based enforcement of client receive-side request association #1055 scripts deliberately.initializeafterserver/discoverfails (~L1382, on theinitializednotification). Same shape, reached throughClientLifecycleMode::Auto.Work: gate session tracking and
spawn_common_streamonnegotiated_version < ProtocolVersion::STANDARD_HEADERS, so that at2026-07-28and later no session id is retained and no GET stream is opened.PeerRequestAssociation::Unassociatedthen becomes unreachable over streamable HTTP rather than merely rejected, and the enforcement from #1055 stays as the defense for anything that still slips through.Two open questions worth settling in the same change:
2026-07-28. Dropping the id silently is the least disruptive, but the server plainly expects it echoed back, so failing startup may be more honest than half-speaking its dialect.SessionCleanupInfo, theDELETEon shutdown) should also be skipped, or kept so a session the server did create still gets torn down.This also bears on #863 (expose the negotiated
Mcp-Session-Id): if there is deliberately no session at2026-07-28, the accessor is a legacy-only affordance, and that's probably worth saying there either way.Not a conformance gap — the receive-side check is a SHOULD and #1055 covers the scenarios — so this is hardening rather than a fix.