原文:https://dev.to/gde/fastmcp-is-now-mcpserver-migrating-a-python-mcp-server-to-the-mcp-sdk-2x-2nhj(作者 @xbill)
本文提供了一份分步迁移指南,将 Python MCP 服务器从 MCP Python SDK 1.x(FastMCP)迁移到 2.x(MCPServer),随后还将 Gemma 4 E2B 分步部署到 Cloud Run 托管的 GPU 系统上。我们构建了一套 Python MCP 工具,用于简化对 vLLM 托管部署的管理。
https://github.com/xbill9/gemma4-dev/tree/main/gpu-2B-cloudrun-devops-agent
什么坏了?
仓库里什么都没变。变的是全新安装。
项目的 requirements.txt 中列出了 mcp,但没有版本约束,所以下一次 pip install 解析到了 2.x 版本。服务器无法导入:
python3 -c "from mcp.server.fastmcp import FastMCP"
raise ModuleNotFoundError(_MESSAGE, name=__name__) ModuleNotFoundError: No module named 'mcp.server.fastmcp'. This is mcp 2.x, where FastMCP was renamed to MCPServer (from mcp.server.mcpserver import MCPServer) and other APIs changed; see the migration guide at https://py.sdk.modelcontextprotocol.io/v2/migration/#fastmcp-renamed-to-mcpserver or pin 'mcp<2' to keep running v1 code.
这个错误首先出现在 Claude Code 中,而且提示信息不太有帮助。MCP 服务器被标记为失败,错误为 Connection closed:Claude Code 用 python3 启动它,导入时崩溃,stdio 在握手前就关闭了。
这个错误信息值得称赞。2.x 包仍然附带了 mcp/server/fastmcp.py,它的唯一作用就是抛出这个错误。如果只是一个简单的 No module named,所有人都得去排查是不是装坏了。
固定版本还是迁移?
两种修复方式,错误信息都提到了。
- 固定
mcp<2:无代码改动;修复位置是每个运行服务器的解释器;会降级共享系统 Python 上所有项目的 mcp;未来修复依赖 v1 维护线。 - 迁移到
MCPServer:只需改 import 和类名;修复位置在仓库内;全局无变化;未来修复在当前开发线。
迁移指南说 v1.x 维护线会持续接收关键 bug 修复和安全补丁,所以固定版本是合理的。但这个部署环境因为另一个原因排除了它:这些项目都安装到同一个系统 Python 中,没有虚拟环境,所以在一个项目里固定版本,等于降级机器上所有其他项目。迁移则能把改动限制在仓库内部。
开始前你需要准备…
- Python 3.10 或更新版本——mcp 2.x 声明了
Requires-Python >=3.10 - 已登录的 Google Cloud SDK,并配置了应用默认凭据
- 一个在你所在区域已启用 Cloud Run GPU 的 Google Cloud 项目(本示例运行在
us-east4) - 一个用于存放模型权重的 GCS 存储桶,命名为
<project>-bucket - 已克隆仓库,并将上述目录作为工作目录
2.x 中改了什么?
官方迁移指南开头用一张表列出了几乎所有项目都会碰到的问题。下面是对照这个服务器的情况——stdio 传输、@mcp.tool() 工具、一个 @mcp.resource(),以及没有任何客户端代码:
FastMCP更名为MCPServer:首个症状是No module named 'mcp.server.fastmcp'——本服务器已中招。httpx被httpx2取代:首个症状是No module named 'httpx'——本服务器已暴露,但本就安全。- 同步处理器改为在工作线程上运行:首个症状是
get_running_loop()在def处理器中抛出异常——本服务器的处理器会跨线程。 - camelCase 字段更名为 snake_case:首个症状是
'Tool' object has no attribute 'inputSchema'——未使用。 - 资源 URI 改为
str而非AnyUrl:首个症状是'str' object has no attribute 'host'——测试里已经调用了str()。 - 传输参数从构造函数移出:首个症状是
unexpected keyword argument 'port'——只改了名称。 McpError更名为MCPError:首个症状是cannot import name 'McpError'——未使用。
有三行值得用更多篇幅说明。
`mcp` 不再安装 `httpx`。 v2 改依赖 httpx2,这是 httpx 的一个 fork。server.py 中的 HTTP 探测和基准测试都 import httpx,而指南也警告了接下来会发生什么:ModuleNotFoundError 的堆栈跟踪完全不会提到 mcp。本服务器之所以幸免,仅仅是因为 requirements.txt 里已经单独列出了 httpx。如果你的服务器 import 了 httpx 却没有声明,现在就该声明。
同步处理器换了线程。 在 v1 中,普通的 def 工具会内联在事件循环上运行,阻塞调用会让服务器上的其他请求全部停滞。v2 将同步处理器运行在 worker 线程。唯一会坏掉的,是那些期望运行在事件循环线程上的代码——在 def 处理器里调用 asyncio.get_running_loop() 现在会抛出异常。本服务器没有这类代码,所以它的同步工具免费获得了并发能力。这一变更只覆盖 def 处理器:如果 async def 工具内部有阻塞调用,它照样会阻塞事件循环,v1 和 v2 都一样。
服务器版本变空了。 在 v1 中,未设置版本的服务器会把已安装的 mcp 版本报告为自己的版本。在 v2 中,它报告空字符串。这不会破坏任何东西,但会在第 5 步的握手中显示出来。
什么没有变
该指南列出了可以原样沿用的日常接口,它们覆盖了典型 FastMCP 服务器的大部分内容:
@mcp.tool()、@mcp.resource()和@mcp.prompt()的参数与处理函数签名保持不变- 工具返回值处理:字符串、字典、模型和内容块仍按相同规则包装
list_tools()和list_resources()返回相同列表lifespan=用法与之前一致
对本服务器而言,这意味着没有任何工具函数需要改动,每个工具的函数体都保持原样。
第一步——摸清影响范围
编辑之前先做评估。下面五条 grep 即可覆盖上文列出的全部内容:
grep -c "^@mcp\.\(tool\|resource\)" server.py
grep -A1 "^@mcp\." server.py | grep -c "^def"
grep -n "get_running_loop\|asyncio.run(" server.py || echo "(no matches)"
grep -n "MCP_\|dotenv" server.py || echo "(no matches)"
grep -n "^import httpx" server.py; grep -n "^httpx" requirements.txt
28 12 (no matches) (no matches) 13:import httpx 14:httpx
共 28 个处理函数,其中 12 个是同步的,没有一个涉及事件循环,且 httpx 已在依赖中声明。
MCP_ 那条 grep 还用于检查另一项变更。v2 不再将 MCP_* 环境变量或 .env 文件读入服务器设置。指南指出,构造函数参数始终具有更高优先级,所以那些变量本来也很少起作用。本服务器从 GOOGLE_CLOUD_PROJECT、VLLM_BASE_URL 等环境变量读取自己的配置,因此不受影响。
有一条是 grep 无法检查的:v2 在构造函数的位置参数中插入了 `title` 和 `description`。 像 FastMCP("Demo", "You answer questions…") 这样的 v1 调用在 v2 上仍能运行,但第二个字符串会被静默当作 title,而不再作为指令传给模型。请将名称保留为位置参数,其余参数一律用关键字传递。本服务器只传了名称。
第二步——重命名
完整的代码改动如下:
-from mcp.server.fastmcp import FastMCP
+from mcp.server.mcpserver import MCPServer
from openai import AsyncOpenAI
...
-# Initialize FastMCP server
-mcp = FastMCP("Self-Hosted vLLM DevOps Agent")
+# Initialize MCP server (mcp 2.x; FastMCP was renamed MCPServer)
+mcp = MCPServer("Self-Hosted vLLM DevOps Agent")
@mcp.tool()、@mcp.resource()、mcp.run() 以及所有工具函数体均保持原样。其他子模块也以同样方式迁移——mcp.server.fastmcp.* 现在是 mcp.server.mcpserver.*,ctx.fastmcp 现在是 ctx.mcp_server——但本服务器两者都没用到。
提示:改导入之前先改用法
这一步曾白白浪费了一次测试运行。项目里有一个 Claude Code 钩子,会在每次编辑后运行 ruff format 和 ruff check --fix。如果先改 import 行,那么在那一瞬间 MCPServer 被导入但未被使用,钩子会把它删除:
cat server.py ruff check --fix --diff server.py
from mcp.server.mcpserver import MCPServer
mcp = FastMCP("demo")
--- server.py
+++ server.py
@@ -1,3 +1,2 @@
-from mcp.server.mcpserver import MCPServer
mcp = FastMCP("demo")
Would fix 1 error.
下一次编辑再重命名类,文件里就完全没有 import 了:
mcp = MCPServer("Self-Hosted vLLM DevOps Agent")
^^^^^^^^^
NameError: name 'MCPServer' is not defined
在同一个编辑操作中完成两处修改,或者先改用法。任何在保存时运行 ruff check --fix 的编辑器都会做同样的事。
第三步——锁定最低版本
导入 mcp.server.mcpserver 的代码无法在 1.x 上运行,所以依赖声明应当体现这一点:
-mcp +mcp>=2
指南自己的示例同样限定了主版本:mcp>=2,<3。如果你更想有意识地迎接 3.x,而不是由 pip install 意外装上,这一行更安全。出于上述原因,httpx 仍保留在同一个文件中的独立一行。
第四步——Lint 与测试
make lint
ruff check . All checks passed! ruff format --check . 14 files already formatted mypy . Success: no issues found in 6 source files
make test
---------------------------------------------------------------------- Ran 28 tests in 1.058s OK
测试套件通过 mcp.list_tools() 将已注册的工具集合与硬编码列表进行比对。该调用属于指南中未变更的列表部分,正是这项测试能捕获重命名后工具静默注册失败的问题。
第五步——手动测试协议
单元测试调用的是 Python API。而真正的客户端是通过 stdio 用 JSON-RPC 通信的,所以那部分也要测。用 `sleep` 保持 stdin 打开:如果只用管道接一个裸 printf,服务器会在应答完 initialize 后看到输入结束并退出。
{ printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"0"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \
'{"jsonrpc":"2.0","id":3,"method":"resources/list","params":{}}'; sleep 5; } \
| python3 server.py 2>/dev/null
响应为 JSON,摘要如下:
initialize OK: name='Self-Hosted vLLM DevOps Agent' version='' proto 2025-06-18 tools/list OK: 27 tools -> cloudrun_analyze_cloud_logging, cloudrun_analyze_gpu_logs, cloudrun_check_gpu_quotas, cloudrun_deploy, ... resources/list OK: ['config://vllm-deployment-template']
27 个工具及资源,与套件的预期列表一致。注意 version=''——即前述表格中的「未版本化服务器」变更。如果客户端或仪表盘会显示版本号,请向 MCPServer(...) 传入 version="..."。
保留这段代码。它是区分「我的服务器坏了」和「我的客户端配置坏了」最快的方法。
第 6 步——注册
Claude Code 会读取 .mcp.json。迁移过程中该文件无需任何改动——它直接用系统自带的 python3 启动 server.py:
{
"mcpServers": {
"cloudrun-devops": {
"command": "python3",
"args": ["/home/xbill/gemma4-dev/gpu-2B-cloudrun-devops-agent/server.py"],
"env": {
"GOOGLE_CLOUD_PROJECT": "aisprint-491218",
"GOOGLE_CLOUD_LOCATION": "us-east4",
"VLLM_BASE_URL": "https://gpu-2b-l4-devops-agent-289270257791.us-east4.run.app",
"MODEL_NAME": "/mnt/models/gemma-4-E2B-it"
}
}
}
}
如果服务在本会话早期启动失败,请从 /mcp 重新连接,或开启新会话以加载修复后的代码。
第 7 步——暂存模型权重
Cloud Run 通过 GCS FUSE 将存储桶只读挂载到 /mnt/models,因此权重只需上传到 GCS 一次。请下载到真实磁盘而非 /tmp——这台主机上的 /tmp 是内存型 tmpfs,容量比模型还小:
hf download google/gemma-4-E2B-it --local-dir ~/hf-downloads/gemma-4-E2B-it gcloud storage rsync ~/hf-downloads/gemma-4-E2B-it gs://aisprint-491218-bucket/gemma-4-E2B-it \ --recursive --exclude='^\.cache/' gcloud storage ls -l gs://aisprint-491218-bucket/gemma-4-E2B-it/
Average throughput: 41.4MiB/s
4954 2026-09-10T14:51:14Z gs://aisprint-491218-bucket/gemma-4-E2B-it/config.json
10246621918 2026-09-10T14:55:13Z gs://aisprint-491218-bucket/gemma-4-E2B-it/model.safetensors
32169626 2026-09-10T14:51:34Z gs://aisprint-491218-bucket/gemma-4-E2B-it/tokenizer.json
TOTAL: 9 objects, 10278849571 bytes (9.57GiB)
检查架构,而不是看文件夹名。 这个存储桶里原本就有一个 gemma-2b-it/ 文件夹,乍看就是要找的模型,但那是原始版 Gemma,gemma4 解析器无法处理它。config.json 能一锤定音:
gcloud storage cat gs://aisprint-491218-bucket/gemma-4-E2B-it/config.json \ | python3 -c 'import json,sys; c=json.load(sys.stdin); t=c["text_config"]; print(c["model_type"], c["architectures"], "hidden", t["hidden_size"], "layers", t["num_hidden_layers"])'
gemma4 ['Gemma4ForConditionalGeneration'] hidden 1536 layers 35
第 8 步——部署到 Cloud Run
Makefile 中的 deploy-vllm 目标是 vLLM 与 Cloud Run 各标志的单一事实来源:
--gpu-type:nvidia-l4——每个实例一块 L4 GPU--concurrency:4——Cloud Run 发送给单个实例的请求数--max-num-seqs:8——vLLM 的批次上限--tool-call-parser、--reasoning-parser:gemma4——缺少任何一个,Gemma 4 工具调用都会失败--no-allow-unauthenticated:不带值——调用方需要携带身份令牌
make deploy
Deploying container to Cloud Run service [gpu-2b-l4-devops-agent] in project [aisprint-491218] region [us-east4] Deploying new service... Creating Revision....................done Routing traffic.....done Done. Service [gpu-2b-l4-devops-agent] revision [gpu-2b-l4-devops-agent-00001-ssq] has been deployed and is serving 100 percent of traffic. Service URL: https://gpu-2b-l4-devops-agent-289270257791.us-east4.run.app
gcloud run deploy 只有在启动探针通过后才会返回,而探针在首次检查前会等待 initialDelaySeconds=180 秒。请做好等待几分钟的准备。
演示模式:固定实例
默认配置会在 0 到 1 个实例之间自动扩缩容,这意味着服务空闲后会经历一次 GPU 冷启动。演示场景等不了这个时间。Makefile 支持 SCALING 变量:
SCALING ?= auto ifeq ($(SCALING),auto) SCALING_FLAGS = --scaling=auto --max-instances=1 --min-instances=0 else SCALING_FLAGS = --scaling=$(SCALING) endif
make deploy SCALING=1 gcloud run services describe gpu-2b-l4-devops-agent --region us-east4 --format='yaml(metadata.annotations)'
run.googleapis.com/manualInstanceCount: '1'
run.googleapis.com/scalingMode: manual
gcloud 的手动扩缩容只接受正整数实例数,因此无法把服务固定在 0 个实例。min 和 max 标志只在 auto 模式下才会传递,因为 gcloud 的帮助说明并未提及它们与固定实例数如何配合。现在会有一块 L4 GPU 持续运行,直到你手动更改。
直接执行 make deploy,以及 cloudrun_deploy、cloudrun_update_scaling 这两个工具,都会有意传入 --scaling=auto——如果服务卡在手动扩缩容的 0 实例状态,所有请求都会返回 503。因此它们中的任何一个都会悄悄把演示环境恢复为缩容到零。
第 9 步——验证
向 vLLM 查询它实际加载了什么:
curl -s -H "Authorization: Bearer $(gcloud auth print-identity-token)" \ https://gpu-2b-l4-devops-agent-289270257791.us-east4.run.app/v1/models | python3 -m json.tool
{
"object": "list",
"data": [
{
"id": "/mnt/models/gemma-4-E2B-it",
"object": "model",
"owned_by": "vllm",
"max_model_len": 16384
}
]
}
模型 ID 是挂载路径,而不是 Hugging Face 仓库 ID。容器是以 --model=/mnt/models/<path> 启动的,所以 OpenAI API 所期望的模型名就是这个路径。
然后让 Agent 执行 cloudrun_verify_model_health 工具:
✅ Model health check PASSED. Model: /mnt/models/gemma-4-E2B-it Response: 'Hello! Yes, I am working. I am Gemma 4, a Large La...' Latency: 2.48 seconds.
这个答案来自迁移后的服务器:Claude Code 通过 MCP 调用工具,工具再调用 vLLM。
第 10 步——基准测试扫描
让 Agent 以默认参数执行 cloudrun_run_benchmark:1 次预热请求,然后在 1、2、4、8 各并发级别下各发送 20 个请求,每个请求最多生成 128 个输出 token,温度设为 0,使用单一固定提示词。
- 并发 1:0.39 Req/s,49.63 Tokens/s,平均延迟 2.58 秒,P95 延迟 2.59 秒
- 并发 2:0.75 Req/s,95.36 Tokens/s,平均延迟 2.68 秒,P95 延迟 2.74 秒
- 并发 4:1.47 Req/s,188.46 Tokens/s,平均延迟 2.71 秒,P95 延迟 2.78 秒
- 并发 8:1.48 Req/s,189.53 Tokens/s,平均延迟 4.86 秒,P95 延迟 5.47 秒
每个级别的所有请求都成功了。三点解读:
从并发 1 到 4,吞吐量几乎线性扩展。 188.46 Tokens/s 是单流 49.63 的 3.8 倍(算术计算),平均延迟只从 2.58 秒升到 2.71 秒。并发 4 时 L4 离满载还远。
从并发 4 到 8,吞吐量停住不再增长。 吞吐量只增加了 0.6%(算术计算),平均延迟却从 2.71 秒涨到 4.86 秒。一半的请求在排队等待。
瓶颈是 Cloud Run 配置,不是 GPU。 单个实例一次只接受 --concurrency=4 个并发请求,而 vLLM 最多批处理 --max-num-seqs=8 个序列。下一次值得跑的测试应该用 --concurrency=8 重新部署,量出 L4 的真实上限而不是配置项的上限。
SDK 版本无法改变以上任何一个数字。基准测试的 HTTP 调用从 server.py 直达 vLLM;MCP 只传输发起测试的那一次工具调用。这次扫描的意义在于展示迁移后的服务器驱动了整个生命周期,而不是衡量 SDK 本身的性能。
资源清理
make destroy
本文发布时没有实际执行——演示服务仍在运行。该命令会删除 Cloud Run 服务;模型权重保留在存储桶里,供下次部署继续使用。
速查表
# 暴露面检查
grep -rn "mcp.server.fastmcp" .
grep -A1 "^@mcp\." server.py | grep -c "^def"
grep -n "^import httpx" server.py; grep -n "^httpx" requirements.txt
# 重命名,一次编辑解决
# from mcp.server.fastmcp import FastMCP -> from mcp.server.mcpserver import MCPServer
# FastMCP("name") -> MCPServer("name")
# requirements.txt: mcp -> mcp>=2(或 mcp>=2,<3),如果 import 了 httpx 要一并声明
make lint && make test
# stdio 冒烟测试:保持 stdin 开启
{ printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"p","version":"0"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'; sleep 5; } | python3 server.py 2>/dev/null
# 部署、演示、清理
make deploy
make deploy SCALING=1
make destroy
总结
本文的目标是:在不改变 Python MCP 服务器现有功能的前提下,把它从 FastMCP 迁移到 MCP Python SDK 2.x。解决方案的关键在于编辑代码之前先对照迁移指南量化暴露面,这使整个改动缩小到一次 import 和一个类名的替换。迁移结果如下:
- 通过:代码改动只有 import 和构造函数;全部 27 个工具和 1 个资源注册原样保留
- 注意:
mcp2.x 不再自动安装httpx;这台服务器之所以安全,只是因为自己声明了httpx依赖 - 通过:12 个同步处理器现在运行在工作线程上,它们的阻塞调用不再卡住事件循环
- 失败:一个格式化钩子在编辑中途删掉了新加的 import;修改用法时要连同 import 一起改
- 通过:迁移后的服务器把 Gemma 4 E2B 部署到了 Cloud Run L4,并跑完了整个基准测试扫描,峰值达到 189.53 Tokens/s,上限来自 Cloud Run 的
--concurrency=4配置
测试范围:mcp 2.2.0、Python 3.14.7,以 1.30.0 wheel 作为 v1 参照。单台 Cloud Run 实例配单张 NVIDIA L4(位于 us-east4),vLLM v0.26.0-cu129,模型为 Gemma 4 E2B,扫描期间手动缩放固定在单实例。每个级别 20 个请求、单一固定提示词、128 个最大输出 token。测的是推理服务栈,不是 SDK。
本文验证了"用 MCP 增量分步把 Python MCP 服务器迁移到 MCP SDK 2.x"这一策略,并且按步骤逐一落地完成。
参考链接
* gpu-2B-cloudrun-devops-agent | GitHub
mcp 2.2.0(mcp-types 2.2.0)、Python 3.14.7、ruff 0.16.6、Google Cloud SDK 583.0.0、vLLM v0.26.0、单张 NVIDIA L4、Cloud Run `us-east4`。
原文:https://dev.to/gde/fastmcp-is-now-mcpserver-migrating-a-python-mcp-server-to-the-mcp-sdk-2x-2nhj(作者 @xbill)




