Tools do things. Resources provide things. This distinction matters more than it sounds. A tool executes code with side effects – it searches, writes, sends, deletes. A resource is a read-only window into data – it gives the model (or the user) access to content without triggering any action. The resources primitive is MCP’s answer to the question: “how do I give the AI access to my data without writing a bespoke data-access tool every time?”

What Resources Are and How They Work
Every MCP resource has a URI – a unique identifier that the client uses to request it. The URI can follow any scheme: file://, db://, https://, custom-scheme://. The server defines what URIs exist and what they return. The client requests a URI and gets back content blocks (text or binary).
Resources come in two forms: direct resources (static items with known URIs that the server lists upfront) and resource templates (URI patterns with parameters, for dynamic resources where the set of possible URIs is not fixed).
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import fs from 'node:fs/promises';
const server = new McpServer({ name: 'file-server', version: '1.0.0' });
// Direct resource - static, known URI
server.resource(
'config',
'config://app/settings',
{ description: 'The application configuration', mimeType: 'application/json' },
async (uri) => {
const config = await fs.readFile('./config.json', 'utf8');
return { contents: [{ uri: uri.href, mimeType: 'application/json', text: config }] };
}
);
// Resource template - dynamic, parameterised URI
server.resource(
'user-profile',
new ResourceTemplate('users://{userId}/profile', { list: undefined }),
{ description: 'User profile by ID' },
async (uri, { userId }) => {
const user = await db.getUser(userId);
if (!user) throw new Error(`User ${userId} not found`);
return {
contents: [{
uri: uri.href,
mimeType: 'application/json',
text: JSON.stringify(user, null, 2),
}],
};
}
);
“Resources represent any kind of data that an MCP server wants to make available to clients. This can include file contents, database records, API responses, live system data, screenshots, images, log files, and more.” – MCP Documentation, Resources
Resource Content Types
Resources return content blocks with either text or blob (binary) content. Text resources are the most common – JSON, Markdown, plain text, CSV, code. Binary resources use base64-encoded data.
// Text resource
return {
contents: [{
uri: uri.href,
mimeType: 'text/markdown',
text: '# Product Manual\n\nThis product does X...',
}],
};
// Binary resource (e.g. an image or PDF)
const imageBuffer = await fs.readFile('./logo.png');
return {
contents: [{
uri: uri.href,
mimeType: 'image/png',
blob: imageBuffer.toString('base64'),
}],
};
// Multiple content items (e.g. a resource that returns several files)
return {
contents: [
{ uri: 'file:///src/main.js', mimeType: 'text/javascript', text: mainJsContent },
{ uri: 'file:///src/utils.js', mimeType: 'text/javascript', text: utilsJsContent },
],
};

users://{userId}/profile resolve to dynamic content.Resource Subscriptions
If a resource changes over time, the server can support subscriptions. Clients subscribe to a URI and receive notifications when its content changes. This is useful for live data: a log file that grows, a database record that updates, a sensor reading that changes.
// Server with subscription support
const server = new McpServer({
name: 'live-data-server',
version: '1.0.0',
capabilities: { resources: { subscribe: true } },
});
server.resource(
'live-metrics',
'metrics://system/cpu',
{ description: 'Live CPU usage percentage' },
async (uri) => {
const usage = await getCpuUsage();
return {
contents: [{ uri: uri.href, mimeType: 'text/plain', text: `${usage}%` }],
};
}
);
// When the data changes, notify subscribers:
setInterval(async () => {
server.server.notification({
method: 'notifications/resources/updated',
params: { uri: 'metrics://system/cpu' },
});
}, 5000); // every 5 seconds
Failure Modes with Resources
Case 1: Returning Mutable Data from Resources
Resources are semantically read-only. If your resource handler has side effects (incrementing a counter, logging access, triggering a build), you are violating the contract. Clients may cache resource responses and re-use them without re-fetching. Side effects in resource handlers lead to missed triggers and hard-to-reproduce bugs.
// BAD: Side effect in a resource handler
server.resource('report', 'reports://quarterly', {}, async (uri) => {
await markReportAsViewed(userId); // Side effect - will not fire on cached reads
return { contents: [{ uri: uri.href, text: reportContent }] };
});
// GOOD: Side effects belong in tools
server.tool('mark_report_viewed', '...', { report_id: z.string() }, async ({ report_id }) => {
await markReportAsViewed(report_id);
return { content: [{ type: 'text', text: 'Marked as viewed.' }] };
});
Case 2: Using Resources When Tools Are the Right Primitive
Resources are for pre-existing data the AI reads passively. If the data requires parameters that affect what is returned, the access has query semantics, or you need to aggregate data from multiple sources on the fly – that is a tool, not a resource.
// Ambiguous: is this a resource or a tool?
// If it takes user query parameters and runs a search algorithm -> Tool
// If it returns a fixed, addressable document -> Resource
// RESOURCE: Fixed, URI-addressable content
server.resource('user-manual', 'docs://user-manual', {}, handler);
// TOOL: Dynamic query with parameters
server.tool('search_docs', '...', { query: z.string() }, handler);
What to Check Right Now
- Identify your read-only data sources – any data your AI needs to read but not modify is a resource candidate: config files, user profiles, product catalogues, documentation.
- Use resource templates for parameterised access – if you have N users with profiles, use
users://{userId}/profilerather than registering N individual resources. - Enable subscriptions for live data – if any of your resources update frequently, implement subscription support so clients can receive push notifications rather than polling.
- Test resource listing – call
resources/listfrom the Inspector and verify all your direct resources appear with correct URIs and descriptions.
nJoy 😉
