Client Subscriptions
Purpose and Scope
Client subscriptions are the client-side way to receive change notifications without relying on unsolicited server traffic. In the 2026-era protocol model described by the SDK docs, a client opens one long-lived subscription request and asks for the kinds of changes it wants. The stream then carries matching notifications until either side closes it or the connection drops. This page focuses on how to open that stream, dispatch notifications, re-read changed data, react to closure, and decide when to use the legacy per-resource fallback for older servers. Sources: docs/clients/subscriptions.md, docs/clients/connect.md
Relevant Source Files
- docs/clients/subscriptions.md — Primary how-to for opening subscription streams, registering notification handlers, reading the honored filter, closing streams, retrying remote drops, and falling back to legacy resource subscriptions.
- docs/clients/calling.md — Explains the list and read client calls that subscription handlers typically use after a change notification arrives.
- docs/clients/caching.md — Describes response caching and cache modes, which matter when a notification handler refreshes lists or resources after a change.
- docs/clients/connect.md — Defines the connected Client lifecycle, transport choice, negotiated server capabilities, and clean disconnect behavior required before subscriptions are useful.
- docs/clients/machine-auth.md — Shows how authenticated HTTP transports attach credentials and refresh on authorization failures while subscription-related requests are in flight.
- docs/clients/middleware.md — Covers fetch middleware for Streamable HTTP clients, including logging and request wrapping that can observe subscription setup traffic.
Core Primitives
The subscription workflow starts with a connected client, so construct a client, choose a transport, and wait for the initialize handshake before listening. After that handshake, the client knows the negotiated protocol version and the server capability object. The subscription API is centered on a listen request, a filter, notification handlers, and a returned subscription object. The filter names desired change families, while notification handlers define what application code should do when those changes arrive. Treat the stream as delivery infrastructure, not as your local state model. Sources: docs/clients/connect.md, docs/clients/subscriptions.md
The filter fields documented for the SDK are tools list changes, prompts list changes, resources list changes, and an array of specific resource URIs for per-resource updates. The server may narrow the request, and the returned subscription exposes the honored subset. That detail is important for robust hosts: asking for a resource update stream does not prove the server accepted it. Check the honored filter before presenting a live-refresh user experience, and keep a slower manual refresh path for capabilities the server did not advertise or did not honor. Sources: docs/clients/subscriptions.md
Execution Flow
Register notification handlers before opening the stream. This ordering avoids a race where the server acknowledges the subscription and immediately sends a change that your process is not ready to handle. The same handler registration is used for stream-delivered notifications and older unsolicited notifications, so client code can share handlers across protocol eras. A typical tool-list handler calls the list operation again, while a resource-update handler reads the resource URI from the notification and fetches current contents. Sources: docs/clients/subscriptions.md, docs/clients/calling.md
client.setNotificationHandler('notifications/tools/list_changed', async () => {
const { tools } = await client.listTools();
console.log('Tools changed:', tools.length);
});
const subscription = await client.listen({
toolsListChanged: true,
resourceSubscriptions: ['config://app']
});
console.log(subscription.honoredFilter);When a handler re-lists tools, prompts, resources, or reads a resource, it is using the same high-level calls documented for normal client interaction. Those calls may aggregate paginated list responses, may validate tool results when prior metadata is available, and may participate in response caching when the server supplied cache hints. For subscriptions, this means a change notification is usually a signal to refresh through the public client API, not a complete replacement payload. Use cache modes deliberately when freshness is more important than avoiding a round trip. Sources: docs/clients/subscriptions.md, docs/clients/calling.md, docs/clients/caching.md
Notification Handling and Refresh Patterns
For resource subscriptions, the notification includes the changed resource URI, and the documented pattern is to call the resource read operation for that URI. For list-change notifications, call the matching list method and update your UI, index, or model context from the returned list. Keep handlers idempotent because a reconnect loop or server restart can cause your application to re-read state it already has. Also keep handlers small: do durable work through your own queue if refreshes may be slow, rate-limited, or user-specific. Sources: docs/clients/subscriptions.md, docs/clients/calling.md
Caching can make refresh behavior surprising if you ignore it. The client has a response cache, and cacheable methods can serve still-fresh data locally when the server marked results with freshness hints. That is useful for ordinary repeated calls, but after a change notification you may want a refresh mode so the handler crosses the wire and stores updated data. Conversely, bypass mode can fetch without mutating cache state. Choose a per-call cache mode based on whether the notification invalidates a local view, warms a shared cache, or simply checks whether content changed. Sources: docs/clients/caching.md, docs/clients/subscriptions.md
Stream Closure and Retry Strategy
A returned subscription has a close operation and a closed promise. Closing locally tears down the stream, while the closed promise resolves exactly once with a reason and does not reject. The documented reasons distinguish local closure, deliberate graceful server closure, and remote drop without a response. The SDK does not automatically listen again, so applications that need continuous watching should implement their own loop. Re-listen only after remote drops, and add a backoff so a failing server or network does not create a tight retry cycle. Sources: docs/clients/subscriptions.md
while (watching) {
const sub = await client.listen({ resourceSubscriptions: ['config://app'] });
const reason = await sub.closed;
if (reason !== 'remote') break;
await new Promise(resolve => setTimeout(resolve, 1000));
}Clean connection shutdown still matters outside the subscription object. The connection guide shows that a client holds one connection to one server and that Streamable HTTP clients can terminate the server-side session before closing. If your application owns both a transport and subscriptions, stop watchers first, then terminate or close the underlying transport. For stdio child processes, closing the client also participates in child-process shutdown. Keeping the order explicit makes it easier to distinguish expected local closure from a remote drop that should be retried. Sources: docs/clients/connect.md, docs/clients/subscriptions.md
Automatic List-Changed Streams, Auth, and Middleware
The subscriptions guide also documents a client option for list-changed behavior that opens and manages a stream for you. Use that option when the application only needs refreshed tool, prompt, or resource lists and does not need to manage a custom watch loop. Manual listen remains the right choice when you need per-resource subscriptions, custom retry policy, or direct access to the honored filter and closure reason. In both cases, handlers should be written as refresh callbacks that can receive errors and update application state safely. Sources: docs/clients/subscriptions.md
On HTTP transports, subscription setup is just part of the same authenticated and middleware-wrapped request path as other client operations. Machine-auth providers can attach bearer tokens, run client credentials, or refresh after an unauthorized response, while middleware can add headers, logging, tracing, or OAuth behavior around the underlying fetch. This does not change the subscription API, but it affects observability and failure handling. For example, logging middleware can show the initialize and request traffic around listen setup, while auth providers decide whether a failed request can be retried once. Sources: docs/clients/machine-auth.md, docs/clients/middleware.md, docs/clients/subscriptions.md
Compact API Reference
- Client connection prerequisite: create a Client, connect it with a transport, and use server capabilities from the handshake to decide what to request.
- Notification registration: set handlers with setNotificationHandler before opening the stream.
- Listen filter fields: toolsListChanged, promptsListChanged, resourcesListChanged, and resourceSubscriptions.
- Listen result: an McpSubscription with honoredFilter, close, and closed.
- Closure reasons: local for client-initiated close, graceful for deliberate server ending, and remote for dropped streams.
- Refresh calls commonly used by handlers: listTools, listPrompts, listResources, listResourceTemplates, and readResource.
- Cache-sensitive calls can use per-call cache modes when a notification should force a fresh read.
Legacy Fallback and Next Steps
Use the stream API when the negotiated server supports the 2026-era subscription model. For older behavior, the docs keep compatibility by allowing the same notification handlers to receive unsolicited notifications and by describing a legacy per-resource subscribe fallback. That lets client applications keep one handler surface while varying the transport-era mechanics underneath. Next, read the client connection page for transport setup, the calling page for refresh operations used inside handlers, and the caching page before deciding whether notification-driven reads should use cached, refreshed, or bypassed results. Sources: docs/clients/subscriptions.md, docs/clients/connect.md, docs/clients/calling.md, docs/clients/caching.md