1. 先弄明白:MCP 里的资源到底是个什么东西
1.1 从一次真实卡壳说起
我最初接触 MCP 资源这个概念时,脑子里全是问号。当时我在 Claude Code 里配好了一个内部的 GitLab MCP Server,原本预期是让 Claude 直接读取仓库里的某个配置文件,结果它反复告诉我“没有权限访问外部 URL”。我一开始以为是鉴权没配好,折腾半天才发现,问题出在我对这个 MCP Server 暴露的能力模型理解错了——它只暴露了 tools(工具),根本就没暴露 resources(资源)。
这个卡壳经历其实是大多数人的共同起点。Claude Code 里的 MCP 一共提供了三类核心原语:Tools(工具)、Resources(资源) 和 Prompts(提示词模板)。绝大部分教程都在讲 Tools,因为工具最直观——“让 Claude 能执行动作”;但 Resources 同样关键,它解决的是另一个问题:让 Claude 能读取上下文。简单说,工具是手,资源是眼睛。没有资源,Claude 在很多时候就是睁眼瞎,只能靠对话里你手动粘贴的内容来干活。
这篇教程就是想把 Resources 这块讲透。不管是静态文件、数据库查询结果、API 返回的 JSON,还是某个动态生成的报告,只要你想让 Claude 在对话过程中“按需拿到外部信息”,资源就是标准答案。适合正在折腾 Claude Code MCP 配置的开发者、把 Claude 接入内部系统的运维/平台工程师,以及想给团队搭一套统一知识接入层的人。
1.2 资源、工具、提示词,三者的分工
在继续往下之前,先建立一个清晰的心智模型。MCP 的三大原语,分别对应模型交互中的三种不同需求:
| 原语 | 类比 | 典型用途 | 谁主动发起 |
|---|---|---|---|
| Tools | 手 | 执行操作:查天气、创建工单、写文件、调 API | 模型自主决定调用 |
| Resources | 眼睛 | 提供上下文:读配置文件、拉取文档、查询数据快照 | 模型按需读取,也可由用户显式指引 |
| Prompts | 剧本 | 预置指令模板:让模型按固定套路处理某类任务 | 用户主动触发 |
注意 Tools 和 Resources 的一个关键区别:工具是有副作用的动作,资源是纯读取的信息。MCP 协议在设计上故意分开,就是为了让客户端可以做精细化权限管理——你想让模型能“看”某些敏感数据,但未必希望它能“改”这些数据。
1.3 为什么说资源是 MCP 里被低估的一块
社区里讨论 MCP 时,90% 的帖子在讲 Server 怎么写、Tool 怎么调,Resources 经常被一笔带过。但我实际用下来,资源才是最贴合日常开发场景的部分。举个例子:你让 Claude 帮忙改一个 Spring Boot 项目的配置,它如果不先读一遍 application.yml,全靠猜,改出来的东西大概率是错的;如果能把配置作为资源暴露给它,它第一步就会去读取,之后所有建议都建立在真实上下文上。
另一个高频场景是团队知识库接入。把内部规范文档、接口定义、历史决策记录通过 MCP Resource 暴露出去,Claude 在对话中就能按需查阅,不用每次把文档内容复制进 prompt。这个用法对上下文窗口的节省非常可观——不是所有内容都要塞进对话,而是等模型需要时再去取。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心概念:Resource URI 与 Resource Template
2.1 一切资源都是 URI
MCP 协议里,资源用 URI 来标识。这里的 URI 不只是 http:// 那种,它可以是任何自定义 scheme。比如:
code复制file:///Users/me/projects/foo/config.yaml
git://repo/commit/abc123/docs/README.md
db://users/42/profile
doc://internal/onboarding-guide
之所以采用 URI 而不是普通字符串 ID,是因为 URI 自带结构和语义,客户端可以对 URI 做解析、校验和层级管理。比如你可以通过 URI 的路径部分判断资源所属的项目或分类,便于做权限控制。
在 Claude Code 中,你不需要直接手写 URI 去请求资源,但理解 URI 结构有助于你排查问题。比如当某个资源读取失败时,错误信息里通常带着完整的 URI,你能一眼看出是 scheme 写错了、路径拼错了,还是 Server 端根本没有这个资源。
2.2 静态资源与资源模板
MCP 资源分两种形态:
- 静态资源(Resource):URI 固定,内容相对稳定,直接通过 URI 读取。
- 资源模板(Resource Template):URI 中有参数占位符,客户端可以用不同的参数值实例化出具体资源。
模板的典型表示:
code复制db://users/{userId}/profile
客户端不知道用户 42 的 profile 是否存在时,可以先通过模板能力发现“这类资源存在”,再尝试用具体参数去读取。Claude Code 在处理资源模板时,通常会向模型暴露模板结构,模型会根据对话上下文推断出应该填入什么参数值。
2.3 Claude Code 对资源的处理机制
Claude Code 作为 MCP 客户端,和 Server 建立会话后,会执行 resources/list 拿到全部静态资源列表,再执行 resources/templates/list 拿到模板列表。但注意一个细节:它不会在会话一开始就把所有资源内容都拉进上下文。那样做既浪费 token 又可能超出上下文窗口,而是采用“按需读取”策略——模型在推理过程中决定“我需要看某个资源”,然后客户端调用 resources/read 去获取内容。
这个机制背后有一层很重要的设计逻辑:Claude Code 把“资源发现”和“资源读取”分开了。发现阶段只拿元数据,比如 URI、名称、描述、MIME 类型;读取阶段才传输实际内容。所以你在编写 MCP Server 时,resources/list 的响应应该轻量化,只返回元信息即可,真正重的数据放到 resources/read 里去加载。
3. 实操准备:在 Claude Code 中接入一个带资源的 MCP Server
3.1 最小可用示例:从文件系统暴露资源
先从最简单的场景开始。假设我想把本地某个目录下的 Markdown 文档作为资源暴露给 Claude Code。这里我用官方 mcp-server-fetch 或者自己写一个 Node.js 的 MCP Server 都可以,但为了讲清楚机制,我手写一个极简的 TypeScript Server。
typescript复制import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import fs from "node:fs/promises";
import path from "node:path";
const server = new McpServer({
name: "docs-resource-server",
version: "1.0.0"
});
const DOCS_ROOT = path.join(process.cwd(), "docs");
// 暴露一个静态资源:项目总览
server.resource(
"overview",
"docs://overview",
async (uri) => ({
contents: [{
uri: uri.href,
text: await fs.readFile(path.join(DOCS_ROOT, "overview.md"), "utf-8")
}]
})
);
// 暴露一个资源模板:根据文档名读取指定文件
server.resource(
"doc-by-name",
new ResourceTemplate("docs://{filename}", { list: undefined }),
async (uri, { filename }) => {
const safePath = path.normalize(filename).replace(/^(\.\.(\/|\\))+/, "");
const fullPath = path.join(DOCS_ROOT, safePath);
const content = await fs.readFile(fullPath, "utf-8");
return {
contents: [{
uri: uri.href,
text: content,
mimeType: "text/markdown"
}]
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport);
上面这段代码做了两件事:第一,注册了一个 docs://overview 静态资源,内容就是 docs 目录下的 overview.md;第二,注册了一个 docs://{filename} 模板,客户端可以传入任意文件名来读取对应 Markdown。
注意我在模板实现里对 filename 做了路径穿越防护。这是很多人容易忽略的点,因为文件名是模型推断出来的参数,存在被恶意构造的可能。path.normalize 加上对 .. 序列的过滤,能防止模型通过 docs://../../etc/passwd 这类路径读到服务器上的敏感文件。
3.2 在 Claude Code 中注册这个 Server
本地写好 Server 后,在 ~/.claude.json 或项目根目录的 .mcp.json 中注册:
json复制{
"mcpServers": {
"docs-resource": {
"command": "node",
"args": ["/path/to/your/server/dist/index.js"],
"env": {}
}
}
}
或者在 Claude Code 会话中直接使用:
code复制/mcp
这个命令会列出当前已配置的所有 MCP Server 及各自暴露的工具和资源。如果一切正常,你能在资源列表里看到 docs://overview 这样的条目。
3.3 触发资源读取的几种方式
配置好之后,你可能会问:怎么让 Claude 去读取这些资源?
第一种方式是自然语言引导。直接对 Claude 说“请先读取项目总览文档”,它通常会主动调用资源读取。
第二种方式是任务驱动。你让它“根据文档目录中的 network.md 帮我排查配置问题”,模型会推断出需要读取 docs://network.md。
第三种方式是显式引用 URI。在对话中直接粘贴 docs://overview,Claude Code 会识别这个 URI 并尝试读取对应资源。
我在实测中发现,第二种方式在实际开发中最常用。因为模型的工具调用和资源读取是自主决策的,你只要把任务描述清楚,它自己会判断该看哪个资源。
4. 资源模板的高级玩法:动态拼接与参数校验
4.1 动态参数的两种来源
资源模板的参数值,既可以来自模型根据语义推断,也可以来自工具调用的结果。举个例子,假设我注册一个查询数据库的模板:
code复制db://users/{userId}/orders
模型在对话中得知用户 ID 是 42 后,会尝试把模板实例化为 db://users/42/orders 再读取。但如果模型不确定参数值,它可能反过来问你。这就要求我们在编写 Server 时,对模板参数做好校验和兜底,避免参数错误时返回 500 或者直接崩溃。
4.2 参数校验的实操建议
参数校验的核心原则是:先收紧,再放开。第一步把参数当作不可信输入处理,校验类型、长度、枚举范围;第二步再做业务层的存在性检查。
typescript复制server.resource(
"user-orders",
new ResourceTemplate("db://users/{userId}/orders", {
list: async () => ({
resources: [{ uri: "db://users/42/orders", name: "User 42 Orders" }]
})
}),
async (uri, { userId }) => {
const id = Number.parseInt(userId, 10);
if (!Number.isInteger(id) || id <= 0) {
throw new Error(`Invalid userId: ${userId}`);
}
// 查询数据库...
return { contents: [{ uri: uri.href, text: JSON.stringify(orders) }] };
}
);
list 回调是可选的,它向客户端提供“该模板当前可以实例化哪些具体资源”,方便 Claude Code 在列表阶段就能发现具体资源,减少模型瞎猜参数的概率。
4.3 资源内容格式的选择
MCP 资源内容有几种承载格式:
text:纯文本,适合 Markdown、日志、代码片段。blob:二进制数据,需要 base64 编码。mimeType字段:告诉客户端内容的实际类型。
我建议优先用 text + 明确 mimeType,因为 Claude Code 对文本内容的处理最自然,对 Markdown 和 JSON 的解析能力也最强。如果必须传图片或文件,就用 blob,但要注意大小控制,否则很容易撑爆上下文。
5. 踩坑记录:资源读取失败的典型问题与排查思路
5.1 资源存在但 Claude 说读不到
现象:MCP Server 注册了资源,/mcp 里也能看到,但 Claude 在对话中说“无法访问该资源”或“找不到对应内容”。
排查思路:
- 检查 Server 是否实现了
resources/read方法。有些 SDK 版本中,只注册了资源但没正确实现 read 处理,列表能看到、读取就会失败。 - 检查 URI scheme 是否匹配。比如客户端里写的是
docs://overview,Server 注册的却是自定义的my-docs://overview,协议解析时很容易漏掉。 - 检查资源读取时是否抛了异常。Server 端异常不会显式显示在 Claude 对话里,建议在 Server 端加上日志输出,读一下 stdout 里的错误堆栈。
5.2 资源内容太大导致上下文爆炸
MCP 资源虽然可以有效扩展 Claude 的“视野”,但代价是 token 消耗。一次读取一个 10 万字的文档,直接就把上下文塞满了。
我的处理方案是三个:
- 在 Server 端做内容摘要:不返回全文,返回结构化摘要 + 关键段落。
- 在 Server 端做分块:把大文档拆成多个资源,如
docs://manual/part1、docs://manual/part2,让模型按需读取。 - 在资源描述里写清楚使用建议:告诉模型“该资源仅在看第 3 章时读取”,减少误用。
5.3 资源模板参数被模型猜错
模型推断模板参数时,不一定总是拿到正确的值。比如路径参数中带有空格、中文或特殊字符,就容易实例化失败。
解决办法:
- 在模板的描述里写清楚参数格式要求。
- 在
list回调中提供尽量多的具体资源实例,让模型优先从列表中选择。 - 在 Server 端做宽松解析:对参数做 URL decode,兼容编码差异。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 资源列表为空 | Server 未实现 resources/list |
检查 SDK 版本和注册逻辑 |
| 列表可见但读取失败 | resources/read 未正确实现 |
实现 read 方法,加日志 |
| Claude 不主动读资源 | 资源描述不清晰 | 在 Server 端写好描述 |
| 读取内容乱码 | 编码不一致 | 明确 UTF-8,设置 mimeType |
| 读取超时 | 内容生成太慢或网络问题 | 加缓存,缩短响应时间 |
| 上下文窗口被撑爆 | 单次读取内容过大 | 做摘要、分块、按需加载 |
6. 进阶思路:把资源模块变成团队基础设施
6.1 资源与工具:组合使用的姿势
我实际项目中常用的模式是:先用资源读取配置,再用工具修改配置。比如:
- 模型读取
config://services/order-service.yaml - 模型分析配置与需求是否匹配
- 模型调用
update_config工具修改具体字段
这个模式里,资源提供了“现状”,工具负责“变更”。二者组合,才构成一个完整的“读-改-写”闭环。如果你只开放工具不开放资源,模型就是盲改;只开放资源不开放工具,模型就只能看不能做。
6.2 资源权限的边界设计
把资源接入团队基础设施时,权限是绕不开的问题。我的经验是:资源读取权限和工具调用权限要分开管控。比如线上数据库的 schema 信息可以作为资源供模型读取,但写库工具必须走单独的审批流程。Claude Code 支持在 MCP Server 配置层做封装,可以在 Server 内部做身份校验,根据不同的用户上下文决定返回哪些资源。
另一个容易被忽略的点是资源内容脱敏。如果你的资源直接映射数据库行,务必在 Server 端做字段过滤,把密码、密钥、手机号等敏感字段在返回前剔除。不要寄希望于“Claude 不会主动读敏感资源”,能力边界要自己守住。
6.3 资源发现机制的延伸
Claude Code 的资源发现机制值得进一步利用。你可以把资源列表当作“能力目录”来运营——每个资源对应团队的一份文档、一个数据视图、一个外部系统接口的说明。这样模型的能力边界和团队的知识边界就能统一起来。新成员加入时,可以通过这些资源快速了解系统;老成员排查问题时,也可以让 Claude 沿着资源目录逐层深入。
我之前在自己的团队里搭过一套内部服务资源目录:每个微服务一个 MCP Server,暴露自身的配置、接口文档和运行指标。Claude Code 接入后,排查跨服务问题的时间明显缩短,因为模型能直接读取多个服务的真实状态,而不是靠人肉粘贴日志。
7. 对资源受限场景的思考(结合热词的延伸)
7.1 资源受限机器人与“资源依赖”
最近搜到一些热词,比如“资源受限机器人”、“资源依赖”,跟 MCP 资源放在一起看很有意思。资源受限机器人通常指那些内存、带宽受限的边缘设备,它们更需要“按需拉取、用完即弃”的资源访问模式,而 MCP Resource 的按需读取机制恰好契合这种轻量化思路——不要把所有数据都拉到端上,而是保持一个长期连接,按需获取。
7.2 从“资源网址”到“结构化资源”
还有一类热词是“网址资源”“云资源”,本质上是把信息变成可寻址的网络资源。MCP 的 URI 体系其实就是这个思路的一种具体实现。你可以把一份云存储上的文件映射为 cloud://bucket/key,把一份在线文档映射为 doc://doc-id。这和直接给 Claude 一个 URL 的区别在于,MCP 资源有类型、有元数据、有标准的读取协议,内容组织更结构化,权限控制更细粒度。
7.3 蓝湖 MCP、Figma MCP 等设计类资源接入
设计领域接入 MCP 资源也是一个热门方向,比如蓝湖 MCP 和 Figma MCP。这类 Server 的核心就是把设计稿的图层信息、组件属性、标注数据作为资源暴露出来,让 Claude 能“看懂”设计稿再写代码。这再次印证了资源的本质:把人类可读的信息,转成模型可读的结构化上下文。你在自己写 MCP Server 时,也可以借鉴这个思路——凡是人肉查看后还要再贴给模型的内容,都值得做成资源。
8. 一点个人实操体会
写到这里,MCP 资源的核心脉络基本讲完了。最后再分享几点我在实际使用中的体会。
第一,资源列表本身就是一种文档。当你为一个系统写完 MCP 资源注册后,回头审视这些资源的 URI 和描述,基本就是这个系统的知识地图。维护好这份地图,比维护单独的文档库更贴近实际代码。
第二,资源读取要克制。我见过不少团队把大量数据塞进资源里,结果模型每次都读取,token 消耗飞速上涨。正确姿势是给每个资源写清楚“什么时候才需要读”,并把描述放在资源定义里,让模型自己按需判断。
第三,资源和工具要成对设计。不要孤立地设计一个资源,要想想模型读取之后能做什么。一个配套了工具的资源和一组孤立的资源相比,前者对实际任务的帮助大得多。
第四,调试 MCP Server 时别只看 Claude 对话。直接命令行跑一下 Server,自己手动发 JSON-RPC 请求验证 resources/list 和 resources/read 的响应,能帮你把问题分层——是协议层的问题还是模型调用层的问题,一下就清楚了。
MCP 的资源机制其实是整个协议里最优雅的设计之一:它让模型不再依赖上下文窗口里堆积的静态文本,而是拥有了一个动态的、可寻址的、按需加载的外部世界。搞懂它之后,你会发现 Claude Code 的很多能力边界都被悄悄拓宽了。
