Resource 的本质
Resource(资源)是 MCP 中由应用程序控制的数据单元,区别于 Tool(由模型控制的操作)。Resource 代表 Server 能够提供给 AI 的上下文数据——文件内容、数据库记录、配置信息、实时状态等。
Resource(资源)
通过 URI 寻址的数据单元,由 Host 应用决定将哪些资源注入 AI 的上下文。Resource 本身不执行操作,只是数据的提供者。AI 不会主动调用资源(这与 Tool 的关键区别),而是由 Host 将资源内容提供给 AI。
URI(统一资源标识符)
每个 Resource 有唯一的 URI 地址,格式为
scheme://authority/path。例如 file:///home/user/doc.md、postgres://db/users/schema、github://repo/issues/open。Resource 与 Tool 的对比
| 维度 | Resource | Tool |
|---|---|---|
| 控制方 | Host/应用程序决定使用 | AI 模型自主决定调用 |
| 性质 | 被动数据提供 | 主动操作执行 |
| 副作用 | 通常无(只读) | 通常有(读写操作) |
| 寻址方式 | URI | 工具名称 |
| 典型用途 | 文件内容、状态信息 | 执行操作、修改数据 |
Resource URI 设计
URI 是 Resource 的核心标识机制。好的 URI 设计应该自描述(从 URI 就能猜到资源内容)、层次清晰、稳定不变。
URI 设计模式
file:// — 文件系统资源
file:///home/user/project/src/main.ts映射到本地文件系统路径,是最常见的 Resource 类型。
自定义 scheme — 领域资源
postgres://mydb/users/schema — 数据库表结构github://myorg/myrepo/issues — GitHub Issues 列表config://app/database — 应用配置信息URI Templates — 动态资源
file:///projects/{project}/src/{filename}包含
{变量} 占位符的 URI 模板,允许客户端按需请求特定资源。实现 Resource
静态资源实现
静态资源的 URI 在列举时就已确定,每个 URI 对应固定的数据来源。
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import * as fs from "fs/promises"; import * as path from "path"; const server = new McpServer({ name: "resource-server", version: "1.0.0" }); // 注册静态资源 server.resource( "project-readme", // 资源名称(唯一标识) "file:///project/README.md", // 资源 URI { name: "项目 README", description: "项目的主要文档,包含安装和使用说明", mimeType: "text/markdown", // MIME 类型 }, async (uri) => { const content = await fs.readFile( "/project/README.md", "utf-8" ); return { contents: [{ uri: uri.toString(), mimeType: "text/markdown", text: content, // 文本内容 }], }; } );
动态资源实现
动态资源在读取时根据请求参数或当前状态生成内容,适合数据库查询、API 数据等场景。
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; // 使用 URI Template 注册动态资源 server.resource( "github-file", new ResourceTemplate( "github://{owner}/{repo}/contents/{path}", { list: undefined } // 动态资源不支持列举 ), { name: "GitHub 文件内容", description: "通过 GitHub API 读取指定仓库的文件", mimeType: "text/plain", }, async (uri, variables) => { const { owner, repo, path: filePath } = variables; const apiUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${filePath}`; const response = await fetch(apiUrl, { headers: { "Authorization": `Bearer ${process.env.GITHUB_TOKEN}`, "Accept": "application/vnd.github.v3.raw", }, }); if (!response.ok) { throw new Error(`GitHub API 错误:${response.status}`); } const content = await response.text(); return { contents: [{ uri: uri.toString(), mimeType: "text/plain", text: content, }], }; } );
资源内容类型
Resource 返回的内容可以是文本或二进制:
TextResourceContents(文本资源)
包含
uri、mimeType(可选)和 text 字段。适用于代码文件、Markdown、JSON、CSV 等文本格式。BlobResourceContents(二进制资源)
包含
uri、mimeType(必填)和 blob(base64 编码字符串)字段。适用于图片、PDF、音频等二进制格式。// 图片资源示例(二进制) server.resource( "screenshot", "app://screenshot/current", { mimeType: "image/png", name: "当前截图" }, async (uri) => { const imageBuffer = await captureScreen(); return { contents: [{ uri: uri.toString(), mimeType: "image/png", blob: imageBuffer.toString("base64"), // base64 编码 }], }; } );
订阅与实时更新
当 Server 声明 resources.subscribe 能力后,Client 可以订阅特定资源的变更通知,实现实时数据推送。
Client Server resources/subscribe ─────────────────────► { uri: "file:///project/config.json" } ◄───────────────────────────────────────── result: {} (文件发生变更) ◄───────────────────────────────────────── notifications/resources/updated { uri: "file:///project/config.json" } resources/read ──────────────────────────► { uri: "file:///project/config.json" } ◄───────────────────────────────────────── { contents: [...新内容...] } resources/unsubscribe ───────────────────► { uri: "file:///project/config.json" }
在低级 Server API 中实现订阅
import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { ListResourcesRequestSchema, ReadResourceRequestSchema, SubscribeRequestSchema, UnsubscribeRequestSchema } from "@modelcontextprotocol/sdk/types.js"; import { watch } from "fs"; const server = new Server( { name: "watch-server", version: "1.0.0" }, { capabilities: { resources: { subscribe: true } } } // 声明支持订阅 ); // 维护订阅状态 const subscriptions = new Map<string, ReturnType<typeof watch>>(); server.setRequestHandler(SubscribeRequestSchema, async (req) => { const uri = req.params.uri; const filePath = new URL(uri).pathname; // 监听文件变更 const watcher = watch(filePath, () => { // 文件变更时发送通知 server.notification({ method: "notifications/resources/updated", params: { uri }, }); }); subscriptions.set(uri, watcher); return {}; }); server.setRequestHandler(UnsubscribeRequestSchema, async (req) => { const watcher = subscriptions.get(req.params.uri); if (watcher) { watcher.close(); subscriptions.delete(req.params.uri); } return {}; });
完整示例:文件系统 Resource Server
const ROOT_DIR = process.env.ROOT_DIR || process.cwd(); // 动态扫描目录,暴露所有文本文件作为 Resource server.setRequestHandler(ListResourcesRequestSchema, async () => { const files = await scanTextFiles(ROOT_DIR); return { resources: files.map(filePath => ({ uri: `file://${filePath}`, name: path.basename(filePath), description: `${path.relative(ROOT_DIR, filePath)}`, mimeType: getMimeType(filePath), })), }; }); server.setRequestHandler(ReadResourceRequestSchema, async (req) => { const uri = req.params.uri; const filePath = new URL(uri).pathname; // 安全检查 const resolved = path.resolve(filePath); if (!resolved.startsWith(path.resolve(ROOT_DIR))) { throw new Error("Access denied"); } const content = await fs.readFile(resolved, "utf-8"); return { contents: [{ uri, mimeType: getMimeType(filePath), text: content, }], }; });
Resource 设计建议
将资源 URI 设计为有意义的层次结构,例如
db://myapp/tables/users 比 resource://1234 更有语义。好的 URI 设计让 AI 仅通过 URI 就能大致了解资源的内容类型,从而做出更好的决策。