Chapter 5: Resources Management

Haiyue
16min

Chapter 5: Resources Management

Learning Objectives

  1. Understand the concept and purpose of Resources
  2. Learn to create and manage various resource types
  3. Master resource reading, updating, and listening mechanisms
  4. Implement resource providers for file systems, databases, etc.
  5. Learn resource permissions and security control

1. Resources Concept Overview

1.1 What are Resources

Resources are a mechanism in the MCP protocol for providing various types of data sources and content to AI models. Unlike Tools, Resources are primarily used for:

  • Providing readable data content
  • Exposing various information sources to AI models
  • Implementing dynamic content access
  • Supporting real-time data updates

1.2 Resources vs Tools Differences

FeatureResourcesTools
PurposeProvide data and contentExecute operations and tasks
Access MethodRead-focusedExecution-focused
Data FlowServer → ClientClient → Server → Client
Real-timeSupports change listeningOn-demand execution

2. Resource Types and Structure

2.1 Resource Basic Structure

interface Resource {
  uri: string;           // Resource unique identifier
  name: string;          // Resource display name
  description?: string;  // Resource description
  mimeType?: string;     // MIME type
  annotations?: {        // Metadata annotations
    audience?: ["human" | "assistant"];
    priority?: number;
  };
}

2.2 Common Resource Types

  1. File Resources - Local file system content
  2. Database Resources - Database query results
  3. API Resources - External API data
  4. Configuration Resources - Application configuration information
  5. Log Resources - System log data

3. Implementing Resources Functionality

3.1 Basic Resource Provider

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { z } from "zod";

class ResourceManager {
  private server: Server;
  private resources: Map<string, any> = new Map();

  constructor(server: Server) {
    this.server = server;
    this.setupResourceHandlers();
  }

  private setupResourceHandlers() {
    // List available resources
    this.server.setRequestHandler(
      "resources/list",
      async () => {
        return {
          resources: Array.from(this.resources.values())
        };
      }
    );

    // Read specific resource
    this.server.setRequestHandler(
      "resources/read",
      async (request) => {
        const { uri } = request.params;
        return await this.readResource(uri);
      }
    );
  }

  async readResource(uri: string) {
    const resource = this.resources.get(uri);
    if (!resource) {
      throw new Error(`Resource not found: ${uri}`);
    }

    // Read content based on resource type
    const content = await this.loadResourceContent(resource);

    return {
      contents: [{
        uri: resource.uri,
        mimeType: resource.mimeType,
        text: content
      }]
    };
  }

  private async loadResourceContent(resource: any): Promise<string> {
    // Implement specific resource content loading logic
    return "Resource content";
  }
}

3.2 File System Resource Provider

import { promises as fs } from "fs";
import path from "path";

class FileSystemResourceProvider {
  private basePath: string;

  constructor(basePath: string) {
    this.basePath = path.resolve(basePath);
  }

  async listResources(): Promise<Resource[]> {
    const files = await this.scanDirectory(this.basePath);
    return files.map(file => ({
      uri: `file://${file}`,
      name: path.basename(file),
      description: `File: ${path.relative(this.basePath, file)}`,
      mimeType: this.getMimeType(file)
    }));
  }

  async readResource(uri: string): Promise<string> {
    const filePath = uri.replace("file://", "");

    // Security check: ensure file is within allowed directory
    if (!filePath.startsWith(this.basePath)) {
      throw new Error("Access denied: file outside allowed directory");
    }

    try {
      const content = await fs.readFile(filePath, "utf-8");
      return content;
    } catch (error) {
      throw new Error(`Failed to read file: ${error.message}`);
    }
  }

  private async scanDirectory(dir: string): Promise<string[]> {
    const files: string[] = [];
    const entries = await fs.readdir(dir, { withFileTypes: true });

    for (const entry of entries) {
      const fullPath = path.join(dir, entry.name);
      if (entry.isDirectory()) {
        files.push(...await this.scanDirectory(fullPath));
      } else {
        files.push(fullPath);
      }
    }

    return files;
  }

  private getMimeType(filePath: string): string {
    const ext = path.extname(filePath).toLowerCase();
    const mimeTypes: Record<string, string> = {
      ".txt": "text/plain",
      ".md": "text/markdown",
      ".json": "application/json",
      ".js": "text/javascript",
      ".ts": "text/typescript",
      ".py": "text/x-python",
      ".html": "text/html",
      ".css": "text/css"
    };
    return mimeTypes[ext] || "text/plain";
  }
}

4. Database Resource Provider

4.1 Database Query Resources

import { Database } from "sqlite3";

class DatabaseResourceProvider {
  private db: Database;

  constructor(dbPath: string) {
    this.db = new Database(dbPath);
  }

  async listResources(): Promise<Resource[]> {
    return [
      {
        uri: "db://users",
        name: "Users Table",
        description: "All users in the system",
        mimeType: "application/json"
      },
      {
        uri: "db://orders",
        name: "Orders Table",
        description: "Order records",
        mimeType: "application/json"
      }
    ];
  }

  async readResource(uri: string): Promise<string> {
    const table = uri.replace("db://", "");

    return new Promise((resolve, reject) => {
      this.db.all(`SELECT * FROM ${table} LIMIT 100`, (err, rows) => {
        if (err) {
          reject(new Error(`Database query failed: ${err.message}`));
        } else {
          resolve(JSON.stringify(rows, null, 2));
        }
      });
    });
  }

  async executeQuery(query: string): Promise<string> {
    return new Promise((resolve, reject) => {
      this.db.all(query, (err, rows) => {
        if (err) {
          reject(new Error(`Query failed: ${err.message}`));
        } else {
          resolve(JSON.stringify(rows, null, 2));
        }
      });
    });
  }
}

5. Resource Listening and Change Notifications

5.1 Resource Change Listening

import { EventEmitter } from "events";
import { watch } from "chokidar";

class ResourceWatcher extends EventEmitter {
  private watchers: Map<string, any> = new Map();

  watchFileResource(uri: string, filePath: string) {
    const watcher = watch(filePath);

    watcher.on("change", () => {
      this.emit("resourceChanged", {
        uri,
        changeType: "modified"
      });
    });

    watcher.on("unlink", () => {
      this.emit("resourceChanged", {
        uri,
        changeType: "deleted"
      });
    });

    this.watchers.set(uri, watcher);
  }

  unwatchResource(uri: string) {
    const watcher = this.watchers.get(uri);
    if (watcher) {
      watcher.close();
      this.watchers.delete(uri);
    }
  }

  stopAllWatchers() {
    for (const watcher of this.watchers.values()) {
      watcher.close();
    }
    this.watchers.clear();
  }
}

5.2 Notify Client of Resource Changes

class ManagedResourceProvider {
  private server: Server;
  private watcher: ResourceWatcher;

  constructor(server: Server) {
    this.server = server;
    this.watcher = new ResourceWatcher();
    this.setupChangeNotifications();
  }

  private setupChangeNotifications() {
    this.watcher.on("resourceChanged", (change) => {
      // Notify client of resource changes
      this.server.notification({
        method: "notifications/resources/updated",
        params: {
          uri: change.uri,
          changeType: change.changeType
        }
      });
    });
  }

  addFileResource(uri: string, filePath: string) {
    // Add file resource and start watching
    this.watcher.watchFileResource(uri, filePath);
  }
}

6. Resource Permissions and Security Control

6.1 Access Permission Control

interface ResourcePermission {
  uri: string;
  allowedOperations: ("read" | "write" | "delete")[];
  accessLevel: "public" | "protected" | "private";
}

class ResourceSecurityManager {
  private permissions: Map<string, ResourcePermission> = new Map();

  setResourcePermission(permission: ResourcePermission) {
    this.permissions.set(permission.uri, permission);
  }

  checkReadAccess(uri: string, userContext?: any): boolean {
    const permission = this.permissions.get(uri);
    if (!permission) {
      return false; // Default deny access
    }

    // Check read permission
    if (!permission.allowedOperations.includes("read")) {
      return false;
    }

    // Perform permission checks based on access level
    switch (permission.accessLevel) {
      case "public":
        return true;
      case "protected":
        return this.validateUserAccess(userContext);
      case "private":
        return this.validateAdminAccess(userContext);
      default:
        return false;
    }
  }

  private validateUserAccess(userContext?: any): boolean {
    // Implement user permission validation logic
    return userContext && userContext.authenticated;
  }

  private validateAdminAccess(userContext?: any): boolean {
    // Implement admin permission validation logic
    return userContext && userContext.role === "admin";
  }
}

6.2 Input Validation and Sanitization

class ResourceValidator {
  static validateUri(uri: string): boolean {
    // URI format validation
    try {
      new URL(uri);
      return true;
    } catch {
      return /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(uri);
    }
  }

