从零实现MCP Server:打通AI代理自动报价与Agent-to-Agent Commerce闭环
发布时间:2026/10/1 7:32:16来源:尧图网络
1. 从一个真实需求说起为什么我要给产品加 MCP server去年年底我把自己的一个小工具挂到了线上功能不复杂就是帮独立开发者做 API 文档的自动整理和格式化。上线之后流量还行但有个问题一直困扰我每天都有不少用户来问“能不能对接我的 AI 助手”“能不能让我的 agent 直接调用”。一开始我没当回事觉得用户手动复制粘贴也能用。直到有个做 agent 开发的朋友跟我说了一句“你这个产品如果不能让 AI 代理自动发现那在 agent-to-agent commerce 这波里就等于不存在。”这句话点醒了我。2024 年下半年开始MCPModel Context Protocol逐渐成为 AI 代理和外部工具之间通信的事实标准。Claude Desktop、Cursor、以及后来一堆 IDE 和 CLI 工具都开始支持 MCP server。简单说MCP 就是给 AI 代理提供了一套“说明书 遥控器”代理通过它知道你的产品能干什么、需要什么参数、返回什么结果然后自动调用。没有 MCP server你的产品在 AI 眼里就是一个黑盒代理只能靠人去手动操作。所以我决定给自己的小产品写一个 MCP server。目标很明确让 Claude、Cursor 这类支持 MCP 的客户端能够自动发现我的产品能力并且在我的产品支持“报价”这个动作时代理可以自动完成询价和报价流程。这篇文章就是整个过程的完整记录包括我踩过的坑、工具选型的思考、以及最后跑通 agent-to-agent commerce 闭环的实操细节。如果你也在做独立产品或者正在研究 MCP server 怎么落地这篇应该能帮你省下不少时间。2. 整体设计思路MCP server 到底该暴露什么2.1 先搞清楚 MCP 的通信模型MCP 本质上是一个基于 JSON-RPC 2.0 的协议客户端比如 Claude Desktop、Cursor作为 host通过 stdio 或 SSE 跟 MCP server 通信。Server 端需要声明自己支持哪些 capabilities主要包括三类resources资源类似只读数据、tools工具可调用的函数、prompts提示模板。对于我的场景核心是 tools因为代理需要“调用”我的产品来完成报价。这里有个关键设计决策我是把整个产品 API 都暴露成 tools还是只暴露跟报价相关的我试过全量暴露结果 Claude 在对话里经常选错工具因为工具太多、描述太相似。后来我砍到只保留三个核心 tooldiscover_product发现产品能力、get_quote获取报价、confirm_order确认下单。这样代理的决策路径非常清晰实测下来准确率高很多。提示MCP server 的 tool 数量建议控制在 5 个以内每个 tool 的描述要写得像给新人看的 API 文档因为 LLM 就是靠这段描述来决定调不调、怎么调。2.2 为什么选 TypeScript 而不是 PythonMCP 官方提供了 Python 和 TypeScript 两套 SDK。我两个都试了最后选了 TypeScript。原因有三个第一我的产品后端本来就是 Node.js复用现有的类型定义和校验逻辑最省事第二TypeScript SDK 的McpServer类封装得更干净注册 tool 的时候可以直接用 zod 做参数校验省掉一大堆手写校验第三stdio 传输在 Node 环境下调试更方便我可以直接用tsx跑起来配合 Claude Desktop 的日志看请求响应。Python SDK 也不是不能用但如果你跟我一样产品是 JS 技术栈没必要为了 MCP 单独起一个 Python 服务多一个进程就多一份运维成本。工具选型的原则就是离你现有代码越近越好能复用的逻辑绝不重写。2.3 报价流程的状态机设计Agent-to-agent commerce 最麻烦的地方在于“报价”不是一次性的它是有状态的。代理先问价我的产品返回一个带有效期的报价单代理确认后我再生成订单。如果代理隔了十分钟才确认报价可能已经过期了。所以我在 server 端维护了一个轻量的报价状态机状态触发动作有效期下一步quotedget_quote 调用成功15 分钟confirm_order 或过期confirmedconfirm_order 调用成功永久生成订单expired超过 15 分钟未确认-需重新 get_quote这个状态机我用内存 Map 实现因为我的产品量不大没必要上 Redis。但如果你要做多实例部署状态必须外置否则代理的 confirm 请求打到另一个实例就找不到报价单了。这是我在设计阶段就考虑到的扩展点虽然现在用不上但接口留好了。3. 核心细节解析tool 定义与参数设计的门道3.1 tool 描述怎么写才能让代理选对MCP tool 的 description 字段是给 LLM 看的不是给人看的。我一开始写的是“获取产品报价”结果 Claude 经常在用户只是问“这个多少钱”的时候就直接调用但其实用户可能只是想了解价格区间。后来我把描述改成“当用户明确表达购买意向并需要正式报价单时调用。返回包含价格、有效期和报价单 ID 的结构化数据。如果用户只是询问价格区间不要调用此工具。”加上这句否定约束之后误调用率明显下降。参数设计也有讲究。get_quote我原本设计了一个product_id必填参数但代理经常不知道 product_id 是什么。后来我改成product_name字符串让代理直接传用户说的产品名server 端做模糊匹配。虽然多了一层匹配逻辑但代理的调用成功率从 60% 提升到了 90% 以上。这就是“以代理为中心”的设计思路代理不擅长精确 ID但擅长自然语言。3.2 zod schema 做参数校验的实操TypeScript SDK 用 zod 定义参数 schema这个设计非常顺手。我贴一段实际代码import { z } from zod; server.tool( get_quote, 当用户明确表达购买意向并需要正式报价单时调用..., { product_name: z.string().describe(产品名称支持模糊匹配), quantity: z.number().int().positive().default(1).describe(购买数量默认 1), customer_tier: z.enum([free, pro, enterprise]).default(free) .describe(客户等级影响折扣), }, async ({ product_name, quantity, customer_tier }) { // 报价逻辑 } );注意.describe()一定要写而且要用自然语言写清楚。LLM 在决定传什么值的时候会参考这个描述。我试过不写 describe代理传参经常瞎猜。另外default值也很重要它让代理在信息不全时也能调用而不是卡住等用户补充。3.3 返回值的结构化设计MCP tool 的返回值必须是content数组每个元素有type和text。我一开始直接把 JSON 字符串塞进 text结果代理解析起来很费劲。后来我改成两段式第一段是给代理看的自然语言摘要第二段是结构化 JSON。比如return { content: [ { type: text, text: 报价单已生成${product_name} x ${quantity}总价 ${total} 元报价单 ID 为 ${quoteId}有效期 15 分钟。, }, { type: text, text: JSON.stringify({ quoteId, total, currency: CNY, expiresAt }), }, ], };这样代理既能直接读懂结果又能在需要精确数据时解析 JSON。实测下来Claude 在后续对话里引用报价单 ID 的准确率大幅提升。4. 实操过程从零跑通 MCP server 并接入 Claude 和 Cursor4.1 项目初始化和依赖安装我用的 Node 20 TypeScript初始化很简单mkdir my-product-mcp cd my-product-mcp npm init -y npm install modelcontextprotocol/sdk zod npm install -D typescript tsx types/node然后tsconfig.json里把module设成NodeNexttarget设成ES2022。这里有个坑MCP SDK 是 ESM only 的如果你的项目是 CommonJSimport 会报错。我一开始没注意折腾了半小时才反应过来。要么整个项目用 ESM要么用动态 import我选了前者因为新项目没必要背 CJS 的包袱。4.2 编写 server 入口和 stdio 传输入口文件src/index.ts的核心结构import { McpServer } from modelcontextprotocol/sdk/server/mcp.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; const server new McpServer({ name: my-product-mcp, version: 1.0.0, }); // 注册 tools... const transport new StdioServerTransport(); await server.connect(transport);stdio 传输意味着 server 通过标准输入输出跟客户端通信。这里有个关键注意事项绝对不要在 stdout 里打印任何调试信息因为 stdout 是协议通道你打印一个console.log就会污染 JSON-RPC 消息导致客户端解析失败。调试信息一律走console.error它会输出到 stderr不影响协议。我在这上面踩过坑代理突然不响应了查了半天才发现是一行遗留的 console.log。4.3 接入 Claude Desktop 的配置Claude Desktop 的 MCP 配置在~/Library/Application Support/Claude/claude_desktop_config.jsonmacOS。配置内容{ mcpServers: { my-product: { command: npx, args: [tsx, /absolute/path/to/src/index.ts] } } }注意路径必须是绝对路径相对路径 Claude Desktop 解析不了。配置完重启 Claude Desktop然后在对话里问“你能看到哪些工具”如果配置成功Claude 会列出你注册的 tools。我第一次配置时忘了重启一直以为配置没生效其实重启就好了。4.4 接入 Cursor 的配置Cursor 的 MCP 配置在设置里的 Features MCP Servers或者直接编辑~/.cursor/mcp.json。格式跟 Claude Desktop 类似但 Cursor 对 stdio server 的启动超时更短如果你的 server 启动慢比如要加载大量依赖可能会被判定为失败。我的做法是提前npm run build出 JS 文件配置里直接跑node dist/index.js启动速度快很多。Cursor 里用 MCP 的方式是在 Composer 或 Chat 里代理会自动发现可用的 tools。我实测在 Cursor 里让代理“帮我给客户报个价”它会自动调用get_quote然后把结果整理成一段话。整个过程不需要我手动指定工具这就是 MCP 的价值。4.5 报价参数的计算逻辑报价不是简单查表我设计了一个阶梯折扣 客户等级系数的计算模型。基础价格从产品数据库取然后数量折扣1-9 件无折扣10-49 件 95 折50-99 件 9 折100 件以上 85 折客户等级系数free 1.0pro 0.95enterprise 0.9最终价格 基础价 × 数量 × 数量折扣 × 客户等级系数这个计算过程我在 tool 的返回摘要里会简单说明比如“已应用 9 折数量折扣和 pro 等级 95 折”。代理看到这个说明后在跟用户解释价格时会更准确。我试过不说明代理经常自己编一个折扣理由容易误导用户。5. 常见问题与排查技巧实录5.1 代理不调用我的 tool 怎么办这是最常见的问题。排查顺序第一确认客户端日志里能看到你的 server 注册成功第二检查 tool 的 description 是否足够明确代理不调用通常是因为描述太模糊或者跟其他 tool 重叠第三在对话里显式引导比如“请使用报价工具”看是否能触发。如果显式引导能触发说明是描述问题如果还不能说明是连接问题。我遇到过一次代理死活不调用最后发现是 tool 的 inputSchema 里有个必填参数没有 default代理不知道传什么就放弃了。加上 default 之后立刻正常。5.2 stdio 通信中断的排查stdio server 最常见的故障是进程崩溃或输出污染。排查方法把 server 单独跑起来手动发一条 JSON-RPC 消息测试。比如echo {jsonrpc:2.0,id:1,method:tools/list,params:{}} | node dist/index.js如果返回正常的 JSON说明 server 本身没问题问题在客户端配置。如果没返回或报错看 stderr 的输出。我遇到过因为 Node 版本不兼容导致 SDK 内部报错的情况升级 Node 到 20 就好了。5.3 报价状态丢失的问题前面提到我用内存 Map 存报价状态。有一次测试时代理问完价隔了一会儿才确认结果 confirm 失败说报价单不存在。查下来是 server 进程被客户端重启了内存状态清空。解决办法有两个一是把状态持久化到文件或数据库二是让 confirm_order 接受完整的报价信息作为参数不依赖 server 端状态。我选了后者因为更简单代理本来就能拿到报价单的完整数据。问题现象可能原因排查方法解决方案代理不调用 tool描述模糊/参数缺 default显式引导测试优化 description加 defaultserver 无响应stdout 被污染检查 console.log调试信息走 stderr报价单找不到状态丢失检查 server 是否重启状态外置或参数传递启动超时依赖加载慢看客户端日志预编译成 JS5.4 代理传参格式错误的处理LLM 有时候会把数字传成字符串比如quantity: 5而不是5。zod 的.number()会直接拒绝导致调用失败。我的做法是在 schema 里用z.coerce.number()它会自动转换。这个细节很小但能显著提升鲁棒性。另外对于枚举值如果代理传了不在枚举里的值zod 也会拒绝我建议在 description 里把可选值列清楚减少代理瞎猜的概率。6. 上线后的效果与后续扩展方向6.1 实际运行数据上线两周后我统计了一下通过 MCP 渠道进来的报价请求占总请求的 23%而且这个比例还在涨。更重要的是这些请求的转化率比手动操作高因为代理在报价后可以直接引导用户确认中间没有跳失。有个做 agent 的客户跟我说他把我的 MCP server 接进了自己的采购 agent现在整个询价流程全自动他只需要最后点确认。6.2 可以继续做的扩展目前我只暴露了报价相关的 tool后续我打算加两个方向一是check_inventory查库存让代理在报价前先确认有货二是subscribe_updates订阅更新让代理能主动推送价格变动。另外 MCP 还支持 resources我可以用它暴露产品目录让代理在对话开始时就加载产品信息减少来回调用。还有一个值得关注的方向是 agent-to-agent commerce 的标准化。现在每个产品的 MCP server 都是自己定义的 tool 名称和参数代理需要针对每个产品适配。如果未来出现通用的报价协议比如统一的request_quote接口规范那代理就能跨产品自动比价。我现在的设计尽量往通用方向靠比如参数名用product_name而不是我自己的内部字段名就是为了将来好迁移。6.3 给同样想做 MCP server 的朋友的建议如果你也在考虑给自己的产品加 MCP server我的建议是先想清楚代理调用你的产品能完成什么闭环不要为了 MCP 而 MCP。一个只能返回静态信息的 MCP server 价值有限能完成“发现-报价-确认”这种有状态流程的才有商业价值。另外tool 的数量宁少勿多描述宁细勿粗这两条是我踩坑最多的地方。最后调试时一定要看客户端的日志Claude Desktop 和 Cursor 都有 MCP 日志面板里面能看到完整的请求响应比你自己猜高效得多。我在实际使用中发现MCP server 的维护成本比想象中低因为协议本身很稳定SDK 也封装得很好。真正花时间的是 tool 的设计和描述优化这部分需要反复测试和调整。但一旦跑通你的产品就真正进入了 AI 代理的生态这在未来一两年会越来越重要。
网站建设高端定制企业官网