MCP Elicitation:Server 的结构化中途输入
当 MCP Server 执行复杂任务(如填写多步表单或跳转浏览器认证)而缺少必要输入时,它可以通过 Elicitation 机制反向向用户发起请求。这并不是简单的对话询问,而是一套严谨的协议化交互。
核心概念
MCP Elicitation 本质上是 MCP Server 暂时借用 Claude Code 的输入栈,向用户发起的一次受 Schema 约束的中途问询 (Structured Mid-run Inquiry)。
它解决的问题是:Server 在执行过程中由于某些原因(权限、输入缺失、需要跳转)无法继续,必须由人类介入并提供符合 JSON Schema 规范的数据。Claude Code 在此扮演了“交互桥梁”的角色,它将 Server 的原始 JSON 请求渲染成当前会话中的对话框或表单。
源码级拆解
整个流程由 Elicitation Handler 进行中枢调度,并与 UI 层的对话框深度绑定:
协议入口 (src/services/mcp/elicitationHandler.ts): 只有在 MCP Client 协商时声明了
elicitation能力,Claude Code 才会注册ElicitRequestSchema。当 Server 抛出该请求时,Handler 首先会运行预设的 Hooks(如runElicitationHooks),尝试自动化响应,如果 Hooks 未处理,才进入交互流程。交互模式 (src/components/mcp/ElicitationDialog.tsx):
- 表单模式 (Form Mode):Handler 根据 Server 提供的
requestedSchema动态生成字段。用户输入后,由src/utils/mcp/elicitationValidation.ts进行校验。这确保了输入数据在返回 Server 前就是合法的。 - URL 模式 (URL Mode):Server 发送一个 URL 让用户在浏览器中打开。这通常伴随着一个两阶段过程:用户点击“接受”后,Claude 会立刻向 Server 发送
accept响应,并进入等待状态(Waiting Phase),直到收到来自 Server 的ElicitationCompleteNotification。
- 表单模式 (Form Mode):Handler 根据 Server 提供的
任务队列与 REPL 调度 (src/screens/REPL.tsx): Elicitation 请求会被封装成事件并推入
elicitation.queue。REPL 屏幕会根据队列头部渲染出对应的ElicitationDialog。这意味着 Elicitation 请求具有“焦点夺取”特性,它会暂时阻塞正常的对话流,直到该请求被解决或取消。非交互式分发 (src/cli/structuredIO.ts): 如果 Claude Code 运行在 SDK 或集成环境下,Handler 会将请求通过 Control Protocol 转发给宿主程序(Host),而不是直接在本地弹窗。
踩坑指南
- 非权限系统:Elicitation 解决的是“Server 缺数据”,而不是“Claude 缺执行某个工具的权限”。虽然它们都会弹窗,但底层的协议路径完全不同。
- URL 模式的异步性:点击浏览器跳转后的“Accept”并不意味着交互结束。如果 Server 没有发送对应的“Complete”通知,该交互项可能会在 REPL 队列中挂起。
- Hooks 的优先级:你可以通过编写
Elicitation/ElicitationResultHooks 来绕过频繁的表单弹窗。这是实现 MCP 自动化高级场景的核心。 - 输入合法性:本地校验(Validation)遵循 Server 提供的原始 Schema。如果校验失败,Claude 会在本地阻断提交,减少无效的网络往返。
延伸阅读
- 如果你想了解 URL 模式背后更复杂的登录流程,请看 MCP 认证与 OAuth 流程。
- 如果你关插件的底层通信机制,请看 MCP 协议与客户端架构。
- 如果你对 Claude Code 的 REPL 组件感兴趣,请看 全屏渲染:Ink 布局与状态同步。
源码锚点
claude-code-opensource/src/services/mcp/elicitationHandler.ts— Elicitation 核心分发与 Hooks 逻辑。
📄 src/services/mcp/elicitationHandler.ts — Elicitation 核心分发与 Hooks 逻辑。
ElicitationCompleteNotificationSchema,
type ElicitRequestParams,
ElicitRequestSchema,
type ElicitResult,
} from '@modelcontextprotocol/sdk/types.js'
import type { AppState } from '../../state/AppState.js'claude-code-opensource/src/components/mcp/ElicitationDialog.tsx— 表单与 URL 交互的具体界面实现。
📄 src/components/mcp/ElicitationDialog.tsx — 表单与 URL 交互的具体界面实现。
/** Called when the phase 2 waiting state is dismissed (URL elicitations only). */
onWaitingDismiss?: (action: 'dismiss' | 'retry' | 'cancel') => void;
};
const isTextField = (s: PrimitiveSchemaDefinition) => ['string', 'number', 'integer'].includes(s.type);
const RESOLVING_SPINNER_CHARS = '\u280B\u2819\u2839\u2838\u283C\u2834\u2826\u2827\u2807\u280F';
const advanceSpinnerFrame = (f: number) => (f + 1) % RESOLVING_SPINNER_CHARS.length;
/** Timer callback for enumTypeaheadRef — module-scope to avoid closure capture. */
function resetTypeahead(ta: {claude-code-opensource/src/utils/mcp/elicitationValidation.ts— 基于 Schema 的表单字段实时验证逻辑。
📄 src/utils/mcp/elicitationValidation.ts — 基于 Schema 的表单字段实时验证逻辑。
EnumSchema,
MultiSelectEnumSchema,
PrimitiveSchemaDefinition,
StringSchema,
} from '@modelcontextprotocol/sdk/types.js'
import { z } from 'zod/v4'claude-code-opensource/src/cli/structuredIO.ts— Control Protocol 中 Elicitation 请求的转发落点。
📄 src/cli/structuredIO.ts — Control Protocol 中 Elicitation 请求的转发落点。
SDKControlRequest,
SDKControlResponse,
StdinMessage,
StdoutMessage,
} from 'src/entrypoints/sdk/controlTypes.js'
import type { CanUseToolFn } from 'src/hooks/useCanUseTool.js'claude-code-opensource/src/screens/REPL.tsx— Elicitation 队列管理与界面激活。
📄 src/screens/REPL.tsx — Elicitation 队列管理与界面激活。
// Elicitation dialog handles its own Escape, and closing it shouldn't affect any loading state.
return;
}
logForDebugging(`[onCancel] focusedInputDialog=${focusedInputDialog} streamMode=${streamMode}`);