Introduction

Resources are not always static. A configuration file changes, a database record is updated, a log file grows. MCP provides a subscription mechanism that lets your client receive notifications when a resource changes, so you can refresh your cached data and keep the LLM's context up to date. When subscriptions are not supported, you can fall back to periodic polling.

Key Concepts

  • client.subscribeResource(): Subscribes to notifications for a specific resource URI. When the resource changes on the server, the client receives a notification.
  • notifications/resources/updated: The notification type sent by the server when a subscribed resource changes. The notification includes the resource URI.
  • Cache Invalidation: When you receive an update notification, invalidate your cached version of the resource and optionally re-read it immediately.
  • Polling Fallback: Not all servers support subscriptions. When the subscribe capability is absent, fall back to periodic polling by re-reading the resource at regular intervals.

Real World Context

Your AI assistant monitors a configuration file that controls deployment settings. You subscribe to config://app/production via MCP. When a team member changes the configuration through a web UI, the MCP server detects the change and sends a notification. Your client receives the notification, re-reads the resource, and the LLM's next response reflects the updated configuration — no manual refresh needed.

Deep Dive

Subscribing to a Resource

Subscription requires two steps: subscribe to the resource, and set up a notification handler to receive updates.

typescript
// Step 1: Set up the notification handler
client.setNotificationHandler(
  "notifications/resources/updated",
  async (notification) => {
    const uri = notification.params.uri;
    console.error(`Resource updated: ${uri}`);

    // Re-read the resource to get the latest content
    const updated = await client.readResource({ uri });
    // Update your cache or notify the application
    resourceCache.set(uri, updated.contents);
  }
);

// Step 2: Subscribe to a specific resource
await client.subscribeResource({
  uri: "file:///project/config.json"
});

The notification handler fires every time the server detects a change to any subscribed resource. The notification.params.uri tells you which resource changed. You then re-read the resource to get the updated content.

Managing Multiple Subscriptions

You can subscribe to multiple resources. Each subscription is independent:

typescript
const criticalResources = [
  "config://app/settings",
  "file:///project/.env",
  "db://schema/migrations"
];

// Subscribe to all critical resources
for (const uri of criticalResources) {
  await client.subscribeResource({ uri });
}

// Single handler receives all updates
client.setNotificationHandler(
  "notifications/resources/updated",
  async (notification) => {
    const uri = notification.params.uri;
    console.error(`Updated: ${uri}`);

    // Handle different resources differently
    if (uri.startsWith("config://")) {
      await refreshConfig(uri);
    } else if (uri.startsWith("db://")) {
      await refreshSchema(uri);
    } else {
      await refreshFile(uri);
    }
  }
);

A single notification handler handles all resource updates. Route to different refresh logic based on the URI.

Unsubscribing

When you no longer need updates for a resource, unsubscribe:

typescript
await client.unsubscribeResource({
  uri: "file:///project/config.json"
});

Unsubscribing stops the server from sending update notifications for that URI. This reduces unnecessary network traffic and processing.

Cache Invalidation Pattern

A robust cache invalidation pattern combines subscriptions with lazy re-reading:

typescript
class ResourceCache {
  private cache = new Map<string, {
    contents: unknown;
    valid: boolean;
  }>();

  constructor(private client: Client) {
    client.setNotificationHandler(
      "notifications/resources/updated",
      async (notification) => {
        const uri = notification.params.uri;
        const entry = this.cache.get(uri);
        if (entry) {
          entry.valid = false; // Mark as stale
        }
      }
    );
  }

  async get(uri: string) {
    const entry = this.cache.get(uri);

    if (entry && entry.valid) {
      return entry.contents; // Return cached version
    }

    // Re-read from server
    const resource = await this.client.readResource({ uri });
    this.cache.set(uri, {
      contents: resource.contents,
      valid: true
    });

    return resource.contents;
  }

  async subscribe(uri: string) {
    await this.client.subscribeResource({ uri });
    // Eagerly populate cache
    await this.get(uri);
  }
}

This pattern marks cache entries as stale when a notification arrives but only re-reads the resource when the data is actually requested. This avoids unnecessary reads for resources that change frequently but are accessed infrequently.

Polling Fallback

Not all servers support the subscribe capability. Check the server's capabilities and fall back to polling if subscriptions are not available:

typescript
async function watchResource(
  client: Client,
  uri: string,
  onChange: (contents: unknown) => void,
  pollIntervalMs: number = 5000
) {
  // Check if server supports subscriptions
  const serverCapabilities = client.getServerCapabilities();
  const supportsSubscriptions =
    serverCapabilities?.resources?.subscribe === true;

  if (supportsSubscriptions) {
    // Use subscriptions
    client.setNotificationHandler(
      "notifications/resources/updated",
      async (notification) => {
        if (notification.params.uri === uri) {
          const resource = await client.readResource({ uri });
          onChange(resource.contents);
        }
      }
    );
    await client.subscribeResource({ uri });
  } else {
    // Fall back to polling
    let lastContent = JSON.stringify(
      (await client.readResource({ uri })).contents
    );

    setInterval(async () => {
      const resource = await client.readResource({ uri });
      const currentContent = JSON.stringify(resource.contents);
      if (currentContent !== lastContent) {
        lastContent = currentContent;
        onChange(resource.contents);
      }
    }, pollIntervalMs);
  }
}

The polling fallback periodically reads the resource and compares the content to the last known version. When a change is detected, it triggers the callback. This is less efficient than subscriptions but works with any server.

Common Pitfalls

  1. Not checking subscription support — If you call subscribeResource() on a server that does not support subscriptions, it will return an error. Always check the server's capabilities first.
  2. Forgetting to re-read after notification — The notifications/resources/updated notification tells you that a resource changed, but it does not include the new content. You must call readResource() to get the updated data.
  3. Setting up handlers after subscribing — Set up your notification handler before calling subscribeResource(). Otherwise, you might miss notifications that arrive between the subscribe call and the handler registration.

Best Practices

  1. Set up handlers before subscribing — Always register your notification handler before calling subscribeResource() to avoid missing early notifications.
  2. Use lazy invalidation — Mark cache entries as stale on notification, but only re-read when the data is requested. This avoids unnecessary reads for frequently changing resources.
  3. Implement the polling fallback — Check server capabilities and fall back to polling when subscriptions are not supported. This makes your client work with any MCP server.

Summary

  • client.subscribeResource({ uri }) subscribes to change notifications for a specific resource.
  • The server sends notifications/resources/updated when a subscribed resource changes — you must re-read to get the new content.
  • Use lazy cache invalidation: mark as stale on notification, re-read on access.
  • Check serverCapabilities.resources.subscribe before subscribing. Fall back to polling when not supported.
  • Always set up notification handlers before subscribing to avoid missing updates.

Code Examples

typescript
// Subscribe and handle updates
client.setNotificationHandler(
  "notifications/resources/updated",
  async (notification) => {
    const uri = notification.params.uri;
    console.error(`Resource updated: ${uri}`);
    const updated = await client.readResource({ uri });
    resourceCache.set(uri, updated.contents);
  }
);

await client.subscribeResource({
  uri: "config://app/settings"
});
✓ Completed