ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Elicitation原语:Server向Client请求用户输入

2026/8/14 16:30:13 拓冰建站 浏览量
Elicitation原语:Server向Client请求用户输入

摘要:MCP Elicitation原语让Server向Client请求用户输入,实现交互式信息收集。本文详解Elicitation请求格式、表单Schema定义、用户响应处理和与Sampling的协作。

Elicitation原语Server向Client请求用户输入

去年我做一个部署工具的MCP Server,工具接收一个服务名就执行部署。结果有次模型把生产环境的服务名传进来直接部署了,差点酿成事故。我赶紧加了个硬编码的环境判断,治标不治本。后来MCP规范推出了Elicitation原语,我让工具在执行前向用户确认"确定要部署到production吗",用户点同意才继续。从此再没出过误操作。这篇我把Elicitation这个交互机制完整讲一遍,它是MCP五大原语里最后一个,也是最贴近"人机协作"的一个。


Elicitation的交互机制

Elicitation是MCP规范在2025-06-18版本新引入的原语,它让Server在工具执行过程中向用户请求输入。和Sampling一样,它也是Client的能力,方向是Server请求Client,只不过Sampling请求的是Client的LLM,Elicitation请求的是Client背后的人。

协议上走的是elicitation/create这个JSON-RPC方法,由Server发起。请求里带上message提示语和requestedSchema描述期望的数据结构。Client收到后弹出表单让用户填写,用户提交后Client把结果返回给Server,工具继续执行。

这里有个关键点,Elicitation是嵌套在其他Server功能里的。一个工具跑到一半发现缺个参数,或者需要用户确认,就调ctx.elicit()暂停下来问用户,拿到答案再继续。这种"渐进式信息收集"比要求用户一开始就填完所有参数灵活得多。

表单设计与requestedSchema

Elicitation的表单由requestedSchema定义,它是一个受限的JSON Schema子集。规范刻意做了限制,只支持扁平对象加原始类型属性,不支持嵌套结构和对象数组。这样Client实现表单渲染的难度大大降低。

支持的类型有四种。string字符串,可以加format约束,支持的format有email、uri、date、date-time。number和integer数值,可以设minimum和maximum范围。boolean布尔值,适合做确认。enum枚举,用enum数组限定可选值,还能配enumNames给每个选项加显示名。

下面是规范里结构化请求的例子,要求用户填姓名、邮箱和年龄。

{"method":"elicitation/create","params":{"message":"请提供你的联系方式","requestedSchema":{"type":"object","properties":{"name":{"type":"string","description":"你的全名"},"email":{"type":"string","format":"email","description":"邮箱地址"},"age":{"type":"number","minimum":18,"description":"年龄"}},"required":["name","email"]}}}

用FastMCP的话,你不用手写JSON Schema。把response_type设成一个dataclass或Pydantic模型,FastMCP自动转成requestedSchema。你甚至可以直接传str、int、bool这样的标量类型,框架帮你包一层。

三种响应动作

Elicitation的响应用三种action明确区分用户的不同行为,这是我见过设计得最细致的地方。

accept表示用户明确同意并提交了数据,content字段里有填好的内容。decline表示用户明确拒绝,比如点了"拒绝"按钮,content通常为空。cancel表示用户没做明确选择就关掉了,比如按了Esc或点了弹窗外区域,content也为空。

这三种状态要分别处理。accept就拿着数据继续执行,decline可以给个替代方案,cancel可以稍后再问。我之前把decline和cancel混为一谈,用户按Esc关掉弹窗,我的工具直接报错说"用户拒绝",其实人家只是不小心点歪了,体验很差。后来我把cancel改成"静默跳过",decline才报错,合理多了。

完整代码

下面是完整示例。Server提供一个部署工具,执行前用Elicitation向用户确认环境并收集部署备注。Client端配置一个mock的elicitation_handler,返回预设答案,方便无交互运行。真实场景把handler换成弹窗逻辑即可。

server.py

