三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Resources原语:让AI读取你的数据

Resources原语:让AI读取你的数据

摘要:MCP Resources原语让AI按需读取外部数据源,支持静态URI和模板URI。本文详解资源定义、订阅通知、分页读取和与Tools原语的协作模式。

Resources原语让AI读取你的数据

上个月我给团队搭了个代码审查助手,想让模型读项目里的配置文件和日志。我第一反应是用Tools写个read_file工具,结果发现模型每次都要"决定调用"一下,对话里塞满了工具调用的噪音,而且同样的文件读了好几遍。后来我改用MCP的Resources原语,把文件作为资源暴露出去,客户端自己决定何时把内容塞进上下文,对话干净多了。这篇我把Resources从URI设计到订阅机制完整讲一遍。


Resources是什么

MCP规范里Resources原语让Server以标准化方式向客户端暴露数据。每个资源用唯一的URI标识,可以是文件、数据库表结构、配置信息,任何能给模型提供上下文的数据都行。

Resources有一个核心定位要记住,它是application-driven的,由宿主应用决定怎么使用这些资源。应用可以把资源做成树形列表让用户点选,也可以根据启发式规则自动把相关资源塞进上下文。这和Tools的model-controlled完全相反,Resources的主动权在应用和用户手里,模型只是被动地读到了数据。

Resources还有一个天然属性,它是只读的。你拿URI去read,拿到内容,就这么简单。要修改数据请走Tools,Resources只负责提供上下文。

URI设计与资源模板

每个Resource靠URI唯一标识。规范里列了几种常用scheme,file://表示文件系统资源,https://表示Web资源,git://表示版本控制集成,你也可以自定义scheme,比如config://、db://,只要符合RFC3986就行。

Resources分两种。一种是静态资源,URI固定,比如config://app-settings指向一份配置。另一种是资源模板,URI里带参数占位符,用RFC 6570的URI Template语法,比如file:///{path},客户端填入不同的path就能读不同文件。

资源模板的发现走resources/templates/list,和普通的resources/list分开。客户端先拿到模板列表,知道有哪些参数化的资源可用,再根据需要构造具体URI去read。下面是规范里的模板示例。

{"uriTemplate":"file:///{path}","name":"Project Files","description":"Access files in the project directory","mimeType":"application/octet-stream"}

我用FastMCP定义资源模板特别方便,URI里写{name}占位符,函数签名里加同名参数就行,框架自动匹配。

静态资源和动态资源

静态资源的内容是固定的,比如一份配置文件、一段说明文字。动态资源的内容由函数实时生成,比如当前系统状态、最近一小时日志。两者在FastMCP里都用@mcp.resource装饰器,区别在于函数有没有参数。

资源内容支持两种格式。文本内容放在text字段,二进制内容用base64编码放在blob字段,配合mimeType标识类型。我做过一个把项目里PNG图标当资源暴露的实验,二进制走blob字段,客户端拿到base64解码就能显示。

规范还定义了annotations,给客户端提供使用提示。audience标识内容给谁看,可选user和assistant。priority从0到1表示重要性,1最重要。lastModified是最后修改时间。这些注解帮客户端决定要不要把资源塞进上下文、塞进去的优先级。

订阅机制

Resources支持两种通知机制。第一种是listChanged,资源列表变化时Server推notifications/resources/list_changed,客户端重新拉列表。第二种是subscribe,客户端对某个具体URI发resources/subscribe请求,之后这个资源内容变了,Server就推notifications/resources/updated,客户端再read一次拿最新内容。

订阅机制对日志类资源特别有用。我那个代码审查助手暴露了一个logs://recent资源,客户端订阅它,每次有新日志进来Server推通知,客户端自动刷新,模型就能看到最新报错。

我踩过一个坑,Server声明了subscribe能力但忘了在内容变化时发updated通知,客户端一直拿到旧数据,查了半天才发现是通知没发。订阅的整个过程一定要端到端测一遍。

完整代码

下面是一个完整的Resources示例,包含静态资源、动态资源模板和目录资源。客户端测试脚本读取这些资源。

server.py

# server.py MCP Resources原语完整示例# 运行方式 python server.py# 依赖安装 pip install fastmcpimportjsonfrompathlibimportPathfromfastmcpimportFastMCP,Context# 创建服务器实例mcp=FastMCP(name="DataResourcesServer")# 模拟一个项目目录, 后面目录资源会用到PROJECT_DIR=Path("./sample_project")PROJECT_DIR.mkdir(exist_ok=True)# 写一个示例文件, 方便测试读取(PROJECT_DIR/"notes.txt").write_text("这是项目的备注文件, 记录待办事项.",encoding="utf-8")# ---------- 静态资源, URI固定, 无参数 ----------@mcp.resource("config://app-settings")defget_app_settings()->str:"""返回应用配置, 以JSON字符串形式提供. 静态资源的典型用法, URI写死, 客户端直接read这个URI就能拿到内容. """settings={"app_name":"CodeReviewBot","version":"2.1.0","max_file_size_mb":10,"enabled_checks":["lint","type-check","security"],}# 返回字符串, FastMCP会当作TextResourceContents处理returnjson.dumps(settings,ensure_ascii=False,indent=2)# ---------- 动态资源模板, URI带参数占位符 ----------@mcp.resource("file:///{path}")defread_project_file(path:str)->str:"""根据路径读取项目文件内容. 这是一个资源模板, URI里的{path}会映射到函数的path参数. 客户端构造 file:///notes.txt 就能读 sample_project/notes.txt. """# 拼接完整路径, 注意防止路径穿越攻击full_path=PROJECT_DIR/pathifnotfull_path.exists():# 文件不存在时返回提示, 而不是抛异常returnf"文件不存在{path}"# 读取并返回文件内容returnfull_path.read_text(encoding="utf-8")# ---------- 动态资源, 实时生成日志 ----------@mcp.resource("logs://recent")asyncdefget_recent_logs(ctx:Context)->str:"""返回最近的系统日志, 内容实时生成. 这个资源每次读取都会重新生成内容, 适合配合订阅机制使用, 客户端订阅后能拿到最新日志. """# 用Context记录一条调试日志, 会发回客户端awaitctx.debug("正在生成最近日志")logs=["[2026-08-09 10:00:01] INFO 服务启动完成","[2026-08-09 10:00:05] WARN 内存使用率偏高 78%","[2026-08-09 10:00:10] ERROR 文件解析失败 notes.txt 第3行",]return"\n".join(logs)# ---------- 目录资源, 列出目录下的文件 ----------@mcp.resource("dir://project-files")deflist_project_files()->str:"""列出项目目录下的所有文件. 返回JSON格式的文件列表, 方便客户端知道有哪些文件可以读. """files=[]forfinPROJECT_DIR.iterdir():files.append({"name":f.name,"size":f.stat().st_size,"is_dir":f.is_dir(),})returnjson.dumps(files,ensure_ascii=False,indent=2)if__name__=="__main__":# 以stdio模式启动mcp.run()

client_test.py

# client_test.py Resources客户端测试# 运行方式 python client_test.pyimportasynciofromfastmcpimportClientasyncdefmain():asyncwithClient("server.py")asclient:# 第一步, 列出所有静态资源, 相当于发resources/listresources=awaitclient.list_resources()print("=== 静态资源列表 ===")forrinresources:print(f" URI{r.uri}")print(f" 名称{r.name}")print()# 第二步, 列出资源模板, 相当于发resources/templates/listtemplates=awaitclient.list_resource_templates()print("=== 资源模板列表 ===")fortintemplates:print(f" 模板{t.uriTemplate}")print(f" 名称{t.name}")print()# 第三步, 读取静态资源, 相当于发resources/readprint("=== 读取 config://app-settings ===")contents=awaitclient.read_resource("config://app-settings")print(f" 内容{contents[0].content}")print()# 第四步, 用模板构造URI读取动态资源print("=== 读取 file:///notes.txt ===")contents=awaitclient.read_resource("file:///notes.txt")print(f" 内容{contents[0].content}")print()# 第五步, 读取实时日志print("=== 读取 logs://recent ===")contents=awaitclient.read_resource("logs://recent")print(f" 内容{contents[0].content}")if__name__=="__main__":asyncio.run(main())

