4.2 高级特性——Sampling、Elicitation 与 Roots
小周把安全方案给同事看了,小李终于放心了。但在实现数据库查询工具时,小周遇到一个新问题:Server 执行复杂分析时,能不能让 AI 帮忙"思考"?
到目前为止,小周看到的都是 Client 主动请求 Server。但 MCP 有一个独特的能力:Server 可以反过来请求 Client 做事。
注意:本节的 Sampling 和 Elicitation 示例使用了底层 API(
session.request()),因为 FastMCP 暂未提供高层封装。理解原理后,可直接复制使用。
双向通信
MCP 不同于传统的请求-响应协议。Server 可以主动向 Client 发出三种请求:
Sampling——Server 请求 LLM 推理
Sampling 让 Server 在执行工具过程中,请求 Client 的 LLM 进行推理。
为什么需要 Sampling?
想象一个代码分析 Server:它读取代码后,需要 LLM 帮忙理解代码意图。与其自己调用 LLM API(需要 API Key),不如请求 Client 的 LLM(Client 已经有 LLM 连接)。
协议消息
// Server → Client 请求
{
"method": "sampling/createMessage",
"params": {
"messages": [
{ "role": "user", "content": { "type": "text", "text": "这段代码做了什么?" } }
],
"maxTokens": 500,
"systemPrompt": "你是一个代码分析专家"
}
}
// Client → Server 响应
{
"result": {
"role": "assistant",
"content": { "type": "text", "text": "这段代码实现了一个 HTTP 服务器..." },
"model": "claude-sonnet-4-20250514"
}
}Python 实现
# Server 端——在工具中请求 Sampling
@mcp.tool()
async def analyze_code(code: str) -> str:
"""分析代码功能"""
# 请求 Client 的 LLM 进行分析
result = await mcp.get_context().session.request(
"sampling/createMessage",
{
"messages": [
{"role": "user", "content": {"type": "text", "text": f"分析这段代码:\n{code}"}}
],
"maxTokens": 500
}
)
return result.content.text安全约束
Sampling 请求涉及 LLM 推理,有严格的安全约束:
- Client 可以拒绝任何 Sampling 请求
- 用户应该审批 Sampling 请求(特别是涉及敏感内容时)
- Server 不能无限次请求 Sampling——Client 应实施速率限制
端到端可运行示例
小周想写一个 Server,让工具在执行过程中能"思考"——比如分析代码质量。下面是一个最小可运行的完整示例:
Server 端(使用 Sampling 分析代码)
# sampling_server.py
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("code-analyzer")
@mcp.tool()
async def analyze_code_quality(code: str) -> str:
"""分析代码质量——让 AI 帮忙评估代码的可读性、安全性和性能"""
ctx = mcp.get_context()
result = await ctx.session.request(
"sampling/createMessage",
{
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": f"请用 3 句话评估这段代码的质量(可读性、安全性、性能):\n\n```\n{code}\n```"
}
}
],
"maxTokens": 300,
"systemPrompt": "你是一个代码审查专家。用简洁的中文回答。"
}
)
return result.content.text
mcp.run()运行方式:python sampling_server.py,然后在支持 Sampling 的 Client(如 Claude Desktop)中连接。
使用场景:在 Claude Desktop 中说"分析这段代码的质量:def foo(x): return x/0",Server 会通过 Sampling 让 Client 的 LLM 进行分析,返回"这段代码有一个除零错误..."。
关键点:Server 不需要自己的 API Key——它借用 Client 已有的 LLM 连接。
Elicitation——Server 请求用户输入
Elicitation(v2 引入)让 Server 向用户请求信息,比如确认操作、获取凭据、选择选项。
协议消息
// Server → Client 请求
{
"method": "elicitation/create",
"params": {
"message": "需要数据库连接密码才能继续",
"requestedSchema": {
"type": "object",
"properties": {
"password": { "type": "string", "description": "数据库密码" }
},
"required": ["password"]
}
}
}
// Client → Server 响应(用户提供了密码)
{
"result": {
"action": "accept",
"content": { "password": "user_provided_password" }
}
}
// Client → Server 响应(用户取消了)
{
"result": {
"action": "cancel"
}
}响应动作
| 动作 | 说明 |
|---|---|
accept | 用户填写了信息并确认 |
cancel | 用户取消了操作 |
reject | 用户拒绝了请求 |
Schema 限制
Elicitation 的 Schema 有严格限制,确保安全:
- 只支持
string、number、boolean基本类型 - 不支持
object、array嵌套 - 不支持
$ref引用 - 不支持
format、contentMediaType、anyOf、allOf、oneOf
Python 实现
# Server 端——请求用户确认
@mcp.tool()
async def delete_records(table: str, condition: str) -> str:
"""删除数据库记录(需用户确认)"""
# 向用户请求确认
result = await mcp.get_context().session.request(
"elicitation/create",
{
"message": f"确认要从 {table} 表中删除满足条件 {condition} 的记录吗?",
"requestedSchema": {
"type": "object",
"properties": {
"confirm": {
"type": "boolean",
"description": "确认删除"
}
},
"required": ["confirm"]
}
}
)
if result.get("action") != "accept" or not result.get("content", {}).get("confirm"):
return "操作已取消"
# 执行删除...
return f"已从 {table} 删除记录"端到端可运行示例
Server 端(使用 Elicitation 请求确认)
# elicitation_server.py
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("safe-todo")
todos: list[str] = []
@mcp.tool()
def add_todo(text: str) -> str:
"""添加待办事项"""
todos.append(text)
return f"已添加:{text}(共 {len(todos)} 条)"
@mcp.tool()
async def clear_todos() -> str:
"""清除所有待办事项(需要用户确认)"""
ctx = mcp.get_context()
result = await ctx.session.request(
"elicitation/create",
{
"message": f"确认要清除所有 {len(todos)} 条待办事项吗?此操作不可撤销。",
"requestedSchema": {
"type": "object",
"properties": {
"confirm": {
"type": "boolean",
"description": "确认清除所有待办"
}
},
"required": ["confirm"]
}
}
)
if result.get("action") != "accept" or not result.get("content", {}).get("confirm"):
return "操作已取消"
count = len(todos)
todos.clear()
return f"已清除 {count} 条待办事项"
mcp.run()运行后,在 Claude Desktop 中说"清除所有待办"——Server 会弹窗请求用户确认,用户点击"取消"则不执行。
Roots——暴露文件系统根目录
Roots 让 Client 告诉 Server 自己暴露了哪些文件系统目录。这样 Server 知道可以在哪些范围内操作。
协议消息
// Server → Client 请求
{ "method": "roots/list" }
// Client → Server 响应
{
"result": {
"roots": [
{ "uri": "file:///home/user/project", "name": "My Project" },
{ "uri": "file:///home/user/docs", "name": "Documents" }
]
}
}使用场景
- 代码分析 Server 需要知道项目目录在哪里
- 文件搜索 Server 需要知道可以搜索哪些目录
- IDE 集成 Server 需要知道工作区的根目录
Completions——参数自动补全
Completions(v2 引入)让 Server 为 Prompt 和 Resource Template 的参数提供自动补全建议。
// Client 请求补全
{
"method": "completion/complete",
"params": {
"ref": { "type": "ref/prompt", "name": "code_review" },
"argument": { "name": "language", "value": "py" }
}
}
// Server 返回补全建议
{
"result": {
"completion": {
"values": ["python", "pytorch", "pyside"],
"hasMore": true
}
}
}这在用户填写工具参数时特别有用——比如输入 py 后自动建议 python。
Logging——结构化日志
Server 可以通过 notifications/message 向 Client 发送结构化日志:
// Client 设置日志级别
{ "method": "logging/setLevel", "params": { "level": "info" } }
// Server 发送日志
{
"method": "notifications/message",
"params": {
"level": "error",
"logger": "database",
"data": { "error": "Connection failed", "host": "localhost" }
}
}日志级别(RFC 5424):debug → info → notice → warning → error → critical
本节核心要点
- MCP 支持双向通信——Server 可以反过来请求 Client
- Sampling:Server 请求 Client 的 LLM 推理(需要用户审批)
- Elicitation:Server 向用户请求信息(确认、凭据、选择)
- Roots:Client 告诉 Server 可操作的文件系统目录
- Completions:Server 为参数提供自动补全(v2+)
- 所有 Server→Client 请求都需要 Client 声明对应能力
思考题:在什么场景下你会使用 Sampling 而不是直接在 Server 中调用 LLM API?两者各有什么优缺点?
参考思路
Sampling 的优势:Server 不需要自己的 API Key(使用 Client 已有的 LLM 连接);计费统一在 Client 侧。直接调用 LLM API 的优势:更可控、不依赖 Client 能力、延迟更低。选择依据:如果你的 Server 需要部署给多个 Client 使用(它们可能有不同的 LLM),用 Sampling;如果 Server 是独立服务,直接调用 API 更简单。