7.2 两种模式:表单与 URL 本节摘要:本节讲引导填写的两种实现模式——表单模式( 带 Schema,宿主弹结构化表单)与 URL 模式( ,宿主跳转外部页面)。两者都是「把用户引导到一个界面,拿到回答再继续」,但适用场景不同:表单适合简单确认与填几个字段,URL 适合 OAuth、富交互、复用现有 Web 流程。本节讲透两种模式的写法、何时用哪个,为下一节(现代协议下的 MRTR 实现)做好铺垫。 一、表单模式:结构化表单 表单模式让宿主弹出一个结构化表单,你用一个 Schema 描述「要问什么、什么类型」: 接受: :要问的问题文本 :表单结构(JSON Schema,描述字段与类型) 返回的 是用户填的答案(字典)。
本节摘要:本节讲引导填写的两种实现模式——表单模式(
elicit带 Schema,宿主弹结构化表单)与 URL 模式(elicit_url,宿主跳转外部页面)。两者都是「把用户引导到一个界面,拿到回答再继续」,但适用场景不同:表单适合简单确认与填几个字段,URL 适合 OAuth、富交互、复用现有 Web 流程。本节讲透两种模式的写法、何时用哪个,为下一节(现代协议下的 MRTR 实现)做好铺垫。
表单模式让宿主弹出一个结构化表单,你用一个 Schema 描述「要问什么、什么类型」:
from mcp.server.mcpserver import Context @mcp.tool() async def transfer(from_id: str, to_id: str, amount: float, ctx: Context) -> bool: """Transfer money, asking confirmation for large amounts.""" if amount > 1000: # 中途反问:用 Schema 描述表单 result = await ctx.elicit( message=f"转账金额 {amount} 较大,确认执行吗?", schema={ "confirm": {"type": "boolean", "description": "确认转账"}, "note": {"type": "string", "description": "备注(可选)"} } ) if not result.data["confirm"]: raise PermissionError("用户取消了转账") return do_transfer(from_id, to_id, amount)
ctx.elicit 接受:
message:要问的问题文本schema:表单结构(JSON Schema,描述字段与类型)返回的 result.data 是用户填的答案(字典)。宿主据 schema 渲染表单(布尔变复选框、字符串变文本框),用户填完提交,答案回传。
表单模式的特点:
| 特点 | 说明 |
|---|---|
| 结构化 | 用 Schema 描述,宿主自动渲染表单 |
| 适合简单交互 | 确认、填几个字段、选选项 |
| 体验一致 | 跨宿主都用宿主原生 UI |
| 无需外部页面 | 一切在宿主界面内完成 |
URL 模式让宿主跳转一个外部页面,用户在那里完成交互后回跳:
@mcp.tool() async def link_account(provider: str, ctx: Context) -> str: """Link an external account via OAuth.""" # 中途反问:跳转到 OAuth 授权页 result = await ctx.elicit_url( message=f"请在页面完成 {provider} 授权", url=f"https://auth.example.com/authorize?provider={provider}" ) if result.status != "completed": raise PermissionError("授权未完成") return result.data.get("account_id", "")
ctx.elicit_url 接受:
message:提示文本url:要跳转的 URL用户在浏览器打开 url,在那里完成交互(如 OAuth 授权),完成后页面回跳,宿主拿到结果。
URL 模式的特点:
| 特点 | 说明 |
|---|---|
| 富交互 | 外部页面可任意复杂(OAuth、富表单、多步流程) |
| 复用现有 Web | 不用重写已有 Web 流程 |
| 适合复杂场景 | OAuth、第三方支付、SSO |
| 体验离开宿主 | 用户要去浏览器/外部页面 |
把两种模式放一起对照,差异清晰:
| 维度 | 表单模式(elicit) |
URL 模式(elicit_url) |
|---|---|---|
| 交互位置 | 宿主界面内 | 外部浏览器页面 |
| 描述方式 | JSON Schema | 一个 URL |
| 渲染 | 宿主自动 | 外部页面自己渲染 |
| 适合 | 简单确认、填字段 | OAuth、富交互、复用 Web |
| 体验 | 原生、一致 | 离开宿主、跳转 |
| 实现成本 | 低(写 Schema) | 中(要有外部页面) |
判断口诀:
问:这个交互能在宿主界面内用简单表单完成吗? 能 → 表单模式 不能(需要 OAuth/富交互/复用 Web)→ URL 模式
表单模式适合「宿主内能搞定的简单交互」:
| 场景 | Schema | 说明 |
|---|---|---|
| 二次确认 | {confirm: boolean} |
一个复选框 |
| 填验证码 | {code: string} |
一个文本框 |
| 选环境 | {env: enum(["dev","prod"])} |
一个下拉框 |
| 补邮箱 | {email: string} |
一个文本框 |
| 多字段补充 | {name, email, phone} |
几个文本框 |
共同点:字段少、类型简单、宿主原生 UI 能渲染。这种场景用表单模式最顺,体验也最好(用户不离开宿主)。
URL 模式适合「需要外部页面才能完成」的复杂交互:
| 场景 | 为什么用 URL |
|---|---|
| OAuth 授权 | 授权必须在 provider 的页面完成 |
| 第三方支付 | 支付页面由支付商提供 |
| SSO 登录 | 身份验证在 IdP 页面 |
| 复杂多步表单 | 已有 Web 表单,不想重写 |
| 富交互(拖拽、上传) | 宿主表单能力不够 |
共同点:交互复杂度超出宿主表单能力,或必须在外部页面完成。
💡 技巧:URL 模式的前提是你有一个外部页面。如果你只是想问个简单问题,别为了「炫」用 URL——表单模式更快、体验更好。URL 是「表单搞不定时」的选择,不是首选。
两种模式都返回一个结果对象,但要处理「用户没完成」的情况:
# 表单模式 result = await ctx.elicit(message="...", schema={...}) if result.action == "cancel": # 用户取消 raise PermissionError("用户取消") data = result.data # 用户填的答案 # URL 模式 result = await ctx.elicit_url(message="...", url="...") if result.status != "completed": # 用户没完成 raise PermissionError("未完成") data = result.data # 回跳带回的数据
关键实践:永远处理「用户取消/未完成」的情况。别假设用户一定会填——用户可能关掉表单、可能不授权。不处理这些情况,工具会卡住或基于空数据继续,导致问题。
这里点一个下一节会详讲的差异——两种模式在协议层的实现不同:
表单模式(ctx.elicit): - legacy 协议:服务端直接发起请求 - 现代协议(2026-07-28):靠 Resolve 返回 Elicit + MRTR(或受限支持) URL 模式(elicit_url): - 主要靠跳转 + 回调,协议层相对简单 - 在两种协议下都可用
这个差异解释了为什么第 7.3 节要专门讲「现代路径」——表单模式在 2026-07-28 下的实现变了,而 URL 模式相对稳定。但写代码时,SDK 替你处理了协议差异,你只需关心「用表单还是 URL」这个业务选择。
ctx.elicit):用 Schema 描述,宿主弹原生表单,适合简单确认/填字段。ctx.elicit_url):跳转外部页面,适合 OAuth、富交互、复用 Web。两种模式清楚了,下一节讲 v2 最微妙的变化——现代协议下引导填写靠 Resolve + 多轮往返(MRTR)。