周一上午,老李接了一个让他脸都绿了的电话。客户张总劈头盖脸:“你们那个 AI 客服怎么又说我们的合同条款是旧版的?去年十一月就改了,这都半年了!你们系统到底更不更新?”
老李挂了电话,一脸黑线地查后台。他发现 Prompt 里直接嵌了一段公司合同条款的文本——是去年十月的版本。他忘改了。这已经是本月第三次因为 Prompt 里的过时数据被投诉。
小王端着一杯拿铁走过来:“老李,要不要试试把数据从 Prompt 里抽出来?”
“抽出来放哪儿?放数据库里?那 AI 怎么读到?”老李没好气。
“用 MCP Resources。”小王把椅子拉近,“你知道你的 Prompt 现在多长吗?两万多字!里面三分之一是静态数据——产品介绍、合同条款、公司地址。不仅难维护,而且每次对话都把这些全部发给 AI,Token 烧得飞快。Resources 就是帮你把这些数据变成可按需访问的‘接口’,AI 真正需要时才去查,Prompt 瘦身了,数据更新也只需要改一处。”
老李皱起眉头:“说人话。”
“就像你以前把整个合同档案室搬到了办公桌上,每次有人问就从头翻一遍。现在你把档案室恢复成原来的样子,需要哪份合同,叫秘书去档案室调出来。”
Resource 是什么:给每份数据一个“门牌号”
小王打开笔记本,调出一个 MCP Server 的配置文件。“MCP 把数据抽象成 Resource,每个 Resource 都有一个 URI,就像每本书在图书馆里有一个索书号。AI 需要数据时,不是翻整本 Prompt,而是用 URI 去请求。”
“比如,你们公司的产品介绍,可以定义成 docs://products/api-gateway。AI 发现用户在问 API 网关的功能,它不会去搜那两万字的 Prompt 海洋,而是直接请求 docs://products/api-gateway,拿到最新版本的产品介绍,再回复用户。”
老李若有所思:“那如果用户问的是‘所有产品的价格’,不可能一个一个 URI 去请求吧?”
“你可以定义参数化的 URI 模板。就像 REST API 一样——docs://products/{product_id},AI 可以传不同的 product_id 去拉取不同产品的数据。这不是 Prompt 里能动态做到的。”
// MCP Server 中定义 Resource 及 URI 模板
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = new Server({
name: "company-docs-server",
version: "1.0.0",
}, {
capabilities: { resources: {} }
});
// 静态 Resource:公司地址
server.setRequestHandler("resources/list", async () => ({
resources: [{
uri: "company://address",
name: "公司地址",
mimeType: "text/plain",
description: "公司总部地址和联系方式"
}]
}));
server.setRequestHandler("resources/read", async (request) => {
if (request.params.uri === "company://address") {
return {
contents: [{
uri: request.params.uri,
mimeType: "text/plain",
text: "公司地址:北京市海淀区中关村软件园 A 座 1001 室,电话 010-12345678"
}]
};
}
throw new Error(`Resource not found: ${request.params.uri}`);
});“看到没?”小王指着代码,“Resource 不嵌入 Prompt,不改 AI 的代码逻辑。客户条款更新了,只需要改 Server 里那个 text 字符串。AI 下次请求自动拿到新数据。”
URI 模板:让资源“活”起来
老李翻出他之前的产品手册 Prompt,指着一大段文字:“那这种按产品 ID 来查的怎么办?我总不能列几十个静态 URI。”
“用 URI 模板。”小王在原来的 Server 代码上添了几行。
// 添加参数化 Resource:docs://products/{productId}
server.setRequestHandler("resources/list", async () => ({
resources: [{
uri: "docs://products/{productId}",
name: "产品文档",
mimeType: "text/markdown",
description: "根据产品 ID 获取详细的产品介绍文档"
}]
}));
server.setRequestHandler("resources/read", async (request) => {
const uri = request.params.uri;
const match = uri.match(/^docs:\/\/products\/(.+)$/);
if (match) {
const productId = match[1];
// 从实际数据库或文件系统读取
const doc = await getProductDoc(productId);
return {
contents: [{
uri,
mimeType: "text/markdown",
text: doc
}]
};
}
throw new Error(`Resource not found: ${uri}`);
});“AI 知道了模板 docs://products/{productId},当它需要某个产品的文档时,就会用具体的 ID 去构造 URI 并请求。你的 Prompt 里只需要一句‘相关产品文档可通过 MCP Resource 获取’,不需要把整本手册贴进去。”
老李拿起保温杯喝了一口,点点头:“这个模板跟 OpenAPI 的 path parameter 挺像。AI 怎么知道 productId 应该填什么?”
“靠列表接口。MCP 可以提供一个 resources/list 返回所有可用资源。你还可以做一个 docs://products 的静态 Resource,列出所有产品 ID 和名称。AI 可以先查列表,再根据列表去取具体文档——两步操作,很像人在翻目录。”
不止文本:内容类型与二进制
“那如果我的资源不是纯文本呢?比如产品架构图是 PNG,或者 API 文档是 JSON?”老李追问道。
“MCP Resource 支持多种 MIME 类型。text/plain、text/markdown、application/json,甚至 image/png。对于二进制资源,SDK 返回 base64 编码的 blob。AI 如果是多模态的,可以直接看图片;如果不是,至少能收到文件大小和类型信息。”
小王又展示了一个读取图片资源的示例:
// 二进制 Resource 示例
server.setRequestHandler("resources/read", async (request) => {
if (request.params.uri === "images://architecture") {
const imageBuffer = fs.readFileSync("arch.png");
return {
contents: [{
uri: request.params.uri,
mimeType: "image/png",
blob: imageBuffer.toString("base64")
}]
};
}
throw new Error(`Resource not found: ${request.params.uri}`);
});“设计 Resource 的时候,你可以根据内容选择最合适的 MIME 类型。合同用 Markdown,表格用 JSON,架构图用 PNG。AI 的上下文处理能力会越来越强,现在结构化数据给它 JSON 比纯文本效果好得多。”
老李忽然问:“如果我更新了产品文档,AI 怎么知道该重新请求?”
订阅机制:数据更新时,AI 不用被蒙在鼓里
“这是 MCP 比 REST 更聪明的地方。Server 可以主动发通知——notifications/resources/updated。Client 收到通知后,可以把缓存的 Resource 标记为过期,下次 AI 用到时会重新拉取。这样你就不会因为 Prompt 没改而用旧数据。”
小王把订阅相关的代码也加到 Server 里:
// 在 Server 初始化时声明支持资源订阅
const server = new Server({
name: "company-docs-server",
version: "1.0.0",
}, {
capabilities: {
resources: { subscribe: true } // 关键:声明支持订阅
}
});
// 当文档更新时,发送通知给所有订阅了该 URI 的 Client
async function onProductDocUpdated(productId: string) {
server.notification({
method: "notifications/resources/updated",
params: { uri: `docs://products/${productId}` }
});
}“这个通知机制,要求 Client 也支持。目前 Claude Desktop 能接收到这类通知并自动刷新资源缓存。就算你的 AI 应用暂时不支持实时刷新,至少你可以用这个通知触发一个 webhook,人工检查一下更新。”
老李听完,靠在椅背上沉默了一会儿。然后他把之前那两万字的 Prompt 文档打开,用鼠标选中全部硬编码的数据部分,按下了 Delete。
“这些全删。改用 MCP Resources。你给我写个资源清单——公司地址、产品文档、合同条款、还有那个老出问题的年假政策。”
“老李,你这效率——”
“下不为例啊。”老李赶紧绷住脸,但嘴角已经翘了起来,“对了,你刚才说的订阅通知,如果 Client 不在线,它怎么知道错过了更新?Server 有没有类似事件序号的东西?你给我看看那部分的协议定义。”
小王咧嘴一笑,打开了 MCP 的 spec 文档。窗外阳光正好,老李的 Prompt 终于从一个“杂物间”变成了一个干净的“调度中心”,那些硬编码的数据碎片找到了各自的 URI 门牌号,安静地等待着 AI 的按需探访。

