Skip to content

Adding MCP Resources

Guide for creating custom resources in Seed to expose structured data and configuration to MCP clients.

Overview

MCP resources provide structured, read-only access to data. Unlike tools (which perform actions) and prompts (which generate text), resources expose existing data and configuration for clients to read.

Use resources when you want to:

  • Expose configuration settings
  • Provide user profile data
  • Share application state
  • Offer structured documentation
  • Deliver real-time data feeds

Resource Concepts

Resource URIs

Resources are identified by URIs following a custom scheme:

scheme://path

Examples:

  • config://server - Server configuration
  • user://profile - User profile data
  • app://status - Application status
  • docs://getting-started - Documentation

Resource Types

Resources can provide different content types:

  • Text - Plain text or markdown
  • JSON - Structured data
  • Binary - Images, files (base64 encoded)

Resource Updates

Resources can notify clients when data changes using MCP's resource update protocol. This enables real-time data synchronization.

Creating a Resource

Step 1: Create Resource File

Create a new file in src/mcp/resources/:

typescript
// src/mcp/resources/my-resource.ts

import {Server as McpServer} from "@modelcontextprotocol/sdk/server/index.js";
import {
  ListResourcesRequestSchema,
  ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";

export function registerMyResource(server: McpServer): void {
  // Resource implementation will go here
}

Step 2: Define Resource Metadata

Add resource to the resources/list handler:

typescript
export function registerMyResource(server: McpServer): void {
  // List resources handler
  server.setRequestHandler(ListResourcesRequestSchema, async () => {
    return {
      resources: [
        {
          uri: "myapp://data",
          name: "Application Data",
          description: "Current application state and configuration",
          mimeType: "application/json",
        },
      ],
    };
  });
}

Resource metadata:

  • uri - Unique resource identifier
  • name - Human-readable name
  • description - What the resource contains
  • mimeType - Content type (text/plain, application/json, image/png, etc.)

Step 3: Implement Read Handler

Add resource content handler:

typescript
export function registerMyResource(server: McpServer): void {
  // ... list handler from above ...

  // Read resource handler
  server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
    const uri = request.params.uri;

    if (uri === "myapp://data") {
      const data = {
        version: "1.0.0",
        environment: process.env.NODE_ENV || "development",
        features: {
          authentication: true,
          rateLimit ing: true,
        },
        timestamp: new Date().toISOString(),
      };

      return {
        contents: [
          {
            uri: uri,
            mimeType: "application/json",
            text: JSON.stringify(data, null, 2),
          },
        ],
      };
    }

    throw new Error(`Unknown resource: ${uri}`);
  });
}

Step 4: Register Resource

Add to src/mcp/resources/index.ts:

typescript
import {Server as McpServer} from "@modelcontextprotocol/sdk/server/index.js";
import {registerConfigResource} from "./config.js";
import {registerUserResource} from "./user.js";
import {registerMyResource} from "./my-resource.js"; // Add import

export function registerAllResources(server: McpServer): void {
  registerConfigResource(server);
  registerUserResource(server);
  registerMyResource(server); // Add registration
}

Complete Example: Database Stats Resource

Here's a complete example exposing database statistics:

typescript
// src/mcp/resources/database-stats.ts

