Understanding MCP Resources

Learn what MCP resources are, how they expose data to AI models through URI templates, the difference between static and dynamic resources, and how to build them with mcp-framework.


title: "Understanding MCP Resources" description: "Learn what MCP resources are, how they expose data to AI models through URI templates, the difference between static and dynamic resources, and how to build them with mcp-framework." order: 5 level: "beginner" duration: "12 min" keywords:

  • MCP resources
  • MCP resource development
  • MCPResource class
  • URI templates MCP
  • static vs dynamic resources
  • mcp-framework resources
  • AI data access
  • MCP resource patterns date: "2026-04-01"

Quick Summary

Resources are the data primitive in MCP. While tools let AI models perform actions, resources let them read data. Resources expose content through URIs — similar to how a web server exposes pages through URLs. This guide covers how to create both static and dynamic resources.

MCP Resource

An MCP Resource is a data endpoint that exposes content to AI models through the Model Context Protocol. Each resource has a URI (like config://app/settings or docs://api/users), a name, a description, and a MIME type. Resources provide a read-only way for AI assistants to access data from your server without performing actions.

What Are MCP Resources For?

If tools are the "write" side of MCP (performing actions), resources are the "read" side (accessing data). Resources are ideal for:

  • Configuration files — Expose application settings, environment configs
  • Database records — Provide read access to specific data tables or views
  • Documentation — Make internal docs, API references, or runbooks available
  • Log files — Let AI assistants read and analyze log data
  • System status — Expose health checks, metrics, or deployment info
  • File contents — Provide access to project files, templates, or examples

When Should You Use a Resource vs a Tool?

This is a common question. The distinction is:

  • Use a Resource when the AI needs to read data that already exists
  • Use a Tool when the AI needs to perform an action that has side effects
ScenarioUse ResourceUse Tool
Read a config fileYesNo
Query a databaseFor simple readsFor complex queries with parameters
Fetch API documentationYesNo
Create a new fileNoYes
Read server logsYesNo
Send an HTTP requestNoYes
List available schemasYesNo
Run a database migrationNoYes
Resources Are Read-Only

Resources in MCP are strictly for reading data. They do not accept input parameters the way tools do. If you need to filter, search, or parameterize data access, use a tool instead — or use a resource template with URI parameters.

How Do Resources Work in the MCP Protocol?

When an MCP client connects to your server, resources are discovered and accessed through two protocol operations:

  1. resources/list — The client asks the server for a list of available resources. The server responds with each resource's URI, name, description, and MIME type.
  2. resources/read — The client requests the content of a specific resource by its URI. The server responds with the resource's content.

Some clients also support resource subscriptions, where the server can notify the client when a resource's content changes.

How Do You Create a Resource with mcp-framework?

In mcp-framework, resources extend the MCPResource class. Here is a complete example:

import { MCPResource } from "mcp-framework";

class AppConfigResource extends MCPResource {
  uri = "config://app/settings";
  name = "Application Settings";
  description = "Current application configuration and settings";
  mimeType = "application/json";

  async read() {
    const config = {
      appName: "My Application",
      version: "2.1.0",
      environment: process.env.NODE_ENV || "development",
      features: {
        darkMode: true,
        notifications: true,
        analytics: false,
      },
      database: {
        host: "localhost",
        port: 5432,
        name: "myapp_db",
      },
    };

    return JSON.stringify(config, null, 2);
  }
}

export default AppConfigResource;

Required Properties

Every resource must define these properties:

| Property | Type | Purpose | |----------|------|---------| | uri | string | Unique identifier for the resource. Uses a URI format. | | name | string | Human-readable name displayed to clients. | | description | string | Explains what data the resource provides. | | mimeType | string | The content type (e.g., application/json, text/plain, text/markdown). |

And one required method:

| Method | Returns | Purpose | |--------|---------|---------| | read() | Promise<string> | Async method that returns the resource content as a string. |

Understanding Resource URIs

Resource URIs follow a scheme-based format similar to web URLs. The scheme indicates the type of resource:

scheme://authority/path

Common URI patterns include:

| URI Pattern | Use Case | |------------|----------| | config://app/settings | Application configuration | | docs://api/users | API documentation for user endpoints | | db://tables/users | Database table contents | | file://project/README.md | Project file contents | | logs://app/errors | Application error logs | | status://server/health | Server health information |

URI Naming Conventions

Choose a URI scheme that reflects the type of data. Use hierarchical paths for organization. Keep URIs consistent across your server — if you use db://tables/users, use db://tables/orders for another table, not database://orders/table.

Static Resources vs Dynamic Resources

Static Resources

A static resource returns the same content every time (or content that changes infrequently). Think of it like a static file on a web server.

import { MCPResource } from "mcp-framework";

class ApiDocsResource extends MCPResource {
  uri = "docs://api/overview";
  name = "API Documentation";
  description = "Overview of the REST API endpoints and authentication";
  mimeType = "text/markdown";

  async read() {
    return `# API Overview

## Authentication
All API requests require a Bearer token in the Authorization header.

## Base URL
\`https://api.example.com/v2\`

## Rate Limits
- Standard: 100 requests/minute
- Premium: 1000 requests/minute

## Endpoints
- GET /users - List all users
- GET /users/:id - Get user by ID
- POST /users - Create a new user
- PUT /users/:id - Update a user
- DELETE /users/:id - Delete a user
`;
  }
}

export default ApiDocsResource;

Dynamic Resources

A dynamic resource fetches fresh data on each read. This is useful for database records, live metrics, or any data that changes frequently.

import { MCPResource } from "mcp-framework";
import fs from "fs/promises";

class ServerLogsResource extends MCPResource {
  uri = "logs://app/recent";
  name = "Recent Server Logs";
  description = "The last 100 lines from the application log file";
  mimeType = "text/plain";

  async read() {
    try {
      const logPath = "/var/log/myapp/app.log";
      const content = await fs.readFile(logPath, "utf-8");
      const lines = content.split("\n");
      const recentLines = lines.slice(-100).join("\n");
      return recentLines;
    } catch (error) {
      return `Error reading logs: ${error instanceof Error ? error.message : String(error)}`;
    }
  }
}

export default ServerLogsResource;
import { MCPResource } from "mcp-framework";

class SystemStatusResource extends MCPResource {
  uri = "status://server/health";
  name = "Server Health Status";
  description = "Current server health metrics including uptime, memory, and CPU usage";
  mimeType = "application/json";

  async read() {
    const memUsage = process.memoryUsage();

    const status = {
      status: "healthy",
      uptime: Math.floor(process.uptime()),
      uptimeHuman: this.formatUptime(process.uptime()),
      memory: {
        heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024),
        heapTotal: Math.round(memUsage.heapTotal / 1024 / 1024),
        rss: Math.round(memUsage.rss / 1024 / 1024),
        unit: "MB",
      },
      nodeVersion: process.version,
      platform: process.platform,
      pid: process.pid,
    };

    return JSON.stringify(status, null, 2);
  }

  private formatUptime(seconds: number): string {
    const days = Math.floor(seconds / 86400);
    const hours = Math.floor((seconds % 86400) / 3600);
    const mins = Math.floor((seconds % 3600) / 60);
    return `${days}d ${hours}h ${mins}m`;
  }
}

