Introduction
Many resources are not static — they change over time. A log file grows, database records are updated, configuration is reloaded. MCP provides mechanisms for servers to notify clients when resources change and for resource handlers to return fresh data on every read.
Key Concepts
- Dynamic handlers read data at call time rather than returning cached values, ensuring clients always get current information
- List change notifications alert clients that the set of available resources has changed (new resources added or existing ones removed)
notifications/resources/list_changedis the notification type sent to connected clients when the resource list changes- Caching strategies determine when to recompute data versus serve cached results
Real World Context
Dynamic resources are essential for real-world integrations:
- A file system resource that reflects the current contents of a directory
- A database resource where records are created and deleted at runtime
- An API status resource that reflects live system health
- Log files that grow continuously throughout the day
Deep Dive
A resource handler that reads from a file at call time is inherently dynamic. Every time a client reads the resource, the handler fetches the latest data.
typescriptimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import * as fs from 'fs/promises'; const server = new McpServer({ name: 'file-server', version: '1.0.0' }); server.registerResource( 'app-log', 'logs://app/current', { title: 'Application Log', description: 'Current application log file', mimeType: 'text/plain' }, async (uri): Promise<ReadResourceResult> => { const content = await fs.readFile('/var/log/app.log', 'utf-8'); return { contents: [{ uri: uri.href, text: content }] }; } );
Each read returns the latest log content because the handler reads the file every time it is called.
When your list of available resources changes — for example, when a new file is created in a watched directory — you should notify clients so they can refresh their resource list.
typescriptimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import * as chokidar from 'chokidar'; const server = new McpServer({ name: 'file-server', version: '1.0.0' }); // Watch a directory for changes const watcher = chokidar.watch('/data/reports'); watcher.on('add', async (filePath) => { // Register the new file as a resource const fileName = filePath.split('/').pop(); server.registerResource( `report-${fileName}`, `reports://${fileName}`, { title: fileName, description: 'Generated report', mimeType: 'application/json' }, async (uri): Promise<ReadResourceResult> => { const content = await fs.readFile(filePath, 'utf-8'); return { contents: [{ uri: uri.href, text: content }] }; } ); // Notify clients that the resource list has changed await server.server.sendNotification({ method: 'notifications/resources/list_changed' }); });
The notifications/resources/list_changed notification tells connected clients to re-fetch the resource list. The notification carries no payload — it simply signals that something has changed.
For database-backed resources, the handler queries the database on each read.
typescriptserver.registerResource( 'active-users', 'db://users/active', { title: 'Active Users', description: 'Currently active user list', mimeType: 'application/json' }, async (uri): Promise<ReadResourceResult> => { const users = await db.query('SELECT id, name FROM users WHERE active = true'); return { contents: [{ uri: uri.href, text: JSON.stringify(users) }] }; } );
The data is always fresh because the handler queries the database on every read.
Common Pitfalls
- Not notifying clients of list changes: if you add or remove resources at runtime without sending
notifications/resources/list_changed, clients will have a stale view of available resources - Expensive handlers without caching: if reading the data is slow (large files, complex queries), consider caching with a TTL rather than reading on every call
- Ignoring error handling: file reads and database queries can fail; always handle errors gracefully in your handler
- Sending too many notifications: if resources change rapidly, debounce notifications to avoid flooding clients
Best Practices
- Use file watchers or database change listeners to detect when resources change and send notifications
- Implement caching with a short TTL for expensive data sources — balance freshness against performance
- Debounce rapid changes to avoid sending a notification for every individual change
- Return meaningful error messages when data sources are unavailable
- Log resource access patterns to understand which resources are read most frequently
Summary
Dynamic resources serve fresh data by reading from files, databases, or APIs inside the handler. When the set of available resources changes at runtime, servers send notifications/resources/list_changed to tell clients to refresh their resource list. Caching and debouncing help manage performance for frequently changing data sources.
Code Examples
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import * as fs from 'fs/promises';
const server = new McpServer({ name: 'file-server', version: '1.0.0' });
server.registerResource(
'app-log',
'logs://app/current',
{ title: 'Application Log', description: 'Current log file', mimeType: 'text/plain' },
async (uri): Promise<ReadResourceResult> => {
const content = await fs.readFile('/var/log/app.log', 'utf-8');
return { contents: [{ uri: uri.href, text: content }] };
}
);// After adding or removing a resource at runtime:
// McpServer wraps the low-level Server class; access it via server.server
await server.server.sendNotification({ method: 'notifications/resources/list_changed' });