使用 Python 入门 Model Context Protocol(MCP)——引导澄清(Elicitation)

83 阅读9分钟

Elicitation 指“获取或产生某物(尤其是信息或反应)的过程”。

这对 MCP 有什么意义?官方文档这样描述:
Model Context Protocol(MCP)提供了一种标准化方式,让服务器在交互过程中通过客户端用户请求额外信息。该流程既让客户端保持对用户交互与数据共享的控制,又使服务器能动态收集所需信息。

换句话说:当服务器发现需要更多信息时,会借助客户端去再问用户,以便完成任务。
想象一个场景:你在订假期,但所查日期无票。用 elicitation,服务器不会只说“没票”,而是进一步追问推荐临近可选日期。这能显著提升成交或预订的概率。

本章你将学到:

  • 解释什么是 elicitation
  • 了解何时使用它
  • 构建一次 elicitation 集成

涵盖主题:

  • 为什么要用 elicitation?
  • 如何实现 elicitation
  • Elicitation 的流程
  • JSON-RPC 消息形式
  • 服务器端实现
  • 用 VS Code 测试 elicitation

为什么需要 elicitation?

进一步概括适用动机:

  • 任务复杂度:有些任务无法一次性提供全部信息,需要多步选择/补充。例如订电影票:选片名和日期后,还要选座位类型、是否要电子票等。一次性全问会糟糕,分步询问更友好。
  • 提升转化:若用户想要的“红色毛衣”缺货,服务器可引导选择其他颜色登记到货通知,更可能达成交易。
  • 更好的用户体验:比起直接“No”,提供合理由选项与替代方案,体验更佳。

实施前的指引

官方建议(信任/安全/隐私):

  • 服务器不得通过 elicitation 请求敏感信息

  • 应用应当:

    • 提供 UI 清楚标示哪个服务器在请求信息
    • 允许用户在发送前审阅与修改回答
    • 尊重隐私,提供拒绝/取消选项

Elicitation 流程

总体上,当服务器在处理某个工具、资源或提示模板的调用时,发现信息不足,就会触发 elicitation。它分两步:

  1. 服务器→客户端:先询问客户端是否可以发起一次 elicitation 请求给用户。
  2. 客户端→用户:客户端向用户展示请求并收集输入。

用户在第 1、2 步都可以接受或拒绝。可配合行程预订的序列图来理解该过程。

image.png

JSON-RPC 消息

请求(服务器→客户端)

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "elicitation/create",
  "params": {
    "message": "Please provide your GitHub username",
    "requestedSchema": {
      "type": "object",
      "properties": {
        "name": { "type": "string" }
      },
      "required": ["name"]
    }
  }
}

要点:

  • message:展示给用户的说明/问题。
  • requestedSchema:明确需要哪些字段、类型与规则(如格式、校验)。例如请求姓名、邮箱、年龄并带验证:
"requestedSchema": {
  "type": "object",
  "properties": {
    "name": { "type": "string", "description": "Your full name" },
    "email": { "type": "string", "format": "email", "description": "Your email address" },
    "age": { "type": "number", "minimum": 18, "description": "Your age" }
  },
  "required": ["name", "email"]
}

接受型响应(客户端→服务器)

{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "action": "accept",
    "content": {
      "name": "Monalisa Octocat",
      "email": "octocat@github.com",
      "age": 30
    }
  }
}

拒绝型响应

{
  "sonrpc": "2.0",
  "id": 2,
  "result": { "action": "decline" }
}

注:还有取消(cancel) ,类似用户关闭对话框或按下 Esc,不明确回答,仅终止当前询问。

