# 第七阶段 · 模块七 · 第五节:MCP Elicitation 用户交互
什么是 MCP Elicitation?Claude Code 如何处理 MCP 服务器的用户交互请求?交互请求的完整流程是什么?如何处理超时和取消?
Claude Code 全局架构
┌─────────────────────────────────────────────────────────────────────┐
│ 服务层(services/mcp/) │
│ │
│ MCP Elicitation ← 本节 │
│ ├── 用户交互请求 │
│ ├── 通知处理 │
│ └── 钩子集成 │
└─────────────────────────────────────────────────────────────────────┘
源码位置:`src/services/mcp/elicitationHandler.ts`
Elicitation 是 MCP 协议中服务器向用户请求信息或确认的机制,允许 MCP 服务器在需要用户输入时暂停执行。
┌─────────────────────────────────────────────────────────────────────┐
│ MCP Elicitation 流程 │
│ │
│ MCP Server ──► ElicitRequest ──► Claude Code ──► 用户确认 │
│ │ │ │
│ │◄───── ElicitResult ─────┘ │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ Elicitation vs 工具调用 │
│ │
│ 工具调用: │
│ - Claude 自动执行 │
│ - 无需用户介入 │
│ - 适合确定性操作 │
│ │
│ Elicitation: │
│ - 需要用户确认或输入 │
│ - Claude 等待用户响应 │
│ - 适合需要人工决策的场景 │
└─────────────────────────────────────────────────────────────────────┘
问 1:Elicitation 的典型使用场景?
// 典型场景:
// 1. 确认操作
// "确定要删除这些文件吗?"
// - destructive operation confirmation
// 2. 输入敏感信息
// "请输入 API 密钥:"
// - API key input
// 3. 选择确认
// "选择部署环境:production/staging"
// - option selection
// 4. 表单填写
// "请填写配置信息"
// - structured input
源码位置:`src/services/mcp/elicitationHandler.ts`
// ElicitRequestParams
export type ElicitationRequestEvent = {
serverName: string
requestId: string | number
params: ElicitRequestParams
signal: AbortSignal // 用于取消
respond: (response: ElicitResult) => void // 响应回调
waitingState?: ElicitationWaitingState // URL 模式专用
onWaitingDismiss?: (action: 'dismiss' | 'retry' | 'cancel') => void
completed?: boolean // 服务器确认完成
}
// 等待状态配置
export type ElicitationWaitingState = {
actionLabel: string // 按钮标签,如 "Retry now" 或 "Skip confirmation"
showCancel?: boolean // 是否显示取消按钮(错误重试流程)
}
// 两种 Elicitation 模式
type ElicitRequestParams = {
message: string
requestedSchema?: {...}
defaultValue?: unknown
mode?: 'form' | 'url' // 新增模式字段
}
// form 模式:传统的表单填写
// url 模式:URL 确认(如"打开外部链接")
// ElicitResult
type ElicitResult =
| { action: 'accept'; value: unknown } // 用户接受了
| { action: 'reject' } // 用户拒绝了
| { action: 'cancel' } // 用户取消了
// 注意:对于错误重试(-32042),'accept' 是空操作
// 重试由 onWaitingDismiss 驱动
源码位置:`src/services/mcp/elicitationHandler.ts`
// 接收 ElicitRequest
async function handleElicitRequest(params: ElicitRequestParams) {
// 1. 解析请求参数
const { message, requestedSchema, defaultValue } = params
// 2. 创建等待状态
const waitingState: ElicitationWaitingState = {
actionLabel: '确认',
showCancel: true
}
// 3. 显示 UI 并等待用户响应
const result = await showElicitationUI(message, waitingState)
// 4. 返回结果给 MCP 服务器
return result
}
// 显示 Elicitation UI
interface ElicitationUIOptions {
message: string
requestedSchema?: Schema
defaultValue?: unknown
waitingState: ElicitationWaitingState
}
async function showElicitationUI(options: ElicitationUIOptions): Promise<ElicitResult> {
// 1. 渲染模态框
renderElicitationModal(options)
// 2. 等待用户响应
const userResponse = await waitForUserResponse()
// 3. 返回结果
return userResponse
}
┌─────────────────────────────────────────────────────────────────────┐
│ Elicitation 完整流程 │
│ │
│ 1. MCP Server 发送 ElicitRequest │
│ ↓ │
│ 2. Claude Code 接收并验证请求 │
│ ↓ │
│ 3. 执行 elicitation 钩子(可选) │
│ ↓ │
│ 4. 显示 UI 给用户 │
│ ↓ │
│ 5. 用户响应(接受/拒绝/取消) │
│ ↓ │
│ 6. 执行 result 钩子(可选) │
│ ↓ │
│ 7. 返回结果给 MCP Server │
└─────────────────────────────────────────────────────────────────────┘
源码位置:`src/services/mcp/elicitationHandler.ts`
export type ElicitationWaitingState = {
/** 按钮标签 */
actionLabel: string
/** 是否显示取消按钮 */
showCancel?: boolean
/** 错误消息(用于重试流程)*/
errorMessage?: string
}
// 确认操作
const confirmState: ElicitationWaitingState = {
actionLabel: '确认删除',
showCancel: true
}
// 输入表单
const inputState: ElicitationWaitingState = {
actionLabel: '提交',
showCancel: true
}
// 错误重试
const retryState: ElicitationWaitingState = {
actionLabel: '重试',
showCancel: true,
errorMessage: '输入无效,请重试'
}
// 用户操作
// 1. accept - 用户点击确认/提交
// - 返回 { action: 'accept', value: userInput }
// 2. reject - 用户拒绝
// - 返回 { action: 'reject' }
// - 通常用于 destructive operations
// 3. cancel - 用户取消
// - 返回 { action: 'cancel' }
// - 关闭模态框,不做任何改变
源码位置:`src/services/mcp/elicitationHandler.ts`
// 执行 elicitation 钩子
import {
executeElicitationHooks,
executeElicitationResultHooks
} from '../../utils/hooks.js'
// 在显示 UI 前执行钩子
const modifiedParams = await executeElicitationHooks(params)
// 在返回结果前执行钩子
const modifiedResult = await executeElicitationResultHooks(result)
┌─────────────────────────────────────────────────────────────────────┐
│ Elicitation 钩子用途 │
│ │
│ 1. 日志记录 │
│ - 记录所有交互请求 │
│ │
│ 2. 访问控制 │
│ - 拦截敏感的 Elicitation │
│ │
│ 3. 参数验证 │
│ - 修改或验证用户输入 │
│ │
│ 4. 自动化处理 │
│ - 根据条件自动响应 │
└─────────────────────────────────────────────────────────────────────┘
问 1:如何实现自动接受某些 Elicitation?
// 自动接受钩子
async function autoAcceptHook(event: ElicitationRequestEvent): Promise<ElicitResult | null> {
// 检查条件
if (event.params.message.includes('API密钥') && hasStoredApiKey()) {
// 自动返回存储的值
return {
action: 'accept',
value: { apiKey: getStoredApiKey() }
}
}
// 不自动处理,继续正常流程
return null
}
源码位置:`src/services/mcp/channelNotification.ts`
// MCP 服务器可以发送通知
type Notification =
| { type: 'message'; message: string } // 信息提示
| { type: 'progress'; progress: number } // 进度更新
| { type: 'complete' } // 完成通知
// 处理服务器通知
async function handleNotification(notification: Notification) {
switch (notification.type) {
case 'message':
showToast(notification.message)
break
case 'progress':
updateProgressBar(notification.progress)
break
case 'complete':
hideProgressBar()
break
}
}
// 执行通知钩子
executeNotificationHooks(notification)
// 使用 AbortSignal 处理取消
async function handleElicitRequest(params: ElicitRequestParams) {
const { signal, ...rest } = params
// 监听取消信号
signal?.addEventListener('abort', () => {
// 清理 UI
hideElicitationModal()
// 返回取消结果
respond({ action: 'cancel' })
})
// 正常处理
...
}
// 超时处理
async function handleWithTimeout(
params: ElicitRequestParams,
timeoutMs: number = 60000
): Promise<ElicitResult> {
const timeoutPromise = new Promise<ElicitResult>((resolve) => {
setTimeout(() => {
resolve({ action: 'cancel' })
}, timeoutMs)
})
const responsePromise = handleElicitRequest(params)
return Promise.race([responsePromise, timeoutPromise])
}
┌─────────────────────────────────────────────────────────────────────┐
│ 处理长时间等待 │
│ │
│ 1. 显示进度 │
│ - 使用 notification 机制显示进度 │
│ │
│ 2. 心跳保活 │
│ - 定期 ping MCP server │
│ │
│ 3. 用户可取消 │
│ - 提供取消按钮 │
│ - 发送 abort signal │
└─────────────────────────────────────────────────────────────────────┘
答案:
// 工具调用:
// - Claude 根据参数自动执行
// - 无需用户介入
// - 返回执行结果
// Elicitation:
// - 需要用户确认或输入
// - Claude 等待用户响应
// - 用户可以拒绝或取消
答案:
// 方法:
// 1. AbortSignal
// - 服务器可发送取消信号
// - 客户端监听并处理
// 2. 超时机制
// - 设置最大等待时间
// - 超时后自动取消
// 3. 进度通知
// - 使用 notification 显示进度
// - 让用户了解当前状态
// 4. 后台处理
// - 在后台继续处理
// - 用户可做其他事
答案:
// 嵌套处理:
// 1. 技术上支持
// - 可以打开新的 Elicitation
// - 模态框堆叠
// 2. 用户体验问题
// - 多个模态框堆叠混乱
// - 用户容易困惑
// 3. 推荐做法
// - 扁平化处理
// - 串行等待,而非嵌套
// - 或者使用不同的 UI 形式(如 sidebar)
| 资源 | 说明 |
| `src/services/mcp/elicitationHandler.ts` | Elicitation 处理 |
| `src/services/mcp/channelNotification.ts` | 通知处理 |
第八章我们将进入 调试与日志:
- 日志系统架构
- 调试模式详解
- 常见问题诊断