7.3 现代路径:Resolve 返回 Elicit 与多轮往返


文档摘要

7.3 现代路径:Resolve 返回 Elicit 与多轮往返 本节摘要:本节是第 7 章的硬核核心,讲 v2 最微妙的变化——2026-07-28 现代协议下,引导填写怎么实现。背景是:新协议移除了「服务端直接发起请求」的能力(为简化协议),所以 这种「服务端执行中主动要输入」的方式,在现代连接上行不通。替代方案是多轮往返(MRTR, Multi-Round-Trip Request)——Resolve 解析函数返回一个 对象,SDK 把它转成「请求返回一个问题」,客户端下次重试时带上用户的答案,工具继续执行。本节讲透这套机制,以及它为什么让协议更简单也更安全。 一、协议变化:为什么移除服务端发起请求 先理解背景。

7.3 现代路径:Resolve 返回 Elicit 与多轮往返

本节摘要:本节是第 7 章的硬核核心,讲 v2 最微妙的变化——2026-07-28 现代协议下,引导填写怎么实现。背景是:新协议移除了「服务端直接发起请求」的能力(为简化协议),所以 ctx.elicit 这种「服务端执行中主动要输入」的方式,在现代连接上行不通。替代方案是多轮往返(MRTR, Multi-Round-Trip Request)——Resolve 解析函数返回一个 Elicit 对象,SDK 把它转成「请求返回一个问题」,客户端下次重试时带上用户的答案,工具继续执行。本节讲透这套机制,以及它为什么让协议更简单也更安全。

一、协议变化:为什么移除服务端发起请求

先理解背景。在 legacy 协议下,服务端可以直接发起请求——比如工具执行中,服务端主动发一个 elicitation/request 给客户端要输入。这听起来方便,但带来几个问题:

问题 说明
协议复杂 服务端既要回应请求,又要发起请求,角色混乱
状态管理难 服务端要管理「等用户回答」的挂起状态
安全风险 服务端能主动发起请求,增加了攻击面
难扩展 每种「服务端要东西」都要单独定义协议方法

2026-07-28 协议做了一个关键简化:移除服务端发起请求的能力,让服务端只回应。这样服务端角色纯粹(被动响应),协议简单、安全、易扩展。

但问题是:引导填写原本靠服务端发起请求实现,现在怎么办? 答案是多轮往返。

二、多轮往返(MRTR)的核心思想

多轮往返(MRTR, Multi-Round-Trip Request)的核心思想是:请求可以返回一个问题,客户端下次重试时带答案

传统请求(一口气): 客户端:调用 transfer(...) 服务端:执行 → 返回结果 (一轮完成) 多轮往返(MRTR): 客户端:第 1 次调用 transfer(...) 服务端:执行到一半需要确认 → 返回「问题」(不是结果) 客户端:把问题转给用户 用户:回答 客户端:第 2 次调用 transfer(...),带上答案 服务端:拿到答案,继续执行 → 返回结果 (两轮完成)

关键点:同一个请求可能要多次往返——每次返回要么是「问题」(要更多输入),要么是「最终结果」(完成)。客户端据此决定「问用户」还是「收工」。

三、Resolve 返回 Elicit:现代写法

在现代协议下,引导填写靠 Resolve 解析函数返回 Elicit 对象实现(回顾第 6.2 节的 Resolve):

from typing import Annotated from mcp.server import MCPServer from mcp.server.mcpserver import Context from mcp.shared import resolve mcp = MCPServer("Bank") # 解析函数:返回 Elicit,触发引导填写 async def get_confirmation(ctx: Context): return resolve.Elicit( message="确认执行这个大额转账?", schema={ "confirm": {"type": "boolean", "description": "确认"} } ) @mcp.tool() async def transfer( from_id: str, to_id: str, amount: float, confirmed: Annotated[bool, Resolve(get_confirmation)], # ← Resolve 返回 Elicit ) -> bool: """Transfer money with confirmation.""" if amount > 1000 and not confirmed: raise PermissionError("未确认") return do_transfer(from_id, to_id, amount)

发生了什么?

第 1 轮:客户端调 transfer(from_id, to_id, amount) │ ▼ SDK 运行 Resolve:get_confirmation(ctx) get_confirmation 返回 Elicit(...) │ ▼ SDK 把 Elicit 转成「请求返回问题」 服务端返回:{action: "elicit", message: "...", schema: {...}} │ ▼ 客户端把问题转给用户 用户回答:confirm=true │ ▼ 客户端第 2 次调 transfer,带 confirmed=true SDK 运行 Resolve:get_confirmation(ctx) 这次 Resolve 拿到用户答案,返回 True(填充 confirmed) │ ▼ 工具执行 transfer(..., confirmed=True) → 返回结果

