跳到主要内容

另外两类 — 只读数据与现成话术

这一章讲三件事: 服务器怎么把只读的数据交给你、你怎么把它塞进发给模型的消息里; 服务器写好的话术是个什么东西、由谁来挑;以及三类东西在设计上各归谁管。

它在全书链条里的位置: 第 05 章那个「先要清单、再挑一个用」的套路, 在这一章换两个方法名再走两遍。同时它还清掉第 05 章欠下的一笔账—— 那两种当时认不出的内容块,这一章补上。

1. 工具让模型动手,资源让模型看到

这一节回答:服务器要给模型一份数据,为什么不干脆包成一个工具?

它可以。但 MCP 专门为「只读的数据」留了一类,叫资源——资源就是服务器提供的、 读了不会改变任何东西的那些数据

书里对它的说法很直接:资源让宿主应用和它连着的模型, 能拿到 MCP 服务器提供的数据——可以是数据库里的记录、图片、文本文件等等1

为什么要跟工具分开?两条理由。

第一条,一个会改变世界,一个不会。 调一次工具可能发出一封邮件、删掉一个文件; 读一次资源不会改变任何东西。分开之后,「哪些操作需要用户点头」这件事才好判断。

第二条,决定权不一样。 工具是摊给模型让它挑的; 资源是应用替用户挑好、再塞进去的——用户说「看看这个脚本」, 应用就去把那个脚本取来放进消息里,模型没有挑的机会。 (第 8 节会把三类东西各归谁挑列成一张表。)

2. 用户挑一条服务器写好的话术

这一节是本章主走查的第 1 步。

本章主走查的输入:用户在聊天框里打了一行 prompt: debug_script script_name deploy_script 交互写法(prompt: 开头、后面跟名字和成对的参数)来自书里的示例; 具体的话术名、脚本名、脚本内容和行数是我们为演示编的,不是真实数值。

第三类东西叫提示——服务器写好的一段可以反复用的话,中间留了空让你填

它和你随手打的那句话不一样,差别在谁来挑: 书里说得很清楚,提示是设计给应用的用户来控制的, 用户应该能够自己挑要用哪一条2

一条提示的定义里有三样:名字(唯一,用来指认)、一句给人看的说明、 以及一份参数表——每个参数有名字、说明,还有一个「必不必填」的标志2参数的值由客户端或宿主应用填进去,书里把「填了值的提示」叫做动态提示2

我们这台服务器的提示清单里有这么一条:

里面有什么
名字debug_script
说明「让模型逐行排查一个脚本为什么失败」
参数script_name(必填):要排查哪个脚本

用户打的那行里,script_name 的值是 deploy_script。 客户端把它填进去,调 prompts/get,拿回来的不是一段文字,是一整组现成的消息3:

取回来的两条消息:
① role = user
「请分析脚本 deploy_script 为什么会失败。逐行看,先列出最可疑的三处。」
② role = assistant
「我会先看退出码,再看文件权限,最后看路径。请把脚本正文给我。」

图说:第 ② 条是服务器替你写好的「模型该怎么开口」——
它把模型的排查顺序定死了。这就是话术的价值:
同一个问题,不同的开场白会得到完全不同的回答质量。

这两条消息可以直接发给模型,不需要再加工3。 但我们还差一样东西:脚本正文还没拿到。

3. 每份资源靠一个地址来指认

这一节是主走查的第 2 步,也是资源这一类最容易误解的地方。

客户端向服务器要一张资源清单,拿回来的每一条长这样4:

里面有什么deploy_script 这一条的值
地址file:///scripts/deploy.sh
名字deploy_script
说明「生产环境部署脚本」
内容类型(说明这是什么格式的东西,§4 细说)text/x-sh
大小约 1 200 字节——原始正文的字节数,不是行数,也不是 base64 之后那个涨了三分之一的大小5

注意清单里没有正文。 这是最容易踩的一脚: 列清单只拿到地址和说明,内容要拿着地址再取一次。

这里的「地址」不是街道那种地址——它是一串用来唯一指认某样东西在哪儿的字符串; 你天天见的网址就是它最常见的一种(官方文档里写作 URI,对照表见总纲)。 file:///scripts/deploy.sh 里,前面的 file 说明这是本机文件,后面是路径。

为什么要用地址而不是名字? 因为名字只在这台服务器内部唯一, 而地址天生带着「哪一类、在哪儿」的信息——换一台服务器也不会撞。

于是主走查的第 2 步是:客户端拿着 file:///scripts/deploy.shresources/read

