MCP 协议设计与实现
第17章 Sampling:服务端发起的 LLM 调用
第17章 Sampling:服务端发起的 LLM 调用
The most elegant protocol inverts the most expected direction.
本章要点
- Sampling 反转了 MCP 的请求方向——Server 通过 Client 调用 LLM
- 零 API Key 架构——Server 贡献逻辑,用户提供算力
- 两层模型抽象:hints(字符串模糊匹配)+ priorities(三维能力量化)
- Agent 循环内嵌——Sampling 支持工具调用,Server 可以实现完整 Agent 行为
- 人机审批三点:请求审查 / 工具审查 / 响应审查
- 五层安全防线:能力协商 / 人工审批 / Client 控制 / 迭代限制 / 内容验证
- 革命性意义:把 Agent 认知能力从基础设施变成按需获取的服务
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 能力?
graph LR
subgraph 传统方式
S1[MCP Server] -->|需要 API Key| LLM1[LLM API]
end
subgraph Sampling 方式
S2[MCP Server] -->|sampling/createMessage| C[MCP Client]
C -->|已有 API Key| LLM2[LLM API]
end
style S1 fill:#fee2e2,stroke:#ef4444
style S2 fill:#dcfce7,stroke:#22c55e
style C fill:#dbeafe,stroke:#3b82f6
传统方式的四大痛点
传统方式下,如果一个 MCP Server 的逻辑中需要调用 LLM(比如对用户提交的代码进行审查、生成摘要、做分类判断),它必须自己持有一个 LLM API Key,自己管理调用、计费、限流。这带来了几个严重问题:
- 密钥管理负担:每个 Server 都要安全地存储和轮转 API Key——小团队做不了,大厂也会出事
- 计费碎片化:用户可能同时使用多个 Server,每个 Server 各自计费,费用不透明
- 模型选择权丧失:Server 绑定了特定的 LLM 提供商,用户无法选择自己偏好的模型
- 安全风险放大:API Key 散布在各种第三方 Server 中,攻击面急剧扩大
Sampling 的设计哲学
LLM 的调用权归 Client 所有,Server 只需要描述”我需要什么样的 AI 输出”,具体用哪个模型、怎么调用、花多少钱,全部由 Client 决定。
这个哲学带来三个根本性改变:
- 开发者门槛降低:不需要有 LLM 账号就能写 MCP Server
- 用户体验统一:所有 Sampling 都在同一个 Client 里,用户感知统一的 AI 账单
- 模型选择自由:用户自主决定用哪家 LLM,Server 无法锁定
17.2 能力声明
Sampling 不是默认启用的。Client 必须在初始化阶段通过能力声明告知 Server 自己支持 Sampling。
最基本的声明方式:
{
"capabilities": {
"sampling": {}
}
}
如果 Client 还支持在 Sampling 过程中使用工具(这是更高级的能力),需要额外声明:
{
"capabilities": {
"sampling": {
"tools": {}
}
}
}
只有当 Client 声明了 sampling 能力后,Server 才被允许发送 sampling/createMessage 请求。这是 MCP 协议一贯的能力协商原则——不要假设对方支持什么,先问再用。
能力协商的工程价值
能力协商看似繁琐,但它带来了协议演进的自由度:
- 新增能力不破坏老 Client:声明了才用,没声明就不用
- 老 Server 不需要理解新 Client 的声明:能忽略就行
- 渐进升级路径清晰:先发布支持能力,再真正使用
这是一个在 HTTP、IMAP、XMPP 等成熟协议中反复验证过的设计范式。
值得注意的是,规范中还有一个 context 子能力(用于 includeContext 参数),但它已被标记为”软弃用”(soft-deprecated),新的实现不建议使用。
17.3 CreateMessage 请求与响应
17.3.1 请求结构
Server 通过发送 sampling/createMessage 来请求 LLM 生成。请求的核心字段包括:
{
"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
},
"temperature": 0.7,
"stopSequences": ["\n\n---"],
"metadata": {
"purpose": "code-analysis"
}
}
}
六个关键字段的设计
1. messages 数组
Server 构造一个完整的对话上下文传给 Client。消息内容支持三种类型:
- 文本(text)
- 图片(image,Base64 编码)
- 音频(audio,Base64 编码)
这意味着 Sampling 不仅支持纯文本场景,也天然支持多模态交互。
2. systemPrompt
Server 可以指定系统提示词。但 Client 有权修改它——这是人机审批机制的一部分。
3. maxTokens
生成的最大 token 数。这是一个硬约束,防止 Server 请求过大的生成量导致不可控的费用。Client 还可以在用户设置中进一步压低这个上限。
4. modelPreferences
这是最精妙的设计之一,在 17.5 节详细分析。
5. temperature 和 stopSequences
标准的生成控制参数,让 Server 可以调优输出特性。
6. metadata
自定义元数据字段,Server 可以携带额外信息供 Client 记录或路由——比如用途分类(code-review / summary / classification)。
17.3.2 响应结构
Client 处理完请求后返回:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"role": "assistant",
"content": {
"type": "text",
"text": "这段代码使用了嵌套循环,时间复杂度为 O(n²)..."
},
"model": "claude-3-sonnet-20240307",
"stopReason": "endTurn",
"usage": {
"inputTokens": 42,
"outputTokens": 128
}
}
}
关键字段:
model:告诉 Server 实际使用的是哪个模型——Server 只是”建议”了一个模型,Client 可能用了完全不同的模型stopReason:生成停止的原因,常见值:endTurn——正常结束toolUse——需要调用工具maxTokens——达到 maxTokens 限制stopSequence——匹配到 stopSequences
usage:token 使用统计,供 Server 做记账和监控
17.4 Sampling 中的工具调用
Sampling 的基础用法是”Server 请求一次 LLM 生成,拿到文本结果”。但 MCP 规范进一步支持了一个更强大的模式:在 Sampling 过程中使用工具。
这意味着 Server 可以定义一组工具,让 Client 侧的 LLM 在生成过程中调用这些工具,形成一个完整的 Agent 循环——全部发生在一次 Sampling 会话中。
完整流程
sequenceDiagram
participant Server as MCP Server
participant Client as MCP Client
participant User as 用户
participant LLM as LLM
Note over Server,Client: 第一轮:发起带工具的 Sampling 请求
Server->>Client: sampling/createMessage<br/>(messages + tools)
Client->>User: 展示请求,等待审批
User-->>Client: 批准
Client->>LLM: 转发请求(附带工具定义)
LLM-->>Client: 返回 tool_use 响应<br/>(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<br/>(完整历史 + tool_result + tools)
Client->>User: 展示继续请求
User-->>Client: 批准
Client->>LLM: 转发(含工具结果)
LLM-->>Client: 最终文本响应<br/>(stopReason: "endTurn")
Client->>User: 展示最终响应
User-->>Client: 批准
Client-->>Server: 返回最终结果
整个流程的关键在于:工具的定义由 Server 提供,但工具的执行也由 Server 完成。Client 和 LLM 只负责”决定调用哪个工具、传什么参数”,真正的执行权还在 Server 手中。
控制反转的优美
这是一个精妙的控制反转:
- Server 把”认知决策权”交给 LLM(通过 Client)
- Client 把”执行权”交回给 Server
- 用户在两端都有审批点
这种分工让每一方都做自己擅长的事,同时用户始终保持控制权。
工具选择模式
Server 可以通过 toolChoice 字段控制 LLM 使用工具的行为:
| 模式 | 含义 | 典型场景 |
|---|---|---|
{mode: "auto"} | LLM 自主决定是否使用工具(默认) | 开放式任务 |
{mode: "required"} | LLM 必须至少调用一个工具 | 强制路由到工具 |
{mode: "none"} | 禁止 LLM 使用任何工具 | 最终轮强制输出文本 |
{mode: "specific", tool: "get_weather"} | 指定调用某个工具 | 流程固定 |
一个常见的实践是: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 需要执行全部工具并一次性返回所有结果
这些约束看起来很”硬”,但正是这种严格性让协议能被多种 LLM 后端支持。
17.5 模型偏好系统
Server 和 Client 可能使用完全不同的 LLM 提供商。一个 Server 不能简单地说”请用 claude-3-sonnet”,因为 Client 可能根本没有接入 Anthropic 的 API。
MCP 用一个两层抽象解决了这个问题:hints(提示)+ priorities(优先级)。
17.5.1 Hints:模型名称的模糊匹配
hints 是一个有序的模型名称建议列表,每个名称被当作子字符串匹配:
{
"hints": [
{ "name": "claude-3-sonnet" },
{ "name": "claude" },
{ "name": "gpt-4" }
]
}
Client 按顺序尝试匹配:
- 先看有没有包含 “claude-3-sonnet” 的模型
- 没有就退而求其次找包含 “claude” 的
- 还没有就找 “gpt-4”
- 如果都没有,Client 根据 priorities 选择能力最接近的替代模型
比如,一个只接入了 Google 的 Client 可能会把 “claude-3-sonnet” 映射到 gemini-1.5-pro(相似能力级别的模型)。
Hints 只是建议,不是命令。 最终的模型选择权始终在 Client 手中。
17.5.2 Priorities:能力维度的量化表达
三个归一化的优先级值(0 到 1),让 Server 精确表达自己的需求侧重:
| 优先级 | 含义 | 高值倾向 |
|---|---|---|
costPriority | 成本敏感度 | 更便宜的模型 |
speedPriority | 延迟敏感度 | 更快的模型 |
intelligencePriority | 能力需求 | 更强的模型 |
17.5.3 场景示例
场景 1:代码审查 Server
{
"intelligencePriority": 0.9,
"speedPriority": 0.3,
"costPriority": 0.2
}
需要高质量的分析,不在乎多等几秒。
场景 2:实时聊天机器人
{
"speedPriority": 0.9,
"intelligencePriority": 0.5,
"costPriority": 0.4
}
响应速度比深度分析更重要。
场景 3:批量内容分类
{
"costPriority": 0.9,
"speedPriority": 0.5,
"intelligencePriority": 0.3
}
要处理海量数据,省钱第一。
17.5.4 设计的前瞻性
这个设计的精妙之处在于:它把”用哪个模型”的决策完全解耦了。
- Server 描述需求
- Client 根据自己实际可用的模型做最优匹配
- 未来出现新的模型、新的提供商,协议本身不需要任何修改
这是一个面向不确定未来的 API 设计——不假设模型名称,只描述能力维度。
17.6 人机审批:安全的核心防线
Sampling 赋予了 Server 通过 Client 调用 LLM 的能力。但这也意味着:一个恶意的 Server 可以构造精心设计的 prompt,通过用户的 Client 生成有害内容,或者通过工具调用执行危险操作。
MCP 规范对此的回答是:人机审批(Human-in-the-Loop)是必须的。
三个审批点
sequenceDiagram
participant Server as MCP Server
participant Client as MCP Client
participant User as 用户
participant LLM as LLM
Server->>Client: sampling/createMessage
rect rgb(255, 240, 230)
Note over Client,User: 审批点 1:审查请求
Client->>User: 展示 Server 的 prompt<br/>允许用户查看和编辑
User-->>Client: 批准 / 修改 / 拒绝
end
Client->>LLM: 转发(可能已被用户修改)
LLM-->>Client: 返回生成结果
rect rgb(240, 240, 255)
Note over Client,User: 审批点 2:工具调用(如果有)
Client->>User: 展示 LLM 要调用的工具<br/>允许用户审批
User-->>Client: 批准 / 拒绝
end
rect rgb(230, 255, 230)
Note over Client,User: 审批点 3:审查响应
Client->>User: 展示 LLM 生成的内容<br/>允许用户审查和编辑
User-->>Client: 批准 / 修改 / 拒绝
end
Client-->>Server: 返回(可能已被用户修改的)结果
规范要求的能力
规范要求 Client 应当(SHOULD)提供以下能力:
- 请求审查:用户可以看到 Server 构造的完整 prompt,包括系统提示词和对话历史,并有权编辑或拒绝
- 响应审查:LLM 生成的内容在发回 Server 之前,用户可以查看和修改
- 工具调用审查:当 LLM 决定调用工具时,用户可以审批每一次工具调用
如果用户拒绝了 Sampling 请求,Client 应返回错误码 -1:
{
"jsonrpc": "2.0",
"id": 3,
"error": {
"code": -1,
"message": "User rejected sampling request"
}
}
SHOULD 而非 MUST:务实的权衡
需要注意的是,规范使用的是 SHOULD 而非 MUST——这是一个务实的选择。
在某些自动化场景中(比如 CI/CD 流水线中的 Agent),要求每次 Sampling 都弹出审批窗口并不现实。但规范明确建议:在面向终端用户的应用中,人机审批应该是默认行为。
这种”强制最佳实践,容忍特殊场景”的表述方式,是协议规范设计的艺术。
审批 UI 的最佳实践
Client 的审批 UI 设计是用户体验的关键。一些最佳实践:
- 摘要+详情:默认展示 prompt 摘要,点击展开查看完整内容
- 敏感内容高亮:检测到可能的 PII、代码、URL 时高亮显示
- 批量审批模式:受信任的 Server 可以开启”本会话内自动批准”
- 一次拒绝全局生效:用户拒绝一次后,同类请求一段时间内自动拒绝
- 审批日志:记录所有审批历史,便于事后审计
17.7 安全考量:五层防御
Sampling 的安全模型建立在五个层面上:
graph TD
Request[Server 的 Sampling 请求]
Request --> L1{① 能力协商<br/>Client 是否声明支持?}
L1 -->|否| Block[❌ 拒绝]
L1 -->|是| L2{② 人机审批<br/>用户是否批准?}
L2 -->|否| Block
L2 -->|是| L3{③ Client 控制<br/>满足约束?}
L3 -->|否| Block
L3 -->|是| L4{④ 迭代限制<br/>未超循环上限?}
L4 -->|否| Block
L4 -->|是| L5{⑤ 内容验证<br/>消息格式合法?}
L5 -->|否| Block
L5 -->|是| Pass[✅ 执行]
style L1 fill:#dbeafe,stroke:#3b82f6
style L2 fill:#fef3c7,stroke:#f59e0b
style L3 fill:#dcfce7,stroke:#22c55e
style L4 fill:#f3e8ff,stroke:#a855f7
style L5 fill:#fecaca,stroke:#dc2626
第一层:能力协商
Server 只有在 Client 明确声明支持 Sampling 后才能发起请求。Client 可以选择不支持 Sampling,从而完全规避相关风险。
第二层:人机审批
如 17.6 节所述,用户对每次 Sampling 的 prompt 和结果都有审查权。
第三层:Client 的完全控制权
Client 可以:
- 修改 Server 发来的 systemPrompt
- 选择与 Server 建议不同的模型
- 限制 maxTokens 的上限
- 实施请求速率限制(rate limiting)
- 对消息内容进行安全审查和过滤
- 剔除敏感数据
- 注入自己的安全 prompt
第四层:工具调用的迭代限制
当 Sampling 中涉及工具调用时,Server 和 Client 都应实现循环次数上限,防止 LLM 陷入无限的工具调用循环。典型设置:
- 单次 Sampling 会话最多 10 轮工具调用
- 达到上限时强制
toolChoice: "none"让 LLM 输出最终答案
第五层:内容验证
双方都应验证消息内容的合法性,包括:
- tool_result 消息是否只包含 tool_result 类型
- tool_use 和 tool_result 是否正确配对
- 消息角色是否符合 user/assistant/system 的约束
- 敏感数据是否被妥善处理
错误码
这些错误场景对应明确的错误码:
| 错误码 | 含义 |
|---|---|
-1 | 用户拒绝了 Sampling 请求 |
-32602 | 参数无效(如缺少 tool_result、内容类型混合) |
-32603 | 内部错误 |
-32000 到 -32099 | Server 定义的业务错误 |
17.8 实战:用 Sampling 实现代码审查 Server
一个实际的 Sampling 用法示例——用 MCP Server 实现代码审查功能:
import { Server } from '@modelcontextprotocol/sdk/server';
const server = new Server({
name: 'code-reviewer',
version: '1.0.0',
});
server.setRequestHandler('tools/call', async (req) => {
if (req.params.name !== 'review_code') {
throw new Error('Unknown tool');
}
const { code, language } = req.params.arguments;
// 通过 Sampling 让 LLM 做代码审查
const result = await server.createMessage({
messages: [{
role: 'user',
content: {
type: 'text',
text: `Review the following ${language} code for bugs, style issues, and potential improvements.
Be specific and actionable.
\`\`\`${language}
${code}
\`\`\``
}
}],
systemPrompt: 'You are a senior software engineer performing code review. Focus on correctness and maintainability.',
maxTokens: 1000,
modelPreferences: {
hints: [{ name: 'claude-3-sonnet' }, { name: 'gpt-4' }],
intelligencePriority: 0.9,
speedPriority: 0.3,
},
temperature: 0.2, // 低 temperature 让审查更稳定
});
return {
content: [{
type: 'text',
text: result.content.text,
}],
_meta: {
modelUsed: result.model,
tokensUsed: result.usage,
}
};
});
这个 Server 完全不持有任何 LLM API Key。用户通过自己的 MCP Client(带着自己的 Claude/OpenAI 账号)连接它,所有 LLM 调用的费用都走用户自己的账号。
17.9 Sampling 的革命性意义
理解 Sampling 的设计,需要把它放在更大的架构图景中看。
前后对比
在没有 Sampling 的世界里,MCP Server 本质上是”被动的工具提供者”——Client 问什么它答什么,Client 不问它就沉默。这限制了 Server 的能力上限:它无法实现需要”思考”的复杂逻辑。
有了 Sampling 之后,Server 变成了”有认知能力的智能体”。它可以:
- 自主推理:在执行复杂任务的过程中,调用 LLM 进行中间推理
- 动态决策:根据 LLM 的判断决定下一步操作
- 嵌套 Agent:在一个 MCP 工具调用的内部,启动一轮完整的 LLM 对话
- 多步规划:结合工具调用和 LLM 推理,实现复杂的多步骤任务
开源生态的新范式
更关键的是,这一切都不需要 Server 自己持有任何 LLM 的 API Key。
【传统模式】:
开源 Server 开发者 → 自备 LLM API Key → 为用户代付费用 → 无法持续
【Sampling 模式】:
开源 Server 开发者 → 贡献逻辑 → 用户提供算力 → 双赢
一个开源社区开发者可以发布一个功能强大的 MCP Server,用户只需要用自己已有的 AI 客户端连接它,所有的 LLM 调用都走用户自己的账号。
开发者贡献能力,用户提供算力,协议负责桥接。
这是 MCP 协议设计中最具前瞻性的部分之一。它把 Agent 的”认知能力”从一个需要自建的基础设施,变成了一个可以通过协议按需获取的服务。
产业影响
从产业视角看,Sampling 可能会催生以下变化:
- MCP Server 开源数量爆炸——没有了 API Key 负担,开发者门槛降至接近为零
- AI 产品边界模糊化——Server 的 AI 能力不再绑定特定提供商
- 计费模式标准化——用户对所有 Sampling 统一计费,成本可见
- 模型即商品——Server 不再锁定某个模型提供商,竞争加剧
17.10 反模式清单
反模式一:滥用 Sampling
现象:本来能用确定性代码做的事,Server 也用 Sampling 让 LLM 做。
问题:浪费用户 API 额度、响应慢、不确定性。
对策:能用 if-else、正则、规则引擎做的,不要用 Sampling。
反模式二:Server 绕过审批
现象:Server 构造复杂的 prompt 试图让用户批准一个看起来无害但实际有害的请求。
对策:Client 应该做 prompt 安全扫描,高亮可疑内容。
反模式三:无限工具循环
现象:Server 不设置工具调用次数上限,LLM 陷入来回调用。
对策:强制设 maxIterations,最后一轮 toolChoice: none。
反模式四:忽视 stopReason
现象:Server 不检查 stopReason,即使 LLM 返回 maxTokens 也当作正常结果用。
对策:每次都检查 stopReason,maxTokens/stopSequence 要特别处理。
17.10.1 两个 SDK 的 Sampling 类型设计:Base vs WithTools 双变体的真实实现
§17.4 讨论 Sampling 工具调用——但没有说两个 SDK 在类型层面如何处理”有工具 vs 无工具”两种调用形态。实测两份源码——
TS SDK(packages/server/src/server/server.ts:475-548)——
// 三个签名构成 overload 链
async createMessage(params: CreateMessageRequestParamsBase, options?: RequestOptions): Promise<CreateMessageResult>;
async createMessage(params: CreateMessageRequestParamsWithTools, options?: RequestOptions): Promise<CreateMessageResultWithTools>;
async createMessage( // 真实实现
params: CreateMessageRequestParamsBase | CreateMessageRequestParamsWithTools,
options?: RequestOptions
): Promise<CreateMessageResult | CreateMessageResultWithTools> { ... }
类型定义(packages/core/src/types/types.ts:548-550)——
/** Excludes tools/toolChoice to indicate they should not be provided. */
export type CreateMessageRequestParamsBase = Omit<CreateMessageRequestParams, 'tools' | 'toolChoice'>;
Base 通过 TypeScript 的 Omit 工具类型显式去掉 tools 和 toolChoice 两字段——而不是把它们写成 optional——这样编译器在调用方写 params.tools(Base 路径下)时直接报 type error。这是 TypeScript 类型工程的精致用法、和 ch08 §8.4.4 揭示的 LangChain @tool 4 个 @overload 同款套路。
Python SDK(mcp/types/_types.py:1372-1392)——
class CreateMessageResult(Result):
"""The client's response to a sampling/createMessage request from the server."""
...
class CreateMessageResultWithTools(Result):
"""The client's response to a sampling/createMessage request when tools were provided."""
...
Python 直接定义两个独立类——没有 Omit 这种类型操作、就开两个 dataclass——更直白;mypy 能识别 union return type。mcp/server/session.py:618 的注释原文 “Build a sampling/createMessage request without sending it” 暗示Python SDK 把请求构造和发送切开——TS SDK 的 createMessage 是合成的——这是另一个”Python 多小函数 vs TS 大方法”风格分野(§3.8.3 / §16.9.1 的延续)。
ModelPreferences 在两个 SDK 完全对齐——4 字段 hints / costPriority / speedPriority / intelligencePriority(TS schemas.ts:1526 + Python _types.py:1264)——三个 priority 都是 0..1 浮点——是 §17.5 那张”5 维偏好雷达图”在源码层的直接印证。
ResourceNotFound (-32002) 仅 TS 有 vs Sampling 错误码两边都有——和 §3.8.3 揭示的不对称结合看:MCP spec 的实现一致性很取决于具体功能模块——有些是双 SDK 严格对齐(Sampling、Streamable HTTP),有些是 TS 单边(ResourceNotFound),有些是 Python 单边(CONNECTION_CLOSED、服务端 OAuth)。
17.10.2 Python ClientSession 如何把 Sampling 变成能力
Sampling 的关键不是“服务端能调用模型”,而是客户端愿意代表用户处理这类反向请求。Python SDK 的 ClientSession 把这个边界写得很清楚:mcp-python-sdk/src/mcp/client/session.py:110-123 构造函数接受 sampling_callback、elicitation_callback、list_roots_callback 和可选 sampling capabilities;session.py:127-130 如果用户没有传回调,就使用默认回调;session.py:148-177 初始化时只有在 sampling callback 不是默认值时才声明 sampling 能力,elicitation 和 roots 也是同一逻辑。能力不是“SDK 支持某类型”,而是“当前会话真的有处理该类型请求的用户代码”。
schema 对 Sampling 请求本身也施加了几道约束。mcp-specification/schema/2025-11-25/schema.ts:1580-1608 要求 sampling/createMessage params 至少包含 messages 与 maxTokens,可以附带 modelPreferences、systemPrompt、includeContext、temperature、stopSequences 和 provider-specific metadata;schema.ts:1614-1623 允许请求携带 tools 与 toolChoice,但明确要求客户端只有声明了 ClientCapabilities.sampling.tools 才能接受这两个字段;schema.ts:1641-1648 进一步说明客户端对模型选择有完全裁量权,并应在开始 sampling 前让用户检查请求。
执行路径同样是“能力先行”。session.py:424-434 收到 CreateMessageRequest 后,如果 params 带 task 元数据,就走 experimental task handler;否则才调用 _sampling_callback,并用 ClientResponse adapter 校验返回值后 respond。也就是说,服务端不会直接拿到一个裸 LLM API;它拿到的是一个被客户端 policy、UI、人机审批、模型选择与类型校验包裹过的反向调用入口。客户端可以换模型、缩短 maxTokens、拒绝 tools、过滤 includeContext,仍然符合协议精神。
这给 Server 设计带来几个硬边界。第一,Sampling 不适合替代普通 tool call:如果服务端只是想查自己的数据库,应该自己实现工具,而不是请求客户端再让模型猜。第二,Sampling 请求要足够短且可审计:messages、systemPrompt、includeContext 都可能暴露上下文,必须让用户能看懂。第三,带 tools 的 Sampling 要格外谨慎,因为它等于让“被服务端请求的模型调用”再次拥有工具使用能力,循环深度、权限与审计都必须由客户端控制。第四,Server 应接受客户端选择不同模型或返回拒绝,而不是把某个模型名当作强制要求。
和《Harness Engineering》第 5 章的工具设计相呼应,Sampling 是一种“反向工具”:调用发起方是 server,但权限最终由 client 和用户掌握。它把模型能力从具体应用中抽离出来,却没有把安全责任外包给 server。理解这点,才能避免把 Sampling 写成“远程帮我调用 LLM”的便利 API,而是把它当成一种带审批、带能力声明、带上下文最小化要求的协议能力。
一个可靠客户端还应把 Sampling 纳入审计:记录发起 server、请求 method、messages 摘要、maxTokens、是否携带 tools、用户是否批准、最终模型名和 stopReason。审计不是为了窥探内容,而是为了在“服务端为什么突然要求模型生成代码”“为什么一次请求消耗过高”“为什么工具循环没有停住”时能复盘。没有审计的 Sampling,等价于给 server 开了一扇看不见的模型调用门。
Server 侧也要把 Sampling 当成稀缺能力:失败要能降级,拒绝要能继续流程,超时要能释放资源。把它设计成必经路径,会让用户审批、模型限流和客户端策略变成服务可用性的单点。
所以默认路径应是“不依赖 Sampling 也能完成核心任务”。
17.11 本章小结
Sampling 是 MCP 协议中最独特的设计之一。它反转了传统的请求方向,让 Server 可以通过 Client 间接调用 LLM,实现了以下关键能力:
- 零密钥架构:Server 无需持有 LLM API Key,消除了密钥管理和安全风险
- 模型选择权归用户:通过 hints 和 priorities 的两层抽象,Server 表达需求,Client 做最终决策
- 工具增强的 Agent 循环:Sampling 中支持工具定义和多轮调用,Server 可以实现完整的 Agent 行为
- 人机审批保障安全:用户对 prompt、工具调用、响应都有审查权,防止恶意 Server 滥用
- 跨提供商兼容:协议设计兼顾了 Claude、OpenAI、Gemini 等主流 LLM API 的差异
- 五层安全防线:能力协商、人工审批、Client 控制、迭代限制、内容验证
- 革命性的生态影响:开源 Server 开发者贡献逻辑,用户提供算力,双赢模式
核心心得:
Sampling 不是功能,是协议哲学的体现——权力始终在用户手中。
物理事实:TS SDK 用 Omit<..., 'tools' | 'toolChoice'> 在类型层显式区分 Base/WithTools 两种 createMessage 形态、3 个 overload 串成调用链;Python SDK 直接两个独立类;ModelPreferences 4 字段在两边完全对齐——是 spec 实现一致性”具体功能模块决定”现实的又一例(与 §3.8.3 错误码不对齐对照)。