请求 Schema 的常见类型

  • string(可加长度/正则/格式校验)

    {
      "type": "string",
      "title": "Display Name",
      "description": "Description text",
      "minLength": 3,
      "maxLength": 50,
      "pattern": "^[A-Za-z]+$",
      "format": "email"
    }
    
  • number / integer(可设最小/最大)

    {
      "type": "number",
      "title": "Display Name",
      "description": "Description text",
      "minimum": 0,
      "maximum": 100
    }
    
  • boolean(是/否,支持默认值)

    {
      "type": "boolean",
      "title": "Display Name",
      "description": "Description text",
      "default": false
    }
    
  • enum(枚举下拉,例如不同日期/套餐)

    {
      "type": "string",
      "title": "Display Name",
      "description": "Description text",
      "enum": ["option1", "option2", "option3"],
      "enumNames": ["Option 1", "Option 2", "Option 3"]
    }
    

以上类型由客户端渲染成相应的输入控件,以便用户填写或选择。接下来就可以按本章后续的实现部分将多种类型组合运用。

实现服务端功能(Implementing the server-side functionality)

先从服务端开始。需要了解的是:当某个服务端功能(tool、resource、prompt)在执行过程中发现信息不足时,应当触发一次 elicitation(引导澄清) 消息。

首先看看如何产生这类消息。我们先用一个 if 判断是否需要发起 elicitation;如果需要,就在上下文对象上调用 elicit,并提供要展示的 message 以及需遵循的 schema

class BookingPreferences(BaseModel):
    """用于收集用户偏好的模式定义。"""
    checkAlternative: bool = Field(description="是否尝试其他日期?")
    alternativeDate: str = Field(
        default="2024-12-26",
        description="备选日期(YYYY-MM-DD)",
    )

def is_available_date -> bool:
    pass

@mcp.tool()
def book_table(date: str, ctx: Context[ServerSession, None]) -> str:
    # 1. 检查日期是否可用
    if not is_available_date(date):
        result = await ctx.elicit(
            message=(f"{date} 无法预订。要尝试其他日期吗?"),
            schema=BookingPreferences,
        )

接着检查客户端/用户的响应:

if result.action == "accept" and result.data:
    if result.data.checkAlternative:
        return f"[SUCCESS] 已为 {result.data.alternativeDate} 预订"
    return "[CANCELLED] 未进行预订"
return "[CANCELLED] 预订已取消"

如上所示,我们检查 action 属性来判断用户是否接受并提交了附加数据。若接受,则解析其选择;若未接受,则返回取消信息。

把所有内容放在一起:

from pydantic import BaseModel, Field
from mcp.server.fastmcp import Context, FastMCP
from mcp.server.session import ServerSession

mcp = FastMCP(name="Elicitation Example")

class BookingPreferences(BaseModel):
    """用于收集用户偏好的模式定义。"""
    checkAlternative: bool = Field(description="是否尝试其他日期?")
    alternativeDate: str = Field(
        default="2024-12-26",
        description="备选日期(YYYY-MM-DD)",
    )

@mcp.tool()
async def book_trip(date: str, ctx: Context[ServerSession, None]) -> str:
    """带日期可用性检查的行程预订。"""
    # 检查日期是否可用
    if not is_available_date(date):
        # 日期不可用——询问用户是否选择备选日期
        result = await ctx.elicit(
            message=(f"{date} 无法预订。要尝试其他日期吗?"),
            schema=BookingPreferences,
        )
        if result.action == "accept" and result.data:
            if result.data.checkAlternative:
                return f"[SUCCESS] 已为 {result.data.alternativeDate} 预订"
            return "[CANCELLED] 未进行预订"
        return "[CANCELLED] 预订已取消"

    # 日期可用
    return f"[SUCCESS] 已为 {date} 预订"

使用 VS Code 测试 Elicitation

要在 VS Code 中测试 elicitation 功能,需要在 mcp.json 中添加该 MCP 服务器的配置,例如:

"server": {
  "type": "sse",
  "url": "http://localhost:8000/sse"
}

然后确保处于 Agent 模式,输入如下提示词:

Book trip on 2025-02-01