import {Server as McpServer} from "@modelcontextprotocol/sdk/server/index.js";
import {
  ListResourcesRequestSchema,
  ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import {getRedisClient} from "../../services/redis.js";

interface DatabaseStats {
  redis: {
    connected: boolean;
    memory: {
      used: string;
      peak: string;
    };
    keys: number;
    clients: number;
  };
  sessions: {
    active: number;
    total: number;
  };
}

async function getStats(): Promise<DatabaseStats> {
  const redis = getRedisClient();

  const info = await redis.info("memory");
  const dbSize = await redis.dbsize();
  const clients = await redis.clientList();
  const sessions = await redis.keys("seed:session:*");

  // Parse memory info
  const usedMemory = info.match(/used_memory_human:(.+)/)?.[1] || "unknown";
  const peakMemory = info.match(/used_memory_peak_human:(.+)/)?.[1] || "unknown";

  return {
    redis: {
      connected: true,
      memory: {
        used: usedMemory.trim(),
        peak: peakMemory.trim(),
      },
      keys: dbSize,
      clients: clients.length,
    },
    sessions: {
      active: sessions.length,
      total: sessions.length,
    },
  };
}

export function registerDatabaseStatsResource(server: McpServer): void {
  server.setRequestHandler(ListResourcesRequestSchema, async () => {
    return {
      resources: [
        {
          uri: "database://stats",
          name: "Database Statistics",
          description: "Real-time Redis and session statistics",
          mimeType: "application/json",
        },
      ],
    };
  });

  server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
    if (request.params.uri === "database://stats") {
      try {
        const stats = await getStats();

        return {
          contents: [
            {
              uri: request.params.uri,
              mimeType: "application/json",
              text: JSON.stringify(stats, null, 2),
            },
          ],
        };
      } catch (error) {
        throw new Error(`Failed to fetch database stats: ${error.message}`);
      }
    }

    throw new Error(`Unknown resource: ${request.params.uri}`);
  });
}

Resource Patterns

1. Static Configuration

Expose server configuration:

typescript
export function registerConfigResource(server: McpServer): void {
  server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
    if (request.params.uri === "config://server") {
      const config = {
        version: "1.0.0",
        auth: {
          required: process.env.AUTH_REQUIRED !== "false",
          provider: process.env.OIDC_ISSUER,
        },
        features: {
          tools: true,
          prompts: true,
          resources: true,
        },
      };

      return {
        contents: [
          {
            uri: request.params.uri,
            mimeType: "application/json",
            text: JSON.stringify(config, null, 2),
          },
        ],
      };
    }
  });
}

2. User-Specific Data

Expose authenticated user information:

typescript
export function registerUserResource(server: McpServer): void {
  server.setRequestHandler(ReadResourceRequestSchema, async (request, extra) => {
    if (request.params.uri === "user://profile") {
      // Access user context from request
      const user = (extra?.req as any)?.user;

      if (!user) {
        throw new Error("Authentication required");
      }

      const profile = {
        id: user.sub,
        email: user.email,
        name: user.name,
        groups: user.groups || [],
      };

      return {
        contents: [
          {
            uri: request.params.uri,
            mimeType: "application/json",
            text: JSON.stringify(profile, null, 2),
          },
        ],
      };
    }
  });
}

3. Dynamic Data Feeds

Provide real-time data:

typescript
export function registerMetricsResource(server: McpServer): void {
  server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
    if (request.params.uri === "metrics://current") {
      const metrics = {
        timestamp: new Date().toISOString(),
        system: {
          uptime: process.uptime(),
          memory: {
            used: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
            total: Math.round(process.memoryUsage().heapTotal / 1024 / 1024),
          },
          cpu: process.cpuUsage(),
        },
        requests: {
          total: global.requestCount || 0,
          rate: global.requestRate || 0,
        },
      };

      return {
        contents: [
          {
            uri: request.params.uri,
            mimeType: "application/json",
            text: JSON.stringify(metrics, null, 2),
          },
        ],
      };
    }
  });
}

4. Documentation Resources

Expose structured documentation:

typescript
export function registerDocsResource(server: McpServer): void {
  server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
    const uri = request.params.uri;
    const path = uri.replace("docs://", "");

    const docs: Record<string, string> = {
      "getting-started": `# Getting Started\\n\\nWelcome to Seed...`,
      "authentication": `# Authentication\\n\\nSeed uses OAuth 2.0...`,
      "tools": `# Available Tools\\n\\n1. healthcheck...`,
    };

    if (path in docs) {
      return {
        contents: [
          {
            uri: uri,
            mimeType: "text/markdown",
            text: docs[path],
          },
        ],
      };
    }

    throw new Error(`Documentation not found: ${path}`);
  });
}

