chore: import upstream snapshot with attribution
Deploy Docs / deploy-docs (push) Failing after 1s
Conformance Tests / client-conformance (push) Failing after 3s
Conformance Tests / server-conformance (push) Failing after 1s
GitHub Actions Security Analysis / zizmor (push) Failing after 1s
CI / checks (push) Failing after 59m20s
CI / all-green (push) Has been cancelled
Deploy Docs / deploy-docs (push) Failing after 1s
Conformance Tests / client-conformance (push) Failing after 3s
Conformance Tests / server-conformance (push) Failing after 1s
GitHub Actions Security Analysis / zizmor (push) Failing after 1s
CI / checks (push) Failing after 59m20s
CI / all-green (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
# subscriptions
|
||||
|
||||
Server-originated change notifications on the 2026-07-28 protocol. A client
|
||||
opens one `subscriptions/listen` request whose response **is** the stream; the
|
||||
server publishes with `ctx.notify_resource_updated(uri)` /
|
||||
`ctx.notify_tools_changed()` and the SDK does the wire work (ack-first,
|
||||
per-stream filtering, subscription-id tagging). Replaces the handshake-era
|
||||
`resources/subscribe` + standalone-GET notification path.
|
||||
|
||||
The client opens the stream with `client.listen(...)`, edits a note it did
|
||||
not subscribe to (silence), edits the one it did (a typed `ResourceUpdated`),
|
||||
registers a tool at runtime (a typed `ToolsListChanged`, then re-lists and
|
||||
calls it), and finally leaves the `async with` block, which ends the
|
||||
subscription while the connection lives on.
|
||||
|
||||
## Run it
|
||||
|
||||
```bash
|
||||
# HTTP: the client self-hosts the server on a free port, runs, then tears it
|
||||
# down (subscriptions/listen is 2026-era only)
|
||||
uv run python -m stories.subscriptions.client --http
|
||||
# same, against the lowlevel-API server variant
|
||||
uv run python -m stories.subscriptions.client --http --server server_lowlevel
|
||||
```
|
||||
|
||||
## What to look at
|
||||
|
||||
- `client.py`: the whole subscription is one context manager,
|
||||
`async with client.listen(...) as sub`. Entering waits for the server's
|
||||
acknowledgment, so `sub.honored` is already in hand on the first line of the
|
||||
block. Events arrive as typed values from `anext(sub)`; the edit to the
|
||||
unsubscribed note never shows up, because the filter is enforced
|
||||
server-side. Leaving the block ends the subscription (over HTTP the SDK
|
||||
closes that request's response stream) and the session carries on, which the
|
||||
final `search` call proves.
|
||||
- `server.py`: publishing is one `await ctx.notify_*()` line per change; the
|
||||
filter, the tagging, and the ack ordering are the SDK's job. Publishing with
|
||||
no subscribers is a no-op.
|
||||
- `server_lowlevel.py`: the same machinery held by hand: an
|
||||
`InMemorySubscriptionBus`, handlers that `await bus.publish(...)`, and
|
||||
`ListenHandler(bus)` passed as `on_subscriptions_listen=`. A multi-replica
|
||||
deployment swaps the bus for one backed by its own pub/sub
|
||||
(`MCPServer(subscriptions=...)` on the high-level server).
|
||||
|
||||
## Caveats
|
||||
|
||||
- 2026-era only: on a 2025 connection the method does not exist (clients there
|
||||
use `resources/subscribe` and unsolicited notifications instead), so the
|
||||
story pins the modern era and has no legacy leg.
|
||||
- No replay: events published while no stream is open are not queued. The
|
||||
contract after a dropped stream is re-listen and re-fetch.
|
||||
|
||||
## Spec
|
||||
|
||||
[Subscriptions, basic utilities](https://modelcontextprotocol.io/specification/draft/basic/utilities/subscriptions)
|
||||
|
||||
## See also
|
||||
|
||||
`streaming/` (request-scoped notifications), `events/` (the events extension
|
||||
on top of this channel, deferred), and the narrative versions:
|
||||
`docs/handlers/subscriptions.md` (server) and `docs/client/subscriptions.md`
|
||||
(client).
|
||||
Reference in New Issue
Block a user