fix(transport): stop keeping a session and GET stream at 2026-07-28 - #1256
Open
LizunovSergey wants to merge 1 commit into
Open
Conversation
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.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1108.
What was wrong
SEP-2567 removes sessions and the standalone GET endpoint at
2026-07-28, so anMcp-Session-Idand a GET stream are both artifacts of a pre-2026-07-28server shape. The modern path already gets this right — aserver/discoverbootstrap setssession_idtoNoneunconditionally. The legacy-shaped paths did not: all threespawn_common_streamcall sites were guarded only onif let Some(session_id), never onnegotiated_version.initializewith both a session id andprotocolVersion: 2026-07-28got a GET stream.initializeafterserver/discoverfails, on theinitializednotification.How
The gate goes where a session id is adopted, not at each spawn.
session_id_for_versiondrops 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::Unassociatedunreachable 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 atracing::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.
SessionCleanupInfois still built from the id the server sent, so the shutdownDELETEstill 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 legacyinitializewith anMcp-Session-Idheader and a configurableprotocolVersion, recording every request as(HTTP method, JSON-RPC method, session header):modern_version_drops_the_session_and_opens_no_stream— at2026-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— at2025-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_streamwhilelegacy_version_keeps_the_session_and_opens_the_streamstill 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 --checkclean;cargo clippy -- -D warningsclean on the lib and on the new test.