Roots 与 Elicitation:作用域与执行中的用户输入 本节摘要:硬编码路径在用户打开另一个项目的那一刻就崩了;预填的工具参数在用户没说全的那一刻就崩了。Roots 把服务端的作用域限定在用户控制的 URI 集合;Elicitation 在工具调用中途暂停,通过表单或 URL 向用户征询结构化输入。两个客户端原语,修掉 MCP 的两类常见失败模式。SEP-1036(URL 模式 elicitation,2025-11-25)在 2026 上半年仍是实验性的——依赖前先查 SDK 版本。 学习目标 阅读完本节,你应当能够: 声明 并响应 。 把服务端的文件操作限制在声明的 root 集合内。 用 在工具调用中途向用户征询确认或结构化输入。
本节摘要:硬编码路径在用户打开另一个项目的那一刻就崩了;预填的工具参数在用户没说全的那一刻就崩了。Roots 把服务端的作用域限定在用户控制的 URI 集合;Elicitation 在工具调用中途暂停,通过表单或 URL 向用户征询结构化输入。两个客户端原语,修掉 MCP 的两类常见失败模式。SEP-1036(URL 模式 elicitation,2025-11-25)在 2026 上半年仍是实验性的——依赖前先查 SDK 版本。
阅读完本节,你应当能够:
roots 并响应 notifications/roots/list_changed。elicitation/create 在工具调用中途向用户征询确认或结构化输入。一个笔记 MCP 服务端在生产里撞上两类具体失败:
~/notes。另一台机器笔记在 ~/Documents/Notes 的用户,得到一个静默失败(找不到文件)或更糟(写错地方)的工具调用。notes_delete(title:"TPS report"),但有 2023、2024、2025 三条匹配。工具不能猜——报「歧义」很烦,三条都删是灾难。Roots 修第一个:客户端在 initialize 声明服务端可触碰的 URI 集合。Elicitation 修第二个:服务端暂停工具调用,发 elicitation/create 请用户挑一个。
客户端在 initialize 声明 root 列表:
{"capabilities": {"roots": {"listChanged": true}}}
服务端随后可调 roots/list:
{"roots": [{"uri": "file:///Users/alice/Documents/Notes", "name": "Notes"}]}
服务端必须把 roots 当作边界:任何在 root 集合外的文件读写都被拒绝。这不是客户端强制的(服务端仍是用户信任的代码),但合规服务端都会遵守。
用户增删 root 时,客户端发 notifications/roots/list_changed;服务端重调 roots/list 更新边界。
Roots 由客户端声明,因为它们代表用户的同意模型。用户告诉 Claude Desktop「给这个笔记服务端访问这两个目录的权限」。服务端无法自行扩大这个范围。
elicitation/create 取一个表单 schema 加自然语言提示:
{ "method": "elicitation/create", "params": { "message": "删除『TPS 报告』?有多条匹配,挑一个。", "requestedSchema": { "type": "object", "properties": { "note_id": {"type": "string", "enum": ["note-3", "note-7", "note-14"]}, "confirm": {"type": "boolean"} }, "required": ["note_id", "confirm"] } } }
客户端渲染表单、收集用户答案、返回:
{"action": "accept", "content": {"note_id": "note-14", "confirm": true}}
三种可能的 action:accept(用户填了)、decline(用户关了)、cancel(用户中止了整个工具调用)。
表单 schema 是扁平的——v1 不支持嵌套对象。SDK 通常会拒绝任何比单层更复杂的东西。
2025-11-25 新增。服务端不发 schema,而是发一个 URL:
{ "method": "elicitation/create", "params": { "message": "登录 GitHub", "url": "https://github.com/login/oauth/authorize?client_id=..." } }
客户端在浏览器开 URL,等完成,用户回来时返回。适合 OAuth 流、支付授权、文档签署等表单不够用的场景。
⚠️ 漂移提示:SEP-1036 的响应形状仍在收敛;有的 SDK 返回回调 URL,有的返回完成 token。生产用 URL 模式前读你的 SDK release notes。
Elicitation 与 sampling 一起,构成 MCP 的「人在回路」模型。服务端的 Agent 循环可暂停等待用户输入(elicitation)或模型推理(sampling)。第 11 节讲了 sampling,本节讲 elicitation。两者合用,实现完整的中途循环控制。
| 维度 | Roots | Elicitation(表单) | Elicitation(URL) |
|---|---|---|---|
| 谁发起 | 客户端声明 | 服务端 mid-call | 服务端 mid-call |
| 解决 | 作用域/路径 | 消歧/确认 | OAuth/支付/签署 |
| 状态(2026) | 标准活跃 | 标准活跃 | 实验性(SEP-1036) |
| 用户中断 | 不中断 | 中断对话 | 中断,跳浏览器 |
💡 心法:Roots 是「同意边界」,elicit 是「中途打断问用户」。两者都该克制使用——频繁打断对话的 elicitation 是糟糕 UX。
本节产出 outputs/skill-elicitation-form-designer.md——给定一个可能需要用户确认或消歧的工具,它设计 elicitation 表单 schema 与消息模板。
code/main.py 把笔记服务端扩展为:roots/list 响应(root-list-changed 后重查);一个 notes_delete 工具,多条匹配时用 elicitation/create 消歧;一个 notes_setup 工具,用 URL 模式 elicitation 打开首次配置页(模拟);一个边界检查,拒绝声明 roots 外的 URI 操作。demo 跑三个场景:happy path(一条匹配)、消歧(三条匹配,elicitation 触发)、root 外写入(被拒)。
触发消歧:运行 code/main.py,触发消歧路径,确认模拟用户答案被路由回工具。
加强制确认:加一个 notes_archive 工具,每次都要求 elicitation 确认(destructive hint)。对比 UX:这比模型用文本再问一次如何?
实现 URL OAuth:为首次运行 OAuth 流实现 URL 模式 elicitation,加 SDK 版本守卫(漂移风险)。
原子重读:扩展 roots/list 处理——通知到达时,服务端应原子地重读并重扫可能已出作用域的打开文件句柄。
读 SEP 讨论:读 GitHub 上 SEP-1036 的 issue 讨论线,找出一个影响服务端如何处理 URL 模式回调的开放问题。
initialize 声明可触碰 URI,服务端必须把 root 外操作当越界拒绝。notifications/roots/list_changed。requestedSchema + 消息,客户端渲染表单;accept/decline/cancel 三分支。下一节,我们看 MCP 的异步任务——让长跑工作即使跨越会话重连也能存活,以及 SEP-1686 带来的 task 生命周期。