Serve the 2026-07-28 protocol over stdio: decide the era from the opening request#3152
Open
maxisbey wants to merge 2 commits into
Open
Serve the 2026-07-28 protocol over stdio: decide the era from the opening request#3152maxisbey wants to merge 2 commits into
maxisbey wants to merge 2 commits into
Conversation
… opening request The stdio driver derived a connection's protocol era from whichever request finished first, so it could never host a request that does not return: subscriptions/listen was hard-refused with -32601 even though the same connection advertised list-changed capabilities, and a legacy handshake arriving during a slow 2026 request was accepted and locked the connection out from under it. Decide the era from the client's first request instead, once, before any handler runs: a request carrying the 2026-07-28 per-request envelope opens a 2026 connection, anything else (the initialize handshake) opens a 2025 one, and the deciding frame is replayed into the chosen serving loop. A conflicting later claim is refused in one place (initialize on a 2026 connection gets -32022 naming the served versions; an enveloped request on a handshake connection gets -32600). The listen refusal is deleted; the handler was always transport-agnostic.
Contributor
📚 Documentation preview
|
There was a problem hiding this comment.
All reported issues were addressed across 3 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
…ening peek - The opening-request replay now carries each frame's captured sender context, so the streams the SSE transport hands the loop keep their contextvars propagation; pinned by a test on the dual-era loop. - Bound how many frames may precede the client's first request; past the limit the loop stops looking for an opening request and serves a handshake connection, so a peer streaming pre-request frames cannot grow the buffer without bound. - Drop the stale docs admonition that stdio still rejects listen, and trim the era-decision docstring to the rule it now implements.
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.
Supersedes #3151, which grew far beyond the actual fix. This one does one thing.
Why
The stdio driver derived a connection's protocol era from whichever request completed first, so it could never host a request that doesn't return.
subscriptions/listenwas therefore hard-refused over stdio with-32601, even though the same connection advertisedtools.listChanged: true. The same rule meant a legacyinitializearriving while a slow 2026 request was in flight got accepted and locked the connection out from under that request.What changed
serve_dual_era_loopnow decides the era from the client's first request, once, before any handler runs, and replays that frame into the chosen serving loop:_metaenvelope opens a 2026 connection;initializehandshake, which doesn't exist at 2026 versions even when a client stamps the envelope on it) opens a 2025 one;initializeon a 2026 connection gets-32022naming the served versions, an enveloped request on a handshake connection gets-32600.With the era no longer waiting on completions, the
subscriptions/listenrefusal is simply deleted; the handler was always transport-agnostic and now serves over the stream pair like any other 2026 request. No public API changes: same driver signature,Server,MCPServer, the dispatcher, and the transports are untouched.Observable corners that move
-32601).initializeduring an in-flight 2026 request: refused-32022; the running request finishes. (Previously accepted, re-locking the connection.)initializegets-32022— which is the spec's own "this server is modern, use a listed version" signal — rather than running the handshake.server/discoversent without the envelope is 2025-era vocabulary: it answers-32601and a fallback handshake still lands on the same connection (previously-32602, same client outcome).Not in scope
The trailing
{"code": 0, "message": "Request cancelled"}frame the dispatcher writes after a client cancel is a separate stdio conformance item and stays as it is here.Testing
The dual-era tests in
tests/server/test_runner.pyare updated to the new rule (two that encoded "the era locks on the first request to succeed" are gone; one now drives an actualsubscriptions/listenover the stream pair through ack and graceful close; the straddle case is pinned). Full suite green with 100% branch coverage; pyright and ruff clean.One process note:
AGENTS.mdasks that new 2026 features carry a matching conformance-suite test. I don't believe the upstream conformance suite covers listen over stdio yet; flagging that here rather than skipping it.AI Disclaimer