[FastMCP设计、原理与应用-18]MCP Elicitation——构建双向反馈的Agent
Elicitation是我们可以在工具执行期间请求用户的结构化输入。它允许工具无需预先要求所有输入而是可以根据需要交互式地询问缺失的参数、澄清说明或补充上下文信息。启发功能使工具能够暂停执行并向用户请求特定信息如下是几种典型的应用场景参数缺失询问用户是否提供了初始未提供的必要信息澄清请求针对模糊不清的情况获取用户的确认或选择渐进披露逐步收集复杂信息动态工作流程根据用户响应调整工具行为1. 利用Elicitation收集个人信息如下程序演示了工具collect_user_info在执行过程中利用Elicitation向客户端收集用于信息。fromfastmcpimportFastMCP,Contextfromfastmcp.clientimportClientfrommcp.typesimportElicitRequestParamsfrommcp.shared.contextimportRequestContextfromdataclassesimportdataclassimportasyncio serverFastMCP(Server)dataclassclassUserInfo:name:strage:intserver.toolasyncdefcollect_user_info(ctx:Context)-str:resultawaitctx.elicit(messagePlease provide your information,response_typeUserInfo)ifresult.actionaccept:userresult.datareturnfHello{user.name}, you are{user.age}years old.elifresult.actiondecline:returnInformation not providedelse:returnOperation cancelledasyncdefhandle_elicitation(message:str,response_type:type|None,params:ElicitRequestParams,context:RequestContext,)-UserInfo:returnUserInfo(nameJayden,age18)asyncdefmain():asyncwithClient(server,elicitation_handlerhandle_elicitation)asclient:resultawaitclient.call_tool(namecollect_user_info)assertresult.content[0].textHello Jayden, you are 18 years old.# type: ignoreasyncio.run(main())2. Context的elicit方法工具执行过程中可以通过调用Context如下这些重载的elicit方法请求客户端获取结构化的输入。我们可以利用message参数自定义一段请求文本消息response_type参数则用于指定承载结构化输入Schema的类型。TTypeVar(T,defaultAny)ResultTTypeVar(ResultT,defaultstr)dataclassclassContext:overloadasyncdefelicit(self,message:str,response_type:None,)-(AcceptedElicitation[dict[str,Any]]|DeclinedElicitation|CancelledElicitation):...overloadasyncdefelicit(self,message:str,response_type:type[T],)-AcceptedElicitation[T]|DeclinedElicitation|CancelledElicitation:...overloadasyncdefelicit(self,message:str,response_type:list[str],)-AcceptedElicitation[str]|DeclinedElicitation|CancelledElicitation:...overloadasyncdefelicit(self,message:str,response_type:dict[str,dict[str,str]],)-AcceptedElicitation[str]|DeclinedElicitation|CancelledElicitation:...overloadasyncdefelicit(self,message:str,response_type:list[list[str]],)-(AcceptedElicitation[list[str]]|DeclinedElicitation|CancelledElicitation):...overloadasyncdefelicit(self,message:str,response_type:list[dict[str,dict[str,str]]],)-(AcceptedElicitation[list[str]]|DeclinedElicitation|CancelledElicitation):...asyncdefelicit(self,message:str,response_type:type[T]|list[str]|dict[str,dict[str,str]]|list[list[str]]|list[dict[str,dict[str,str]]]|NoneNone,)-(AcceptedElicitation[T]|AcceptedElicitation[dict[str,Any]]|AcceptedElicitation[str]|AcceptedElicitation[list[str]]|DeclinedElicitation|CancelledElicitation)3. 响应类型的设置服务器必须向客户端发送一个模式指示其期望在响应请求中返回的数据类型。MCP规范仅支持单一浅层JSON对象并要求其数据成员类型为string, number (or integer), boolean和enum这样的标量基元类型。FastMCP对此做了扩展并兼容MCP规范。3.1 None有时侯请求的目的仅仅是让用户批准或拒绝某个操作。将响应类型设置为None表示不需要数据。当用户接受操作时数据字段将为None。mcp.toolasyncdefapprove_action(ctx:Context)-str:resultawaitctx.elicit(Approve this action?,response_typeNone)ifresult.actionaccept:returndo_action()else:raiseValueError(Action rejected)3.2 标量类型我们可以请求简单的标量数据类型作为基本输入例如字符串、整数或布尔值。当请求标量类型时FastMCP会自动将其包装在对象模式中以兼容MCP规范。客户端将看到一个模式该模式请求一个指定类型的“值”字段。客户端响应后提供的对象将被“解包”并将标量值直接返回到数据字段中。mcp.toolasyncdefpick_a_number(ctx:Context)-str:resultawaitctx.elicit(Pick a number!,response_typeint)ifresult.actionaccept:returnfYou picked{result.data}returnNo number provided3.3 限制选项单选很多情况下并不希望用户做“选择题”所以我们可以指定字符串列表、Literal或者枚举的形式提供候选选项并让用户选择其中一个选项单选。mcp.toolasyncdefset_priority(ctx:Context)-str:resultawaitctx.elicit(What priority level?,response_type[low,medium,high],)ifresult.actionaccept:returnfPriority set to:{result.data}fromtypingimportLiteralmcp.toolasyncdefset_priority(ctx:Context)-str:resultawaitctx.elicit(What priority level?,response_typeLiteral[low,medium,high])ifresult.actionaccept:returnfPriority set to:{result.data}returnNo priority setfromenumimportEnumclassPriority(Enum):LOWlowMEDIUMmediumHIGHhighmcp.toolasyncdefset_priority(ctx:Context)-str:resultawaitctx.elicit(What priority level?,response_typePriority)ifresult.actionaccept:returnfPriority set to:{result.data.value}returnNo priority set3.4 限制选项多选如果需要用户做多项选择可以将响应类型设置为字符串列表的列表list[list[str]]或者枚举的列表。mcp.toolasyncdefselect_tags(ctx:Context)-str:resultawaitctx.elicit(Choose tags,response_type[[bug,feature,documentation]]# Note: list of a list)ifresult.actionaccept:tagsresult.datareturnfSelected tags:{, .join(tags)}fromenumimportEnumclassTag(Enum):BUGbugFEATUREfeatureDOCSdocumentationmcp.toolasyncdefselect_tags(ctx:Context)-str:resultawaitctx.elicit(Choose tags,response_typelist[Tag])ifresult.actionaccept:tags[tag.valuefortaginresult.data]returnfSelected:{, .join(tags)}3.5 限制选项带标题为了获得更好的用户界面显示效果可以为枚举选项提供易于理解的标题。FastMCP使用带有const和title字段的oneOf模式生成符合SEP-1330标准的模式。单选mcp.toolasyncdefset_priority(ctx:Context)-str:resultawaitctx.elicit(What priority level?,response_type{low:{title:Low Priority},medium:{title:Medium Priority},high:{title:High Priority}})ifresult.actionaccept:returnfPriority set to:{result.data}多选mcp.toolasyncdefselect_priorities(ctx:Context)-str:resultawaitctx.elicit(Choose priorities,response_type[{low:{title:Low Priority},medium:{title:Medium Priority},high:{title:High Priority}}])ifresult.actionaccept:returnfSelected:{, .join(result.data)}3.6 结构化响应使用dataclass、TypedDict或Pydantic模型作为响应类型请求包含多个字段的结构化数据。请注意MCP规范仅支持具有标量字符串、数字、布尔值或枚举属性的浅层对象。fromdataclassesimportdataclassfromtypingimportLiteraldataclassclassTaskDetails:title:strdescription:strpriority:Literal[low,medium,high]due_date:strmcp.toolasyncdefcreate_task(ctx:Context)-str:resultawaitctx.elicit(Please provide task details,response_typeTaskDetails)ifresult.actionaccept:taskresult.datareturnfCreated task:{task.title}(Priority:{task.priority})returnTask creation cancelled3.7 默认值使用Pydantic的Field(default...)为请求字段提供默认值。客户端将使用这些默认值预先填充表单字段。具有默认值的字段将自动标记为可选字段。frompydanticimportBaseModel,FieldfromenumimportEnumclassPriority(Enum):LOWlowMEDIUMmediumHIGHhighclassTaskDetails(BaseModel):title:strField(descriptionTask title)description:strField(default,descriptionTask description)priority:PriorityField(defaultPriority.MEDIUM,descriptionTask priority)mcp.toolasyncdefcreate_task(ctx:Context)-str:resultawaitctx.elicit(Please provide task details,response_typeTaskDetails)ifresult.actionaccept:returnfCreated:{result.data.title}returnTask creation cancelled4. Form Elicitation V.S. URL ElicitationElicitation是服务器向用户索取额外信息的机制。根据信息的敏感程度和交互方式它分为Form表单和URL两种模式Form模式这是一种 “在客户端内” 完成的交互方式。服务器发送一个JSON Schema给客户端客户端根据这个架构在聊天窗口或UI中直接渲染一个输入框或表单如单选、多选、文本框。这种模式用于收集非敏感的结构化信息URL模式这是一种需要 “跳转到外部” 完成的交互方式。服务器向客户端发送一个外部URL客户端引导用户在外部浏览器中打开该链接进行操作。这种模式用于处理高敏感度或必须绕过AI客户端的操作确保敏感数据如密码不会流经LLM或MCP 客户端。典型的例子包括OAuth授权、支付处理和用户凭证收集等Form是为了“在聊天里补全信息”而URL是为了“去外部安全地办成某件事”。URL模式是MCP协议在2025-11-25修订版SEP-1036中新引入的特性,FastMCP服务器并未提供支持。5. AcceptedElicitation、DeclinedElicitation和CancelledElicitation当客户端接收到服务端发送的Elicitation请求它有权利拒绝此请求。除此之外客户端很有可能置之不理或者做出的回复时已经超出最长的等待时间这种场景被视为“撤销Cacnel”该Elicitation请求。所以作为Elicitation请求的结果体现在AcceptedElicitation、DeclinedElicitation和CancelledElicitation这三个类型上它们分别表示针对Elicitation请求的接受、拒绝和撤销。这三个类型是对在MCP规范的实现由mcp库提供模块路径为mcp.server.elicitation。上述三个类型都是Pydantic模型都具有相同的字段actionAcceptedElicitation利用data字段承载用户提供的输入。ElicitSchemaModelTTypeVar(ElicitSchemaModelT,boundBaseModel)classAcceptedElicitation(BaseModel,Generic[ElicitSchemaModelT]):action:Literal[accept]acceptdata:ElicitSchemaModelTclassDeclinedElicitation(BaseModel):action:Literal[decline]declineclassCancelledElicitation(BaseModel):action:Literal[cancel]cancel由于AcceptedElicitation要求data字段必需是Pydantic模型对于FastMCP来说这个约束太强所以它没有使用这个类型而是重新定义了如下这个对data字段没有约束的AcceptedElicitation类型。由于标量结果并不符合MCP规范FastMCP将其封装成ScalarElicitationType对象来兼容MCP规范。TTypeVar(T,defaultAny)classAcceptedElicitation(BaseModel,Generic[T]):action:Literal[accept]acceptdata:TdataclassclassScalarElicitationType(Generic[T]):value:T6. 客户端针对Elicitation请求的处理客户端可以在创建Client时设置其elicitation_handler参数的方式来处理Elicitation请求。具体设置的是通过如下这个ElicitationHandler类型表示的异步可执行对象它的四个输入参数分别表示请求的文本消息、响应类型、ElicitRequestParams和表示请求上下文服务端向客户端的请求。ElicitRequestParams是ElicitRequestURLParams和ElicitRequestFormParams类型的联合分别表示上面介绍的基于From和URL模式的Elicitation请求。处理器返回一个的ElicitResult对象最终会被Context的elicitate方法转换成对应的Elicitation类型AcceptedElicitation、DeclinedElicitation、CancelledElicitation或者ScalarElicitationType。ElicitationHandler:TypeAliasCallable[[str,# messagetype[T]|None,# a class for creating a structured response (None for URL elicitation)ElicitRequestParams,RequestContext[ClientSession,LifespanContextT],],Awaitable[T|dict[str,Any]|ElicitResult[T|dict[str,Any]]],]ElicitRequestParams:TypeAliasElicitRequestURLParams|ElicitRequestFormParamsclassElicitResult(Result):action:Literal[accept,decline,cancel]content:dict[str,str|int|float|bool|list[str]|None]|NoneNoneclassElicitRequestFormParams(RequestParams):mode:Literal[form]formmessage:strmodel_configConfigDict(extraallow)classElicitRequestURLParams(RequestParams):mode:Literal[url]urlmessage:strurl:strelicitationId:strmodel_configConfigDict(extraallow)