Skip to content

fix(transport): stop keeping a session and GET stream at 2026-07-28 - #1256

Open
LizunovSergey wants to merge 1 commit into
modelcontextprotocol:mainfrom
LizunovSergey:fix/1108-sessionless-at-modern-version
Open

fix(transport): stop keeping a session and GET stream at 2026-07-28#1256
LizunovSergey wants to merge 1 commit into
modelcontextprotocol:mainfrom
LizunovSergey:fix/1108-sessionless-at-modern-version

Conversation

@LizunovSergey

Copy link
Copy Markdown

Closes #1108.

What was wrong

SEP-2567 removes sessions and the standalone GET endpoint at 2026-07-28, so an Mcp-Session-Id and a GET stream are both artifacts of a pre-2026-07-28 server shape. The modern path already gets this right — a server/discover bootstrap sets session_id to None unconditionally. The legacy-shaped paths did not: all three spawn_common_stream call sites were guarded only on if let Some(session_id), never on negotiated_version.

  • Legacy startup. A server that answers legacy initialize with both a session id and protocolVersion: 2026-07-28 got a GET stream.
  • Fallback initialize after server/discover fails, on the initialized notification.
  • Session re-establishment after an expired-session 404, which respawned on the new id without reconsulting the version.

How

The gate goes where a session id is adopted, not at each spawn. session_id_for_version drops the id once the negotiated version has no sessions, and because all three spawn sites are already guarded on the id being present, they are left with nothing to open — no change needed at any of them. The id also stops being echoed on requests.

That makes PeerRequestAssociation::Unassociated unreachable over streamable HTTP rather than merely rejected, and the receive-side enforcement from #1055 stays as the defence for anything that still slips through.

The two open questions from the issue

Both needed a decision to write the change at all, so here is what I picked and why. Happy to move either way.

1. Server returns a session id and negotiates 2026-07-28. The issue weighed dropping it silently against failing startup. I went with dropping it plus a tracing::warn!, rather than either extreme. Failing would break clients against servers that work today over a SHOULD-level hardening, which seemed too blunt; dropping in complete silence hides a real server bug from whoever has to debug it. A warning naming the negotiated version puts the evidence in the log without changing whether the client connects.

2. Should session cleanup be skipped too? I kept it. SessionCleanupInfo is still built from the id the server sent, so the shutdown DELETE still fires and a session the server really did create gets torn down. Skipping cleanup would leak server-side state as the price of a client-side conformance fix, which felt like the wrong trade. The ordering in the diff is deliberate: the cleanup record is built first, then the id is dropped for request and stream purposes.

I did not touch #863; if there is deliberately no session at 2026-07-28, that accessor is a legacy-only affordance, but saying so belongs in that issue.

Tests

New tests/test_streamable_http_sessionless_version.rs, a scripted server that answers legacy initialize with an Mcp-Session-Id header and a configurable protocolVersion, recording every request as (HTTP method, JSON-RPC method, session header):

  • modern_version_drops_the_session_and_opens_no_stream — at 2026-07-28, no GET request arrives at all and no post-handshake POST carries the header.
  • legacy_version_keeps_the_session_and_opens_the_stream — at 2025-11-25, exactly one GET arrives, it carries the session id, and the id is echoed afterwards.

Both are witnesses, checked by mutation. Removing the three gating lines fails modern_version_drops_the_session_and_opens_no_stream while legacy_version_keeps_the_session_and_opens_the_stream still passes — so the legacy assertion is genuinely pinning the unchanged path rather than passing by accident.

Also green, unchanged: test_streamable_http_stale_session (4, and it covers the recovery path this touches), test_client_lifecycle_modes (15), test_server_discover_http (12), test_streamable_http_priming (5), test_discover_http_client_startup (3). cargo +nightly fmt --all --check clean; cargo clippy -- -D warnings clean on the lib and on the new test.

SEP-2567 removes sessions and the standalone GET endpoint at
2026-07-28, so an Mcp-Session-Id and a GET stream are both artifacts of
a pre-2026-07-28 server shape. The modern startup path already gets
this right: a server/discover bootstrap sets session_id to None
unconditionally. The legacy-shaped paths did not, and all three
spawn_common_stream call sites were guarded only on a session id being
present, never on the negotiated version:

  - legacy startup, where a server can answer legacy initialize with
    both a session id and protocolVersion 2026-07-28;
  - the fallback initialize taken after server/discover fails;
  - session re-establishment after an expired-session 404, which
    respawned on the new id without reconsulting the version.

Gating happens where a session id is adopted rather than at each
spawn: session_id_for_version drops the id once the negotiated version
has no sessions, which leaves all three call sites with nothing to open
and stops the id being echoed on requests. PeerRequestAssociation
Unassociated then becomes unreachable over streamable HTTP instead of
merely rejected, and the receive-side enforcement stays as the defence
for anything that still slips through.

A volunteered id is dropped with a warning rather than being treated as
fatal, and the id is still recorded for the shutdown DELETE, so a
session the server really did create is torn down.
@LizunovSergey
LizunovSergey requested a review from a team as a code owner September 10, 2026 22:03
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@github-actions github-actions Bot added T-dependencies Dependencies related changes T-test Testing related changes T-config Configuration file changes T-core Core library changes T-transport Transport layer changes labels Sep 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

T-config Configuration file changes T-core Core library changes T-dependencies Dependencies related changes T-test Testing related changes T-transport Transport layer changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant