news 2026/9/10 19:19:38

AI SDK Next.js 实战:基于 MCP Elicitation 的人机协作工具调用完整实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
AI SDK Next.js 实战:基于 MCP Elicitation 的人机协作工具调用完整实现

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

导读

MCP(Model Context Protocol)Elicitation 是让 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)如下:

  1. 用户发送消息,请求某个动作,例如"register me as a new user";
  2. AI 模型调用对应的 MCP 工具,例如register_user
  3. MCP 服务器发起 elicitation 请求,附带一段提示消息和一份 JSON Schema;
  4. 前端根据 Schema 渲染模态表单,展示各字段及其类型、必填性、默认值;
  5. 用户填写表单,并选择提交(Submit)、拒绝(Decline)或取消(Cancel);
  6. 响应回传 MCP 服务器:前端先把结果 POST 到独立的/respond端点,由 Next.js 服务端通过内存中的 pending 注册表把 Promise resolve 掉,再以 MCP 协议格式返回给服务器;
  7. 工具执行完成,AI 模型拿到工具结果,继续后续对话。

值得强调的是,第 3 步的 elicitation 请求并非"卡住"整个对话——Next.js 的 API 路由把streamText流合并进createUIMessageStream(见 route.ts),前端一边等待用户输入,一边仍可保持消息流的正常接收。

三、环境准备与启动步骤

3.1 前置依赖

  • 一个可用的 OpenAI API Key(示例默认使用gpt-4o-mini模型,见 route.ts);
  • pnpm 与 Node.js 环境;
  • 仓库已按根目录package.json完成依赖安装(monorepo,@ai-sdk/mcpai@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.json):

cd examples/mcp pnpm server:elicitation-ui

启动成功后,服务器监听http://localhost:8085/sse端点用于建立 SSE 传输,/messages端点接收客户端消息,见 server.ts)。

3.3 第二步:运行 Next.js 应用

cd examples/ai-e2e-next pnpm dev

3.4 第三步:打开示例页面

浏览器访问http://localhost:3000/mcp-elicitation(注意与 README 中标注一致,这是该示例页面的访问路径)。

四、MCP 服务器端:如何定义一个"会要参数"的工具

示例 MCP 服务器基于官方@modelcontextprotocol/sdk构建,注册了一个register_user工具。关键点在于工具执行体内调用elicitInput发起 elicitation(见 server.ts):

server.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' }, );

需要注意两个细节:

  1. inputSchema为空对象:模型并不需要预先提供参数,所有必填信息都通过 elicitation 阶段收集;
  2. JSON Schema 驱动 UIrequestedSchema中每个属性的typetitledescriptionformatminLengthmaxLengthdefaultrequired等字段,都会被前端直接用来渲染表单控件与校验约束(详见第六节)。

工具在拿到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.ts):

const 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' | 'cancel'content仅在 accept 时携带(见 packages/mcp/src/tool/types.ts)。

5.2 响应路由:把用户选择"接"回挂起的 Promise

前端提交的用户响应会被 POST 到/api/mcp-elicitation/respond(respond/route.ts):

const 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 } = useChat<MCPElicitationUIMessage>({ transport: new DefaultChatTransport({ api: '/api/chat/mcp-elicitation', }), });

6.1 监听 data part 并弹出模态框

useEffect逆序遍历消息,用isDataUIPart识别data-elicitation-requestpart,并借助handledElicitationsRef(一个Set)保证每个 elicitation 只弹一次框,且始终展示最新的未处理请求(page.tsx):

if (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: Record<string, 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.tsx):

  • boolean→ checkbox,初始值取 Schemadefault(示例中newsletter默认false);
  • number/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 三种动作的提交语义

模态框提供三个按钮,统一走handleElicitationResponse(page.tsx):

const 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 }), });
  • Submit(accept):携带{ ...formData }作为content
  • Decline(decline)contentundefined,表示用户拒绝提供信息;
  • Cancel(cancel):同样不携带content,表示放弃整个操作。

实现上通过"先关闭模态框、清空状态、再发请求"的顺序杜绝重复提交(setShowModal(false)await fetch之前执行)。content字段的类型在 types.ts 中定义为Record<string, unknown>,并通过对UIMessage泛型注入自定义的ElicitationDataTypes,使useChat能正确解析这两种自定义 data part 的类型。

七、底层原理:@ai-sdk/mcp如何实现 elicitation

7.1 协议层 Schema

packages/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中,客户端对收到的服务端请求做如下分流:

  1. ping→ 立即回空结果;
  2. elicitation/create的方法 → 回-32601(方法不支持)错误;
  3. elicitation/create但未注册处理器 → 回-32601并提示No elicitation handler registered on client
  4. ElicitationRequestSchema.safeParse校验参数,失败回-32602(无效参数);
  5. 调用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: 'decline'(route.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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 19:19:19

Ricon组态系统在智能交通中的实践与优化

1. Ricon组态系统在智能交通领域的核心价值第一次接触Ricon组态系统是在去年参与城市智慧交通改造项目时。当时我们需要一个能够实时监控、快速响应的交通管理系统&#xff0c;而传统的PLC控制系统已经无法满足复杂多变的交通场景需求。Ricon组态系统的出现&#xff0c;彻底改变…

作者头像 李华
网站建设 2026/9/10 19:15:57

AI编程助手实测:8款免费版与付费版对比,团队选型避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 19:15:54

源代码加密工具盘点:从git-crypt到企业级透明加密方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 19:08:15

DELL笔记本BIOS损坏的三种特殊恢复方法详解

1. DELL笔记本BIOS恢复的特殊场景与必要性遇到DELL笔记本BIOS损坏的情况时&#xff0c;常规的恢复方法往往失效。我经历过多次类似故障&#xff0c;特别是在刷写第三方修改版BIOS、意外断电或病毒破坏后&#xff0c;机器会出现黑屏、键盘灯亮但无显示、反复重启等典型症状。这时…

作者头像 李华