MCP 史上最大升级:握手没了、会话没了,Java 生态的六个坑

本文事实核查基于 MCP 2026-07-28 官方 changelog官方发布公告MCP Java SDK ReleasesLangChain4j 1.19.0 官方文档

开篇:一次"把桌子掀了"的协议升级

2026 年 7 月 28 日,MCP 发布了 2026-07-28 版规范。官方口径毫不客气:这是 MCP 自发布以来规模最大的一次协议升级

这次升级不是加几个字段,而是动了协议的根基:initialize 握手没了,Mcp-Session-Id 没了,ping 没了,服务器主动发起的请求也没了------整个协议从"有会话的双向流"变成了"自描述的请求/响应"。配合 Linux 基金会旗下 AAIF(Agentic AI Foundation)的治理背景,MCP 想把自己变成"AI 世界的 HTTP"的野心已经写在脸上了。

规范发布快一个月了,Java 生态跟上了吗?我把官方 changelog、MCP Java SDK 的全部 release notes、Spring AI 与 LangChain4j 的文档翻了一遍,结论是:客户端先行(LangChain4j 1.19 已支持),服务端原地(MCP Java SDK 截至 2.0.1 未跟进),而中间还埋着一个大多数人会搞混的概念陷阱。这篇文章把新规范讲清楚,再把 Java 侧的坑一个个列出来。

一、为什么要无状态:有状态 MCP Server 的水平扩展困境

先用一分钟理解新规范的动机。旧协议(2025-11-25 及之前)的工作方式:

  1. 客户端连上服务器,发送 initialize 握手;
  2. 服务器返回能力清单,并分配一个 Mcp-Session-Id
  3. 之后所有请求都带着这个 session ID,服务器在会话里记住"你是谁、协商了什么能力"。

单机跑没问题,但想象你为一个百万用户的产品部署远程 MCP Server:

  • **多副本怎么办?**session 存在哪个实例的内存里,后续请求就得路由到哪个实例------要么做会话亲和(sticky routing),要么上共享会话存储(Redis 之类);
  • **网关怎么办?**负载均衡器得理解 Mcp-Session-Id 才能正确路由,甚至需要深度包检测;
  • **弹性伸缩怎么办?**实例重启,内存里的会话全丢,客户端全部断线重来。

官方公告里有一句话说得很直白:升级后,原先需要粘性会话、共享会话存储和网关深度包检测的远程 MCP 服务器,现在可以运行在普通的轮询负载均衡器后面 ,按 Mcp-Method 请求头路由,客户端还能按 ttlMs 缓存 tools/list 响应。翻译一下:MCP 服务器终于获得了和普通 HTTP 微服务一样的部署待遇------随便扩、随便缩、随便滚动发布。

二、2026-07-28 规范改了什么:九大变更逐条拆解

以下全部核对自官方 changelog 原文(Major changes 部分),标注了对应的 SEP 编号。

1. 协议级会话移除(SEP-2567)

Mcp-Session-Id header 从 Streamable HTTP 传输中移除,tools/listresources/listprompts/list 这些列表端点不再按连接区分。需要跨调用状态的服务器,改用显式句柄(server-minted handles):服务器生成的 ID 作为普通工具参数传给客户端,客户端在后续调用中传回来。这是整个新规范的设计核心,后面单独展开。

2. initialize 握手移除,请求自描述(SEP-2575)

initialize / notifications/initialized 握手彻底删除。每个请求自己在 _meta 里携带身份:

json 复制代码
{
  "jsonrpc": "2.0",
  "method": "tools/call",
  "_meta": {
    "io.modelcontextprotocol/protocolVersion": "2026-07-28",
    "io.modelcontextprotocol/clientInfo": { "name": "my-client", "version": "1.0" },
    "io.modelcontextprotocol/clientCapabilities": { ... }
  }
}

客户端 SHOULD 在每个请求里自报家门(clientInfo),服务器 SHOULD 在每个结果的 _meta 里标识自己(serverInfo)。版本不匹配返回 UnsupportedProtocolVersionError协议的"我是谁、我支持什么"从握手时的一次性声明,变成了每个报文的随身携带。

3. 新增 server/discover 发现方法(SEP-2575)

服务器 MUST 实现 server/discover RPC,广播自己支持的协议版本、能力和身份。客户端 MAY 在发任何请求之前先调它做版本选择,在 STDIO 传输上也可以用它做向后兼容探测。这是无握手协议的配套机制------不知道对面是什么版本?问一句。

4. subscriptions/listen 取代 GET SSE 流(SEP-2575)

