《MCP 协议设计与实现》完整目录
- 前言
- 第1章 为什么需要 MCP
- 第02章 架构总览:Host-Client-Server 模型
- 第03章 JSON-RPC 与消息格式
- 第04章 生命周期与能力协商
- 第05章 Tool:让 Agent 调用世界
- 第6章 Resource:结构化的上下文注入
- 第7章 Prompt:可复用的交互模板
- 第8章 TypeScript Server 实现剖析
- 第09章 TypeScript Client 实现剖析
- 第10章 Python Server 实现剖析
- 第11章 Python Client 实现剖析
- 第12章 STDIO 传输:本地进程通信
- 第13章 Streamable HTTP:远程流式传输
- 第14章 SSE 与 WebSocket
- 第15章 OAuth 2.1 认证框架
- 第16章 服务发现与客户端注册
- 第17章 sampling(当前)
- 第18章 Elicitation、Roots 与配置管理
- 第19章 Claude Code 的 MCP 客户端:12 万行的实战
- 第20章 从零构建一个生产级 MCP Server
- 第21章 设计模式与架构决策
第17章 Sampling:服务端发起的 LLM 调用
17.1 什么是 Sampling
在前面的章节中,我们看到的都是 Client 向 Server 发起请求的模式------调用工具、读取资源、获取提示词模板。这些都是经典的"Client 主动,Server 被动"的交互方式。
Sampling 彻底反转了这个方向。
Sampling 是 MCP 协议中唯一允许 Server 主动向 Client 发起请求的核心功能之一。 具体来说,Server 可以通过 sampling/createMessage 请求,要求 Client 使用其连接的 LLM 进行一次文本生成(completion)。Client 收到请求后,将其转发给 LLM,拿到生成结果,再返回给 Server。
这个看似简单的"反向调用"设计,解决了一个困扰 Agent 生态已久的核心问题:Server 如何在不持有 LLM API Key 的情况下使用 AI 能力?
传统方式下,如果一个 MCP Server 的逻辑中需要调用 LLM(比如对用户提交的代码进行审查、生成摘要、做分类判断),它必须自己持有一个 LLM API Key,自己管理调用、计费、限流。这带来了几个严重问题:
- 密钥管理负担:每个 Server 都要安全地存储和轮转 API Key
- 计费碎片化:用户可能同时使用多个 Server,每个 Server 各自计费,费用不透明
- 模型选择权丧失:Server 绑定了特定的 LLM 提供商,用户无法选择自己偏好的模型
- 安全风险放大:API Key 散布在各种第三方 Server 中,攻击面急剧扩大
Sampling 的设计哲学是:LLM 的调用权归 Client 所有,Server 只需要描述"我需要什么样的 AI 输出",具体用哪个模型、怎么调用、花多少钱,全部由 Client 决定。
17.2 能力声明
Sampling 不是默认启用的。Client 必须在初始化阶段通过能力声明告知 Server 自己支持 Sampling。
最基本的声明方式:
json
{
"capabilities": {
"sampling": {}
}
}
如果 Client 还支持在 Sampling 过程中使用工具(这是更高级的能力),需要额外声明:
json
{
"capabilities": {
"sampling": {
"tools": {}
}
}
}
只有当 Client 声明了 sampling 能力后,Server 才被允许发送 sampling/createMessage 请求。这是 MCP 协议一贯的能力协商原则------不要假设对方支持什么,先问再用。
值得注意的是,规范中还有一个 context 子能力(用于 includeContext 参数),但它已被标记为"软弃用"(soft-deprecated),新的实现不建议使用。
17.3 CreateMessage 请求与响应
17.3.1 请求结构
Server 通过发送 sampling/createMessage 来请求 LLM 生成。请求的核心字段包括:
json
{
"jsonrpc": "2.0",
"id": 1,
"method": "sampling/createMessage",
"params": {
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "请分析这段代码的时间复杂度"
}
}
],
"systemPrompt": "你是一位资深的算法工程师。",
"maxTokens": 500,
"modelPreferences": {
"hints": [{ "name": "claude-3-sonnet" }],
"intelligencePriority": 0.8,
"speedPriority": 0.5
}
}
}
几个关键设计决策值得深入理解:
messages 数组:Server 构造一个完整的对话上下文传给 Client。消息内容支持三种类型------文本(text)、图片(image,Base64 编码)和音频(audio,Base64 编码)。这意味着 Sampling 不仅支持纯文本场景,也天然支持多模态交互。
systemPrompt:Server 可以指定系统提示词。但 Client 有权修改它------这是人机审批机制的一部分,后文会详细讨论。
maxTokens:生成的最大 token 数。这是一个硬约束,防止 Server 请求过大的生成量导致不可控的费用。
modelPreferences:这是最精妙的设计之一,我们在 17.5 节专门分析。
17.3.2 响应结构
Client 处理完请求后返回:
json
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"role": "assistant",
"content": {
"type": "text",
"text": "这段代码使用了嵌套循环,时间复杂度为 O(n^2)..."
},
"model": "claude-3-sonnet-20240307",
"stopReason": "endTurn"
}
}
model 字段告诉 Server 实际使用的是哪个模型------Server 只是"建议"了一个模型,Client 可能用了完全不同的模型。stopReason 表明生成停止的原因,常见值包括 endTurn(正常结束)和 toolUse(需要调用工具)。
17.4 Sampling 中的工具调用
Sampling 的基础用法是"Server 请求一次 LLM 生成,拿到文本结果"。但 MCP 规范进一步支持了一个更强大的模式:在 Sampling 过程中使用工具。
这意味着 Server 可以定义一组工具,让 Client 侧的 LLM 在生成过程中调用这些工具,形成一个完整的 Agent 循环------全部发生在一次 Sampling 会话中。
(messages + tools) Client->>User: 展示请求,等待审批 User-->>Client: 批准 Client->>LLM: 转发请求(附带工具定义) LLM-->>Client: 返回 tool_use 响应
(stopReason: "toolUse") Client->>User: 展示工具调用,等待审批 User-->>Client: 批准工具调用 Client-->>Server: 返回 tool_use 结果 Note over Server: 执行工具(如查询天气 API) Server->>Server: 运行 get_weather("北京") Note over Server,Client: 第二轮:携带工具结果继续对话 Server->>Client: sampling/createMessage
(完整历史 + tool_result + tools) Client->>User: 展示继续请求 User-->>Client: 批准 Client->>LLM: 转发(含工具结果) LLM-->>Client: 最终文本响应
(stopReason: "endTurn") Client->>User: 展示最终响应 User-->>Client: 批准 Client-->>Server: 返回最终结果
整个流程的关键在于:工具的定义由 Server 提供,但工具的执行也由 Server 完成。Client 和 LLM 只负责"决定调用哪个工具、传什么参数",真正的执行权还在 Server 手中。
工具选择模式
Server 可以通过 toolChoice 字段控制 LLM 使用工具的行为:
| 模式 | 含义 |
|---|---|
{mode: "auto"} |
LLM 自主决定是否使用工具(默认) |
{mode: "required"} |
LLM 必须至少调用一个工具 |
{mode: "none"} |
禁止 LLM 使用任何工具 |
一个常见的实践是:Server 在多轮工具循环中设置最大迭代次数,当到达最后一轮时,传入 {mode: "none"} 强制 LLM 输出最终的文本结果,避免无限循环。
消息内容约束
工具调用引入了严格的消息格式约束,这些约束是为了兼容不同 LLM 提供商的 API 设计(如 OpenAI 的 tool 角色、Gemini 的 function 角色):
- tool_result 消息不能混合其他内容 :当一条
user消息包含tool_result类型的内容时,该消息只能包含tool_result,不能混入text或image - tool_use 和 tool_result 必须成对出现 :每个
assistant消息中的tool_use(带有id)都必须在紧接其后的user消息中有对应的tool_result(带有匹配的toolUseId) - 支持并行工具调用 :LLM 可以在一条
assistant消息中返回多个tool_use,Server 需要执行全部工具并一次性返回所有结果
17.5 模型偏好系统
Server 和 Client 可能使用完全不同的 LLM 提供商。一个 Server 不能简单地说"请用 claude-3-sonnet",因为 Client 可能根本没有接入 Anthropic 的 API。
MCP 用一个两层抽象解决了这个问题:hints(提示)+ priorities(优先级)。
Hints:模型名称的模糊匹配
hints 是一个有序的模型名称建议列表,每个名称被当作子字符串匹配:
json
{
"hints": [
{ "name": "claude-3-sonnet" },
{ "name": "claude" }
]
}
Client 按顺序尝试匹配:先看有没有包含 "claude-3-sonnet" 的模型,没有就退而求其次找包含 "claude" 的。如果都没有,Client 可以根据 priorities 选择能力最接近的替代模型。比如,一个只接入了 Google 的 Client 可能会把 "claude-3-sonnet" 映射到 gemini-1.5-pro。
Hints 只是建议,不是命令。 最终的模型选择权始终在 Client 手中。
Priorities:能力维度的量化表达
三个归一化的优先级值(0 到 1),让 Server 精确表达自己的需求侧重:
| 优先级 | 含义 | 高值倾向 |
|---|---|---|
costPriority |
成本敏感度 | 更便宜的模型 |
speedPriority |
延迟敏感度 | 更快的模型 |
intelligencePriority |
能力需求 | 更强的模型 |
一个代码审查 Server 可能设置 intelligencePriority: 0.9, speedPriority: 0.3------它需要高质量的分析,不在乎多等几秒。而一个实时聊天机器人的 Server 可能设置 speedPriority: 0.9, intelligencePriority: 0.5------响应速度比深度分析更重要。
这个设计的精妙之处在于:它把"用哪个模型"的决策完全解耦了。Server 描述需求,Client 根据自己实际可用的模型做最优匹配。未来出现新的模型、新的提供商,协议本身不需要任何修改。
17.6 人机审批:安全的核心防线
Sampling 赋予了 Server 通过 Client 调用 LLM 的能力。但这也意味着:一个恶意的 Server 可以构造精心设计的 prompt,通过用户的 Client 生成有害内容,或者通过工具调用执行危险操作。
MCP 规范对此的回答是:人机审批(Human-in-the-Loop)是必须的。
允许用户查看和编辑 User-->>Client: 批准 / 修改 / 拒绝 end Client->>LLM: 转发(可能已被用户修改) LLM-->>Client: 返回生成结果 rect rgb(230, 255, 230) Note over Client,User: 审批点 2:审查响应 Client->>User: 展示 LLM 生成的内容
允许用户审查和编辑 User-->>Client: 批准 / 修改 / 拒绝 end Client-->>Server: 返回(可能已被用户修改的)结果
规范要求 Client 应当(SHOULD)提供以下能力:
- 请求审查:用户可以看到 Server 构造的完整 prompt,包括系统提示词和对话历史,并有权编辑或拒绝
- 响应审查:LLM 生成的内容在发回 Server 之前,用户可以查看和修改
- 工具调用审查:当 LLM 决定调用工具时,用户可以审批每一次工具调用
如果用户拒绝了 Sampling 请求,Client 应返回错误码 -1:
json
{
"jsonrpc": "2.0",
"id": 3,
"error": {
"code": -1,
"message": "User rejected sampling request"
}
}
需要注意的是,规范使用的是 SHOULD 而非 MUST------这是一个务实的选择。在某些自动化场景中(比如 CI/CD 流水线中的 Agent),要求每次 Sampling 都弹出审批窗口并不现实。但规范明确建议:在面向终端用户的应用中,人机审批应该是默认行为。
17.7 安全考量
Sampling 的安全模型建立在几个层面上:
第一层:能力协商。Server 只有在 Client 明确声明支持 Sampling 后才能发起请求。Client 可以选择不支持 Sampling,从而完全规避相关风险。
第二层:人机审批。如前所述,用户对每次 Sampling 的 prompt 和结果都有审查权。
第三层:Client 的完全控制权。Client 可以:
- 修改 Server 发来的 systemPrompt
- 选择与 Server 建议不同的模型
- 限制 maxTokens 的上限
- 实施请求速率限制(rate limiting)
- 对消息内容进行安全审查和过滤
第四层:工具调用的迭代限制。当 Sampling 中涉及工具调用时,Server 和 Client 都应实现循环次数上限,防止 LLM 陷入无限的工具调用循环。
第五层:内容验证。双方都应验证消息内容的合法性,包括:
- tool_result 消息是否只包含 tool_result 类型
- tool_use 和 tool_result 是否正确配对
- 敏感数据是否被妥善处理
这些错误场景对应明确的错误码:
| 错误码 | 含义 |
|---|---|
-1 |
用户拒绝了 Sampling 请求 |
-32602 |
参数无效(如缺少 tool_result、内容类型混合) |
17.8 Sampling 的革命性意义
理解 Sampling 的设计,需要把它放在更大的架构图景中看。
在没有 Sampling 的世界里,MCP Server 本质上是"被动的工具提供者"------Client 问什么它答什么,Client 不问它就沉默。这限制了 Server 的能力上限:它无法实现需要"思考"的复杂逻辑。
有了 Sampling,Server 变成了"有认知能力的智能体"。它可以:
- 自主推理:在执行复杂任务的过程中,调用 LLM 进行中间推理
- 动态决策:根据 LLM 的判断决定下一步操作
- 嵌套 Agent:在一个 MCP 工具调用的内部,启动一轮完整的 LLM 对话
- 多步规划:结合工具调用和 LLM 推理,实现复杂的多步骤任务
更关键的是,这一切都不需要 Server 自己持有任何 LLM 的 API Key。一个开源社区开发者可以发布一个功能强大的 MCP Server,用户只需要用自己已有的 AI 客户端连接它,所有的 LLM 调用都走用户自己的账号。开发者贡献能力,用户提供算力,协议负责桥接。
这是 MCP 协议设计中最具前瞻性的部分之一。它把 Agent 的"认知能力"从一个需要自建的基础设施,变成了一个可以通过协议按需获取的服务。
17.9 本章小结
Sampling 是 MCP 协议中最独特的设计之一。它反转了传统的请求方向,让 Server 可以通过 Client 间接调用 LLM,实现了以下关键能力:
- 零密钥架构:Server 无需持有 LLM API Key,消除了密钥管理和安全风险
- 模型选择权归用户:通过 hints 和 priorities 的两层抽象,Server 表达需求,Client 做最终决策
- 工具增强的 Agent 循环:Sampling 中支持工具定义和多轮调用,Server 可以实现完整的 Agent 行为
- 人机审批保障安全:用户对 prompt 和响应都有审查权,防止恶意 Server 滥用
- 跨提供商兼容:协议设计兼顾了 Claude、OpenAI、Gemini 等主流 LLM API 的差异
下一章我们将分析 Elicitation 和 Roots------MCP 协议中另外两个重要的交互机制。