mcp SDK 2.x 迁移探路(2026-09-12)¶
Status: 探路完成,兼容代码与 mcp-sdk-2x CI job 已于 2026-09-12 合并进
main(分支 spike/mcp-sdk-2x 保留作历史)。合并后该 job 在 main 上首次真实
运行并通过——ci.yml 只在 main/develop push 与 PR 上触发,所以探路阶段它
从未在云端跑过。依赖上限仍为 mcp<2.0.0,未发版,两者都需要用户单独批准。
触发点:FEATURE_EXPANSION_RESEARCH_2026-09-12.md §3.2 实测到
pyobfus-mcp 在 mcp 2.x 下根本起不来(FastMCP 已更名 MCPServer),
而 MCP 规范 2026-07-28 已 GA。本文件回答两个问题:移植要改多少,
以及不移植到底损失什么。
1. 结论¶
| 问题 | 答案 |
|---|---|
| 移植成本 | 小。适配层三处改动,8 个工具的注册代码一行未改 |
| 1.x / 2.x 能否同一份代码 | 能。97 个 MCP 测试在两个 major 下全过 |
| 行为是否有差异 | 实测无差异:8 工具、meta、status/ai_hint/next_tool 契约、serverInfo 版本全部一致 |
升级能否拿到 2026-07-28 协议 |
不能——见 §3,这是本轮最重要的发现,它推翻了「生态时限」这个立项理由 |
| 建议 | 合并兼容代码 + 新增真跑 2.x 的 CI;本轮仍保留 mcp<2.0.0 上限,等 2.x 线跑一段时间再放开 |
2. 移植实际改了什么¶
适配层 pyobfus_mcp/server.py 三处:
_load_server_class():优先from mcp.server.mcpserver import MCPServer(2.x),ImportError时回落from mcp.server.fastmcp import FastMCP(1.x)。_construct_server():先试server_class(name=..., version=...)。2.x 把version=变成了一等构造参数,于是 1.x 时代那个戳私有app._mcp_server.version的补丁在 2.x 上自动退役;TypeError时才回落到 老路径。main()不变——两个 major 的run()都默认 stdio。
8 个 @app.tool(name=, description=, meta=) 一行未改:2.x 的
MCPServer.tool() 签名仍然接受 meta=(还多了 title / annotations /
icons / structured_output,我们没用)。
另外两处是 1.x 内部形状的假设,已改成两边都认:
tool_manifest.compute_live_manifest():Tool.inputSchema(1.x)↔Tool.input_schema(2.x)。schema 的 JSON 本身两边逐字相同,所以 shipped manifest 的 digest 校验在 2.x 下照样通过——这条是实测,不是推断。test_build_server_advertises_this_packages_version:1.x 走app._mcp_server,2.x 走公开的app.version(内层对象也从_mcp_server更名为_lowlevel_server)。测试改为「哪个 seam 存在就查哪个」,断言落在 「客户端实际收到的版本」上。
3. 🔴 升级不会让 stdio server 用上 2026-07-28 协议¶
立项时的假设是「我们停在 1.x 线 → 最高只能协商到 2025-11-25 → 正在落后」。
实测把它推翻了一半:2.x 的 stdio 服务器同样只协商到 2025-11-25。
同一份 handshake 脚本(自己收发 JSON-RPC,不用 SDK 客户端,因为要看「宿主 看到什么」):
| SDK | 客户端请求 | 协商结果 | serverInfo | 工具数 | 契约字段 |
|---|---|---|---|---|---|
| 1.27.0 | 2025-11-25 |
2025-11-25 |
pyobfus 0.3.12 |
8 | status/ai_hint/next_tool |
| 2.2.0 | 2026-07-28 |
2025-11-25 |
pyobfus 0.3.12 |
8 | 同上 |
| 2.2.0 | 2025-11-25 |
2025-11-25 |
pyobfus 0.3.12 |
8 | 同上 |
原因在 SDK 自己的版本表里:
HANDSHAKE_PROTOCOL_VERSIONS = ('2024-11-05', '2025-03-26', '2025-06-18', '2025-11-25')
MODERN_PROTOCOL_VERSIONS = ('2026-07-28',)
LATEST_HANDSHAKE_VERSION = 2025-11-25
2026-07-28 属于 modern(stateless)那一档,不走 initialize 握手,而是
靠每请求的 io.modelcontextprotocol/protocolVersion 信封 + 探测协商,代码里
所有相关分支都在 client 侧与 streamable-http 上。给 2.x 的 stdio 连接发一条
带该信封的 tools/list,服务器明确拒绝:
-32600 this connection serves the handshake protocol era;
requests carrying the 2026-07-28 envelope are not accepted on it
所以:升到 2.x 对一个本地 stdio server 不带来任何协议层收益。 想要
2026-07-28,要做的是换传输(streamable HTTP / 远程),而那正是
CURRENT_PLAN_ZH.md 里明确「不做」的 hosted MCP 方向。
这条把 2.x 迁移从「生态时限」重新定性为依赖卫生:该做,但不急,且不应 用「协议落后」当理由对外宣传。
4. 验收证据¶
pyobfus_mcp/tests/97 passed 在mcp 1.27.0与mcp 2.2.0下各跑一遍。- 真实 stdio 握手在两个 major 下逐项一致(见 §3 表)。
- shipped tool-manifest digest 在 2.x 下校验通过 → 两边生成的 JSON schema 相同。
- 两个 major 的
requires-python都是>=3.10,与pyobfus-mcp现有下限一致, 放开上限不会影响任何 Python 版本支持。
5. 为什么本轮仍不放开 mcp<2.0.0¶
- 2.x 线很新(2.0.1 / 2.1.0 / 2.2.0 都在约三周内发布)。上限一旦放开,
uvx pyobfus-mcp的用户会自动拿到 2.x,我们没有真实宿主侧的使用数据。 - 升级目前没有可讲的用户收益(§3),所以承担新线的回归风险不划算。
- 代价也已经记清楚:上限存在期间,想主动装 2.x 的用户会遇到 pip 依赖冲突。 如果有人真的来要,这就是放开的信号。
配套动作是新 CI job mcp-sdk-2x:装完包之后故意越过上限装 mcp>=2,<3,
建服务器、断言 8 个工具、跑完整 MCP 测试套件。它是阻塞的,与既有
mcp-sdk-latest 同一思路(那个 job 也是为了抓 SDK 漂移而存在)。顺带修正了
既有 job 的名字:它叫 "latest mcp SDK",但因为本包的上限,resolver 永远给它
1.x,从来没测过 2.x,名字有误导性,现已改为 "mcp SDK 1.x"。
放开上限的建议条件(满足任一即可重新评估):
mcp-sdk-2x这个 job 连续绿若干周;- 有用户或宿主明确要求 2.x;
- 1.x 线进入维护终止或停止接收修复。
6. 不在本轮范围¶
- 不实现 stateless / streamable-HTTP 传输(那是 hosted MCP 方向,已明确不做)。
- 不实现 MCP Apps(沙箱 iframe UI)、extensions framework、CIMD 授权。
- 不改 8 个工具的响应契约。