news 2026/10/1 7:42:05

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
独立开发者实战:为小产品接入MCP server,让AI代理自动发现并报价

1. 一个独立开发者为什么要给自己的小产品接上 MCP server

先说清楚背景。我手上有一个自己维护的小产品,规模不大,属于那种“一个人写代码、一个人运维、一个人接客服”的状态。它提供一项具体的服务能力,过去用户想用它,得先打开网页、注册、手动填参数、点提交,整个链路对人是友好的,但对 AI 代理完全不友好——AI 代理根本不知道这个产品存在,更别说自动调用它。

这两年 AI 代理的形态变化很快。Claude、Cursor 这类工具已经不只是“帮你补全代码”,它们开始具备自主规划任务、调用外部工具、多步执行的能力。问题在于,代理能调用的工具,基本被限制在它自己生态里预置的那几个。你想让它用上你自己的服务,过去只有两条路:要么写一个插件塞进它的插件市场(审核周期长、门槛高),要么让用户手动把结果复制粘贴进去(体验稀碎)。

MCP(Model 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、prompts

MCP 协议里,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、data
workloadnumber是预估工作量,单位为人天
urgencystring(枚举)否紧急程度,normal 或 rush,默认 normal
detailstring否补充说明,用于人工复核

改完之后,代理的调用成功率明显上升。因为每个参数都有明确的类型和枚举值,代理知道该向用户追问什么,也知道自己填的值合不合法。

注意:枚举值一定要在 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 LTS)
  • npm 或 pnpm
  • 一个支持 MCP 的客户端,我用 Claude Desktop 和 Cursor 各测了一遍

初始化项目:

mkdir my-mcp-server && cd my-mcp-server npm init -y npm install @modelcontextprotocol/sdk zod

@modelcontextprotocol/sdk是官方 SDK,zod用来定义参数 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 后,第一件事就是拉工具清单。你需要处理ListToolsRequestSchema:

server.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:真正执行报价逻辑

工具被调用时,处理CallToolRequestSchema:

const 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 和 Cursor

server 写完了,得让客户端能连上。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 状态,比翻日志快得多。这个工具我每次接入新客户端都会先用一遍,省了不少排查时间。

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

GVIM块注释效率翻倍:把配置改到 TaoToken 的 AI 补全实践

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

作者头像 李华
网站建设 2026/10/1 7:39:10

openclaw太耗token怎么办?用TaoToken统一Key给AI Agent长期记忆瘦身

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

作者头像 李华
网站建设 2026/10/1 7:38:53

Windows反弹Shell实战:nc/msfvenom/openssl三阶加固链

1. 反弹Shell不是“黑产专属”&#xff0c;而是Windows系统安全能力的试金石在Windows运维、红队评估、渗透测试或安全加固工作中&#xff0c;“反弹Shell”这个词常被误读为某种高危攻击动作。但真实情况是&#xff1a;它本质上是一种双向通信建立机制&#xff0c;核心价值在于…

作者头像 李华
网站建设 2026/10/1 7:38:04

专升本英语词汇:高效记忆技巧与学习习惯养成

对于准备专升本考试的学生来说&#xff0c;英语词汇是备考过程中的重要一环。如何高效记忆词汇&#xff0c;养成良好的学习习惯&#xff0c;成为许多学生和家长关注的焦点。今天&#xff0c;我就来和大家分享一下我的经验和心得。 一、词汇记忆方法 1. 语境记忆法 词汇脱离了语…

作者头像 李华