FastMCP实战指南:用Python构建AI可调用的MCP服务 MCP这个词在圈子里火起来的时候我就知道很大一部分人其实被协议两个字吓住了。我第一次接到需求要用FastMCP开发MCP应用时第一反应也是去翻JSON-RPC规范结果被initialize、notification、tools/call这一串术语绕得头大。后来真正用FastMCP把项目落地才意识到这玩意儿本质上就是把一个个普通的Python函数变成AI能调用、能感知的外接能力传输层的复杂度被框架悄悄吞掉了。今天这篇我不讲空话直接从环境到实战再到排坑把我用FastMCP开发MCP应用的全过程摊开写你能照着跑通也能理解背后发生了什么。1. 别再自己手撕JSON-RPC了MCP开发最省力的切入点1.1 MCP到底在解决什么问题先花点时间把MCPModel Context Protocol的位置说清楚。大模型本身是有脑子但没手的训练数据截止到某个时间点也没有办法主动访问你本地的数据库、浏览器、企业系统。以前想让AI做点真实操作要么让它猜要么用各种私有插件API去拼接每家一个接口格式想换模型就得重写一遍。MCP就是在这个混战里冒出来的usb接口它给AI和外部工具定了一套统一的通信规则服务端把能力暴露成工具Tools、资源Resources、提示词Prompts客户端负责把模型发起的消息翻译成标准的JSON-RPC调用再传给服务端执行。换句话说MCP是一个中间层协议不负责算计逻辑也不管你是用Python还是Node实现它只定义双方怎么握手、怎么发现能力、怎么传参数、怎么回结果。理解了这一点你就明白了为什么开发MCP应用时选对框架很关键协议细节太琐碎硬手写一遍纯属浪费生命而FastMCP就是帮我们把规约折叠成装饰器的那层奶油。1.2 FastMCP如何把协议封装成人话FastMCP是MCP生态里面向Python的快速开发封装。如果你没用它要自己处理的东西远比想象中多服务端要响应initialize握手维护会话状态然后在客户端请求tools/list时把所有工具函数的信息转成JSON Schema返回还得在tools/call被调用时做参数校验、分发到具体函数、把异常映射成协议错误码。一套下来没有几百行代码打不住还特别容易在子协议细节上翻车。FastMCP的做法非常直白你在代码里写一个普通函数加一个装饰器它就自动完成了剩余的所有事。比如from mcp.server.fastmcp import FastMCP mcp FastMCP(Demo) mcp.tool() def add(a: int, b: int) - int: 把两个整数相加 return a b if __name__ __main__: mcp.run()这段程序跑起来就是一个完整的MCP ServerAI客户端可以列出来一个叫add的工具传到好的参数并拿到结果。框架自动生成函数签名对应的Schema自动处理JSON-RPC的请求分发、序列化和错误包装你真正要投入的注意力只用放在业务函数怎么写上。1.3 官方SDK的FastMCP和社区版fastmcp怎么选这里必须先说一个坑目前市面上有两个名叫FastMCP的东西。一个是官方MCP Python SDK里的mcp.server.fastmcp.FastMCP另一个是社区个人维护的fastmcp包两者API长得很像但开发节奏和使用场景有差异。如果你在搜索引擎里查资料看到from fastmcp import FastMCP那是社区版看到from mcp.server.fastmcp import FastMCP那是官方SDK内置的。怎么选我给个实际建议正式项目或长期维护的工具优先用官方SDK它跟MCP规范保持同步该废弃的接口会及时调整以后遇到客户端升级兼容问题少一些。如果只是做个快速原型、或者想看到最简的人性化文档社区版也很好用但要注意它和官方SDK在传递密钥、传输方式上有些微差别。下面所有示例我统一用官方SDK因为它更接近规范风向标你学会了迁移到别的语言、框架也不费劲。2. 环境准备与最小Demo30分钟让AI调用你的第一个工具2.1 安装与版本选择先说环境要求Python建议3.10以上因为FastMCP的很多类型注解和异步特性都要依赖新版本。我习惯用虚拟环境隔离每个MCP项目不然装了一堆全局包以后升级第三方依赖时很容易造成明明代码一样别人能跑你不能跑的诡异问题。python -m venv .venv source .venv/bin/activate # Windows 下是 .venv\Scripts\activate pip install mcp[cli]注意安装包名是mcp不是fastmcp装完后你可以用pip show mcp确认版本。mcp[cli]这个写法会额外装一些命令行工具方便后面用mcp命令调试。装完后也可以跑一句python -c import mcp; print(mcp.__version__)检查能不能正常引入。2.2 写一个最小加法工具新建一个server.py把刚才的加法示例放进去然后终端运行python server.py。你会发现程序看起来卡住了没有任何输出这不是bug而是stdio transport正在等待客户端通过标准输入传递请求。MCP的本地模式大多采用stdio——客户端把JSON-RPC消息写进服务器进程的stdin服务器把响应写到stdout整个过程不需要起端口非常适合本地开发场景。第一次写MCP Server的新手最容易在这里疑惑我明明启动了服务为什么什么都没发生这是因为AI模型还不在场你需要一个中间人发起会话。后面第3节我会讲用MCP Inspector来扮演这个角色到时候你就能看到自己的工具真正被远程AI发现和调用的完整链路。2.3 用MCP Inspector可视化调试官方给了一个非常好用的调试工具MCP Inspector。如果你的电脑有Node环境一条命令就能把它启动然后把你的MCP Server挂上去npx modelcontextprotocol/inspector python server.py浏览器会自动弹出调试面板或者你可以手动打开http://localhost:6277。在页面的工具列表里你会看到add这个tool填入{a: 1, b: 2}点调用会返回{content: [{type: text, text: 3}]}。到这里你已经完整走通了一个MCP应用的开发-暴露-调用闭环。我自己习惯在写任何新工具之前都会先用Inspector验证一遍函数行为而不是直接丢给Claude去试。因为Inspector可以直观看参数格式化的问题你还能在调用结果里检查返回内容是不是AI能理解的结构这比反复调Agent省钱省时间得多。2.4 底层到底发生了什么虽然FastMCP包装得很好但理解底层消息长什么样对排查问题特别有帮助。MCP是基于JSON-RPC 2.0的当客户端调用add工具时会往服务器发大致这样一段消息{jsonrpc:2.0,id:1,method:tools/call,params:{name:add,arguments:{a:1,b:2}}}FastMCP解析到method是tools/call后会根据name找到注册过的add函数把arguments展开成函数参数执行然后把返回值打包成MCP定义好的结构回给客户端{jsonrpc:2.0,id:1,result:{content:[{type:text,text:3}]}}不难吧之所以强调这一点是因为后面一旦出现工具存在但AI调用报错的情况十有八九是参数类型不匹配比如AI传了一个字符串1而你的Python函数注释是int这时看JSON-RPC里的arguments几个来回就能定位问题。3. 把工具、资源、提示词全部用起来FastMCP三件套实战3.1 Tool函数即工具参数Schema全靠类型注解在FastMCP里mcp.tool()装饰器是核心它把任意函数变成一个可被AI发现和调用的工具。但有个容易被忽略的细节AI调不调得对很大程度上取决于你的函数说明书写得好不好。函数名要尽量用小写蛇形、动词开头比如get_weather、add_todo参数需要完整类型注解因为FastMCP会把这些注解转成JSON SchemaAI就是看着这个Schema决定怎么传参的。docstring更是不能含糊这是给AI看的使用手册。对比一下这两个写法# 反例模型大概率不知道要传什么 mcp.tool() def deal(a, b): return a b # 正例模型能明白参数含义和边界 mcp.tool() def add(a: int, b: int) - int: 将两个整数相加并返回结果。a和b都必须是整数不要传小数。 return a b如果你有更复杂的数据结构比如一个订单对象、一段配置信息可以定义一个Pydantic模型作为参数类型FastMCP会把它展示给AI的字段描述。这相当于在AI看得懂和Python函数用得舒服之间搭了一座桥。3.2 Resource让AI主动读取上下文而不是只有工具工具适合做动作比如增删改查但很多场景AI更需要的是数据。MCP里的Resource概念就是为此设计的它类似REST里的GET接口但返回的是可以被当作上下文使用的数据块。用mcp.resource()装饰器定义一个URI。mcp.resource(todos://list) def get_todos() - list[dict]: 返回当前全部待办事项列表 return todos这里我建议URI不要乱起尽量用类似scheme://module/name的清晰结构比如config://app_settings、data://orders。当客户端或AI需要了解当前有什么待办时可以直接请求这个资源而不需要AI先猜是不是应该调某个tool。MCP的哲学是能通过资源给模型更多上下文就少写几个机械工具。3.3 Prompt把常用套路固化成模板Prompt这个能力很多人第一次看会忽略但对实际项目体验提升特别大。它的作用是让开发者预定义一个提示词模板用户可以一键选用。比如你可以做一个任务拆解员模板让AI一听就知道要做什么mcp.prompt() def todo_plan(task: str) - str: return f请帮我把任务拆解为待办事项然后用待办工具逐项录入系统任务内容{task}这样AI拿到这个prompt后就会按你预设的路径工作而且模板里可以直接写请使用待办工具录入引导模型调用你暴露的tools。说白了Prompt就是给你一个把人AIMCP服务三者协作流程固化下来的机会。3.4 一个实用Demo待办事项MCP服务把三件套串起来我写了一个待办事项服务代码不长但基本覆盖了开发所需的全部分类from mcp.server.fastmcp import FastMCP mcp FastMCP(Todos) todos: list[dict] [] mcp.resource(todos://list) def get_todos() - list[dict]: 返回当前全部待办事项列表 return todos mcp.tool() def add_todo(title: str, due: str | None None) - dict: 添加一个待办事项。title是任务名due是可选截止时间格式写成YYYY-MM-DD。 item {id: len(todos) 1, title: title, due: due, done: False} todos.append(item) return item mcp.tool() def complete_todo(todo_id: int) - dict: 根据id把某个待办标记为完成不存在时返回错误信息。 for t in todos: if t[id] todo_id: t[done] True return t return {error: ftodo {todo_id} not found} mcp.prompt() def todo_plan(task: str) - str: return f请帮我把任务拆解为待办事项然后用待办工具逐项录入系统任务内容{task} if __name__ __main__: mcp.run()这段代码我在Inspector里全部调用过添加两条待办标记其中一条完成再看资源里的列表数据完全正确。你可以看到资源、工具、提示词三者互相配合资源回答现在有什么工具负责新增/变更提示词则帮AI一步步操作这样的服务对模型来说非常友好。4. 从本地到远端transport模式与鉴权踩坑记录4.1 stdio vs Streamable HTTP本地和远程的界限要分清从这节开始我们讲点进阶内容。很多教程跑完本地Demo就直接让你部署但没告诉你MCP有两种大不相同的传输通道。stdio是最简单的本地模式进程间通过标准输入/输出通信适合个人电脑上让Claude Desktop、Cursor这类客户端直接拉起Python进程。但stdio没法跨机器访问你不可能让别人的电脑通过stdin连到你的电脑。远程部署现在推荐使用streamable-http这是一种基于HTTP的MCP transport取代了早期不太灵活的SSE方案。使用方式也很简单if __name__ __main__: mcp.run(transportstreamable-http)运行后就起了一个HTTP服务默认监听在本地端口你可以用指定的endpoint让远程的MCP client来连接。如果一个项目打算同时兼顾本地和远程我建议代码里用环境变量控制transport这样一份代码两处复用部署时只改配置不用改业务逻辑。4.2 远程HTTP服务如何做鉴权与超时把MCP Server暴露到公网前必须先考虑一个问题任何能够连接到这个端点的人可能都会让你的工具执行一堆危险操作。默认情况下FastMCP的HTTP服务没有任何鉴权所以绝对不要让它裸奔在公网IP上。我的做法通常是在前面加一层网关做Token校验MCP客户端的请求头里带上Authorization: Bearer token网关校验通过后才把请求转发到FastMCP进程。还有一个容易踩的坑是超时。很多MCP客户端在调用工具时会有自己的timeout设定如果你的工具执行了耗时操作比如调用第三方API、查大表客户端那边可能先超时了。针对这个情况要么把耗时的操作拆成两步先创建任务拿到task_id再轮询查询结果要么在工具里引入异步能力把真正阻塞的操作丢给后台线程执行及时返回已受理的提示避免整个会话卡死。4.3 工具不是越多越好拆服务比堆接口重要我见过一个团队把一个MCP Server里塞了四五十个工具结果AI经常选择困难有时候调用错工具有时候干脆忽略一部分。原因很简单模型在有限上下文里理解工具列表的能力也是有限度的工具描述越长、数量越多每一份被注意到的概率就会越低。我的经验是把相关工具按领域拆到不同的MCP Server中比如todo-server只负责待办事务web-server只负责页面浏览和抓取db-server只负责数据库操作。客户端可以按需挂载多个服务AI在进入某个场景时只要看到该场景的工具就行。这比一个大而全的神级Server要稳得多。5. 对接客户端从Claude Desktop到自定义客户端5.1 在Claude Desktop中挂载你的服务写好了FastMCP服务最直观的验证方式就是把它挂到Claude Desktop上。以macOS为例配置文件在~/Library/Application Support/Claude/claude_desktop_config.jsonWindows在%APPDATA%\Claude\claude_desktop_config.json。你需要在里面声明一个MCP Server{ mcpServers: { todos: { command: python, args: [/absolute/path/to/server.py], env: {} } } }这里有个常见坑command里写python不一定能用因为Claude进程可能没有继承你终端里激活虚拟环境后的PATH。所以我更推荐直接用虚拟环境里的绝对路径比如/home/me/.venv/bin/python或者用uv这类工具来包一层确保拉起服务时能import到mcp包。配置保存后重启Claude Desktop对话界面的工具列表里就会出现你定义的add_todo、complete_todo等工具。这时你尝试对它说帮我把写博客这件事记成待办它就会自己去搜索并调用你的MCP Server整个流程跟打开系统插件一样自然。5.2 用Python写一个轻量MCP客户端自测不是所有场景都适合用桌面软件调试比如写自动化测试、跑批处理时你需要一个程序里直接调用MCP服务。MCP Python SDK也提供了客户端封装配合stdio连接最快的路径是这么写import asyncio from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client async def main(): server StdioServerParameters( command/home/me/.venv/bin/python, args[server.py] ) async with stdio_client(server) as (read, write): async with ClientSession(read, write) as session: await session.initialize() tools await session.list_tools() print([t.name for t in tools]) result await session.call_tool(add_todo, {title: 写博客}) print(result.content) asyncio.run(main())虽然这段代码是异步写法但你细看会发现它只是把stdio的读写抽象成了read和write两个流ClientSession在中间完成协议协商。理解了这个模式后面自己封装RPC客户端、做自动化回归测试就都不难了。5.3 常见调试姿势日志、stdout污染与重连说到调试本地stdio模式有一个特别反直觉的坑你绝对不能在你的工具函数里用print()输出任何日志因为MCP Server是通过stdout返回协议响应的。如果你在函数里print了一行调试信息客户端解析协议时就会收到一条非法的JSON直接报错。正确的做法是把调试信息写到sys.stderr或日志文件里。好在Python的logging模块默认就是输出到stderr所以我在MCP项目里都会加一句import logging logging.basicConfig(levellogging.INFO)这样你既能在终端看到FastMCP内部的调试日志又不会污染协议通道。遇到连接失败时我的排查顺序是先看进程能不能手动启动、import是否正常再看客户端日志里的具体报错最后用Inspector单独连一次判断问题出在客户端配置还是服务端逻辑。6. 我踩过的坑与经验总结6.1 工具描述写不好AI就是不会调这绝对是我排在第一位的高频事故。很多人写完工具只留一句把两个数相加结果AI传了一个字符串参数函数报TypeErrorAI愣在那里不知道怎么修正。后来我养成了一个习惯docstring里写明参数的含义、单位、取值范围、常见异常甚至给一个使用示例。你写清楚AI的调用成功率会从听天由命变成稳如老狗。6.2 异步函数里的阻塞调用会把整个server卡死FastMCP支持async def工具但有一个隐藏陷阱如果你在异步函数里用了requests.get这类同步IO就会阻塞事件循环。当一个耗时请求卡住时服务器就无法再响应其他客户的调用。我的解决方法是异步代码里用httpx.AsyncClient同步代码里如果非要表阻塞操作就用await asyncio.to_thread(func, ...)把任务丢到线程池。6.3 返回类型尽量保持JSON友好别给模型喂野结构MCP返回给模型的内容需要能转成结构化文本所以我一般要求所有工具函数返回Python内置类型或Pydantic模型。如果返回一个自定义对象FastMCP虽然有时候能转成字符串但那个字符串可能长得很乱模型根本没法用。你可以在函数里先model_dump()转成字典或者返回一个格式明确的{status: ..., data: ...}结构模型跟你配合起来会顺畅很多。6.4 安全边界不要给AI配一把万能钥匙最后一个也是最重要的提醒MCP工具是一次真实的远程或本地调用不是模拟世界里的玩具按钮。如果你提供了执行任意shell命令、读取任意路径文件、删除数据库记录的工具那AI一旦被诱骗或误操作后果非常直接。我的原则是每个工具的能力范围必须极窄路径、URL、命令都用白名单或前缀限制密钥等敏感信息一律通过环境变量注入不要写进工具参数或返回结果里高危工具在前端加一层人机确认宁可麻烦一点也不能把系统裸奔在AI手上。我个人做完这个待办事项Demo后最大的感受是FastMCP把MCP应用的开发门槛降到了写函数加装饰器级别但真正决定项目能不能用好MCP的还是你对AI调用习惯的理解。工具命名要直观描述要精确返回结构要稳定这些看起来“非功能”的细节才是AI能顺畅使用你的服务的基石。所以不管未来MCP规范怎么演进把工具设计成清晰、安全、单一职责的小模块这条路永远不会错。