
# Resource Subscriptions

> `resources/subscribe` and `resources/unsubscribe`, legacy only.

> [!NOTE]
> Removed in `2026-07-28` — use [`subscriptions/listen`](/guide/modern/subscriptions) instead, which carries the stream and the filter in one request.

Legacy clients subscribe to individual resource URIs, then receive `notifications/resources/updated` over the [GET/SSE stream](/guide/legacy/sse). The two halves are separate: the callbacks below only tell you _what_ a client wants; delivering updates is the stream's job.

```ts
const subscriptions = new Map<string, Set<string>>(); // sessionId → URIs

defineMcpHandler({
  name: "my-server",
  version: "1.0.0",
  session: true,
  onSubscribe(uri, event) {
    const sessionId = event.context.mcp!.sessionId!;
    if (!subscriptions.has(sessionId)) subscriptions.set(sessionId, new Set());
    subscriptions.get(sessionId)!.add(uri);
  },
  onUnsubscribe(uri, event) {
    const sessionId = event.context.mcp!.sessionId!;
    subscriptions.get(sessionId)?.delete(uri);
  },
  resources: [/* ... */],
});
```

The URI is validated before your callback runs. Declaring `onSubscribe` is what advertises `resources.subscribe: true` in the `initialize` capabilities.

> [!IMPORTANT]
> That `Map` is yours to bound and clean up. Delete the session's entry on `DELETE` and cap the number of URIs a single client may subscribe to — otherwise a client can grow it for as long as the process lives. The modern `subscriptions/listen` registry is bounded for you; this one is not.

Pair it with the stream to actually notify:

```ts
onStream(stream, event) {
  const sessionId = event.context.mcp?.sessionId;
  const off = watchResources((uri) => {
    if (!sessionId || !subscriptions.get(sessionId)?.has(uri)) return;
    stream.push({
      data: JSON.stringify({
        jsonrpc: "2.0",
        method: "notifications/resources/updated",
        params: { uri },
      }),
    });
  });
  event.req.signal?.addEventListener("abort", off, { once: true });
}
```
