Harden the dual-era stream loop's era-lock and rejection semantics (#3040)

This commit is contained in:
Max 2026-07-01 17:07:12 +01:00 • committed by GitHub
commit 080f2a869d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 652 additions and 125 deletions

View file

@ -46,6 +46,12 @@ Two more things the stream is *not*:
* **It is not a replay log.** A dropped stream is gone; events published while nobody was connected are not queued. The client's contract is to re-listen and re-fetch what it cares about.
* **It is not the 2025 path.** Clients on earlier protocol versions that called `resources/subscribe` are served by `ctx.session.send_resource_updated(uri)` — the `notify_*` methods reach `subscriptions/listen` streams only.
!!! warning "Streamable HTTP only, for now"
`subscriptions/listen` is served on the streamable-HTTP transport. Over stdio (and other
stream-pair transports) a 2026-07-28 connection rejects it with METHOD_NOT_FOUND — the
open-stream semantics haven't been built for that transport yet, even though
`server/discover` still advertises the subscription capabilities there.
## One process is the default. More takes a bus
Publishes travel from your handler to the open streams over a `SubscriptionBus`. The default is in-memory: one process, every stream in it. That is the right answer until you run replicas behind a load balancer — then a client's stream is pinned to one replica, and a publish on another replica has to reach it.

View file

@ -26,9 +26,9 @@ Either way you come out connected, and `client.protocol_version` tells you which
That is the whole feature. One `Client`, any era of server, no branching in your code.
!!! info
`MCPServer` answers `server/discover`, so against your own in-memory server `auto` always lands
on `2026-07-28`. The fallback only ever fires against a real pre-2026 server, which is exactly
when you want it to.
`MCPServer` answers `server/discover` on every transport — in-memory, stdio, streamable
HTTP — so against your own server `auto` always lands on `2026-07-28`. The fallback only
ever fires against a real pre-2026 server, which is exactly when you want it to.
## `mode="legacy"`

View file

@ -411,6 +411,8 @@ On the high-level `Client`, `client.server_capabilities`, `client.server_info`,
In v1, connecting to a server always performed the `initialize` handshake. In v2, `Client` defaults to `mode='auto'`: on enter it probes `server/discover` and, if the server doesn't support it, falls back to the `initialize` handshake. Pass `mode='legacy'` to force the initialize handshake and reproduce v1's byte-identical pre-2026 behavior, or pass a modern protocol-version string (e.g. `mode='2026-07-28'`) to pin a version without probing.
The probe is transport-independent: v2 servers answer it over stdio (and any other stream-pair transport) as well as streamable HTTP, so `mode='auto'` lands on `2026-07-28` against a v2 server on every transport. If your stdio workflow relies on server-initiated requests (sampling, push elicitation), pass `mode='legacy'` — a 2026-07-28 connection refuses them on every transport.
For an in-process `Client(server)` (where `server` is a `Server` or `MCPServer` instance), `mode='auto'` dispatches calls directly through `DirectDispatcher` with no JSON-RPC framing. Pass `mode='legacy'` if you need the in-memory JSON-RPC transport that v1 used.
`Client.send_ping()` is deprecated (ping is removed in 2026-07-28); pin `mode='legacy'` if you need it.