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