Skip to content

SEP-2260 follow-up: skip session tracking and the standalone GET stream at protocol >= 2026-07-28 #1108

Description

@gocamille

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 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:

  • Legacy startup (~L969). A server that answers legacy initialize with both a session id and protocolVersion: 2026-07-28 gets 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.
  • 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    2026-07-28P2Medium: important but non-blocking improvementT-transportTransport layer changesenhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions