docs(servers): document resources/subscribe serving; exercise subscriptions in the resources example (#2413)

This commit is contained in:
Felix Weinberger 2026-07-02 14:18:25 +01:00 • committed by GitHub
commit ddf6cc1d73
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 221 additions and 11 deletions

View file

@ -87,7 +87,7 @@ On stdio, [`serveStdio`](../serving/stdio.md) routes the instance's own `send*Li
:::
::: info Coming from 2025-era subscriptions
A 2025-era connection delivers per-resource updates without a listen stream — [Subscriptions](../clients/subscriptions.md#fall-back-to-legacy-per-resource-subscribe) covers that path, and [Protocol versions](../protocol-versions.md) the era split.
A 2025-era connection delivers per-resource updates without a listen stream — [Resources](./resources.md#serve-per-resource-subscriptions) covers the server's subscription bookkeeping, [Subscriptions](../clients/subscriptions.md#fall-back-to-legacy-per-resource-subscribe) the client call, and [Protocol versions](../protocol-versions.md) the era split.
:::
## Pick an event bus for multi-process deployments

View file

@ -212,6 +212,51 @@ server.sendResourceListChanged();
The notification tells connected clients to call `resources/list` again. A change to one resource's content is a different signal, `notifications/resources/updated` — [Notifications](./notifications.md) covers both.
## Serve per-resource subscriptions
A 2025-era client opts into `notifications/resources/updated` for one URI with `resources/subscribe`. The SDK routes the verb; the bookkeeping is yours: advertise the capability, track the URIs per connection, and send the notification to subscribers only.
```ts source="../../examples/guides/servers/resources.examples.ts#sendResourceUpdated_subscribers"
let deployStatus = 'idle';
const deploys = new McpServer({ name: 'deploys', version: '1.0.0' }, { capabilities: { resources: { subscribe: true } } });
deploys.registerResource(
'deploy-status',
'deploys://status',
{ description: 'The current deploy state', mimeType: 'text/plain' },
async uri => ({ contents: [{ uri: uri.href, text: deployStatus }] })
);
// The SDK routes the two verbs; which URIs this connection watches is yours to track.
const subscribedUris = new Set<string>();
deploys.server.setRequestHandler('resources/subscribe', request => {
subscribedUris.add(request.params.uri);
return {};
});
deploys.server.setRequestHandler('resources/unsubscribe', request => {
subscribedUris.delete(request.params.uri);
return {};
});
async function setDeployStatus(status: string): Promise<void> {
deployStatus = status;
if (subscribedUris.has('deploys://status')) {
await deploys.server.sendResourceUpdated({ uri: 'deploys://status' });
}
}
```
The `Set` belongs to one server instance, and each connection gets its own instance from your factory — a subscription never leaks across connections. Send `resources/updated` only to connections that subscribed; unsolicited per-resource updates are wrong on 2025-era connections.
The pattern needs a connection that outlives the subscribe call: over stdio (and any sessionful wiring) the instance and its `Set` live as long as the connection. Behind `createMcpHandler`'s stateless legacy fallback each POST gets a fresh instance, so `resources/subscribe` succeeds and the `Set` is discarded with it — no update can ever be delivered on that posture. [Support legacy clients](../serving/legacy-clients.md) covers the serving postures.
::: info
On [2026-07-28](../protocol-versions.md) connections the verb does not exist: clients name resource URIs in their `subscriptions/listen` filter, and the entry filters delivery itself — `serveStdio` routes the instance's own `sendResourceUpdated` call onto matching streams, and `createMcpHandler` delivers what you publish on its notifier ([Notifications](./notifications.md#publish-a-resource-update-through-the-handler)).
:::
A dual-era server therefore still calls `sendResourceUpdated` on 2026-07-28 connections, where the subscribe set is always empty — gate on the connection's era as well as the set. The [`resources` example](https://github.com/modelcontextprotocol/typescript-sdk/tree/main/examples/resources) guards with `reqCtx.era === 'modern' || subscribedUris.has(uri)` in its factory and runs as a self-verifying pair: delivery is asserted over stdio on both eras and over HTTP on the 2026-07-28 listen path; the stateless legacy HTTP leg asserts only that the subscribe calls succeed.
## Recap
- `registerResource(name, uri, config, readCallback)` registers a resource at a fixed URI.
@ -220,3 +265,4 @@ The notification tells connected clients to call `resources/list` again. A chang
- A template's `list` callback is what makes its instances appear in `resources/list`.
- Resolve file-backed paths to their real location and reject anything outside the root before reading.
- Registration changes emit `notifications/resources/list_changed` automatically.
- `resources/subscribe` bookkeeping is the server's: advertise `resources: { subscribe: true }`, track URIs per connection, send `resources/updated` to subscribers only.