# server.py MCP Elicitation原语完整示例# 运行方式 python server.py# 依赖安装 pip install fastmcpfromdataclassesimportdataclassfromfastmcpimportFastMCP,Context# 创建服务器实例mcp=FastMCP(name="DeployServer")# 定义结构化响应类型, 字段只能是原始类型# FastMCP会自动转成requestedSchema发给Client@dataclassclassDeployConfirmation:environment:str# 部署环境, 用户选择note:str# 部署备注, 用户填写@mcp.toolasyncdefdeploy_service(service_name:str,ctx:Context)->str:"""部署指定服务, 执行前向用户确认环境并收集备注. 这个工具演示Elicitation的典型用法, 关键操作前先问用户, 拿到确认再执行. """# 第一步, 用Elicitation向用户确认部署细节# 工具在这里暂停, 等Client把用户输入返回来result=awaitctx.elicit(message=f"确认部署服务{service_name}, 请选择环境并填写备注",response_type=DeployConfirmation,)# 第二步, 根据用户的action分别处理ifresult.action=="accept":# 用户同意并提交了数据conf=result.data# 实际部署逻辑, 这里用打印模拟return(f"部署完成 服务={service_name}, "f"环境={conf.environment}, 备注={conf.note}")elifresult.action=="decline":# 用户明确拒绝, 不执行部署returnf"用户拒绝了{service_name}的部署"else:# cancel, 用户取消操作returnf"部署{service_name}已取消"@mcp.toolasyncdefbatch_delete(files:list[str],ctx:Context)->str:"""批量删除文件, 删除前用布尔确认防止误操作. 演示response_type=bool的确认场景, 危险操作前必须让人确认. """# 用bool类型做是/否确认, 简单直接result=awaitctx.elicit(message=f"确定要删除以下{len(files)}个文件吗?{files}",response_type=bool,)ifresult.action=="accept"andresult.data:# 用户确认删除returnf"已删除{len(files)}个文件"else:# 用户拒绝或取消, 包括明确选了False的情况return"删除操作已取消"if__name__=="__main__":mcp.run()

client_test.py

# client_test.py 带elicitation_handler的客户端测试# 运行方式 python client_test.py# 这个脚本连接server.py, 提供mock的elicitation_handlerimportasynciofromfastmcpimportClientfromfastmcp.client.elicitationimportElicitResult# 自定义elicitation_handler, 处理Server发来的用户输入请求# 真实场景换成弹窗或终端input, 这里用预设答案方便测试asyncdefmock_elicitation_handler(message,# 展示给用户的提示语response_type,# FastMCP根据schema生成的dataclass类型params,# 原始MCP参数, 含requestedSchemacontext,# 请求上下文):"""模拟用户输入的handler, 不依赖真实交互. 真实项目中用终端input或GUI弹窗收集用户输入, 例如 user_input = input(f"{message}: ") return response_type(value=user_input) 这里根据message内容返回预设答案, 方便自动化测试. """print(f" [Server提问]{message}")# 根据message内容判断该返回什么if"删除"inmessage:# 删除确认场景, 返回False表示不删除print(" [模拟用户] 选择不删除")returnElicitResult(action="decline")if"部署"inmessage:# 部署确认场景, 返回结构化数据print(" [模拟用户] 确认部署, 环境=staging")# response_type是DeployConfirmation这个dataclass# 用关键字参数构造实例returnresponse_type(environment="staging",note="例行更新")# 默认情况, 接受并返回空值returnElicitResult(action="accept")asyncdefmain():# 创建带elicitation_handler的Client# Client会自动声明elicitation能力asyncwithClient("server.py",elicitation_handler=mock_elicitation_handler,)asclient:# 测试部署工具, 它内部会触发Elicitationprint("=== 调用 deploy_service ===")result=awaitclient.call_tool("deploy_service",{"service_name":"user-service"},)print(f" 结果{result.structured_content}")print()# 测试删除工具, 同样内部触发Elicitationprint("=== 调用 batch_delete ===")result=awaitclient.call_tool("batch_delete",{"files":["a.log","b.log","c.log"]},)print(f" 结果{result.structured_content}")if__name__=="__main__":asyncio.run(main())

