Resource Subscriptions
resources/subscribe and resources/unsubscribe, legacy only.
Note
Removed in 2026-07-28 — use subscriptions/listen 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. The two halves are separate: the callbacks below only tell you what a client wants; delivering updates is the stream's job.
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:
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 });
}