Skip to main content
最终版标准轨道
字段
SEP1577
标题带工具的采样
状态最终版
类型标准轨道
创建日期2025-09-30
作者Olivier Chafik (@ochafik)
赞助方
PR#1577

摘要

本 SEP 引入了 toolstoolChoice 参数到 sampling/createMessage,并软弃用 includeContext(将 thisServerallServers 置于某项能力之下进行限制)。这使得 MCP 服务器能够使用客户端的 token 运行自己的代理循环(仍在用户监督之下),并降低了客户端实现的复杂性(上下文支持变为显式可选)。

动机

  • 采样 不支持工具调用,尽管它是现代代理行为的基石。如果没有明确的支持,使用采样的 MCP 服务器要么尝试通过复杂的提示/自定义解析输出来模拟工具调用,要么仅限于更简单的非代理请求。添加工具调用支持可以解锁 MCP 生态系统中的许多新颖用例。
  • 上下文包含定义模糊(参见 本文档):这使得完全实现采样变得特别棘手,连同采样所需的其他预防措施(不受本 SEP 影响),可能导致了 客户端中该功能的采用率较低(该功能是在 2024 年 11 月的 MCP 规范中引入的)。
请注意一些相关工作:
  • MCP 采样 (@jerome3o-anthropic):极其相似的提案:
    • 添加相同的工具语义,
    • 弃用 includeContext(文档解释了为何其语义模糊)
    • (进一步建议显式上下文共享,但这超出了本提案的范围)
  • 允许 Prompt/采样消息包含多个内容块。#198
    • 在此 PR 中,我们使 {CreateMessageResult,SamplingMessage}.content 接受单个内容或内容数组。result.content 的变更是向后不兼容的,但对于支持并行工具调用是必需的。SamplingMessage.content 的变更使得编写工具循环更加自然(参见参考实现中的示例:toolLoopSampling.ts
在下方的“后续可能的步骤”部分中,我们列举了一些未纳入本 SEP 范围但我们在设计本 SEP 时已确保合理兼容的功能示例。

规范

概述

  • CreateMessageRequest 中添加传统工具调用支持,包含 tools(带 JSON 架构)和 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 缺失时必须抛出错误
    • 当定义了 tooltoolChoiceclientCapabilities.sampling.tools 缺失时必须抛出错误
    • 服务器应避免 [includeContext](https://modelcontextprotocol.io/specification/2025-06-18/schema#createmessagerequest) != ‘none’,因为值 "thisServer""allServers" 可能会在未来的规范版本中被移除。
    • CreateMessageRequest.messages 必须平衡任何带有 ToolUseContent(及 id: $id1)的 “assistant” 消息与带有 ToolResultContent(及 tool_result_id: $id1)的 “user” 消息
      • 注意:这是 Claude API 实现的要求(并行工具调用必须一次性全部响应)
    • 带有工具结果内容块的 SamplingMessage 不得包含其他内容类型。

架构变更

  • ClientCapabilities
    interface ClientCapabilities {
      ...
      sampling?: {
        context?: object; // 新增:允许 CreateMessageRequest.params.includeContext != "none"
        tools?: object;   // 新增:允许 CreateMessageRequest.params.{tools,toolChoice}
      };
    }
    
  • CreateMessageRequest (使用现有 Tool)
    interface CreateMessageRequest {
     method: "sampling/createMessage";
     params: {
       ...
       messages: SamplingMessage[]; // 注意:类型已更新,见下文
       
       tools?: Tool[] // 新增(现有类型)
    
       toolChoice?: ToolChoice // 新增
     };
    }
    
    interface ToolChoice { // 新增
      mode?: "auto" | "required" | "none";
      // disable_parallel_tool_use?: boolean; // 更新(11 月 10 日):已移除,见下文
    }
    
    • 注意:
      • 避免并行工具调用的 OpenAI 与 Anthropic API 习惯用法:
        • OpenAI: parallel_tool_calls: false (顶层参数)
        • Anthropic: tool_choice.disable_parallel_tool_use: true
          • 此处首选,因为如果未设置,默认值为 false(即允许并行工具调用)
      • 关于 tool_choice "none"tools 的 OpenAI 与 Anthropic API 对比:
        • OpenAI: tools: [$Foo], tool_choice: "none" 禁止任何工具调用
          • 此处首选行为
        • Anthropic: tools: [$Foo], tool_choice: {mode: "none"} 仍可能调用工具 Foo
      • 关于 disable_parallel_tool_use 的 Gemini 与 OAI / Anthropic 对比:
        • Gemini API 目前无法禁用并行工具调用(不同于 OAI / Anthropic API)。暂时移除此标志,待 Gemini 支持后再引入。否则客户端会收到意外的多个工具调用(或者如果那样实现,会导致意外失败/代价高昂的重试直到发出单个工具调用)
        • Gemini API 的 函数调用模式 有一个 ANY 值,应匹配提议的 required
  • SamplingMessage:
    /*
      之前:
      
      interface SamplingMessage {
        content: TextContent | ImageContent | AudioContent
        role: Role;
      }
    */
    
    type SamplingMessage = UserMessage | AssistantMessage; // 新增
    
    type AssistantMessageContent =
      | TextContent
      | ImageContent
      | AudioContent
      | ToolUseContent;
    type UserMessageContent =
      | TextContent
      | ImageContent
      | AudioContent
      | ToolResultContent;
    interface AssistantMessage {
      // 新增
      role: "assistant";
      content: AssistantMessageContent | AssistantMessageContent[];
    }
    
    interface ToolUseContent {
      // 新增
      type: "tool_use";
      name: string;
      id: string;
      input: object;
    }
    
    interface UserMessage {
      // 新增
      role: "user";
      content: UserMessageContent | UserMessageContent[];
    }
    
    interface ToolResultContent {
      // 新增
      _meta?: { [key: string]: unknown };
      type: "tool_result";
      toolUseId: string;
      content: ContentBlock[];
      structuredContent: object;
      isError?: boolean;
    }
    
  • 注意:
    • 关于工具调用时 role 与 content 类型在不同 API 之间的差异:
      • OpenAI: role: "system" | "user" | "assistant" | "tool"(其中 tool 用于工具结果),而工具调用嵌套在 assistant 消息中,content 通常为 null,但一些”OpenAI 兼容”API 接受非 null 值
        • [
            { role: "user", content: "what is the temperature in london?" },
            {
              role: "assistant",
              content: "Let me use a tool...",
              tool_calls: [
                {
                  id: "call_1",
                  type: "function",
                  function: {
                    name: "get_weather",
                    arguments: '{"location": "London"}',
                  },
                },
              ],
            },
            {
              role: "tool",
              content: '{"temperature": 20, "condition": "sunny"}',
              tool_call_id: "call_1",
            },
          ];
          
      • Claude API: role: "user" | "assistant",工具使用和结果通过特殊类型的消息内容部分传递:
        • [
            {
              "role": "user",
              "content": [
                {
                  "type": "text",
                  "text": "what is the temperature in london?"
                }
              ],
            {
              "role": "assistant",
              "content": [
                {
                  "type": "text",
                  "text": "Let me use a tool..."
                },
                {
                  "type": "tool_use",
                  "id": "call_1",
                  "name": "get_weather",
                  "input": {"location": "London"}
                }
              ]
            },
            {
              "role": "user",
              "content": [
                {
                  "type": "tool_result",
                  "tool_call_id": "call_1",
                  "content": {"temperature": 20, "condition": "sunny"}
                }
              ]
            }
          ]
          
      • Gemini API:
        • function 角色(类似于 OAI 的 tool 角色)
        • 无工具调用 id 概念(函数调用:Gemini 要求工具结果的提供顺序与工具使用部分完全一致。实现可以生成工具调用 id 并在需要时使用它们重新排序工具结果。
  • CreateMessageResult
    /*
      之前:
    
      interface CreateMessageResult {
        _meta?: { [key: string]: unknown };
        content: TextContent | ImageContent | AudioContent;
        role: Role;
        stopReason?: string;
        [key: string]: unknown;
    }
    */
    interface CreateMessageResult {
      _meta?: { [key: string]: unknown };
    
      content: AssistantMessageContent | AssistantMessageContent[] // 已更新
    
      role: "assistant"; // 已更新
    
      stopReason?: "endTurn" | "stopSequence" | "toolUse" | "maxToken" | string // 已更新
    
      [key: string]: unknown;
    }
    
    • 注意:
      • 向后兼容性问题:将 CreateMessageResult.content 作为内容数组或单个内容返回是有问题的,因此我们建议:
        • 在规范版本 2025 年 11 月之前,sampling/createMessage 不得在 CreateMessageResult.content 中返回数组。
          • 这保证了传输级别的向后兼容性
        • 使用采样的现有代码可能会在新的 SDK 版本中中断,因为它需要测试 content 是数组还是单个块,并相应处理。
        • 这似乎是合理的 (?)
      • CreateMessageResult.stopReason 字段目前定义为开放 string,规范仅提及 endTurn 作为示例值。
      • OpenAI 与 Anthropic API 习惯用法
        • 完成/停止原因
          • OpenAI 的 ChatCompletion: finish_reason: "stop" | "length" | "tool_use" (…?)
          • Anthropic: stop_reason: "end_turn" | "max_tokens" | "stop_sequence" | "tool_use" | "pause_turn" | "refusal"

可能的后续步骤

这些不在本 SEP 的范围内,但已注意不排除它们,因此在适当的地方我们给出了如何在本 SEP 之上/之后实现它们的示例。

流式支持

参见:流式工具使用结果 #117 这对于某些运行时间较长的用例或当延迟很重要时可能很重要,但如果能与 MCP 工具中的流式支持配合会更好。 实现这一点的一种可能方法是使用带负载的通知,并可能创建一个新方法 sampling/createMessageStreamed。这两者都应与本 SEP 正交(但我们需要为结果创建增量类型,类似于推理 API 中的流式 API,如 Claude API 和 OpenAI API)。

缓存友好性更新

这里需要两点:
  • 引入缓存感知
    • 隐含的缓存指南,表述为 SHOULD(应该)
    • 显式缓存点和 TTL 语义 如 Claude API 中所示?(包括更长缓存的测试行为)
      • 优点:易于实现 对于至少 1 个实现者(Anthropic)
      • 缺点:如果对其他实现者来说难以实现,则不太可能获得批准。
    • “整个提示词”/ 带有显式键的提示词前缀缓存 如 OpenAI API 中所示?
      • 优点:
        • 对用户更简单(无需考虑共享前缀在哪里停止)
        • 隐式支持更新缓存(甚至可能作为子树)
      • 缺点:可能更难实现 / 存储效率更低
  • 引入 allowed_tools 功能以启用/禁用工具而不破坏上下文缓存

允许客户端在代理循环中自行调用服务器的工具

从服务器的角度来看,这将消除自行调用工具/在后续采样调用中注入工具结果的需要。 MCP 服务器只需在采样请求中允许列出其自己的工具,使用专用的工具定义,例如:
{
  type: "server-tool"; // 来自同一服务器的 MCP 工具。
  name: string;
}
优点:
  • 安全,仅限于该服务器的工具。
  • 如果我们传播 mcp-session-id,可以利用保持任何服务器端会话上下文/缓存

允许客户端在代理循环中自行调用任何其他 MCP 服务器的工具

虽然这听起来与上一个类似(仅允许同一服务器的工具),但此选项不需要协议更改/可以由客户端完全作为其采样支持的实现细节来完成。 最终用户将允许列出来自任何其他 MCP 服务器的工具以供采样请求使用,而无需服务器请求任何内容。客户端 UI 例如可以在采样批准流程中显示工具选择 UI,默认自动启用来自同一服务器的工具。 优点:
  • 技术上不需要规范更改(如果有的话,提及这是客户端拥有的自由)
  • 可能类似于 CreateMessageRequest.params.includeContext = thisServer / allServers 预期的语义可能意味着
    • CreateMessageRequest.params.allowImplicitToolCalls = "none" | "thisServer" | "allServers" (假设我们想给服务器任何控制权)
缺点:
  • 可能需要分类器以避免高潜在的隐私泄露/滥用
    • 如果用户错误地批准了 Gmail MCP 工具使用/委托,服务器可以通过采样访问他们的私人电子邮件

允许服务器列出和调用客户端的工具(客户端/服务器 → p2p)

如果我们说客户端现在可以暴露服务器可以调用的工具,它开启了一系列可能性:
  • 客户端可以“转发”其他服务器的工具(可能带有一些命名空间以实现无缝聚合)
    • 然后服务器可以在其工具循环中调用这些工具。
  • 客户端和服务器语义开始失去权重,我们进入更点对点、对称的关系
    • 客户端也可以在此过程中向服务器请求采样
    • 协议层的对称性,但传输层仍有方向性(例如对于 HTTP 传输,POST 请求的方向仍然很重要)

简化结构化输出用例

采样的一个主要用例是获得符合给定模式的输出。 例如,这在 OpenAI 的 API 中是可能的。 最常见的变通方法是提供单个工具并设置 tool_choice: "required",这保证输出是一个包含符合工具输入模式的输入的 ToolCall。 虽然本 SEP 提议我们启用这种基于 "required" 的变通方法,但作为后续步骤,最好提供更明确/更简单的 JSON 模式支持,这也允许工具输入中不允许的模式类型(这需要带有属性的对象,因此必须至少为输出选择一个名称,这需要思考/与提示策略的互动):
interface CreateMessageRequest {
  method: "sampling/createMessage";
  params: {
    messages: SamplingMessage[];
    ...
    format: {
      type: "json_schema",
      "schema": {
        "type": "array",
        "minItems": 5,
        "maxItems": 100
      }
    }
  }