export default SystemStatusResource;

Building Multiple Related Resources

A server often exposes multiple related resources. Here is a pattern for exposing database schema information:

import { MCPResource } from "mcp-framework";

class DatabaseSchemaResource extends MCPResource {
  uri = "db://schema/tables";
  name = "Database Tables";
  description = "List of all tables in the database with their column definitions";
  mimeType = "application/json";

  async read() {
    // In production, query your actual database schema
    const schema = {
      tables: [
        {
          name: "users",
          columns: [
            { name: "id", type: "serial", primaryKey: true },
            { name: "email", type: "varchar(255)", unique: true },
            { name: "name", type: "varchar(100)" },
            { name: "created_at", type: "timestamp", default: "now()" },
          ],
        },
        {
          name: "orders",
          columns: [
            { name: "id", type: "serial", primaryKey: true },
            { name: "user_id", type: "integer", foreignKey: "users.id" },
            { name: "total", type: "decimal(10,2)" },
            { name: "status", type: "varchar(20)" },
            { name: "created_at", type: "timestamp", default: "now()" },
          ],
        },
      ],
    };

    return JSON.stringify(schema, null, 2);
  }
}

export default DatabaseSchemaResource;

This is particularly powerful when combined with a SQL query tool — the AI can read the schema resource to understand the database structure, then use the query tool to fetch specific data.

Resources Enhance Tool Usage

Resources and tools work best together. Expose your database schema as a resource so the AI understands your data model, then provide query tools for data access. The AI will reference the schema resource when constructing queries, leading to more accurate results.

How Do You Create Resources with the Official SDK?