5. Binary Resources

Expose images or files:

typescript
import fs from "fs/promises";
import path from "path";

export function registerImageResource(server: McpServer): void {
  server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
    if (request.params.uri.startsWith("image://")) {
      const imageName = request.params.uri.replace("image://", "");
      const imagePath = path.join(__dirname, "../../assets", imageName);

      try {
        const data = await fs.readFile(imagePath);
        const base64 = data.toString("base64");

        return {
          contents: [
            {
              uri: request.params.uri,
              mimeType: "image/png",
              blob: base64,
            },
          ],
        };
      } catch (error) {
        throw new Error(`Image not found: ${imageName}`);
      }
    }
  });
}

Resource Updates

Resources can notify clients when data changes:

typescript
export function registerRealtimeResource(server: McpServer): void {
  // Store current value
  let currentValue = {
    counter: 0,
    lastUpdate: new Date().toISOString(),
  };

  // Update every 5 seconds
  setInterval(() => {
    currentValue = {
      counter: currentValue.counter + 1,
      lastUpdate: new Date().toISOString(),
    };

    // Notify clients of update
    server.notification({
      method: "notifications/resources/updated",
      params: {
        uri: "realtime://counter",
      },
    });
  }, 5000);

  server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
    if (request.params.uri === "realtime://counter") {
      return {
        contents: [
          {
            uri: request.params.uri,
            mimeType: "application/json",
            text: JSON.stringify(currentValue, null, 2),
          },
        ],
      };
    }
  });
}

Multiple Resources in One File

You can handle multiple related resources in a single file:

typescript
export function registerAppResources(server: McpServer): void {
  server.setRequestHandler(ListResourcesRequestSchema, async () => {
    return {
      resources: [
        {
          uri: "app://status",
          name: "Application Status",
          description: "Current application health and status",
          mimeType: "application/json",
        },
        {
          uri: "app://version",
          name: "Application Version",
          description: "Version and build information",
          mimeType: "application/json",
        },
        {
          uri: "app://config",
          name: "Application Configuration",
          description: "Current runtime configuration",
          mimeType: "application/json",
        },
      ],
    };
  });

  server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
    const uri = request.params.uri;

    switch (uri) {
      case "app://status":
        return {
          contents: [
            {
              uri: uri,
              mimeType: "application/json",
              text: JSON.stringify({status: "ok", uptime: process.uptime()}, null, 2),
            },
          ],
        };

      case "app://version":
        return {
          contents: [
            {
              uri: uri,
              mimeType: "application/json",
              text: JSON.stringify({version: "1.0.0", build: "12345"}, null, 2),
            },
          ],
        };

      case "app://config":
        return {
          contents: [
            {
              uri: uri,
              mimeType: "application/json",
              text: JSON.stringify({env: process.env.NODE_ENV}, null, 2),
            },
          ],
        };

      default:
        throw new Error(`Unknown resource: ${uri}`);
    }
  });
}

Testing Resources

Manual Testing

Test resources using curl:

bash
# Initialize session
curl -X POST http://localhost:3000/mcp \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{
    "jsonrpc": "2.0",
    "method": "initialize",
    "params": {
      "protocolVersion": "2024-11-05",
      "capabilities": {"resources": {}},
      "clientInfo": {"name": "test", "version": "1.0"}
    },
    "id": 1
  }'

# List resources
curl -X POST http://localhost:3000/mcp \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{
    "jsonrpc": "2.0",
    "method": "resources/list",
    "id": 2
  }'

# Read resource
curl -X POST http://localhost:3000/mcp \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{
    "jsonrpc": "2.0",
    "method": "resources/read",
    "params": {"uri": "config://server"},
    "id": 3
  }'

Unit Testing