界面中会经历如下过程:

  1. 输入提示并触发工具调用:你输入预订请求,系统识别为工具调用;需要你批准该调用后继续。 image.png

    (Figure 10.2 – 输入提示并看到工具调用)

  2. 批准工具调用:批准后,界面会告知选定日期已满,并将引导你填写 elicitation 响应

image.png

(Figure 10.3 – 批准工具调用)

  1. 编辑 elicitation 响应:按之前定义的 schema 采集更多信息。界面会先问你是否要改期;若选择 true,接着会要求填写新的日期;若选择 false,流程结束。

image.png

(Figure 10.4 – 编写 elicitation 响应)

  1. 提交 elicitation:选择继续后,填写备选日期,例如:

image.png

(Figure 10.5 – 填写备选日期)

  1. 得到最终结果:服务器校验该备选日期可订,并返回确认信息:

image.png

(Figure 10.6 – 最终结果)

至此,就完成了在服务端实现并通过 VS Code 测试的 elicitation 流程。

在客户端实现 Elicitation(引导澄清)

太好了——我们已经理解了服务端的实现方式,并且知道了如何在 VS Code 中测试。接下来实现客户端部分。

通常在编写客户端时,你会与一个 ClientSession 对象打交道。它负责管理与服务器的连接。除了传入 read_streamwrite_stream 外,你还可以传入 elicitation_callback 来处理任何引导澄清事件:

async with ClientSession(
    read_stream,
    write_stream,
    elicitation_callback=elicitation_callback_handler
) as session:

来看一下 elicitation_callback_handler

async def elicitation_callback_handler(
    context: RequestContext[ClientSession, None],
    params: ElicitRequestParams
):
    print(f"[CLIENT] Received elicitation data: {params.message}")

可以看到,这个回调接收两个参数:请求上下文和引导澄清的请求参数。此时我们需要构造一个客户端响应并发回给服务器。可以发送三种响应:

1) accept(接受)

表示希望继续引导澄清流程。发送 accept 时,应符合服务器声明的输入 Schema。例如,下面的响应是合规的:

return ElicitResult(action="accept", content={
    "checkAlternative": True,
    "alternativeDate": "2025-01-01"
})  # 应该改订为 1 月 1 日,而非最初的 1 月 2 日

这里我们将 action 设为 accept,并在 content 中携带负载,字段为 checkAlternativealternativeDate(与服务器端的 schema 对应)。注意示例里直接硬编码了日期;在更贴近生产的应用里,alternativeDate 的值应来自向用户询问的输入结果。


2) decline(拒绝)

表示要终止引导澄清流程。可以这样返回:

return ElicitResult(action="decline")

这里不设置 content,清楚地表明我们不提供任何额外信息或上下文。这类 decline 通常发生在流程很早的阶段,类似用户直接说“不”。


3) accept(接受流程,但提供的内容表示不再选择备选)

用户接受继续交互,但最终不提供备选日期:

# 1. 拒绝选择其他日期
return ElicitResult(action="accept", content={
    "checkAlternative": False
})  # 应返回“未进行预订”,验证有效 -->

这种“拒绝”发生在更晚的阶段:用户被给予选择备选日期的机会后,选择不更改。

image.png

文中还配有一个时序图(Figure 10.7),展示了在 Python 客户端里实现引导澄清时的整体流程。总体来看,用户可能在不同阶段取消或拒绝,因此关键点是让客户端优雅地处理这些不同的分支与结果。

小结

本章详细介绍了引导澄清(elicitation)的客户端实现流程,包括如何被触发、可能出现的多种情形,以及在此过程中可向服务器返回的不同响应类型。Elicitation 的目的,是在系统执行请求时主动向用户索取更多信息,以更好地理解意图或完成任务。
我们也看到,用户可以在流程的不同阶段接受中止
最后,良好地利用引导澄清能显著提升交互体验,帮助系统更有效地满足用户需求。

下一章将介绍如何使用 Basic Auth、JWT、OAuth 2.1 等方式来保护你的 MCP 服务器与客户端。