For comparison, here is how resources are defined with @modelcontextprotocol/sdk:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";

const server = new McpServer({
  name: "my-server",
  version: "1.0.0",
});

// Static resource
server.resource(
  "app-config",
  "config://app/settings",
  {
    description: "Current application configuration and settings",
    mimeType: "application/json",
  },
  async (uri) => ({
    contents: [
      {
        uri: uri.href,
        mimeType: "application/json",
        text: JSON.stringify({
          appName: "My Application",
          version: "2.1.0",
          environment: "production",
        }, null, 2),
      },
    ],
  })
);

// Resource template (for dynamic URIs)
server.resource(
  "user-profile",
  new ResourceTemplate("users://profile/{userId}", { list: undefined }),
  {
    description: "User profile data by user ID",
    mimeType: "application/json",
  },
  async (uri, { userId }) => ({
    contents: [
      {
        uri: uri.href,
        mimeType: "application/json",
        text: JSON.stringify({
          id: userId,
          name: "Jane Doe",
          email: "jane@example.com",
        }, null, 2),
      },
    ],
  })
);

The SDK uses ResourceTemplate for parameterized URIs, which is useful for resources that represent a collection of items where each has a unique URI.

Resource Organization Patterns

src
resources
AppConfigResource.ts
DatabaseSchemaResource.ts
ApiDocsResource.ts
ServerStatusResource.ts
RecentLogsResource.ts
Organize Resources by Domain

Group related resources logically. A database server might have db://schema/tables, db://schema/views, and db://statistics/overview. A documentation server might have docs://api/overview, docs://api/authentication, and docs://guides/quickstart. Consistent naming makes resources easier for both humans and AI to discover.

Common Resource Patterns

Configuration Exposer

Expose application configuration so AI can understand your system:

class EnvConfigResource extends MCPResource {
  uri = "config://env/current";
  name = "Environment Configuration";
  description = "Current environment variables and configuration (sensitive values masked)";
  mimeType = "application/json";

  async read() {
    return JSON.stringify({
      NODE_ENV: process.env.NODE_ENV,
      PORT: process.env.PORT,
      DATABASE_URL: this.maskSensitive(process.env.DATABASE_URL),
      API_KEY: this.maskSensitive(process.env.API_KEY),
    }, null, 2);
  }

  private maskSensitive(value?: string): string {
    if (!value) return "not set";
    if (value.length <= 8) return "****";
    return value.substring(0, 4) + "****" + value.substring(value.length - 4);
  }
}

export default EnvConfigResource;
Never Expose Secrets

Be extremely careful about what data your resources expose. Never return raw API keys, passwords, or other secrets. Always mask sensitive values. Remember that resource content is sent to AI models, which may log or display it.

Project File Map

Give AI a map of your project structure:

import { MCPResource } from "mcp-framework";
import fs from "fs/promises";
import path from "path";

class ProjectStructureResource extends MCPResource {
  uri = "project://structure";
  name = "Project File Structure";
  description = "Complete directory tree of the current project";
  mimeType = "text/plain";

  async read() {
    const projectRoot = process.cwd();
    const tree = await this.buildTree(projectRoot, "", 0, 3);
    return tree;
  }

  private async buildTree(
    dir: string,
    prefix: string,
    depth: number,
    maxDepth: number
  ): Promise<string> {
    if (depth >= maxDepth) return "";

    const entries = await fs.readdir(dir, { withFileTypes: true });
    const filtered = entries.filter(
      (e) => !e.name.startsWith(".") && e.name !== "node_modules" && e.name !== "dist"
    );

    let result = "";
    for (const [i, entry] of filtered.entries()) {
      const isLast = i === filtered.length - 1;
      const connector = isLast ? "└── " : "├── ";
      const nextPrefix = prefix + (isLast ? "    " : "│   ");

      result += `${prefix}${connector}${entry.name}\n`;

      if (entry.isDirectory()) {
        const subPath = path.join(dir, entry.name);
        result += await this.buildTree(subPath, nextPrefix, depth + 1, maxDepth);
      }
    }

    return result;
  }
}

export default ProjectStructureResource;

Testing Your Resources

You can test resources with MCP Inspector just like tools:

npx @modelcontextprotocol/inspector node dist/index.js

In the inspector, navigate to the Resources tab to see all registered resources, read their contents, and verify the output format.

Frequently Asked Questions


You now understand how to expose data through MCP resources. Next, learn about the third and final primitive — head to Understanding MCP Prompts to create reusable prompt templates.