4. 取回来的内容:文字和二进制两条路

这一节是主走查的第 3 步。

读回来的东西,只有两种形状6:

形状正文放在哪客户端该怎么办
文字text直接当字符串用
二进制blob一坨 base64,还要看它是什么类型才知道能不能用

第二行那个「什么类型」,靠的是资源清单里 mimeType 那一格。 清单里的每一格都有自己的名字,这种「一条数据里的一格」行话叫字段—— mimeType 就是一个字段名,§3 那张表里的每一行也各是一个字段。

这一格装的东西叫内容类型—— 一个标准写法的类型标签,前半截说大类、后半截说具体格式, 比如 image/pngaudio/mpegtext/x-sh7

为什么必须看它? 因为模型能吃的类型是有限的。 书里那个例子的做法是:如果类型是模型支持的几种图片格式之一,就照图片打包塞进去; 都不是的话,就打一条警告告诉用户「这个塞不进去」,然后原样把用户那句话发给模型8

我们这次拿到的是文字:

{ uri: "file:///scripts/deploy.sh",
mimeType: "text/x-sh",
text: "#!/usr/bin/env bash\nset -e\n…" ← 42 行、约 1 200 字节的脚本正文
}

图说:正文 1 200 字节,而清单里那条说明「生产环境部署脚本」只有 24 字节
(8 个汉字,一个汉字 3 字节)——**正文是它的五十倍**。
这正是清单里不放正文的原因:一台服务器可能有上千份资源,
清单要是带正文,一次列表就爆了。

5. 塞进去的姿势:追加进同一条消息,而不是新开一条

这一节是主走查的第 4 步。这一步做错不会报错,只会让模型答得莫名其妙。

现在你手里有两样东西:第 2 节那两条现成的消息、第 4 节那 42 行脚本正文。 怎么把正文交给模型?

最直觉的做法是再发一条消息:「这是脚本内容:……」。书里明确说不要这么做。

正确做法是:把那条用户消息展开成一个块列表,再把资源作为其中一块追加进去9:

❌ 错的:两条消息
① user: 「请分析脚本 deploy_script 为什么会失败……」
② user: 「#!/usr/bin/env bash …」 ← 模型会以为你又问了一遍

✅ 对的:一条消息,两块
① user: [ { type: "text", text: "请分析脚本 deploy_script 为什么会失败……" },
{ type: "text", text: "#!/usr/bin/env bash …" } ] ← 追加进来的
② assistant: 「我会先看退出码……」

图说:块列表是同一次发言的几个部分,新消息则是一次新的发言。
另开一条,模型会把它当成用户的第二个请求。

到这里,主走查跑完了:一条用户输入 → 一次要提示清单 → 一次取提示 → 一次要资源清单 → 一次读资源 → 拼成消息 → 一次调模型。 对照第 05 章那条走查:那边是两次模型一次服务器,这边是一次模型四次服务器。

6. 回头认领第 05 章欠的那两种块

这一节还账。 第 05 章讲工具返回的内容块时,我们说「还有两种块认不出来」。 现在你知道什么是资源了,可以认了。

第一种:嵌入资源——一份资源被整个塞进了一个内容块里,名字就是字面意思10。 工具返回它,通常是为了顺手多给一点背景资料,或者把数据先存一份下来备用11。 块里那份资源同样只有两种形状:文字或者一坨 base64——和第 4 节完全一样。

第二种:资源链接——它不塞正文,只给一个地址,让客户端自己决定要不要去取。 这一种书里没有提到,是官方规范里的东西12

两者的取舍很实在:

嵌入资源资源链接
块里装的是整份正文一个地址
一次来回够不够不够,要再取一次
一份 3 MB 的运行记录整个 3 MB 都塞进来了只有几十个字
什么时候用小、而且一定会用到大、或者未必用得上

记住第三行那个对照: base64 还会让体积再涨三分之一(3 换 4,见第 05 章脚注 14), 所以 3 MB 的运行记录嵌进来实际是 4 MB——它会挤掉模型能读的其他东西。

7. 地址可以留空格,填进去才算数

这一节讲一个专治「清单列不完」的机制。

假设一台服务器背后是一个有 5 万条记录的数据库。它要把 5 万条资源全列在清单里吗? 那张清单本身就没法用了。

所以还有一种东西叫资源模板——服务器给出一个带占位的地址样板, 由客户端把空着的那一段填上,才变成一个真地址13

服务器给的样板: db://customers/{customer_id}
↑ 花括号里是要你填的那一段
客户端填进去: db://customers/8421
然后拿这个地址去读

