先说我最近亲手踩出来的一个痛点。团队做内部AI助手想让模型直接查订单系统、算库存、读日志最初的想法很天真让模型调几个函数不就行了吗真动手就发现完全不是那么回事。不同客户端要各自适配一套调用方式函数定义散落在各个项目里返回格式模型还在靠猜光是联调就耗了一周。后来整个方案切到MCPModel Context Protocol模型上下文协议上一个Server统一管理所有工具调用开发效率和稳定性都上来了才算是真正把事做成。这篇文章围绕MCP Server和Tool开发把我从零手搓、调试、联调客户端、再推到生产环境的全过程完整拆开讲。内容不吹概念直接给可复现的代码、命令、配置和踩坑记录。如果你正准备给AI应用扩展工具能力或者想为Agent接入企业内部系统这篇文章正好适合你。看完你就能自己写一个能在主流AI客户端里跑起来的MCP工具并且知道怎么调试、怎么避免上线后被人吐槽。1. MCP到底解决了什么问题1.1 一个困局AI能力再强也碰不到你的数据大语言模型最刺眼的短板就是看不见。模型再聪明训练时也没看过你公司的订单表、库存表、工单系统它对真实世界的了解停在你喂给它的上下文里。要想让模型真正干活必须有人把模型调用外部系统这条路打通。早些年大家都用Function Calling。思路不复杂把每个业务能力包装成函数给模型一份函数签名列表模型根据用户请求选一个函数、填好参数你的程序收到后再去执行。听起来很顺但真在工程里落地就会发现问题每个客户端、每个框架都要重新适配一套函数定义格式模型接口变了函数描述可能也要跟着改多个应用想共享同一套工具能力时只能靠复制粘贴代码。说白了大量精力耗在协议对接而不是业务逻辑上。MCP出现后这套逻辑统一了。协议层面定好了客户端和工具提供方之间怎么握手、怎么描述能力、怎么传参、怎么返回工具方不再为每个调用方单独写适配。过去那种每次接新客户端都得返工的问题从根本上被解决掉。这也是MCP在2024年底一发布就能快速普及的原因它把模型如何访问外部世界这件事标准化了。1.2 三层角色Host、Client、ServerMCP这套架构其实很像我们熟悉的客户端-服务器模式只不过对端从浏览器变成了AI应用。理解它只要抓住三个角色。Host是用户面对的AI应用比如常见的聊天客户端、IDE插件这类产品它负责承载对话界面和模型推理但不直接跟工具打交道。Client是Host内部的MCP协议客户端负责和外部Server建立连接、发送请求、接收响应。Server则是工具提供方实现了MCP协议的服务端对外暴露能力。举个例子我在一个聊天界面上问帮我查一下订单C001的状态。Host拿着这个问题交给模型推理模型判断需要调用订单查询工具于是Host让内置的MCP Client去连接订单系统的MCP ServerClient发出列出工具清单和调用某个工具的请求Server执行查询逻辑并返回结构化结果。这个结果会被塞回模型的上下文模型再组织成自然语言回答我。这套分工的核心价值在于Server不需要关心Host是聊天客户端还是IDE插件只要协议一致同样的Server可以被任何支持MCP的客户端复用。这正是我选择MCP的最大理由——写一次工具到处可用。1.3 三种能力类型Tool、Resource、PromptMCP Server对外提供的能力分三种新手经常搞混我一开始也踩过这坑。Tool是可执行动作模型根据用户需求主动调用。它映射到代码里就是一个函数输入参数、输出结果适合做查询订单、创建工单、执行计算这类事情。Tool是本文的主角。Resource是可读取的数据通常由模型在需要时选取相当于给模型提供了一扇主动读资料的窗口比如读取一份产品文档、一条日志、一张图片元数据。它更像一个可寻址的数据源用URI标识。Prompt是预置的提示词模板服务端可以定义一套Prompt模板客户端通过协议拉取并组合进去用来统一对话风格、固化高频指令。实际开发中Tool用得最多Resource和Prompt属于锦上添花。初次上手不必贪多先把Tool写明白后面按需补上另外两类。2. 开发前的准备SDK选型与工程初始化2.1 先选语言Python还是TypeScriptMCP官方SDK主要支持Python和TypeScript选型其实主要看团队现有的技术栈。我个人建议如果是做内部效率工具、数据处理类工具无脑选Python。原因有三一是FastMCP这个高级封装非常省事几行代码就能定义一个带Pydantic校验的工具二是Python生态做数据分析和系统对接最方便三是大多数跑AI应用的后端服务本身就是Python放一起维护成本低。如果目标是嵌入前端生态、或者要跟Node.js应用深度集成选TypeScript SDK。它的API设计和Python版高度对齐概念上没有额外学习成本。Java、Kotlin、Ruby、C#等社区也有社区版SDK但成熟度和文档质量参差不齐。不是特殊情况别选小众语言SDK排错的时候真的会怀疑人生。2.2 工程初始化虚拟环境与依赖安装为了演示效果我全程用Python。第一步是建虚拟环境这步别省。MCP依赖的Pydantic版本和很多项目容易打架不隔离环境的话你可能会在某天升级依赖时被一堆版本冲突淹没。python -m venv .venv source .venv/bin/activate # Windows 使用 .venv\Scripts\activate pip install mcp[cli]装完之后命令行里会出现mcp命令这个后面调试要用所以务必装[cli]扩展。装好后可以跑一句pip show mcp确认版本瞬间心里有底。开发阶段还可以顺手装上httpx远程部署测试时会用到。2.3 第一个最小Server10行代码跑通全链路我习惯先写一个什么都不干的Server跑通了再加业务。这能让你在最早期就排除掉环境和依赖问题后面所有的精力都可以集中到业务上。from mcp.server.fastmcp import FastMCP mcp FastMCP(hello-mcp) mcp.tool() def add_numbers(a: int, b: int) - int: 计算两个整数的和。 return a b if __name__ __main__: mcp.run()把这个文件保存为server.py运行python server.py注意这个进程会一直挂着看起来什么都没发生其实它已经进入stdio传输模式正在标准输入上等待JSON-RPC消息。不要觉得它卡死了这是MCP Server的常态。从需求端来看它作为一个被客户端拉起并持续等待指令的服务是需要一直活着的。跑到这一步你就已经拥有一个最小但真实可用的MCP Server了。下一步就是把业务工具写进去。3. 手写第一个实用工具从函数签名到输入校验3.1 FastMCP的高级API拆解官方Python SDK分两层。底层是mcp.server.Server暴露的是协议级API要自己手动处理会话、请求、错误适合写框架或做深度定制日常业务开发用起来太重。上层就是FastMCP它把底层那一堆状态管理全封装掉了暴露出来的就是一个装饰器。你写一个普通函数加一行mcp.tool()它就成了一个MCP Tool。FastMCP会自动做四件事根据函数签名生成JSON Schema描述工具参数把函数名和docstring转成工具的name和description在客户端调用时自动做参数校验和类型转换把返回值序列化成合适的格式。我强烈建议新手只用FastMCP。等哪天真的碰到它满足不了的需求了再去看底层Server也不迟。3.2 一个能落地的工具查询订单模块光说理论没意思我带你写一个真实业务场景的工具按用户ID查询订单列表。这个例子涵盖了多参数、枚举约束、默认值、错误处理、结构化返回基本覆盖了80%工具开发会遇到的写法。from typing import Literal from mcp.server.fastmcp import FastMCP mcp FastMCP(order-toolkit) mcp.tool(description根据客户ID查询订单列表可按订单状态过滤最多返回50条。) def query_orders( customer_id: str, status: Literal[pending, shipped, finished] pending, limit: int 10, ) - dict: 查询某个客户的订单记录适合在用户询问订单状态时调用。 Args: customer_id: 客户在系统中的唯一标识通常是C开头的一串字符。 status: 订单状态pending表示待处理shipped表示已发货finished表示已完成。 limit: 最多返回的订单条数默认10条最大50条。 # 真实场景这里会去查数据库或调用内部API # 此处为了演示返回一个模拟的订单对象 orders [ { order_id: SO-2024-1001, amount_decimal: 99.90, status: status, } ] return {customer_id: customer_id, count: len(orders), orders: orders}有几个设计细节值得琢磨。第一customer_id没有用默认值模型必须从对话里提取到一个ID才能调用这是有意的宁可让模型多问一次也不能拿空ID去查库。第二status用Literal限定枚举配合默认值模型拿不准用户想查什么状态时就直接走pending不至于瞎猜出非法值。第三limit给了一个合理上限防止模型或用户一句把全部订单拉出来就把服务打垮。3.3 为什么函数签名是给模型看的API这里必须说透一个认知MCP工具的函数签名不是给人看的是给模型看的。模型完全靠工具名称、参数描述、docstring来决定什么时候调用、传什么参数。你写得不清楚模型就瞎调、错调、不调。几条硬经验工具名称用动词开头例如query_orders、send_notification短且表意明确。模型在选择工具时第一眼扫的就是name。docstring里写清楚什么时候该用、什么时候不该用。比如适合在用户询问订单状态时调用就是在告诉模型触发条件。反过来如果某个工具只允许在有权限时用一定要写清限制否则模型会在不合适的场景硬调。参数名必须是业务上的公共语言。customer_id就比c1好理解一百倍模型从帮我查客户C001的订单提取参数时更容易映射到前者。参数能做枚举就做枚举能给默认值就给默认值能不接收自由文本就不接收自由文本。3.4 返回结构设计要贴合模型工具返回给模型的内容会直接塞进上下文。设计返回结构时得想着让模型一眼看懂、少占Token。首选方案是返回结构化数据而不是一堆自然语言。模型能直接从结构化字段里抽信息回答时也更准确。次选方案是摘要明细例如返回一个总计数字加上前N条明细既给模型足够信息又控制上下文体积。唯一要避免的是把一个几百KB的文件内容原样塞回去那会把模型上下文的宝贵空间直接耗尽。通用返回我习惯固定成三件套count条数、items明细列表、next_page分页游标没有就到None。这样模型处理起来有个稳定的套路出错率也低。4. 本地运行与客户端接入让Server真正被用起来4.1 stdio和远程传输怎么选MCP协议本身是传输无关的目前最常见的两种方式是stdio和Streamable HTTP。stdio模式下客户端在本地启动一个子进程运行Server代码通过标准输入输出传JSON-RPC消息。优点是零网络配置、进程隔离好、安全边界清晰。缺点是Server必须跟客户端同一台机器。适合本地开发、个人工具、内网信息查询这类场景。我上面写的mcp.run()默认走的就是stdio开发阶段完全够用。如果要让多个客户端通过HTTP访问同一个Server就得用Streamable HTTP模式。FastMCP换一个入口即可if __name__ __main__: mcp.run(transporthttp, host0.0.0.0, port8000)走HTTP后Server就变成了一个真正的网络服务远程接入、跨机器调用、统一部署都可以实现。4.2 在AI客户端里挂载本地Server开发阶段接客户端之前建议先用命令行验证Server能正常启动mcp run server.py这个命令会以stdio模式启动Server并一直等待客户端连接。主流AI客户端都支持通过配置文件声明MCP Server。配置格式大同小异本质是一个固定的JSON结构{ mcpServers: { order-toolkit: { command: python, args: [server.py] } } }注意事项只有一个核心点command要写绝对路径的python解释器或者确保当前Shell环境激活了虚拟环境。很多人配置完发现客户端连不上九成是因为客户端拉起的不是虚拟环境里的Python导致依赖没装上。4.3 用环境变量管好敏感信息生产开发中一定不要把数据库地址、Token、密钥这类敏感信息硬编码在Server代码里之前就有同事把数据库口令直接写在工具函数里提交了代码事后大扫除才清出来。正确做法是从环境变量读取import os DATABASE_URL os.getenv(DATABASE_URL, )本地跑的时候用Shell导出环境变量export DATABASE_URLpostgresql://user:passlocalhost/order_db python server.py客户端配置里通常也支持env字段可以给被拉起的Server进程注入环境变量{ mcpServers: { order-toolkit: { command: python, args: [server.py], env: { DATABASE_URL: postgresql://user:passlocalhost/order_db } } } }这样敏感信息只在配置层出现代码库里干干净净。5. 调试工具链Inspector与日志三件套5.1 MCP Inspector开发调试首选MCP官方提供了一个叫Inspector的图形化调试工具。装好mcp[cli]之后直接启动开发模式mcp dev server.py执行后会自动拉起Server并打开一个本地调试页面。在这个页面里可以看到Server暴露的工具列表、完整的JSON Schema、调用时的参数结构、返回结果。我强烈建议一切联调工作先在Inspector里做不要一上来就接客户端。Inspector能帮你百分之百确认Server本身的功能是好的之后再接客户端时遇到问题就可以放心把锅甩给客户端配置或网络。调试页面里重点看三个地方Tools列表确认工具有没有注册成功Schema确认参数类型有没有被正确识别Call结果确认返回数据格式是否符合预期。5.2 新手必踩的坑stdout被污染这是MCP开发里最经典的一个坑网上搜排错帖一半都在问这个。在stdio模式下MCP协议的消息是通过标准输出传输的。如果你在工具函数里随手写了一个print(查询完成)这行字就会和JSON-RPC响应一起被塞进stdout客户端解析直接崩溃。表现就是工具调通了但客户端报错或者返回内容夹杂着乱七八糟的文本。解决办法很简单不要在Server代码里用print输出任何日志。所有日志统一走logging模块。logging默认输出到标准错误流和协议使用的标准输出隔离互不干扰。import logging logging.basicConfig(levellogging.INFO) logger logging.getLogger(mcp-demo) mcp.tool() def query_orders(customer_id: str) - dict: logger.info(query_orders called: customer%s, customer_id) ...这个习惯要养成。凡是STDIO模式跑MCP的项目日志输出到stderr这条规则是铁律。5.3 用ClientSession写自动化联调脚本Inspector适合人工调试但每次都打开浏览器点来点去效率太低。我习惯写一个脚本用MCP客户端SDK去连自己的Server把联调自动化。先用mcp.run(transporthttp)把Server在8000端口跑起来然后写测试脚本import asyncio from mcp import ClientSession from mcp.client.sse import sse_client async def main(): async with sse_client(http://127.0.0.1:8000/sse) as (read, write): async with ClientSession(read, write) as session: await session.initialize() tools await session.list_tools() print(工具列表:, [tool.name for tool in tools]) result await session.call_tool( query_orders, {customer_id: C001, status: pending, limit: 5} ) print(调用结果:, result.content) asyncio.run(main())如果Server是stdio模式把sse_client换成stdio_client传一个StdioServerParameters对象就行。这个脚本跑通等于在协议层面验证了完整链路再接任何客户端都只是换个壳的问题。6. 生产化必读鉴权、安全、性能与问题速查6.1 远程部署从stdio切换到Streamable HTTP本地跑明白之后最终要部署成远端服务。FastMCP的HTTP transport实现的是Streamable HTTP同时支持SSE和POST请求模式。启动之后需要验证几个关键端点。Inspector在dev模式下会自动带上鉴权配置但当你手动启动httptransport时要注意给服务加鉴权。最简单的方案是让Server处于受控内网并在前面加一层网关做Token校验。如果要暴露到公网建议在启动前确认HTTP transport是否支持自定义鉴权通常的做法是在网关层统一校验JWT或API KeyServer代码本身不处理鉴权保持纯粹。远程部署的另一个关键点是进程管理。不要裸跑python server.py我建议用进程管理工具比如systemd或Docker拉起守护它配合健康检查和自动重启才不会半夜被一个偶发异常把服务打挂。6.2 安全清单LLM调用工具不可全信这里要说一个很多新人容易忽略的问题模型调用工具时参数是模型想象出来的。它本质上是在做模式匹配完全可能传出一个看似合理但毫无根据的值。因此Server端必须把模型当成一个不可信的外部调用方来对待。必做的安全实践这么几点输入校验是最后一道防线Pydantic校验和业务校验双重叠加。数据库访问一律用参数化查询绝对不能把模型给的参数拼进SQL字符串。模型可能被恶意诱导构造出注入类的输入。工具执行要有超时控制。每个工具在真实系统里都可能有对应的外部依赖不加超时一个慢查询就能拖死整个Server。敏感数据的访问权限要在Server层二次校验不要只靠模型不会乱问。全量审计日志。记录谁在什么时间调了什么工具、传了什么参数。这不仅是排查问题的手段更是出事时的追溯依据。6.3 性能优化别让工具拖垮上下文MCP Server本身不是一个高频请求系统它的瓶颈往往在返回内容撑爆上下文和执行链路的网络延迟上。第一类问题是数据库连接。每次工具调用都新建连接会让延迟高出几倍。建议在Server进程里维护一个全局连接池。FastMCP的进程生命周期较长全局连接是合理的。第二类问题是返回体过大。几千行日志、几十张表结构如果原样返回模型上下文立刻被撑满。我处理长数据的方法是先返回摘要模型如果需要明细再让它带参数调用第二个工具。这套摘要先行、按需取明细的策略很管用。第三类问题是IO阻塞。工具里如果有耗时操作比如请求外部API建议控制并发数量。FastMCP默认支持并发调用但并发太多会把外部依赖打崩。在工具函数里加一个简单的信号量就能限制最大并发。6.4 常见问题与排查速查表把我在实践中经常遇到的问题整理成了一张表优先排查顺序也是从上往下。现象可能原因处理方式客户端连接失败客户端拉起的python解释器不含依赖command用虚拟环境里的绝对路径工具列表为空忘了加mcp.tool()装饰器或函数定义被嵌套确认装饰器存在且函数是模块级定义参数校验报错、调用失败函数缺少类型注解或用了Any、dict等宽泛类型给所有参数写明确类型枚举用Literal返回内容包含乱码或多余文本工具里用了print污染stdout全部改用logging输出到stderr模型反复不调用工具工具描述不清晰、触发条件没写重写docstring写明使用时机和参数含义调用超时工具执行耗时过长优化IO、加缓存、把长任务拆成异步查询HTTP模式客户端连不上未启动HTTP transport或端口被防火墙拦截确认mcp.run(transporthttp)检查监听地址这条表里的每一条我几乎都亲自踩过。最坑的就是print污染stdout因为它表面上日志和结果都在很容易让人误判成序列化问题绕很多弯路才找到真正原因。写在最后如果让我重来我会怎么规划我实际写完几个Server之后最大的感受是MCP工具本质上是替模型写外部接口设计时永远要站在模型的角度思考而不是站在调用者角度思考。模型是那个读函数签名、读描述、猜参数的人你给它清晰一点后端收到脏数据的概率就低一点。再分享一个我觉得很值的小技巧新工具上线前先写一个模拟客户端脚本用固定的几组参数把工具全调一遍把结果存成基线。以后每次改代码跑一遍比对基线行为变化一眼就能看出来。别指望模型测试能把边界情况都覆盖到协议层的回归测试只能靠自己做。这套路子我从零开始捋完最大的建议是别追求大而全。先写一个工具接到一个客户端跑通一条链路再慢慢往里面加内容。MCP的生态还在快速演进协议版本也在迭代保持简单反而是最稳妥的路线。希望你动手的时候能少走我踩过的那几个坑。