docs: rebuild the v2 documentation around a task-based page tree (#2397)

This commit is contained in:
Felix Weinberger 2026-06-30 21:18:42 +01:00 • committed by GitHub
commit b153505428
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -0,0 +1,137 @@
---
shape: how-to
---
# 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](../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 source="../../examples/guides/clients/subscriptions.examples.ts#listen_open"
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 source="../../examples/guides/clients/subscriptions.examples.ts#listen_updated"
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 source="../../examples/guides/clients/subscriptions.examples.ts#listen_close"
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 source="../../examples/guides/clients/subscriptions.examples.ts#listen_watchLoop"
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 source="../../examples/guides/clients/subscriptions.examples.ts#listChanged_auto"
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 source="../../examples/guides/clients/subscriptions.examples.ts#subscribeResource_legacy"
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](../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.