图说:一条样板顶替了 5 万条清单项。
书里说这种字符串在 Python 工具包里「长得像 f-string」,变量段用花括号括起来。

它和普通资源几乎一模一样:属性完全相同,只是那个「地址」那一格换成了「地址样板」14。 清单也单独有一个方法(resources/templates/list),别忘了取。

顺便提一句书里留的作业: 作者在这一节末尾建议读者自己去实现资源模板的列出和使用, 因为他那个示例程序里没有做15。所以书里对这一块只有描述,没有走查。

8. 三类东西各归谁管

这一节给出一张表,它是 MCP 设计里最容易被忽略、也最容易被违反的一条约定。

这一类归谁挑长什么样
工具模型摊在模型面前,它自己决定用不用、用哪个
资源应用应用替用户取好、塞进消息里,模型没有挑的机会
提示用户用户从菜单里点一条(比如斜杠命令),客户端填参数

书里对第三行说得很明确:提示是设计给应用的用户来控制的2。 另外两行来自官方规范的同一张表16

这条约定在真实实现里经常被打破,而且打破之后会出问题:

常见的打破法出什么问题
把资源包成一个工具,让模型自己去读模型可能读一堆没用的东西,把它能读的篇幅占满;而且「只读」这条保证没了
把提示做成工具,让模型自己挑话术用户失去了控制权——他以为自己在提问,其实模型换了一套开场白
把工具做成资源(读一下就触发副作用)最危险的一种:所有「只读所以安全」的假设全部失效

判断(我们的,不是书里的): 这张归属表的实际作用不是「规定你只能这么写」, 而是给你一把尺子去量别人的服务器。 你接一台第三方服务器时,先看它有没有把该归用户的东西挪给了模型—— 挪了的话,你就失去了对这台服务器的控制点。 如果错,会错在: 如果宿主应用本来就打算让模型全权代理(比如无人值守的批处理), 那么「用户失去控制权」就不是缺陷而是设计目标,这把尺子就不适用。

9. 边界:资源怎么用,书里说还没定论

这一节说清楚这一章哪些是确定的、哪些不是。

书里在讲资源的第一句就打了预防针:资源可以扮演很多角色, 其中有些角色社区自己都还没探索完——他举的例子正是 拿资源当一份先存好、反复取用的底稿:同一份资料不必每轮对话都完整塞进去一遍1

省下来的是模型能读的篇幅——而篇幅是要花钱的(这笔账在第 09 章算)。

作者给了一个真项目当例证:Tim Kellogg 的 tupac, 一个把 MCP 资源当成先存好的底稿、用来提高提示效率的极简客户端实现17

这一章其余的边界:

什么状态
资源清单变了、某份资源改了,服务器可以主动告诉你书里提到了,但说 Python 工具包只做了一半——没有地方挂处理函数18(第 08 章讲)
资源模板的实际用法书里留成作业,没有示例15
资源链接这种块书里没有,取自官方规范12
三类东西各归谁挑的完整表书里只写了提示那一行,另两行取自官方规范16
资源那两个「变了就告诉我」的登记方法(resources/subscribe)书里列了,但它在 2026-07-28 的规范里已被换掉(第 12 章)

10. 可带走的

  1. 资源 = 服务器提供的只读数据;和工具分开,是因为一个会改变世界、一个不会;
  2. 资源清单里只有地址和说明,没有正文——正文要拿着地址再取一次;
  3. 地址比名字可靠:名字只在一台服务器内唯一,地址自带「哪一类、在哪儿」;
  4. 读回来只有两种形状:文字,或者一坨 base64;后者还要看内容类型才知道模型吃不吃得下;
  5. 塞进去要追加进同一条消息的块列表,不能另开一条——另开一条模型会当成新请求;
  6. 提示 = 服务器写好的、留了空的一段话,由用户挑、由客户端填参数,取回来是一整组现成消息;
  7. 嵌入资源 = 正文整个塞进块里;资源链接 = 只给地址——大文件用后者,base64 还要再涨三分之一;
  8. 资源模板 = 带花括号占位的地址样板,一条顶替上万条清单项;
  9. 三类东西各归谁挑:工具归模型、资源归应用、提示归用户——用它当尺子去量第三方服务器;
  10. 资源的玩法书里自己说没定论,拿它当一份反复取用的底稿是社区在试的一条路。

11. 原文地图

