What Are MCP Resources and Why They Matter
You've built a tool-calling MCP server. The AI can run functions — but it still can't see your data unless you explicitly pass it in every prompt.
That's the gap MCP Resources fills.
Resources are named, URI-addressable data contexts that a client (like Claude Desktop or your own app) can fetch and inject into the model's context window. Think of them as read-only files the AI can open on demand — database records, config files, API snapshots, documentation pages.
You'll learn:
- How the MCP Resource protocol works under the hood
- How to define static and dynamic resources in a TypeScript MCP server
- How to expose live data (e.g., a database row) as a resource
- How clients list, read, and subscribe to resource updates
Time: 20 min | Difficulty: Intermediate
How MCP Resources Work
The MCP spec defines three resource primitives:
Client MCP Server
│ │
├─ resources/list ────────▶│ Returns URI list + metadata
│◀─────────────────────────┤
│ │
├─ resources/read ────────▶│ Returns content for one URI
│◀─────────────────────────┤
│ │
├─ resources/subscribe ───▶│ Notify me when URI changes
│◀─ notifications/updated ─┤
Each resource has:
- A URI (e.g.,
file:///logs/app.logordb://users/42) - A MIME type (
text/plain,application/json,text/markdown) - Contents — either inline text or a base64-encoded blob
The client decides when to fetch a resource and where to inject it. Most clients inject it as a system-level context block before the user message.
Setting Up an MCP Server with Resources
Start from a minimal TypeScript MCP server using the official SDK.
Step 1: Install Dependencies
mkdir mcp-resources-demo && cd mcp-resources-demo
npm init -y
# MCP SDK + TypeScript tooling
npm install @modelcontextprotocol/sdk zod
npm install -D typescript tsx @types/node
Add a tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"strict": true
}
}
Expected: node_modules folder with @modelcontextprotocol/sdk installed.
Step 2: Define a Static Resource
Create src/server.ts. This first example exposes a static markdown document as a resource — useful for injecting docs or README content into context.
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = new McpServer({
name: "data-context-server",
version: "1.0.0",
});
// Static resource — content defined at server startup
server.resource(
"api-guidelines", // internal name
"docs://api-guidelines", // URI the client uses to fetch
async (uri) => ({
contents: [
{
uri: uri.href,
mimeType: "text/markdown",
// The AI will read this text as context
text: `# API Guidelines\n\n- Always paginate with cursor tokens\n- Return 422 for validation errors, not 400\n- Include \`request_id\` in every response`,
},
],
})
);
const transport = new StdioServerTransport();
await server.connect(transport);
Run it to verify:
npx tsx src/server.ts
Expected: Process stays open, waiting on stdin (stdio transport). No crash = server registered correctly.
Step 3: Define a Dynamic Resource from a Database
Static resources are fine for docs. The real power is resources that fetch live data when read.
This example exposes individual database rows via a URI template:
import Database from "better-sqlite3";
const db = new Database("./app.db");
// Seed a test table if empty
db.exec(`
CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, role TEXT);
INSERT OR IGNORE INTO users VALUES (1, 'Alice', 'admin'), (2, 'Bob', 'viewer');
`);
// ResourceTemplate lets you define URI patterns with parameters
server.resource(
"user-record",
new ResourceTemplate("db://users/{userId}", { list: undefined }),
async (uri, { userId }) => {
// userId is extracted from the URI path automatically
const user = db.prepare("SELECT * FROM users WHERE id = ?").get(userId);
if (!user) {
throw new Error(`User ${userId} not found`);
}
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
// Serialize the row as JSON — the model gets this as context
text: JSON.stringify(user, null, 2),
},
],
};
}
);
Install better-sqlite3:
npm install better-sqlite3
npm install -D @types/better-sqlite3
If it fails:
Error: Cannot find module 'better-sqlite3'→ Runnpm installagain; native bindings sometimes neednpm rebuildTypeError: db.prepare is not a function→ You imported the default export wrong; useimport Database from 'better-sqlite3'
Step 4: Implement resources/list
Clients call resources/list to discover what's available. Without it, Claude Desktop won't show your resources in the UI.
// The SDK collects registered resources automatically when you use server.resource()
// For dynamic collections (e.g., all rows), implement a custom list handler:
server.setRequestHandler(ListResourcesRequestSchema, async () => {
// Fetch all user IDs and build URIs dynamically
const users = db.prepare("SELECT id, name FROM users").all() as { id: number; name: string }[];
return {
resources: users.map((u) => ({
uri: `db://users/${u.id}`,
name: `User: ${u.name}`,
mimeType: "application/json",
description: `Profile record for ${u.name}`,
})),
};
});
Add the missing import at the top of your file:
import { ListResourcesRequestSchema } from "@modelcontextprotocol/sdk/types.js";
Step 5: Add Resource Change Notifications
If your data changes, you can push an update notification to the client. The client re-fetches the resource and refreshes the model's context.
import { ResourceUpdatedNotificationSchema } from "@modelcontextprotocol/sdk/types.js";
// Call this whenever a user record changes
async function notifyUserUpdated(userId: number) {
await server.notification({
method: "notifications/resources/updated",
params: { uri: `db://users/${userId}` },
});
}
// Example: simulate an update after 5 seconds
setTimeout(() => notifyUserUpdated(1), 5000);
Clients that have subscribed to this URI will re-read it automatically. Clients that haven't subscribed ignore the notification.
Verification
Test the full server with the MCP Inspector:
npx @modelcontextprotocol/inspector npx tsx src/server.ts
You should see:
- Inspector UI opens at
http://localhost:5173 - Resources tab lists
docs://api-guidelinesanddb://users/1,db://users/2 - Clicking a resource shows its content in the preview pane
- After 5 seconds,
db://users/1shows a refresh indicator (notification fired)
To test inside Claude Desktop, add to ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"data-context": {
"command": "npx",
"args": ["tsx", "/absolute/path/to/src/server.ts"]
}
}
}
Restart Claude Desktop. In any conversation, type / to open the resource picker and attach a resource to context.
Production Considerations
Access control: Resources have no built-in auth. If your server is multi-tenant, validate the requesting client's identity in each read handler before returning data.
Size limits: Most MCP clients cap injected context at ~100KB per resource. For large datasets, return a summary or paginated slice — not the full table dump.
Caching: The SDK doesn't cache resource reads. If your read handler hits a database on every call, add an in-process cache (e.g., a Map with TTL) for resources that change infrequently.
Binary resources: Set mimeType to image/png or application/pdf and return blob (base64 string) instead of text. Not all clients render binary resources — check your target client's docs before relying on this.
What You Learned
- MCP Resources expose named, URI-addressable data that AI models consume as context — separate from tool calls
ResourceTemplatehandles parameterized URIs; the SDK extracts path variables automaticallyresources/listis required for clients to discover resources; implement it for dynamic collections- Change notifications let the model react to live data updates without the user re-prompting
- Keep resource payloads under ~100KB and add access control in the read handler for production use
Tested on @modelcontextprotocol/sdk 1.8.0, Node.js 22.x, Claude Desktop 0.10.x, macOS Sequoia and Ubuntu 24.04