旧协议里,客户端通过 HTTP GET 打开一条 SSE 长连接接收服务器通知(列表变更、资源更新)。新规范把这换成 subscriptions/listen:一条长生命周期的 POST 响应流,客户端按类型订阅(toolsListChanged / promptsListChanged / resourcesListChanged / resourceSubscriptions),服务器确认订阅并用 subscriptionId 标记后续通知。请求级的通知(notifications/progressnotifications/message)继续走各自请求的响应流,不受影响。

5. ping、logging/setLevel、roots/list_changed 移除(SEP-2575)

三个方法/通知消失。日志级别改为请求级:_meta 里的 io.modelcontextprotocol/logLevel 随请求携带,服务器不得为没带这个字段的请求发日志通知。ping 的存活检查职责交给了传输层本身(HTTP 状态码、TCP 连接)。

6. Tasks 从核心协议移入官方扩展(SEP-2663)

2025-11-25 引入的实验性 Tasks 进了独立扩展(io.modelcontextprotocol/tasks),并围绕无状态重新设计:阻塞式的 tasks/result 换成 tasks/get 轮询,新增 tasks/update 支持客户端向服务器补充输入,tasks/list 被移除(无会话下无法安全限定范围),服务器可以不经请求级 opt-in 直接返回任务句柄。基于旧 Tasks API 构建的实现必须迁移------这是官方迁移建议里明确点名的破坏性变更。

7. MRTR 取代服务器发起请求(SEP-2322)

旧协议里,服务器可以主动向客户端发请求:roots/list(要文件系统根目录)、sampling/createMessage(要模型补全)、elicitation/create(向用户征询输入)。新规范全部废除,换成多轮往返请求(Multi Round-Trip Requests)模式:

  1. 服务器处理请求时发现缺信息,返回 InputRequiredResultresultType: "input_required"),inputRequests 字段里列出需要客户端补充什么;
  2. 客户端收集好答案(可能要问用户),带着 inputResponses 重新发起原始请求
  3. 任何服务器实例都能接手这次重试------因为所需信息全在载荷里,不需要会话。

配合强制规则"服务器发起的请求只能发生在处理客户端请求期间"(SEP-2260,从 SHOULD 升级为 MUST),用户永远不会被凭空打扰。

8. resultType 必填(SEP-2322)

所有结果必须携带 resultType 字段:"complete"(普通结果)或 "input_required"(MRTR 中间结果)。客户端必须把旧协议服务器省略此字段的结果当作 "complete"

9. SSE 流恢复能力移除(SEP-2575)

Last-Event-ID header 和 SSE 事件 ID 从 Streamable HTTP 传输中移除。响应流断了,in-flight 的请求就丢了,客户端必须用新的请求 ID 重新发起。断点续传的复杂度被砍掉,换来的是"任何实例都能处理重试"的简单性。

顺手记一下:弃用清单与小变更

这次规范同时建立了正式的功能生命周期与弃用政策(最少 12 个月弃用窗口),并挂出了弃用清单:

被弃用 官方迁移建议
Roots 用工具参数、资源 URI 或服务器配置传递目录/文件
Sampling 直接集成 LLM 提供商 API,不经过 MCP 中转
Logging stdio 场景写 stderr,远程场景用 OpenTelemetry
HTTP+SSE 传输(2025-03-26 起弃用) 迁移到 Streamable HTTP
OAuth 2.0 动态客户端注册(RFC 7591) 改用 Client ID Metadata Documents

值得 Java 开发者注意的小变更:资源未找到错误码从 -32002 改为 -32602 (对齐 JSON-RPC 标准);错误码空间正式分区(-32000~-32019 实现自定义,-32020~-32099 规范保留,新错误码 HeaderMismatch / MissingRequiredClientCapability / UnsupportedProtocolVersion 分别落在 -32020 / -32021 / -32022);Mcp-Method / Mcp-Name 成为 Streamable HTTP POST 的标准头;列表结果要求携带 ttlMs / cacheScope 缓存提示;OTel 的 traceparent / tracestate / baggage 写进 _meta 成为标准追踪约定。

三、Java 生态踩坑实录

规范讲完了,进入正题:Java 侧现在动手会踩到什么。

坑 1:两种"无状态"根本不是一回事(最致命的概念陷阱)

如果你看过 Spring AI 的文档,会发现它早就有 spring.ai.mcp.server.protocol=STATELESS 配置(上一篇速成篇的第 10 分钟用过)。于是很容易得出结论:"Spring AI 已经支持无状态了,新规范无非就是把我们现有配置变成默认"。

错。这是两代完全不同的"无状态":

Spring AI 的 STATELESS 2026-07-28 规范的无状态
规范代际 2025-11-25 规范的无状态变体 2026-07-28 规范的核心形态
initialize 握手 仍然存在(客户端照常握手,服务器不分配会话) 彻底移除,请求自描述
会话 服务器不维护会话状态,但协议层会话语义还在 协议级会话概念整体消失
服务器发起请求 协议层保留该机制(但 Spring AI 的 STATELESS 模式下不可用,见上一篇第 10 分钟) 全部废除,改 MRTR
实现载体 MCP Java SDK 2.0.x 的 stateless server 尚无 Java SDK 实现

证据链很硬:MCP Java SDK 2.0.0 的 release notes 明确写着 "tracks the latest 2025-11-25 MCP specification";截至 2.0.1(2026-08-19 发布)的所有 release notes 里,没有任何一个版本提及 2026-07-28。Spring AI 2.0.x 构建在这个 SDK 之上,自然只能讲 2025-11-25 的"方言"。

实操含义 :你的 Spring AI MCP Server 配了 STATELESS,在新协议客户端眼里依然是一个 legacy(2025-11-25)服务器------有握手、有旧式通知机制。它解决的只是"多副本部署"这一个维度的问题(这点确实有用),不要把它当成新规范兼容性。

坑 2:MCP Java SDK 服务端还没跟上,等还是不等?

时间线摆在这(全部核对自 GitHub Releases):

时间 事件
2026-05-21 2026-07-28 规范 RC 锁定,进入十周验证窗口
2026-06-11 MCP Java SDK 2.0.0 GA(对应 2025-11-25 规范)
2026-07-28 2026-07-28 规范正式发布
2026-08-14 LangChain4j 1.19.0 发布,MCP 客户端 支持 2026-07-28(#5881
2026-08-19 MCP Java SDK 2.0.1 发布------11 项变更全部是修复与依赖升级,没有新规范支持

官方 RC 公告对 Tier 1 SDK 的预期是"在这个窗口内提供支持"(expected,措辞并非强制)。Java SDK 方面,规范正式发布两周后(2026-08-13)开启了设计 issue #1089 "MCP Spec 28-7-2026 Design",状态进行中,但没有给出任何时间表承诺。要不要等?我的判断是服务端不用等、也不用怕

  • 新规范对服务端 的破坏性变更集中在传输层与会话管理------这恰恰是 SDK 和框架替你扛的部分。等 Spring AI 升级到支持 2026-07-28 的 SDK 版本,你的 @McpTool 注解代码大概率一行不用改(工具本身与协议版本无关);
  • 需要提前动手的是依赖协议语义的代码:错误码字面匹配(见坑 5)、订阅与通知逻辑(见坑 4)、以及如果用了实验性 Tasks API 的存量实现(官方明确说必须迁移)。

坑 3:LangChain4j 已支持,但默认自动检测有一个 30 秒的坑

Java 侧唯一跟上了新规范的是 LangChain4j 1.19 的 MCP 客户端。它同时支持两代协议:2025-11-25(legacy,有状态)和 2026-07-28(modern,无状态),默认自动检测 ------启动时发一个 server/discover 请求,服务器报错或超时就按 legacy 处理。

坑在超时的默认值:protocolDetectionTimeout 默认等于 initializationTimeout,也就是 30 秒。官方文档解释了这个默认值的原因:很多 MCP 服务器是子进程方式拉起的,冷启动需要时间,检测窗口太短会把现代服务器误判成传统服务器。但对连接远程服务器的场景,30 秒的检测等待会让你的应用启动(或首次工具调用链路)显得莫名奇慢,而且因为"静默超时回退 legacy"只记一条 warning 日志,没有报错------排查起来非常隐蔽。

解法是已知服务器版本时显式指定,跳过检测

java 复制代码
// 已知对面是新规范服务器:直接指定,省掉 server/discover 往返
McpClient mcpClient = DefaultMcpClient.builder()
    .transport(transport)
    .protocolVersion("2026-07-28")
    .build();

// 已知对面是 legacy 服务器(比如所有 Spring AI 2.0.x 的 MCP Server)
McpClient mcpClient = DefaultMcpClient.builder()
    .transport(transport)
    .protocolVersion("2025-11-25")
    .build();

官方文档还点名了另一个适用场景:一些旧版 MCP 服务器收到不认识的方法不是返回错误而是直接终止,显式指定协议版本可以完全避开这种服务器被探测请求打死的问题。

顺带一个实战推论:用 LangChain4j 客户端连接 Spring AI 2.0.x 的 MCP Server 时,无论自动检测结果如何,实际通信必然落在 legacy 模式------因为服务端只会说 2025-11-25 的"方言"。与其让客户端白等探测,不如直接 protocolVersion("2025-11-25")。这里还有一个值得知道的互操作细节:MCP Java SDK 2.0.0 收到 server/discover 探测请求会直接返回 HTTP 500(java-sdk #1072,P1 级 bug,2.0.1 已修复)------所以用 LangChain4j 自动探测一个基于 java-sdk 2.0.0 的服务器时,日志里那条探测请求的 500 不是你的服务出了问题,而是它的"回退 legacy"机制在正常工作。

坑 4:通知与订阅的 API 形态在两代协议间不兼容

LangChain4j 文档里专门列了两代协议的功能差异,写跨协议客户端代码时容易踩:

资源订阅------legacy 是单个 URI 订阅加回调:

java 复制代码
// legacy(2025-11-25)
McpClient mcpClient = DefaultMcpClient.builder()
    .transport(transport)
    .onResourceUpdated((client, uri) -> client.readResource(uri))
    .build();
mcpClient.subscribeToResource("file:///status");

// modern(2026-07-28):批量订阅,返回订阅 ID
long subId = mcpClient.subscribeToResources(List.of("file:///status", "file:///config"));
mcpClient.unsubscribeFromResources(subId);

服务端通知通道 ------legacy 时代的辅助 GET SSE 流(LangChain4j 的 subsidiaryChannel(true),默认关闭)在新协议下不复存在,2026-07-28 的通知全部走 subscriptions/listen。如果你的代码里有针对 GET SSE 流的重连逻辑,迁移时要整体重写。

坑 5:错误码字面匹配会在升级时静默失效

前面提过的错误码变更单独拎出来讲,因为它是最典型的"静默失效":

  • 资源未找到从 MCP 自定义的 -32002 改为 JSON-RPC 标准的 -32602(Invalid Params)。任何 if (errorCode == -32002) 式的字面匹配,在对面服务器升级新规范后会永远走不进分支------不报错、不告警,只是业务逻辑悄悄不对了;
  • 错误码空间分区后,新引入的规范错误码(HeaderMismatch -32020、MissingRequiredClientCapability -32021、UnsupportedProtocolVersion -32022)与旧 drafts 时代的编号不同(分别曾是 -32001 / -32003 / -32004),跨版本 SDK 的错误处理代码要留意。

排查建议:全局搜索一遍 -32002-32003-32004 的字面量;错误码语义判断尽量收敛到常量或枚举,别散落硬编码。

坑 6:x-mcp-header------新规范里最实用的新特性之一

新规范要求 Streamable HTTP POST 携带标准头 Mcp-Method / Mcp-Name,并支持从工具参数生成自定义 HTTP 头 :服务器在工具的 input schema 里用 x-mcp-header 标记参数,客户端就会在调用时把它同时放进请求体和 HTTP 头。

LangChain4j 客户端已支持:参数值会被转成形如 Mcp-Param-X-Tenant-Id 的请求头(非 ASCII 值自动 Base64 编码)。限制是参数类型只能是 string / integer / boolean,违反约束的工具会被排除出 listTools() 并记一条警告日志------和坑 3 一样是"不报错、只记日志"的排障盲区,多租户场景接完后记得核对工具列表是否完整。

这个机制对网关侧的多租户路由、API-Key 认证是量身定做的:租户 ID 走 HTTP 头,网关和鉴权层不用解析 JSON-RPC 报文体。

四、显式句柄:官方给"有状态需求"的新答案

无状态化之后,"我的工具需要跨调用状态"怎么办(购物车、浏览器会话、多步流程)?官方的答案是显式句柄模式 :服务器生成 ID(basket_idbrowser_id),作为普通工具参数交给模型,后续调用由模型把句柄传回来。

官方公告里有一段值得完整转述的辩护:这个模式甚至比会话更强------模型可以跨工具组合句柄、推理句柄之间的关系、把句柄传给别的工具,而藏在传输元数据里的会话状态做不到这些。我自己的引申是:句柄既然只是普通数据,任何服务器实例都能处理携带同一句柄的请求,"状态只活在某一个实例内存里"的问题自然消失;再进一步,给句柄配上签名还能防伪造------这属于规范之外的工程实践,官方公告并未涉及。

对 Java 开发者,落地姿势很朴素:把原来依赖 Mcp-Session-Id 隐式关联的状态,改成工具的第一个显式参数:

java 复制代码
// 旧思路:状态挂在会话上(Spring AI 有状态模式)
// 新思路:状态挂在句柄上,任何实例都能处理
@McpTool(name = "cart_add", description = "向购物车添加商品。首次调用可不传 cart_id,返回值中会给出新句柄")
public CartAddResult addToCart(
        @McpToolParam(description = "购物车句柄,首次调用可省略", required = false) String cartId,
        @McpToolParam(description = "商品 SKU", required = true) String sku,
        @McpToolParam(description = "数量", required = true) int quantity) {
    Cart cart = (cartId == null) ? cartService.create() : cartService.load(cartId);
    cart.add(sku, quantity);
    return new CartAddResult(cart.id(), cart.itemCount());   // 把句柄还给模型
}

注意 description 里的引导语------模型如何正确使用句柄,靠的就是这几句话。

五、迁移建议:什么场景动,什么场景等

一个月的观察期加上上面的核查,给出我的决策建议:

你的场景 建议
新写的 MCP Server(Spring AI 2.0.x) 照常上:2025-11-25 规范的 Streamable HTTP 完全可用于生产;从第一天就用显式句柄模式管理状态,别依赖会话
存量 MCP Server,多副本部署压力 protocol: STATELESS 解决部署问题(注意坑 1:这是旧规范变体),同时把会话依赖重构成显式句柄------这两步做了,未来升级新规范时传输层代码几乎不用动
Java MCP 客户端(LangChain4j) 升 1.19.0;连接已知服务器时显式设 protocolVersion,避开 30 秒检测;检查错误码字面匹配(坑 5)与订阅代码(坑 4)
用了 2025-11-25 实验 Tasks API 的实现 必须迁移 ------新规范里 Tasks 已换成扩展(io.modelcontextprotocol/tasks)且生命周期重设计,这是官方点名的破坏性变更
等 Spring AI 支持新规范 关注 MCP Java SDK 的 release notes(支持 2026-07-28 的版本大概率是 2.1 或 3.0 级别的升级);注解层的 @McpTool 代码预期保持兼容,届时主要是传输与配置层的迁移

一句话总结给赶时间的人:协议已经无状态,服务端生态还在路上;今天写代码,把状态显式化、把错误码和订阅逻辑收敛好,就是在为新规范铺路。

小结

回头看这次升级的本质:MCP 把"连接"这个概念整个换掉了------从"先握手建立会话、再在会话里一来一回",变成"每个请求自带全部上下文、任何实例可接手"。这和 HTTP 之于 TCP 长连接的哲学如出一辙,也和 REST 之于有状态会话的历史进程完全同构。AI 工具调用的基础设施正在重走 Web 的老路,而 Java 生态这次的位置是:客户端(LangChain4j)站在第一梯队,服务端(MCP Java SDK / Spring AI)还需一个版本周期。

对 Java 开发者,我建议把这篇的三个要点带走:分清两代"无状态" (Spring AI 的 STATELESS ≠ 新规范)、显式句柄模式 (无状态时代管理状态的正确姿势)、LangChain4j 的检测超时与静默回退(新客户端最容易踩的性能坑)。

参考资料

相关推荐
不一样的少年_1 小时前
图解 AI Agent ②:模型到底是怎么读取文件的?
人工智能·agent·ai编程
threerocks1 小时前
《The AI-Native SDLC Playbook》万字拆解
算法·aigc·ai编程
Lambert2812 小时前
一个 @McpTool 注解,让 Claude 和 Cursor 直接调用你的 Spring 服务
ai编程
全栈弄潮儿3 小时前
用 AI 做代码优化:哪些建议值得采纳
aigc·openai·ai编程
会周易的程序员3 小时前
软件接入大模型实现 Agent —— 从原理到 C++ 落地完全指南
c++·人工智能·物联网·架构·agent·工业协议·mcp
薛定猫AI4 小时前
【技术干货】Claude Code多模型代理与反馈闭环:Python实现可验证的AI编程工作流
开发语言·python·ai编程
必须会一定会4 小时前
AI 编程隐私保护清单:API Key、代码上传、Agent 权限与 Git 历史排查
人工智能·git·ai编程
plainGeekDev4 小时前
外层六构件:把 Loop 放大成系统
ai编程·claude
火云牌神5 小时前
长连接与流式推送:规范 SSE / WebSocket 实现,替换无效轮询
websocket·网络协议·架构·ai编程·流式推送