Guide · MCP Protocol
MCP server resources API
The MCP Resources protocol lets your server expose structured data — files, database records, API responses, configuration snapshots — that LLM clients can read and reference in context. Unlike tools (which execute actions), resources are readable artifacts with stable URIs. This guide covers resources/list, resources/read, URI schemes, MIME types, dynamic resource generation, and subscriptions for real-time data changes.
TL;DR
Register resources with server.resource() using a URI template and a read handler. Return a contents array with uri, mimeType, and either text (for text content) or blob (for base64-encoded binary). Prefix URIs with a custom scheme that describes the data domain (db://, config://, git://). To push updates when underlying data changes, call server.sendResourceUpdated(uri) after subscribing clients have called resources/subscribe. Resources are passive reads — for operations that modify state, use tools instead.
Resources vs tools: when to use each
Resources and tools serve different purposes in the MCP protocol:
| Dimension | Resources | Tools |
|---|---|---|
| Intent | Expose data for reading | Execute actions |
| Side effects | None expected | Expected and explicit |
| Client invocation | resources/read | tools/call |
| URI-based | Yes — each resource has a stable URI | No — tools are named functions |
| Real-time updates | Yes — subscriptions + notifications | No |
| Typical use cases | Files, DB records, configs, logs | Write operations, computations, external API calls |
A database table that a user queries read-only is a good resource candidate. An insert or update operation is a tool. A file the LLM should read for context is a resource; a tool that writes a file is a tool.
Registering a static resource
The simplest form: a fixed URI that returns fixed or computed content.
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
const server = new McpServer({ name: 'my-server', version: '1.0.0' });
// Static text resource
server.resource(
'app-config', // internal name
'config://app/settings', // URI clients use to read this
{
name: 'Application Settings',
description: 'Current application configuration as JSON',
mimeType: 'application/json',
},
async (uri) => ({
contents: [{
uri: uri.href,
mimeType: 'application/json',
text: JSON.stringify({ maxConnections: 10, timeout: 30 }, null, 2),
}],
})
);
The read handler receives the parsed URI object. Return a contents array — most resources return a single item, but you can return multiple chunks for paginated or multi-part data.
URI templates for dynamic resources
Use URI templates to expose parameterized resources — a single registration that covers many URIs:
import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
// Expose individual database rows as resources
server.resource(
'user-record',
new ResourceTemplate('db://users/{userId}', { list: undefined }),
{
name: 'User Record',
description: 'A single user object from the database',
mimeType: 'application/json',
},
async (uri, { userId }) => {
const user = await db.users.findById(userId);
if (!user) {
throw new Error(`User not found: ${userId}`);
}
return {
contents: [{
uri: uri.href,
mimeType: 'application/json',
text: JSON.stringify(user, null, 2),
}],
};
}
);
The list property on the template controls whether clients can call resources/list to enumerate all matching URIs. Set it to a handler function to enable listing, or undefined to disable it (clients can read resources if they know the URI but cannot enumerate them). This is useful for large collections where enumeration is impractical.
Enabling resources/list
To let clients enumerate available resources, provide a list handler. Clients call resources/list to discover what your server exposes before deciding what to read.
server.resource(
'log-files',
new ResourceTemplate('logs://{filename}', {
list: async () => {
const files = await fs.readdir('./logs');
return {
resources: files
.filter(f => f.endsWith('.log'))
.map(f => ({
uri: `logs://${f}`,
name: f,
description: `Log file: ${f}`,
mimeType: 'text/plain',
})),
};
},
}),
{ name: 'Log Files', mimeType: 'text/plain' },
async (uri, { filename }) => {
const content = await fs.readFile(`./logs/${filename}`, 'utf8');
return {
contents: [{ uri: uri.href, mimeType: 'text/plain', text: content }],
};
}
);
MIME types and binary resources
Choose the MIME type that accurately describes the content format. Clients and LLMs use this to decide how to process or display the resource.
| Content type | MIME type | Field to use |
|---|---|---|
| Plain text, logs | text/plain | text |
| Markdown documentation | text/markdown | text |
| JSON data structures | application/json | text |
| HTML pages | text/html | text |
| CSV tables | text/csv | text |
| Images (PNG, JPEG) | image/png, image/jpeg | blob (base64) |
| PDF documents | application/pdf | blob (base64) |
For binary content, base64-encode the buffer and use the blob field:
server.resource(
'screenshot',
'screenshots://current',
{ name: 'Current Screenshot', mimeType: 'image/png' },
async (uri) => {
const buffer = await captureScreenshot();
return {
contents: [{
uri: uri.href,
mimeType: 'image/png',
blob: buffer.toString('base64'),
}],
};
}
);
Resource subscriptions and real-time updates
Clients can subscribe to individual resources with resources/subscribe. When data changes, send a notifications/resources/updated notification. The client re-reads the resource after receiving the notification.
// When your data source changes, notify subscribed clients
async function onDatabaseRowUpdated(userId: string) {
// The SDK tracks which clients have subscribed to which URIs.
// Call sendResourceUpdated and the SDK delivers the notification.
await server.sendResourceUpdated(`db://users/${userId}`);
}
// Hook this into your data layer
db.users.on('updated', (user) => {
onDatabaseRowUpdated(user.id);
});
Not all clients implement subscriptions. Check client.capabilities?.resources?.subscribe before assuming notifications are delivered. The canonical flow is:
- Client calls
resources/subscribewith a URI - Server receives the subscription (SDK handles registration automatically)
- Data changes; server calls
server.sendResourceUpdated(uri) - Client receives
notifications/resources/updatednotification - Client calls
resources/readagain to fetch the new data
Notifying clients when the resource list changes
If resources are created or removed dynamically (a new log file, a deleted database row), notify clients with sendResourceListChanged(). This triggers a fresh resources/list call from the client.
// When a new log file appears
watcher.on('add', (filename) => {
if (filename.endsWith('.log')) {
server.sendResourceListChanged();
}
});
URI scheme design
Choose a URI scheme that clearly identifies the data domain. Use custom schemes rather than file:// for server-managed data — this avoids ambiguity with the client's local filesystem.
| Domain | Example URIs |
|---|---|
| Database rows | db://users/123, db://orders/456 |
| Application config | config://app/settings, config://feature-flags |
| Git repository | git://HEAD/src/main.ts, git://branches |
| Application logs | logs://app.log, logs://errors-2026-06-09.log |
| External APIs | api://weather/london, api://github/repos/myorg |
| Memory/cache state | cache://sessions, cache://rate-limits |
Resources and external monitoring
If your MCP server exposes a resources endpoint over HTTP (via Streamable HTTP or SSE transport), that endpoint needs the same uptime monitoring as your tools endpoint. A resources/list failure — a crashed handler, a database connection drop, a memory limit hit — silently breaks LLM workflows that depend on that data. AliveMCP probes your HTTP MCP endpoint on a 60-second interval using the full MCP initialize handshake, catching both transport-level failures and protocol-level handler errors before your users do.
Further reading
- MCP server prompts API — reusable prompt templates for LLM clients
- MCP tool design — naming, argument schemas, and return shapes
- MCP tool annotations — hints for safe and destructive tool calls
- MCP server roots — workspace context from the client
- MCP server Streamable HTTP transport — remote deployment
- MCP server SSE transport — HTTP+SSE remote server setup
- MCP server authentication — securing resource access
- MCP server error handling — protocol errors and handler failures
- AliveMCP — uptime monitoring for HTTP-deployed MCP servers