Skip to content
源码分析手册

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 层的对话框深度绑定:

  1. 协议入口 (src/services/mcp/elicitationHandler.ts): 只有在 MCP Client 协商时声明了 elicitation 能力,Claude Code 才会注册 ElicitRequestSchema。当 Server 抛出该请求时,Handler 首先会运行预设的 Hooks(如 runElicitationHooks),尝试自动化响应,如果 Hooks 未处理,才进入交互流程。

  2. 交互模式 (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
  3. 任务队列与 REPL 调度 (src/screens/REPL.tsx): Elicitation 请求会被封装成事件并推入 elicitation.queue。REPL 屏幕会根据队列头部渲染出对应的 ElicitationDialog。这意味着 Elicitation 请求具有“焦点夺取”特性,它会暂时阻塞正常的对话流,直到该请求被解决或取消。

  4. 非交互式分发 (src/cli/structuredIO.ts): 如果 Claude Code 运行在 SDK 或集成环境下,Handler 会将请求通过 Control Protocol 转发给宿主程序(Host),而不是直接在本地弹窗。

踩坑指南

  • 非权限系统:Elicitation 解决的是“Server 缺数据”,而不是“Claude 缺执行某个工具的权限”。虽然它们都会弹窗,但底层的协议路径完全不同。
  • URL 模式的异步性:点击浏览器跳转后的“Accept”并不意味着交互结束。如果 Server 没有发送对应的“Complete”通知,该交互项可能会在 REPL 队列中挂起。
  • Hooks 的优先级:你可以通过编写 Elicitation / ElicitationResult Hooks 来绕过频繁的表单弹窗。这是实现 MCP 自动化高级场景的核心。
  • 输入合法性:本地校验(Validation)遵循 Server 提供的原始 Schema。如果校验失败,Claude 会在本地阻断提交,减少无效的网络往返。

延伸阅读

源码锚点

  • claude-code-opensource/src/services/mcp/elicitationHandler.ts — Elicitation 核心分发与 Hooks 逻辑。
📄 src/services/mcp/elicitationHandler.ts — Elicitation 核心分发与 Hooks 逻辑。L3-8 of 314
typescript
  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 交互的具体界面实现。L23-31 of 1169
tsx
  /** 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 的表单字段实时验证逻辑。L2-7 of 337
typescript
  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 请求的转发落点。L17-22 of 860
typescript
  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 队列管理与界面激活。L2108-2111 of 5006
tsx
      // Elicitation dialog handles its own Escape, and closing it shouldn't affect any loading state.
      return;
    }
    logForDebugging(`[onCancel] focusedInputDialog=${focusedInputDialog} streamMode=${streamMode}`);

基于 Claude Code v2.1.88 开源快照的深度分析