Title: Serve the 2026-07-28 era over stdio and other stream-pair transports by maxisbey · Pull Request #3038 · modelcontextprotocol/python-sdk · GitHub
Open Graph Title: Serve the 2026-07-28 era over stdio and other stream-pair transports by maxisbey · Pull Request #3038 · modelcontextprotocol/python-sdk
X Title: Serve the 2026-07-28 era over stdio and other stream-pair transports by maxisbey · Pull Request #3038 · modelcontextprotocol/python-sdk
Description: Motivation A modern client over stdio had no path to the 2026-07-28 protocol. The stream-pair loop driven by Server.run only understood the initialize handshake: a server/discover probe was rejected, so Client(mode='auto') silently fell back to the legacy era, and a pinned-2026 client failed outright. The modern per-request-envelope path existed only on the single-exchange HTTP entry. What this does Server.run now drives a new serve_dual_era_loop driver — the stream-pair counterpart of the HTTP era router. Era is a property of the connection, decided by how the client opens it; mid-stream switching is undefined, so the first era-distinctive message locks it (matching the typescript-sdk): initialize locks legacy. The connection runs the existing loop runner byte-identically for its lifetime. Modern envelope traffic afterwards is rejected with INVALID_REQUEST. The modern _meta envelope triple (or server/discover, a modern-only method) locks modern. Every request is validated by the existing transport-agnostic classify_inbound_request ladder and served single-exchange via serve_one with a born-ready per-request Connection — the same dispatch model as the modern HTTP entry, so -32022 version negotiation comes for free. A later initialize gets UNSUPPORTED_PROTOCOL_VERSION naming the supported modern versions (mirroring the HTTP modern entry). A rejected classification never locks, so a failed probe leaves the legacy handshake available. Modern stream-pair connections get a notify-only standalone outbound: server notifications ride the duplex pipe (something stateless HTTP can't offer), while server-initiated requests are refused on both the standalone and request-scoped channels, as 2026-07-28 requires. Client-side cancellation works unchanged — notifications/cancelled is consumed at the dispatcher layer, so the stdio-only cancellation path works at the modern era for free. The streamable-HTTP manager calls serve_loop directly and is untouched. Note that Server.run also serves SSE and in-memory stream pairs, so those entries are dual-era too — deliberate, since the era doctrine is transport-class-generic for duplex pipes, and SDK SSE clients always run mode='legacy'. Interop Zero client changes. The server/discover probe has always carried the envelope triple in the request body (not just the HTTP header), so released auto-negotiating clients silently upgrade to 2026-07-28 against this server. The released fallback predicate keys on the error code alone (only -32022 is special-cased), so the new rejection shapes cannot strand older clients; a test pins this. Known gap (follow-up) subscriptions/listen is rejected with METHOD_NOT_FOUND on the stream-pair modern path for now: the registered listen handler assumes the HTTP entry's stream semantics and would misbehave over a duplex pipe. Capability derivation is handler-keyed and transport-blind, so server/discover still advertises subscription capabilities — wiring listen over stdio (and reconciling that advertisement) is follow-up work. Tests Driver-level (era lock, both cross-era rejections, -32022 round-trip, bare-probe rejection, ping neutrality, listen containment, notify-over-pipe, server-initiated request refusal), client end-to-end over a real Server.run stream pair (auto, pinned-modern, and legacy modes), and a process-stdio test driving MCPServer.run("stdio") through the full probe → feature-request exchange. The pre-existing run("stdio") tests closed stdin immediately after writing the request, racing the dispatcher's EOF-time cancellation of in-flight handlers (a latent flake). They now gate stdin EOF on the responses having arrived, like a real client; the family passes a 30x stress run. Note: the conformance suite currently has no stdio scenarios (it is HTTP-only), so SDK tests carry the coverage here — worth a conformance-repo issue for stdio-modern scenarios. AI Disclaimer
Open Graph Description: Motivation A modern client over stdio had no path to the 2026-07-28 protocol. The stream-pair loop driven by Server.run only understood the initialize handshake: a server/discover probe was rejecte...
X Description: Motivation A modern client over stdio had no path to the 2026-07-28 protocol. The stream-pair loop driven by Server.run only understood the initialize handshake: a server/discover probe was rejecte...
Opengraph URL: https://github.com/modelcontextprotocol/python-sdk/pull/3038
X: @github
Domain: github.com
| route-pattern | /:user_id/:repository/pull/:id/files(.:format) |
| route-controller | pull_requests |
| route-action | files |
| fetch-nonce | v2:8819bbe0-5f3f-229d-7018-7434f1350bdf |
| current-catalog-service-hash | ae870bc5e265a340912cde392f23dad3671a0a881730ffdadd82f2f57d81641b |
| request-id | A7CE:392B7D:1FAC87A:2A3FC48:6A5BC6AF |
| html-safe-nonce | 3f2dacbda96c023f8c2a68b0d737072e4dbdf7b6a50bf9f8ba1366da0b178577 |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJBN0NFOjM5MkI3RDoxRkFDODdBOjJBM0ZDNDg6NkE1QkM2QUYiLCJ2aXNpdG9yX2lkIjoiODQzODEyNzYzMDM4MjUxNTg4NyIsInJlZ2lvbl9lZGdlIjoiaWFkIiwicmVnaW9uX3JlbmRlciI6ImlhZCJ9 |
| visitor-hmac | 01270e7922b59795ee602d59944e72b14c25ba8b83d585905c2ea3aaa0f9efe5 |
| hovercard-subject-tag | pull_request:3966801317 |
| github-keyboard-shortcuts | repository,pull-request-list,pull-request-conversation,pull-request-files-changed,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/modelcontextprotocol/python-sdk/pull/3038/files |
| twitter:image | https://avatars.githubusercontent.com/u/224885523?s=400&v=4 |
| twitter:card | summary_large_image |
| og:image | https://avatars.githubusercontent.com/u/224885523?s=400&v=4 |
| og:image:alt | Motivation A modern client over stdio had no path to the 2026-07-28 protocol. The stream-pair loop driven by Server.run only understood the initialize handshake: a server/discover probe was rejecte... |
| og:site_name | GitHub |
| og:type | object |
| hostname | github.com |
| expected-hostname | github.com |
| None | 5290d7e14309ad1e76106a9c4237bd1041517e83ea182c8ab756752cb0c6940b |
| turbo-cache-control | no-preview |
| diff-view | unified |
| go-import | github.com/modelcontextprotocol/python-sdk git https://github.com/modelcontextprotocol/python-sdk.git |
| octolytics-dimension-user_id | 182288589 |
| octolytics-dimension-user_login | modelcontextprotocol |
| octolytics-dimension-repository_id | 862584018 |
| octolytics-dimension-repository_nwo | modelcontextprotocol/python-sdk |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 862584018 |
| octolytics-dimension-repository_network_root_nwo | modelcontextprotocol/python-sdk |
| turbo-body-classes | logged-out env-production page-responsive full-width |
| disable-turbo | true |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | 9c975978430e9ad293956f2bbdaf153b1bd84a99 |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
Links:
Viewport: width=device-width