" MCP自由"这个词,最近在 AI 开发者圈子里出现的频率越来越高。但很多人对它的理解停留在"把热门的 MCP Server 仓库拉下来,配到 Claude 或 Cursor 里跑起来"。我个人的观点不太一样:真正的 MCP 自由,是你自己随时能搓出一个 MCP Server,想给 AI Agent 暴露什么能力就暴露什么能力,而不是被社区仓库里现成的 Server 牵着走。 这篇文章我会从零开始,完整走一遍 MCP Server 的开发和部署链路——从协议核心原理、开发环境选型、手写代码,到本地调试、远程部署,最后在客户端里完成配置接入。适合三类人:想把 AI 接进自己业务系统但没头绪的后端工程师、被各种 MCP 配置折腾到头晕的工具型开发者,以及单纯想搞懂 MCP 协议到底是什么的学习者。我会尽量把为什么这样做讲清楚,而不是只丢一堆命令。
1. 先搞明白 MCP 是什么,再决定要不要手搓
1.1 AI Agent 的工具孤岛问题
如果你在 2024 年底之后才开始接触 AI 应用开发,大概率碰到过这类需求:想让 Claude 或某个 Agent 直接查数据库、读写内部系统的数据、操作某个设计软件,或者跑一段运维脚本。传统做法是在每个应用里单独封装一次 function calling,模型要什么就给什么,一个场景一套胶水代码。
这就是工具孤岛。集成的应用越多,胶水代码越长越乱;工具改了接口,你还得跟着改一遍。MCP(Model Context Protocol)就是在这种情况下出现的——它把"AI 调用外部工具"这件事做成了标准化协议。模型侧实现一次 MCP Client,工具侧实现一次 MCP Server,两边通过固定的 JSON-RPC 消息格式通信,谁也不用迁就谁的私有格式。
这个协议最早由 Anthropic 提出并开源,后来被 OpenAI、Microsoft 等多家厂商接受,实际上已经成了 Agent 生态里的事实标准。理解了这层背景你就能明白,MCP 不是一个和 LangChain 类似的开发框架,而是一个协议层标准。就像 HTTP 之于 Web 服务一样,MCP 定义了 AI 应用与工具之间"如何握手、如何发现能力、如何调用、如何返回结果"的一套规则。
1.2 什么时候值得自己写 Server,什么时候用现成的
先泼一盆冷水:不是所有场景都需要自己手搓 MCP Server。只想让 AI 读取文件、访问网页、做点代码搜索,官方和社区仓库里的现成 Server 已经够用,自己再造一遍轮子纯属浪费时间。
真正值得自己动手的,是这三类场景。
第一类是内部系统数据对接。你的用户数据在自建数据库里,订单状态在内部接口里,这些信息不可能让第三方 MCP Server 替你暴露,只能自己写。第二类是私有业务逻辑封装。比如你有一套风控规则,要把它封装成 AI 能调的判断函数,这里也涉及安全边界,没法依赖外部实现。第三类是对现有工具做能力裁剪,比如某个现成的 GitHub MCP Server 暴露了几十个工具,但你想让 AI 只访问某几个仓库,最稳的方式是自己包一层。
判断标准其实很简单:现成 Server 能不能安全、精确地满足需求。注意"安全"和"精确"缺一不可。社区里很多 Server 是个人项目,维护频率不高,权限模型也未必贴合你的场景。用它接入生产环境前,一定要评估清楚。
1.3 "手搓"到底意味着什么
我说的"手搓",并不是让你从零实现 JSON-RPC 协议层。那是 MCP SDK 该做的事,自己造一遍没有任何性价比。手搓的意思是:你能完全掌控 Server 的代码、依赖、启动方式、暴露的能力集合,而不是只能配置别人写好的黑盒。
换句话说,你要掌握的是"用 SDK 高效地写出一个符合自己需求的 MCP Server"的能力。当你会写了,你自然就理解了协议层是怎么回事,这时候再回去看现成 Server 的配置,会发现很多报错你自己就能定位。这也是我从 0 开始写这篇教程的初衷——把最终能力握在自己手里。
2. MCP 协议核心拆解:三件事弄懂,代码就是填空
2.1 JSON-RPC 2.0 是通信基座
MCP 的所有消息都走 JSON-RPC 2.0。这个协议本身非常简单:客户端发一个 request,服务端回一个 response,request 里有 id 字段用于配对;服务端也可以主动发 notification,这种消息不需要客户端回复。 很多人第一次打开 MCP 的协议文档,会被里面一长串 method 名称吓到,实际上核心逻辑跟普通的 RPC 框架没有区别。
一次完整的调用长这样:
{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "get_user", "arguments": {"user_id": 123}}}服务端返回:
{"jsonrpc": "2.0", "id": 1, "result": {"content": [{"type": "text", "text": "{"id": 123, "name": "张三"}"}]}}你可以把它理解成一份快递单:id 是快递单号,method 是收件地址,params 是包裹内容,result 是回执。快递公司不关心你寄的是什么,只要单号和地址对,就能送到。MCP 也是同理,协议层不关心你工具内部逻辑,只负责把消息正确送达。
2.2 initialize 握手:Client 和 Server 互相认识的过程
MCP 的连接过程只有一个核心握手:客户端发送 initialize 请求,把协议版本、客户端名称等信息告诉服务端;服务端返回支持的协议版本、能力列表和服务端信息。然后客户端再发一个 initialized 通知,表示"握手完成,可以开始干活了"。
很多人在这里踩坑:协议版本号不匹配、capabilities 里没声明支持的工具类型,导致后续调用时收到 405 Method Not Found。我实际项目中一般是让 Server 端保持和官方 SDK 同步,客户端会自动适配。你不需要记住所有细节,但要知道握手的目的是协商——两边先对齐"都支持什么、用什么版本、有哪些能力",再进行后续操作。
2.3 三件套能力模型:Tools、Resources、Prompts
MCP 定义了三种核心能力,我把它们称作三件套。
- Tools:AI 主动调用的动作,比如"查询订单状态""运行构建脚本",调用后返回结构化结果。
- Resources:可被 AI 读取的数据资源,比如"项目的 README"或"某个配置文件",应用场景是让 AI 在回答前先获取上下文。
- Prompts:预定义的提示词模板,供客户端在特定交互流程中一键使用。
这三者的区别经常有人问。我的理解:工具是"做事情"的,资源是"给信息"的,提示词是"引导回答"的。很像操作系统的系统调用、文件系统和 shell 脚本——三种不同层级的能力,各有各的使用场景。多数业务场景下,工具和资源是使用频率最高的,提示词用得相对少一些。
2.4 stdio 和 Streamable HTTP:两种传输方式怎么选
MCP 支持两类传输方式:stdio 和 Streamable HTTP(早期文档里叫 SSE,后来协议演进为 Streamable HTTP,但很多旧文档还保留着 SSE 的叫法)。
stdio 适合本地场景。客户端直接以子进程方式启动 Server 的可执行文件,通过标准输入输出通信。这种方式的优点是无需网络、天然安全、不会暴露端口,缺点是只能在本地用。
Streamable HTTP 则是 Server 跑在一个 HTTP 端点上,客户端通过网络请求调用,支持远程部署、多客户端共享,适合放到服务器上给团队或公网使用。如果你的 Server 只服务本机一个 AI 客户端,用 stdio 就够了;如果想让多个客户端共享,或者让远端 Agent 调用,就必须走 HTTP。
搞懂这三件事之后,再去看官方文档或者别人写的 Server 源码,基本不会再发怵。接下来就进入实操。
3. 技术选型:为什么我选 Python + FastMCP,而不是从协议裸写
3.1 语言和 SDK 对比
MCP 官方维护了 TypeScript SDK 和 Python SDK,社区也有 FastMCP(Python)和 mcp-ts 这类更高层的封装。怎么选,核心看你的周边生态。
如果写插件、开发前端工具链,周边都在 Node.js 生态里,选 TypeScript SDK 很顺手。如果你的目标是业务系统对接、数据处理、运维自动化这类偏后端场景,Python 无疑是更合适的底座,库多、社区大、写起来快。
我最终用的是 FastMCP,一个 Python 生态里封装度很高的库。它给我的感觉和 FastAPI 很像——你不需要关心 JSON-RPC 的细节,只需要按普通函数的方式定义工具,剩下的协议转换、参数校验、能力声明都由它自动完成。下面所有代码示例都基于 FastMCP,因为它是目前让我写起来最接近"没有在学习协议"状态的库。
3.2 环境准备与项目初始化
我本机是 macOS,但下面步骤在 Windows 和 Linux 上同样适用。建议所有依赖装进独立虚拟环境,避免污染系统 Python。
mkdir mcp-demo cd mcp-demo python3 -m venv .venv source .venv/bin/activate pip install fastmcp uvicorn gunicorn这里单独装了 uvicorn 和 gunicorn,是为了后面远程部署章节用的。本地调试阶段其实只需要 fastmcp 一个包。装完验证一下版本:
fastmcp --version正常情况下会输出一行版本号。看不到版本号就说明环境有问题,先解决这一步再往下走。
3.3 依赖安装的坑:版本、Python 解释器和 PATH
安装 fastmcp 时第一大坑是版本。早期版本和 2.x 的 API 差异不小,mcp.run()的参数、@mcp.resource()的用法都有变化。我建议直接装最新版,并在 requirements.txt 里锁定版本号:
pip install "fastmcp>=2.0,<3.0"第二大坑是 Python 解释器。如果你的电脑上装了多个 Python 版本,用裸pip很容易装到旧版本解释器的 site-packages 里,启动时就会报 ModuleNotFoundError。解决方法是始终用python3 -m pip install,或者先激活虚拟环境再操作。
还有个小细节,macOS 上如果用了 pyenv 或 Homebrew 的 Python,路径很容易乱。我习惯在项目根目录放一个.python-version文件明确锁定版本,同时把启动命令里的 python 写成绝对路径,这样可以避开各种 PATH 问题。
4. 手写 MCP Server:从"能跑"到"好用"的完整过程
4.1 最简 Server:先让握手成功
第一步,写一个能启动的 MCP Server。新建main.py:
from fastmcp import FastMCP mcp = FastMCP("my-demo-server") if __name__ == "__main__": mcp.run()跑起来:
python main.py没有任何输出,进程也一直不退出,这是正常的。因为 mcp.run() 默认以 stdio 模式启动,等待客户端从标准输入发消息。你现在可以用 MCP Inspector 或者其他客户端连上去试试,后面第五章会详细讲调试。
这里最核心的是FastMCP("my-demo-server"),括号里的字符串是 Server 名称,会在 initialize 握手时发送给客户端,用于展示和区分。一个机器上挂多个 MCP Server 时,这个名字一定要起得有意义,方便在客户端界面上认出来。
4.2 实现第一个工具:让 AI 查询真实数据
光能握手没有价值,接下来注册第一个工具。假设要让 AI 查询 SQLite 数据库里的用户表:
import sqlite3 from fastmcp import FastMCP mcp = FastMCP("user-query-server") DB_PATH = "app.db" @mcp.tool() def get_user(user_id: int) -> dict: """根据用户ID查询用户基本信息""" conn = sqlite3.connect(DB_PATH) cur = conn.cursor() cur.execute("SELECT id, name, email FROM users WHERE id = ?", (user_id,)) row = cur.fetchone() conn.close() if row is None: return {"error": "user not found"} return {"id": row[0], "name": row[1], "email": row[2]} if __name__ == "__main__": mcp.run()这段代码里有两个关键设计。
一个就是函数 docstring。FastMCP 会自动把 docstring 作为工具描述发给模型,模型靠这段描述决定什么时候调用、传入什么参数。只写"查询用户"四个字是不够的,最好包含参数的取值范围、返回值结构、出错时的行为。比如改成"根据用户ID查询用户基本信息,返回 id、name、email 三个字段;用户不存在时返回 error 字段"。模型看到这个描述后,才能更准确地完成调用。
另一个是函数签名里的类型标注。FastMCP 内部用 Pydantic 把类型标注转成 JSON Schema,模型端靠这个 Schema 知道参数应该怎么填。如果忘了类型标注,FastMCP 会直接报错或者把参数当成任意类型,模型就有可能传入字符串,然后你的 SQL 查询就炸了。
4.3 加一个 Resource:让 AI 提前读到上下文
很多时候,AI 在响应之前需要先读一些业务上下文。比如一个售后服务助手,得先知道公司的退货政策,才能给出正确答复。这时候可以用 Resource:
from fastmcp import FastMCP mcp = FastMCP("refund-policy-server") @mcp.resource("policy://refund") def get_refund_policy() -> str: """退货政策内容,供模型在回答前读取""" with open("refund_policy.md", "r", encoding="utf-8") as f: return f.read() if __name__ == "__main__": mcp.run()Resource 的 URI 由你自己定义,上面的 scheme 是policy,客户端可以通过resources/read方法读取。FastMCP 还支持带参数的 Resource 模板,比如policy://refund/{category},这样你就可以根据参数动态返回不同品类的退货政策。
实际产品里,这个 Resource 可以换成从数据库或接口动态拉取内容。比如 AI 每次回答前,都先读取最新版本的上架商品列表,这比让模型靠训练数据里的旧知识回答靠谱得多。
4.4 错误处理与协议合规:别让一个异常毁掉整个会话
真实环境中,数据库会连接失败、参数会传错,如果 Server 内部直接抛异常,FastMCP 默认会把它包装成 JSON-RPC 错误返回,客户端能看到错误信息,但不至于整个会话崩溃。不过我更推荐在工具内部主动捕获可预期的异常,返回带error字段的结构:
@mcp.tool() def get_user(user_id: int) -> dict: try: conn = sqlite3.connect(DB_PATH) cur = conn.cursor() cur.execute("SELECT id, name, email FROM users WHERE id = ?", (user_id,)) row = cur.fetchone() conn.close() except sqlite3.Error as e: return {"error": f"database error: {e}"} if row is None: return {"error": "user not found"} return {"id": row[0], "name": row[1], "email": row[2]}一个经验:给工具函数返回 dict 时,要么固定成功结构,要么固定 error 结构,不要两种混在一起。模型看到结构不稳定的返回,很容易在下一次调用时做出错误判断。我自己的习惯是,成功返回统一用{"data": ...},失败返回{"error": "..."},清晰、可预期、也好写上层逻辑。
5. 本地调试实战:Inspector、日志和"进程秒退"的解决思路
5.1 用 MCP Inspector 做可视化调试
MCP 官方提供了命令行调试工具 Inspector,一条命令就能起:
npx @modelcontextprotocol/inspector前提是你本机装了 Node.js 环境。启动后它会打开一个网页工具,你可以填上要启动的 MCP Server 命令和参数,然后点连接。它会以子进程方式启动你的 Server,并实时展示握手消息、工具列表、参数 Schema,以及每次调用时来回的完整 JSON-RPC 消息。
我的习惯是:每次写完一个新的 MCP Server,先在 Inspector 里把所有工具调通,再接入客户端,这样能提前堵死 80% 的接入问题。你要是在 Inspector 里都调不通,说明问题出在 Server 代码,和客户端没有关系;反之,Inspector 正常但客户端报错,那基本都是客户端配置文件写错了。
5.2 日志定位技巧:stdout 不能随便用
MCP stdio 模式有个非常容易坑新人的特性:stdout 是协议通道,不能用来打日志。如果你在代码里写了print("hello"),这个字符串会被当成非法 JSON-RPC 消息扔给客户端,直接导致通信错乱。所有调试日志都不能往 stdout 打,要么写 stderr,要么写到日志文件。
FastMCP 默认会把 logger 输出到 stderr。你也可以自己加一行,把日志级别调到 DEBUG,看到更细的请求和响应:
import logging logging.basicConfig(level=logging.DEBUG)我排查参数校验问题时几乎必开 DEBUG 日志。比如模型传入的参数类型不对,日志里会明确打出 Pydantic 的校验错误,一眼就能定位。
5.3 "进程秒退"和"连接失败"的排查顺序
stdio 模式最常见的报错就是客户端提示连接失败,或者进程启动后立即退出。我按出现频率排了序,排查时按这个顺序来:
- 虚拟环境没激活。python 指向系统解释器,fastmcp 根本没装上,启动即抛 ModuleNotFoundError。
- 入口文件路径写错。配置文件里 args 写的相对路径,但客户端的工作目录和你终端不一致,文件找不到。
- 用了 npx 启动 Node 版 Server 但本机没有 Node,或者 npx 不在 PATH 里。
- stdio 配置文件里的 command 和 args 带了 shell 语法。比如写了
command: "python main.py",但客户端是直接 exec 的,不经过 shell,整个字符串会被当成一个可执行文件名,必然失败。正确写法是拆成 command 和 args 两项。
Windows 下还经常遇到一种诡异报错,提示"以一种访问权限不允许的方式做了一个访问"。这种大概率是端口或文件句柄被占用,或者启动用户缺少某个目录的权限。排查方式是先确认有没有其他进程占了同一个端口,再看日志里具体卡在哪一步。MCP 本身不要求管理员权限,但如果你把 Server 放在系统保护目录下,普通用户可以读,却未必有执行权限,也会触发类似问题。
6. 从本地到服务器:用 Streamable HTTP 部署 MCP Server
6.1 代码改造:从 stdio 到 HTTP
本地 stdio 调试通过之后,如果要让远程的 Agent 或者其他同事的客户端也能调用,就需要把 Server 改成 HTTP 传输方式。FastMCP 对这层做了封装,改造量非常小:
import uvicorn from fastmcp import FastMCP mcp = FastMCP("remote-user-query-server") @mcp.tool() def get_user(user_id: int) -> dict: # 逻辑和之前一样 ... if __name__ == "__main__": mcp.run(transport="http", port=8000)启动后用浏览器访问http://localhost:8000/,能看到 FastAPI 的自动文档页面,说明服务起来了。MCP 客户端实际访问的端点是http://localhost:8000/mcp,请求方法包括 POST 和 SSE 流式响应。
这里有一个值得提醒的点:HTTP 模式下,Server 的日志输出和协议通道不再共用 stdout,所以你终于可以使用 print 了。但为了规范,生产环境还是建议用 logging 而不是 print。
6.2 生产启动:别用 python 直接跑
本地开发可以python main.py,生产环境不建议直接跑。建议用 gunicorn 加 uvicorn worker:
gunicorn -k uvicorn.workers.UvicornWorker -w 1 -b 0.0.0.0:8000 main:mcp.app注意这里对象名是mcp.app,不是mcp。因为 FastMCP 内部基于 FastAPI,mcp.app暴露的是可以直接交给 ASGI 服务器运行的 FastAPI 应用。-w 1是因为 MCP Server 通常是有状态会话,多 worker 可能导致会话状态不一致,虽然很多场景下无状态工具函数多 worker 也安全,但保守起见,先从单 worker 开始。
启动后如果访问文档页正常,再用 MCP Inspector 连接远程地址验证一遍工具列表,能列出来就说明 HTTP 传输没问题。
6.3 认证配置:暴露公网前必须做的事
MCP 协议本身没有强制认证,但把服务暴露到公网之前,认证是必须做的底线操作。最简单的方案是在 Nginx 层面做 Bearer Token 校验,或者在 FastAPI 应用里加一个中间件拦截请求头:
from starlette.middleware.base import BaseHTTPMiddleware from starlette.responses import JSONResponse VALID_TOKEN = "my-secret-token" class AuthMiddleware(BaseHTTPMiddleware): async def dispatch(self, request, call_next): if request.headers.get("Authorization") != f"Bearer {VALID_TOKEN}": return JSONResponse({"error": "unauthorized"}, status_code=401) return await call_next(request) mcp.app.add_middleware(AuthMiddleware)这样客户端在配置远程 Server 时,需要带上请求头Authorization: Bearer my-secret-token。支持 HTTP MCP 的客户端一般都有配置认证信息的地方,没有的话可以手动在中间件里支持从查询参数或自定义头读取。
如果团队内部使用,Token 方式够用。如果服务更敏感,建议升级为 OAuth2 或对接内部 SSO,这个就超出本篇范围了,但思路是一样的——先有认证,再谈功能。
6.4 Nginx 反向代理配置参考
如果你服务器上已经用 Nginx 管理域名和证书,只需要加一条 location 规则,把/mcp路径转发到 8000 端口:
location /mcp { proxy_pass http://127.0.0.1:8000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header Authorization $http_authorization; }这里proxy_set_header Upgrade和Connection "upgrade"是为了支持 MCP HTTP 传输中的流式响应。不加的话,长连接可能提前断开,表现为"有时候通,有时候超时"。另外如果前面还挂了 HTTPS,别忘了在同一层把 HTTP 跳 HTTPS 或直接 443 监听,确保客户端访问的是https://your-domain.com/mcp。
7. 客户端配置接入:Claude Desktop、Cursor 不完全一样
7.1 Claude Desktop 配置本地 stdio Server
Claude Desktop 的 MCP 配置在claude_desktop_config.json文件里。macOS 路径是~/Library/Application Support/Claude/claude_desktop_config.json,Windows 在%APPDATA%\Claude\claude_desktop_config.json。
{ "mcpServers": { "my-demo-server": { "command": "python", "args": ["/absolute/path/to/main.py"] } } }两点要注意。一是 command 必须是可以直接执行的可执行文件路径,args 里不要包含任何 shell 语法。二是如果你用了虚拟环境,最好把 command 写成虚拟环境里 python 的绝对路径,比如/Users/xxx/.venv/bin/python,而不是裸的python。因为 Claude Desktop 启动子进程时,不一定继承你终端里的 PATH。
配置保存后,重启 Claude Desktop,在有 MCP 标记的会话里应该就能看到工具列表。如果看不到,检查日志文件~/Library/Logs/Claude/下的输出,启动失败的原因一般会在那里。
7.2 Cursor 的 MCP 配置方式
Cursor 的配置和 Claude Desktop 略有不同。它有两种方式:全局配置面板,以及项目级.cursor/mcp.json。
在 Cursor 的 Settings 里找到 MCP 相关面板,可以添加一个新的 stdio server,填写名称、command 和 args,和 Claude Desktop 基本一样。项目管理团队更推荐用.cursor/mcp.json,跟着仓库走,团队其他人 clone 下来就能直接用:
{ "mcpServers": { "my-demo-server": { "command": "/Users/xxx/.venv/bin/python", "args": ["/Users/xxx/projects/mcp-demo/main.py"] } } }Cursor 在较新版本里还支持远程 HTTP 类型的 MCP server,就是在配置界面里选择类型为 http,然后填 URL 和认证信息。这点比 Claude Desktop 目前的配置方式更灵活,适合我们已经部署好的远程 Server。
7.3 远程 HTTP Server 的配置方式
如果你的客户端支持 HTTP 类型的 MCP,配置结构就不再是 command/args,而是 URL 和 header 模板。以 Cursor 为例,添加 server 时选 http,填写:
Name: my-demo URL: https://your-domain.com/mcp Header: Authorization: Bearer my-secret-token初始化方式选"HTTP"而不是"Command"。连接成功后,客户端会通过握手拿到 Server 的名称和工具列表,界面上会显示在线状态。
我自己的部署顺序是:先在本地 stdio 模式全部调通,再做 HTTP 改造,最后才加认证和反向代理。原因很简单,一旦加入了网络、认证、代理这些环节,出问题时定位链路会变长,如果你连 Server 本身都没验证过,根本分不清是代码问题还是网络配置问题。
7.4 配置后看不到工具的通用排查步骤
配置完成后,界面上看不到工具,按下面的顺序排查,效率最高:
- 先在终端手动跑一次启动命令,看能不能正常启动,有没有报错。
- 检查配置文件是不是合法 JSON。MCP 的配置不允许注释,也不允许尾逗号,写错了客户端经常沉默地忽略掉。
- 确认路径全部是绝对路径。相对路径的工作目录不一定是你的项目目录。
- 看客户端日志。Claude Desktop 日志在
~/Library/Logs/Claude/,Cursor 的控制台或日志面板也能看到 MCP 连接信息。 - 最后,确认 Server 的名称和工具列表没有被客户端缓存。有时改完代码重启客户端才能生效。
8. MCP 自由之后的进阶方向与踩坑总结
8.1 我踩过的五个坑
第一个坑是 stdout 污染。最早写 Server 时图方便,直接在工具函数里print了一个结果,结果是客户端各种解析失败。排查了很久才意识到 stdout 不能随便用,从那以后日志一律走 stderr 或文件。
第二个坑是工具函数类型标注不规范。有个工具函数参数写了user_id: int,但模型调用时可能传字符串,FastMCP 会用 Pydantic 做强制转换,如果传了转换不了的值就报错。后来我在 docstring 里明确写了参数格式,模型传错的概率才明显下降。
第三个坑是 docstring 写得太敷衍。早期工具描述就一句话,模型经常在错误场景下调用。后来我养成了习惯:docstring 里写清楚这个工具做什么、参数含义、返回值结构、何时会失败。这个习惯让 Agent 的工具调用准确率明显提升。
第四个坑是版本不匹配。FastMCP 0.x 和 2.x 的 API 变化很大,早期代码升级后被各种报错淹没。现在我在所有项目里都锁定版本号,避免收到惊喜。
第五个坑是环境隔离。有次在服务器上直接用的全局 Python,后来系统升级把依赖弄坏了,MCP Server 全挂了。从那以后我坚持所有项目用虚拟环境,并把启动文件的 command 写成绝对路径,一劳永逸。
8.2 进阶玩法:多工具聚合与权限控制
当你把一个 MCP Server 搓顺了,下一步就是把它做得更强大、更生产化。多工具聚合是很自然的演进:一个 Server 暴露一组相关的工具,比如用户查询、订单查询、退款处理,合成一个"客服助手 Server",AI 客户端只挂一个入口,就能获得一整套业务能力。
聚合时的经验是控制工具数量。一个 Server 挂几十个工具,会让模型的工具选择变得困难,每次请求都要把所有 Schema 发给模型,过度消耗上下文。我一般控制在一组相关工具 5 到 8 个,超过就拆成多个 Server。
权限控制同样重要。同一个 Server 里,不是所有工具都适合让模型随意调用。比如"删除数据"这类危险操作,我建议单独拆成一个 Server 或者增加二次确认机制,避免模型误触发。FastMCP 层面没有内置权限,但你可以用 middleware 或装饰器思路做一层调用审计,记录每次工具调用的入参和结果,方便回溯。
8.3 关于"MCP 自由"的一点个人体会
做 MCP Server 这件事,刚开始确实会花一些时间,但把整条链路走通之后,你获得的能力是可复用的。以后不管遇到什么业务需求,你都可以很快地把一个函数变成 AI 可调用的工具,把一份数据变成 AI 可读的上下文。这种感觉,才是真正的 MCP 自由。
最后再分享一个小技巧:给 MCP Server 做版本的语义化。每改一次工具的签名或行为,就升一个版本号,并存一份 changelog。当接入方多了以后,你会发现这个动作省下的沟通成本远超预期——这是我被几个项目的同事反复问"为什么今天突然少了两个工具"之后总结出来的经验。