- 状态(Status): Final
- 类型(Type): Standards Track
- 创建(Created): 2025-09-30
- 作者(Author(s)): Olivier Chafik (@ochafik)
- Issue: #1577
更新:
- 10 月 1 日:将
tool_choice重命名为toolChoice(+"none"值);移除了异类stopReason"refusal"和"other";允许{CreateMessageResult,SamplingMessage}.content为单个内容或内容数组; - 10 月 6 日:使
ToolResultContent与CallToolResult对齐(支持图像/音频);添加了”可能的后续”章节。 - 10 月 10 日:用简单的工具注册表更新了参考实现示例(统一 mcp 工具与工具循环工具,见下方评论),以及一个使用带工具采样 + 征询的”选择你自己的冒险”游戏。
- 10 月 27 日:使
ToolResultContent.content与CallToolResult.content对齐(使用 ContentBlock);添加了ToolResultContent._meta - 11 月 5 日:
- 将
stopReason保留为开放字符串,但带有冗余的显式枚举以提高可见性 - 移除了当
includeContext与公告的ClientCapabilities.sampling.context不匹配时抛错的要求 - 通过规定采样在较早规范版本中**不得(MUST NOT)**返回数组,缓解了
CreateMessageResult.content是内容数组或单个内容所带来的向后兼容问题(+ 承认使用采样的代码的 SDK 更新将需要小的代码改动)
- 将
- 11 月 7 日:将类型
ToolCallContent重命名为ToolUseContent(以匹配其tool_use类型和toolUsestopReason)。SEP 已获批准! - 11 月 10 日:移除
disable_parallel_tool_use/ 保留待后续更新,因为 Gemini API 目前无法实现这一点。 - 11 月 11 日:添加了关于 Gemini API 函数调用模式和角色的额外说明;要求带工具结果内容的 SamplingMessage 不与其他内容类型混合
摘要
本 SEP 向sampling/createMessage 引入 tools 和 toolChoice 参数,并软弃用 includeContext(将 thisServer 和 allServers 置于一个能力之下)。这允许 MCP 服务器使用客户端的 token 运行自己的代理式循环(仍在用户监督之下),并降低客户端实现的复杂性(上下文支持变为显式可选)。
动机
- 采样不支持工具调用,尽管它是现代代理式行为的基石。没有对它的显式支持,使用采样的 MCP 服务器要么尝试用复杂的提示/对输出的自定义解析来模拟工具调用,要么被局限于更简单的、非代理式的请求。添加对工具调用的支持可以在 MCP 生态中解锁许多新颖用例。
- 上下文包含的定义含糊(见此文档):它使得完整实现采样特别棘手,这连同采样所需的其他预防措施(不受本 SEP 影响)可能导致了该特性在客户端中的低采用(该特性在 2024 年 11 月的 MCP 规范中引入)。
- MCP Sampling(@jerome3o-anthropic):极为相似的提案:
- 添加相同的 tools 语义,
- 弃用
includeContext(该文档解释了为何其语义含糊) - (更进一步建议显式上下文共享,这超出本提案范围)
- 允许 Prompt/Sampling 消息包含多个内容块。#198
- 在此 PR 中,我们使
{CreateMessageResult,SamplingMessage}.content接受单个内容或内容数组。result.content的变更向后不兼容,但支持并行工具调用所必需。SamplingMessage.content的变更则使编写工具循环自然得多(见参考实现中的示例:toolLoopSampling.ts)
- 在此 PR 中,我们使
规范
概览
- 在 CreateMessageRequest 中添加传统的工具调用支持,带
tools(含 JSON schema)和toolChoice参数,需要服务器端的工具循环- 采样现在可以产出 ToolCallBlock 响应
- 服务器需要自行调用工具
- 服务器再次以 ToolResultParamBlock 调用采样以注入工具结果
toolChoice.mode可以是"auto" | "required" | "none",以支持常见的结构化输出用例(可能的后续改进见下文)- 由新能力(
sampling { tools {} })设限
- 修复/更新 CreateMessageResult 中规定不足的字符串:
stopReason: "endTurn" | "stopSequence" | "toolUse" | "maxToken" | string(显式枚举 + 为兼容而设的开放字符串)role: "assistant"
- 软弃用 CreateMessageRequest.params.includeContext != ‘none’(现由能力设限)
- 激励实现无上下文的采样
协议变更
sampling/createMessage当includeContext为"thisServer" | "allServers"但缺少clientCapabilities.sampling.context时,必须抛错- 当定义了
tool或toolChoice但缺少clientCapabilities.sampling.tools时,**必须(MUST)**抛错 - 服务器**应当(SHOULD)**避免
includeContext != 'none',因为值"thisServer"和"allServers"可能在未来的规范版本中被移除。 CreateMessageRequest.messages**必须(MUST)**使任何带ToolUseContent(和id: $id1)的 “assistant” 消息与一条带 ToolResultContent(和tool_result_id: $id1)的 “user” 消息相平衡- 注意:这是 Claude API 实现的一个要求(并行工具调用必须一次性全部得到响应)
- 带工具结果内容块的 SamplingMessage **不得(MUST NOT)**包含其他内容类型。
Schema 变更
-
ClientCapabilities
-
CreateMessageRequest(使用既有的 Tool)
- 说明:
- OpenAI vs. Anthropic API 避免并行工具调用的惯用法:
- OpenAI:
parallel_tool_calls: false(顶层参数) - Anthropic:
tool_choice.disable_parallel_tool_use: true- 此处优先,因为未设时默认值为 false(即允许并行工具调用)
- OpenAI:
- OpenAI vs. Anthropic API 关于
tool_choice"none"vs.tools:- OpenAI:
tools: [$Foo], tool_choice: "none"禁止任何工具调用- 此处优先采用的行为
- Anthropic:
tools: [$Foo], tool_choice: {mode: "none"}仍可能调用工具Foo
- OpenAI:
- Gemini vs. OAI / Anthropic 关于
disable_parallel_tool_use:- Gemini API 目前无法禁用并行工具调用(不像 OAI / Anthropic API)。暂时移除此标志,待 Gemini 有任何支持方式时再重新引入。否则客户端会得到意外的多个工具调用(或者若那样实现,则意外失败/代价高昂地重试直到发出单个工具调用)
- Gemini API 的函数调用模式有一个
ANY值,应当与所提议的required匹配
- OpenAI vs. Anthropic API 避免并行工具调用的惯用法:
- 说明:
-
SamplingMessage:
-
说明:
- 在工具调用方面,各 API 之间角色 vs. 内容类型的差异:
- OpenAI:
role: "system" | "user" | "assistant" | "tool"(其中 tool 用于工具结果),而工具调用嵌套在 assistant 消息中,此时 content 通常为 null,但一些”OpenAI 兼容”API 接受非 null 值 -
- Claude API:
role: "user" | "assistant",工具使用和结果通过特殊类型的消息内容部分传递: -
- Gemini API:
function角色(类似于 OAI 的tool角色)- 没有工具调用 id 概念(函数调用:Gemini 要求工具结果以与工具使用部分完全相同的顺序提供。实现可以生成工具调用 id,并在需要时用它们来重排工具结果。
- OpenAI:
- 在工具调用方面,各 API 之间角色 vs. 内容类型的差异:
-
CreateMessageResult
- 说明:
- 向后兼容问题:将 CreateMessageResult.content 作为内容数组或单个内容返回是有问题的,因此我们提议:
- 在 2025 年 11 月规范版本之前,
sampling/createMessage**不得(MUST NOT)**在CreateMessageResult.content中返回数组。- 这保证了线路层的向后兼容
- 使用采样的既有代码可能随新 SDK 发布而破坏,因为它需要测试 content 以知道它是数组还是单个块,并相应处理。
- 这似乎合理(?)
- 在 2025 年 11 月规范版本之前,
CreateMessageResult.stopReason字段当前定义为开放string,规范只提到endTurn作为示例值。- OpenAI vs. Anthropic API 惯用法
- 完成/停止原因
- OpenAI 的 ChatCompletion:
finish_reason: "stop" | "length" | "tool_use"(……?) - Anthropic:
stop_reason: "end_turn" | "max_tokens" | "stop_sequence" | "tool_use" | "pause_turn" | "refusal"
- OpenAI 的 ChatCompletion:
- 完成/停止原因
- 向后兼容问题:将 CreateMessageResult.content 作为内容数组或单个内容返回是有问题的,因此我们提议:
- 说明:
可能的后续
以下超出本 SEP 范围,但我们注意不排除它们,因此在适当之处,我们给出它们如何能在本 SEP 之上/之后实现的示例。流式支持
见:流式传输工具使用结果 #117 这对某些较长时间运行的用例,或当延迟重要时可能很重要,但与 MCP 工具中的流式支持配合会更好。 一种可能的实现方式是使用带载荷的通知,并可能创建一个新方法sampling/createMessageStreamed。两者都应与本 SEP 正交(但我们需要为结果创建 delta 类型,类似于 Claude API 和 OpenAI API 等推理 API 中的流式 API)。
缓存友好性更新
此处需要两点:- 引入缓存感知
- 以 SHOULD 措辞的隐式缓存指南
- 如 Claude API 中的显式缓存点和 TTL 语义?(包括更长缓存的 beta 行为)
- 优点:*对至少 1 个实现者(Anthropic)*易于实现
- 缺点:如果对其他人难以实现,则不太可能获批。
- 如 OpenAI API 中带显式键的”整个提示”/提示前缀缓存?
- 优点:
- 对用户更简单(无需考虑共享前缀在哪里停止)
- 隐式支持更新缓存(甚至可能作为子树)
- 缺点:可能更难实现/存储效率更低
- 优点:
- 引入 allowed_tools 特性,以在不破坏上下文缓存的情况下启用/禁用工具
-
与本 SEP 相关,因为我们可能想将此特性合并到 tool_choice 字段下,类似于 OpenAI 所做的。
-
与本 SEP 相关,因为我们可能想将此特性合并到 tool_choice 字段下,类似于 OpenAI 所做的。
允许客户端在代理式循环中自行调用服务器的工具
从服务器的角度看,那将消除自行调用工具/在后续采样调用中注入工具结果的需要。 MCP 服务器只需在采样请求中将其自身工具列入允许列表,用一个专门的工具定义,例如:- 安全,仅限于该服务器的工具。
- 如果我们传播 mcp-session-id,可以利用并保持任何服务器端会话上下文/缓存
允许客户端在代理式循环中自行调用任何其他 MCP 服务器的工具
尽管这听起来与前一个(只允许同一服务器的工具)相似,但此选项无需协议变更/可完全由客户端作为其采样支持的实现细节完成。 终端用户将来自任何其他 MCP 服务器的工具列入允许列表以用于采样请求,而无需服务器请求任何东西。客户端 UI 例如可以将工具选择 UI 作为采样批准流的一部分显示,默认自动启用来自同一服务器的工具。 优点:- 技术上无需规范变更(如果有的话,将此作为客户端拥有的一种自由提及)
- 可能类似于 CreateMessageRequest.params.includeContext = thisServer / allServers 预期语义可能意味着的东西
CreateMessageRequest.params.allowImplicitToolCalls = "none" | "thisServer" | "allServers"(假设我们想给服务器对此的任何控制权)
- 可能需要分类器以避免隐私泄露/滥用的高潜在风险
- 如果用户误批准 Gmail MCP 工具的使用/委托,服务器就能通过采样访问其私密邮件
允许服务器列出并调用客户端的工具(客户端/服务器 → p2p)
如果我们说客户端现在可以暴露服务器能调用的工具,就打开了一系列可能性:- 客户端可以”转发”其他服务器的工具(也许带一些命名空间以实现无缝聚合)
- 服务器随后可以将这些工具作为其工具循环的一部分调用。
- 客户端和服务器语义开始失去分量,我们进入一种更点对点、对称的关系
- 客户端也可以顺便请求服务器进行采样
- 协议层的对称性,但传输层仍有方向性(例如对 HTTP 传输,POST 请求的方向仍然重要)
简化结构化输出用例
采样的一个主要用例是获取符合给定 schema 的输出。 这在例如 OpenAI 的 API 中是可能的。 最常见的变通办法是给出单个工具并设置tool_choice: "required",这保证输出是一个 ToolCall,其中包含符合该工具输入 schema 的输入。
虽然本 SEP 提议我们启用这种基于 "required" 的变通办法,但作为后续,最好能提供更显式/更简单的 JSON schema 支持,这也将允许工具输入中不被允许的 schema 类型(工具输入要求一个带属性的对象,因此必须为其输出至少选一个名称,这需要思考/与提示策略相互作用):