MCP Resources: Giving the Model Read Access to Your Data
Tools get all the attention in MCP tutorials, but Resources are where you give the model read access to live data — files, databases, configs — without making it call a tool every time.
On this page
If you have read the first MCP walkthrough, you know how tools work — the model calls a function, gets a result, and uses it to answer. Resources are the other half of the protocol, and they work differently in ways that matter.
A Resource is data the client exposes for the model to read, rather than a function the model invokes. Think of it as the model pulling context rather than pushing a request. Files, database records, configuration state, live metrics — anything you want available in context without a round-trip tool call per item.
Tools vs Resources: the practical difference
The distinction is more than architectural:
| Tools | Resources | |
|---|---|---|
| Who decides when to use it | The model | The client / user |
| Interaction style | Request → response | Subscribe / read |
| Best for | Actions, fetching specific data | Background context, browsable data |
| Overhead | One call per piece of data | Loaded into context up front |
Tools are right when the model needs to decide what to fetch based on the conversation. Resources are right when data should simply be available — the model should know the current project config without needing to call get_project_config on every message.
Registering a Resource
The SDK's resource API mirrors the tool API closely:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import * as fs from "fs";
const server = new McpServer({ name: "file-server", version: "1.0.0" });
// Static resource — content known at registration time
server.registerResource(
"readme",
"file:///project/README.md",
{
title: "Project README",
description: "The project README. Read this to understand the codebase.",
mimeType: "text/markdown",
},
async () => ({
contents: [
{
uri: "file:///project/README.md",
mimeType: "text/markdown",
text: fs.readFileSync("./README.md", "utf-8"),
},
],
}),
);
const transport = new StdioServerTransport();
await server.connect(transport);The handler is called when the client reads the resource. The content is fetched fresh each time, so a resource backed by a file or a database query always returns the current state.
Dynamic resources with templates
Real use cases often need parameterized resources — not "the README" but "the logs for deployment X." MCP supports URI templates for this:
server.registerResourceTemplate(
"deployment-logs",
"logs://{deploymentId}",
{
title: "Deployment logs",
description: "CloudWatch logs for a given deployment ID.",
},
async ({ deploymentId }) => {
const logs = await fetchLogsFromCloudWatch(deploymentId);
return {
contents: [
{
uri: `logs://${deploymentId}`,
mimeType: "text/plain",
text: logs.join("\n"),
},
],
};
},
);The client can now request logs://deploy-abc123 and get the logs for that specific deployment. The model sees a list of available templates and constructs URIs to request what it needs.
Resource change notifications
For data that updates while a session is running — a live dashboard, a build status, a queue depth — the server can push change notifications:
// Tell the client that a resource's content has changed
server.notifyResourceUpdated("file:///project/README.md");Clients that subscribe to the resource will re-fetch it when they receive this notification. This is how you keep the model's context current without polling.
What to expose as a resource vs a tool
The heuristic that has worked for me:
- If the model should have it available as background reading — config files, project docs, environment state — use a Resource.
- If the model needs to decide whether and when to fetch it — database queries, API calls, actions — use a Tool.
- If it is large and the model will only sometimes need it, use a Tool so it is only fetched when needed.
Resources shine for the context a developer would paste into a prompt manually: the relevant config, the recent logs, the current schema. Once it is a Resource, the model always has it without anyone doing the pasting.