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"

Quick Summary

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

MCP Resource

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).

AspectResourcesTools
PurposeProvide data/contextPerform actions
AnalogyGET endpointsPOST/PUT/DELETE endpoints
Side effectsNone (read-only)May have side effects
InputURI parameters onlyFull schema input
Use whenAI needs context to answerAI 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 Template Syntax

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";
}
Security for File Resources

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;
my-mcp-server/
src/
resources/
UserProfileResource.ts
SystemStatusResource.ts
WorkspaceFileResource.ts
tools/
UpdateUserTool.ts
index.ts

Resource Subscriptions

MCP supports subscription-based updates so clients are notified when resource data changes.

How Subscriptions Work

  1. Client subscribes to a resource URI
  2. Server acknowledges the subscription
  3. When resource data changes, server sends a notification
  4. 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}`);
  }
});
Resource Subscription

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",
    }],
  })
);
Cache External Resources

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",
      }],
    };
  }
);
Binary Resource Size

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

PatternWhen to UseExample URI
Static singletonServer config, health statussystem://config
Parameterized entityIndividual records by IDusers://{userId}/profile
Filtered collectionQueried listsorders://status/{status}
Hierarchical pathFile systems, nested dataworkspace://{path}
AggregateDashboards, summariesanalytics://dashboard/{period}

Frequently Asked Questions