Claude Code 里 MCP 的资源(Resources)是我最近才开始认真用的一块。以前总觉得"让模型读数据"最简单的方式就是开个工具函数直接返回,直到有一次我让 Agent 帮忙对比本地和线上的配置,它在工具列表里反复找 get_config、get_cache、get_secret,最后还把整份配置内容塞进上下文,又慢又占 token。后来我把这些只读数据从工具挪到了资源里,Claude Code 的上下文一下清爽了很多。这篇教程是这个系列的第 31 篇,核心就聊一件事:MCP 里面的资源到底怎么用,以及怎么在 Claude Code 里把它真正跑起来。
很多教程会把重点放在"MCP 是什么"和"怎么加 server"上,资源往往被一句话带过。但资源在协议里是一个非常独特的原语:它不像工具那样需要被"调用",更像一本书被"翻阅"。这篇文章我会从协议形态讲起,写一个实际的 FastMCP 资源服务,再展示它在 Claude Code 里的接入路径,最后分享一些我在真实项目里踩过的边界和坑。适合已经会装 MCP 工具、想让 Agent 更会"读书"的朋友。
1. MCP 资源:为什么它比"让模型读文件"更值得研究
1.1 一个让我重新认识资源的问题
Claude Code 用久了,你会很自然地把能做的事都包装成 Tool。查数据库用一个工具,读配置文件用一个工具,看文档再用一个工具。工具列表越来越长,模型选择工具时消耗的 token 和出错概率也同步上升。更麻烦的是,工具本质上是"操作",它适合被调用,但不适合承载大段上下文。
有一次我为了排查线上环境一个时区问题,让 Agent 读配置。因为配置是放在 MCP server 里的,我一开始做成 get_app_config() 工具。结果模型每次分析都先调用这个工具,把整份配置塞回对话,然后我再问它"时区字段是多少",它又要重新从上下文里找。配置不长还好,如果是一份 5000 行的配置文件,这种玩法会让上下文瞬间爆炸。
后来我把配置改成资源暴露,比如 config://app。模型在真正需要时才去读取,读取到的是"内容",不是"一次调用的返回结果"。这个区别听起来很小,实际体验差别巨大:Prompt 里不再堆满重复数据,模型也能在推理过程中把不同资源组合起来用。从这次之后,我开始认真研究 MCP 资源,而不是只把注意力放在工具上。
1.2 资源和工具的关系:书与开关的比喻
如果把 Claude Code 里的 Agent 想象成一个实习员工,MCP 里的 Tool 就是它手里的开关和按钮:按下搜索按钮能联网,按下数据库按钮能查数据,按下写文件按钮能改文件。这些操作有行为、有副作用,执行完会产生结果。Resources 则更像办公室里的书架和资料柜:资料放在那里,Agent 需要的时候自己去翻,翻到哪页读哪页。
这个比喻能帮你做第一轮判断:数据是给模型"参考"的,还是让模型"操作"的?前者放资源,后者放工具。比如"项目 README"是资源,而"把 README 发送到某个接口"就是工具。很基础,但很多人一开始就搞反了。你如果总是在资源里放需要副作用才能生成的数据,或者在工具里返回大段静态内容,那一定会踩到上下文管理或者调用时机的问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 资源在协议层面到底长什么样
2.1 MCP 资源协议的两个核心方法
MCP 协议里,资源相关的核心方法有三个,平时打交道最多的是前两个:
resources/list:客户端向 server 要一份资源列表,里面包含资源 URI、名称、MIME 类型和描述。resources/read:客户端拿着具体 URI,向 server 读取内容。resources/templates/list:返回资源模板列表,让客户端知道有哪些参数化资源可用。
实际流程很简单。Claude Code 连接上 server 后,会先通过 list 或 templates 知道"你有这些东西",然后在模型觉得需要时,用 read 去拉具体内容。这也意味着服务端可以控制"哪些资源给模型看"——你在 read 里做权限校验,不给读就返回错误。
注意一个关键点:资源读取是服务端主动返回内容,模型不需要"选择调用哪个函数",而是"决定要不要读这个 URI"。这决定了资源非常适合作上下文供给,而不是动作执行。
2.2 URI 是资源的身份证
资源必须有 URI。MCP 没有限制 scheme,但强烈建议用表意明确的 scheme,否则模型很难凭名字猜出资源的用途。下面是我在项目里常用的几种:
| scheme | 示例 | 典型用途 |
|---|---|---|
file:// |
file:///home/user/docs/README.md |
映射本地文件 |
config:// |
config://app |
应用配置项 |
schema:// |
schema://users |
数据库表结构 |
docs:// |
docs://repo/guide |
文档片段 |
git:// |
git://commits/abc123 |
版本库信息 |
data:// |
data://orders/2024/01 |
业务数据 |
URI 不要乱起。我见过有人把 resource URI 写成 abc,模型根本不知道这是什么。起成 config://app 之后,Claude 看到 config 就知道是和配置有关,配合 description,基本零学习成本。
还有一个容易被忽略的问题:URI 的可读性要优先于"简洁"。d://u123 长是短了,但模型和人都看不懂,排查问题时也要猜半天。用 user://u123/profile 这类带语义前缀的写法,长期来看收益远大于多敲几个字符的成本。
2.3 资源模板:让资源从"固定页"变成"查询接口"
如果每个用户的资料都注册成一个独立资源,资源列表会爆炸。MCP 提供了 Resource Template,也就是资源模板。它像 URL 模式一样,支持路径参数。比如 schema://{table},客户端把这个模板拿过去后,可以通过替换 {table} 生成 schema://users、schema://orders,然后调用 resources/read 读取。
这其实很像 REST API 里的路径参数设计。不同的是,MCP 客户端需要在读取前先 list templates,而不是直接猜 URI。所以你在服务端加模板时,要确保模板表达式规范,参数名清晰,最好在 description 里写明示例。比如在资源模板的描述里写一句"传入表名获取建表 SQL,例如 users 或 orders",模型读了之后就知道该怎么构造 URI。
2.4 资源内容的两种基本形态
资源内容在协议层分两种:文本(text)和二进制(blob)。文本资源最常见,配置、Markdown、JSON、日志都能作为 text 返回;图片、PDF、Excel 这类二进制内容用 blob,配合 MIME 类型让客户端识别。
有个常见误区:JSON 不是二进制,按理说应该作为 text 返回,方便模型直接解析。但如果你返回的 JSON 特别大,模型一个上下文窗口装不下,那就得考虑分块或摘要。另外二进制资源在使用前要确认客户端是否支持读取,Claude Code 对图像类 blob 的支持能力会随版本变化,不要假设所有客户端都能"看懂"一张截图,尽量在资源描述里注明内容格式。
3. 自己写一个带资源的 MCP Server:FastMCP 实操
3.1 我为什么用 FastMCP 而不是纯 SDK
写 MCP server 有很多方式,官方 Python SDK、TypeScript SDK 都能写。我偏爱 FastMCP,主要是它把资源注册做成了装饰器,代码量少,读起来像 Flask。更重要的是,FastMCP 内置了本地调试入口,配 MCP Inspector 用起来很顺。
如果你本身就是 Node 技术栈,用 TypeScript SDK 也完全没问题。协议层面都一样,Claude Code 只认 MCP,不认语言。我自己选择 Python 还有一个原因:在数据处理、数据库 Schema 这类资源场景,Python 写起来最顺手,从数据库读数据、生成 Markdown、做摘要都很方便。
3.2 初始化与代码骨架
先建一个虚拟环境,装依赖:
bash复制mkdir mcp-resource-demo
cd mcp-resource-demo
python -m venv .venv
source .venv/bin/activate
pip install fastmcp
然后创建一个 server.py:
python复制from fastmcp import FastMCP
mcp = FastMCP("resource-demo")
if __name__ == "__main__":
mcp.run()
一个最基本的 server 就到位了。现在里面什么都没有,下面开始加资源。跑起来之后,默认走 stdio 通道,Claude Code 和 MCP Inspector 都能通过标准输入输出来和它通信。
3.3 注册静态资源
静态资源最简单,注册后内容固定,适合放配置、README、接口文档。FastMCP 里用 @mcp.resource("URI") 装饰一个函数,函数的返回值就是资源内容:
python复制@mcp.resource("config://app")
def app_config() -> str:
return (
"timezone=Asia/Shanghai\n"
"log_level=info\n"
"version=1.2.0\n"
)
@mcp.resource("docs://readme")
def readme() -> str:
return "# resource-demo\n\n这是一个演示 MCP 资源的示例项目。"
我用 FastMCP 写完之后,习惯先本地跑一下,确保没有语法错误,再去接 Claude Code。因为 MCP server 进程崩溃是 Claude Code 接入时最安静的坑:列表里能看到 server,但模型就是拿不到内容。
静态资源虽然简单,但它已经展示了资源的核心价值:模型可以按 URI 直接读取。配置和文档这类内容,不需要做成工具函数,也不需要每次对话都重复塞进 Prompt。
3.4 用资源模板暴露一组可参数化数据
如果不想为每张表都写一个固定资源,可以用模板。FastMCP 的资源模板装饰器看起来像这样:
python复制@mcp.resource_template("schema://{table}")
def get_table_schema(table: str) -> str:
schemas = {
"users": "CREATE TABLE users (\n id INTEGER PRIMARY KEY,\n name TEXT NOT NULL\n);",
"orders": "CREATE TABLE orders (\n id INTEGER PRIMARY KEY,\n user_id INTEGER NOT NULL,\n amount NUMERIC\n);",
}
return schemas.get(table, f"table not found: {table}")
这里 {table} 就是参数。Claude Code 从 server 拿到模板后,会知道存在 schema://{table} 这样的资源,当它需要了解某个表的 schema 时,就会构造 schema://users 之类 URI 去读取。
有一个细节:模板函数的参数名要和模板里的占位符一致。我最早把函数参数写成 table_name,模板里写 {table},结果一直匹配不上。这个坑对新手很不友好,排查了半天才发现。FastMCP 在处理模板参数时是直接按名字注入的,名字对不上就不会触发。
3.5 动态资源:读取时现算
资源的内容不一定非得是静态的。我经常把动态资源用在"实时状态"类数据上,比如当前服务健康状态、最近一次构建结果。每次 resources/read 被调用时,服务端现算,返回最新值:
python复制import time
@mcp.resource("status://health")
def health_status() -> str:
return f"ok, ts={int(time.time())}"
动态资源好用,但要注意两点:一是每次读取都会执行代码,如果背后查的是慢接口或大表,Agent 可能等很久;二是同一份数据不同时间读结果不一样,模型容易困惑。后面避坑部分我会给出我的做法。
动态资源最适合的场景是"你希望模型拿到最新值,但不希望模型误以为这是稳定事实"的状态类数据。如果是业务规则、代码规范这类长期不变的内容,做静态资源更合适。
3.6 用 MCP Inspector 验证资源是否可用
写完之后不要直接连 Claude Code,先用 MCP Inspector 做一次独立验证。启动命令:
bash复制npx @modelcontextprotocol/inspector python server.py
Inspector 会启动一个本地调试面板,里面专门有 Resources 页签。你能看到 server 暴露了哪些资源和模板,可以手动输入 URI 去读。我用它验证一个 server,通常只做三件事:
- 看资源列表里有没有出现预期的 URI;
- 手动调用
resources/read,确认文本内容正确返回; - 观察动态资源的函数是否每次都被触发。
这一步能过滤掉绝大多数低级问题,等到接 Claude Code 时,我只需要排查接入配置,不用再怀疑 server 本身。如果你用的是 TypeScript SDK 或官方 SDK,同样可以接 Inspector,MCP 协议层的调试方式
