Skip to content

Commit 00a7014

Browse files
authored
Serve the 2026-07-28 protocol over stdio: decide the era from the opening request (#3152)
1 parent 837ef90 commit 00a7014

6 files changed

Lines changed: 335 additions & 253 deletions

File tree

docs/handlers/subscriptions.md

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,6 @@ Two things the stream is *not*:
5252
handler on the low-level `Server` and acknowledge a smaller filter than the client asked
5353
for; the acknowledgment is how the client learns what it actually got.
5454

55-
!!! warning "Streamable HTTP only, for now"
56-
`subscriptions/listen` needs a transport that can stream a request's response, which today
57-
means streamable HTTP. Over stdio a 2026-07-28 connection rejects the method with
58-
METHOD_NOT_FOUND, even though `server/discover` advertises the subscription capabilities
59-
there. Serving it over stdio is planned; the open-stream semantics for that transport are
60-
not built yet.
61-
6255
## The client end
6356

6457
Here is a client on the other side of that stream, following the board:

docs/whats-new.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ That file is the pitch in one place: one server, one `Resolve`-backed tool, and
191191

192192
### Change notifications become one stream
193193

194-
At 2026-07-28 the standalone HTTP GET stream and `resources/subscribe` are replaced by `subscriptions/listen`: the client opens one long-lived stream and names the notification kinds it wants. `MCPServer` serves it out of the box; you publish with `await ctx.notify_resource_updated(uri)` (and `notify_tools_changed()`, and so on), and multi-replica deployments plug in a shared `SubscriptionBus`. On the client (since `2.0.0b2`), `async with client.listen(...)` opens the stream: the filter goes in as keyword arguments, typed change events come back, and `sub.honored` is the subset the server agreed to deliver. One honest caveat: over stdio the server does not serve the stream yet.
194+
At 2026-07-28 the standalone HTTP GET stream and `resources/subscribe` are replaced by `subscriptions/listen`: the client opens one long-lived stream and names the notification kinds it wants. `MCPServer` serves it out of the box; you publish with `await ctx.notify_resource_updated(uri)` (and `notify_tools_changed()`, and so on), and multi-replica deployments plug in a shared `SubscriptionBus`. On the client (since `2.0.0b2`), `async with client.listen(...)` opens the stream: the filter goes in as keyword arguments, typed change events come back, and `sub.honored` is the subset the server agreed to deliver.
195195

196196
**[Subscriptions](handlers/subscriptions.md)** covers publishing and serving, **[its Clients twin](client/subscriptions.md)** the watching end, and **[Deploy & scale](run/deploy.md)** the bus.
197197

src/mcp/server/lowlevel/server.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -703,9 +703,9 @@ async def run(
703703
704704
Thin wrapper over `serve_dual_era_loop`: enters the server lifespan,
705705
then drives the loop, serving the legacy handshake era and the modern
706-
per-request-envelope era (the first era-distinctive message to succeed
707-
locks the connection). Transports with their own lifespan owner (the
708-
streamable-HTTP manager) call `serve_loop` directly instead.
706+
per-request-envelope era (the client's first request decides which).
707+
Transports with their own lifespan owner (the streamable-HTTP manager)
708+
call `serve_loop` directly instead.
709709
"""
710710
async with self.lifespan(self) as lifespan_context:
711711
await serve_dual_era_loop(

0 commit comments

Comments
 (0)