前阵子我终于把“让 AI 代理能自动给我的小产品报价”这件事打通了。事情起因很简单有用户直接在 AI 对话框里问我的小工具怎么收费、有哪些版本、能不能批量授权结果 AI 一本正经地胡说八道给出一个我从来没定过的价格。与其等大模型被喂错信息不如主动把产品目录和报价逻辑做成一个 MCP server让 AI agent 通过标准协议自动“发现”我的产品再按真实规则报价。如果你没接触过 MCP可以把它理解成给 AI 插上的“USB 接口”——AI 不再只会聊天而是通过一套标准化协议去调用外部工具。这套协议全称 Model Context ProtocolGPT、Claude 这类模型通过它读写外部数据、触发工具。这篇文章不聊概念我会直接从一个独立开发者的视角讲讲我从零开始给自己的小产品写 MCP server 的完整过程需求怎么拆、工具怎么设计、代码怎么写、客户端怎么接以及我踩过的几个坑。1. 先拆需求为什么小产品需要被 AI“发现”1.1 从“用户问 AI”到“AI 主动问你”以前小产品要被人知道靠搜索引擎、应用商店、公众号。现在多了一个入口用户让 Claude、ChatGPT、Cursor 里的 AI agent 帮忙找产品或报价。我试过让 AI 推荐一款适合做批量 PDF 合并的小工具它真的会“编”出来一个——编名称、编价格、编购买链接。如果你的产品没有接入这些模型可感知的渠道它对你的产品就是“看不见、摸不着、只能编”。MCP server 解决的就是这个问题当用户在一个支持 MCP 的客户端Claude Desktop、Cursor、Trae 等里提问时客户端会向已注册的 MCP server 发送list_tools调用server 返回“我能提供这些能力”AI 再根据用户意图选择合适工具去执行整个流程里“发现产品”和“获取报价”都变成了可验证的真实程序逻辑而不是模型瞎猜。1.2 为什么是 MCP而不是我熟悉的 REST API我一开始也犯过嘀咕我的产品有 REST API文档也写得挺清楚为什么还要再包一层 MCP区别在于调用方和交互方式。REST API 是给程序员调的调用方必须知道接口路径、参数、鉴权方式还要自己写请求代码。MCP server 是给 AI agent 调的它通过协议把工具清单、参数结构、返回语义暴露给模型模型只需要“看懂描述然后自动拼参数去调用”。换句话说REST API 是“说明书”MCP 是“把说明书也塞给 AI并让它直接操作仪器”。我这里给出一张不太严谨但很直观的对比表维度REST APIMCP Server调用方开发者写的程序AI agent / 支持 MCP 的客户端发现机制人工读文档协议层list_tools自动发现参数传递开发者按文档构造模型根据描述自动生成 JSON 参数返回格式由接口自行定义按 MCP 约定封装带内容和结构化字段最适合的场景可控的应用程序交互让 AI 自主理解并完成多步任务对于“让 AI 代理自动发现并报价”这个目标MCP 是成本最低的路径。我不需要维护两套文档不需要让模型去理解一长串 Markdown 文档只要把工具和数据暴露出去客户端会自动完成发现和调用。1.3 “报价”场景的特殊性绝对不能靠模型心算报价这件事太容易出错了。价格涉及组合促销、会员折扣、量价阶梯、地区运费。如果你在工具描述里写“根据产品 ID、数量、地区报价”然后让大模型自己心算一个结果那一定会翻车。我的原则只有一个模型只负责识别意图和传参所有数值计算必须回到我自己的程序里执行。这在 MCP 设计上很自然——报价工具接收结构化参数内部读规则表、算价格、返回结果。模型的“聪明才智”用在理解“用户说的是要报价哪个产品多少数量哪个地区”而结果的准确性和一致性完全由我的代码保证。2. 方案选型SDK、传输方式与工具设计2.1 SDK 选型Python 用 fastmcp真省事MCP 官方提供了 TypeScript SDK 和 Python SDK。我作为独立开发者后端主要用 Python所以选了 Python 生态。用官方 SDK 其实不难但需要自己处理协议消息、校验 JSON Schema、实现 lifecycle handler样板代码量不小。后来我换成了社区非常流行的fastmcp库体验好了非常多。fastmcp核心价值是“装饰器写工具、Pydantic 定义参数、自动生成 MCP 协议字段”。我只需要定义一个FastMCP实例用mcp.tool()装饰几个函数它自动完成工具注册、参数 schema 生成、请求分发。如果你不是特别需要深度定制协议层我强烈建议直接用这个库半小时就能跑通。如果坚持官方 SDK注意版本对齐MCP 协议现在更新很快不同版本的 SDK 与客户端之间的兼容性偶尔出问题。fastmcp会帮我锁相对稳定的协议版本省掉很多兼容性痛苦。2.2 传输方式stdio 和 Streamable HTTP 怎么选MCP 目前主流有两种传输方式stdio和Streamable HTTP旧的 SSE 传输逐渐被替代。stdio模式下MCP client 作为父进程启动 server双方通过标准输入和标准输出通信。这种方式非常适合本地桌面客户端一个 agent 对应一个 server 进程配置简单。我在本地调试和连接 Claude Desktop、Trae 的时候用的就是 stdio。Streamable HTTP 模式则是让 server 跑成一个 HTTP 服务客户端通过 URL 来连接适合远程部署、多客户端共享、服务化场景。比如我想让多个用户或线上 agent 都能调用我的报价服务就把它发布为一个 HTTP endpoint。我的建议是只给自己本地用直接 stdio几行 JSON 配置搞定要做成服务给别人/别的系统用用 Streamable HTTP加鉴权考虑并发两种模式可以在同一个代码里都支持fastmcp提供了mcp.run(transportstdio/streamable-http)的切换一个参数的事。2.3 工具设计每个 MCP Tool 都是“一句话 一个 Schema”MCP server 的核心资产是“工具”Tools。AI 代理能发现什么完全取决于你暴露了哪些工具以及它们的描述写得好不好。我设计了两个工具search_products搜索我的产品目录按分类/关键词/状态过滤返回产品 ID、名称、规格、是否在售。get_product_quote根据产品 ID、数量、所在地区、会员状态计算报价返回单价、总价、运费、优惠明细。设计的原则是“语义单一、描述精准”。search_products只做目录检索不做计算get_product_quote只做报价不返回目录。这样 AI 在执行“帮我找找能一键合并 PDF 的产品并报 10 个授权的价格”这个任务时会自然地先调用前一个工具拿到产品 ID再调用后一个工具拿到报价逻辑非常清晰。工具描述里我还要额外写清楚“什么时候该调用这个工具”。比如get_product_quote的描述里我会加上“当用户要求获取价格、报价、折扣、总价时调用仅在用户表达具体购买意向后调用”。这看起来啰嗦却极其重要——描述写得太宽泛模型会在不该报价的时候也去报价写得太窄模型可能漏调用。每个工具的入参我都用 Pydantic 模型定义让fastmcp自动生成 JSON Schema。这里尤其要注意字段描述也要写清楚。模型读不到你的源码它只能读到 schema 里的description。比如quantity字段我写的是“需要授权的用户数量或采购数量必须是大于 0 的整数只允许 1~1000”这对模型正确传参帮助巨大。3. 实操过程用 fastmcp 半小时搭起来3.1 初始化项目与依赖我建了一个独立目录把它和我的主产品代码分开只通过内部模块或静态数据文件访问产品信息。这样 MCP server 即便被模型调出问题也不会影响主业务。mkdir mcp-product-server cd mcp-product-server python -m venv .venr source .venr/bin/activate pip install fastmcp httpx注意 Mac/Linux 的source命令Windows 是.venr\Scripts\activate这里不同系统差异挺大大家按自己的系统来。项目结构mcp-product-server/ ├── products_data.json # 产品目录与报价规则 ├── server.py # MCP server 主文件 ├── pricing.py # 报价计算逻辑纯函数 └── README.md我特意把pricing.py单独拆出来原因是报价逻辑将来很可能复用给 REST API 或定时任务纯函数化之后测试也容易。3.2 产品目录数据准备为了让 AI 代理“发现”产品我先把产品目录整理成一份结构化 JSON。格式不需要太复杂关键是字段语义明确便于程序读取。我的products_data.json长这样[ { sKu: PDF-MERGE-PRO, name: PDF Merge Pro, category: pdf-tools, tags: [pdf, merge, batch], description: 支持批量合并、拆分 PDF 的桌面工具Windows 和 Mac 可用。, in_stock: true, base_price: 129.0, licenses: single }, { sKu: PDF-BATCH-10, name: PDF Batch Pack (10 seats), category: pdf-tools, tags: [pdf, batch, team], description: 10 人团队版批量 PDF 授权。, in_stock: true, base_price: 899.0, licenses: team } ]这个文件我放在 server 进程启动时加载到内存里。对于小产品来说产品数量不会很多内存加载完全够用没必要引数据库。如果你的产品有成百上千个 SKU再考虑用 SQLite 甚至外部 API。3.3 实现“发现”工具核心代码非常简单import json from typing import Optional from fastmcp import FastMCP mcp FastMCP(my-product-server) with open(products_data.json, r, encodingutf-8) as f: PRODUCTS json.load(f) mcp.tool() def search_products( category: Optional[str] None, keyword: Optional[str] None, in_stock: bool True, limit: int 10, ) - list[dict]: 搜索我的产品目录返回产品列表。 当用户想了解有哪些产品、查找特定类型产品、询问是否在售时调用此工具。 - category: 产品分类例如 pdf-tools、image-tools、video-tools - keyword: 模糊匹配产品名称、标签、描述中的关键词 - in_stock: 是否只返回在售产品 - limit: 返回结果数量上限默认 10最大 20 results [] for p in PRODUCTS: if not in_stock or p[in_stock]: if category and p[category] ! category: continue if keyword: haystack .join([p[name], .join(p[tags]), p[description]]) if keyword.lower() not in haystack.lower(): continue results.append(p) if len(results) limit: break return results这里有几个细节值得注意函数 docstring 实际上会变成工具描述所以我把“何时调用”和参数含义都写在 docstring 里。fastmcp会自动把 docstring 和参数类型解析为 MCP 的description字段。返回的是list[dict]fastmcp会把它序列化成 JSON。返回结构不宜太深AI 处理扁平列表更稳。我给每个搜索结果都带上base_price字段这样模型也能直接看到基础价格。但这不是报价报价必须走get_product_quote。3.4 实现“报价”工具报价逻辑我放在pricing.py然后用mcp.tool()暴露from pydantic import BaseModel, Field class QuoteRequest(BaseModel): product_id: str Field(..., description产品 SKU ID来自 search_products 返回的 sKu 字段) quantity: int Field(..., ge1, le1000, description购买数量/授权数量大于0的整数) region: str Field(CN, description地区代码可选 CN、US、EU、SG) is_member: bool Field(False, description是否为老用户/会员) mcp.tool() def get_product_quote(request: QuoteRequest) - dict: 根据产品 SKU、数量、地区和会员状态计算报价。 当用户要求获取具体价格、总价、折扣、运费时必须调用本工具。 返回结构化报价单包含单价、数量、折扣、运费、总价和有效期。 from pricing import calculate_quote return calculate_quote(request.product_id, request.quantity, request.region, request.is_member)pricing.py里是纯计算逻辑def calculate_quote(product_id: str, quantity: int, region: str, is_member: bool) - dict: # 从 PRODUCTS 里找到产品这里简化 product find_product_by_id(product_id) if not product: return {error: product not found, sKu: product_id} unit_price product[base_price] # 量价阶梯 if quantity 50: unit_price round(unit_price * 0.8, 2) elif quantity 20: unit_price round(unit_price * 0.9, 2) elif quantity 5: unit_price round(unit_price * 0.95, 2) # 会员折上折 if is_member: unit_price round(unit_price * 0.95, 2) # 地区运费简化规则 shipping 0.0 if region CN: shipping 0.0 elif region in (US, EU, SG): shipping 29.0 if quantity 10 else 0.0 subtotal round(unit_price * quantity, 2) return { sKu: product_id, unit_price: unit_price, quantity: quantity, subtotal: subtotal, shipping: shipping, total: round(subtotal shipping, 2), currency: CNY, valid_until: 2026-12-31 }这里我要再次强调让 AI“发现”产品没问题但“报价”这个动作一定要全部拉到程序侧。我见过有人在工具描述里写“请根据 base_price 和数量自行计算总价”结果模型把129 * 3算成369这种离谱结果。报价工具存在的意义就是让模型不需要做任何算术。3.5 接入本地客户端本地调试我用stdio模式。fastmcp在结尾加一行if __name__ __main__: mcp.run(transportstdio)然后在 Claude Desktop 的配置文件中添加 server{ mcpServers: { my-product-server: { command: python, args: [/path/to/mcp-product-server/server.py] } } }注意command必须是绝对路径下的 Python 解释器尤其是在 Mac/Linux 下尽量用which python查一下避免配置里写了/usr/bin/python而项目依赖装在 venv 里。我一开始在这里踩了坑后面统一用.venr/bin/python的绝对路径就稳定了。在支持 MCP 的 IDE 中比如 Cursor 或 Trae 的 MCP 配置面板同样加一条命令启动项即可。配好后AI 客户端会自动完成“发现”流程你在对话框里问“你们的 PDF 合并工具买 10 个授权多少钱”模型会先调用search_products再调用get_product_quote。整个过程它对用户是透明的只输出一个合理的报价单。3.6 用 MCP Inspector 快速验证在没有客户端的情况下可以直接用官方调试工具来验证 server 是否正常工作npx modelcontextprotocol/inspector python /path/to/server.pyInspector 会打开一个本地 Web 页面界面左侧能看到Tools列表右侧能手动调用每个工具并查看 JSON 返回。我强烈建议在接入客户端之前先在这里把每个工具的输入输出都过一遍能省去不少对接阶段的排查时间。4. 常见问题与排查技巧实录4.1 stdio 模式连不上多半是 stdout 被污染了我最开始跑通时在 server 里加了几行print调试日志结果客户端一直报告连接失败。原因是 stdio 模式下协议消息就是通过 stdout 传输的你在 stdout 里打印任何额外内容都会把协议流弄坏。任何日志都只能走stderr或外部文件。正确做法是设置一个专门的文件日志或者用logging模块输出到 stderr。在server.py开头加上import logging logging.basicConfig(streamsys.stderr, levellogging.INFO)这样你看本地终端时能实时看到日志又不会干扰 MCP 协议通信。4.2 AI 传参错误收紧 JSON Schema 的描述和约束有一次测试时模型把quantity传成了字符串10个就是因为我的 Pydantic 字段没有严格类型约束schema 里type字段太宽松。后来我强制quantity: int加上ge1, le1000模型基本就稳定传整数了。经验就是能收窄的字段一定收窄。字段描述里要把单位、取值范围、默认值都写清楚。JSON Schema是模型理解你的接口的唯一途径写得越细模型出错率越低。4.3 幻觉和乱报价所有数值必须程序计算我在测试阶段故意问 AI“如果买 3 套会员运费到美国总价多少”如果get_product_quote没被触发AI 就会按照脑海里已经抓取的base_price自己心算。只要我让get_product_quote工具名足够显眼、描述足够明确、入参足够结构化之后模型就会稳定调用它。我还做了另一个保险在报价工具返回的数据里带一个valid_until字段和currency。这样 AI 拿到的是一份“看起来很正式的报价单”它就没有动力自己重算一份了。4.4 并发与超时远程部署时考虑无状态化如果你只是本地给自己用一个 agent 对应一个 stdio 进程并发问题基本不存在。但如果我把 MCP server 部署成 Streamable HTTP 服务就要考虑多个 agent 同时请求怎么办。我的建议是保持无状态。所有报价计算都不依赖进程内存中的可变状态产品数据可以启动时加载或直接读文件每次请求都是独立计算。还要在服务端设置一个合理的超时上限比如计算超过 10 秒直接返回错误避免一个慢请求拖住整个 worker。fastmcp在新版本里对 Streamable HTTP 的支持很成熟用mcp.run(transportstreamable-http)即可。4.5 日志管理从 print 到结构化 JSON 日志前面提到 stdio 不能用 stdout 打印日志那线上服务怎么做日志管理我推荐直接用标准logging格式改为 JSON便于后面接日志平台或做关键词搜索。用 Python 的logging加一个简单的 JSON Formatterimport json import logging class JsonFormatter(logging.Formatter): def format(self, record): log_entry { time: self.formatTime(record, %Y-%m-%d %H:%M:%S), level: record.levelname, module: record.module, message: record.getMessage(), } return json.dumps(log_entry, ensure_asciiFalse)这样每条日志都是一行 JSON排查时 grep 一下某个 SKU 就能找到对应报价链路的所有记录。建议至少记录调用方请求 IDMCP request id、工具名、入参摘要、返回结果状态、耗时。这对后续分析模型调用行为特别有帮助。5. 几个我踩过的坑与后续扩展思路5.1 工具过度暴露不是所有能力都该做成 Tool一开始我为了让 AI “更懂产品”把产品规格表、价格表、库存表全部做成了工具。结果模型经常不知道该调哪个甚至一次调用里带出一大堆无用信息反而干扰决策。后来我只保留了“搜索产品”和“报价”两个核心工具其余产品说明类内容换成 MCP 的resources暴露。Resources 是另一种数据暴露方式它更像“可被检索的静态文档”模型按需读取而非主动调用。这个思路很值得大家参考能做成资源文档的别做成工具工具越少模型判断越准。5.2 对返回的结果做“语义包装”减少 AI 二次发挥get_product_quote返回的原始 JSON 是这样的{ sKu: PDF-MERGE-PRO, unit_price: 122.55, quantity: 10, subtotal: 1225.5, shipping: 0, total: 1225.5, currency: CNY, valid_until: 2026-12-31 }其实模型拿这个给用户看也够用但我后来在返回前加了一步“整理成人类可读的报价单摘要”把折扣信息和会员优惠也写进去。这样模型几乎不用自己组织语言直接转发给用户准确性更高、体验更自然。5.3 后续可以这样扩展写完之后我发现自己这套思路完全可以复用比如把单个产品的 MCP server 升级成支持多产品、多商户的报价网关再比如把报价工具接入到真实的订单系统让 AI 不只报参考价还能直接锁定库存、生成订单草稿。另一个有意思的方向是在 MCP server 里接入支付能力让 agent 完成“发现-报价-下单”全链路闭环。我个人的体会是给 AI 写 MCP server 的核心并不是“套一个协议框架”而是想清楚你希望模型在哪些环节介入、哪些环节程序必须兜底。报价计算这种一错就坏信誉的事一定让程序做搜索和意图理解这种事放心交给模型。只要这个边界立住了MCP server 就会非常可靠。最后再分享一个小经验当我第一次把配置好的 MCP server 接进 IDE 时故意用一句非常口语化的话去测试——“帮我看下你们家有没有能批量合并 PDF 的东西给我报个 10 台的价格”。看着 AI 自动列出工具、传参、正确算出总价的那一瞬间你会觉得这半小时的配置确实值了。