Why architecture matters here

Subscriptions matter because context freshness and efficiency pull in opposite directions, and polling resolves them badly. Consider an MCP server exposing a live document the user is editing, or a dashboard's underlying metrics, or a ticket whose status changes. If the LLM app polls every resource each turn to stay current, it pays repeated fetch cost — network, database load, serialization — for data that is usually identical to last time, and it still lags reality by up to one poll interval. If it instead reads once and caches, it goes stale and the model reasons over outdated context, which for an agent taking actions can be actively harmful. Subscriptions collapse the dilemma: the client learns of a change as soon as it happens and re-reads exactly then, so context is both fresh and cheap.

The architectural weight lands on the server, because a subscription is durable, per-subscriber state that must be reconciled with a change source and a transport. Unlike a request/response call that is born and dies in one exchange, a subscription lives across many messages and must be cleaned up precisely when the client goes away — otherwise the server accumulates dead subscriptions, each holding a watcher and consuming notification fan-out, until it degrades. And because MCP runs over transports ranging from a local stdio pipe to a streamable HTTP connection, the notification path has to be designed for the delivery guarantees each transport actually offers, including reconnection.

The second reason the architecture matters is protection against the server's own change source. A resource backed by a high-frequency feed — a log file being appended to hundreds of times a second, a row updated in a tight loop — will, if wired naively, emit a notification per change, and the client will re-read hundreds of times a second, defeating the entire efficiency goal and possibly melting the model context budget. So subscriptions are not merely 'emit on change'; they are 'emit a bounded, coalesced signal that a re-read is worthwhile', which is a meaningfully harder design that the good implementations get right and the naive ones discover in production.

There is a final semantic point that shapes the whole design: the updated notification is a hint, not a guarantee of delivery of contents. It tells the client 'something moved, come look', and the client is responsible for the actual read. That framing is deliberate — it means a dropped or duplicated notification is recoverable rather than catastrophic. A duplicate simply causes a redundant re-read that returns the same bytes; a notification lost to a transient transport glitch is caught the next time the client refreshes or reconnects. Designing the contract as an idempotent 'go re-read' signal, rather than as an authoritative delivery of new state, is what makes subscriptions robust across the unreliable transports MCP has to run over, and it keeps the server's obligations small: track interest, detect change, nudge — never guarantee that a specific byte reached a specific client at a specific instant.

Advertisement

The architecture: every piece explained

Top row: the participants and the subscribe path. The client is the MCP host (the LLM application) that wants live context. During initialization it checks the server's capabilities: a server advertises resource support and, within it, whether it supports subscribe and listChanged — the client must not subscribe to a server that never offered the capability. To subscribe, the client sends resources/subscribe with a resource uri, declaring interest. The server resource is whatever that URI addresses — a file, a database record, an API-backed document — and behind it sits a change source: a filesystem watcher, a database trigger or change-data-capture stream, or an internal event bus that tells the server when the underlying data moved.

Middle row: the server's machinery. The subscription registry maps each resource URI to the set of subscribers interested in it, scoped per client session so cleanup is precise. When the change source fires, the server emits a notifications/resources/updated message naming the URI (notably, the notification carries the URI, not the new contents — it is a signal to re-read, keeping the message small and letting the client decide when to fetch). The debounce/coalesce stage sits between the change source and the notification: it collapses a burst of rapid changes into a single 'updated' signal within a small window, so a hot resource produces a bounded notification rate rather than one per underlying write. Separately, list_changed is the catalog-level signal: when resources are added or removed, the server sends notifications/resources/list_changed so the client re-runs resources/list.

Bottom rows: the client's response and lifecycle. On receiving an updated notification, the client performs a re-read — a fresh resources/read for that URI — and updates whatever context it holds; this is the only point at which contents actually transfer, so the client controls the cost. Unsubscribe and lifecycle close the loop: the client can send resources/unsubscribe, and, crucially, the server must tear down all of a session's subscriptions when the connection closes, releasing watchers and registry entries. The ops strip names the metrics that keep this healthy: live subscription counts, notification latency and rate, fan-out per change, and leak detection for subscriptions that outlive their clients.

MCP resource subscriptions — push updates for context that changes, not repeated pollingthe client subscribes to a resource URI; the server notifies when it changesClienthost / LLM appsubscribe(uri)declare interestServer resourcefile, record, feedChange sourcewatcher / DB / eventSubscription registryuri -> subscribersupdated notificationresources/updatedDebounce / coalescecollapse burstslist_changedcatalog changesRe-read on notifyclient fetches fresh contentsUnsubscribe / lifecyclecleanup on disconnectOps — subscription counts + notify latency + fan-out + leak detectionregisteremitdetectsignalnotifycoalesceannounceoperateoperate
MCP resource subscriptions: a client subscribes to a resource URI, a change source drives updated notifications through a registry with debouncing, and the client re-reads fresh contents on notify.
Advertisement

End-to-end flow

Trace a live-document scenario. An LLM coding assistant connects to a project MCP server over a streamable HTTP transport. During initialization the server advertises resources with subscribe: true and listChanged: true. The user opens a config file; the assistant reads it once via resources/read and, because it wants to keep reasoning over the current version, sends resources/subscribe for that file's URI. The server records the subscription in its registry under this session and ensures a filesystem watcher is active for the path.

The user edits and saves the file. The watcher fires; the server's debounce window (say 200ms) opens. The editor's save actually writes the file three times in quick succession (a common editor behavior), but the coalescer collapses all three into one signal. When the window closes, the server sends a single notifications/resources/updated naming the URI. The notification is tiny — no file contents — so it is cheap regardless of file size. The assistant receives it and issues a fresh resources/read, pulling the new contents exactly once, and updates the model's context. The user's next question is answered against the current file without the assistant having polled at all.

Now the catalog changes: the user adds a new file to the project. This is not a change to any subscribed resource's contents, so the server instead emits notifications/resources/list_changed. The assistant re-runs resources/list, sees the new file, and can offer it as available context — subscribing to it too if the user opens it. The two signals stay cleanly separated: 'a thing you watch changed' versus 'the set of things changed'.

Then a disruption: the HTTP connection drops and the client reconnects. This is the moment naive implementations lose freshness. The server, on session teardown, released the old subscriptions, and the reconnected session starts clean — so the client must re-subscribe as part of its reconnect logic, and, because a change may have occurred while it was disconnected, it re-reads the resource once on reconnect rather than trusting its cached copy. A well-behaved server also bounds its per-session subscription count and rejects a runaway client that tries to subscribe to ten thousand URIs, returning an error rather than letting one client exhaust the registry. The result across all these cases is context that tracks reality closely while transferring data only when it genuinely changed.