Introduction
Resources are not always static. Database tables get new rows, files are edited, and configuration changes over time. MCP provides a subscription mechanism that lets clients receive notifications when resources change, enabling real-time updates without polling.
Key Concepts
- Resource subscription: A mechanism where the client tells the server it wants to be notified when a specific resource changes.
- notifications/resources/updated: A server-to-client notification sent when a subscribed resource's content has changed.
- notifications/resources/list_changed: A server-to-client notification sent when the overall list of available resources has changed (new resources added or existing ones removed).
- Cache invalidation: When a client receives an update notification, it should discard its cached copy and re-read the resource.
Real World Context
Consider an MCP server connected to a live database. A client subscribes to the db://users/stats resource that shows user count metrics. When a new user signs up, the server sends a notifications/resources/updated notification, and the client re-fetches the resource to get the latest numbers. Without subscriptions, the client would have to poll repeatedly, wasting bandwidth and adding latency.
Deep Dive
Subscribing to a Resource
A client subscribes by sending a resources/subscribe request with the resource URI:
json{"jsonrpc":"2.0","id":5,"method":"resources/subscribe","params":{"uri":"db://users/stats"}}
The server acknowledges the subscription. From this point, the server will notify the client whenever the resource changes. The client sends this request once and receives notifications for the lifetime of the subscription.
Receiving Update Notifications
When a subscribed resource changes, the server sends a notification:
json{"jsonrpc":"2.0","method":"notifications/resources/updated","params":{"uri":"db://users/stats"}}
This notification tells the client which resource changed but does not include the new content. The client must send a resources/read request to get the updated data. This design keeps notifications lightweight.
List Change Notifications
Separately from individual resource updates, the server can notify clients when the overall resource list changes:
json{"jsonrpc":"2.0","method":"notifications/resources/list_changed"}
This notification does not specify what changed. The client must re-fetch the full resource list with resources/list to see what was added or removed. Servers send this when new resources become available (a new database table is created) or existing ones are removed.
Client-Side Caching and Invalidation
Efficient clients cache resource content to avoid unnecessary reads. The caching strategy follows a simple pattern:
typescriptconst cache = new Map<string, string>(); // Read and cache async function readResource(uri: string) { if (!cache.has(uri)) { const result = await client.readResource({ uri }); cache.set(uri, result.contents[0].text); } return cache.get(uri); } // Invalidate on notification client.setNotificationHandler( "notifications/resources/updated", async (notification) => { cache.delete(notification.params.uri); } ); // Re-fetch list on list change client.setNotificationHandler( "notifications/resources/list_changed", async () => { const newList = await client.listResources(); // Update UI with new resource list } );
This code demonstrates the standard cache invalidation pattern: cache reads, delete the cache entry on update notifications, and re-read on next access.
Unsubscribing
Clients can unsubscribe from a resource when they no longer need updates:
json{"jsonrpc":"2.0","id":6,"method":"resources/unsubscribe","params":{"uri":"db://users/stats"}}
After unsubscribing, the server stops sending notifications for that resource. Clients should unsubscribe from resources they are no longer displaying to reduce unnecessary server work.
Common Pitfalls
- Re-reading immediately in the notification handler — If multiple resources update simultaneously, re-reading each one immediately can cause a burst of requests. Consider debouncing or batching reads.
- Not checking subscription support — Not all servers support subscriptions. Check the server's capabilities during initialization before attempting to subscribe.
- Caching without invalidation — If you cache resource content but do not handle update notifications, your cache will serve stale data indefinitely. Always pair caching with subscription-based invalidation.
Best Practices
- Subscribe only to resources you are actively using — Do not subscribe to every resource on the server. Subscribe when a resource is displayed to the user and unsubscribe when it is no longer visible.
- Implement cache-then-revalidate — Show cached content immediately and update it when the re-read completes. This gives users instant feedback while keeping data fresh.
- Handle subscription failures gracefully — If the server does not support subscriptions, fall back to periodic polling. Check the
subscribecapability in the server's initialization response.
Summary
- Clients subscribe to individual resources with
resources/subscribeto receive change notifications. notifications/resources/updatedsignals that a specific resource's content has changed.notifications/resources/list_changedsignals that the overall resource list has been modified.- Notifications do not include new content — clients must re-read the resource to get updated data.
- Pair caching with subscription-based invalidation to keep data fresh without excessive polling.
Code Examples
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
const client = new Client(
{ name: "my-app", version: "1.0.0" },
{ capabilities: {} }
);
// Subscribe to resource updates
await client.subscribeResource({ uri: "db://users/stats" });
// Handle update notifications
client.setNotificationHandler(
"notifications/resources/updated",
async (notification) => {
console.log(`Resource changed: ${notification.params.uri}`);
const updated = await client.readResource({ uri: notification.params.uri });
console.log("New content:", updated.contents[0].text);
}
);
// Handle list changes
client.setNotificationHandler(
"notifications/resources/list_changed",
async () => {
const resources = await client.listResources();
console.log("Updated resource list:", resources);
}
);