
AI SDK Next.js 实战基于 MCP Elicitation 的人机协作工具调用完整实现【免费下载链接】aiThe AI Toolkit for TypeScript. From the creators of Next.js, the AI SDK is a free open-source library for building AI-powered applications and agents项目地址: https://gitcode.com/GitHub_Trending/ai/ai导读MCPModel Context ProtocolElicitation 是让 AI 工具在缺少必要参数时主动向用户索取输入的能力当用户只说了一句帮我注册一个新账号而注册工具需要用户名、邮箱、密码时MCP 服务器可以通过 elicitation 请求携带一份 JSON Schema把该填什么的问题抛给前端由用户以表单形式补全信息再回传给工具继续执行。本文以 examples/ai-e2e-next/app/chat/mcp-elicitation/README.md 为骨架结合该示例的页面、API 路由、MCP 服务器与ai-sdk/mcp底层实现完整讲解在 Next.js AI SDK 中搭建注册用户类 human-in-the-loop 流程的每一行关键代码读完即可在自己的应用中复刻这套「模型调用工具 → 工具请求补参 → 前端模态表单 → 回传继续执行」的闭环。一、什么是 MCP Elicitation从参数缺失到主动补参在传统工具调用function calling / tool calling中模型必须一次性从对话上下文里推断出工具的全部参数。但在真实场景下用户可能不会一次性提供所有必填信息例如帮我注册账号这句话里并没有用户名、邮箱和密码。MCP Elicitation 解决的就是这个问题当工具执行过程中发现自己缺少输入时向客户端这里即 Next.js 服务端发起一次elicitation/create请求携带一段面向用户的提示消息和一个 JSON Schema客户端把它转交给 UI由真人填写、拒绝或取消再将结果返回工具据此继续执行。这属于典型的 human-in-the-loop人在回路交互模式。在 AI SDK 生态中ai-sdk/mcp包为createMCPClient增加了onElicitationRequest注册点见 packages/mcp/src/tool/mcp-client.ts并在协议层实现了elicitation/create请求的接收、校验与应答使其成为一条开箱即用的标准能力。二、整体工作流程7 步闭环该示例的完整流程源自 README.md如下用户发送消息请求某个动作例如register me as a new userAI 模型调用对应的 MCP 工具例如register_userMCP 服务器发起 elicitation 请求附带一段提示消息和一份 JSON Schema前端根据 Schema 渲染模态表单展示各字段及其类型、必填性、默认值用户填写表单并选择提交Submit、拒绝Decline或取消Cancel响应回传 MCP 服务器前端先把结果 POST 到独立的/respond端点由 Next.js 服务端通过内存中的 pending 注册表把 Promise resolve 掉再以 MCP 协议格式返回给服务器工具执行完成AI 模型拿到工具结果继续后续对话。值得强调的是第 3 步的 elicitation 请求并非卡住整个对话——Next.js 的 API 路由把streamText流合并进createUIMessageStream见 route.ts前端一边等待用户输入一边仍可保持消息流的正常接收。三、环境准备与启动步骤3.1 前置依赖一个可用的 OpenAI API Key示例默认使用gpt-4o-mini模型见 route.tspnpm 与 Node.js 环境仓库已按根目录package.json完成依赖安装monorepoai-sdk/mcp、ai、ai-sdk/react等均为 workspace 包。3.2 第一步启动 MCP 服务器README 给出的命令是pnpm tsx src/elicitation-ui/server.ts该脚本实际位于 examples/mcp/src/elicitation-ui/server.ts因此需要在examples/mcp目录下执行仓库也提供了等价快捷脚本见 examples/mcp/package.jsoncd examples/mcp pnpm server:elicitation-ui启动成功后服务器监听http://localhost:8085/sse端点用于建立 SSE 传输/messages端点接收客户端消息见 server.ts。3.3 第二步运行 Next.js 应用cd examples/ai-e2e-next pnpm dev3.4 第三步打开示例页面浏览器访问http://localhost:3000/mcp-elicitation注意与 README 中标注一致这是该示例页面的访问路径。四、MCP 服务器端如何定义一个会要参数的工具示例 MCP 服务器基于官方modelcontextprotocol/sdk构建注册了一个register_user工具。关键点在于工具执行体内调用elicitInput发起 elicitation见 server.tsserver.registerTool( register_user, { description: Register a new user account by collecting their information, inputSchema: {}, }, async () { const elicitInput server.server?.elicitInput?.bind(server.server); if (!elicitInput) { return { content: [ { type: text, text: Elicitation is not supported by this SDK version. }, ], }; } const result await elicitInput({ message: Please provide your registration information:, requestedSchema: { type: object, properties: { username: { type: string, title: Username, description: Your desired username (3-20 characters), minLength: 3, maxLength: 20, }, email: { type: string, title: Email, description: Your email address, format: email, }, password: { type: string, title: Password, description: Your password (min 8 characters), minLength: 8, }, newsletter: { type: boolean, title: Newsletter, description: Subscribe to newsletter?, default: false, }, }, required: [username, email, password], }, }); // result.action: accept | decline | cancel }, );需要注意两个细节inputSchema为空对象模型并不需要预先提供参数所有必填信息都通过 elicitation 阶段收集JSON Schema 驱动 UIrequestedSchema中每个属性的type、title、description、format、minLength、maxLength、default、required等字段都会被前端直接用来渲染表单控件与校验约束详见第六节。工具在拿到result后会按action分支处理accept时输出注册成功的文本decline/cancel时分别输出用户拒绝注册或注册已取消的文本见 server.ts 中 L73-L119。这意味着工具与模型都能感知用户的真实选择从而自然衔接后续对话。五、Next.js API 路由桥接 MCP 服务器与前端 UI该示例的前后端桥接由两个路由 一个内存注册表构成全部位于 examples/ai-e2e-next/app/api/chat/mcp-elicitation 目录。5.1 主路由创建带 elicitation 能力的 MCP 客户端route.ts 的核心逻辑const mcpClient await createMCPClient({ transport: { type: sse, url: http://localhost:8085/sse, }, capabilities: { elicitation: {}, // 向 MCP 服务器宣告客户端支持 elicitation }, }); // 注册 elicitation 请求处理器 mcpClient.onElicitationRequest(ElicitationRequestSchema, async request { const elicitationId elicit-${Date.now()}-${Math.random().toString(36).slice(2)}; // 1. 把请求写入 UI 消息流前端据此弹窗 writer.write({ type: data-elicitation-request, id: elicitationId, data: { elicitationId, message: request.params.message, requestedSchema: request.params.requestedSchema, }, }); // 2. 挂起等待直到 /respond 端点 resolve 这个 pending 请求 const userResponse await createPendingElicitation(elicitationId); // 3. 以 MCP 期望的格式返回 return { action: userResponse.action, content: userResponse.action accept ? userResponse.content : undefined, }; });随后通过streamText驱动对话route.tsconst result streamText({ model: openai(gpt-4o-mini), tools, // 来自 mcpClient.tools() stopWhen: isStepCount(10), instructions: You are a helpful assistant. When asked to register a user, use the register_user tool., messages: await convertToModelMessages(messages), onEnd: async () { await mcpClient.close(); }, }); writer.merge(toUIMessageStream({ stream: result.stream, originalMessages: messages }));几个实现要点capabilities: { elicitation: {} }在 MCP initialize 握手阶段向服务器声明客户端具备 elicitation 能力对应 packages/mcp/src/tool/types.ts 中ClientCapabilities的可选elicitation字段maxDuration 30允许该路由的流式响应最长运行 30 秒route.ts保证挂起的 elicitation 不会被平台超时提前掐断onElicitationRequest返回值的形状必须匹配ElicitResultSchemaaction只能是accept | decline | cancelcontent仅在 accept 时携带见 packages/mcp/src/tool/types.ts。5.2 响应路由把用户选择接回挂起的 Promise前端提交的用户响应会被 POST 到/api/mcp-elicitation/respondrespond/route.tsconst response: ElicitationResponse await req.json(); const resolved resolvePendingElicitation(response); if (!resolved) { return Response.json( { error: Elicitation request not found or already resolved }, { status: 404 }, ); } return Response.json({ success: true });5.3 内存注册表让两条路由共享同一份状态elicitation-store.ts 用挂在globalThis上的Map保存所有挂起的 elicitation避免 Next.js 开发模式热重载导致模块级状态丢失并为每个请求设置了60 秒超时与 MCP 层超时对齐和10 分钟过期清理const timeoutId setTimeout(() { if (pendingElicitations.has(id)) { pendingElicitations.delete(id); reject(new Error(Request timed out)); } }, 60 * 1000); pendingElicitations.set(id, { resolve, reject, createdAt: Date.now(), timeoutId });主路由await createPendingElicitation(id)会一直挂起直到/respond路由调用resolvePendingElicitation把 Promise resolve见 elicitation-store.ts。这正是等待用户输入的异步桥梁用户在 UI 上的每一次点击最终都会通过这个 Map 转化为 MCP 协议层的应答。注意此方案为单实例内存态生产环境如需多副本部署应替换为 Redis 等共享存储。六、前端页面Schema 驱动的模态表单页面 page.tsx 使用useChat配合DefaultChatTransport连接主路由const { messages, sendMessage } useChatMCPElicitationUIMessage({ transport: new DefaultChatTransport({ api: /api/chat/mcp-elicitation, }), });6.1 监听 data part 并弹出模态框useEffect逆序遍历消息用isDataUIPart识别data-elicitation-requestpart并借助handledElicitationsRef一个Set保证每个 elicitation 只弹一次框且始终展示最新的未处理请求page.tsxif (isDataUIPart(part) part.type data-elicitation-request) { const elicitationId part.data.elicitationId; if (!handledElicitationsRef.current.has(elicitationId)) { handledElicitationsRef.current.add(elicitationId); setCurrentElicitation(part.data); setShowModal(true); // 从 Schema 的 default 字段初始化表单boolean 默认 false const schema part.data.requestedSchema as any; if (schema?.properties) { const defaults: Recordstring, any {}; for (const [key, prop] of Object.entries(schema.properties)) { if (prop.default ! undefined) defaults[key] prop.default; else if (prop.type boolean) defaults[key] false; } setFormData(defaults); } return; } }6.2 按 Schema 渲染不同类型的输入控件renderFormField根据属性类型分派控件page.tsxboolean→ checkbox初始值取 Schemadefault示例中newsletter默认falsenumber/integer→ number 输入框应用minimum/maximum约束integer 走parseInt、number 走parseFloat字符串→ 按format email渲染 email 类型、按type password渲染 password 类型其余为 text并应用minLength/maxLength必填标记schema.required数组中的字段标签旁显示红色*并设置required属性。同时消息流中每个data-elicitation-request/data-elicitation-responsepart 也会被渲染为蓝色/绿色高亮的对话气泡让用户看到系统正在向你索取信息以及自己做出的选择page.tsx。6.3 三种动作的提交语义模态框提供三个按钮统一走handleElicitationResponsepage.tsxconst dataToSend action accept ? { ...formData } : undefined; const response await fetch(/api/mcp-elicitation/respond, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ id: elicitationId, action, content: dataToSend }), });Submitaccept携带{ ...formData }作为contentDeclinedeclinecontent为undefined表示用户拒绝提供信息Cancelcancel同样不携带content表示放弃整个操作。实现上通过先关闭模态框、清空状态、再发请求的顺序杜绝重复提交setShowModal(false)在await fetch之前执行。content字段的类型在 types.ts 中定义为Recordstring, unknown并通过对UIMessage泛型注入自定义的ElicitationDataTypes使useChat能正确解析这两种自定义 data part 的类型。七、底层原理ai-sdk/mcp如何实现 elicitation7.1 协议层 Schemapackages/mcp/src/tool/types.ts 定义了 elicitation 的协议形状const ElicitationRequestParamsSchema BaseParamsSchema.extend({ message: z.string(), // 向用户展示的提示语 requestedSchema: z.unknown(), // 需要用户填写的 JSON Schema }); export const ElicitationRequestSchema RequestSchema.extend({ method: z.literal(elicitation/create), // 唯一的请求方法 params: ElicitationRequestParamsSchema, }); export const ElicitResultSchema ResultSchema.extend({ action: z.union([z.literal(accept), z.literal(decline), z.literal(cancel)]), content: z.optional(z.record(z.string(), z.unknown())), });也就是说MCP 服务器通过elicitation/create这个 JSON-RPC 方法发起请求客户端必须回以{ action, content? }格式的应答。7.2 客户端处理链路在 packages/mcp/src/tool/mcp-client.ts 的onRequestMessage中客户端对收到的服务端请求做如下分流ping→ 立即回空结果非elicitation/create的方法 → 回-32601方法不支持错误elicitation/create但未注册处理器 → 回-32601并提示No elicitation handler registered on client用ElicitationRequestSchema.safeParse校验参数失败回-32602无效参数调用onElicitationRequest注册的处理器将返回值经ElicitResultSchema.parse校验后作为 JSON-RPC result 返回。而onElicitationRequest注册点mcp-client.ts要求传入的 schema 必须严格等于ElicitationRequestSchema否则抛出Unsupported request schema错误——这是当前版本对扩展请求类型的刻意限制。7.3 能力协商客户端通过capabilities: { elicitation: {} }在初始化阶段声明支持 elicitation服务端据此决定是否向该客户端发起elicitation/create请求。这也解释了为什么示例主路由必须显式传入该配置route.ts。八、扩展与注意事项命令行版对照实现仓库 examples/mcp/src/elicitation/client.ts 提供了一个终端交互版本pnpm client:elicitation用readline逐字段向用户提问逻辑与前端模态框完全同构按 Schema 遍历properties区分必填/可选、按类型解析 number/boolean 等是理解本流程的最小实现适合在没有浏览器环境时联调验证。多步 elicitation仓库另有elicitation-multi-step示例见 examples/mcp/package.json演示同一会话内多次 elicitation 的编排说明该机制天然支持一问一答逐步收集复杂信息。超时与失败兜底主路由的onElicitationRequest处理器在 catch 分支中默认返回action: declineroute.ts配合注册表 60 秒超时保证即使前端断连MCP 服务器的工具调用也不会永久挂死。运行前提本文全部路径与配置以当前仓库为准需要OPENAI_API_KEY环境变量且 MCP 服务器与 Next.js 应用须同时运行、端口一致SSE 为8085页面为3000。总结本文以 mcp-elicitation 示例 为主线从 MCP 服务器的elicitInput、Next.js 服务端的createMCPClient onElicitationRequest 内存注册表到前端的useChat data part 监听 Schema 驱动模态框再到ai-sdk/mcp协议层的elicitation/create处理链路完整还原了 human-in-the-loop 工具调用的端到端实现。这套模式的价值在于它把模型猜参数升级为模型与用户协作补全参数既保证了工具调用的完整性又保留了用户对敏感信息如密码、邮箱的最终控制权。无论是注册、下单、审批还是任何需要人工确认的场景都可以直接复用本文的架构。【免费下载链接】aiThe AI Toolkit for TypeScript. From the creators of Next.js, the AI SDK is a free open-source library for building AI-powered applications and agents项目地址: https://gitcode.com/GitHub_Trending/ai/ai创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考