Dynamic Resources & Templates
Learn to build dynamic MCP resources with URI templates, runtime resource generation, subscription-based updates, and real-time data feeds using the official TypeScript SDK and mcp-framework.
title: "Dynamic Resources & Templates" description: "Learn to build dynamic MCP resources with URI templates, runtime resource generation, subscription-based updates, and real-time data feeds using the official TypeScript SDK and mcp-framework." order: 11 level: "intermediate" duration: "25 min" keywords:
- "MCP resources"
- "MCP URI templates"
- "MCP dynamic resources"
- "MCP resource subscriptions"
- "MCP resource updates"
- "@modelcontextprotocol/sdk resources"
- "mcp-framework resources"
- "MCP real-time data" date: "2026-04-01"
MCP resources expose data to AI models — think of them as a read-only data layer for your server. This lesson goes beyond static resources to cover URI templates for parameterized data, dynamic resource generation, subscription mechanisms for real-time updates, and patterns for serving live data from databases, APIs, and file systems. You will see examples using both the official TypeScript SDK and mcp-framework.
Resources vs Tools: When to Use Which
A named, URI-identified piece of data that an MCP server exposes for AI models to read. Resources are the "nouns" of MCP — they represent data, while tools represent actions. Resources can be static (always the same) or dynamic (generated on each request).
| Aspect | Resources | Tools |
|---|---|---|
| Purpose | Provide data/context | Perform actions |
| Analogy | GET endpoints | POST/PUT/DELETE endpoints |
| Side effects | None (read-only) | May have side effects |
| Input | URI parameters only | Full schema input |
| Use when | AI needs context to answer | AI needs to do something |
A common pattern is to expose data as resources for reading, and provide tools for modifying that same data.
URI Templates
URI templates let you define parameterized resources. Instead of registering one resource per entity, you define a template that matches a pattern.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
const server = new McpServer({
name: "data-server",
version: "1.0.0",
});
// Static resource — fixed URI
server.resource(
"system-status",
"system://status",
{ description: "Current system health status" },
async () => ({
contents: [{
uri: "system://status",
text: JSON.stringify(await getSystemHealth()),
mimeType: "application/json",
}],
})
);
// Template resource — parameterized URI
server.resource(
"user-profile",
"users://{userId}/profile",
{ description: "User profile data by user ID" },
async (uri, { userId }) => {
const user = await db.users.findById(userId);
if (!user) {
return { contents: [] };
}
return {
contents: [{
uri: uri.href,
text: JSON.stringify(user),
mimeType: "application/json",
}],
};
}
);
URI templates use {paramName} placeholders following RFC 6570. The SDK automatically parses these and passes the extracted values to your handler. Multiple parameters are supported: projects://{orgId}/{projectId}/config.
Multi-Parameter Templates
// Resource with multiple URI parameters
server.resource(
"project-file",
"projects://{projectId}/files/{filePath}",
{ description: "File contents within a project" },
async (uri, { projectId, filePath }) => {
const project = await projects.get(projectId);
const content = await project.readFile(filePath);
const mimeType = filePath.endsWith(".json")
? "application/json"
: filePath.endsWith(".ts")
? "text/typescript"
: "text/plain";
return {
contents: [{
uri: uri.href,
text: content,
mimeType,
}],
};
}
);
Dynamic Resource Generation
Sometimes you do not know the full set of resources at server startup. Dynamic resources are generated based on runtime state.
Listing Dynamic Resources
The MCP protocol's resources/list call lets clients discover available resources. For dynamic resources, your list handler should query the current state:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { ListResourcesRequestSchema } from "@modelcontextprotocol/sdk/types.js";
const server = new McpServer({
name: "dynamic-server",
version: "1.0.0",
});
// Register a template for individual documents
server.resource(
"document",
"docs://{docId}",
{ description: "A document by its ID" },
async (uri, { docId }) => {
const doc = await documents.findById(docId);
return {
contents: [{
uri: uri.href,
text: JSON.stringify(doc),
mimeType: "application/json",
}],
};
}
);
File System Resources
A practical example — exposing a directory as browsable resources:
import fs from "fs/promises";
import path from "path";
const WORKSPACE_ROOT = "/workspace";
// Template resource for any file in the workspace
server.resource(
"workspace-file",
"workspace://{filepath}",
{ description: "Files in the workspace directory" },
async (uri, { filepath }) => {
const fullPath = path.resolve(WORKSPACE_ROOT, filepath);
// Security: prevent directory traversal
if (!fullPath.startsWith(WORKSPACE_ROOT)) {
return { contents: [] };
}
try {
const stat = await fs.stat(fullPath);
if (stat.isDirectory()) {
const entries = await fs.readdir(fullPath, { withFileTypes: true });
const listing = entries.map(e => ({
name: e.name,
type: e.isDirectory() ? "directory" : "file",
}));
return {
contents: [{
uri: uri.href,
text: JSON.stringify(listing, null, 2),
mimeType: "application/json",
}],
};
}
const content = await fs.readFile(fullPath, "utf-8");
return {
contents: [{
uri: uri.href,
text: content,
mimeType: getMimeType(filepath),
}],
};
} catch {
return { contents: [] };
}
}
);
function getMimeType(filepath: string): string {
const ext = path.extname(filepath).toLowerCase();
const mimeTypes: Record<string, string> = {
".json": "application/json",
".ts": "text/typescript",
".js": "text/javascript",
".md": "text/markdown",
".yaml": "text/yaml",
".yml": "text/yaml",
".html": "text/html",
".css": "text/css",
};
return mimeTypes[ext] || "text/plain";
}
Always validate and sanitize file paths. Use path.resolve() and verify the resulting path stays within your allowed root directory. Never expose system directories or sensitive files like .env through MCP resources.
Resources in mcp-framework
The mcp-framework uses a class-based approach for resources with automatic registration:
import { MCPResource } from "mcp-framework";
class UserProfileResource extends MCPResource {
uri = "users://{userId}/profile";
name = "user-profile";
description = "User profile data by user ID";
mimeType = "application/json";
async read(uri: URL, params: { userId: string }) {
const user = await db.users.findById(params.userId);
if (!user) return [];
return [{
uri: uri.href,
text: JSON.stringify(user),
mimeType: this.mimeType,
}];
}
}
export default UserProfileResource;
Resource Subscriptions
MCP supports subscription-based updates so clients are notified when resource data changes.
How Subscriptions Work
- Client subscribes to a resource URI
- Server acknowledges the subscription
- When resource data changes, server sends a notification
- Client re-reads the resource to get updated data
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
// Using the lower-level Server class for subscription support
const server = new Server(
{ name: "realtime-server", version: "1.0.0" },
{ capabilities: { resources: { subscribe: true } } }
);
// Track subscriptions
const subscriptions = new Map<string, Set<string>>();
server.setRequestHandler(
"resources/subscribe",
async (request) => {
const { uri } = request.params;
if (!subscriptions.has(uri)) {
subscriptions.set(uri, new Set());
}
subscriptions.get(uri)!.add(uri);
return {};
}
);
server.setRequestHandler(
"resources/unsubscribe",
async (request) => {
const { uri } = request.params;
subscriptions.get(uri)?.delete(uri);
return {};
}
);
// Notify subscribers when data changes
async function notifyResourceChanged(uri: string) {
if (subscriptions.has(uri)) {
await server.notification({
method: "notifications/resources/updated",
params: { uri },
});
}
}
// Example: watch for file changes and notify
import { watch } from "fs";
watch("/workspace", { recursive: true }, (event, filename) => {
if (filename) {
notifyResourceChanged(`workspace://${filename}`);
}
});
A mechanism where an MCP client registers interest in a specific resource URI. The server then pushes notifications when that resource changes, allowing the client to re-read the updated data. Not all clients support subscriptions.
Real-Time Data Patterns
Database-Backed Resources
// Resource backed by a database query
server.resource(
"recent-orders",
"orders://recent",
{ description: "Most recent 20 orders" },
async () => {
const orders = await db.query(
"SELECT id, customer, total, status, created_at FROM orders ORDER BY created_at DESC LIMIT 20"
);
return {
contents: [{
uri: "orders://recent",
text: JSON.stringify(orders.rows, null, 2),
mimeType: "application/json",
}],
};
}
);
// Resource with filtered template
server.resource(
"orders-by-status",
"orders://status/{status}",
{ description: "Orders filtered by status" },
async (uri, { status }) => {
const validStatuses = ["pending", "processing", "shipped", "delivered"];
if (!validStatuses.includes(status)) {
return { contents: [] };
}
const orders = await db.query(
"SELECT * FROM orders WHERE status = $1 ORDER BY created_at DESC LIMIT 50",
[status]
);
return {
contents: [{
uri: uri.href,
text: JSON.stringify(orders.rows, null, 2),
mimeType: "application/json",
}],
};
}
);
API-Backed Resources with Caching
// Simple in-memory cache
const cache = new Map<string, { data: string; expiry: number }>();
function cachedResource(ttlMs: number) {
return function (
fetchFn: () => Promise<string>
): () => Promise<string> {
return async () => {
const key = fetchFn.toString();
const cached = cache.get(key);
if (cached && cached.expiry > Date.now()) {
return cached.data;
}
const data = await fetchFn();
cache.set(key, { data, expiry: Date.now() + ttlMs });
return data;
};
};
}
const fetchWeather = cachedResource(5 * 60 * 1000)(async () => {
const response = await fetch("https://api.weather.example/current");
return await response.text();
});
server.resource(
"current-weather",
"weather://current",
{ description: "Current weather conditions (updated every 5 minutes)" },
async () => ({
contents: [{
uri: "weather://current",
text: await fetchWeather(),
mimeType: "application/json",
}],
})
);
Always cache resources backed by external APIs. AI models may read resources multiple times during a conversation. Without caching, each read triggers an API call, increasing latency and potentially hitting rate limits. A 1-5 minute TTL works well for most use cases.
Multi-Content Resources
A single resource can return multiple content items:
server.resource(
"project-overview",
"projects://{projectId}/overview",
{ description: "Complete project overview with config and recent activity" },
async (uri, { projectId }) => {
const [config, activity, metrics] = await Promise.all([
getProjectConfig(projectId),
getRecentActivity(projectId),
getProjectMetrics(projectId),
]);
return {
contents: [
{
uri: `projects://${projectId}/config`,
text: JSON.stringify(config, null, 2),
mimeType: "application/json",
},
{
uri: `projects://${projectId}/activity`,
text: JSON.stringify(activity, null, 2),
mimeType: "application/json",
},
{
uri: `projects://${projectId}/metrics`,
text: JSON.stringify(metrics, null, 2),
mimeType: "application/json",
},
],
};
}
);
Binary Resources
Resources can serve binary data using base64 encoding:
server.resource(
"chart-image",
"charts://{chartId}",
{ description: "Generated chart image" },
async (uri, { chartId }) => {
const chartConfig = await getChartConfig(chartId);
const imageBuffer = await renderChart(chartConfig);
return {
contents: [{
uri: uri.href,
blob: imageBuffer.toString("base64"),
mimeType: "image/png",
}],
};
}
);
Keep binary resources under a few megabytes. Large binary payloads slow down the MCP protocol and may exceed client-side limits. For large files, consider returning a URL or file path instead.
Resource Design Patterns
| Pattern | When to Use | Example URI |
|---|---|---|
| Static singleton | Server config, health status | system://config |
| Parameterized entity | Individual records by ID | users://{userId}/profile |
| Filtered collection | Queried lists | orders://status/{status} |
| Hierarchical path | File systems, nested data | workspace://{path} |
| Aggregate | Dashboards, summaries | analytics://dashboard/{period} |