1. 一个独立开发者为什么要给自己的小产品写 MCP server
先说结论:我给我的小产品写了一个 MCP server,现在 Claude、Cursor 这类 AI 代理在对话里就能直接「发现」它、读取它的能力清单,甚至自动完成报价和下单流程。整个过程不需要用户手动打开浏览器、复制粘贴参数,也不需要我再去对接一堆平台方的私有协议。
这件事的背景其实很简单。我手上有一个小产品,提供的是某种标准化的服务能力(具体业务不展开,你可以理解成「按量计费的 API 服务」)。过去用户想用它,路径是这样的:打开我的官网,看文档,注册账号,拿 API key,写代码调用。这条链路对开发者还算友好,但对大量非技术用户来说,门槛高得离谱。而 2024 年下半年开始,越来越多人习惯直接问 AI:「帮我找个能做 XX 的服务」,然后让 AI 去执行。问题来了——AI 怎么知道我的产品存在?就算知道了,它怎么调用?
MCP(Model Context Protocol)就是解决这个问题的。它本质上是一套让 AI 代理和外部工具/服务对话的标准协议。你可以把它类比成「AI 世界的 USB 接口」:以前每个 AI 工具都要为每个外部服务写一套私有对接代码,现在只要服务方实现一个 MCP server,所有支持 MCP 的客户端(Claude Desktop、Cursor、各种 IDE 插件)都能即插即用。
我写这个 MCP server 的核心动机有三个。第一,降低发现成本:让 AI 代理能主动「看到」我的服务,而不是等用户去搜。第二,降低调用成本:把「注册-拿 key-写代码」压缩成「AI 直接调用」。第三,探索 agent-to-agent commerce:当 AI 代理可以自主发现服务、比较报价、完成交易时,商业的入口就从「人找服务」变成了「代理找服务」。这是个趋势,早入场比晚入场好。
这篇文章我会把整个实现过程拆开讲:协议怎么理解、server 怎么设计、工具怎么暴露、报价逻辑怎么处理、和 Claude/Cursor 怎么联调、踩了哪些坑。适合两类人看:一是手里有小产品、想让 AI 代理能调用它的独立开发者;二是想搞懂 MCP 到底怎么回事、准备自己动手写一个的技术同学。不需要你之前用过 MCP,但需要你会一点 Python 或 TypeScript,懂基本的 HTTP 和 JSON。
2. MCP 协议到底在解决什么问题
2.1 从「函数调用」到「标准化工具暴露」
很多人第一次接触 MCP 会懵:这不就是 function calling 吗?OpenAI 早就有了。区别在哪?
Function calling 是单个 AI 应用内部的能力:你在调用某个模型时,把可用的函数列表塞进 prompt,模型决定调哪个,你的代码去执行。这套机制的问题在于,它是绑死在具体应用里的。你在 Claude Desktop 里定义的工具,Cursor 用不了;你在自己写的 agent 里定义的工具,换个框架就得重写。
MCP 把这个过程协议化了。它定义了一套标准的通信方式:客户端(AI 应用)和服务端(工具提供方)通过 JSON-RPC 通信,服务端声明自己有哪些 tools、resources、prompts,客户端负责把这些能力转译给模型。模型看到的还是「工具列表」,但底层已经解耦了。
这个解耦带来的直接好处是:我写一次 MCP server,所有支持 MCP 的客户端都能用。Claude Desktop 能调,Cursor 能调,以后任何新出的 AI IDE 只要支持 MCP,我的服务就自动在里面可用。这就是我决定投入时间写它的根本原因——一次投入,多端复用。
2.2 MCP 的三个核心原语:Tools、Resources、Prompts
MCP 协议里服务端可以暴露三类东西,理解这三类是设计 server 的基础。
Tools(工具)是最重要的。它代表「可以被 AI 调用的动作」,比如「查询价格」「创建订单」「获取服务状态」。每个 tool 有名字、描述、输入参数的 JSON Schema。AI 根据描述决定什么时候调、传什么参数。这是 agent-to-agent commerce 的核心——AI 通过 tools 完成实际交易动作。
Resources(资源)代表「可以被读取的数据」,比如文档、配置、数据库记录。它和 tools 的区别是:resources 是只读的、被动的,AI 读取它来获取上下文;tools 是主动的、有副作用的。我的产品里,resources 用来暴露服务说明、价格表、SLA 文档。
Prompts(提示模板)是预定义的提示词模板,用户可以主动选择。这个我用得少,因为我的场景里 AI 是自主决策的,不太需要用户手动选模板。但如果你做的是「引导式」产品,prompts 很有用。
设计时的关键判断:哪些能力该做成 tool,哪些该做成 resource。我的原则是——如果这个操作会改变状态(下单、扣费、创建资源),必须是 tool;如果只是读取信息(查价、看文档),优先做 resource,因为 resource 更轻量,AI 读取时不需要「调用」的仪式感。
2.3 传输层:stdio 还是 HTTP
MCP 支持两种传输方式:stdio(标准输入输出)和HTTP with SSE(Server-Sent Events)。
stdio 适合本地运行的服务:客户端启动时把 server 作为子进程拉起来,通过标准输入输出通信。Claude Desktop、Cursor 本地配置 MCP server 时默认走这条路。优点是简单、无需网络配置、天然隔离;缺点是只能本地用,没法部署到服务器给多人共享。
HTTP+SSE 适合远程服务:server 部署在公网,客户端通过 URL 连接。这是我要走的路,因为我的产品是给所有用户用的,不可能让每个人都本地跑一个 server。
实际实现时我用了官方 SDK 的 Streamable HTTP 传输(这是较新的方式,比早期的 SSE 方案更稳定)。配置上就是暴露一个/mcp端点,客户端用 URL 连接。这里有个坑后面会讲:认证怎么做。stdio 模式下进程隔离,不太需要考虑鉴权;HTTP 模式下必须考虑,否则任何人都能调你的付费接口。
3. 我的 MCP server 架构设计
3.1 整体分层:协议层、业务层、计费层
我没有把 MCP server 写成一个大文件,而是分了三层,这个结构在后期维护时救了我很多次。
协议层负责 MCP 协议的实现:注册 tools、处理 JSON-RPC 请求、参数校验、错误格式化。这一层用官方 SDK,尽量薄,不掺业务逻辑。好处是协议升级时改动集中在这里。
业务层是真正的服务能力:查询、下单、状态检查。这一层是我原有的业务代码,MCP server 只是给它套了个壳。关键设计:MCP server 不重新实现业务,只做适配。这样业务逻辑只有一份,不会出现「网页端和 MCP 端行为不一致」的问题。
计费层单独抽出来,因为 agent-to-agent commerce 的核心就是「AI 调用要能计费」。这一层处理配额检查、扣费、生成账单。它被业务层调用,但独立于协议层。
分层的直接收益:调试时我能明确知道问题出在哪一层。协议层报错通常是 schema 写错了;业务层报错是逻辑问题;计费层报错多半是配额或并发问题。
3.2 工具清单设计:让 AI 一眼看懂你能干什么
工具清单是 AI 代理「发现」你的产品的唯一入口,设计得好不好直接决定 AI 会不会用你。
我最终暴露了 5 个 tool,每个都经过反复打磨描述文案:
| Tool 名称 | 作用 | 关键参数 | 是否有副作用 |
|---|---|---|---|
list_services | 列出所有可用服务及简介 | 无 | 否 |
get_quote | 根据需求返回报价 | service_id, quantity, options | 否 |
create_order | 创建订单并返回订单号 | quote_id, confirm_token | 是 |
get_order_status | 查询订单状态 | order_id | 否 |
get_service_docs | 获取某服务的详细文档 | service_id | 否 |
设计原则有三条。第一,读操作和写操作分离:get_quote和create_order分开,让 AI 可以先报价、让用户确认、再下单。这符合 agent-to-agent commerce 的信任模型——AI 不应该在用户没确认的情况下直接扣费。
第二,报价和下单之间用 quote_id 绑定。get_quote返回一个带时效的 quote_id,create_order必须带上它。这样能防止「AI 拿旧价格下单」的问题,也让我能在服务端校验价格一致性。
第三,描述文案要写给 AI 看,不是写给人看。我最初的描述是「获取报价」,太模糊。改成「根据服务 ID、数量和可选配置,返回当前有效报价及报价有效期。报价 15 分钟内有效,下单时需携带返回的 quote_id」之后,AI 的调用准确率明显提升。描述里要包含:什么时候用、参数含义、返回值含义、约束条件。
3.3 报价逻辑:为什么不能直接返回一个数字
报价这块我踩过坑。最初get_quote直接返回一个价格数字,结果 AI 在对话里说「这个服务 100 元」,用户问「为什么是 100」,AI 答不上来,因为它只拿到了一个数字。
后来我改成返回结构化报价:
{ "quote_id": "q_abc123", "service_id": "svc_translate", "unit_price": 0.05, "quantity": 2000, "subtotal": 100.0, "discount": 0.0, "total": 100.0, "currency": "CNY", "valid_until": "2025-01-01T12:15:00Z", "breakdown": [ {"item": "基础翻译", "unit": "千字", "qty": 2, "price": 50.0} ] }这样 AI 能向用户解释价格构成,用户也更信任。breakdown字段是关键——它让报价「可解释」。在 agent-to-agent commerce 场景里,可解释性直接影响成交率,因为用户会追问,AI 答不上来就会放弃。
valid_until也很重要。报价有时效,过期后create_order会拒绝,AI 需要重新报价。这防止了价格波动带来的纠纷。
4. 动手实现:从零到能被 Claude 调用
4.1 环境准备与依赖安装
我用 Python 实现,因为业务代码本来就是 Python。官方 SDK 是mcp,安装很简单:
pip install mcp如果你用 TypeScript,对应的是@modelcontextprotocol/sdk。两者 API 设计思路一致,选你熟悉的。
环境上要注意 Python 版本,我用的是 3.11。3.10 以下某些类型语法会报错。另外建议用虚拟环境,因为 MCP SDK 迭代较快,隔离环境方便升级。
4.2 最小可运行 server 骨架
先上一个最小骨架,让你看到全貌:
from mcp.server.fastmcp import FastMCP mcp = FastMCP("my-product-server") @mcp.tool() def list_services() -> list[dict]: """列出所有可用服务及其简介。""" return [ {"id": "svc_translate", "name": "文本翻译", "desc": "中英互译,按千字计费"}, {"id": "svc_summarize", "name": "文本摘要", "desc": "长文摘要,按篇计费"}, ] @mcp.tool() def get_quote(service_id: str, quantity: int) -> dict: """根据服务 ID 和数量返回报价。报价 15 分钟内有效。""" # 业务逻辑:查价、算折扣、生成 quote_id ... if __name__ == "__main__": mcp.run(transport="streamable-http")FastMCP是官方提供的高层封装,装饰器一挂,函数签名自动转成 JSON Schema,docstring 自动变成 tool 描述。这个设计非常省事,但有个坑:docstring 会被原样暴露给 AI,所以别在里面写内部注释或 TODO。
4.3 参数校验与错误处理
AI 传参是不可控的。它可能传字符串给整型参数,可能漏传必填项,可能传一个不存在的 service_id。这些都要在服务端兜住。
我的做法是:用 Pydantic 定义参数模型,让 SDK 自动校验。FastMCP 支持直接标注类型,但复杂校验还是 Pydantic 更清晰:
from pydantic import BaseModel, Field class QuoteRequest(BaseModel): service_id: str = Field(..., description="服务 ID,来自 list_services") quantity: int = Field(..., gt=0, le=100000, description="数量,必须为正整数") @mcp.tool() def get_quote(req: QuoteRequest) -> dict: ...错误处理上,不要抛裸异常。MCP 协议里 tool 执行失败应该返回结构化的错误信息,让 AI 能理解并决定下一步。我统一用这样的返回:
return { "error": "INVALID_SERVICE", "message": "服务 ID 不存在,请先调用 list_services 获取有效 ID", "retryable": False }retryable字段很关键。如果是临时故障(比如下游超时),标True,AI 会重试;如果是参数错误,标False,AI 会换参数或告知用户。这个字段让 AI 的决策更聪明。
4.4 认证与配额:HTTP 模式下的必答题
stdio 模式下进程隔离,鉴权压力小。HTTP 模式下,任何人都能访问你的/mcp端点,必须做认证。
我的方案是Bearer Token + 用户绑定。用户在网页端登录后生成一个 MCP token,配置到 Claude/Cursor 里。server 收到请求后校验 token,解析出用户身份,后续所有操作都绑定到这个用户。
from mcp.server.fastmcp import FastMCP from starlette.middleware.base import BaseHTTPMiddleware class AuthMiddleware(BaseHTTPMiddleware): async def dispatch(self, request, call_next): token = request.headers.get("authorization", "").replace("Bearer ", "") user = verify_token(token) if not user: return JSONResponse({"error": "unauthorized"}, status_code=401) request.state.user = user return await call_next(request)配额检查放在业务层,每次 tool 调用前先查用户剩余额度。这里有个细节:报价不扣费,下单才扣费。因为报价是只读操作,如果报价也扣费,AI 反复报价会烧掉用户额度,体验很差。
注意:token 一定要支持吊销。用户可能把 token 泄露了,或者想换设备。我在网页端做了「重新生成 token」按钮,旧 token 立即失效。
5. 让 AI 代理真正「发现」并调用你的服务
5.1 在 Claude Desktop 里配置
Claude Desktop 的 MCP 配置在claude_desktop_config.json里。远程 HTTP server 的配置大概长这样:
{ "mcpServers": { "my-product": { "url": "https://api.example.com/mcp", "headers": { "Authorization": "Bearer YOUR_TOKEN" } } } }配置完重启 Claude Desktop,在对话里问「你能用哪些工具」,如果能看到list_services、get_quote这些,说明连接成功。
这里有个常见坑:Claude Desktop 对 HTTP MCP 的支持是逐步完善的,早期版本只支持 stdio。如果你配置后没反应,先确认版本。另外,配置文件的路径在不同系统下不一样,Mac 在~/Library/Application Support/Claude/,Windows 在%APPDATA%\Claude\。
5.2 在 Cursor 里配置
Cursor 的 MCP 配置在设置里的 MCP 面板,或者直接编辑配置文件。格式和 Claude 类似。Cursor 的优势是它本身是 IDE,你可以在写代码时直接让 AI 调用你的服务,比如「帮我用 my-product 翻译这段注释」。
实测下来,Cursor 对 MCP 的调用触发比 Claude Desktop 更「主动」——它会根据你当前编辑的内容判断是否需要调用工具。这在开发场景里很顺手,但也意味着你的 tool 描述要足够精确,否则 Cursor 可能在不该调用的时候调用。
5.3 让 AI「发现」你的关键:描述文案与示例
工具能被调用,不等于 AI 会主动调用。发现这件事,靠的是描述文案的质量。
我做了三件事提升「被发现率」。第一,在 tool 描述里写清楚适用场景。比如get_quote的描述里我加了「当用户询问价格、预算、费用时使用此工具」。这直接告诉 AI 什么时候该调。
第二,提供参数示例。JSON Schema 里可以写examples字段,AI 看到示例后传参准确率明显提升。
第三,在 server 的 instructions 里写一段总述。MCP 协议支持 server 声明自己的用途,这段文字会进入 AI 的上下文。我写的是「本服务提供文本翻译和摘要能力,按量计费。用户询问相关需求时,先调用 list_services 了解可用服务,再调用 get_quote 获取报价」。
这三件事做完,我在测试里发现 AI 主动调用率从大概三成提升到了八成以上。描述文案就是你的产品在 AI 世界里的门面,值得反复打磨。
6. 踩过的坑与排查实录
6.1 常见问题速查表
| 现象 | 可能原因 | 排查方向 | 解决 |
|---|---|---|---|
| 客户端连不上 | URL 或 token 错 | 看客户端日志 | 检查配置、token 有效期 |
| 工具列表为空 | server 启动失败 | 看 server 日志 | 检查依赖、端口占用 |
| AI 不调用工具 | 描述太模糊 | 看对话记录 | 补充场景说明和示例 |
| 参数校验失败 | Schema 与实现不符 | 对比签名 | 用 Pydantic 统一 |
| 报价后下单失败 | quote 过期 | 看 valid_until | 重新报价 |
| 并发下单重复扣费 | 幂等没做 | 看订单表 | 加幂等键 |
6.2 三个我印象最深的坑
第一个坑:报价和下单的价格不一致。早期我没做 quote_id 绑定,AI 报价后过了很久才下单,期间我调了价,导致用户看到的价格和实际扣费不符。后来加了 quote_id 和 valid_until,下单时校验,问题解决。教训是:任何涉及金额的流程,都要有服务端的一致性校验,不能信任客户端传来的价格。
第二个坑:AI 反复调用只读工具。有次测试,AI 在对话里连续调了七八次list_services,浪费 token 也拖慢响应。原因是我的描述里没说明「结果稳定,无需重复调用」。加上这句后,重复调用大幅减少。AI 的行为高度依赖描述,你没想到的边界,它就会踩。
第三个坑:错误信息太技术化。最初我返回的错误是 Python 异常堆栈,AI 看到后完全不知道怎么处理,直接把堆栈念给用户听。改成结构化错误 + 人类可读 message + retryable 标志后,AI 能正确决策:参数错就换参数,临时故障就重试,权限问题就提示用户。错误信息是给 AI 看的,要按 AI 能理解的方式组织。
6.3 日志与可观测性
MCP server 的日志和普通服务不太一样,因为你要同时观察「协议层」和「业务层」。
我的做法是:协议层日志记录每次 JSON-RPC 请求的方法、参数、耗时;业务层日志记录业务动作和结果;两层用同一个 trace_id 串联。这样出问题时能快速定位是协议问题还是业务问题。
另外,记录 AI 的调用序列很有价值。你会发现 AI 的调用模式和人类完全不同——它可能先调get_service_docs再调get_quote,也可能反过来。分析这些序列能帮你优化工具设计,比如把常用的调用组合做成一个复合 tool。
7. 关于 agent-to-agent commerce 的一些实践体会
写这个 MCP server 最大的收获,不是技术上的,而是对「AI 代理如何做生意」的理解。
第一,AI 代理是「急性子」。它不会像人类那样耐心读完整篇文档,它看的是 tool 描述和参数 schema。所以你的产品信息必须结构化、前置、精炼。我把最重要的信息(价格区间、交付时间、能力边界)放在 tool 描述的第一句,效果立竿见影。
第二,信任靠可解释性建立。人类用户看到「100 元」会问为什么,AI 代理也一样——它会向用户转述。报价里的breakdown字段让 AI 能解释价格构成,用户信任度明显提升。在 agent-to-agent 场景里,可解释性就是转化率。
第三,幂等和一致性是底线。AI 可能因为网络抖动重试,可能因为理解偏差重复调用。所有写操作必须幂等,所有金额相关操作必须服务端校验。我用了「幂等键 + quote 绑定」双保险,实测下来没出现过重复扣费。
第四,别指望 AI 一次调对。设计 tool 时要假设 AI 会犯错,用清晰的错误信息和 retryable 标志引导它自我纠正。好的错误处理能让 AI 的端到端成功率提升一大截。
这个 MCP server 上线后,我观察到一些有意思的数据:通过 AI 代理进来的订单,客单价和人类用户差不多,但决策链路短很多——从「询问」到「下单」平均只有两三轮对话。这说明 AI 代理确实在改变交易入口。对于手里有小产品的开发者,我的建议是:尽早把你的能力 MCP 化,这不是赶时髦,而是在为「代理找服务」的新入口提前占位。工具描述多打磨几遍,错误处理多写几行,这些投入在 AI 调用场景里的回报比想象中高。