MCP入门教程-第2章:MCP 资源 (Resources)

444 阅读5分钟

在MCP的核心概念中,**资源(Resources)**是最重要的基础组件之一。资源允许MCP服务器向客户端暴露数据和内容,为大语言模型(LLM)提供丰富的上下文信息。

什么是MCP资源?

MCP资源是服务器向客户端暴露的任何类型的数据,可以包括:

  • 文件内容(源代码、配置文件、日志文件)
  • 数据库记录
  • API响应数据
  • 实时系统数据
  • 屏幕截图和图像
  • 以及更多类型的数据

每个资源都通过唯一的URI进行标识,并且可以包含文本或二进制数据。

资源的控制模式

MCP资源采用应用程序控制的设计模式,这意味着客户端应用程序决定如何以及何时使用这些资源。不同的MCP客户端可能会以不同方式处理资源:

  • Claude Desktop目前要求用户明确选择资源才能使用
  • 其他客户端可能基于启发式算法自动选择资源
  • 某些实现甚至允许AI模型自己决定使用哪些资源

资源URI结构

资源通过URI进行标识,遵循以下格式:

<protocol>://<host>/<path>

例如:

  • file:///home/user/documents/report.pdf
  • postgres://database/customers/schema
  • screen://localhost/display1

协议和路径结构由MCP服务器实现定义,服务器可以定义自己的自定义URI方案。

资源类型

MCP支持两种类型的资源内容:

文本资源

包含UTF-8编码的文本数据,适用于:

  • 源代码
  • 配置文件
  • 日志文件
  • JSON/XML数据
  • 纯文本

二进制资源

包含base64编码的原始二进制数据,适用于:

  • 图像文件
  • PDF文档
  • 音频文件
  • 视频文件
  • 其他非文本格式

TypeScript SDK实战演示

下面我们使用TypeScript SDK来创建一个完整的MCP服务器示例,展示如何实现资源功能:

1. 基础服务器设置

import { McpServer } from '@modelcontextprotocol/sdk';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import fs from 'fs/promises';
import path from 'path';

// 创建MCP服务器实例
const server = new McpServer({
  name: "FileSystemResourceServer",
  version: "1.0.0",
});

// 定义支持的资源类型
interface FileResource {
  uri: string;
  name: string;
  description: string;
  mimeType: string;
}

// 模拟文件系统数据
const fileDatabase: FileResource[] = [
  {
    uri: "file:///projects/config.json",
    name: "应用配置文件",
    description: "包含应用程序的主要配置信息",
    mimeType: "application/json"
  },
  {
    uri: "file:///projects/logs/app.log",
    name: "应用日志",
    description: "应用程序运行日志文件",
    mimeType: "text/plain"
  },
  {
    uri: "file:///projects/docs/readme.md",
    name: "项目文档",
    description: "项目的详细说明文档",
    mimeType: "text/markdown"
  }
];

2. 实现资源发现功能

// 处理资源列表请求
server.setRequestHandler('resources/list', async () => {
  return {
    resources: fileDatabase.map(file => ({
      uri: file.uri,
      name: file.name,
      description: file.description,
      mimeType: file.mimeType
    }))
  };
});

// 实现资源模板支持(用于动态资源)
server.setRequestHandler('resources/templates', async () => {
  return {
    resourceTemplates: [
      {
        uriTemplate: "file:///projects/{category}/{filename}",
        name: "项目文件模板",
        description: "访问项目目录下的任意文件",
        mimeType: "text/plain"
      }
    ]
  };
});

3. 实现资源读取功能

// 处理资源读取请求
server.setRequestHandler('resources/read', async (request) => {
  const { uri } = request.params;
  
  try {
    // 解析URI并获取文件路径
    const filePath = decodeURIComponent(uri.replace('file://', ''));
    
    // 安全检查:防止目录遍历攻击
    if (filePath.includes('..') || !filePath.startsWith('/projects/')) {
      throw new Error('无效的文件路径');
    }
    
    // 读取文件内容
    const content = await fs.readFile(filePath, 'utf-8');
    const stats = await fs.stat(filePath);
    
    // 确定MIME类型
    const mimeType = getMimeType(filePath);
    
    return {
      contents: [
        {
          uri: uri,
          mimeType: mimeType,
          text: content
        }
      ]
    };
  } catch (error) {
    throw new Error(`读取资源失败: ${error.message}`);
  }
});

// MIME类型判断辅助函数
function getMimeType(filePath: string): string {
  const ext = path.extname(filePath).toLowerCase();
  const mimeTypes: { [key: string]: string } = {
    '.json': 'application/json',
    '.js': 'application/javascript',
    '.ts': 'application/typescript',
    '.md': 'text/markdown',
    '.txt': 'text/plain',
    '.log': 'text/plain',
    '.html': 'text/html',
    '.css': 'text/css',
    '.xml': 'application/xml',
    '.pdf': 'application/pdf',
    '.png': 'image/png',
    '.jpg': 'image/jpeg',
    '.jpeg': 'image/jpeg'
  };
  
  return mimeTypes[ext] || 'text/plain';
}

4. 实现资源订阅和更新机制

// 存储订阅信息
const subscriptions = new Set<string>();

// 处理资源订阅请求
server.setRequestHandler('resources/subscribe', async (request) => {
  const { uri } = request.params;
  subscriptions.add(uri);
  
  console.log(`订阅资源: ${uri}`);
  
  // 可以在这里设置文件监控
  watchFileChanges(uri);
  
  return {};
});

// 处理取消订阅请求
server.setRequestHandler('resources/unsubscribe', async (request) => {
  const { uri } = request.params;
  subscriptions.delete(uri);
  
  console.log(`取消订阅资源: ${uri}`);
  
  return {};
});

// 文件变化监控函数
async function watchFileChanges(uri: string) {
  const filePath = decodeURIComponent(uri.replace('file://', ''));
  
  try {
    // 使用Node.js的fs.watch监控文件变化
    const watcher = fs.watch(filePath, (eventType) => {
      if (eventType === 'change' && subscriptions.has(uri)) {
        // 发送资源更新通知
        server.sendNotification('notifications/resources/updated', {
          uri: uri
        });
      }
    });
    
    // 存储watcher以便后续清理
    // 在实际应用中,你需要管理这些watcher的生命周期
  } catch (error) {
    console.error(`监控文件失败: ${error.message}`);
  }
}

5. 高级功能:批量资源处理

// 处理目录资源读取(返回多个文件)
server.setRequestHandler('resources/read', async (request) => {
  const { uri } = request.params;
  
  if (uri.endsWith('/')) {
    // 处理目录请求
    return await readDirectory(uri);
  } else {
    // 处理单个文件请求
    return await readSingleFile(uri);
  }
});

async function readDirectory(dirUri: string) {
  const dirPath = decodeURIComponent(dirUri.replace('file://', ''));
  
  try {
    const files = await fs.readdir(dirPath);
    const contents = [];
    
    for (const file of files) {
      const filePath = path.join(dirPath, file);
      const stats = await fs.stat(filePath);
      
      if (stats.isFile()) {
        const content = await fs.readFile(filePath, 'utf-8');
        contents.push({
          uri: `file://${filePath}`,
          mimeType: getMimeType(filePath),
          text: content
        });
      }
    }
    
    return { contents };
  } catch (error) {
    throw new Error(`读取目录失败: ${error.message}`);
  }
}

async function readSingleFile(uri: string) {
  const filePath = decodeURIComponent(uri.replace('file://', ''));
  
  try {
    const content = await fs.readFile(filePath, 'utf-8');
    
    return {
      contents: [{
        uri: uri,
        mimeType: getMimeType(filePath),
        text: content
      }]
    };
  } catch (error) {
    throw new Error(`读取文件失败: ${error.message}`);
  }
}

6. 启动服务器

// 启动服务器
async function main() {
  // 创建标准输入输出传输
  const transport = new StdioServerTransport();
  
  // 连接服务器和传输
  await server.connect(transport);
  
  console.log('MCP资源服务器已启动');
}

// 处理优雅关闭
process.on('SIGINT', async () => {
  console.log('正在关闭服务器...');
  await server.close();
  process.exit(0);
});

// 启动应用
main().catch(console.error);

最佳实践

在实现MCP资源支持时,应该遵循以下最佳实践:

1. 安全性考虑

  • 验证所有资源URI
  • 实现适当的访问控制
  • 清理文件路径以防止目录遍历攻击
  • 谨慎处理二进制数据
  • 考虑对资源读取进行速率限制

2. 性能优化

  • 为大型资源列表考虑分页
  • 适当缓存资源内容
  • 实现读取操作的超时机制
  • 对频繁变化的资源使用订阅机制

3. 用户体验

  • 使用清晰、描述性的资源名称和URI
  • 包含有用的描述来指导LLM理解
  • 在已知时设置适当的MIME类型
  • 优雅地处理错误并提供清晰的错误消息

4. 扩展性设计

  • 为动态内容实现资源模板
  • 记录你的自定义URI方案
  • 考虑未来的扩展需求
  • 保持API的向后兼容性

总结

MCP资源是连接AI应用程序与外部数据源的强大机制。通过TypeScript SDK,我们可以轻松构建功能丰富、安全可靠的MCP服务器,为大语言模型提供所需的上下文数据。

资源的设计哲学体现了MCP协议的核心价值:标准化、安全性和灵活性。无论是简单的文件访问还是复杂的数据库查询,MCP资源都能提供一致的接口,让AI应用程序能够无缝访问各种数据源。

随着MCP生态系统的不断发展,资源概念将继续演进,为AI应用程序的数据集成提供更多可能性。掌握MCP资源的概念和实现方法,将为构建下一代AI应用程序奠定坚实的基础。