《MCP 协议设计与实现》完整目录
- 前言
- 第1章 为什么需要 MCP
- 第02章 架构总览:Host-Client-Server 模型
- 第03章 JSON-RPC 与消息格式
- 第04章 生命周期与能力协商
- 第05章 Tool:让 Agent 调用世界
- 第6章 Resource:结构化的上下文注入(当前)
- 第7章 Prompt:可复用的交互模板
- 第8章 TypeScript Server 实现剖析
- 第09章 TypeScript Client 实现剖析
- 第10章 Python Server 实现剖析
- 第11章 Python Client 实现剖析
- 第12章 STDIO 传输:本地进程通信
- 第13章 Streamable HTTP:远程流式传输
- 第14章 SSE 与 WebSocket
- 第15章 OAuth 2.1 认证框架
- 第16章 服务发现与客户端注册
- 第17章 sampling
- 第18章 Elicitation、Roots 与配置管理
- 第19章 Claude Code 的 MCP 客户端:12 万行的实战
- 第20章 从零构建一个生产级 MCP Server
- 第21章 设计模式与架构决策
第6章 Resource:结构化的上下文注入
大语言模型的能力边界,本质上取决于它能"看到"什么。模型的推理再强大,如果上下文窗口中没有相关的代码文件、数据库 schema 或 API 文档,它也只能凭空臆测。MCP 的 Resource 机制正是为了解决这个问题而设计的------它提供了一种标准化的方式,让服务端向客户端暴露结构化的上下文数据,使 LLM 在正确的信息基础上进行推理。
但 Resource 不仅仅是"读取数据"这么简单。它背后蕴含着一个关键的设计哲学:由谁来决定模型应该看到什么? 在 MCP 的世界观中,这个决定权不在模型手中,而在应用程序和用户手中。这一设计选择,将 Resource 与 Tool 从根本上区分开来,也奠定了 MCP 在安全性和可控性上的基调。
本章将从协议规范出发,逐层深入到 TypeScript 和 Python SDK 的实现细节,完整剖析 Resource 的设计哲学、协议消息、URI 体系、发现机制、订阅模型以及工程实践模式。
:::tip 本章要点
- Resource 的本质:应用程序控制的上下文数据,而非模型驱动的操作
- 与 Tool 的核心区别:Resource 由应用/用户选择,Tool 由模型选择------这不是细节差异,而是安全模型的基石
- URI 寻址体系:静态 URI 与 URI Template 的双层设计,支持从固定资源到参数化资源的完整光谱
- 发现与订阅:listResources / resourceTemplates / subscribe 三位一体的资源生命周期管理
- SDK 实现模式:TypeScript 的 registerResource 与 Python 的 @resource 装饰器,两种风格殊途同归
- 数据类型:text 与 blob 的二元内容模型,MIME type 驱动的类型识别 :::
6.1 Resource 是什么:重新理解"上下文"
6.1.1 从信息不对称说起
在传统的 API 设计中,客户端调用服务端获取数据是一件稀松平常的事。但在 LLM 应用的语境下,"获取数据"有了全新的含义------这些数据不是给人看的,而是要注入到模型的上下文窗口中,直接影响模型的推理过程。
MCP 规范对 Resource 的定义开门见山:
Resources allow servers to share data that provides context to language models, such as files, database schemas, or application-specific information.
这里的关键词是 context(上下文)。Resource 不是通用的数据获取接口,它的存在目的非常明确------为 LLM 提供推理所需的背景信息。一个文件的内容、一张数据库表的 schema、一段 git 历史记录,这些都是典型的 Resource。
6.1.2 应用程序控制 vs 模型控制
Resource 最核心的设计特征,在于它的交互模型是 application-driven(应用程序驱动的)。规范中明确指出:
Resources in MCP are designed to be application-driven, with host applications determining how to incorporate context based on their needs.
这意味着什么?意味着是应用程序(而非模型)决定哪些 Resource 应该被读取并注入到上下文中。应用程序可以:
- 在 UI 中提供资源选择器,让用户显式地挑选要注入的上下文
- 基于启发式规则自动决定哪些资源与当前对话相关
- 允许模型建议需要哪些资源,但最终由应用程序或用户做出决策
这与 Tool 形成了鲜明的对比。Tool 是模型驱动的------模型在推理过程中自主决定调用哪个工具、传入什么参数。而 Resource 的选择权在应用程序和用户手中。这不是一个随意的设计选择,而是出于深思熟虑的安全考量,我们将在 6.8 节详细讨论其设计理性。
这张图清晰地展示了两者的控制流差异。在 Resource 模式中,控制流从用户/应用程序出发;在 Tool 模式中,控制流从 LLM 出发。这个差异决定了两者在安全模型、权限管理和使用场景上的根本不同。
6.2 Resource 的数据类型:text 与 blob
Resource 的内容只有两种形态:文本(text)和二进制(blob)。这种简洁的二元设计覆盖了几乎所有的实际场景。
6.2.1 文本内容
文本内容是最常见的 Resource 类型。源代码文件、配置文件、数据库查询结果、API 文档------这些都以纯文本的形式返回:
json
{
"uri": "file:///project/src/main.rs",
"mimeType": "text/x-rust",
"text": "fn main() {\n println!(\"Hello world!\");\n}"
}
mimeType 字段告诉客户端如何解释这段文本。text/plain 是纯文本,text/x-rust 是 Rust 代码,application/json 是 JSON 数据。客户端可以根据 MIME 类型决定是否进行语法高亮、格式化或其他处理。
6.2.2 二进制内容
对于图片、PDF、编译产物等非文本资源,Resource 使用 base64 编码的 blob 字段:
json
{
"uri": "file:///project/logo.png",
"mimeType": "image/png",
"blob": "iVBORw0KGgoAAAANSUhEUgAA..."
}
blob 内容必须经过 base64 编码。这是 JSON-RPC 传输二进制数据的标准做法------JSON 本身不支持二进制,base64 是最通用的编码方式。
6.2.3 一次读取返回多个内容
resources/read 的响应中,contents 是一个数组,意味着单次读取可以返回多个内容块。这在实践中非常有用------例如读取一个目录时,服务端可以一次返回目录下所有文件的内容:
json
{
"contents": [
{ "uri": "file:///project/src/main.rs", "mimeType": "text/x-rust", "text": "..." },
{ "uri": "file:///project/src/lib.rs", "mimeType": "text/x-rust", "text": "..." }
]
}
6.3 URI 寻址体系:标识与模板
每个 Resource 都通过 URI 唯一标识。这不是随意的选择------URI 是互联网上最成熟的资源标识机制,MCP 直接复用了 RFC 3986 的语义。
6.3.1 标准 URI Scheme
MCP 规范定义了几种常见的 URI scheme:
| Scheme | 用途 | 示例 |
|---|---|---|
file:// |
文件系统资源 | file:///project/src/main.rs |
https:// |
Web 资源(客户端可直接获取) | https://api.example.com/schema |
git:// |
Git 版本控制资源 | git://repo/commit/abc123 |
| 自定义 scheme | 应用特定资源 | postgres://db/users/schema |
这里有一个重要的细节:https:// scheme 仅应在客户端能够自行获取资源时使用。如果资源需要服务端代为获取(即使服务端内部通过 HTTP 获取),应该使用自定义 scheme。这个约定避免了客户端与服务端之间的职责混淆。
自定义 URI scheme 必须符合 RFC 3986 规范,但协议对 scheme 名称没有强制限制。这意味着你可以设计 postgres://、redis://、jira:// 等任意 scheme 来标识你的领域资源。
6.3.2 URI Template:参数化的资源定位
静态 URI 只能标识固定的资源。但很多场景下,资源是动态的------用户想查看特定城市的天气、特定提交的代码差异、特定表的 schema。URI Template(RFC 6570)为此提供了解决方案:
json
{
"uriTemplate": "weather://{city}/current",
"name": "Current Weather",
"description": "Get current weather for a city"
}
URI Template 使用 {variable} 语法定义参数占位符。客户端在读取资源时,将参数替换为实际值,得到一个具体的 URI(如 weather://beijing/current),然后通过 resources/read 请求该 URI。
URI Template 的参数还支持自动补全。客户端可以通过 MCP 的 completion API 获取参数的候选值,为用户提供智能提示。这让 Resource 的使用体验接近于搜索引擎的自动建议。
6.4 资源发现:listResources 与 resourceTemplates
客户端如何知道服务端提供了哪些资源?MCP 通过两个独立的发现端点来解决这个问题。
6.4.1 listResources:枚举静态资源
resources/list 请求返回服务端当前可用的所有静态资源列表。该操作支持分页,适用于资源数量较大的场景:
json
// 请求
{ "method": "resources/list", "params": { "cursor": "optional-cursor" } }
// 响应
{
"resources": [
{
"uri": "file:///project/src/main.rs",
"name": "main.rs",
"title": "Rust Application Main File",
"description": "Primary application entry point",
"mimeType": "text/x-rust"
}
],
"nextCursor": "next-page-cursor"
}
每个资源描述包含以下字段:
- uri:资源的唯一标识符,是后续读取操作的关键
- name:资源名称,用于程序化处理
- title:可选的人类可读标题,用于 UI 展示
- description:可选的资源描述
- mimeType:可选的 MIME 类型,指示内容格式
- size:可选的资源大小(字节),帮助客户端预估上下文消耗
- annotations:可选的元数据注解,包括受众(audience)、优先级(priority)和最后修改时间(lastModified)
annotations 字段值得特别关注。audience 可以设为 ["user"]、["assistant"] 或 ["user", "assistant"],告诉客户端这个资源的目标受众是谁。priority 是 0.0 到 1.0 的浮点数,1.0 表示"几乎必须包含",0.0 表示"完全可选"。客户端可以利用这些注解来智能地决定哪些资源值得注入上下文窗口。
6.4.2 resourceTemplates:发现动态资源
resources/templates/list 请求返回服务端提供的所有 URI 模板:
json
// 请求
{ "method": "resources/templates/list" }
// 响应
{
"resourceTemplates": [
{
"uriTemplate": "file:///{path}",
"name": "Project Files",
"description": "Access files in the project directory",
"mimeType": "application/octet-stream"
}
]
}
两个发现端点的分离设计是有意为之的。静态资源可以直接枚举和展示,而模板资源代表的是一个无限的资源空间------你无法枚举所有可能的 {path} 值。将两者分开,让客户端可以用不同的 UI 模式来处理:静态资源展示为列表或树形结构,模板资源展示为搜索框或输入表单。
6.4.3 Capability 声明
服务端在初始化阶段通过 capability 声明自己支持 Resource 功能:
json
{
"capabilities": {
"resources": {
"subscribe": true,
"listChanged": true
}
}
}
两个子能力都是可选的:
- subscribe:是否支持客户端订阅单个资源的变更通知
- listChanged:是否会在资源列表变化时发送通知
6.5 订阅与实时更新
Resource 不是一次性的快照------在很多场景下,资源的内容会随时间变化。一个正在被编辑的文件、一个不断更新的数据库表、一个实时变化的监控指标,都需要通知机制来让客户端保持数据的新鲜度。
6.5.1 资源列表变更通知
当服务端的可用资源列表发生变化(新增资源、移除资源)时,声明了 listChanged 能力的服务端应该发送通知:
json
{
"method": "notifications/resources/list_changed"
}
客户端收到此通知后,应重新调用 resources/list 获取最新的资源列表。
6.5.2 单资源订阅
对于特定资源的内容变更,MCP 提供了细粒度的订阅机制:
json
// 订阅
{ "method": "resources/subscribe", "params": { "uri": "file:///project/config.yaml" } }
// 服务端确认订阅后,当资源内容变化时发送通知
{ "method": "notifications/resources/updated", "params": { "uri": "file:///project/config.yaml" } }
注意:更新通知只告诉客户端"这个资源变了",但不包含新内容。客户端需要自行决定是否重新读取------这是一个推(push)+拉(pull)的混合模式。通知是推送的,但内容获取仍然是拉取的。这种设计避免了不必要的数据传输:客户端可能在收到通知后决定暂时不更新(比如用户已经切换到了其他上下文)。
6.5.3 取消订阅
当客户端不再需要某个资源的变更通知时,可以发送 resources/unsubscribe 请求:
json
{ "method": "resources/unsubscribe", "params": { "uri": "file:///project/config.yaml" } }
良好的客户端实现应该在不再关注某个资源时及时取消订阅,避免服务端做不必要的变更检测和通知推送。
6.6 SDK 实现:TypeScript 与 Python 的注册模式
理解了协议层面的设计后,让我们深入到两个官方 SDK 的实现,看看开发者在实践中如何注册和使用 Resource。
6.6.1 TypeScript SDK:registerResource
TypeScript SDK 的 McpServer 类通过 registerResource 方法注册资源。该方法支持两种重载签名------静态资源和模板资源:
typescript
// 静态资源注册
server.registerResource(
'config', // name
'config://app', // URI 字符串
{
title: 'Application Config',
mimeType: 'text/plain'
},
async (uri) => ({
contents: [{
uri: uri.href,
text: 'App configuration here'
}]
})
);
// 模板资源注册
server.registerResource(
'user-profile',
new ResourceTemplate('users://{userId}/profile', { list: undefined }),
{ title: 'User Profile', mimeType: 'application/json' },
async (uri, { userId }) => ({
contents: [{
uri: uri.href,
text: JSON.stringify({ id: userId, name: 'Example User' })
}]
})
);
静态资源传入 URI 字符串,模板资源传入 ResourceTemplate 对象。两者的回调函数签名略有不同:静态资源回调只接收 uri 参数,模板资源回调额外接收模板变量的键值对。
注册完成后,registerResource 返回一个 RegisteredResource 对象,提供了动态管理的能力:
typescript
const resource = server.registerResource('config', 'config://app', {}, callback);
// 动态禁用(从列表中移除,但不删除注册)
resource.disable();
// 重新启用
resource.enable();
// 完全移除注册
resource.remove();
// 动态更新资源属性
resource.update({
name: 'new-config',
metadata: { title: 'Updated Config' }
});
这个设计让资源的生命周期管理变得灵活------服务端可以根据运行时状态动态调整可用资源,而不需要重启或重新初始化。每次调用 disable()、enable()、remove() 或 update() 都会自动触发 listChanged 通知,客户端会自动感知到资源列表的变化。
6.6.2 Python SDK:@resource 装饰器
Python SDK 采用了更 Pythonic 的装饰器风格。McpServer 的 resource() 方法返回一个装饰器,用于将普通函数注册为资源处理器:
python
from mcp.server.mcpserver import McpServer
server = McpServer("example-server")
# 静态资源
@server.resource("config://app", name="config", title="Application Config")
def get_config() -> str:
return "App configuration here"
# 模板资源------URI 中包含 {variable} 时自动识别为模板
@server.resource("weather://{city}/current", name="weather")
def get_weather(city: str) -> str:
return f"Weather for {city}: sunny, 25°C"
# 异步资源
@server.resource("db://users/schema")
async def get_user_schema() -> str:
schema = await fetch_db_schema("users")
return json.dumps(schema)
Python SDK 的设计中有一个巧妙的自动推断机制:如果 URI 中包含 {variable} 模式,或者函数有参数(排除 Context 参数),SDK 会自动将其注册为模板资源。同时,SDK 会验证 URI 模板中的变量名与函数参数名是否匹配,不匹配时抛出 ValueError。
函数的返回值类型也被自动处理:
- 返回
str:作为文本内容 - 返回
bytes:作为二进制内容(blob) - 返回其他类型:自动序列化为 JSON
6.6.3 两种风格的对比
| 维度 | TypeScript SDK | Python SDK |
|---|---|---|
| 注册方式 | registerResource() 方法调用 |
@resource() 装饰器 |
| 模板识别 | 显式传入 ResourceTemplate 对象 |
自动根据 URI 和函数签名推断 |
| 回调签名 | (uri, variables?) => ReadResourceResult |
`(variables?) => str |
| 返回值 | 需要手动构造 contents 数组 |
自动封装为合适的内容类型 |
| 生命周期管理 | 返回 RegisteredResource 对象 |
通过 add_resource() 方法 |
两种风格各有所长。TypeScript SDK 更显式、更灵活,给开发者完全的控制权;Python SDK 更简洁、更符合 Python 的约定优于配置哲学,用装饰器和类型推断减少了样板代码。
6.7 实际应用场景
Resource 的抽象足够通用,可以覆盖大量实际场景。以下是几个典型的应用模式。
6.7.1 文件系统资源
最直接的用例是暴露文件系统内容:
python
@server.resource("file:///{path}", name="project-files")
def read_file(path: str) -> str:
file_path = Path(project_root) / path
# 安全检查:防止路径穿越
if not file_path.resolve().is_relative_to(project_root):
raise ValueError("Access denied: path traversal detected")
return file_path.read_text()
这里有一个关键的安全细节:必须验证路径不会穿越出允许的根目录。Resource 的服务端实现承担着访问控制的责任。
6.7.2 数据库 Schema
将数据库的结构信息暴露为 Resource,让 LLM 在编写 SQL 时有准确的 schema 参考:
python
@server.resource("postgres://{table}/schema", name="table-schema")
async def get_table_schema(table: str) -> str:
columns = await db.fetch_columns(table)
return json.dumps({
"table": table,
"columns": [
{"name": c.name, "type": c.type, "nullable": c.nullable}
for c in columns
]
})
6.7.3 Git 历史
将代码仓库的提交历史、文件差异暴露为 Resource:
python
@server.resource("git://log/recent", name="recent-commits")
def get_recent_commits() -> str:
result = subprocess.run(
["git", "log", "--oneline", "-20"],
capture_output=True, text=True
)
return result.stdout
@server.resource("git://diff/{commit}", name="commit-diff")
def get_commit_diff(commit: str) -> str:
result = subprocess.run(
["git", "show", commit, "--stat"],
capture_output=True, text=True
)
return result.stdout
6.7.4 API 响应缓存
将外部 API 的响应缓存为 Resource,避免在多轮对话中重复请求:
python
@server.resource("api://weather/{city}", name="weather-data")
async def get_weather(city: str) -> str:
# 缓存层避免重复请求
cached = cache.get(f"weather:{city}")
if cached:
return cached
data = await weather_api.get_current(city)
result = json.dumps(data)
cache.set(f"weather:{city}", result, ttl=300)
return result
6.8 设计理性:为什么要将 Resource 与 Tool 分开?
这是理解 MCP 架构最重要的问题之一。初看之下,Resource 完全可以用 Tool 来实现------定义一个 read_file 工具不也能读取文件吗?但 MCP 坚持将两者分开,背后有三层深刻的考量。
6.8.1 安全性:控制权的归属
Tool 的执行由模型决定。这意味着如果你把文件读取实现为 Tool,模型在推理过程中可以自主决定读取任意文件------即使用户没有要求这样做。而 Resource 的选择权在应用程序和用户手中,模型无法主动触发资源读取。
这个区别在涉及敏感数据时尤为关键。假设一个 MCP 服务端同时连接了代码仓库和生产数据库。如果数据库查询被实现为 Tool,一个经过精心构造的 prompt 注入攻击可能诱使模型执行恶意查询。但如果数据库 schema 被实现为 Resource,模型只能看到用户/应用程序明确选择注入的 schema 信息,无法主动发起查询。
6.8.2 可控性:上下文窗口的精细管理
LLM 的上下文窗口是有限的。将所有可能的信息都通过 Tool 调用加载到上下文中,会迅速耗尽窗口容量。Resource 的应用控制模型让客户端可以精细地管理上下文:
- 通过
annotations.priority决定哪些资源必须包含、哪些可以省略 - 通过
size字段预估资源对上下文窗口的消耗 - 通过
annotations.audience决定资源是给用户看还是给模型看
这种精细的控制在 Tool 模式下是不可能实现的------模型调用 Tool 获取数据后,数据就已经进入了上下文。
6.8.3 可组合性:声明式 vs 命令式
Resource 是声明式的------它描述"有什么数据可用",而不是"执行什么操作"。这种声明式的特性使得 Resource 具有天然的可组合性:
- 多个 Resource 可以同时注入上下文,不存在执行顺序的问题
- Resource 的读取是幂等的(同一个 URI 读取多次得到相同的结果),不会产生副作用
- 应用程序可以预先加载所有需要的 Resource,构建一个完整的上下文快照
Tool 则是命令式的------它描述"做什么"。Tool 的调用可能有副作用、有执行顺序依赖、有并发安全问题。将上下文数据从 Tool 中分离出来,让 Resource 专注于无副作用的数据提供,使整个系统的行为更加可预测。
6.9 错误处理
Resource 操作可能遇到各种错误。MCP 规范定义了标准的 JSON-RPC 错误码:
- -32002:资源未找到(Resource not found)
- -32603:内部错误(Internal error)
json
{
"error": {
"code": -32002,
"message": "Resource not found",
"data": { "uri": "file:///nonexistent.txt" }
}
}
服务端实现应该:
- 验证所有 URI:防止路径穿越、注入攻击等安全问题
- 实施访问控制:根据客户端身份或上下文限制可访问的资源
- 正确编码二进制数据:确保 blob 内容是有效的 base64
- 在操作前检查权限:不要在返回数据后才发现权限不足
6.10 本章小结
Resource 是 MCP 三大核心原语之一,它的设计哲学可以用一句话概括:让正确的信息,以正确的方式,在正确的时机,出现在正确的位置。
回顾本章的核心要点:
- Resource 是应用程序控制的上下文数据,它的存在目的是为 LLM 提供推理所需的背景信息
- 与 Tool 最本质的区别在于控制权的归属:Resource 由应用/用户选择,Tool 由模型选择
- URI 寻址体系分为静态 URI 和 URI Template 两层,覆盖了从固定资源到无限参数化资源的完整光谱
- 发现机制通过 listResources 和 resourceTemplates 两个端点分别处理可枚举资源和参数化资源
- 订阅模型采用推(通知)+拉(读取)的混合模式,兼顾实时性和效率
- TypeScript SDK 的 registerResource 和 Python SDK 的 @resource 装饰器体现了两种语言生态的不同哲学
- Resource 与 Tool 的分离不是偶然的设计选择,而是安全性、可控性和可组合性三重考量的产物
在下一章中,我们将探讨 MCP 的第三个核心原语------Prompt,看看 MCP 如何通过标准化的提示模板来规范化人机交互的模式。