mcp-spec — 本课题摘录
读了哪几篇: 03-server-primitives(服务器三件套)、04-mrtr-and-client-features(多轮往返请求)。
其余四篇(协议地基、版本协商、传输、鉴权)本轮没读——属「接外部能力」课题。
本课题只取它的一小块:工具的权威形状,以及"一轮没走完"这种中间态怎么表达。传输与 MCP 生态留给后面的课题。
它对本课题回答了什么
决定二:工具的权威形状 —— 而且"三件套"的分法值得抄
它把服务器 能提供的东西分成三种,分法的依据是"谁决定它何时被用":
| 原语 | 谁控制 | 一句话 |
|---|---|---|
| 提示模板 | 用户控制 | 用户主动选的模板/指令 |
| 资源 | 应用控制 | 应用塞给模型的上下文数据 |
| 工具 | 模型控制 | 暴露给模型自主调用的动作 |
(依据:协议库 · Model Context Protocol (规范) · 服务器三件套 Tools / Resources / Prompts —— 三件套按「谁决定它何时被用」分——Prompts 用户控制、Resources 应用控制、Tools 模型控制,这条控制层级决定它们在 UI 与安全策略上的不同待遇)
口诀:提示给用户点、资源给应用塞、工具给模型调。
这条分法对我们直接有用。 我们的最小循环里,"系统提示"是应用塞的、"用户那句话"是用户给的、"工具"是模型调的——三样东西的权限和信任级别本来就不同,但很多实现把它们混成一坨字符串。 这也跟 openai-model-spec 的指令层级、browseros 的"外部内容标成不可信数据"是同一条线。
工具的最小形状:名字 + 描述 + 一份参数格式说明(JSON Schema)。
还可以再配一份返回格式说明。 配了之后服务器必须返回符合它的结构化内容,客户端应当据此校验。 (依据:协议库 · Model Context Protocol (规范) · 服务器三件套 Tools / Resources / Prompts —— 工具可选配 outputSchema,给了之后服务器必须返回符合它的 structuredContent、客户端应据此校验,让工具返回值从一坨文本升级成有类型可校验的结构化数据)
这条把"结果怎么回填"往前推了一步: 大多数实现的工具结果就是一段文本, 这里说的是结果也可以有格式契约。对我们来说,这意味着工具定义时可以同时定"进什么"和"出什么"。
一个小惯例值得抄:无参工具应该明确写成"只收一个空对象",而不是留空。
规范划了一条安全红线:应当始终有人在回路里,能否决工具调用。
决定一:跨调用的状态用"显式句柄",不靠会话
这是这份规范里对我们最有启发的一条。
协议本身没有会话概念,服务器不能靠"连接"把前后两次调用关联起来。那购物车、浏览器上下文、数据库事务这类跨调用状态怎么办?
答案:从创建工具返回一个显式句柄,后续调用把它当普通参数传回来。
调「建购物车」 ──► 结果里返 回一个 id
调「加商品」 ──► 参数里带上那个 id
(依据:协议库 · Model Context Protocol (规范) · 服务器三件套 Tools / Resources / Prompts —— 无协议级会话时,跨调用状态的做法是从创建工具返回一个显式 handle、后续调用把它当普通参数传回来)
设计句柄的四条要求:
| 要求 | 为什么 |
|---|---|
| 句柄是"名字"不是"凭证" | 每次都要校验调用者是否被授权,拿到句柄不等于有权限 |
| 要不透明 | 别把内部结构编码进去 |
| 有有限生命周期 | 不能永久有效 |
| 过期时返回能让模型自救的执行错误 | 让模型知道"重新建一个"而不是卡死 |
最后一条又是"错误即消息"。 而且它点明了错误文本的目标不是给人看,是让模型能自救。
决定四:"一轮没走完"是一种可以表达的中间态
这是全协议最巧妙的一章,而且直接对着本课题。
问题:服务器处理一个工具调用时,发现"我还得先问问用户 / 先让模型生成一段 / 先要客户端 的工作目录"——它怎么反过来要东西,又不留任何状态?
旧做法是服务器直接朝客户端发一个请求。这要求传输层支持反向请求,而且服务器得记住"我正等着这个回复"——这就是状态。
新做法:服务器不推请求,而是把"我需要什么"塞进结果里返回,客户端补齐后重发原请求。
客户端 ──► 请求
服务器 ──► 「还差点信息」+ 需要哪些 + 一段不透明的「上下文存根」
客户端去收集(问用户 / 调模型)
客户端 ──► 重发原请求,带上补齐的信息 + 原样回显那段存根
服务器 ──► 用存根重建上下文,给出最终结果
(依据:协议库 · Model Context Protocol (规范) · MRTR 与客户端能力(无状态下服务器怎么反问客户端) —— MRTR 用 InputRequiredResult 把「我还需要什么」塞进结果返回,客户端补齐后重发原请求并原样回显 requestState;服务器发起的独立请求在 2026-07-28 版被彻底废除)
对本课题的意义: 我们的循环 里,"模型要调工具"和"工具还缺信息"是两种不同的中间态。 大多数实现只有前者,后者被塞进"工具返回一个错误让模型重试"。这里把它做成了协议里的一等状态。
妙在哪:把状态外包给对方保管
服务器不存任何会话状态,而是把"处理到一半时需要记住的上下文"编码进那段存根,丢给客户端保管,重试时再拿回来重建。
好处:任意一台服务器实例都能处理重试——重试请求里自带全部上下文,不需要共享存储、不需要粘性路由。
但代价说得同样清楚:客户端可能篡改它。 所以规范给了严格要求:
| 要求 | 内容 |
|---|---|
| 定性 | 服务器必须把这段存根当攻击者可控输入 |
| 完整性 | 若它影响授权/资源访问/业务逻辑,必须做完整性保护(签名或加密),校验失败就拒 |
| 防重放 | 受保护载荷里应塞:认证主体(拒绝换人重放)、短有效期(拒绝过期)、原请求标识(拒绝张冠李戴) |
(依据:协议库 · Model Context Protocol (规范) · MRTR 与客户端能力(无状态下服务器怎么反问客户端) —— requestState 把状态外包给客户端保管,好处是任意服务器实例都能处理重试;代价是必须把它当攻击者可控输入,影响授权时必须做 HMAC/AEAD 完整性保护,并在载荷里塞认证主体、短 TTL、原请求标识防重放)
这一条跟 vercel-ai-sdk 的"审批凭证要签名"是同一件事:凡是从不可信一端回来的状态,都要当攻击面对待。 我们的最小原型跑在本地、没有不可信端,但一旦把循环拆成前后端,这条立刻生效。
它没回答什么
- 循环本身怎么写——它是协议,不是实现。
- 停止条件——不在它的范围。
- 历史怎么管——不在它的范围。
坑与代价
- "把状态外包"要求全部状态可编码成一个字符串。 跟 rig 的"无 IO 状态机"是同一约束——握着一个活连接的东西编不进去。
- 有一个偏底层的特性容易踩坑: 工具可以把某个参数镜像到一个 HTTP 头里,让中间层不解析请求体就能路由。约束很严,违反时客户端必须把该工具从清单里剔除。 安全提醒:别把密码、密钥、个人信息标成这种参数——头对中间件可见。 (依据:协议库 · Model Context Protocol (规范) · 服务器三件套 Tools / Resources / Prompts —— x-mcp-header 让参数值镜像到 HTTP 头以 便中间层按参数路由,约束严格且违反时客户端必须剔除该工具;安全提醒是别把密码/密钥/PII 标成 header,因为头对中间件可见)
- 这一版是破坏性变更。 服务器发起的独立请求被彻底废除。照旧文档实现会对不上。