你好,我是黄佳。
从这节课开始,我们将通过理论讲解和代码示例相配合的方式,来深入探讨一系列MCP协议中的核心概念(MCP官方文档中把这些概念称为原语——Primitives,其实就是核心概念的意思)。
这节课将详细介绍什么是MCP中的资源(Resources),包括其定义、结构、类型、实现方式及最佳实践,帮你全面了解资源在增强AI驱动应用中的作用。
什么是 MCP 中的资源
资源是 Model Context Protocol(MCP)中的核心原语之一,用于将服务器端的数据内容通过可以被客户端读取的方式暴露给 LLM,用作对话或生成时的上下文。

这里的资源可以是任意类型的文本或二进制数据,包括文件内容、数据库记录、API 响应、日志、截图、音频、视频等。

每个资源都由唯一的 URI 确定,遵循通用的 [协议]://[主机]/[路径] 形式,例如:
file:///home/user/docs/report.pdf
postgres://db.example.com/customers/schema
screen://localhost/display
服务器可根据需要自定义 URI 方案,并在文档中说明各字段含义。
资源是应用控制的,意味着客户端应用程序可以决定如何以及何时使用这些资源。例如,应用可以树状或列表视图通过 UI 元素展示可选资源,或允许用户搜索和过滤可用资源。
不同的MCP客户端处理资源的方式可能不同,例如:
-
像Claude Desktop这样的客户端要求用户明确选择资源。
-
其他客户端可能根据启发式规则自动选择资源。
-
某些实现甚至允许AI模型自行决定使用哪些资源。
因此,服务器开发者在实现资源支持时,需要意识到资源的交互模式是MCP Host确定的。如果需要服务端自动向模型暴露数据(而不是Host来决定),服务器应使用模型控制的原语(如Tools)而非资源。
资源的定义和发现
在 MCP 协议中,“服务端资源的定义和发现”是指服务器如何声明(定义)它能提供的各种数据内容,以及客户端如何探测(发现)并访问这些资源。
服务端资源的定义
服务器需要用一个统一的数据结构来描述每个资源对象,包括后面这几项内容。
-
URI:唯一标识符,形如 scheme://host/path(比如 file:///logs/app.log)。
-
name:人类可读的名称,如 “Application Logs”。
-
description(可选):对资源内容的简要说明。
-
mimeType(可选):资源的媒体类型(text/plain、application/json、image/png 等)。
-
size(可选):资源的字节数
支持资源功能的服务器必须声明 resources 能力:
{
"capabilities": {
"resources": {
"subscribe": true,
"listChanged": true
}
}
}
其中:
-
subscribe:客户端是否可以订阅单个资源的变更通知
-
listChanged:当资源列表变化时,服务器是否会发送通知
这两项均为可选,服务器可以只支持其中一个、两个都支持,也都不支持:
{
"capabilities": {
"resources": {} // 两项都不支持
}
}
静态资源 vs 动态资源
静态资源是值在服务提供时路径已经明确的资源,此时一次性列出URI 即可,服务器返回一组具体的资源对象。
静态资源的示例如下:
{
"uri": "file:///logs/app.log",
"name": "Application Logs",
"description": "实时日志文件",
"mimeType": "text/plain"
}
动态资源的内容或可用集合会随参数变化而变化,此时服务器通过 URI 模板(遵循 RFC 6570)来定义资源,例如:
{
"uriTemplate": "logs://recent?timeframe={duration}",
"name": "最近日志",
"description": "按时长获取日志",
"mimeType": "text/plain"
}
在通信过程中,客户端只要填入模板参数就能构造出具体的资源 URI。
MCP 接口绑定
在代码层面,服务器通过装饰器或注册函数来实现资源定义接口。
# 使用装饰器注册资源列表处理函数
@app.list_resources()
async def list_resources() -> list[types.Resource]:
"""
实现资源列表API,返回服务器提供的资源列表
返回:
list[types.Resource]: 资源对象列表
"""
return [
# 创建一个资源对象
types.Resource(
uri="file:///logs/app.log", # 资源的唯一标识符
name="Application Logs" # 资源的显示名称
mimeType="text/plain"
)
]
当收到 resources/list 请求时,MCP 框架就会调用这个方法返回资源列表。
客户端读取资源
客户端要使用服务器暴露的数据,必须先“发现”有哪些资源可用。
主要有两种方式:
-
直接列出(Direct Listing): 客户端向服务器发送 resources/list 请求,服务器返回一组具体的资源对象。这个方式的优点是客户端一次就能拿到所有可用资源的完整信息,典型的场景为资源数量较少且变化不频繁时。
-
URI 模板(URI Templates): 对于需要根据参数动态生成的资源,客户端向服务器发送 resources/list 请求后,服务器先返回 URI 模板;客户端根据自己的需求填入模板参数(如时间范围、分页参数等),构造出真正的资源 URI,然后再发起 resources/read 请求读取内容。这样,就能避免在列表中暴露大量组合情况,只定义好了参数化方式。典型的场景为读取日志、监控指标、分页数据等动态内容。
列出资源
客户端发起 resources/list 请求以发现可用资源。
客户端请求格式如下:
{
"jsonrpc": "2.0",
"id": 1,
"method": "resources/list",
"params": {
"cursor": "optional-cursor-value"
}
}
服务器响应格式如下:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resources": [
{
"uri": "file:///project/src/main.rs",
"name": "main.rs",
"description": "Primary application entry point",
"mimeType": "text/x-rust"
}
],
"nextCursor": "next-page-cursor"
}
}
读取资源
客户端通过 resources/read 请求指定 URI,服务器会返回包含一个或多个内容项的列表。
客户端请求格式如下:
{
"jsonrpc": "2.0",
"id": 2,
"method": "resources/read",
"params": {
"uri": "file:///logs/app.log",
}
}
服务器响应格式如下:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"contents": [
{
"uri": "file:///logs/app.log",
"mimeType": "text/plain",
"text": "2025-05-11 10:00: INFO Server started\n..."
}
]
}
}
如果读取目录或参数化 URI,也可能返回多项结果,用于列出目录下的所有文件等场景 。
资源模板
资源模板(Resource Templates)允许服务器通过 URI 模板公开可参数化的资源,其参数可通过补全 API(completion API,这个内容我们后续文章中会介绍)自动填充。
客户端请求格式如下:
{
"jsonrpc": "2.0",
"id": 3,
"method": "resources/templates/list"
}
服务器响应格式如下:
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"resourceTemplates": [
{
"uriTemplate": "file:///{path}",
"name": "Project Files",
"description": "Access files in the project directory",
"mimeType": "application/octet-stream"
}
]
}
}
资源的动态更新
MCP 支持实时通知机制,让客户端保持对资源状态的感知。
-
列表变更通知:当服务器端资源列表增删改时,发出notifications/resources/list_changed通知。
-
内容更新订阅:客户端可通过 resources/subscribe 订阅某个 URI;服务器在资源变化时用 notifications/resources/updated 通知,客户端再调用 resources/read 拉取最新内容;必要时可调用 resources/unsubscribe 取消订阅 。
错误处理
服务器应对常见失败情况返回标准 JSON-RPC 错误:
-
资源未找到(Resource not found):错误码 -32002
-
内部错误(Internal error):错误码 -32603
示例:
{
"jsonrpc": "2.0",
"id": 5,
"error": {
"code": -32002,
"message": "Resource not found",
"data": {
"uri": "file:///nonexistent.txt"
}
}
}
资源的交互流程
服务器和客户端的资源交互流程如下图所示。

