使用 mcp-use 构建 MCP Tools:从 Zod Schema 定义到 Widget 与安全加固实战 📅 发布时间:2026/9/10 23:07:31 👁 浏览次数: 使用 mcp-use 构建 MCP Tools从 Zod Schema 定义到 Widget 与安全加固实战【免费下载链接】CopilotKitThe Frontend Stack for Agents Generative UI. React, Angular, Mobile, Slack, and more. Makers of the AG-UI Protocol项目地址: https://gitcode.com/GitHub_Trending/co/CopilotKit导读在 mcp-use 框架中Tools工具是 AI 可以直接调用的后端动作它们接收结构化输入、执行业务逻辑并返回格式化输出是 MCP Server 中最核心的能力载体。本文以 CopilotKit 仓库open-mcp-client示例中内置的mcp-apps-builder技能文档为主体结合仓库内mcp-use-server的真实源码入口文件、工具文件、构建脚本系统讲解工具的定义规范、Zod 输入校验、注解Annotations、上下文能力、错误处理、Widget 视觉返回、输出 Schema 校验、环境变量、性能优化与安全检查清单。读完本文你将能够用 mcp-use 写出一个生产级、可被 AI 正确理解和调用的 MCP 工具集。一、工具的本质AI 的“后端动作”在 MCPModel Context Protocol的四大原语中工具承担的是有输入、有输出的后端操作Tools动作、操作、API 调用、数据变更、数据获取本文主题Resources客户端可获取的只读数据Prompts可复用的消息模板Widgets可视化交互界面。从mcp-apps-builder技能文档SKILL.md的核心原则可以提炼出分工定位“Tools for actions”——凡是需要执行动作的场景都应建模为工具。一个工具由三部分组成配置name / description / schema、异步 handler 函数、以及返回的响应助手response helper。在仓库的 mcp-use-server/index.ts 中可以看到一个最小服务器的完整骨架通过new MCPServer({...})创建实例随后逐个注册工具最后server.listen(port)启动服务。open-mcp-client示例里的mcp-use-server就是按“每个工具一个tools/tool-name.ts文件、导出register(server)函数”的约定组织的新增工具只需三步在resources/widget-name/widget.tsx写 UI、在tools/tool-name.ts里调用server.tool()、再在入口文件两处标记位置完成 import 与 register。二、第一个工具完整的最小示例import { MCPServer, text } from mcp-use/server; import { z } from zod; const server new MCPServer({ name: my-server, version: 1.0.0, baseUrl: process.env.MCP_URL || http://localhost:3000, }); server.tool( { name: send-email, description: Send an email to a user, schema: z.object({ to: z.string().email().describe(Recipient email address), subject: z.string().describe(Email subject line), body: z.string().describe(Email body content), priority: z .enum([low, normal, high]) .optional() .describe(Email priority), }), }, async ({ to, subject, body, priority normal }) { // Your logic here await sendEmail(to, subject, body, priority); return text(Email sent to ${to}); }, );关键点第一个参数工具配置对象包含name、description、schema第二个参数异步 handler 函数接收的入参已经被 Zod 校验过类型与 schema 完全匹配返回值必须使用响应助手text()、object()、widget()等不能直接返回裸对象。响应助手负责设置正确的 MIME 类型、保证序列化正确、支持客户端渲染与多内容响应详见 response-helpers.md。在仓库的真实实现 tools/product-search.ts 中search-tools工具就是完全遵循这一形态namedescriptionschemaquery字段为可选字符串并带.describe()handler 内先按关键字过滤水果目录、用setTimeout模拟 2 秒网络延迟以演示 Widget 的加载态最后返回widget({ props, output })。三、工具定义规范让 AI 正确理解你的工具3.1 命名Name使用kebab-casesend-email、fetch-user、create-todo一个工具只做一件事❌manage-users→ ✅create-user、delete-user、list-users名称要具体避免模糊的“总括式”命名。3.2 描述Description写清楚、可执行的描述说明工具做什么AI 依赖这段描述决定“何时调用你的工具”✅ Send an email to a user with subject and body ❌ Email tool3.3 SchemaZod每个字段都必须使用.describe()这直接决定 AI 能否正确生成入参// ✅ Good z.object({ city: z.string().describe(City name (e.g., New York, Tokyo)), units: z .enum([celsius, fahrenheit]) .optional() .describe(Temperature units), limit: z.number().min(1).max(50).optional().describe(Max results to return), }); // ❌ Bad - no descriptions z.object({ city: z.string(), units: z.string(), limit: z.number(), });Schema 最佳实践非必填字段用.optional()加上校验规则.min()、.max()、.email()、.url()固定取值集合用z.enum()不要用z.string()列表用z.array()键值映射用z.record()。四、工具注解Tool Annotations声明工具性质以保护用户通过annotations声明工具的行为性质客户端可以据此向用户发出提示例如危险操作前二次确认server.tool( { name: delete-user, description: Permanently delete a user account, schema: z.object({ userId: z.string().describe(User ID) }), annotations: { destructiveHint: true, // Deletes or overwrites data readOnlyHint: false, // Has side effects openWorldHint: false, // Stays within users account (not external APIs) }, }, async ({ userId }) { await deleteUser(userId); return text(User ${userId} deleted); }, );三个注解的语义destructiveHint: true—— 会删除/覆盖数据客户端可能要求用户确认readOnlyHint: true—— 无副作用可安全重复调用openWorldHint: true—— 会调用用户控制范围之外的外部 API 或服务。五、工具上下文Tool Context进度、日志与 LLM 协作handler 的第二个参数ctx提供高级能力包括进度上报、结构化日志、请求 LLM 辅助以及能力探测server.tool( { name: process-large-file, schema: z.object({ fileUrl: z.string().describe(URL to file) }), }, async ({ fileUrl }, ctx) { // Progress reporting await ctx.reportProgress?.(0, 100, Starting download...); const file await downloadFile(fileUrl); await ctx.reportProgress?.(50, 100, Processing...); const result await processFile(file); // Structured logging await ctx.log(info, Processed ${file.size} bytes); // Structured logging with additional context (optional third parameter) await ctx.log( info, Processing complete, fileSize: ${file.size} bytes, duration: 2.5s, ); // Check client capabilities if (ctx.client.can(sampling)) { // Ask the LLM to help analyze results const summary await ctx.sample(Summarize this data: ${result}); return text(summary); } await ctx.reportProgress?.(100, 100, Complete); return object(result); }, );上下文方法一览ctx.reportProgress(current: number, total: number, message: string)—— 向用户展示进度ctx.log(level: debug | info | warn | error, message: string, data?: string)—— 结构化日志第三参数为可选的附加上下文字符串ctx.sample(prompt: string)—— 请求 LLM 协助需要客户端支持ctx.client.can(capability: string)—— 探测客户端是否支持某项能力。六、错误处理返回error()而不是抛出异常永远用error()助手优雅地返回失败不要 throw——抛出的异常会让客户端看到原始错误import { text, error } from mcp-use/server; server.tool( { name: fetch-user, schema: z.object({ id: z.string() }) }, async ({ id }) { try { const user await fetchUser(id); if (!user) { return error(User not found: ${id}); } return object(user); } catch (err) { // Log for debugging console.error(Failed to fetch user:, err); // Return error to client return error( Failed to fetch user: ${err instanceof Error ? err.message : Unknown error}, ); } }, );错误处理规则✅ 用error()返回优雅失败❌ 不要抛出异常客户端看到的是原始错误✅ 错误消息中带上有助于排查的上下文✅ 在服务端记录日志便于调试。七、工具 Widget让工具返回可视化 UI当工具需要返回视觉界面时浏览列表、对比条目、交互式选择使用widgetimport { widget, text } from mcp-use/server; server.tool( { name: search-products, description: Search products by keyword, schema: z.object({ query: z.string().describe(Search query), }), widget: { name: product-list, // Must match resources/product-list.tsx invoking: Searching products..., invoked: Products loaded, }, }, async ({ query }) { const products await searchProducts(query); return widget({ props: { products, query, totalCount: products.length, }, output: text(Found ${products.length} products matching ${query}), }); }, );Widget 工具的要求在工具配置中增加widget: { name }handler 返回widget({ props, output })创建同名 Widget 文件resources/{name}.tsxexposeAsTool默认为false此种模式下省略该字段是正确写法。关于widget()响应的结构props是发给 Widget 组件的可视化数据output是AI 模型真正看到的文本/对象摘要。完整的响应助手清单与 Widget 实现可参考 response-helpers.md 与 widgets/basics.md。仓库 tools/product-search.ts 就是这一模式的完整落地search-tools返回widget({ props: { query, results }, output: text(...) })渲染水果搜索结果轮播界面而配套的get-fruit-details是一个纯数据工具专门供 Widget 内部通过useCallTool(get-fruit-details)调用二者组成“视觉入口 数据后盾”的组合。八、结构化输出 Schema运行时校验工具输出在工具配置中加入outputSchema可在运行时验证返回值形状server.tool( { name: calculate-stats, schema: z.object({ data: z.array(z.number()).describe(Array of numbers), }), outputSchema: z.object({ mean: z.number(), median: z.number(), stdDev: z.number(), count: z.number(), }), }, async ({ data }) { const stats calculateStats(data); // Output is validated against outputSchema return object({ mean: stats.mean, median: stats.median, stdDev: stats.stdDev, count: data.length, }); }, );何时使用outputSchema需要对工具输出做运行时校验多条代码路径可能返回不同的形状排查输出结构不一致的问题。仓库中的get-fruit-details工具即为范例outputSchema声明{ fruit, color, facts }的结构其中facts是z.array(z.string())handler 始终按此形状返回object({...})。九、环境变量安全处理密钥与配置绝不能把密钥硬编码进代码。API 密钥一律通过process.env读取并在缺失时给出明确的错误提示// index.ts const WEATHER_API_KEY process.env.WEATHER_API_KEY; server.tool( { name: get-weather, schema: z.object({ city: z.string() }), }, async ({ city }) { if (!WEATHER_API_KEY) { return error( WEATHER_API_KEY not configured. Please set it in environment variables., ); } const data await fetch( https://api.weather.com/v1?key${WEATHER_API_KEY}city${city}, ); // ... rest of logic }, );最佳实践❌ 永远不要硬编码密钥✅ 使用process.env.VAR_NAME✅ 检查必需变量是否已设置✅ 在.env.example中记录所有必需变量。.env.example示例# Weather API key (get from weatherapi.com) WEATHER_API_KEY # Database connection string DATABASE_URL在仓库open-mcp-client示例根目录同样提供有 .env.example 文件用于记录整个多应用编排所需的全部环境变量可作为团队协作时的模板参考。十、性能模式缓存与限流10.1 缓存Caching为昂贵的操作加缓存例如按城市缓存 5 分钟的天气数据const cache new Mapstring, { data: any; expires: number }(); server.tool( { name: fetch-weather, schema: z.object({ city: z.string() }) }, async ({ city }) { const cacheKey weather:${city}; const cached cache.get(cacheKey); // Return cached data if not expired if (cached cached.expires Date.now()) { return object(cached.data); } // Fetch fresh data const data await fetchWeather(city); // Cache for 5 minutes cache.set(cacheKey, { data, expires: Date.now() 5 * 60 * 1000, }); return object(data); }, );10.2 限流Rate Limiting使用hono-rate-limiter防止滥用。注意 key 生成需适配托管环境反向代理 IP 头、CDN 头等import { rateLimiter } from hono-rate-limiter; server.use( rateLimiter({ windowMs: 15 * 60 * 1000, // 15 minutes limit: 100, // 100 requests per window per key keyGenerator: (c) c.req.header(x-forwarded-for)?.split(,)[0]?.trim() ?? c.req.header(cf-connecting-ip) ?? c.req.header(x-real-ip) ?? unknown, }), );请根据你的托管环境调整 key generator。重要兼容性说明mcp-use 构建在 Hono 之上因此请使用 Hono 兼容的中间件Express 中间件例如express-rate-limit不兼容。自定义中间件与高级路由可参考 foundations/architecture.md。十一、部署前安全检查清单在部署工具之前逐项核对所有 schema 字段都有.describe()使用 Zod 做输入校验用户输入已消毒无 SQL 注入、XSSAPI 密钥存放在环境变量中错误通过error()助手返回而非 throw异步操作包裹 try/catch昂贵操作用限流保护破坏性操作标记destructiveHint: truemcp-apps-builder技能文档SKILL.md还总结了四条“黄金法则”与本清单互补一个工具一个能力一次性返回完整数据工具调用昂贵避免list-productsget-product-details的两次往返应让list-products直接返回含详情的数据Widget 自己管理 UI 状态不要在服务端造select-item、set-filter这类工具只在边界做校验信任内部代码与框架保证校验用户输入与外部 API 响应。十二、仓库实战在 open-mcp-client 中按此规范组织工具上述规范并非纸上谈兵——open-mcp-client示例中的mcp-use-server应用正是按这套约定落地的可作为“可运行的教科书”对照学习入口文件apps/mcp-use-server/index.ts 定义了服务器元信息name、version、baseUrl、favicon、icons等通过register(server)聚合各工具文件并以server.listen(parseInt(process.env.PORT ?? 3109, 10))启动工具文件apps/mcp-use-server/tools/product-search.ts 遵循“一文件一工具集”同时演示了带_meta[ui/previewData]预览数据的 Widget 工具供 MCP UI Studio 在未发起真实调用时预览界面与带outputSchema的数据工具构建与运行脚本见 apps/mcp-use-server/package.jsonnpm run dev会先执行mcp-use build --inline内联编译 Widget再以tsx启动开发服务并支持热重载mcp-use deploy可直接部署到托管云postinstall中的mcp-use generate-types会自动生成类型定义。启动开发服务器的方式cd examples/showcases/open-mcp-client/apps/mcp-use-server npm install npm run dev随后可在浏览器打开http://localhost:3109/inspector使用内置 Inspector 测试工具调用。十三、下一步格式化响应→ response-helpers.md添加可视化 UI→ widgets/basics.md查看更多端到端示例→ patterns/common-patterns.md系统了解 MCP 原语定位→ foundations/concepts.md【免费下载链接】CopilotKitThe Frontend Stack for Agents Generative UI. React, Angular, Mobile, Slack, and more. Makers of the AG-UI Protocol项目地址: https://gitcode.com/GitHub_Trending/co/CopilotKit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考