1. 为什么你的 MCP Server 需要 Resources:从日志读取场景说起
如果你正在写 MCP Server,大概率已经写过 Tools:模型决定调用哪个函数、传什么参数,服务器执行完把结果塞回上下文。但有一类需求 Tools 处理起来很别扭——数据本身就在那里,不需要模型“决定调用”,而是客户端应用希望把它作为上下文挂上去。比如本地日志文件、数据库里的一张配置表、当前屏幕截图、一份 PDF 报告。这些东西的共同点是:它们是“被读取的对象”,而不是“被执行的函数”。
MCP 里的 Resources(资源)就是干这个的。它把服务器端的数据和内容暴露给客户端,由应用程序控制何时读取、怎么使用。这一点和 Tools 的边界非常关键:Tools 是模型控制的,Resources 是应用控制的。你在实现资源支持时,要准备好面对不同客户端的交互模式——有的客户端要求用户显式勾选资源后才注入上下文,有的会按启发式规则自动挑选,还有的会把选择权交给模型自己。所以官方文档里有一句很实在的话:如果你想自动向模型暴露数据,应该用 Tools,而不是 Resources。
这篇聚焦 Resources 的核心概念与 URI 设计模式,面向正在构建 MCP Server 的开发者。我会给出可复制的 Resources 定义 JSON 配置、URI 模板示例,以及在本地 MCP Client 中验证资源读取与订阅通知的完整操作步骤。读完你应该能分清 Resources、Tools、Prompts 三者的协作边界,并且能自己跑通一次resources/list→resources/read→resources/subscribe的闭环。
先明确 Resources 能承载什么:文件内容、数据库记录、API 响应、实时系统数据、截图与图像、日志文件,基本任何类型的数据都可以。每个资源由一个唯一 URI 标识,内容可以是 UTF-8 文本,也可以是 base64 编码的二进制。文本资源适合源代码、配置文件、日志、JSON/XML;二进制资源适合图像、PDF、音频、视频。这个分类直接决定了你返回时用text字段还是blob字段,后面配置章节会具体写。
2. TaoToken 前置准备:给 MCP Client 配一个稳定的模型入口
在验证 Resources 之前,你需要一个能跑起来的 MCP Client 环境。我用的是 Claude Code 这类支持 MCP 的客户端,它需要连接一个模型服务来驱动对话。这里用 TaoToken 作为模型接入层,它的 API 地址是https://taotoken.net/api,兼容 Anthropic 与 OpenAI 风格的调用方式,配置起来比较直接。
先说清楚为什么这一步不能跳过:MCP Server 本身只是暴露资源和工具,真正发起resources/read请求、把内容拼进上下文的是客户端里的模型会话。如果你的模型入口没配好,客户端连对话都跑不起来,更别说验证资源订阅通知了。所以先把模型通道打通,再回头调 Server。
TaoToken 的接入文档在https://taotoken.net/doc,API Keys 管理页在https://taotoken.net/api-keys。你需要先去 API Keys 页面生成一个 Key,格式通常是sk-开头的一串字符。拿到 Key 之后,根据你用的客户端类型选择配置方式:如果是 Claude Code,走 Anthropic 兼容配置;如果是 Cline、Continue 这类,走 OpenAI 兼容配置。两种方式的 Base URL 都指向https://taotoken.net/api,区别在路径后缀和请求头。
这里有个容易踩的坑:很多人把 Base URL 写成https://taotoken.net,漏掉了/api,结果请求打到官网首页返回 HTML,客户端报Unexpected token < in JSON。记住 API 入口是带/api的,官网是https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=,两者不要混。
如果你打算长期跑编码类 Agent 任务,可以了解下 Coding Plan,入口在https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=。它适合需要持续调用模型的场景,比按次计费更省心。不过验证 Resources 阶段用普通 API Key 就够了,不必一上来就上套餐。
配置完成后,先用模型对话页面做一次连通性测试,地址是https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=。发一句“你好”,能正常返回就说明 Key 和 Base URL 没问题。这一步过了,再进入下一章的 Server 配置。
3. 可复制配置:Resources 定义 JSON 与 URI 模板实战
这一章是核心。我会给出一个完整的 MCP Server 资源配置,包含resources/list的返回结构、URI 模板定义、以及resources/read的处理逻辑。你可以直接复制到自己的项目里改。
先看资源发现的两种方式。直接资源通过resources/list暴露具体列表,每个资源包含uri、name、description、mimeType四个字段。资源模板用于动态资源,用 RFC 6570 的 URI 模板语法,客户端拿到模板后自己填充参数构造出有效 URI。两者的 JSON 结构如下:
{ "resources": [ { "uri": "file:///logs/app.log", "name": "Application Logs", "description": "应用运行日志,按天滚动", "mimeType": "text/plain" }, { "uri": "postgres://database/customers/schema", "name": "Customers Schema", "description": "客户表结构定义", "mimeType": "application/json" } ], "resourceTemplates": [ { "uriTemplate": "file:///logs/{date}.log", "name": "Daily Log", "description": "按日期读取日志,date 格式 YYYY-MM-DD", "mimeType": "text/plain" }, { "uriTemplate": "screen://localhost/display{displayId}", "name": "Screen Capture", "description": "截取指定显示器画面", "mimeType": "image/png" } ] }URI 的格式是[协议]://[主机]/[路径]。协议和路径结构完全由你的 Server 定义,可以自定义 scheme。上面例子里file://、postgres://、screen://都是合法的。设计 URI 时有几个原则值得遵守:协议名要能表达数据来源类型,路径要稳定可预测,动态部分用模板参数而不是让客户端拼字符串。比如file:///logs/{date}.log就比让客户端自己拼file:///logs/2024-01-01.log更安全,因为模板明确了参数格式。
接下来是resources/read的处理。客户端发来请求带上 URI,服务器返回contents数组。注意这个数组可以包含多个资源——比如读取一个目录时,可以把目录下所有文件一次性返回。文本用text字段,二进制用blob字段(base64 编码):
{ "contents": [ { "uri": "file:///logs/app.log", "mimeType": "text/plain", "text": "2024-01-01 10:00:00 INFO server started\n..." }, { "uri": "screen://localhost/display1", "mimeType": "image/png", "blob": "iVBORw0KGgoAAAANSUhEUg..." } ] }如果你用 TypeScript 写 Server,处理逻辑大概是这样:
server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const uri = request.params.uri; if (uri === "file:///logs/app.log") { const logContents = await readLogFile(); return { contents: [ { uri, mimeType: "text/plain", text: logContents } ] }; } if (uri.startsWith("file:///logs/")) { const date = uri.replace("file:///logs/", "").replace(".log", ""); const dailyLog = await readLogByDate(date); return { contents: [ { uri, mimeType: "text/plain", text: dailyLog } ] }; } throw new Error("Resource not found"); });声明能力时别忘了在 Server 初始化里加上capabilities: { resources: {} },否则客户端不会调用资源相关端点。如果你还要支持订阅,需要额外声明resources: { subscribe: true }。
订阅机制分两种。列表变化:服务器主动发notifications/resources/list_changed,告诉客户端可用资源列表变了。内容变化:客户端先发resources/subscribe带上 URI,服务器在资源更新时发notifications/resources/updated,客户端收到通知后再发resources/read拿最新内容,不需要了发resources/unsubscribe取消。这个设计的好处是客户端不用轮询,服务器也不用推送完整内容,只推一个“变了”的信号。
4. 验证请求:在本地 MCP Client 中跑通读取与订阅
配置写完了,得实际验证。我用 Claude Code 作为本地 MCP Client 来演示,其他客户端操作类似。先确认你的 Server 能被客户端启动,通常是在客户端的 MCP 配置里加一段 server 定义,指定启动命令和参数。
启动后,第一步验证resources/list。在客户端里触发资源列表查询,你应该能看到第 3 章配置的那些资源。如果列表为空,检查 Server 是否声明了resources能力,以及ListResourcesRequestSchema的 handler 是否注册成功。
第二步验证resources/read。选中file:///logs/app.log这个资源,客户端会发读取请求。成功的标志是你能在对话上下文里看到日志内容被注入。这里有个细节:不同客户端对资源的处理方式不同。Claude Desktop 目前要求用户显式选择资源后才使用,所以你得手动勾选;有些客户端会自动按启发式规则挑选;还有的会让模型自己决定用哪个。你实现 Server 时要能兼容这些模式,别假设客户端一定会自动读取。
第三步验证订阅通知。先发resources/subscribe订阅file:///logs/app.log,然后在服务器端手动改一下这个文件的内容,观察客户端是否收到notifications/resources/updated。收到后客户端应该自动或手动触发一次resources/read拿最新内容。如果没收到通知,检查两点:Server 能力里是否声明了subscribe: true,以及订阅请求的 URI 是否和资源列表里的完全一致(包括大小写和斜杠)。
验证过程中可以用模型对话页面辅助观察上下文变化,地址是https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=。把资源内容注入后,问模型“日志里最后一条错误是什么”,如果它能答出来,说明资源确实进了上下文。
实测下来,最容易出问题的是 URI 匹配。比如你列表里写的是file:///logs/app.log,读取时客户端传的是file:///logs/app.log,看起来一样,但如果中间有 URL 编码差异(空格变成%20)就会匹配失败。建议在 Server 里对 URI 做一次规范化处理再比较。
5. 常见报错排查:401、local proxy failed 与 reading choices
这一章对照真实报错来排。Resources 本身是 MCP 协议层的东西,但验证时你往往会先撞上模型接入层的错误,所以两类都要覆盖。
401 Unauthorized。这个通常出现在客户端连模型服务时,不是 MCP Server 的问题。原因一般是 API Key 没配、配错,或者 Base URL 写成了官网地址。检查你的配置文件里ANTHROPIC_API_KEY或OPENAI_API_KEY是否填了sk-开头的 Key,Base URL 是否是https://taotoken.net/api。如果 Key 是对的还报 401,去 API Keys 页面确认这个 Key 没有被删除或过期,入口https://taotoken.net/api-keys。
local proxy failed。这个报错说明客户端尝试走本地代理但失败了。常见原因是环境变量里残留了HTTP_PROXY或HTTPS_PROXY指向一个不存在的本地端口。解决办法是清掉这些环境变量,或者确认你的网络配置不需要代理。注意这里不涉及任何网络工具,纯粹是环境变量清理。
reading 'choices'。这是 OpenAI 兼容接口的典型报错,通常写成Cannot read properties of undefined (reading 'choices')。意思是客户端期望返回体里有choices字段,但实际拿到的响应结构不对。原因可能是 Base URL 路径错了,请求打到了非 API 端点,返回了 HTML 或错误 JSON。确认 Base URL 带/api,并且你用的模型 ID 在 TaoToken 支持的列表里。如果用的是 Claude Code 走 Anthropic 格式,返回结构里是content而不是choices,别把两种格式的客户端配置搞混。
OAuth 相关报错。有些客户端在连接远程 MCP Server 时会走 OAuth 流程,如果 Server 没实现授权端点就会报错。本地验证阶段建议先用 stdio 方式启动 Server,避开 OAuth。等 Resources 逻辑验证通了,再考虑远程部署和授权。
资源读取返回空。如果resources/read返回了contents: [],检查 handler 里的 URI 分支是否命中。建议在 handler 开头打一行日志,把收到的 URI 原样输出,对比资源列表里的 URI。另外注意contents数组里每个元素都必须有uri字段,漏了会导致客户端解析失败。
订阅后收不到通知。除了前面说的能力声明和 URI 匹配,还要确认你的 Server 真的在资源变化时调用了发送通知的方法。很多框架需要你显式调用server.notification()之类的接口,光改文件不会自动触发。
排查时建议按顺序来:先确认模型通道通(能对话),再确认 MCP Server 启动成功(列表能返回),最后确认资源读取和订阅。一层一层往下,别跳步。
6. 把 Resources 用对:边界、协作与下一步
Resources、Tools、Prompts 三者在 MCP 里各管一摊,边界清楚了实现才不会拧巴。Resources 是应用控制的数据暴露,适合“把这份日志/这张表/这个文件作为上下文”;Tools 是模型控制的动作执行,适合“帮我查一下、算一下、改一下”;Prompts 是预定义的提示模板,适合“按这个格式帮我生成”。三者可以协作:比如一个 Tool 执行完产生了一份报告,把报告注册成一个 Resource,客户端再决定要不要把它注入后续对话。
URI 设计上,我的经验是尽量让 scheme 表达数据域,路径表达层级,动态部分用模板参数。不要设计过于复杂的嵌套 URI,客户端解析起来容易出错。二进制资源记得正确设置mimeType,否则客户端可能不知道怎么渲染。
如果你还没配好模型入口,先去https://taotoken.net/api-keys拿 Key,接入文档在https://taotoken.net/doc。验证模型连通性用https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=。长期跑编码 Agent 任务可以看 Coding Plan,入口https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=。
下一步建议你把订阅通知接进一个真实场景:比如监控一个配置文件,变化时自动刷新上下文。跑通这个,Resources 就算真正用起来了。