Create tests in src/mcp/resources/*.test.ts:

typescript
import {describe, it, expect, beforeEach} from "vitest";
import {Server as McpServer} from "@modelcontextprotocol/sdk/server/index.js";
import {registerMyResource} from "./my-resource.js";

describe("My Resource", () => {
  let server: McpServer;

  beforeEach(() => {
    server = new McpServer({name: "test", version: "1.0"}, {capabilities: {}});
    registerMyResource(server);
  });

  it("should list resources", async () => {
    const response = await server.request({
      method: "resources/list",
    }, {});

    expect(response.resources).toContainEqual(
      expect.objectContaining({
        uri: "myapp://data",
        name: "Application Data",
      })
    );
  });

  it("should read resource", async () => {
    const response = await server.request(
      {
        method: "resources/read",
        params: {uri: "myapp://data"},
      },
      {}
    );

    expect(response.contents).toHaveLength(1);
    expect(response.contents[0].uri).toBe("myapp://data");
    expect(response.contents[0].mimeType).toBe("application/json");

    const data = JSON.parse(response.contents[0].text);
    expect(data).toHaveProperty("version");
    expect(data).toHaveProperty("environment");
  });

  it("should throw error for unknown resource", async () => {
    await expect(
      server.request(
        {
          method: "resources/read",
          params: {uri: "unknown://resource"},
        },
        {}
      )
    ).rejects.toThrow("Unknown resource");
  });
});

Best Practices

1. Use Descriptive URIs

typescript
// Good
"config://server"
"user://profile"
"metrics://system/memory"
"docs://getting-started"

// Avoid
"resource1"
"data"
"myresource"

2. Provide Clear Descriptions

typescript
{
  uri: "metrics://requests",
  name: "Request Metrics",
  description: "Real-time HTTP request statistics including rate, latency, and error counts",
  mimeType: "application/json"
}

3. Handle Errors Gracefully

typescript
try {
  const data = await fetchData();
  return {contents: [{uri, mimeType: "application/json", text: JSON.stringify(data)}]};
} catch (error) {
  throw new Error(`Failed to fetch resource: ${error.message}`);
}

4. Sanitize Sensitive Data

typescript
// Remove sensitive fields before exposing
const config = {
  version: "1.0.0",
  database: {
    host: process.env.DB_HOST,
    // Don't expose password!
  },
};

5. Use Appropriate MIME Types

typescript
// JSON data
mimeType: "application/json"

// Markdown documentation
mimeType: "text/markdown"

// Plain text
mimeType: "text/plain"

// Images
mimeType: "image/png"
mimeType: "image/jpeg"

6. Document Your Resources

Add comments explaining what each resource provides:

typescript
/**
 * Exposes current system metrics including:
 * - Memory usage (used, total, percentage)
 * - CPU usage (user, system)
 * - Uptime
 * - Request statistics
 *
 * Updates every 5 seconds via resource update notifications.
 */
export function registerMetricsResource(server: McpServer): void {
  // ...
}

Common Pitfalls

1. Not Handling Unknown URIs

typescript
// Bad - falls through
if (uri === "known://resource") {
  return {...};
}
// Returns undefined

// Good - explicit error
if (uri === "known://resource") {
  return {...};
}
throw new Error(`Unknown resource: ${uri}`);

2. Exposing Sensitive Data

typescript
// Bad - exposes secrets
const config = {
  apiKey: process.env.API_KEY, // Don't do this!
};

// Good - sanitize
const config = {
  features: {apiEnabled: !!process.env.API_KEY},
};

3. Slow Resource Handlers

typescript
// Bad - blocks for long time
const data = await verySlowOperation(); // 10+ seconds

// Good - cache or optimize
const cachedData = cache.get("resource") || await fetchAndCache();

Examples in Seed

Study existing resources:

  • src/mcp/resources/config.ts - Server configuration resource
  • src/mcp/resources/user.ts - User profile resource

Next Steps

  1. Plan your resource - Define URI scheme and data structure
  2. Implement resource - Create file and handlers
  3. Register resource - Add to index.ts
  4. Test thoroughly - Write unit tests and manual tests
  5. Document - Add user documentation in /resources/
  6. Deploy - Rebuild and restart server

Released under the MIT License.