Model Context Protocol(MCP)即将迎来自诞生以来最激进的一次协议调整:RC 于 5 月 21 日锁定,经过 10 周 SDK 验证和社区反馈,最终规范计划在 7 月 28 日定稿。六个 SEP(规范增强提案)共同指向同一个目标:移除协议层状态,其中最明确的变化是彻底取消 Mcp-Session-Id 请求头。
这不只是删掉一个 HTTP Header。没有会话、没有握手,意味着 MCP 服务端不能再假设“前一个请求已经替当前请求准备好了上下文”。负载均衡、重试、扩缩容会变得简单,但身份认证、任务进度和临时上下文也必须找到新的归属。
从连接驱动改成请求驱动
有状态协议通常把一次交互拆成几个阶段:客户端先建立连接或完成初始化,服务端生成会话标识,后续请求再携带该标识。服务端因此能够把能力协商结果、用户上下文和运行状态暂存在内存中。
无状态模型要求每个请求都能被独立处理。任意实例拿到请求后,应当仅根据当前请求以及外部持久化数据完成工作,不能依赖某台进程内保存的会话对象。
这会直接改变服务端的几个关键假设:
- 负载均衡器不再需要根据
Mcp-Session-Id实现粘性路由。 - 实例重启不会天然中断协议会话,因为协议层已经没有会话。
- 客户端可以更安全地重试,但写操作仍然需要幂等键。
- 能力信息不能只在握手阶段交换,客户端和服务端需要按照最终规范重新组织发现与调用流程。
- 长任务不能依赖常驻连接中的隐式状态,应使用明确的任务标识和持久化存储。
“无状态”只约束协议层,并不代表业务系统不能保存状态。订单、审批任务、OAuth 授权和 Agent 运行记录仍然需要数据库;变化在于这些状态必须拥有明确的业务标识,而不能藏在协议会话里。
Mcp-Session-Id 消失后,哪些代码最容易出问题
迁移时应先搜索会话头的生产者和消费者:SDK 中间件可能自动写入它,API 网关可能拿它做哈希路由,服务端也可能用它作为内存缓存的主键。
可以先在代码库和部署配置中做一次静态排查:
rg -n --hidden \
--glob '!node_modules' \
--glob '!.git' \
'Mcp-Session-Id|mcp_session|sessionId|sticky|affinity' .
结果不能机械地全部删除。例如业务登录会话未必属于 MCP 协议状态,需要区分以下概念:
| 状态类型 | 迁移建议 |
|---|---|
| MCP 协议会话标识 | 按新规范移除 |
| 用户认证信息 | 使用标准 Authorization 等机制独立传递 |
| 写请求去重信息 | 使用明确的幂等键,并在共享存储中记录 |
| 长任务进度 | 使用业务任务 ID 查询,不依赖连接或进程内存 |
| 工具调用产生的业务数据 | 持久化到数据库或对象存储 |
| 短期性能缓存 | 可以保留,但不能成为请求正确性的前提 |
尤其要警惕这样的代码:
sessions[request.headers["Mcp-Session-Id"]].selected_workspace
它同时依赖请求头和本机内存。迁移后,工作区应成为请求参数、认证主体的属性,或者由请求中的稳定资源 ID 从数据库查询。
可以这样实践:搭一个真正无会话的请求处理器
下面是一个可直接运行的最小 Python 示例,用来演示迁移结构。它不是对尚未定稿规范字段的断言;tools/call 和参数结构应在接入时以最终 MCP 规范及所用 SDK 为准。示例的重点是:每次请求都携带完成操作所需的信息,服务端不创建会话,也不把正确性建立在进程内缓存上。
将以下内容保存为 server.py,使用 Python 3.10 或更高版本运行:
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json
def send_json(handler, status, payload):
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
handler.send_response(status)
handler.send_header("Content-Type", "application/json; charset=utf-8")
handler.send_header("Content-Length", str(len(body)))
handler.end_headers()
handler.wfile.write(body)
class MCPHandler(BaseHTTPRequestHandler):
def do_POST(self):
if self.path != "/mcp":
send_json(self, 404, {"error": "not_found"})
return
if self.headers.get("Mcp-Session-Id"):
send_json(self, 400, {"error": "legacy_session_header_rejected"})
return
try:
length = int(self.headers.get("Content-Length", "0"))
request = json.loads(self.rfile.read(length))
except (ValueError, json.JSONDecodeError):
send_json(self, 400, {"error": "invalid_json"})
return
if request.get("method") != "tools/call":
send_json(self, 400, {"error": "unsupported_method"})
return
params = request.get("params", {})
if params.get("name") != "sum":
send_json(self, 404, {"error": "unknown_tool"})
return
arguments = params.get("arguments", {})
try:
result = float(arguments["a"]) + float(arguments["b"])
except (KeyError, TypeError, ValueError):
send_json(self, 400, {"error": "a_and_b_must_be_numbers"})
return
send_json(self, 200, {
"jsonrpc": "2.0",
"id": request.get("id"),
"result": {"value": result},
})
def log_message(self, format, *args):
return
if __name__ == "__main__":
server = ThreadingHTTPServer(("127.0.0.1", 8000), MCPHandler)
print("Listening on http://127.0.0.1:8000/mcp")
server.serve_forever()
启动服务并连续发送两个互不相关的请求:
python3 server.py
另开一个终端执行:
curl -sS http://127.0.0.1:8000/mcp \
-H 'Content-Type: application/json' \
-d '{
"jsonrpc": "2.0",
"id": "req-1",
"method": "tools/call",
"params": {
"name": "sum",
"arguments": {"a": 20, "b": 22}
}
}'
curl -sS http://127.0.0.1:8000/mcp \
-H 'Content-Type: application/json' \
-H 'Mcp-Session-Id: legacy-session' \
-d '{
"jsonrpc": "2.0",
"id": "req-2",
"method": "tools/call",
"params": {
"name": "sum",
"arguments": {"a": 1, "b": 2}
}
}'
第一个请求不依赖任何先行握手即可被独立处理。第二个请求会暴露仍在发送旧会话头的客户端。这种“迁移期拒绝模式”适合测试和预发布环境;生产环境是否立即拒绝旧请求,应根据兼容窗口决定,也可以先记录指标再逐步收紧。
无状态解决不了重试带来的重复执行
读操作通常可以直接重试,但调用工具可能发送邮件、创建工单或触发付款。即使协议没有会话,网络超时仍可能发生在服务端已经完成操作、客户端却尚未收到响应的时刻。客户端重试后,业务动作可能执行两次。
可以这样实践:为有副作用的调用增加稳定幂等键,并在共享数据库中保存执行结果。下面的 HTTP 片段只展示设计思路,具体 Header 或请求字段需要按照最终规范和应用网关约定调整:
POST /mcp HTTP/1.1
Host: mcp.example.internal
Authorization: Bearer <access-token>
Content-Type: application/json
Idempotency-Key: create-ticket-8f59bdf1
{
"jsonrpc": "2.0",
"id": "req-42",
"method": "tools/call",
"params": {
"name": "create_ticket",
"arguments": {
"project_id": "OPS",
"title": "Database latency alert"
}
}
}
服务端应对“同一认证主体、同一幂等键”返回第一次调用的结果,而不是再次执行工具。幂等记录必须存入所有实例都能访问的数据库或缓存,并设置与业务风险匹配的过期时间。
迁移时不要把协议状态换个名字藏起来
最危险的伪迁移,是删除 Mcp-Session-Id 后立刻增加一个功能相同的私有 Header。这样做既失去新协议的互操作性,也保留了粘性路由和实例故障问题。
落地时可以按下面的顺序推进:
- 盘点 SDK、代理、网关和服务端对
Mcp-Session-Id及握手流程的依赖。 - 将认证、幂等、长任务和资源定位拆成独立机制。
- 确保同一个请求发送到任意实例都能获得一致结果。
- 增加“旧会话头出现次数”“重复执行次数”和“无握手请求失败率”等指标。
- 用实例重启、随机路由和客户端重试验证服务端是否真的无状态。
- 最终字段和交互流程以 7 月 28 日定稿规范及对应 SDK 版本为准。
这次变化的收益很具体:MCP 服务更容易横向扩展,代理层不必维护协议会话,故障恢复路径也更短。代价同样明确:原本被会话掩盖的认证、去重和任务管理问题,必须由应用显式解决。迁移完成的判断标准不是“Header 已删除”,而是任意实例都能仅凭当前请求和共享持久化数据,可靠地给出结果。