Elicit 对象让 Resolve 「暂停」——第一次返回 Elicit 触发问用户,第二次(客户端带答案重试)Resolve 返回实际值填充参数。

四、为什么用 Resolve 而不是 ctx.elicit

你可能会问:既然 ctx.elicit 也能引导填写,为什么现代协议推崇 Resolve 返回 Elicit?关键差别在协议兼容性:

方式 legacy 协议 现代协议(2026-07-28)
ctx.elicit(服务端直接发起) ✅ 可用 ❌ 不支持(服务端不能发起请求)
Resolve 返回 Elicit(MRTR) ✅ 可用 ✅ 可用

Resolve 返回 Elicit 在两种协议下都能工作,因为它走的是「请求返回问题」的 MRTR 机制,不依赖服务端发起请求。而 ctx.elicit 依赖服务端直接发起,在现代协议下不可用。

💡 技巧:新代码优先用 Resolve 返回 Elicit。它跨协议兼容,是官方推荐的现代写法。ctx.elicit 主要用于维护 legacy 连接的兼容性,新项目少用。

五、MRTR 的协议层细节

深入一点,看 MRTR 在协议层怎么走(概念性,SDK 替你处理):

第 1 轮请求: 客户端 → 服务端: method: tools/call params: {name: "transfer", arguments: {from_id, to_id, amount}} 服务端 → 客户端(返回问题,不是结果): result: { _meta: { elicit_request: {message: "...", schema: {...}} } } 第 2 轮请求(带答案): 客户端 → 服务端: method: tools/call params: { name: "transfer", arguments: {from_id, to_id, amount}, _meta: { elicit_response: {action: "accept", data: {confirm: true}} } } 服务端 → 客户端(返回最终结果): result: {content: [...], structuredContent: {result: true}}

注意 _meta 字段——它承载「问题」与「答案」的传递。SDK 在两端处理 _meta,你写代码时只关心 Resolve 返回 Elicit 与拿到答案,不用管 _meta 细节。

六、多次往返:复杂场景

MRTR 不限于两次往返——一个工具可能需要多次引导填写:

transfer 工具的复杂流程: 第 1 轮:问「确认转账?」→ 用户答 confirm=true 第 2 轮:问「输入验证码」→ 用户答 code=1234 第 3 轮:问「选收款方式:卡/钱包」→ 用户答 method=card 第 4 轮:所有信息齐全,执行,返回结果

每个 Resolve 解析函数对应一次引导填写,SDK 按顺序处理。这种「多步交互」让工具能支撑复杂业务流程,而模型只需一次调用——后续往返由 SDK 与客户端处理。

⚠️ 注意:多次往返会增加延迟(每轮都要等用户)。设计时要权衡——能合并的问题别拆成多轮(如「确认+验证码」可合并成一个表单)。第 7.2 节的 Schema 可以包含多字段,优先用一次问全。

七、现代路径的优势

最后总结现代路径(Resolve + MRTR)的优势,这些是 2026-07-28 协议简化的回报:

优势 说明
协议简单 服务端只回应,不发起,角色纯粹
安全 攻击面减小(服务端不能主动发起)
易扩展 任何「要更多输入」都走同一套 MRTR
跨协议 Resolve + Elicit 在 legacy 与现代都工作
与依赖注入统一 引导填写就是「特殊的依赖」,用 Resolve 表达

这套机制是 v2 最微妙也最优雅的设计——用「请求返回问题 + 客户端带答案重试」替代「服务端主动发起」,既简化了协议,又统一了交互模型

本节要点回顾

  1. 2026-07-28 协议移除服务端发起请求,服务端只回应,角色纯粹。
  2. MRTR(多轮往返):请求可返回问题,客户端带答案重试,工具继续。
  3. 现代写法:Resolve 返回 Elicit,第一次触发问用户,第二次拿答案填参数。
  4. Resolve 返回 Elicit 跨协议兼容,ctx.elicit 仅 legacy 可用。
  5. 新代码优先用 Resolve 返回 Elicit,是官方推荐写法。
  6. _meta 字段承载问题与答案传递,SDK 处理细节,你只关心 Resolve。
  7. 优势:协议简单、安全、易扩展、跨协议、与依赖注入统一。

现代路径清楚了,下一节讲采样与根——两个已显弃用趋势但仍在工作的能力。


发布者: 作者: 灏天文库 转发
评论区 (0)
U