本文事实核查基于 MCP 2026-07-28 官方 changelog、官方发布公告、MCP Java SDK Releases 与 LangChain4j 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 及之前)的工作方式:
- 客户端连上服务器,发送
initialize握手; - 服务器返回能力清单,并分配一个
Mcp-Session-Id; - 之后所有请求都带着这个 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/list、resources/list、prompts/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/progress、notifications/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)模式:
- 服务器处理请求时发现缺信息,返回
InputRequiredResult(resultType: "input_required"),inputRequests字段里列出需要客户端补充什么; - 客户端收集好答案(可能要问用户),带着
inputResponses重新发起原始请求; - 任何服务器实例都能接手这次重试------因为所需信息全在载荷里,不需要会话。
配合强制规则"服务器发起的请求只能发生在处理客户端请求期间"(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_id、browser_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 的检测超时与静默回退(新客户端最容易踩的性能坑)。