效果验证

装好fastmcp后跑client_test.py,输出大致如下。

=== 静态资源列表 === URI config://app-settings 名称 get_app_settings URI logs://recent 名称 get_recent_logs URI dir://project-files 名称 list_project_files === 资源模板列表 === 模板 file:///{path} 名称 read_project_file === 读取 config://app-settings === 内容 { "app_name": "CodeReviewBot", "version": "2.1.0", "max_file_size_mb": 10, "enabled_checks": ["lint", "type-check", "security"] } === 读取 file:///notes.txt === 内容 这是项目的备注文件, 记录待办事项. === 读取 logs://recent === 内容 [2026-08-09 10:00:01] INFO 服务启动完成 ...

客户端先list出三个静态资源和一个资源模板,再用具体URI分别read拿到内容。在真实MCP客户端里,这些资源会以列表或树形展示给用户,用户选择后内容自动进入模型上下文。

与Tools的区别和使用场景选择

Resources和Tools经常被搞混,我做了个对比。

维度ResourcesTools
控制方应用和用户主导(application-driven)模型主导(model-controlled)
操作类型只读, 提供上下文数据可执行, 产生副作用
标识方式URI唯一标识name唯一标识
调用方式客户端按需read模型发call请求
返回内容文本或二进制数据结构化结果或文本
典型场景读文件、看配置、查日志查数据库、调API、发邮件

选择标准很简单。如果只是让模型看到某些数据,用Resources。如果要让模型执行一个动作产生结果或副作用,用Tools。

我踩过一个选择错误的坑。有个查用户信息的需求,我一开始用Resources暴露user://123,结果发现模型没法主动触发查询,只能等应用把资源塞进去。后来改成Tools的get_user工具,模型需要的时候自己调。反过来,读项目README这种被动提供上下文的场景,用Resources就比Tools合适得多,避免了每次都要模型决定调用。

常见问题与避坑

坑1,资源模板参数名和URI占位符对不上。FastMCP靠URI里{name}和函数参数name同名来匹配。写成了file:///{filepath}但函数参数叫path,客户端调用时参数传不进去,read到的永远是空内容。占位符和参数名务必一致。

坑2,路径穿越导致安全问题。资源模板直接拼用户传入的path读文件,攻击者构造file:///…/…/…/etc/passwd就能读到敏感文件。一定要做路径校验,把路径限制在允许的根目录内,用resolve()检查是否越界。

坑3,订阅通知发了但内容没更新。Server推了notifications/resources/updated,但实际资源函数返回的还是旧数据。检查你的资源函数是不是有缓存,或者数据源没真正更新。订阅的整条流程要端到端验证,发了通知就read一次确认内容是新的。

坑4,二进制资源忘了设mimeType。返回bytes类型的内容会被base64编码放进blob字段,但mimeType默认是application/octet-stream。客户端不知道怎么处理,图片显示不出来。在装饰器里显式指定mime_type="image/png"这类正确的类型。

坑5,把Resources当Tools用。Resources是只读的,没有"执行"的概念。有人想在资源读取时顺带修改数据库,这违反了Resources的只读语义。要产生副作用请用Tools,Resources保持纯净的读取职责。

小结

Resources原语解决的是让模型读到你的数据这个问题。核心要点有四个,每个资源用URI唯一标识,资源模板用RFC 6570语法支持参数化,订阅机制让客户端拿到资源更新通知,annotations提供使用提示帮客户端做决策。和Tools相比,Resources是只读的、由应用主导的,适合提供上下文数据。下一篇我们看Prompts原语,它把提示词模板标准化,让模型交互可复用。


相关推荐

  • MCP三大原语初体验:Tools、Resources、Prompts一个都不少
    • 资源开发实战:文件资源、数据库资源、动态资源
    • Tools原语深度解析:从定义到调用全流程
← 返回列表