  static sanitizeContent(content: string, mimeType: string): string {
    switch (mimeType) {
      case "text/html":
        return this.sanitizeHtml(content);
      case "application/json":
        return this.sanitizeJson(content);
      default:
        return content;
    }
  }

  private static sanitizeHtml(html: string): string {
    // Remove potentially malicious HTML content
    return html
      .replace(/<script[^>]*>.*?<\/script>/gi, "")
      .replace(/on\w+="[^"]*"/gi, "")
      .replace(/javascript:/gi, "");
  }

  private static sanitizeJson(json: string): string {
    try {
      // Validate JSON format and re-serialize
      const parsed = JSON.parse(json);
      return JSON.stringify(parsed, null, 2);
    } catch {
      throw new Error("Invalid JSON content");
    }
  }
}

7. Complete Resource Server Example

7.1 Comprehensive Resource Server

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

class ComprehensiveResourceServer {
  private server: Server;
  private fileProvider: FileSystemResourceProvider;
  private dbProvider: DatabaseResourceProvider;
  private securityManager: ResourceSecurityManager;
  private watcher: ResourceWatcher;

  constructor() {
    this.server = new Server(
      {
        name: "comprehensive-resource-server",
        version: "1.0.0"
      },
      {
        capabilities: {
          resources: {
            subscribe: true,  // Support resource change subscriptions
            listChanged: true // Support resource list changes
          }
        }
      }
    );

    this.fileProvider = new FileSystemResourceProvider("./data");
    this.dbProvider = new DatabaseResourceProvider("./db.sqlite");
    this.securityManager = new ResourceSecurityManager();
    this.watcher = new ResourceWatcher();

    this.setupHandlers();
  }

  private setupHandlers() {
    // Resource list
    this.server.setRequestHandler("resources/list", async () => {
      const fileResources = await this.fileProvider.listResources();
      const dbResources = await this.dbProvider.listResources();

      return {
        resources: [...fileResources, ...dbResources]
      };
    });

    // Read resource
    this.server.setRequestHandler("resources/read", async (request) => {
      const { uri } = request.params;

      // Permission check
      if (!this.securityManager.checkReadAccess(uri)) {
        throw new Error("Access denied");
      }

      // Route to appropriate provider
      let content: string;
      if (uri.startsWith("file://")) {
        content = await this.fileProvider.readResource(uri);
      } else if (uri.startsWith("db://")) {
        content = await this.dbProvider.readResource(uri);
      } else {
        throw new Error("Unsupported resource type");
      }

      return {
        contents: [{
          uri,
          mimeType: "text/plain",
          text: content
        }]
      };
    });

    // Resource subscription
    this.server.setRequestHandler("resources/subscribe", async (request) => {
      const { uri } = request.params;

      if (uri.startsWith("file://")) {
        const filePath = uri.replace("file://", "");
        this.watcher.watchFileResource(uri, filePath);
      }

      return {};
    });

    // Unsubscribe
    this.server.setRequestHandler("resources/unsubscribe", async (request) => {
      const { uri } = request.params;
      this.watcher.unwatchResource(uri);
      return {};
    });
  }

  async start() {
    const transport = new StdioServerTransport();
    await this.server.connect(transport);
    console.error("Comprehensive Resource Server running on stdio");
  }
}

// Start server
const server = new ComprehensiveResourceServer();
server.start().catch(console.error);

8. Best Practices

8.1 Resource Design Principles

  1. URI Uniqueness - Ensure each resource has a unique URI
  2. Descriptive Naming - Use clear, descriptive resource names
  3. Appropriate MIME Types - Correctly set resource MIME types
  4. Least Privilege - Provide only necessary access permissions
  5. Error Handling - Gracefully handle resource access errors

8.2 Performance Optimization

  1. Caching Mechanism - Implement caching for frequently accessed resources
  2. Lazy Loading - Load resource content only when needed
  3. Pagination Support - Implement pagination for large resources
  4. Compression - Enable compression for large content

8.3 Security Considerations

  1. Path Traversal Protection - Prevent ../ path traversal attacks
  2. Content Filtering - Filter sensitive information and malicious content
  3. Access Logging - Log resource access activities
  4. Rate Limiting - Prevent resource abuse

Summary

Through this chapter, we have mastered:

  • Basic concepts and architecture of Resources
  • How to implement file system and database resource providers
  • Resource listening and change notification mechanisms
  • Permission control and security protection
  • Complete Resource Server implementation

Resources provide powerful data access capabilities for MCP Server, enabling AI models to access various types of information sources, serving as an important foundation for building intelligent applications.