资讯详情

资讯详情

MCP协议开发实战:从零实现AI Agent工具服务

说到 MCP 协议最近大半年这个圈子里的热度一直没降过。凡是做 AI Agent、做企业内部工具接入、或者折腾过智能助手的人基本都绕不开这三个字母。我自己从最早各家模型各搞一套工具调用标准到后来统一到 MCP 这套协议上中间踩了不少坑也亲手把好几个内部系统接进了不同的 AI 客户端。这篇就当是阶段性的经验整理讲讲 MCP 到底是什么、怎么上手开发一个自己的工具服务以及生产环境里那些文档上不会写清楚的细节。如果你正准备给 AI 应用外接数据源或工具或者想把公司内部已有的 API 暴露给 Agent 使用这篇文章可以给你一条相对完整的落地路径。即使你暂时只是听过 MCP 这个名字看完也应该能明白它解决什么问题以及动手写一个最小可用服务要分几步。1. MCP 到底在解决什么问题1.1 工具调用的“碎片化”困境先说痛点。在 MCP 出现之前给 AI 模型接外部工具是一件相当繁琐的事情。每个模型厂商都有自己的一套函数调用格式有的用 JSON Schema 描述参数有的要求特定的提示词结构甚至同一个厂商的不同版本之间接口还会不兼容。这就导致一个尴尬的局面你为一个模型写好的工具适配层换个模型基本要推倒重来你为某个业务系统开发的数据查询插件换个 AI 应用就完全用不上。当时我参与过一个项目需要同时对接多个模型供应商给它们提供同一套内部数据查询能力。最原始的做法是写一个统一的网关层把各家的函数调用参数解析成内部标准格式再把结果转换回各家要求的响应结构。这听起来可行但实际维护量非常大。每新增一个模型就要改一遍映射逻辑每新增一个工具又要重新梳理入参出参的格式约定再加上各家对工具描述的解析能力参差不齐同一份工具说明在不同模型里表现差异很大调试起来非常痛苦。MCP 协议的价值就在这里。它本质上是一套标准化的“插头”规范把 AI 应用Host和外部工具Server之间的交互方式统一起来。模型侧只需要实现一个 MCP 客户端工具侧只需要实现一个 MCP 服务端两边都按同一套协议对话就不再需要针对每个模型单独开发适配器了。打个比方之前每个电子设备都有自己的充电接口现在 MCP 就是那个统一的 USB-C 口虽然背后还是有各种协议转换但至少接口是标准了。1.2 MCP 的核心架构与三个角色MCP 的架构大致分成三层最上层是宿主应用也就是用户实际面对的那个 AI 产品比如智能 IDE、桌面助手、企业内部的知识库机器人中间层是宿主应用内置的 MCP 客户端负责跟协议服务端建立连接、发送请求、接收响应最底层就是 MCP 服务端它才是真正执行工具逻辑、访问数据源的地方。这三个角色各司其职很好理解宿主应用负责承载交互界面和业务逻辑它是整个架构的“身体”。MCP 客户端负责协议层面的通信相当于给“身体”装上能识别各种外设的“驱动”。MCP 服务端则是具体的“外设”可以是一个查询天气的工具也可以是一个读写数据库的服务甚至可以是一个完整的业务子系统。协议本身定义了三类核心能力分别是工具、资源和提示词模板。工具是最常用的对应模型可以主动调用的函数资源是只读的数据源比如文件内容、数据库记录提示词模板则是一段可复用的文本模板方便复用复杂的提示词结构。做工具开发时主要打交道的是第一类——工具也就是tools/list和tools/call这对接口前者用来让客户端发现有哪些工具可用后者用来真正执行工具。架构清晰之外MCP 还有一个容易被低估的优点它天然地把工具执行的上下文与模型隔离了。模型只负责决定“我要调用哪个工具、传入什么参数”而工具内部怎么处理、访问什么数据、调什么第三方 API模型完全不需要关心。这种隔离对于权限控制、审计和容错都很有帮助。2. 传输层选型与核心接口2.1 本地 stdio 与远程 HTTP怎么选动手写服务端之前得先选一种传输方式。MCP 协议目前主流的两种传输方式是标准输入输出stdio和远程 HTTP包括 HTTPSSE 以及流式 HTTP。这两种方式各有适用场景选错的话后面会很难受。stdio 模式的特点是简单、安全宿主应用直接在本地启动一个子进程通过标准输入和标准输出与这个进程通信。每个 MCP 服务器就是一个独立的本地进程连接的建立和销毁都跟着宿主应用的启动和退出走。好处很明显一是没有网络层不用担心端口占用、防火墙、跨域这些问题二是权限天然本地化一个工具进程能访问的资源就是这个系统用户能访问的资源边界清晰。我做的本地文件处理工具、代码分析工具基本都是用这个模式跑的。stdio 的缺点是没法跨机器部署。如果你想在团队里共享一个 MCP 服务或者想让云端的一个 AI 服务连接你公司内网里的工具stdio 就不合适了必须走 HTTP。远程模式把 MCP 服务部署在一台服务器上客户端通过网络协议访问。这样做的好处是可以中心化部署、统一鉴权、集中管理多个工具服务但相应地要面对网络延迟、鉴权方案、服务可用性这些老生常谈的问题。选择建议很简单如果是个人本机的工具或者跟强相关的数据处理任务优先 stdio如果是团队共享、生产级、需要审计的服务能力走 HTTP而且要在一开始就把鉴权做好。我见过不少团队上来就搞 HTTP 部署结果连最基本的访问控制都没做直接把一个能读写内部数据的工具裸奔在公网上想想都后怕。2.2 工具发现的完整调用链路不管用哪种传输方式客户端与服务端的核心交互链路是一致的。服务端启动之后客户端首先发送一个初始化请求双方交换协议版本和服务端能力信息。这个握手过程很关键如果版本不匹配后续的请求都会失败。接下来客户端调用tools/list拿到服务端声明的工具清单这一步会返回每个工具的名称、描述、输入参数格式以及输出结构声明。模型根据这个清单决定在合适的时机调用哪个工具。然后是tools/call。客户端收到模型的调用意图后把工具名和参数原样发给服务端服务端执行完毕后返回一个结构化结果结果可能是文本、图片、资源链接或者带结构的 JSON 数据。客户端再把这个结果回传给模型模型据此生成最终回复。这个链路中两个地方最容易被忽略。第一工具清单的“描述”是给模型看的模型根据描述决定“该不该用”和“怎么用”所以描述写得越准确、越详细模型的调用准确率越高第二tools/call返回的结果是给模型“读”的返回的数据结构越规整模型后续的推理就越顺畅。这跟以前做 API 文档一样甚至更重要因为模型的“阅读”能力虽然强但面对混乱格式时也会犯傻。3. 从零开发一个最小可用的 MCP 服务器3.1 环境准备与项目骨架现在进入实操环节。我用 Python 做演示因为官方 SDK 对 Python 支持最完善而且上手成本最低。你需要一个 Python 3.10 以上的环境然后按下面的方式安装依赖pip install mcpSDK 装好之后项目结构可以非常简单单文件就能跑起来。当然工具多了以后还是建议拆成多模块但这里先从一个文件开始。mcp-demo/ ├── server.py └── requirements.txtrequirements.txt里面写上mcp即可这里不做版本锁定实际项目建议锁定版本号。3.2 写出第一个工具函数现代版的 Python SDK 提供了一种极简的开发体验直接用装饰器就能把普通函数暴露成 MCP 工具。来看一个完整的示例我以一个查询模拟天气数据的工具为例from mcp.server.fastmcp import FastMCP # 创建一个 MCP 服务实例 mcp FastMCP(weather-demo) mcp.tool() def get_weather(city: str, unit: str celsius) - str: 查询指定城市的当前天气信息。 参数说明 - city: 城市中文名例如“北京” - unit: 温度单位支持 celsius摄氏和 fahrenheit华氏默认 celsius # 这里省略真实的第三方天气 API 调用用模拟数据代替 data { city: city, temperature: 18, condition: 多云, wind: 东南风3级, humidity: 45, unit: unit } if unit fahrenheit: data[temperature] round(data[temperature] * 9 / 5 32, 1) return f{city}当前天气{data[condition]}{data[temperature]}{°F if unit fahrenheit else ℃}湿度{data[humidity]}% if __name__ __main__: mcp.run()就这几行代码一个可用的 MCP 服务端就完成了。FastMCP(weather-demo)创建了一个服务实例mcp.tool()把函数标记为工具函数签名里的类型注解和文档字符串会自动转换成协议需要的 JSON Schema客户端就能看到这个工具的完整描述。mcp.run()默认以 stdio 模式启动服务等待客户端连接。实际开发时我在每个工具函数的 docstring 上花的时间往往比代码本身还多。因为模型就是靠这段文本来决定何时调用、传什么参数的。描述里写清楚每个参数的含义、取值范围、默认值以及工具的适用场景能显著提高模型调用的准确性。建议你在自己写的每个工具上都认真写一段带示例的 docstring。3.3 联调验证的几种方式服务写好了怎么确认它能被正常发现和调用这里有几个验证方式我按排查效率排个序。第一个方式用 SDK 自带的调试客户端。命令行里执行python -m mcp.cli run server.py这个命令会启动一个交互式的客户端直接连上你的服务端你可以手动列工具、调用工具。我通常先跑这一步确认工具注册正常、参数解析正常再去接上层应用。第二个方式用官方调试界面。先在配置文件里指向你的服务端脚本然后启动调试面板npx modelcontextprotocol/inspector python server.py调试面板会提供图形化界面能查看服务端日志、工具列表还能手动构造调用参数。这是排查协议级错误时最顺手的工具比如发现客户端看不到工具或者调用报参数校验错误来这里看最直观。第三个方式直接接到 AI 客户端里测试。如果你用一个支持 MCP 的 AI 桌面应用通常在配置里会有一个mcpServers字段像这样{ mcpServers: { weather-demo: { command: python, args: [/path/to/server.py] } } }配置好之后启动应用连上服务端然后在对话里直接向模型提问“北京天气怎么样”看模型能不能正确触发工具调用并返回结果。这一步才是端到端验证前面两步只验证了协议层这里连模型的理解和决策一起验证了。需要注意stdio 模式下宿主应用是通过你给的command命令拉起子进程的所以python一定要在系统 PATH 环境变量里并且要确保工作目录和脚本权限没问题。很多新手配置了半天连不上其实就是这里出了岔子。4. 生产环境绕不开的进阶能力4.1 从工具到资源与提示词模板工具只是 MCP 的第一层。等到你真要把一个业务系统对接到 AI 应用上时光有工具往往不够。举个例子你做了一个企业内部的文档查询助手你想让 AI 在回答前先检索某个知识库。这个“检索”动作本身可以作为工具暴露给模型但如果你还想让客户端支持“读取某份特定文档内容”这种操作那就需要用到资源的定义了。资源在 MCP 里可以理解成暴露给客户端的只读数据句柄。用 SDK 的话注册资源也很直观mcp.resource(document://{doc_id}) def get_document(doc_id: str) - str: 返回指定文档的文本内容。 with open(f/data/docs/{doc_id}.md, encodingutf-8) as f: return f.read()模型可以通过某种方式拿到这个资源地址然后请求客户端去读取。它在设计上跟工具的区别是工具是“动作”关注执行和副作用资源是“状态”关注获取内容。区分清楚这一层有助于设计出更清晰的 MCP 服务。提示词模板则是给客户端复用的一套预置模板。它不执行任何操作纯粹是把一段精心设计的提示词结构暴露给客户端方便在多层模型协作时保持提示风格一致。如果你的应用场景涉及频繁生成同样格式的回复比如周报总结、邮件草稿用提示词模板能省不少事。4.2 鉴权与安全边界设置生产环境里安全是优先级最高的话题。MCP 协议本身不强制鉴权方式但如果你把服务部署成远程 HTTP 模式网关层面的鉴权必须自己操心。我常用的方案有这么几层。第一层是 API 网关上的认证最简单的做法是要求请求头中带有预期的密钥复杂一点可以接入组织内部的统一登录系统或 OAuth 流程。对于个人工具一个随机生成的长密钥基本够用。第二层是工具级的权限控制不是每个客户端都该有资格调用所有工具。比如面向普通员工的 MCP 服务只开放只读类工具面向管理员的才开放写入类工具。MCP Server SDK 允许你在工具注册时带上权限标记也可以在tools/call的入口自行校验调用方身份。还要注意一个容易忽略的细节工具背后的数据访问必须服务端严格限制。之前见过一个开发者的 MCP 服务暴露了一个执行任意 SQL 的工具而且连接串用的是生产数据库的管理员账号。这种工具放在本地开发和放在公网风险等级是完全不同的。工具能做到的事情越多越要警惕攻击面。我现在的原则是每个工具只开放完成任务所需的最小权限宁可多写几个职责单一的窄接口也不要搞一个“万能执行器”。错误处理也需要提前设计。MCP 协议里的工具调用失败最好返回结构化的错误信息让模型能理解并转达给用户。比如工具内部捕获异常后返回一段带错误提示的文本而不是直接把堆栈抛回去否则客户端界面上会出现一堆对用户毫无意义的异常信息。5. 踩坑实录与经验整理5.1 常见问题排查速查表在实际开发 MCP 工具的过程中很多问题都出在细节里。我把遇到过的典型问题整理成了一个排查表方便你按图索骥问题现象可能原因处理办法客户端列表里看不到任何工具服务端启动失败或握手未完成先用调试客户端跑一次确认进程能正常启动且没有导入错误工具能看到但调用报参数校验错误函数签名与客户端解析出的 schema 不一致在服务端把工具定义打印出来对照检查参数类型和必填项调用超时工具内部执行过慢客户端等待时间太短对耗时操作用异步方式实现并在工具描述里注明预估耗时返回内容混乱、模型无法正确解读返回数据结构不规整统一返回为结构化文本或 JSON并在描述中说明格式stdio 模式启动后马上退出脚本路径错误或依赖缺失命令行手动执行脚本观察报错输出服务能跑但一接入应用就报协议版本错误客户端和服务端的 SDK 版本相差太大升级到同一个大版本并在内部环境固定版本号工具内部调用了外部 API经常失败没有做超时和重试策略给外部调用加上重试和降级逻辑必要时异步化这个表是我自己排障实践的沉淀里面的场景基本覆盖了初期开发会遇到的大部分问题。排查逻辑上有一个经验值得分享先验证协议层再验证业务层。很多问题看起来像是协议没对但实际是你的工具函数内部抛了异常或者参数格式不符合预期。这时候与其盯着配置看半天不如先用调试工具直接调一次把协议层和业务层分开定位节奏会快很多。5.2 工具设计上的几条心得体会做工具多了以后我慢慢琢磨出一些设计原则不一定适用所有人但至少在我经手的项目里是有效的。工具粒度要合理不能太大也不能太小。一个工具函数里面塞了十几个参数、好几层条件分支模型很难正确填出所有参数调用失败率会明显上升但工具拆得太碎也不行一个查询功能拆成五个原子操作模型要分好几步才能完成同一个目标不仅耗时增加出错的概率也跟着上升。我觉得合理的粒度是“一个工具完成一个对用户有意义的完整操作”比如“查询订单详情”是一个工具“获取用户ID”是另一个“获取订单列表”又是一个。每个工具的入参控制在三到五个左右超过这个数就要考虑拆分或合并。工具的描述要面向“模型”而不是“人”。技术团队的同学往往会习惯性地把 docstring 写成给自己同事看的接口注释比如“查询订单信息参数 order_id 必填”但模型理解描述的方式和人不太一样它更在意的是“这个工具在什么场景下使用”“参数之间的逻辑关系”“返回结果应该怎么解读”。我现在的写法是每个工具的 docstring 至少包含场景说明、参数逐条解释、返回值格式说明这三部分。如果是比较特殊的工具还会加一个简单示例模型参考示例后调用意图会更加准确。务必处理边界输入。真实世界的输入远比你想象的混乱比如字符串里带前后空格、数字传成了字符串、时间参数给了 13 位时间戳等等。MCP 的 JSON Schema 能约束类型但约束不了取值范围。每个从模型侧传入的参数都要在服务端做一层校验和清洗。别指望模型总是能传来完美的参数事实上模型在某些边缘场景下对参数的处理很有可能会让你意外比如把“无”传给必填字段或者传一个 9999 年的日期。我曾在生产环境里就因为没校验日期范围让一个统计工具跑出了负数差值。返回结果尽量结构化。尽量返回文本格式其中可以直接带上换行和关键信息也可以直接返回 JSON 字符串。如果是数据量较大的结果控制在单次返回的合理范围内避免一次返回几十万字符那会让模型上下文压力剧增。可以考虑通过分页或者摘要机制来压缩量级。还要留意协议版本的变化。MCP 还在快速演进SDK 的接口和协议细节有过多次变更。这里给一个实用建议在一个固定的时间点锁定 SDK 版本并在发布流程中锁定依赖。不要每月随手升级到最新版否则客户端和服务端两边的版本很容易错位。另外关于测试。给 MCP 工具写自动化测试是个稍微别扭的事情因为最终消费者是 AI而不是固定的调用方。我的经验是至少覆盖两类测试一是单测工具函数在固定输入下返回固定输出的逻辑层测试二是协议层测试用官方客户端连接服务端验证工具发现和调用的流程是否畅通。协议层测试不用每次都跑但每次升级 SDK 或修改服务端入口时跑一轮能避免很多线上的低级故障。写在最后的一块个人心得做了这么久的 MCP 工具开发我最大的感受是协议本身并不复杂真正难的是设计出“模型用起来顺手、业务上安全可控”的工具。这有点像给模型当项目经理你要把一个完整的业务能力拆成模型能理解的模块还要保证每个模块的产出让模型读得懂。从个人实践来看MCP 的价值会在工具数量多起来之后体现得越发明显。一开始只有一两个工具时用不用协议差别不大甚至手写解析还更简单但当工具超过十个、要接入多个 AI 客户端、还要处理权限和日志审计的时候标准化的收益就体现出来了。我也建议正在做的朋友不要一开始就追求把所有能力都塞进 MCP最好先拿一两个有代表性的工具试水跑通链路和验证流程再逐步扩大规模。整个过程里多花点时间在工具描述和边界处理上一定值得。最后分享一个具体的小技巧工具函数的日志一定要打完整尤其是入参、出参和耗时。因为模型调用工具的时机和参数有随机性出了线上问题如果没有日志你根本无从推断是模型决策错了还是工具本身出了问题。我所有 MCP 服务端都强制要求打印这类审计日志这几次救命的排查经历都多亏了这些日志。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →