This is the documentation for the v2 beta — looking for the v1 documentation?
Skip to content

Subscribe to changes

A subscription stream is one long-lived subscriptions/listen request that carries every change notification you opted in to. On a connection that negotiated 2026-07-28, change notifications arrive only on a stream you open — nothing arrives unsolicited.

Open a subscription stream

listen takes a filter naming the notification types you want. Register a handler for each type with setNotificationHandler before you open the stream.

ts
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('Server honored:', subscription.honoredFilter);

listen() resolves once the server acknowledges the stream, and returns an McpSubscription whose honoredFilter is the subset of your filter the server agreed to deliver:

Server honored: { toolsListChanged: true, resourceSubscriptions: [ 'config://app' ] }

The server narrows the filter to its advertised capabilities — resourceSubscriptions survives only when it advertises resources: { subscribe: true }, and each list-change field only when the matching listChanged capability is set. The four filter fields are toolsListChanged, promptsListChanged, resourcesListChanged, and resourceSubscriptions (an array of resource URIs).

Handle the notifications

resourceSubscriptions asked for per-resource updates; register the matching handler and re-read the resource when it fires.

ts
client.setNotificationHandler('notifications/resources/updated', async notification => {
    const { contents } = await client.readResource({ uri: notification.params.uri });
    console.log('Updated', notification.params.uri, contents);
});

Every notification on the stream dispatches through setNotificationHandler — the same registration an unsolicited 2025-era notification fires, so register once for either delivery path. When the server publishes a tool change and an update to config://app, both handlers fire from the one stream:

Tools changed: 2
Updated config://app [ { uri: 'config://app', text: '{"theme":"dark"}' } ]

Close the stream and react to closure

close() tears the stream down. closed resolves exactly once with the reason — it never rejects.

ts
await subscription.close();
console.log('Closed:', await subscription.closed);

The reason names who ended the stream:

Closed: local

'local' means you closed it, 'graceful' means the server ended the subscription deliberately, and 'remote' means the stream dropped without a response. The SDK never re-listens for you.

Re-listen only on 'remote':

ts
while (watching) {
    const sub = await client.listen({ resourceSubscriptions: ['config://app'] });
    const reason = await sub.closed;
    if (reason !== 'remote') break; // 'local' or 'graceful': done
    await new Promise(resolve => setTimeout(resolve, 1000)); // back off, then re-listen
}

Let the SDK open the stream for you

The listChanged client option opens and manages the stream itself.

ts
const watcher = new Client(
    { name: 'notes-watcher', version: '1.0.0' },
    {
        listChanged: {
            tools: {
                onChanged: (error, tools) => {
                    if (error) {
                        console.error('Refresh failed:', error);
                        return;
                    }
                    console.log('Tools refreshed:', tools?.length);
                }
            }
        }
    }
);

After connect() the SDK opens the stream from the intersection of the listChanged types you configured and the capabilities the server advertises, and exposes the handle as autoOpenedSubscription. On every change the SDK re-fetches the list and hands it to onChanged:

Tools refreshed: 1

WARNING

listChanged registers its own handler for each configured list_changed type during connect(). The last registration for a notification type wins, so a manual setNotificationHandler for that type registered after connecting silently disables listChanged for it.

Fall back to legacy per-resource subscribe

On a 2025-era connection, request per-resource updates with subscribeResource instead.

ts
await client.subscribeResource({ uri: 'config://app' });

// The same notifications/resources/updated handler fires.

await client.unsubscribeResource({ uri: 'config://app' });

The notification it produces is the same notifications/resources/updated, dispatched to the handler you already registered.

INFO

listen() is 2026-07-28-only and subscribeResource() is 2025-era — on the wrong era each rejects with an SdkError whose code is METHOD_NOT_SUPPORTED_BY_PROTOCOL_VERSION. See Protocol versions.

Recap

  • listen(filter) opens one stream carrying every change notification you asked for; honoredFilter is the capability-gated subset the server granted.
  • Notifications on the stream dispatch through setNotificationHandler — the same registrations 2025-era unsolicited notifications fire.
  • closed resolves exactly once with 'local', 'graceful', or 'remote', and never rejects; there is no automatic re-listen.
  • The listChanged client option opens and manages the stream for you, exposed as autoOpenedSubscription.
  • subscribeResource and unsubscribeResource are the 2025-era per-resource path; which path your connection supports is an era question.