独立开发者如何用MCP协议让AI代理自动调用你的小产品
发布时间:2026/10/1 5:34:53来源:尧图网络
1. 一个独立开发者为什么要给自己的小产品接上 MCP先说结论我给我的小产品写了一个 MCP server现在 Claude、Cursor 这类 AI 代理在对话里就能直接「发现」它、读取它的能力清单甚至自动完成一次报价请求。整个过程不需要我写一行前端对接代码也不需要用户手动复制粘贴任何参数。这件事的背景是这样的。我手上有一个很小的 SaaS 工具功能很垂直——帮独立开发者做报价单的自动生成和成本估算。以前用户要用它路径是打开网页、注册、填表单、点生成、下载。这套流程对真人来说没问题但对现在越来越多的「AI 代理工作流」来说就非常别扭。因为用户现在习惯在 Claude 或者 Cursor 里直接说一句「帮我给这个项目估个价生成一份报价单」然后希望代理自己去把这件事办完。问题就出在「自己去办」这四个字上。AI 代理再聪明它也没法凭空知道世界上有一个叫某某报价工具的东西更不知道这个工具需要哪些参数、返回什么格式。它需要一个标准化的「自我介绍」入口。MCPModel Context Protocol就是干这个的。MCP 本质上是一套让 AI 代理和外部工具、数据源对话的协议。你可以把它理解成「AI 世界的 USB-C 接口」——以前每个工具都要为每个 AI 客户端单独写适配现在只要实现一次 MCP server所有支持 MCP 的客户端Claude Desktop、Cursor、各种 IDE 插件都能即插即用。我这次做的事情就是把我那个小产品包装成一个 MCP server暴露出「发现能力」和「报价」两个核心动作。适合谁看这篇三类人。第一类是做小产品、小工具的独立开发者想让自己的东西被 AI 代理调用第二类是在用 Claude、Cursor 做 agent 工作流的人想搞清楚 MCP server 到底怎么接第三类是对 agent-to-agent commerce 这个方向好奇、想动手试一下的技术人。不需要你是协议专家但最好有一点 Node 或 Python 基础知道 JSON 长什么样。下面我按「为什么这么设计 → 核心细节 → 完整实操 → 踩坑排查」的顺序把我这次从零到跑通的完整过程拆开讲。中间涉及参数选择、协议字段、调试方法的地方我都会把「为什么这么选」讲清楚方便你直接抄作业或者改成自己的版本。2. 整体设计思路为什么是 MCP而不是写个 API 文档2.1 传统 API 对接和 MCP 对接的本质区别在动手之前我先想清楚了一件事我到底是在解决「人调用工具」的问题还是「代理调用工具」的问题。这两件事看起来像实际上完全不同。传统 API 的思路是给人看的。我写一份 REST 文档说明POST /quote需要传project_name、hours、rate返回一个 JSON。人看了文档知道怎么填。但 AI 代理面对这份文档时它得先「读到」文档再「理解」字段含义再「拼」出请求中间任何一步都可能出错。而且每个 AI 客户端的工具调用格式还不一样Claude 有 Claude 的 tool use 格式Cursor 有 Cursor 的我得为每个都适配一遍。MCP 的思路是给代理看的。它把「工具清单」和「调用方式」标准化了。代理启动时会向 MCP server 发一个「列出你有哪些工具」的请求server 返回一份结构化的清单每个工具带名字、描述、参数 schema。代理拿到这份清单就知道自己能干什么、需要什么参数。调用的时候代理按 schema 填参数server 执行完返回结果。整个过程代理不需要读自然语言文档全靠结构化数据。这个区别带来的直接好处是我只需要维护一份 MCP server所有支持 MCP 的客户端自动就能用。这就是我选 MCP 而不是「再写一份 OpenAPI 文档」的核心理由。2.2 为什么把「发现」和「报价」拆成两个工具设计工具清单的时候我一开始想做一个大而全的工具叫handle_quote参数里塞一个action字段根据 action 决定是查询还是报价。后来我把它拆成了两个独立工具discover_capabilities和create_quote。拆开的理由很实际。AI 代理在决定调用哪个工具时是靠工具的名字和描述来判断的。如果只有一个handle_quote代理得先理解action字段的取值含义多了一层认知负担出错概率上升。拆成两个之后discover_capabilities的描述是「返回本服务支持的所有报价能力、计价维度和限制」create_quote的描述是「根据项目参数生成一份报价单」代理一看名字就知道什么时候该用哪个。这其实是一个通用的 MCP 设计经验工具粒度要匹配代理的决策粒度。代理做决策时是「我现在要干这件事」那工具就应该对应「这件事」而不是对应「这一大类事」。粒度太粗代理要自己做二次判断粒度太细工具数量爆炸代理选择困难。两个到五个工具通常是一个 MCP server 比较舒服的区间。2.3 报价逻辑放在 server 端还是暴露给代理还有一个关键决策报价的计算逻辑是放在 MCP server 里算好返回还是把原始数据返回给代理让它自己算。我选择放在 server 端算。原因是报价涉及我的业务规则——不同项目类型的基础费率、加急系数、复杂度加成、折扣门槛这些是我的核心资产也是我产品差异化的地方。如果我把原始参数返回给代理等于把定价逻辑暴露了而且代理每次算出来的结果可能不一致用户体验反而差。放在 server 端还有一个好处结果可复现。同样的输入永远得到同样的报价这对商业场景很重要。代理拿到的是一个确定的数字和一份结构化的明细它可以直接展示给用户也可以继续拿去做后续操作比如生成 PDF、发邮件。提示涉及商业逻辑、计费规则、权限判断的部分强烈建议放在 server 端。MCP server 不只是「数据搬运工」它应该是「业务能力的封装」。3. 核心细节解析MCP server 到底长什么样3.1 MCP 的三种核心原语Tools、Resources、PromptsMCP 协议里server 能向客户端暴露三类东西理解这三类的区别是设计的基础。Tools工具是代理可以主动调用的动作比如「生成报价」「查询库存」。它有输入参数和输出结果是「做事情」的。Resources资源是代理可以读取的数据比如「当前费率表」「历史报价记录」它是「读数据」的通常不产生副作用。Prompts提示模板是预定义的提示词模板客户端可以把它作为快捷入口展示给用户比如「帮我做一份标准报价」这种一键触发的场景。我这次主要用了 Tools因为核心诉求是「让代理能执行报价动作」。Resources 我也加了一个暴露当前的费率表方便代理在报价前先了解计价维度。Prompts 暂时没加因为我的场景里用户更习惯直接对话不太需要预设模板。这里有个容易混淆的点Resources 和 Tools 都能返回数据区别在于「谁发起」。Tools 是代理决定要调用才调用Resources 是客户端可以主动加载进上下文。如果你希望代理「随时知道」某些信息用 Resources如果希望代理「按需执行」用 Tools。3.2 工具描述description为什么比代码还重要写 MCP server 的时候我花在工具description字段上的时间比写实际业务逻辑还多。这不是夸张。因为代理选择工具、填参数全靠这段描述。描述写得含糊代理就会用错工具或者参数填错。我一开始写的描述是「生成报价单」测试时发现代理经常在用户只是「问价格」的时候就调用它其实用户只是想了解计价方式。后来我把描述改成「根据明确的项目参数工时、费率、复杂度生成一份正式报价单仅在用户确认要出报价时调用」误触发率立刻降下来了。参数描述同样重要。每个参数的description要写清楚「这是什么、单位是什么、取值范围、给个例子」。比如hours参数我写的是「预估工时单位为小时必须是正数例如 40 表示 40 小时」。代理看到这个就知道不能填「两天」这种模糊值。提示把工具描述当成「写给一个聪明但完全不了解你业务的实习生看的说明书」。它不会猜你写多清楚它就理解多清楚。3.3 参数 schema 的设计用 JSON Schema 约束代理行为MCP 的工具参数用 JSON Schema 定义。这个 schema 不只是给代理看的文档它还是运行时的校验规则。代理填的参数如果不符合 schemaserver 可以直接拒绝避免脏数据进入业务逻辑。我这次的核心参数大概是这样设计的参数名类型是否必填说明约束project_namestring是项目名称长度 1-100project_typestring是项目类型枚举web/app/design/consultinghoursnumber是预估工时大于 0小于 10000complexitystring否复杂度枚举low/medium/high默认 mediumrushboolean否是否加急默认 false用枚举enum约束project_type和complexity是关键。如果我用自由字符串代理可能填「网站」「网页」「web 项目」各种变体我后端就得做一堆容错。用枚举之后代理只能从固定值里选数据干净很多。必填和选填的划分也有讲究。必填项越少代理调用成功率越高但业务信息可能不全。我的原则是没有它就算不出结果的才设为必填。complexity和rush都有合理默认值所以设为选填。4. 完整实操从零跑通一个可被代理发现的 MCP server4.1 环境准备与依赖选择我选的是 Node.js 官方 MCP SDK。理由有两个一是官方 SDK 对协议细节封装得比较完整不用自己处理握手、能力协商这些底层东西二是 Node 生态里 JSON 处理很顺手我的业务逻辑本来就是 JS 写的复用成本低。环境要求不复杂Node.js 18 或以上我用的是 20 LTSnpm 或 pnpm一个支持 MCP 的客户端我用 Claude Desktop 和 Cursor 各测了一遍初始化项目mkdir quote-mcp-server cd quote-mcp-server npm init -y npm install modelcontextprotocol/sdk装完之后package.json里把type设成module因为 SDK 用的是 ESM 风格。这一步如果漏了import 会报错我第一次就栽在这。4.2 搭建 server 骨架与注册工具核心代码结构其实很清晰。先创建 server 实例声明自己的能力然后注册工具最后接上传输层。import { Server } from modelcontextprotocol/sdk/server/index.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; import { CallToolRequestSchema, ListToolsRequestSchema, } from modelcontextprotocol/sdk/types.js; const server new Server( { name: quote-server, version: 1.0.0 }, { capabilities: { tools: {}, resources: {} } } );capabilities这里声明我支持 tools 和 resources。如果只声明了 tools 却去响应 resources 请求客户端可能不认。声明什么就实现什么这是协议的基本礼貌。接下来注册工具清单。ListToolsRequestSchema对应的 handler 返回工具数组server.setRequestHandler(ListToolsRequestSchema, async () { return { tools: [ { name: discover_capabilities, description: 返回本报价服务支持的项目类型、计价维度和限制条件。在报价前调用可了解可用选项。, inputSchema: { type: object, properties: {} }, }, { name: create_quote, description: 根据明确的项目参数生成正式报价单。仅在用户确认需要出报价时调用。, inputSchema: { type: object, properties: { project_name: { type: string, description: 项目名称1-100 字符 }, project_type: { type: string, enum: [web, app, design, consulting], description: 项目类型, }, hours: { type: number, description: 预估工时单位小时正数 }, complexity: { type: string, enum: [low, medium, high], description: 复杂度默认 medium, }, rush: { type: boolean, description: 是否加急默认 false }, }, required: [project_name, project_type, hours], }, }, ], }; });注意discover_capabilities的inputSchema是空对象因为它不需要参数。这个设计让代理可以「零成本」地先探一下服务能力再决定要不要报价。4.3 实现报价计算与参数校验工具调用的 handler 里我先做参数校验再走业务逻辑。校验这一步不能省因为代理填的参数虽然受 schema 约束但边界值比如 hours 填了 0 或者负数还是可能漏进来。server.setRequestHandler(CallToolRequestSchema, async (request) { const { name, arguments: args } request.params; if (name discover_capabilities) { return { content: [{ type: text, text: JSON.stringify({ project_types: [web, app, design, consulting], complexity_levels: [low, medium, high], base_rates: { web: 800, app: 1000, design: 600, consulting: 1200 }, rush_multiplier: 1.5, complexity_multiplier: { low: 0.9, medium: 1.0, high: 1.3 }, }), }], }; } if (name create_quote) { const { project_name, project_type, hours, complexity medium, rush false } args; if (typeof hours ! number || hours 0 || hours 10000) { return { content: [{ type: text, text: 参数错误hours 必须是 0 到 10000 之间的正数 }], isError: true, }; } const baseRate { web: 800, app: 1000, design: 600, consulting: 1200 }[project_type]; const complexityMultiplier { low: 0.9, medium: 1.0, high: 1.3 }[complexity]; const rushMultiplier rush ? 1.5 : 1.0; const subtotal baseRate * hours * complexityMultiplier; const total subtotal * rushMultiplier; return { content: [{ type: text, text: JSON.stringify({ project_name, project_type, hours, complexity, rush, base_rate: baseRate, subtotal: Math.round(subtotal), total: Math.round(total), currency: CNY, }, null, 2), }], }; } return { content: [{ type: text, text: 未知工具${name} }], isError: true, }; });报价公式是基础费率 × 工时 × 复杂度系数 × 加急系数。这个公式本身很简单但每个系数的取值是我根据实际业务定的。比如加急系数 1.5是因为加急项目通常要占用额外资源、压缩其他排期成本确实高出一截。复杂度系数 low 是 0.9 而不是 1.0 以下更多是因为再简单的项目也有基础沟通成本不能无限打折。4.4 接上传输层并在客户端里验证最后一步是把 server 接上 stdio 传输层让它能被客户端启动const transport new StdioServerTransport(); await server.connect(transport);stdio 传输的意思是客户端会把这个 server 当成一个子进程启动通过标准输入输出通信。这是本地 MCP server 最常见的模式配置简单不需要开端口。在 Claude Desktop 里配置找到配置文件macOS 一般在~/Library/Application Support/Claude/claude_desktop_config.json加上{ mcpServers: { quote-server: { command: node, args: [/绝对路径/quote-mcp-server/index.js] } } }路径一定要用绝对路径相对路径在客户端启动子进程时经常找不到文件这是我踩过的第一个坑。配置完重启客户端在对话里问「你有哪些报价相关的工具」如果代理能列出discover_capabilities和create_quote说明接上了。Cursor 的配置类似在 MCP 设置里添加 server命令和参数一样。我两个客户端都测了行为基本一致说明协议层的标准化确实到位。5. 常见问题与排查技巧实录5.1 代理「看不见」我的工具怎么办这是最高频的问题。表现是配置写好了客户端也重启了但代理就是说自己没有报价工具。排查顺序我总结成一张表现象可能原因排查方法完全看不到工具配置文件路径错检查绝对路径手动node index.js看能否启动看不到工具server 启动即崩溃看客户端日志通常是依赖缺失或语法错误看到工具但调用失败schema 不合法用 JSON 校验工具检查 inputSchema调用返回空handler 没匹配到工具名打印 request.params.name 确认拼写我遇到过一次原因是package.json没设type: module导致 ESM import 报错server 启动就挂了但客户端日志里只显示「server disconnected」不告诉你具体原因。后来我养成习惯改完代码先在终端手动跑一遍node index.js确认能正常启动再配到客户端里。5.2 代理填错参数、乱调工具的应对代理不是每次都听话。我测试时遇到过代理在用户只是「问一下大概多少钱」的时候就直接调用了create_quote还自己编了个 hours 值。应对方法有两个层面。第一层是描述优化前面说过把「仅在用户确认要出报价时调用」写进 description能挡掉大部分误触发。第二层是 server 端兜底对关键参数做合理性检查比如 hours 如果明显是代理瞎填的比如 99999直接返回错误提示让代理重新问用户。还有一个技巧在返回结果里带上「下一步建议」。比如报价成功后返回文本里加一句「如需调整参数可重新调用本工具」。代理看到这句话会更倾向于在参数不确定时先跟用户确认而不是硬编一个值。5.3 日志与调试stdio 模式下怎么看输出stdio 模式下有个坑你不能用console.log打日志因为标准输出被协议占用了打日志会污染通信导致客户端解析失败。正确做法是用console.error它走标准错误不会干扰协议通信客户端日志里也能看到。我调试阶段所有关键节点都加了console.error比如「收到工具调用请求xxx」「参数校验通过」「报价计算完成」。提示MCP server 里console.log是禁忌console.error才是你的朋友。这个坑不踩一次很难记住。5.4 报价结果不稳定、每次不一样如果发现同样的输入代理展示的报价每次不同大概率是代理在「转述」你的结果时自己做了加工。解决办法是让 server 返回结构化的、带明确字段名的 JSON并在描述里说明「请原样展示 total 字段」。我一开始返回的是纯文本「总价是 3200 元」代理有时会四舍五入成「约 3000 元」。改成返回 JSON 并强调字段后代理就老实了直接引用total的值。这也说明一个原则给代理的数据越结构化它转述时越不容易失真。6. 关于 agent-to-agent commerce 的一点个人判断跑通这个 MCP server 之后我最大的感受是agent-to-agent commerce 这件事门槛比想象中低但设计比想象中重要。低在于协议层的东西官方 SDK 已经封装得很好了一个下午就能让代理发现并调用你的服务。重要在于代理是个「不会猜」的调用方你的工具描述、参数 schema、返回结构每一个细节都直接决定它能不能用对。以前写给人用的 API文档写得糙一点人还能靠常识补写给代理用的 MCP描述含糊一点它就直接用错。我现在把这个 server 当成产品的「代理入口」在维护和网页入口、API 入口并列。后续我打算再加一个list_recent_quotes工具让代理能读取历史报价这样用户在对话里说「把上次那份报价改一下工时」代理就能自己找到并更新。这个扩展方向我觉得挺有意思等跑通了再单独写一篇。如果你也在做小产品我建议尽早把 MCP server 加上。不是因为它是风口而是因为它真的能让你的产品出现在用户和 AI 对话的「第一现场」这个位置的曝光价值比多做一个落地页高得多。
网站建设高端定制企业官网