主题原书章原文位置
资源是什么、还没探索完、当缓存Interacting with MCP Server Capabilitiestext/08-fm-interacting-with-mcp-server-capabilities.txt:317(搜「MCP resources can play many roles」) · :319(搜「as a cache to optimize prompt size」)
资源的五个动作Interacting with MCP Server Capabilitiestext/08-fm-interacting-with-mcp-server-capabilities.txt:333(搜「accesses the file provided by」)
资源的属性Interacting with MCP Server Capabilitiestext/08-fm-interacting-with-mcp-server-capabilities.txt:389(搜「uri, name, description, mimeType, size」)
读回来两种形状Interacting with MCP Server Capabilitiestext/08-fm-interacting-with-mcp-server-capabilities.txt:397(搜「either as a text string or a base64-encoded data string」)
类型不支持就警告Interacting with MCP Server Capabilitiestext/08-fm-interacting-with-mcp-server-capabilities.txt:567(搜「we warn the user and continue」)
不要另开一条消息Interacting with MCP Server Capabilitiestext/08-fm-interacting-with-mcp-server-capabilities.txt:572(搜「should not create a separate user message」)
提示归用户挑、参数、动态提示Interacting with MCP Server Capabilitiestext/08-fm-interacting-with-mcp-server-capabilities.txt:597(搜「designed to be controlled by the application user」) · :606(搜「These are called dynamic prompts」)
取回来是现成消息Interacting with MCP Server Capabilitiestext/08-fm-interacting-with-mcp-server-capabilities.txt:613(搜「ready-to-use messages」)
嵌入资源Interacting with MCP Server Capabilitiestext/08-fm-interacting-with-mcp-server-capabilities.txt:104(搜「Tools can return embedded resources as part of」) · :621(搜「resources that are embedded in a message content block」)
资源模板Interacting with MCP Server Capabilitiestext/08-fm-interacting-with-mcp-server-capabilities.txt:328(搜「list of resource URI templates」) · :331(搜「variable sections enclosed in curly braces」)
tupac 项目Interacting with MCP Server Capabilitiestext/08-fm-interacting-with-mcp-server-capabilities.txt:347(搜「resources as a cache for efficient prompting」)
通知只做了一半Interacting with MCP Server Capabilitiestext/08-fm-interacting-with-mcp-server-capabilities.txt:422(搜「partially implemented in the Python SDK」)

Footnotes

  1. 出处:「Interacting with MCP Server Capabilities」第 317 段(text/08-fm-interacting-with-mcp-server-capabilities.txt:317,搜「MCP resources can play many roles」)与第 319 段(text/08-fm-interacting-with-mcp-server-capabilities.txt:319,搜「as a cache to optimize prompt size」)。原文列的数据类型是数据库记录、图片、文本文件等等(第 321 段,text/08-fm-interacting-with-mcp-server-capabilities.txt:321,搜「This can be database records」)。 2

  2. 出处:「Interacting with MCP Server Capabilities」第 597 段(text/08-fm-interacting-with-mcp-server-capabilities.txt:597,搜「designed to be controlled by the application user」)与第 606 段(text/08-fm-interacting-with-mcp-server-capabilities.txt:606,搜「These are called dynamic prompts」)。原文对参数表的描述是:一个字典列表,每项有名字、可选的说明、以及一个可选的「是否必填」布尔值。 2 3 4

  3. 出处:「Interacting with MCP Server Capabilities」第 613 段(text/08-fm-interacting-with-mcp-server-capabilities.txt:613,搜「ready-to-use messages」)。原文说取回来的是一组 PromptMessage,每条有 rolecontent 两个必填属性,可以转成普通字典直接发给模型;content 可以是文字、图片、音频或嵌入资源。 2

  4. 出处:「Interacting with MCP Server Capabilities」第 389 段(text/08-fm-interacting-with-mcp-server-capabilities.txt:389,搜「uri, name, description, mimeType, size」)。原文一共列了七个属性,本章表里只留了会影响使用的五个。

  5. 补充(不在书里):书里只列了 size 这个属性名(见脚注 4),没有说它装的是什么。官方规范写死了口径:「原始资源内容的大小,单位是字节(也就是说,在 base64 编码或任何词元化之前)」,用途是让宿主显示文件大小、并估算它会占掉多少上下文——正是第 09 章那笔上下文预算账要用的输入。所以这一格填的绝不能是行数。来源:MCP 官方规范资源页 https://modelcontextprotocol.io/specification/2026-07-28/server/resources(查阅于 2026-08-25)。

  6. 出处:「Interacting with MCP Server Capabilities」第 397 段(text/08-fm-interacting-with-mcp-server-capabilities.txt:397,搜「either as a text string or a base64-encoded data string」)。作者还说明了为什么他的示例把这两种类型原样交给调用方、不做转换:它们本身就携带了「这是哪一种字符串」这个信息。

  7. 补充(不在书里,来自通用知识):这种「大类/具体格式」的标签在官方文档与网络标准里叫 MIME type(也叫 media type),是一套早于 MCP 很多年的通用写法;查官方文档时搜这个词。

  8. 出处:「Interacting with MCP Server Capabilities」第 567 段(text/08-fm-interacting-with-mcp-server-capabilities.txt:567,搜「we warn the user and continue」)。原文的判断顺序是:先看是不是纯文本,是就直接做一个文字块;不是的话看内容类型在不在模型支持的图片格式里,在就做一个图片块;两者都不是就警告用户,然后照常把用户那句话发出去。

  9. 出处:「Interacting with MCP Server Capabilities」第 572 段(text/08-fm-interacting-with-mcp-server-capabilities.txt:572,搜「should not create a separate user message」)。原文的做法是:把用户消息转成完整形式——一个带 typecontent 两个键的嵌套字典,content 是一个字典列表,再把上下文块追加进这个列表。

  10. 出处:「Interacting with MCP Server Capabilities」第 621 段(text/08-fm-interacting-with-mcp-server-capabilities.txt:621,搜「resources that are embedded in a message content block」)。原文的措辞是「就是你想的那个意思」——被嵌进一个消息内容块里的资源。

  11. 出处:「Interacting with MCP Server Capabilities」第 105 段(text/08-fm-interacting-with-mcp-server-capabilities.txt:105,搜「typically for extra context or data caching」)。这一句出现在第 05 章讲的那份内容块类型清单里,原文同时说明块里那份资源可以是文字型也可以是二进制型(后者同样是 base64)。

  12. 补充(不在书里):官方规范里工具结果的内容块除了文字、图片、音频、嵌入资源,还有一种 resource_link——只携带资源地址,不携带正文,由客户端决定要不要另外去读。书里列的四种块里没有它。 来源:MCP 官方规范工具页 https://modelcontextprotocol.io/specification/2026-07-28/server/tools(查阅于 2026-08-25)。 2

  13. 出处:「Interacting with MCP Server Capabilities」第 328 段(text/08-fm-interacting-with-mcp-server-capabilities.txt:328,搜「list of resource URI templates」)与第 331 段(text/08-fm-interacting-with-mcp-server-capabilities.txt:331,搜「variable sections enclosed in curly braces」)。原文的说法是:这些样板让你能根据手头的其他信息动态拼出一个资源地址

  14. 出处:「Interacting with MCP Server Capabilities」第 390 段(text/08-fm-interacting-with-mcp-server-capabilities.txt:390,搜「For resource templates, instead of a」)。原文明说两者属性几乎完全一样,只是资源模板用 uriTemplate 代替了 uri

  15. 出处:「Interacting with MCP Server Capabilities」第 578 段(text/08-fm-interacting-with-mcp-server-capabilities.txt:578,搜「listing and using resource」)。作者把「支持列出和使用资源模板」列为留给读者的练习,另外两个练习是支持多份上下文文件、以及把 context: 那段关键字从发给模型的文字里去掉。 2

  16. 补充(不在书里):官方规范用一张「控制层级」表规定了三类原语各归谁控制——提示由用户控制(例如斜杠命令、菜单选项),资源由应用控制(由客户端附加和管理,例如文件内容、版本库历史),工具由模型控制(暴露给模型去执行动作,例如发请求、写文件)。来源:MCP 官方规范服务器总览 https://modelcontextprotocol.io/specification/2026-07-28/server/index(查阅于 2026-08-25)。 2

  17. 出处:「Interacting with MCP Server Capabilities」第 347 段(text/08-fm-interacting-with-mcp-server-capabilities.txt:347,搜「resources as a cache for efficient prompting」)。项目名与作者见第 345 段(text/08-fm-interacting-with-mcp-server-capabilities.txt:345,搜「the tupac project by Tim」)。

  18. 出处:「Interacting with MCP Server Capabilities」第 422 段(text/08-fm-interacting-with-mcp-server-capabilities.txt:422,搜「partially implemented in the Python SDK」)。原文说协议允许客户端订阅资源变更,但当时 Python 工具包只实现了一半,没有提供把这类消息交给处理函数的口子;并说 TypeScript 那一版似乎有。