# 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](https://ts.sdk.modelcontextprotocol.io/v2/protocol-versions.md), 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](https://ts.sdk.modelcontextprotocol.io/v2/protocol-versions.md).
:::

## 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.
