尧图精选

独立开发者实战:为小产品接入MCP server,让AI代理自动发现并报价

🕒 发布时间:2026/10/1 7:42:11 📁 来源:尧图网络
1. 一个独立开发者为什么要给自己的小产品接上 MCP server先说清楚背景。我手上有一个自己维护的小产品规模不大属于那种“一个人写代码、一个人运维、一个人接客服”的状态。它提供一项具体的服务能力过去用户想用它得先打开网页、注册、手动填参数、点提交整个链路对人是友好的但对 AI 代理完全不友好——AI 代理根本不知道这个产品存在更别说自动调用它。这两年 AI 代理的形态变化很快。Claude、Cursor 这类工具已经不只是“帮你补全代码”它们开始具备自主规划任务、调用外部工具、多步执行的能力。问题在于代理能调用的工具基本被限制在它自己生态里预置的那几个。你想让它用上你自己的服务过去只有两条路要么写一个插件塞进它的插件市场审核周期长、门槛高要么让用户手动把结果复制粘贴进去体验稀碎。MCPModel Context Protocol改变的就是这件事。它本质上是一套标准化的“工具描述 调用”协议让 AI 代理能够动态发现外部服务、理解每个工具能干什么、需要什么参数然后自主决定要不要调用。你可以把它理解成给 AI 代理准备的一份“菜单”代理拿到菜单看到有“报价”这道菜知道需要传什么原料就会在合适的时候自己下单。我给自己的小产品写了一个 MCP server做完之后的效果是用户在 Claude 或 Cursor 里说一句“帮我看看这个需求大概多少钱”代理会自动发现我的报价工具、填入参数、拿到结果整个过程用户不需要离开对话窗口。这就是标题里说的“AI 代理现在可以自动发现并报价它了”。这篇文章适合两类人看。一类是手里有小产品、想让 AI 代理能调用它的独立开发者另一类是想搞明白 MCP server 到底怎么落地、不想只看官方文档示例的工程师。我会把设计思路、协议细节、实操步骤、踩过的坑全部摊开讲代码和配置都能直接抄。2. MCP server 到底是什么把“工具”翻译成代理能听懂的话2.1 从“函数调用”到“协议发现”的思维转变很多人第一次接触 MCP会把它和传统的 function calling 混为一谈。两者确实像但有一个关键区别function calling 是你在代码里硬编码告诉模型“有这么个函数”而 MCP 是让模型自己去问“你这儿有哪些函数”。传统做法里你写一个聊天应用想让模型查天气你得在 system prompt 或者 tools 参数里把get_weather(city)这个函数的签名写死。模型只能看到你预先塞给它的工具。换一个应用、换一个模型这套描述就得重写一遍。MCP 的思路是把“工具提供方”和“工具使用方”解耦。你的产品实现一个 MCP server对外暴露一组标准接口列出我有哪些工具、每个工具的参数 schema 是什么、调用后返回什么。任何支持 MCP 的客户端Claude Desktop、Cursor、以及越来越多 IDE都能连上你的 server自动拉取这份“菜单”。这个转变的意义在于你的产品不再需要为每个 AI 平台单独适配只要实现一次 MCP server所有支持该协议的代理都能用。这就是为什么标题里用了“发现”这个词——发现是代理主动发起的不是你推给它的。2.2 MCP 的三种核心原语tools、resources、promptsMCP 协议里server 可以对外提供三类东西理解这三类的区别是设计的第一步。Tools工具代理可以主动调用的动作比如“计算报价”“创建订单”“查询库存”。这是最核心的一类也是我这次主要实现的部分。工具是有副作用的代理调用前通常会请求用户确认。Resources资源代理可以读取的数据比如“产品目录”“价格表”“文档”。资源是只读的代理把它当作上下文来用不会产生副作用。Prompts提示模板server 预置的提示词模板用户可以主动选用。这一类用得相对少适合把常见任务封装成“一键指令”。我的小产品核心诉求是“让代理帮我报价”所以重点放在 Tools 上。但我也顺手暴露了一个 Resources把产品的服务说明和计费规则做成只读资源这样代理在报价前能先读到规则报出来的价格更靠谱。提示不要一上来就把所有功能都做成 tool。有副作用的、需要用户确认的才适合做 tool纯查询、纯展示的优先考虑 resource。这个边界划清楚代理的行为会稳定很多。2.3 为什么选 MCP 而不是自己写一套 API 对接有人会问我直接写个 REST API然后让代理通过 HTTP 调用不就行了技术上可行但有几个现实问题。第一发现机制。REST API 没有自描述能力代理怎么知道你有/quote这个端点、需要传哪些字段你得额外维护一份 OpenAPI 文档还得指望代理能读懂。MCP 把这份描述内建到协议里代理连上就能拿到结构化的工具列表。第二传输层适配。MCP 支持 stdio 和 HTTP 两种传输方式。stdio 模式下server 就是一个本地进程客户端通过标准输入输出和它通信不需要开端口、不需要处理跨域、不需要考虑鉴权暴露。对独立开发者来说这种“本地进程”模式部署成本极低。第三生态红利。Claude Desktop、Cursor 这些工具已经把 MCP 客户端做进去了你实现 server 就能直接接入不用自己写客户端适配层。这个红利在 2024 年下半年之后越来越明显。我选 MCP核心原因就是一次实现、多处可用以及协议自带发现能力。这两点对一个小产品来说性价比太高了。3. 动手前的设计报价工具该怎么切分3.1 先想清楚代理会怎么用你的工具写代码之前我花了半天时间做一件事模拟代理的使用路径。我把自己想象成一个 AI 代理接到用户指令“帮我估算一下这个项目的报价”我会怎么一步步走第一步我得知道有这个报价能力。这对应 MCP 的tools/list代理会拉取工具清单。第二步我得知道报价需要哪些输入。这对应每个 tool 的inputSchema代理会读这个 schema 来决定向用户追问什么。第三步我调用工具拿到结果。这对应tools/call。第四步如果报价依赖一些规则比如不同服务档位的单价我最好能先读到这些规则。这对应 resource。把这四步想清楚工具的设计就出来了一个get_quote工具输入是服务类型、工作量、紧急程度等参数输出是价格区间和说明一个pricing_rules资源暴露计费规则。3.2 参数设计让代理“问得出来”参数设计是 MCP server 里最容易被低估的环节。代理不是人它不会“猜”你的意图。如果你的参数叫p1、p2代理根本不知道要填什么。参数名和描述必须自解释。我最初的版本里报价工具只有一个参数spec让代理把整个需求描述塞进去。结果代理经常传一段模糊的话我的后端解析不出来报价失败。后来我改成结构化参数参数名类型是否必填说明service_typestring枚举是服务类型如 web、mobile、dataworkloadnumber是预估工作量单位为人天urgencystring枚举否紧急程度normal 或 rush默认 normaldetailstring否补充说明用于人工复核改完之后代理的调用成功率明显上升。因为每个参数都有明确的类型和枚举值代理知道该向用户追问什么也知道自己填的值合不合法。注意枚举值一定要在 schema 里写全。代理看到service_type是 string 但没有枚举约束时可能会填“网站开发”“做个 App”这种自然语言你的后端就得做模糊匹配非常痛苦。把枚举写死代理会乖乖从里面选。3.3 返回值设计给代理“能读懂”的结果返回值同样重要。代理拿到结果后往往要基于结果继续推理或向用户解释。如果你返回一个裸数字5000代理不知道这是人民币还是美元、是总价还是单价。我的做法是返回结构化的文本内容MCP 的 tool 返回格式支持 content 数组每项可以是 text、image 等类型。我返回一段结构清晰的文本报价结果 - 服务类型web - 工作量10 人天 - 紧急程度normal - 预估价格45000 - 55000 元 - 说明最终价格以人工评估为准此报价为初步估算这种格式代理读起来毫无压力转述给用户时也不会丢信息。我试过返回纯 JSON代理有时候会把 JSON 原样吐给用户体验反而差。文本 结构化字段的组合是目前最稳的返回方式。4. 从零实现一个 MCP server完整实操4.1 环境准备与依赖选择我用的是 Node.js 生态因为 MCP 官方的 TypeScript SDK 成熟度最高文档也最全。Python SDK 也有但如果你要接入 Cursor 这类工具TS 版本的兼容性更省心。环境要求很简单Node.js 18 以上我用的是 20 LTSnpm 或 pnpm一个支持 MCP 的客户端我用 Claude Desktop 和 Cursor 各测了一遍初始化项目mkdir my-mcp-server cd my-mcp-server npm init -y npm install modelcontextprotocol/sdk zodmodelcontextprotocol/sdk是官方 SDKzod用来定义参数 schema。这两个是核心依赖其他都是可选的。4.2 搭建 server 骨架MCP server 的骨架非常短核心就是创建一个 Server 实例注册能力然后连上传输层。import { Server } from modelcontextprotocol/sdk/server/index.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; import { ListToolsRequestSchema, CallToolRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from modelcontextprotocol/sdk/types.js; const server new Server( { name: my-product-mcp, version: 1.0.0 }, { capabilities: { tools: {}, resources: {} } } ); // 后续在这里注册 tools 和 resources const transport new StdioServerTransport(); await server.connect(transport);这段代码里有两个关键点。第一capabilities声明了你的 server 支持哪些能力客户端会据此决定要不要向你发对应的请求。第二StdioServerTransport表示用标准输入输出通信这是本地进程模式客户端会以子进程方式启动你的 server。提示stdio 模式下千万不要往 stdout 打印调试日志。stdout 是协议通信通道你打印一行日志就可能破坏协议帧导致客户端解析失败。调试信息一律走 stderr用console.error。4.3 注册 tools/list让代理看到你的工具代理连上 server 后第一件事就是拉工具清单。你需要处理ListToolsRequestSchemaserver.setRequestHandler(ListToolsRequestSchema, async () { return { tools: [ { name: get_quote, description: 根据服务类型和工作量估算项目报价返回价格区间, inputSchema: { type: object, properties: { service_type: { type: string, enum: [web, mobile, data], description: 服务类型web 网站、mobile 移动端、data 数据服务, }, workload: { type: number, description: 预估工作量单位为人天必须大于 0, }, urgency: { type: string, enum: [normal, rush], description: 紧急程度默认 normal, }, detail: { type: string, description: 补充说明可选, }, }, required: [service_type, workload], }, }, ], }; });description字段是给代理看的写得越清楚代理判断“什么时候该调用这个工具”就越准。我一开始把 description 写成“获取报价”代理经常在用户只是随口问价格时也去调用。后来改成“根据服务类型和工作量估算项目报价返回价格区间”代理的调用时机就合理多了。4.4 注册 tools/call真正执行报价逻辑工具被调用时处理CallToolRequestSchemaconst PRICING { web: 4500, mobile: 6000, data: 5500, }; server.setRequestHandler(CallToolRequestSchema, async (request) { if (request.params.name ! get_quote) { throw new Error(未知工具: ${request.params.name}); } const { service_type, workload, urgency normal, detail } request.params.arguments; if (!PRICING[service_type]) { return { content: [{ type: text, text: 不支持的服务类型${service_type} }], isError: true, }; } if (typeof workload ! number || workload 0) { return { content: [{ type: text, text: 工作量必须是大于 0 的数字 }], isError: true, }; } const base PRICING[service_type] * workload; const multiplier urgency rush ? 1.5 : 1; const low Math.round(base * multiplier * 0.9); const high Math.round(base * multiplier * 1.1); const text [ 报价结果, - 服务类型${service_type}, - 工作量${workload} 人天, - 紧急程度${urgency}, - 预估价格${low} - ${high} 元, detail ? - 补充说明${detail} : , - 说明最终价格以人工评估为准此报价为初步估算, ] .filter(Boolean) .join(\n); return { content: [{ type: text, text }] }; });这里有几个实操细节值得说。第一参数校验必须自己做。代理虽然会按 schema 填但不保证 100% 合规尤其是枚举值和数字范围。第二错误要用isError: true返回而不是抛异常。抛异常会让整个调用链断掉代理拿不到可读的错误信息用isError返回代理能读到错误文本并向用户解释。第三价格计算里的0.9和1.1是给报价留的浮动区间实际业务里你可以换成更复杂的规则。4.5 注册 resources把计费规则暴露给代理资源部分处理两个请求列出资源和读取资源。server.setRequestHandler(ListResourcesRequestSchema, async () { return { resources: [ { uri: pricing://rules, name: 计费规则, description: 产品各服务类型的单价和计费说明, mimeType: text/plain, }, ], }; }); server.setRequestHandler(ReadResourceRequestSchema, async (request) { if (request.params.uri ! pricing://rules) { throw new Error(未知资源: ${request.params.uri}); } return { contents: [ { uri: pricing://rules, mimeType: text/plain, text: 计费规则web 4500 元/人天mobile 6000 元/人天data 5500 元/人天。紧急项目上浮 50%。, }, ], }; });资源的价值在于代理在报价前可以先读规则报出来的价格解释起来更有依据。我实测下来暴露资源之后代理向用户解释报价时会更详细用户信任感也更强。4.6 接入 Claude Desktop 和 Cursorserver 写完了得让客户端能连上。Claude Desktop 的配置在claude_desktop_config.json里{ mcpServers: { my-product: { command: node, args: [/absolute/path/to/my-mcp-server/index.js] } } }Cursor 的配置在设置里的 MCP 部分格式类似也是指定 command 和 args。路径一定要用绝对路径相对路径在客户端启动子进程时经常找不到文件这是新手最容易踩的坑。配置完重启客户端在对话里问一句“帮我估算一个 10 人天的网站项目报价”代理应该会自动发现get_quote工具并调用。如果没反应先检查 server 进程有没有正常启动再看客户端日志里有没有 MCP 相关的报错。5. 踩坑实录那些文档里不会写的问题5.1 代理不调用工具或者乱调用这是最常见的问题原因通常有三个。第一工具描述太模糊。代理判断要不要调用工具主要看 description。如果 description 写得太泛代理要么不敢调要么在不该调的时候调。解决办法是把 description 写成“什么场景下用、输入是什么、输出是什么”的完整句子。第二参数 schema 不完整。缺 required、缺 enum、缺 description代理就不知道该怎么填干脆不调。我建议每个参数都写 description枚举值全部列全。第三工具太多。如果你一次暴露十几个工具代理的选择困难症就犯了。我的经验是单个 server 暴露的工具控制在 5 个以内超过就考虑拆分或者合并。5.2 stdio 模式下的日志灾难前面提过一次这里再强调。stdio 模式下stdout 是协议通道。我一开始习惯性地用console.log打调试信息结果客户端直接报“无法解析响应”。排查了半天才意识到是日志污染了协议流。正确做法所有调试信息走console.error它会输出到 stderr不影响协议。如果你需要更结构化的日志可以写文件但绝对不要碰 stdout。5.3 中文参数导致的编码问题我的产品面向中文用户参数里难免有中文。早期版本里代理传过来的中文在某些客户端下会出现乱码。排查后发现是传输层的编码没统一。解决办法是在 server 启动时显式设置编码并且所有字符串处理都用 UTF-8。process.stdin.setEncoding(utf8); process.stdout.setEncoding(utf8);这两行加上之后中文乱码问题基本消失。如果你也做中文场景建议一开始就加上。5.4 常见问题速查表现象可能原因排查方向客户端连不上 server路径错误或进程启动失败检查绝对路径手动运行 server 看报错代理看不到工具capabilities 未声明或 list 处理异常检查 capabilities 和 ListTools 返回值代理不调用工具description 模糊或 schema 不完整补全 description 和参数约束调用返回乱码编码未统一显式设置 stdin/stdout 为 utf8调用后客户端崩溃stdout 被日志污染调试信息改用 console.error报价结果代理读不懂返回格式过于原始返回结构化文本而非裸数字6. 上线之后agent-to-agent commerce 的一点观察6.1 代理之间的“自动报价”意味着什么标题里我用了“agent-to-agent commerce”这个词这不是噱头。当你的产品能被 AI 代理发现和调用就意味着代理可以代表用户来和你的产品交互。用户说“帮我找个能做网站的人报个价”代理去发现你的 MCP server、调用报价工具、拿到结果、比较几家、给出建议——整个过程用户只说了。一句话。这对小产品来说是个机会。过去你需要在搜索引擎、应用商店、社交平台里抢曝光现在多了一个入口被 AI 代理发现。而这个入口的门槛目前还很低因为实现 MCP server 的独立开发者还不算多。6.2 我实测下来的几个经验第一报价工具要能容错。代理传参不会永远完美你的工具要能处理边界情况返回可读的错误而不是崩溃。第二返回结果要“可转述”。代理拿到结果后往往要转述给用户所以返回文本要结构清晰、信息完整别让代理去猜。第三资源比工具更适合放规则。计费规则、服务说明这类只读内容做成 resource 比塞进 tool 的返回值更合理代理读起来也更自然。第四先跑通再优化。我第一版 server 只有 80 行代码功能很糙但能跑通完整链路。跑通之后再去优化参数设计、错误处理、返回格式效率高得多。一上来就追求完美很容易卡在细节里出不来。6.3 后续可以扩展的方向这个 server 目前只做了报价后续我打算加几个方向。一是把“下单”做成 tool让代理能直接创建订单当然要加用户确认环节。二是把产品文档做成 resource让代理在回答用户问题时能引用。三是考虑加一个“查询订单状态”的 tool让用户能通过代理查进度。每加一个能力都要重新想一遍“代理会怎么用”。这个思考过程比写代码本身更重要。工具设计得好代理用起来顺设计得差代理要么不用要么用错。最后分享一个小技巧在 server 里加一个“自检”工具输入为空返回 server 的版本、支持的工具列表、当前配置。调试的时候让代理调用一下能快速确认 server 状态比翻日志快得多。这个工具我每次接入新客户端都会先用一遍省了不少排查时间。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →