MCP 协议最大改版:去掉了 Session,你的 Server 要改 6 个地方

5 月 21 日,MCP 2026-07-28 Release Candidate 锁定。7 月 28 日正式发布。

这是 MCP 自 2024 年底问世以来最大的一次协议变更。核心改动一句话:协议层面去掉了 Session。

听起来没什么。实际影响:你现有的每个远程 MCP Server 都需要改代码。不改的话,客户端一升级,你的 Server 就挂。

我花了两天把一个生产环境的 MCP Server 从 2025-11-25 迁移到新版。踩了几个坑,记录在这里。

旧版本到底有什么问题

2025-11-25 版本中,每个连接要走 initialize/initialized 握手,服务端发一个 Mcp-Session-Id

bash 复制代码
POST /mcp HTTP/1.1
Content-Type: application/json

{
  "jsonrpc": "2.0", "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-11-25",
    "capabilities": {},
    "clientInfo": {"name": "my-agent", "version": "1.0"}
  }
}

服务端返回 Session ID,后续每个请求都得带 Mcp-Session-Id: 1868a90c-3a3f-4f5b 这个 header。

问题在部署时暴露:

粘性路由。 Session ID 把客户端绑死在某一台实例上。你需要 sticky session 或共享 session store,或者让网关做深度包检查来路由。

扩缩容要命。 加新实例,老会话路由不过去。减实例,活跃会话直接断。

重启即断连。 服务器重启 = 所有 session 丢失 = 客户端全部重新握手。

我们的场景:一个 MCP Server 挂了 4 个工具(数据库查询、文件搜索、代码执行、审批流),跑在 3 个 K8s Pod 后面。之前用 Redis 存 session state,每次扩容都要确认 Redis 集群健康,Pod 重启后客户端得重新走握手流程。运维成本不低。

新版本的 6 个关键变化

变化 1:握手没了

不用 initialize/initialized。协议版本、客户端信息、capabilities 放进每个请求的 _meta 字段:

makefile 复制代码
POST /mcp HTTP/1.1
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: search
Content-Type: application/json

{
  "jsonrpc": "2.0", "id": 1,
  "method": "tools/call",
  "params": {
    "name": "search",
    "arguments": {"q": "kubernetes pod restart"},
    "_meta": {
      "io.modelcontextprotocol/clientInfo": {
        "name": "my-agent", "version": "1.0"
      }
    }
  }
}

每个请求自包含。任意一台服务器实例都能处理。Round-robin 负载均衡就够了。

变化 2:路由 header

新增 Mcp-MethodMcp-Name 两个 HTTP header。网关不用解析 body 就知道这个请求调的是哪个工具。

实际用途------我们用 Nginx 根据工具名把请求路由到不同后端:

nginx 复制代码
# nginx.conf - 按工具名路由到不同服务
map $http_mcp_name $backend {
    "db_query"     db-service;
    "file_search"  search-service;
    "code_exec"    exec-service;
    default        default-service;
}

server {
    listen 443 ssl;
    location /mcp {
        proxy_pass http://$backend;
        proxy_set_header Host $host;
        # 不再需要 sticky session
        # 不再需要 ip_hash
    }
}

以前做同样的事,要么解析 JSON body 提取 method(性能差),要么所有工具挤在同一个后端(没法独立扩缩容)。

变化 3:状态管理方式变了

协议不再替你管 session state。需要有状态的场景,用「显式 handle」模式------服务端创建一个 ID,让模型在后续调用中传回来:

python 复制代码
from mcp.server import Server
import uuid

carts = {}  # 生产环境用 Redis 或数据库
app = Server("shop")

@app.tool("create_cart")
async def create_cart(user_id: str):
    cart_id = str(uuid.uuid4())
    carts[cart_id] = {"user_id": user_id, "items": []}
    return {"cart_id": cart_id, "message": "购物车已创建"}

@app.tool("add_item")
async def add_item(cart_id: str, item: str, qty: int):
    if cart_id not in carts:
        return {"error": "购物车不存在,请先调用 create_cart"}
    carts[cart_id]["items"].append({"item": item, "qty": qty})
    return {"cart_id": cart_id, "items": carts[cart_id]["items"]}

@app.tool("checkout")
async def checkout(cart_id: str):
    if cart_id not in carts:
        return {"error": "购物车不存在"}
    cart = carts.pop(cart_id)
    return {"order_id": str(uuid.uuid4()), "items": cart["items"]}

以前 cart 状态挂在 session 上,session 断了就丢。现在 cart_id 是显式参数,模型自己传来传去。任何实例都能处理,因为状态在存储层而不是传输层。

这个模式还有一个意外收获:模型可以同时操作多个 handle。比如一个 agent 同时打开两个购物车、比较价格、合并下单------旧版的 session 模式做不到,因为一个 session 只绑一个状态。

变化 4:服务端发起请求的新方式

旧版本里,服务端要在处理过程中问客户端问题(比如确认删除),得保持 SSE 长连接。新版改成了请求-响应模式:

python 复制代码
# 服务端返回 InputRequiredResult,不阻塞连接
{
  "resultType": "inputRequired",
  "inputRequests": {
    "confirm": {
      "type": "elicitation",
      "message": "要删除这 3 个文件吗?",
      "schema": {"type": "boolean"}
    }
  },
  "requestState": "eyJzdGVwIjoxLCJmaWxlcyI6WyJ0bXAvYSIsInRtcC9iIiwidG1wL2MiXX0="
}

客户端收集用户输入后,把原始请求连同用户回答一起重新发:

python 复制代码
# 客户端重新发起请求,带上用户回答
POST /mcp HTTP/1.1
Mcp-Method: tools/call
Mcp-Name: delete_files

{
  "jsonrpc": "2.0", "id": 2,
  "method": "tools/call",
  "params": {
    "name": "delete_files",
    "arguments": {"path": "/tmp/old"},
    "inputResponses": {"confirm": true},
    "requestState": "eyJzdGVwIjoxLCJmaWxlcyI6WyJ0bXAvYSIsInRtcC9iIiwidG1wL2MiXX0="
  }
}

requestState 是 base64 编码的服务端状态。客户端不需要理解里面的内容,原样传回就行。因为所有信息都在 payload 里,任意实例都能接手处理。

变化 5:缓存和可观测性

工具列表和资源响应现在带 ttlMs(缓存有效期)和 cacheScope(缓存范围):

python 复制代码
# tools/list 响应
{
  "tools": [...],
  "_meta": {
    "ttlMs": 300000,       # 5分钟内客户端不用重新拉取
    "cacheScope": "global" # 所有用户可共享这份缓存
  }
}

之前客户端要知道工具列表变没变,得开一个 SSE 长连接等通知。现在用 TTL,跟 HTTP Cache-Control 一个思路。

W3C Trace Context 也正式入规范了。_meta 里放 traceparenttracestatebaggage,OpenTelemetry 后端可以把 agent 的工具调用链路串成一棵完整的 span 树。从 agent 发起请求到 MCP Server 再到下游服务,一条 trace 串到底。

变化 6:三个功能被废弃

Roots、Sampling 和 Logging 进入废弃状态,给了 12 个月缓冲期:

废弃功能 替代方案
Roots 工具参数或服务端配置
Sampling 直接调 LLM 提供商 API
Logging stderr(stdio)或 OpenTelemetry(远程)

Logging 影响最大。之前很多 MCP Server 用协议内置的 logging 能力往客户端推日志。现在建议改用标准方案。

我们的做法:

python 复制代码
import structlog

log = structlog.get_logger()

@app.tool("db_query")
async def db_query(sql: str):
    log.info("executing_query", sql=sql[:100])
    results = await run_query(sql)
    log.info("query_done", rows=len(results), ms=elapsed)
    return results

structlog 输出到 stderr,再由 OpenTelemetry Collector 采集。跟现有的 K8s 日志体系打通,比 MCP 自己的 logging 好管理。

迁移踩的 4 个坑

坑 1:SDK 版本跳跃

Python MCP SDK 的 RC 支持在 mcp>=1.8.0。我们项目锁在 mcp==1.6.2,直接 bump 到 1.8 后发现几个内部 API 签名变了------handler 注册的参数格式有微调,类型注解也更严格了。花了半天定位一个 TypeError

避免方式:先看 SDK 的 CHANGELOG 和 migration guide,别直接改版本号跑 pip install。

坑 2:requestState 序列化炸了

我们的审批工具用了 InputRequiredResult 做多步确认。第一版把 Python dict 直接 json.dumps + base64.b64encode 塞进 requestState。结果 dict 里有 datetime 对象,json.dumps 直接报 TypeError: Object of type datetime is not JSON serializable

修复很简单,但这个坑在测试环节容易漏------因为简单场景(纯字符串和数字)不会触发:

python 复制代码
import json, base64
from datetime import datetime

def encode_state(data: dict) -> str:
    def serialize(v):
        if isinstance(v, datetime):
            return v.isoformat()
        return v
    clean = {k: serialize(v) for k, v in data.items()}
    return base64.b64encode(json.dumps(clean).encode()).decode()

坑 3:Nginx header 值大小写

Nginx 读 HTTP header 时会把 header 名转小写、连字符换下划线,所以 Mcp-Method$http_mcp_method 读------这部分没问题。

坑在 header 的值。tools/callTools/Call 是不同的字符串。我们一个测试客户端(自己写的)发了首字母大写的值,导致路由匹配失败,请求全落到 default-service。官方 SDK 统一用小写值,但规范没有强制。如果你有自研客户端,检查一下。

坑 4:Tasks 扩展的生命周期重写

我们的代码执行工具用了 2025-11-25 的实验性 Tasks API 跟踪长任务。新版把 Tasks 从核心功能挪到了 Extension,改动不小:

  • tasks/list 被移除了。没有 session 就没法安全限定"谁的任务",合理但痛。
  • 创建方式从客户端主动发起变成服务端决定(服务端判断某个 tools/call 应该异步执行时,返回 task handle)。
  • tasks/status 拆成了 tasks/gettasks/updatetasks/cancel

我们代码里有 10 多处 tasks/list 调用,全部需要改成自己维护一个 task registry(用 Redis sorted set,按创建时间排序,客户端传 user_id 来过滤)。改动量 200 行左右,主要是 registry 的 CRUD 逻辑。

迁移清单

把完整步骤列在这里,可以直接当 checklist 用:

sql 复制代码
□ 升级 SDK(Python: mcp>=1.8.0, TS: @modelcontextprotocol/sdk@2.x)
□ 去掉 initialize/initialized 处理逻辑
□ 去掉 Mcp-Session-Id 的读取和校验
□ 有状态工具改用显式 handle 模式
□ 如果用了实验性 Tasks,迁移到 Tasks Extension
□ tools/list 响应加上 ttlMs 和 cacheScope
□ MCP Logging 改成 stderr 或 OpenTelemetry
□ 更新网关配置:去掉 sticky session,加 Mcp-Method / Mcp-Name 路由
□ 测试 InputRequiredResult 的 requestState 序列化(重点测非基本类型)
□ Auth:验证响应中的 iss 参数,注册时声明 application_type
□ 自研客户端:确认 Mcp-Method / Mcp-Name header 值用小写

迁不迁

如果你的 MCP Server 只在本地跑(stdio 传输),影响不大。SDK 升级后基本自动适配。

远程部署、多实例、需要水平扩展的------现在就迁。7 月 28 日正式发布后,新版客户端会用新协议,老 Server 直接不兼容。RC 窗口还有 6 周,够了。

我们迁移花了 2 天。第一天改代码(去 session 依赖 + Tasks 重写),第二天改部署配置和测试。代码改动不到 500 行,但省掉了 Redis session store 和 sticky session 配置,运维复杂度降了一个档次。

协议这次把传输层的状态甩掉了,让 MCP Server 跟普通 HTTP API 一样好部署。方向是对的。

相关推荐
用户5191495848452 小时前
Flex QR Code Generator 漏洞利用工具 CVE-2025-10041
人工智能·aigc
修己xj16 小时前
告别手动画图:用自然语言生成可直接发布的 SVG+PNG 技术图
aigc
用户51914958484521 小时前
Windows 渗透测试载荷加载器 POC 工具集
人工智能·aigc
AI创界者1 天前
PilotTTS 一键整合包(Win/Mac):8G 显存畅跑,实测解锁情绪与副语言的精准控制
人工智能·macos·aigc·音视频
英勇无比的消炎药1 天前
一行命令背后:TinyRobot CLI 如何重构 AI 对话接入的效率范式
vue.js·aigc
用户5191495848451 天前
Flowise预认证任意文件上传漏洞分析(CVE-2025-26319)
人工智能·aigc
DigitalOcean1 天前
砍掉 60% AI 推理成本:深度解构 DigitalOcean 推理路由器的 MoE 门控与智能分流机制
llm·aigc·agent
Vergelight1 天前
实战拆解|三类RAG架构差异:朴素、进阶、多轮RAG落地选型指南
架构·大模型·aigc·agent·ai产品经理·转行·ai后台设计
AI袋鼠帝1 天前
终于找到一键做爆款AI短视频的办法了!OiiOii 2.0升级实测【保姆级教程】
人工智能·aigc