Files
modelcontextprotocol--pytho…/examples/stories/subscriptions
wehub-resource-sync 49b9bb6724
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) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 12:10:27 +08:00
..

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

# 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

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).