十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

python-sdk Elicitation 指南:在工具调用中途向用户提问的两种模式与两种实现

python-sdk Elicitation 指南:在工具调用中途向用户提问的两种模式与两种实现 人工智能MCP 服务MCP Clients【免费下载链接】python-sdkThe official Python SDK for Model Context Protocol servers and clients项目地址https://gitcode.com/gh_mirrors/pythonsd/python-sdk点击查看免费下载Elicitation引导式提问是 Model Context ProtocolMCP中一项允许服务端工具在调用进行到一半时停下来向用户提问的能力工具不需要因为缺少一个关键答案而失败而是把问题抛给用户再把答案接回同一次函数调用中继续执行。本文基于 python-sdk官方 Python MCP SDK的文档与源码完整讲解它的两种模式表单模式与 URL 模式、两种提问方式Resolver 参数注入与ctx.elicit直接调用、客户端侧的应答回调以及不同协议版本下的底层传输差异让读者能够直接写出可运行的服务端与客户端代码。核心概念两种模式、两种提问方式从 docs/handlers/elicitation.md 的定义出发elicitation 解决的是这样一个场景一个工具执行到一半只差一个答案确认、日期、数量不该因此整体失败而是可以在调用中途询问用户。它有两种模式表单模式Form mode服务端需要某个具体值一个确认、一个日期、一个数量。你用字段描述清楚要什么客户端负责渲染表单让用户填写。URL 模式URL mode服务端需要用户去另一个地方OAuth 授权页、支付页面。用户在那里所做的一切都不经过 MCP 协议敏感数据凭证、卡号因此不会流入模型上下文。对应地有两种提问的方式Resolver推荐把问题挂到一个工具参数上由框架代为提问。它在所有连接上都可用——无论客户端讲的是哪个协议世代的规范。直接调用await ctx.elicit(...)这是服务端到客户端的请求该通道只存在于 legacy 连接规范版本 2025-11-25 或更早的客户端上。从源码看这两种方式在 src/mcp/server/elicitation.py 与 src/mcp/server/mcpserver/resolve.py 中分别实现下文将逐一展开。方式一用 Resolver 提问推荐兼容所有连接当一个提问决定整条工具的执行走向——你确定吗三张匹配的账户选哪一张——应该把它从工具函数体里抽出来放进一个resolver解析器由框架代为完成提问。用法与完整示例一个被标注为Annotated[T, Resolve(fn)]的参数会在工具函数体执行之前由fn填充。Resolver 有两种返回直接返回值说明它已经知道答案无需打扰客户端返回Elicit(...)表示需要框架代为提问。以 docs_src/elicitation/tutorial004.py 的删除文件夹工具为例完整代码from typing import Annotated from pydantic import BaseModel from mcp.server import MCPServer from mcp.server.mcpserver import ( AcceptedElicitation, CancelledElicitation, DeclinedElicitation, Elicit, ElicitationResult, Resolve, ) mcp MCPServer(Files) _FOLDERS: dict[str, list[str]] {/tmp/empty: [], /tmp/project: [main.py, README.md]} class Confirm(BaseModel): ok: bool async def confirm_delete(path: str) - Confirm | Elicit[Confirm]: Resolver: ask for confirmation only when the folder is not empty. file_count len(_FOLDERS.get(path, [])) if file_count 0: return Confirm(okTrue) # nothing to confirm, no round-trip to the client return Elicit(f{path} has {file_count} file(s). Delete anyway?, Confirm) mcp.tool() async def delete_folder( path: str, confirm: Annotated[ElicitationResult[Confirm], Resolve(confirm_delete)], ) - str: Delete a folder, asking for confirmation when it is not empty. match confirm: case AcceptedElicitation(dataConfirm(okTrue)): _FOLDERS.pop(path, None) return fdeleted {path} case AcceptedElicitation(): return kept the folder case DeclinedElicitation(): return declined: folder not deleted case CancelledElicitation(): return cancelled: folder not deleted代码中有几个要点只在该问的时候才问confirm_delete通过参数名读取工具自身的path参数、列出文件夹内容空文件夹直接解析为Confirm(okTrue)完全不会与客户端发生往返round-trip。注入完整结果delete_folder把参数标注为ElicitationResult[Confirm]框架注入的是完整结局工具用match覆盖每一种情况接受并确认、接受但保留okFalse、拒绝、取消。参数不进输入 schemaconfirm这个参数永远不会出现在工具对客户端暴露的输入 schema 中——客户端只提供pathresolver 负责提供confirm。这一点由测试 tests/docs_src/test_elicitation.py 验证list_tools()返回的delete_folder的input_schema[properties]中只有{path}。两种注解形态的选择如果工具不需要分支处理就改用未包裹的模型注解Annotated[Confirm, Resolve(confirm_delete)]用户接受时工具直接收到模型实例拒绝或取消时调用以错误中止。该选择在 src/mcp/server/mcpserver/resolve.py 的模块文档中有明确描述其分发逻辑由_wants_union()同文件 L311-L324依据注解类型判定标注ElicitationResult[T]或其成员时消费者拿到完整结局并自行分支标注裸模型T时_unwrap()L645-L648在DeclinedElicitation/CancelledElicitation情况下抛出ToolError中止调用。Resolver 的底层机制从源码结构看resolver 机制比表面更丰富Resolver 构成 DAG一个 resolver 可以声明自己的Resolve(...)依赖、按名字取工具参数、接收Context。build_resolver_plans()在注册时静态分析整张依赖图并检测循环依赖src/mcp/server/mcpserver/resolve.py。返回标记不止Elicitresolver 还可以返回Sample请求客户端 LLM 通过sampling/createMessage采样和ListRoots获取客户端的 roots统一由_Marker联合类型定义L164-L165。提问只是 resolver 能做的事之一。跨协议版本透明resolver 在所有连接上都可用。对 legacy 连接的客户端SDK 直接发送提问对2026-07-28连接的客户端SDK 从调用中返回提问InputRequiredResult客户端下一次尝试时携带答案。你的 resolver 永远不会察觉到差异——底层机制见 docs/handlers/multi-round-trip.md。方式二在工具内部直接提问ctx.elicit工具也可以在自己的函数体中间停下来提问。这需要Context参数——任何工具都能接收一个其完整说明见 docs/handlers/context.md。⚠️协议版本限制重要ctx.elicit()与ctx.elicit_url()是服务端到客户端的请求这个通道只对 legacy 连接规范版本2025-11-25或更早的客户端存在。在2026-07-28连接上没有服务端发起的请求这些调用会失败。Resolver 则在两种连接上都可用。完整背景见 docs/protocol-versions.md。表单模式完整示例以 docs_src/elicitation/tutorial001.py 的订餐工具为例from pydantic import BaseModel, Field from mcp.server import MCPServer from mcp.server.mcpserver import Context mcp MCPServer(Bistro) class AlternativeDate(BaseModel): accept_alternative: bool Field(descriptionTry another date?) date: str Field(default2025-12-26, descriptionAlternative date (YYYY-MM-DD)) mcp.tool() async def book_table(date: str, party_size: int, ctx: Context) - str: Book a table at the bistro. if date ! 2025-12-25: return fBooked a table for {party_size} on {date}. result await ctx.elicit( messagefNo tables for {party_size} on {date}. Would you like to try another date?, schemaAlternativeDate, ) if result.action accept and result.data.accept_alternative: return await book_table(result.data.date, party_size, ctx) return No booking made.要点ctx.elicit接收一个消息和一个 Pydantic 模型消息是给用户看的提示模型是期望回答的 schema。工具必须是async def它要在中途停住等待一个真实的人。只在必要时提问任何其他日期直接返回不需要任何往返。答案会重新进入book_table本身答案与任何输入一样——如果用户接受的备选日期也满座会再次提问而不是盲目确认。这一点由测试 tests/docs_src/test_elicitation.py 验证第一次回答2025-12-25依旧满座会再次触发提问第二次回答2025-12-27才完成预订。客户端收到的内容消息 JSON Schema客户端收到你的消息以及由模型生成的 JSON Schema该 schema 就是表单本身{ properties: { accept_alternative: { description: Try another date?, title: Accept Alternative, type: boolean }, date: { default: 2025-12-26, description: Alternative date (YYYY-MM-DD), title: Date, type: string } }, required: [accept_alternative], title: AlternativeDate, type: object }Field(description...)就是表单字段的标签带默认值的字段会预先填充并变为可选。这正是 docs/servers/tools.md 中描述的 Pydantic 到 JSON Schema 的同一套机制。在源码层面schema 的生成与校验在 src/mcp/server/elicitation.py 的render_elicitation_schema()L91-L100中完成它使用自定义的_ElicitationJsonSchema生成器L57-L72把T | None拍平为T、丢弃None默认值再用_validate_rendered_properties()L75-L88逐字段校验渲染结果是否符合规范的PrimitiveSchemaDefinition。表单 schema 的边界只能是扁平原始字段⚠️限制引导式提问的 schema 不像工具的输入 schema 那样富有表现力。只支持扁平的原始字段str、int、float、bool或字符串的Literal会转成enum。如果在模型里再嵌套一个模型ctx.elicit会在向客户端发送任何内容之前抛异常。工具调用以Error executing tool name失败服务端日志中会有原因TypeError: Elicitation schema field address rendered as {$ref: #/$defs/Address}, which is not a valid PrimitiveSchemaDefinition你正在打断一个正在干活的人。如果答案需要嵌套结构那它本来就应该是工具的参数。这条限制在源码里就是_PRIMITIVE_SCHEMA_ADAPTERsrc/mcp/server/elicitation.py对PrimitiveSchemaDefinition的严格校验。测试 tests/docs_src/test_elicitation.py 专门验证了嵌套模型Applicant(name, address: Address)会以这条精确的TypeError消息被拒绝而Literal[inside, terrace]则作为enum通过同文件 L179-L194。三种回答accept / decline / cancelresult.action告诉你用户做了什么恰好有三种可能accept用户提交了表单。result.data是已通过校验的AlternativeDate实例。decline用户拒绝了。cancel用户没有选择就关掉了提问。result.data只在accept时存在所以示例代码先检查result.action。类型检查器会强制这个顺序result.action accept之后result.data才是AlternativeDate之前根本没有.data可用。拒绝不是错误。工具自己决定拒绝意味着什么示例里是不订位然后正常回复模型。测试 tests/docs_src/test_elicitation.py 验证了decline与cancel都返回普通的No booking made.文本结果且is_error为假。答案先校验再进你的代码答案会在你的代码看到它之前就按模型校验。客户端给bool字段发maybe不会破坏你的订位ctx.elicit抛出ValueError调用失败你的if永远不执行。这在 src/mcp/server/elicitation.py 的elicit_with_validation()中实现——先schema.model_validate(result.content)校验失败即以ValueError抛出测试见 tests/docs_src/test_elicitation.py。URL 模式把用户送到别处去有些东西不该经过模型或客户端凭证、卡号、OAuth 授权。对这些场景你不索取数据而是请用户去某个地方。以 docs_src/elicitation/tutorial002.py 为例from mcp.server import MCPServer from mcp.server.mcpserver import Context mcp MCPServer(Bistro) mcp.tool() async def pay_deposit(booking_id: str, ctx: Context) - str: Take the deposit that confirms a booking. result await ctx.elicit_url( messageA 20 EUR deposit confirms your booking., urlfhttps://pay.example.com/deposit/{booking_id}, elicitation_idfdeposit-{booking_id}, ) if result.action accept: return Complete the payment in your browser. return No deposit taken. The booking expires in one hour. mcp.tool() async def confirm_deposit(booking_id: str, ctx: Context) - str: Record a payment reported by the payment provider. await ctx.session.send_elicit_complete(fdeposit-{booking_id}) return fDeposit received for booking {booking_id}.要点ctx.elicit_url()接收消息、要访问的 URL、以及你自己选的elicitation_id后者是任何能在这个服务端内标识该次引导的字符串。结果只有动作没有数据accept只表示用户同意打开 URL不表示 URL 另一端的事情已完成。支付发生在带外out-of-band在用户浏览器与支付服务商之间完成任何内容都不会通过 MCP 回到协议里。源码 src/mcp/server/elicitation.py 的elicit_url()明确列出了适用场景敏感凭证、OAuth 授权流程、支付与订阅流程、任何不应进入 LLM 上下文的数据交互。注意第二个工具当服务端得知带外流程结束webhook、轮询示例中用第二个工具模拟ctx.session.send_elicit_complete(...)会发送notifications/elicitation/complete携带同一个elicitation_id。这就是客户端得知可以停止显示waiting for payment...的方式——没有它客户端只能猜。测试 tests/docs_src/test_elicitation.py 验证了该通知确实以ElicitCompleteNotification类型、携带deposit-b42的elicitation_id到达客户端。客户端侧用一个回调应答所有提问服务端提问客户端通过向Client(...)传入elicitation_callback来应答from mcp import Client from mcp.client import ClientRequestContext from mcp.types import ElicitRequestParams, ElicitRequestURLParams, ElicitResult async def handle_elicitation(context: ClientRequestContext, params: ElicitRequestParams) - ElicitResult: if isinstance(params, ElicitRequestURLParams): print(fOpen this link to continue: {params.url}) return ElicitResult(actionaccept) print(params.message) return ElicitResult(actionaccept, content{accept_alternative: True, date: 2025-12-27}) async def main() - None: async with Client( http://127.0.0.1:8000/mcp, modelegacy, elicitation_callbackhandle_elicitation, ) as client: result await client.call_tool(book_table, {date: 2025-12-25, party_size: 2}) print(result.content)完整代码见 docs_src/elicitation/tutorial003.py该文件同时是客户端教程的一部分。要点一个回调处理两种模式params是ElicitRequestFormParams与ElicitRequestURLParams的联合类型用isinstance分支即可。URL 模式把params.url展示给用户返回用户选择的动作永远不要返回任何content。表单模式真实应用应渲染params.requested_schema并把用户输入作为content返回。示例回调总是用预设答案说好——这正是测试里想要的那种回调tests/docs_src/test_elicitation.py 直接复用它同时应答了表单与 URL 两种模式。注册回调即声明能力这是服务端获知这个客户端可以被提问的方式。客户端能为服务端应答的其它事情见 docs/client/callbacks.md。ℹ️连接模式说明Elicitation 是服务端到客户端的请求这类请求只存在于经典握手classic-handshake的会话中所以示例客户端传入modelegacy。在2026-07-28连接上工具改为从调用中返回提问那个流程见 docs/handlers/multi-round-trip.md。底层的能力声明检查在 src/mcp/server/mcpserver/resolve.py 的_require_capability()中客户端未声明elicitation能力时服务端抛出MISSING_REQUIRED_CLIENT_CAPABILITY错误。动手实验跑通一次完整的往返按以下步骤即可在本地验证完整流程运行服务端的一行命令见 docs/run/index.md用表单模式启动server.py即book_table那个Streamable HTTP 方式再运行客户端的main()向book_table请求圣诞日2025-12-25。回调会打印收到的提问No tables for 2 on 2025-12-25. Would you like to try another date?回调以{accept_alternative: True, date: 2025-12-27}应答而工具这段时间一直等在await ctx.elicit(...)内部随后完成预订Booked a table for 2 on 2025-12-27.换成 URL 模式的server.py让同一个main()调用pay_deposit同一个回调走另一分支打印支付链接工具带着Complete the payment in your browser.返回。一次往返发生在调用中途双向皆是如此。反例客户端不注册回调会怎样现在把Client的elicitation_callback去掉再对圣诞日调用book_table。整个调用以协议错误失败Elicitation not supported没注册任何回调的客户端从未声明elicitation能力所以没有人可问。你的工具收到的不是decline而是异常测试 tests/docs_src/test_elicitation.py 用pytest.raises(MCPError, matchElicitation not supported)验证。请为此做设计每次引导都需要一个对如果我问不了怎么办的合理回答。总结参数标注为Annotated[T, Resolve(fn)]即由 resolver 填充resolver 需要提问时返回Elicit(...)它在所有连接上可用。Schema 是扁平的 Pydantic 模型只支持原始字段返回时会被校验。result.action是accept、decline或cancelresult.data只在 accept 时存在。await ctx.elicit(message, schemaModel)从工具函数体内部提问await ctx.elicit_url(message, url, elicitation_id)用于一切不该经过模型的数据ctx.session.send_elicit_complete(elicitation_id)通知带外部分已完成。两者都是服务端到客户端的请求需要客户端位于 legacy 连接上。客户端用一个elicitation_callback应答按 params 类型分支注册它就是声明能力。在 2026-07-28 连接上服务端改为返回提问而非推送同一个回调由 docs/handlers/multi-round-trip.md 描述的流程驱动。这个返回行为之下的全部细节——重试循环、保护requestState、自行驱动流程——都在 docs/handlers/multi-round-trip.md 中。若想从声明式提问深入到依赖注入的一般机制不提问的计算型依赖、依赖的依赖、模型能提供什么不能提供什么请阅读 docs/handlers/dependencies.md。赞分享人工智能MCP 服务MCP Clients【免费下载链接】python-sdkThe official Python SDK for Model Context Protocol servers and clients项目地址https://gitcode.com/gh_mirrors/pythonsd/python-sdk点击查看免费下载相关推荐MCP Python SDK 中的 Elicitation征询机制工具中途提问的两种模式与客户端回调实现MCP Python SDK 中的 Elicitation征询机制工具中途提问的两种模式与客户端回调实现 Elicitation征询是 MCPMod人工智能MCP 服务MCP Clientspython-sdk 的 Elicitation 指南在 MCP 工具调用中途向用户提问Resolver、表单模式与 URL 模式python sdk 的 Elicitation 指南在 MCP 工具调用中途向用户提问Resolver、表单模式与 URL 模式 导读 本文讲解 py人工智能MCP 服务MCP Clientspython-sdk 中的 Elicitation 机制让 MCP 工具在调用中途向用户提问python sdk 中的 Elicitation 机制让 MCP 工具在调用中途向用户提问 导读 本文围绕官方 Python SDKModel Conte人工智能MCP 服务MCP Clients创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表