subscriptions/listen: change notifications in MCP The 2026-07-28 Model Context Protocol (MCP) specification makes `subscriptions/listen` the protocol's one deliberately long-lived request in an otherwise stateless design, replacing `resources/subscribe`, `resources/unsubscribe` and the old HTTP GET endpoint with a single stream that carries change notifications until cancelled. Under the spec, a server must send `notifications/subscriptions/acknowledged` as the first message, must not send any notification type the client did not explicitly request, and must tag every message with `io.modelcontextprotocol/subscriptionId`; notifications are signals only, so clients re-read or re-list, and nothing replays missed events after a reconnect. The author, writing part 17 of a series on MCP server behavior, advises developers to plan for fan-out because a stream is pinned to one instance, so every instance must hear about every change. subscriptions/listen: change notifications in MCP In the 2026-07-28 Model Context Protocol MCP specification, every request but one is stateless and can be served by any server instance. subscriptions/listen is the deliberate exception: it opens a stream that carries change notifications until it’s cancelled. I like how much this one method replaces: resources/subscribe , resources/unsubscribe and the old HTTP GET endpoint. This is part 17 of my series on what an MCP server does under the 2026-07-28 specification, after progress, cancellation and timeouts in part 16 https://pournasserian.com/writing/mcp-2026-progress-and-cancellation . The facts are as I read them in October 2026. In brief 1. subscriptions/listen is the one deliberately long-lived request in an otherwise stateless protocol. It holds a stream open for change notifications, and its response only arrives when the stream closes cleanly. 2. The server acknowledges first and sends only what was asked for. The acknowledgement echoes the types it will honour, and no notification on that subscription may come before it. 3. Notifications are signals, not payloads. Each carries the subscription’s ID, and the client reads or lists again. 4. Nothing replays missed events. After a reconnect, a client subscribes again and re-fetches. 5. My advice: plan for fan-out. A stream is pinned to one instance, so every instance has to hear about every change. Opening a stream The client sends subscriptions/listen , a JSON-RPC 2.0 request, with a notifications filter listing the event types it wants. The specification’s Subscriptions page https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/subscriptions says the server MUST NOT send any type the client didn’t explicitly request. { "jsonrpc": "2.0", "id": 1, "method": "subscriptions/listen", "params": { " meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientCapabilities": {} }, "notifications": { "toolsListChanged": true, "resourceSubscriptions": "file:///project/config.json" } } } | Filter field | Delivers | |---|---| | toolsListChanged: true | notifications/tools/list changed | | promptsListChanged: true | notifications/prompts/list changed | | resourcesListChanged: true | notifications/resources/list changed | | resourceSubscriptions: uris | notifications/resources/updated for those Uniform Resource Identifiers URIs | | taskIds: ids Tasks extension | notifications/tasks with the full task state | Every field is optional; leaving one out means not subscribing to that type. Acknowledgement first The server MUST send notifications/subscriptions/acknowledged as the first message on the subscription, and MUST NOT send any notification before it. It echoes only the subset the server will honour, and clients SHOULD compare it with what they asked for. Every message is tagged Every message on the stream carries io.modelcontextprotocol/subscriptionId in meta . Its value is the JSON-RPC id of the listen request, 1 in the example above: { "jsonrpc": "2.0", "method": "notifications/resources/updated", "params": { " meta": { "io.modelcontextprotocol/subscriptionId": 1 }, "uri": "file:///project/config.json" } } Notifications are signals, not payloads. On resources/updated the client calls resources/read part 10 https://pournasserian.com/writing/mcp-2026-resources covers resources , and on a list changed it calls the matching /list again. Several streams, endings and reconnects A client MAY hold several subscriptions at once, for example one for tool-list changes and one per group of resources. On stdio they share the one channel, so clients MUST use subscriptionId to tell each subscription’s messages apart part 7 https://pournasserian.com/writing/mcp-2026-transports covers the transports . The acknowledgement-first rule is per subscription, so another subscription’s messages may arrive before a given acknowledgement. Ending a subscription | Who ends it | How | |---|---| | The client | Closes the Server-Sent Events SSE stream on HTTP, or sends notifications/cancelled with the listen request’s ID on stdio | | The server, for example at shutdown | SHOULD respond to the original listen request with a completion result resultType: "complete" plus the subscription ID , then close the stream | | The transport | An HTTP timeout, a dropped connection or the stdio process exiting, with no response | A graceful response says the subscription closed cleanly; a stream that ends without one is an unexpected disconnect, which the client MAY treat as a cue to reconnect. The cancellation page https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/cancellation also says a server MUST send notifications/cancelled for the listen request when it tears a subscription down. My advice for clients: treat either signal as a clean close. Reconnection and missed events - On stdio, the server holds no subscription state across reconnects, so the client MUST send subscriptions/listen again. - On HTTP, SSE resumability is gone part 2 https://pournasserian.com/writing/mcp-2026-what-changed , so nothing replays missed events. After any reconnect, subscribe again and re-fetch whatever you cache: assume something changed during the gap. - For clients that don’t subscribe at all, the ttlMs hints from part 6 https://pournasserian.com/writing/mcp-2026-caching-and-pagination are the safety net. Clients reopen closed streams within limits. As of October 2026, Claude Code https://code.claude.com/docs/en/mcp sets two limits. If a stream closes again within 10 seconds, Claude Code reopens it up to three times, then stops for that connection. If it stayed open longer before closing, as streams to serverless hosts commonly do, Claude Code waits about six hours after five reopens in an hour, keeping the server’s last fetched lists until then. Notifying a change from Python With the official Python software development kit SDK , mcp 2.3.0, MCPServer handles the acknowledgement, the tags and the filtering for you. Your part is to say what changed. Here a tool changes a resource and says so: python from mcp.server.mcpserver import Context, MCPServer mcp = MCPServer "acme-ops" CONFIG = {"region": "eu-west"} @mcp.resource "config://app" def app config - str: return f"region={CONFIG 'region' }" @mcp.tool async def set region region: str, ctx: Context - str: """Move the app to another region.""" CONFIG "region" = region await ctx.notify resource updated "config://app" return f"region set to {region}" ctx.notify tools changed does the same for the tool list. The in-memory client opens a stream, makes the change and reads again after the signal: python import anyio from mcp import Client async def main : async with Client mcp as client: async with client.listen resource subscriptions= "config://app" as sub: honored = sub.honored.model dump by alias=True, exclude none=True print sub.subscription id, honored await client.call tool "set region", {"region": "us-east"} event = await anext sub contents = await client.read resource event.uri .contents print event, contents 0 .text anyio.run main It prints: listen-1 {'resourceSubscriptions': 'config://app' } ResourceUpdated uri='config://app' region=us-east What the SDK does here: - Entering client.listen ... waits for the acknowledgement, and sub.honored is the acknowledged filter. The client’s string ID, listen-1 , is the subscriptionId on every message. - MCPServer honours every requested type and URI, even a URI that doesn’t exist nothing ever arrives for it . The SDK’s subscriptions guide https://py.sdk.modelcontextprotocol.io/handlers/subscriptions/ shows a middleware that refuses URIs the caller may not read. - A graceful close ends iteration over sub , so an async for loop finishes; an abrupt drop raises SubscriptionLost , your cue to listen again. In my test, the SDK also treated a server’s notifications/cancelled as a drop, where my advice above would close cleanly. One stream, many instances Unlike other requests, a listen stream is pinned to the instance that accepted it, and that instance has to hear about changes made elsewhere, here through a publish/subscribe pub/sub bus: In the Python SDK, each instance reaches that bus through a SubscriptionBus , passed as MCPServer subscriptions=... . The default, InMemorySubscriptionBus , reaches only its own process: in my test, a change on one instance left a stream on another silent. Your own SubscriptionBus needs two methods, publish and subscribe , and carries typed events, not JSON-RPC, so the SDK still tags and filters: python from collections.abc import Callable from mcp.server.mcpserver import MCPServer from mcp.server.subscriptions import InMemorySubscriptionBus, ServerEvent class BrokerBus: """One instance's SubscriptionBus, over a pub/sub broker all instances share.""" def init self, broker - None: self. broker = broker self. local = InMemorySubscriptionBus the streams this instance holds broker.on event self. local.publish events published by any instance async def publish self, event: ServerEvent - None: await self. broker.publish event def subscribe self, listener: Callable ServerEvent , None - Callable , None : return self. local.subscribe listener def build instance broker - MCPServer: return MCPServer "acme-ops", subscriptions=BrokerBus broker broker stands for your pub/sub client; with a real one you also encode each event and run, on every instance, a reader task that hands arriving events to the local bus. Against an in-process stand-in, a change on one instance reached a stream on another. My checklist for servers The rules above are the spec’s; what follows is my own advice. - If you run the stream yourself, without an SDK, register the subscription, send the acknowledgement, and only then start delivering, or you’ll break the ordering rule under concurrency. Filter outgoing notifications against the acknowledged set, not the requested set. - With ping gone, proxies with idle timeouts will cut quiet streams: size the timeouts for it, and expect clients to reconnect and list again. - Decide what happens when a bearer token expires mid-stream. A common policy closes the stream at expiry, so the client reconnects with a fresh token. Method and caveats - Built from my guide’s chapter on subscriptions, written against the 2026-07-28 specification. Claude Code’s behaviour is as its documentation described it on 8 October 2026. - Both Python samples ran in memory against mcp 2.3.0 on 9 October 2026; the output shown is the first sample’s. The other SDK notes come from the package’s source and short probes. - Where Claude Code’s documentation is more precise than my notes after three quick reopens it stops for that connection , this article follows the documentation.