
刷 MCP 更新说明的时候看到一句话:2026-07-28 修订把 initialize 握手和协议层会话整个删掉了 ------那套每个教程都在教的「先 initialize、再发 initialized 通知」的连接仪式,从规范层面不存在了。教程没骗人,只是过时了。我不信邪,用 Python 标准库写了一个 67 行的最小 server(不装任何 SDK),按新规范把 discover、tools/list、tools/call 全流程跑通,连「版本不支持该报什么错」都实测了一遍。结论:协议比 SDK 让你以为的要轻得多------这也解释了为什么它能在一年内被每家厂商采纳、又在 2025-12 被捐进 Linux 基金会下的 Agentic AI Foundation。
目录
-
- 一、删掉的是握手,不是能力协商
- [二、67 行标准库 server:协议只剩 JSON-RPC](#二、67 行标准库 server:协议只剩 JSON-RPC)
- [三、10 条消息实测:全流程 1,528 字节](#三、10 条消息实测:全流程 1,528 字节)
- [四、三个坑:都是 stdout 惹的祸](#四、三个坑:都是 stdout 惹的祸)
一、删掉的是握手,不是能力协商
背景一句话:MCP 已经是 Agent 生态的事实连接标准------官方 server 注册表 2025 年 9 月进入预览,到 2026 年社区与厂商维护的 server 以千计;对要给模型接工具的人来说,这是绕不开的一层。先补时间线:2024-11-05 首发时是 stdio + HTTP+SSE;2025-03-26 换成 Streamable HTTP;2025-06-18 加了 elicitation;2025-11-25 是最后一代「握手 + 会话」基线;2026-07-28(现行)改为无状态核心 ------SEP-2575 删掉 initialize/initialized 握手,SEP-2567 删掉 Mcp-Session-Id 会话头。版本号、客户端信息、能力声明,改放在每个请求的 params._meta 里 随行;客户端想提前了解 server 端支持哪些修订版本,调 server/discover。这一改动是给部署解锁的:任何实例都能应答任何请求,负载均衡不再需要会话粘滞。这次修订一共动了五块:无状态核心 (上述两条删除);Extensions 框架 (扩展第一次成为一等公民,Tasks 长任务与 MCP Apps(由 server 渲染界面)是头两个官方扩展);授权加固 (对齐 OAuth 2.1 与 OIDC 部署,含 issuer 校验);工具 schema 升级到完整 JSON Schema 2020-12 (oneOf、anyOf、条件引用都能用了);以及一条正式的功能生命周期------每个特性标注 Active/Deprecated/Removed,被弃用的特性至少保留十二个月。一句话:2024 年那个「最小可用协议」正式长成了企业级标准。

二、67 行标准库 server:协议只剩 JSON-RPC
核心逻辑就一个 handle 函数,先看精简骨架(完整 67 行在文末仓库式清单里可复用):
python
import json, sys
REVISION = "2026-07-28"
def reply(req_id, result):
result = dict(result, resultType="complete")
return {"jsonrpc": "2.0", "id": req_id, "result": result}
def handle(req):
rid, method = req.get("id"), req.get("method", "")
ver = (req.get("params") or {}).get("_meta", {}).get(
"io.modelcontextprotocol/protocolVersion")
if ver != REVISION:
return {"jsonrpc": "2.0", "id": rid, "error": {
"code": -32022, "message": f"unsupported: {ver!r}"}}
if method == "server/discover":
return reply(rid, {"supportedRevisions": [REVISION],
"capabilities": {"tools": {}},
"serverInfo": {"name": "min-stdio",
"version": "0.1.0"}})
if method == "tools/list":
return reply(rid, {"tools": TOOLS})
if method == "tools/call":
if req["params"].get("name") == "fx_rate":
return reply(rid, {"content": [{"type": "text",
"text": "HKD/USD = 7.80"}]})
return {"jsonrpc": "2.0", "id": rid,
"error": {"code": -32602, "message": "unknown tool"}}
return {"jsonrpc": "2.0", "id": rid,
"error": {"code": -32601, "message": "not found"}}
三个实现决策都来自规范原文:stdio 传输下消息逐行分隔、行内禁止换行 ,所以序列化用紧凑分隔符;诊断信息只准写 stderr,stdout 是纯协议流;现代规范的成功结果必须带 resultType (本 server 统一放 complete,多轮往返的 inputRequired 是另一条路径,本文未覆盖)。
三、10 条消息实测:全流程 1,528 字节
探针脚本做的事很朴素:以子进程拉起 server,往 stdin 写 JSON-RPC,每写一行就从 stdout 读一行应答,把方向、字节数、完整消息记进 transcript,stderr 单独收着做断言。它与本机 server 子进程完整对话五条路径------discover、tools/list、tools/call、坏版本(2025-06-18)、缺 _meta 版本------每条消息的方向、字节数、内容全部落盘:
python
tr = json.load(open("原始返回/mcp_transcript.json", encoding="utf-8"))
assert len(tr) == 10 # 五问五答
assert [t["dir"] for t in tr] == ["C->S", "S->C"] * 5
assert all(t["msg"].get("jsonrpc") == "2.0" for t in tr)
assert tr[1]["msg"]["result"]["serverInfo"]["name"] == "min-stdio"
assert tr[5]["msg"]["result"]["resultType"] == "complete"
# 两条错误路径:版本不支持统一 -32022
assert tr[7]["msg"]["error"]["code"] == -32022
assert tr[9]["msg"]["error"]["code"] == -32022
total = sum(t["bytes"] for t in tr)
assert total == 1528 and max(t["bytes"] for t in tr) == 273
print(f"10 条消息 / {total} B / 最大单条 273 B / -32022 两条路径实测")
结果:10 条消息合计 1,528 字节 ,最大单条是 tools/list 的应答(273 B,带完整 inputSchema)。两个错误路径都按预期返回 -32022------坏版本号和压根不带版本号,本 server 统一按「不支持」处理;这是实现者自己的选择,规范只规定了错误码语义。最贵的洞见是字节数本身:一次完整的能力发现加工具调用,双向不到 1.6 KB ,协议开销小到可以忽略。字节的分布也有信息量:最贵的一条是 tools/list 应答(273 B),因为 inputSchema 的完整 JSON 要逐字段传;最便宜的一条只有 58 B(不带 _meta 的裸请求,代价是被 -32022 拒掉)。换句话说,这个协议里真正占重量的只有 schema 和元数据,信封本身轻得可以忽略------SDK 们包装出来的复杂度,不是协议本身的重量。


四、三个坑:都是 stdout 惹的祸
坑一:调试 print 是新手第一杀手。 stdio 模式下 stdout 就是协议流,print 调试语句会直接插进 JSON-RPC 流,client 的逐行解析立刻崩------而且症状是「client 报格式错误」,你根本想不到是自己的日志。所有日志一律 print(..., file=sys.stderr),规范白纸黑字。
坑二:行内禁止换行。 一条消息一行;json.dumps 默认不带换行,但格式化输出(indent=2)或者消息文本里混入裸换行都会断流。发送前用「序列化结果里不允许出现 \n」做断言,一行代码买断这类事故。
坑三:resultType 的位置。 现代规范要求它长在 result 对象里,不是 JSON-RPC 响应的顶层------我第一版放错位置,探针立刻抓住。对着规范实现协议时,「字段在哪个对象里」和「字段叫什么」同等重要。顺带一提,探针脚本本身也是回归测试:server 每改一行,重跑五条路径十一个断言,十五秒内就知道有没有改坏------协议实现最怕的「看起来能跑」,靠的就是这种土办法。
边界四条说在前面:本次实测只覆盖 stdio 传输、单工具、无多轮往返的最小面------Streamable HTTP 的 Mcp-Method 路由头、Multi Round-Trip 的 InputRequiredResult、以及 Extensions 注册机制都没有跑,别把本文当成全量合规测试;规范细节(尤其错误码的精确语义)以官方文档为准,本文的 -32022 行为是「规范允许范围内的实现选择」;2025-11-25 及更早的 legacy 客户端怎么兼容,官方给的是「先探 discover,失败再回落旧握手」的双 era 模式,本文的 server 只实现了现代侧;最后,demo 工具返回的是写死的汇率,别当真。
这 67 行 server 和探针脚本可以直接搬走当测试床,收藏备用;如果这篇帮你省下读 SDK 源码的一晚上,收藏+点赞。 吐槽与安利各一句:吐槽 MCP 的 Python SDK 把一个 67 行能说清楚的事包了十几层抽象;安利规范本身写得极其克制------删功能比加功能更需要勇气。你升级到无状态版了吗?旧握手的 server 还打算撑多久?评论区聊聊。MCP/Agent 实测系列开更,关注不迷路。
参考链接
- https://blog.modelcontextprotocol.io/posts/2026-07-28-release-candidate/
- https://modelcontextprotocol.io/specification/latest
