Channels and Message Streams
Purpose and Scope
Channels in the TypeScript SDK are best understood from the client perspective as durable conversations carried over a connected transport. A client first establishes one connection to one server, and the connection handshake determines the protocol version, server capabilities, and instructions that shape every later request. On top of that connection, normal calls such as listing tools or reading resources are request and response exchanges, while a subscription stream is a long-lived request that carries later change notifications. This page focuses on how those streams behave, how they relate to transport lifecycle, and how client code should recover when the stream ends.
Sources: docs/clients/connect.md, docs/clients/subscriptions.md, docs/clients/calling.md
The most important terminology is the distinction between an MCP connection, a transport, and a subscription stream. The connection belongs to a Client instance and is created by connect after the initialize handshake. The transport is the underlying carrier, such as Streamable HTTP, stdio, SSE fallback, or an in-memory pair used for tests. A subscription stream is narrower: it is created with listen, lives inside an already connected client, and delivers only the change notifications the server agrees to send. Treating these as separate layers prevents accidental reconnect logic that closes the whole client just because one stream dropped.
Sources: docs/clients/connect.md, docs/clients/subscriptions.md
Relevant Source Files
- docs/clients/subscriptions.md — Defines subscription streams, filters, notification handlers, closure reasons, manual relisten loops, and the automatic listChanged watcher option.
- docs/clients/caching.md — Explains response caching, cache freshness, cache modes, shared stores, and partitioning, which affect when list and read calls after a notification hit the network.
- docs/clients/calling.md — Shows the ordinary request-response calls that subscription handlers commonly run, including listTools and readResource.
- docs/clients/connect.md — Establishes the connection and transport model, handshake results, transport choices, SSE fallback, server capability introspection, and clean disconnect behavior.
- docs/clients/machine-auth.md — Shows how HTTP transports attach auth providers and retry after token refresh, which matters for long-lived HTTP-backed message streams.
- docs/clients/middleware.md — Documents fetch middleware and logging around Streamable HTTP requests, including the GET request used to open a server-to-client stream in the logging example.
Subscription Stream Model
For protocol revision 2026-07-28, change notifications are no longer assumed to arrive unsolicited. The client opens one long-lived subscriptions/listen request and supplies a filter describing what it wants to hear about. The filter can ask for list-change notifications for tools, prompts, and resources, plus per-resource updates for specific resource URIs. The server answers by narrowing the request to an honored filter based on capabilities it advertised. A robust client should inspect that honored filter before assuming that a resource subscription or list-change feed is active.
Sources: docs/clients/subscriptions.md, docs/clients/connect.md
Handlers are registered independently from the stream itself. The same setNotificationHandler registration receives notifications whether they arrive through the modern listen stream or through older unsolicited delivery, so client applications should register handlers before opening the stream and leave them in place across relisten attempts. A typical resource update handler does not trust the notification as the new state; instead, it reacts by calling readResource for the URI in the notification. A typical tool list change handler calls listTools again, because the notification only says that the advertised list changed, not what the complete new list contains.
Sources: docs/clients/subscriptions.md, docs/clients/calling.md
Execution Flow
A practical flow starts with transport selection and connection. For remote servers, StreamableHTTPClientTransport points at the MCP endpoint URL; for local child processes, StdioClientTransport speaks over stdin and stdout; for older HTTP plus SSE servers, the guide recommends trying Streamable HTTP first and retrying with SSEClientTransport on a fresh Client if the first attempt fails. Once connect resolves, getServerCapabilities can be used to understand whether listChanged and resource subscription features are likely to be honored. The stream can then be opened, and the returned McpSubscription becomes the lifecycle handle for closing and observing the stream.
Sources: docs/clients/connect.md, docs/clients/subscriptions.md
client.setNotificationHandler('notifications/resources/updated', async notification => {
const { contents } = await client.readResource({ uri: notification.params.uri });
console.log('Updated', notification.params.uri, contents);
});
const subscription = await client.listen({
toolsListChanged: true,
resourceSubscriptions: ['config://app']
});
console.log(subscription.honoredFilter);Closing is explicit and observable. Calling close on the subscription tears down the stream, and the closed promise resolves exactly once with a reason rather than rejecting. The reason is local when application code closed it, graceful when the server intentionally ended it, and remote when the stream dropped without a response. The SDK deliberately does not re-listen automatically for the manual listen API. That means reconnection policy belongs in application code: retry on remote with a backoff, but stop when the application closed the stream or the server ended it cleanly.
Sources: docs/clients/subscriptions.md
API Components and Options
The compact public surface for stream-aware client code is small but has important sequencing rules. Create the Client, connect it, register notification handlers, call listen with a filter, inspect honoredFilter, then keep the returned subscription until it closes. The four filter fields are toolsListChanged, promptsListChanged, resourcesListChanged, and resourceSubscriptions. The automatic listChanged client option is a higher-level convenience: it opens and manages a stream for list refreshes and calls the configured onChanged callbacks. Use that option when the application only needs watched lists, and use manual listen when it needs per-resource subscriptions or custom retry policy.
Sources: docs/clients/subscriptions.md
Caching changes how much work a handler performs after a notification. The response cache is present on every Client, and cacheable verbs such as listTools, listPrompts, listResources, listResourceTemplates, and readResource may serve a still-fresh server result locally. That is useful for ordinary calls, but a change notification often means the caller wants a fresh view. In those handlers, use the documented cacheMode controls deliberately: refresh refetches and stores the new response, while bypass fetches without reading or writing the cache. Shared stores should also be partitioned per user when authorization context affects the result.
Sources: docs/clients/caching.md, docs/clients/calling.md
Transport, Auth, and Middleware Considerations
Long-lived message streams inherit the behavior of the underlying transport. Streamable HTTP can be decorated with client middleware because the middleware wraps the fetch used by the transport, so it sees SDK-generated requests as well as application calls. The logging example shows initialize, initialized notification, a GET used for a server-to-client stream, and a later tools call all passing through the wrapped fetch. This makes middleware a good place to add request tags, tracing, or safe logging, but the docs warn not to write default logs to stdout when stdout is carrying an MCP stdio transport.
Sources: docs/clients/middleware.md, docs/clients/connect.md
Authentication also lives below the stream abstraction. ClientCredentialsProvider, PrivateKeyJwtProvider, CrossAppAccessProvider, and custom bearer-token AuthProvider implementations are passed to the HTTP transport as authProvider options. The transport attaches tokens to requests, and documented providers can refresh and retry once after an unauthorized response. For a long-lived client, pinning expectedIssuer avoids sending credentials to the wrong authorization server after discovery. These details do not change the listen API, but they strongly affect operational reliability because the stream and follow-up list or resource reads share the same authenticated transport.
Sources: docs/clients/machine-auth.md, docs/clients/subscriptions.md
Lifecycle Checklist and Next Steps
Build stream consumers as small state machines rather than one-shot calls. Establish the client connection, inspect negotiated server information, register handlers, open the subscription, record what the server honored, and decide how each closure reason should affect the watch loop. In handlers, re-read authoritative state using the normal client APIs and choose cache behavior intentionally. When debugging, add HTTP middleware logging around Streamable HTTP, or use an in-memory transport in tests to avoid network timing. Next, read Connect a Client for transport setup, Client Subscriptions for the full listen guide, Client Caching for freshness rules, and Custom Transports if you need to carry JSON-RPC messages over a nonstandard channel.
Sources: docs/clients/connect.md, docs/clients/subscriptions.md, docs/clients/caching.md, docs/clients/middleware.md