Skip to content

4.2 高级特性——Sampling、Elicitation 与 Roots

小周把安全方案给同事看了,小李终于放心了。但在实现数据库查询工具时,小周遇到一个新问题:Server 执行复杂分析时,能不能让 AI 帮忙"思考"?

到目前为止,小周看到的都是 Client 主动请求 Server。但 MCP 有一个独特的能力:Server 可以反过来请求 Client 做事。

注意:本节的 Sampling 和 Elicitation 示例使用了底层 API(session.request()),因为 FastMCP 暂未提供高层封装。理解原理后,可直接复制使用。


双向通信

MCP 不同于传统的请求-响应协议。Server 可以主动向 Client 发出三种请求:

🧠
Sampling
Server → Client LLM
Server ➜ 🧠 Client LLM
Server 请求 Client 的 LLM 进行推理——让工具「思考」
📋
Elicitation
Server → 用户
Server ➜ 👤 用户
Server 向用户请求信息——让工具「问问题」
📂
Roots
Server → Client 文件系统
Server ➜ 📂 Client 文件
Server 询问 Client 暴露的文件系统根目录——让工具「知道上下文」

Sampling——Server 请求 LLM 推理

Sampling 让 Server 在执行工具过程中,请求 Client 的 LLM 进行推理。

为什么需要 Sampling?

想象一个代码分析 Server:它读取代码后,需要 LLM 帮忙理解代码意图。与其自己调用 LLM API(需要 API Key),不如请求 Client 的 LLM(Client 已经有 LLM 连接)。

协议消息

json
// 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 实现

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 分析代码)
python
# 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 向用户请求信息,比如确认操作、获取凭据、选择选项。

协议消息

json
// 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 有严格限制,确保安全:

  • 只支持 stringnumberboolean 基本类型
  • 不支持 objectarray 嵌套
  • 不支持 $ref 引用
  • 不支持 formatcontentMediaTypeanyOfallOfoneOf

Python 实现

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 请求确认)
python
# 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 知道可以在哪些范围内操作。

协议消息

json
// 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 的参数提供自动补全建议。

json
// 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 发送结构化日志:

json
// Client 设置日志级别
{ "method": "logging/setLevel", "params": { "level": "info" } }

// Server 发送日志
{
  "method": "notifications/message",
  "params": {
    "level": "error",
    "logger": "database",
    "data": { "error": "Connection failed", "host": "localhost" }
  }
}

日志级别(RFC 5424):debuginfonoticewarningerrorcritical


本节核心要点

  • 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 更简单。


← 上一节:安全与授权 | 目录 | 下一节:部署模式与版本演进 →