MCP Resources: Expose Any Data Context to AI Models

Learn how MCP Resources work, how to define them in a server, and how AI models consume live data contexts. Complete 2026 guide with code.

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.log or db://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' → Run npm install again; native bindings sometimes need npm rebuild
  • TypeError: db.prepare is not a function → You imported the default export wrong; use import 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:

  1. Inspector UI opens at http://localhost:5173
  2. Resources tab lists docs://api-guidelines and db://users/1, db://users/2
  3. Clicking a resource shows its content in the preview pane
  4. After 5 seconds, db://users/1 shows 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
  • ResourceTemplate handles parameterized URIs; the SDK extracts path variables automatically
  • resources/list is 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