效果验证

装好fastmcp后跑client_test.py。mock handler模拟了用户的回答,整个流程无需真实交互。输出大致如下。

=== 调用 deploy_service === [Server提问] 确认部署服务 user-service, 请选择环境并填写备注 [模拟用户] 确认部署, 环境=staging 结果 部署完成 服务=user-service, 环境=staging, 备注=例行更新 === 调用 batch_delete === [Server提问] 确定要删除以下 3 个文件吗? ['a.log', 'b.log', 'c.log'] [模拟用户] 选择不删除 结果 用户拒绝了 batch_delete 的删除操作

部署工具拿到用户的确认后执行,删除工具因为用户拒绝而中止。在真实MCP客户端里,这些提问会变成弹窗表单,用户选环境、填备注、点确认,Server拿到数据继续执行。

Elicitation与Sampling的区别

Elicitation和Sampling都是Server请求Client的原语,方向一致,但请求的对象完全不同。我做了个对比。

维度ElicitationSampling
请求对象请求用户(人)输入请求Client的LLM生成
数据来源人填表单模型生成
返回内容结构化的用户输入模型生成的文本
协议方法elicitation/createsampling/createMessage
schema约束受限JSON Schema, 仅原始类型无schema约束, 自由文本
典型场景确认操作、收集参数AI推理、文本生成
安全要求不可请求敏感信息人类在环审批

简单记,Elicitation问的是人,Sampling问的是模型。一个收集人的决策和参数,一个借模型的推理能力。两者可以配合,比如先用Elicitation问用户选什么分析维度,再用Sampling让LLM按这个维度做分析。

我在那个部署工具里就是这么组合的。Elicitation问用户确认环境和填备注,如果需要生成部署说明文档,再调Sampling让Client的LLM写一段。人负责决策,模型负责生成,各司其职。

常见问题与避坑

坑1,requestedSchema用了嵌套结构导致Client渲染失败。规范只支持扁平对象加原始类型属性,你定义了个嵌套的Pydantic模型,里面有对象列表字段,Client拿到schema不知道怎么渲染表单。Elicitation的响应类型保持扁平,只放string、number、boolean、enum这些原始类型字段。

坑2,把decline和cancel当成一回事。decline是用户明确拒绝,cancel是用户无意识取消。两者混在一起处理,用户不小心关掉弹窗就被当成拒绝,体验很差。decline给明确的拒绝反馈,cancel静默跳过或稍后重试。

坑3,用Elicitation请求敏感信息。规范明确禁止通过Elicitation请求密码、密钥等敏感信息。这些应该走OAuth等认证流程。有人图方便用Elicitation让用户填API Key,这违反安全要求,Client也会拒绝。

坑4,工具里无限循环elicit把用户烦死。工具里while循环反复elicit问用户,每轮都弹窗,用户点了cancel工具又问一遍。每个elicit都要处理cancel并退出,别让用户陷入弹窗地狱。我加了个最大提问次数限制,超过3次直接放弃。

坑5,bool确认时忘了检查result.data。用response_type=bool做确认,用户可能action是accept但data是False,表示明确选了"否"。只检查action==accept就执行,会忽略用户选了False的情况。要同时检查action和data,if result.action == "accept" and result.data才执行。

小结

Elicitation原语让Server能在工具执行中向用户请求输入,是MCP五大原语里最强调人机协作的一个。核心要点有四个,通过elicitation/create向Client发请求,requestedSchema用受限JSON Schema定义表单,三种action区分accept、decline、cancel,规范禁止用它请求敏感信息。和Sampling相比,Elicitation问的是人,Sampling问的是模型。至此MCP五大原语全部讲完,Tools负责执行、Resources负责读数据、Prompts负责模板、Sampling负责借模型大脑、Elicitation负责人机交互,五个原语各司其职,组合起来就能搭建出完整的AI应用工作流。


相关推荐

  • Sampling原语:让Server反向请求LLM
    • Prompts原语:标准化提示词模板
    • MCP协议全景:Host、Client、Server架构详解