MCP资源实战示例
下面我们结合示例来看看,如何在 MCP 服务器中定义并读取一系列本地知识库资源文件。(和之前示例一样,要在server和client目录中,通过 pyproject.toml 文件中的设置创建虚拟环境并安装依赖。)
首先,我们创建一系列非常简单的医学文档。

然后,我们在服务器端实现资源的发现。
# 导入必要的库
import asyncio
import os
import mcp.types as types
from mcp.server import Server
from mcp.server.stdio import stdio_server
# 创建一个MCP服务器实例,名称为"example-server"
app = Server("example-server")
DOC_DIR = "你的路径/mcp-in-action/05-resource-资源发现/server/medical_docs"
# 注册资源列表
@app.list_resources()
async def list_resources() -> list[types.Resource]:
files = [f for f in os.listdir(DOC_DIR) if f.endswith(".txt")]
return [
types.Resource(
uri=f"file://{os.path.join(DOC_DIR, fname)}",
name=fname,
description="医学文档",
mimeType="text/plain"
)
for fname in files
]
# 读取资源内容
@app.read_resource()
async def read_resource(uri: str) -> str:
path = uri.replace("file://", "")
with open(path, encoding="utf-8") as f:
return f.read()
async def main():
async with stdio_server() as streams:
await app.run(
streams[0],
streams[1],
app.create_initialization_options()
)
if __name__ == "__main__":
asyncio.run(main())
接下来,通过客户端发现服务器的资源,并读取它们。
import asyncio
import sys
from mcp import ClientSession, StdioServerParameters
from mcp.types import Notification, Resource
from mcp.client.stdio import stdio_client
async def main():
# 检查命令行参数 - 必须通过两个参数启动此程序,一个当前客户端,一个服务器程序
if len(sys.argv) < 2:
print("Usage: python simple_client.py <path_to_server_script>")
sys.exit(1)
server_script = sys.argv[1]
# 使用服务器的 Python 解释器来启动 Server,而不是当前客户端的Python环境
params = StdioServerParameters(
command="你的环境路径/bin/python3",
args=[server_script],
env=None
)
# 建立 stdio 传输并创建 Session
async with stdio_client(params) as (reader, writer):
async with ClientSession(reader, writer) as session:
# 初始化握手
await session.initialize()
# 发送初始化完成通知
notification = Notification(
method="notifications/initialized",
params={}
)
await session.send_notification(notification)
# 列出资源
response = await session.list_resources()
print("资源列表:")
# 打印返回格式
print(f"返回类型: {type(response)}")
print(f"返回内容: {response}")
# 简化处理资源列表
resources = getattr(response, 'resources', response)
for res in resources:
print(f"- URI: {res.uri}, Name: {res.name}")
if __name__ == "__main__":
asyncio.run(main())
上述代码结合了标准输入/输出传输(stdio transport)来启动 MCP 服务器,并在初始化后响应资源列表与读取请求。
激活客户端Python环境后,运行下面的命令。
python client.py ../server/simple-resource-read.py
或直接运行
uv run client.py ../server/simple-resource-read.py
输出如下:
资源列表:
返回类型: <class 'mcp.types.ListResourcesResult'>
返回内容: meta=None nextCursor=None resources=[Resource(uri=AnyUrl('file:///home/mcp-in-action/05-resource/server/medical_docs/糖尿病.txt'), name='糖尿病.txt', description='医学文档', mimeType='text/plain', size=None, annotations=None), Resource(uri=AnyUrl('file:///home/mcp-in-action/05-resource/server/medical_docs/心脏病.txt'), name='心脏病.txt', description='医学文档', mimeType='text/plain', size=None, annotations=None), Resource(uri=AnyUrl('file:///home/mcp-in-action/05-resource/server/medical_docs/高血压.txt'), name='高血压.txt', description='医学文档', mimeType='text/plain', size=None, annotations=None)]
- URI: file:///home/mcp-in-action/05-resource/server/medical_docs/糖尿病.txt, Name: 糖尿病.txt
- URI: file:///home/mcp-in-action/05-resource/server/medical_docs/心脏病.txt, Name: 心脏病.txt
- URI: file:///home/mcp-in-action/05-resource/server/medical_docs/高血压.txt, Name: 高血压.txt
通过上述设计,我们把服务器中的知识库资源开放给客户端使用,成为了RAG知识库系统的一部分。
MCP资源最佳实践
MCP 协议的官方文档中,给出了一系列关于资源使用的最佳实践。
-
URI 设计:使用清晰可读的协议和路径,便于调试与文档化。
-
名称与描述:资源列表中提供人性化 name 与详细 description,帮助客户端或用户界面展示上下文。
-
MIME 类型:尽量填写 mimeType,让客户端能够正确解析和展示内容。
-
订阅机制:对于频繁变化的重要资源,结合订阅通知减少轮询开销。
-
分页与缓存:当资源列表较大时,采用分页设计(允许服务器以较小的块形式生成结果,而不是一次性全部生成);对于大型二进制资源,可考虑本地缓存。
在安全与合规方面,则需要遵循下列原则。
-
输入验证:校验所有 URI,防止目录遍历和注入攻击。
-
访问控制:对敏感路径或数据实施访问控制,在执行操作前应检查资源权限。
-
数据清理:二进制数据需要正确的编码。
-
传输加密:在跨网络通信时使用 TLS 等安全协议。
-
速率限制:防止恶意或错误的高频读取请求。
通过以上内容,你就可以在 MCP 服务端完整地实现资源原语(Resources),并以清晰、可维护的方式将各类静态或动态数据暴露给客户端与 LLM,构建丰富的上下文增强能力。
总结一下
MCP中的“资源”允许服务器向客户端暴露数据和内容,这些数据可作为LLM交互的上下文。
-
定义:服务器通过 resources/list 接口,声明自己提供了哪些 URI(或 URI 模板),并给出名称、描述、MIME 类型等元信息。
-
发现:客户端调用 resources/list 拿到资源清单(或模板),再根据清单中的 URI 调用 resources/read 获取实际内容;必要时,还可借助订阅机制保持数据的实时性。
这样,MCP 就在客户端(Consumer)与服务器(Provider) 之间,建立了一套灵活、可扩展的“资源管理”机制,使得 LLM 在生成时能够方便、安全地引用外部数据。
思考题
-
MCP协议是如何通过资源模板实现访问动态资源,以及如何支持资源订阅和更新通知的?
-
在实际生产系统中,要实现真实的资源读取逻辑、添加访问控制、实现真实的资源更新机制、增强错误与安全检查,需要注意哪些要点?
欢迎你在留言区记录自己的收获或者疑问。如果这节课对你有启发,别忘了分享给身边更多朋友。
精选留言
2025-07-06 12:40:23
- 2.1 真实的资源读取逻辑
- 对接真实数据源:如数据库、对象存储,而不是硬编码内容。
- 参数校验:防止 SQL 注入
- 分页与过滤数据:避免一次返回大量数据造成服务器压力。
- 2.2 添加访问控制
- 统一入口校验,对请求进行身份验证,如利用 token,api key 等。
- 对资源进行细粒度权限校验,如使用 RBAC 方案。
- 2.3 真实的资源更新机制
- 变更后及时推送,如果资源变更频繁或订阅的客户端很多,可以用消息队列来接收变更消息,缓解 MCP 服务端的压力。
- 在客户端收到资源更新通知后,如果客户端还需要做其他操作,则还需要保证幂等性。
- 2.4 增强错误与安全检查
- 异常捕获。
- 限制调用获取资源的频率。
- 对输入输出内容进行校验,防止 SQL 注入、XSS 攻击。
- 资源隔离。
- 避免返回敏感信息。
- 监控告警。
2025-07-06 12:39:37
🌈1.1、MCP 协议是如何通过资源模板实现访问动态资源
- 1.1.1、MCP 底层是如何注册资源模板的:文件:src/mcp/server/fastmcp/server.py。通过 @server.resource("resource://{param}") 装饰器注册资源模板,底层会调用 ResourceManager.add_template,并最终创建 ResourceTemplate 实例。
- 1.1.2、开发者是如何注册资源模板的:开发者用 @server.resource("resource://{param}") 注册模板,服务端保存模板和参数定义。类似 Java 中的 MVC 模式中定义的 API,通过路径匹配到 controller 定义的方法。
@mcp.resource("resource://users/{user_id}/posts/{post_id}")
def get_user_post(user_id: str, post_id: str) -> str:
return f"Post {post_id} by user {user_id}"
- 1.1.3、MCP Client 如何请求资源模板列表:MCP client 通过发送 resources/templates/list 请求,获取所有可用的资源模板。服务端收到后,返回所有注册的资源模板。
- 1.4 开发者拼接 URI:将参数填充到模板 URI 中。
```
# 假设你要访问 resource://users/{user_id}/posts/{post_id}
user_id = "123"
post_id = "456"
uri = f"resource://users/{user_id}/posts/{post_id}"
```
- 1.1.5 开发者发送获取资源的请求,resources/read
- 1.1.6 MCP 服务端参数提取与资源生成。
- 服务端遍历所有注册的资源模板,调用 matches() 方法用正则表达式匹配 URI 并提取参数。
- 用提取的参数调用注册的函数,生成实际资源内容并返回。这里才是真正调用注册的函数。
☀️ 1.2、MCP 如何支持资源订阅和更新通知的?
- 1.2.1 在 src/mcp/client/session.py 可以找到 subscribe_resource、unsubscribe_resource 支持资源的订阅和取消订阅。
- 1.2.2 服务端会在资源变更时,向客户端推送 resources/updated 通知,客户端可在 _received_notification 方法中处理。
2025-07-04 17:48:50
(1)通过资源模板实现访问动态资源
1)资源模板的注册发现:
Server在Client初始化session时候向Host注册支持的资源模板;Host维护Server注册的资源模板,并通过ist_resource_templates原语向 Client提供这些模板;Client,通过调用list_resource_templates获取可用模板列表,了解哪些动态资源可以通过模板访问。
2)动态资源请求
Client根据资源模板构造具体的URI,通过read_resource原语发送请求给Host;Host解析Client发送的URI,匹配对应的资源模板,并根据模板路由请求到适当的Server;Server接收Host转发的请求,解析URI中的动态参数,执行相应的操作,并将结果通过Host返回给Client。
3)数据响应:
Server将处理结果封装为JSON-RPC 2.0响应,返回给Host;Host将Server的响应转发给 Client;Client接收响应数据。
(2)如何支持资源订阅和更新通知,应该与实现访问动态资源类似:
1)client发送订阅请求
2)host转发给server
3)server确认订阅并进行订阅管理监控
4)server监控资源变化
5)server在资源发生变化时候通知订阅者
6)client接收到通知,更新数据。
2. 资源模板的标准化、性能、身份认证、权限控制、加密、标准的错误码、版本的兼容性(目前还没实操,只能想到这些)
3. 最后,看完这一章,也明白了高德的MCP为什么list_resources是空的,原来是不开放出来
2025-07-03 11:33:32
工具的本质是 “能力载体”,解决 “LLM 需要执行什么操作” 的问题。
可以理解为资源是“GET”专注获取,工具是“PSOT”专注提交?
2025-06-27 08:09:28