Files
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

68 lines
3.2 KiB
Markdown

# standalone-get
> **Legacy mechanism (2025 handshake era).** The 2026-07-28 protocol delivers
> server-initiated notifications over a `subscriptions/listen` stream instead
> of the standalone GET stream. TODO(maxisbey): unify once
> `subscriptions/listen` lands
> ([#2901](https://github.com/modelcontextprotocol/python-sdk/issues/2901)).
Server-initiated `notifications/resources/list_changed` delivered over the
**standalone GET SSE stream** of a sessionful Streamable-HTTP connection. The
`add_note` tool mutates the resource list and emits the notification with no
related request; the client's `message_handler` receives it on the GET stream,
awaits it on an `anyio.Event`, then re-lists to observe the change.
## Run it
```bash
# HTTP only — the standalone GET stream is a Streamable-HTTP feature. The
# client self-hosts the server on a free port, runs, then tears it down.
uv run python -m stories.standalone_get.client --http --legacy
# same, against the lowlevel-API server variant
uv run python -m stories.standalone_get.client --http --legacy --server server_lowlevel
# against a server you run yourself
uv run python -m stories.standalone_get.server --http --port 8000 &
SERVER_PID=$!
uv run python -m stories.standalone_get.client --http http://127.0.0.1:8000/mcp --legacy
kill "$SERVER_PID"
```
## What to look at
- **`client.py``Client(target, mode=mode, message_handler=on_message)`.**
Unsolicited notifications have no typed callback, so the catch-all
`message_handler` is wired at construction — it (and the `anyio.Event` it
sets) must exist *before* the connection does. The notification is not
guaranteed to arrive before the tool result (different streams), so the body
`await`s the event, bounded by `anyio.fail_after(5)`.
- **`server.py``await ctx.session.send_resource_list_changed()`.**
`MCPServer.add_resource` does **not** auto-emit (unlike the TypeScript SDK's
`registerResource`); the explicit call is the teaching point. Because
`send_*_list_changed()` carries no `related_request_id`, the only route to the
client is the standalone GET stream.
## Caveats
- DNS-rebinding protection is disabled via `transport_security=NO_DNS_REBIND`
because the in-process httpx client sends no `Origin` header. Drop the kwarg
for a real deployment.
- Neither `MCPServer` nor lowlevel `Server` auto-advertises
`resources.listChanged: true` in capabilities, and `MCPServer` exposes no knob
to set it. A spec-conformant client that gates on the capability flag would
skip the handler.
- `ctx.session.*` is the interim path; a later release will shorten it.
- Tool-triggered, not timer-driven, for harness determinism. "Server pushes on
its own schedule" is not demonstrated.
## Spec
[List Changed Notification](https://modelcontextprotocol.io/specification/2025-11-25/server/resources#list-changed-notification),
[Streamable HTTP — Listening for Messages](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#listening-for-messages-from-the-server)
## See also
`stickynotes/` (list_changed inside a feature capstone), `sse_polling/` (the
other GET-stream story — resumability), `json_response/` (what happens when the
server can't stream).