很多 Java 开发者第一次接触 MCP 时,关注的往往是如何使用 Spring AI 编写一个 @McpTool。
但到了 2026 年,真正容易让系统出问题的,已经不是“工具怎么注册”,而是:
客户端和服务端使用的,可能根本不是同一个时代的 MCP 协议。
2026 年 7 月 28 日发布的新版 MCP 规范,彻底移除了协议级会话和初始化握手,开始转向真正的无状态协议。
问题在于,截至 2026 年 8 月 15 日,Java MCP SDK 和 Spring AI 还没有完整跟上这次协议升级。
这意味着,一个在本地运行正常的 Spring AI MCP Server,接入新版托管客户端时,可能在第一次请求到达工具之前就直接失败。
本文将从协议变化、Java 生态现状、兼容性测试、状态管理、安全设计和迁移方案几个方面,完整分析这次 MCP 协议断层。
一、MCP 2026-07-28 到底改变了什么
早期 MCP 更接近一种“先建立连接,再进行交互”的协议。
客户端需要先调用 initialize,服务端根据初始化请求返回协议版本和能力。部分 Streamable HTTP 服务还会通过 Mcp-Session-Id 维护会话状态。
2026-07-28 版本改变了这套模型:
- 删除
initialize和notifications/initialized; - 删除协议级 Session 和
Mcp-Session-Id; - 每个请求都携带协议版本、客户端身份和能力;
- 新增强制实现的
server/discover; - 每个 JSON-RPC 请求都使用独立的 HTTP POST;
- 服务端需要跨请求保存状态时,必须显式返回状态句柄;
- 服务端到客户端的多轮交互改为 MRTR;
- 长连接通知改为
subscriptions/listen。
官方将 2026-07-28 及之后的协议称为现代协议,将 2025-11-25 及之前依赖初始化握手的协议称为旧版协议。(Model Context Protocol)
两种协议的差异可以概括为:
| 对比项 | 2025-11-25 及之前 | 2026-07-28 及之后 |
|---|---|---|
| 协议生命周期 | 先初始化,再调用工具 | 每个请求独立完成 |
| 版本传递 | 在初始化阶段协商 | 每个请求都携带版本 |
| 客户端能力 | 初始化时声明 | 每个请求通过 _meta 声明 |
| 协议会话 | 可以依赖 Session ID | 不再存在协议级会话 |
| 服务端发现 | 依赖初始化结果 | 使用 server/discover |
| 跨请求状态 | 可以绑定连接或会话 | 使用显式状态句柄 |
| HTTP 交互 | POST、GET、SSE 混合 | 每个请求独立 POST |
| 服务端反向请求 | 基于双向会话 | 使用 MRTR 嵌入结果 |
这并不是一次简单的字段调整,而是 MCP 生命周期模型的一次重新设计。
二、“无状态”其实有三种不同含义
MCP 升级后,最容易产生的误解是:
只要 Spring AI 配置了
protocol: STATELESS,就已经支持 MCP 2026-07-28。
这个结论并不成立。
在实际工程中,“无状态”至少包含三个不同层面的概念。
1. 部署层无状态
应用实例本身不保存本地会话,可以在 Kubernetes、Docker Swarm 或普通负载均衡集群中横向扩容。
例如,请求可以随机分发到任意实例。
2. 传输层无状态
Spring AI 的 Stateless MCP Server 不依赖某个固定 HTTP 连接,也不要求客户端一直连接同一个实例。
这主要解决的是服务部署与连接管理问题。
3. 协议层无状态
MCP 2026-07-28 不再存在初始化握手和协议级 Session。
每个请求必须自己携带:
- 协议版本;
- 客户端信息;
- 客户端能力;
- 请求方法相关信息。
Spring AI 文档目前已经提供 STATELESS 服务模式和默认 /mcp 端点,但 Spring AI 主分支仍然引用 MCP Java SDK 2.0.0。与此同时,Java SDK 官方问题记录明确说明,2.0.0 支持的是 2025-11-25 协议,并未实现 server/discover。(Home)
因此,工程上应当这样理解:
Spring AI 的
STATELESS代表当前实现采用无状态服务端模式,并不等于它已经完整实现 MCP 2026-07-28 现代协议。
三、Java MCP 生态目前处于什么状态
截至 2026 年 8 月 15 日,MCP Java SDK 最新正式标签仍然是 v2.0.0,发布时间为 2026 年 6 月 11 日。官方 Java SDK 的 3.x 规划将实现 2026-07-28 规范列为主要目标,但当前里程碑仍处于开发阶段。(GitHub)
目前可以得出以下结论:
| 组件 | 当前状态 |
|---|---|
| MCP 规范 | 已发布 2026-07-28 现代无状态规范 |
| Java MCP SDK 正式版 | 最新为 2.0.0 |
| Java SDK 2.0.0 协议基线 | 2025-11-25 |
server/discover |
Java SDK 2.0.0 尚未完整支持 |
| Spring AI MCP 依赖 | 当前仍引用 Java SDK 2.0.0 |
| Java 现代协议支持 | 正在 3.x 规划中推进 |
这并不代表 Spring AI 的 MCP 功能不能使用。
它意味着:
当前 Java 服务端和旧时代客户端可以正常协作,但不能默认认为它兼容所有已经升级到 2026-07-28 的客户端。
四、一个 server/discover 为什么会让整个请求失败
新版 MCP 规定,服务端必须实现 server/discover。
客户端可以在调用 tools/list 之前先发送:
{
"jsonrpc": "2.0",
"id": "discover-1",
"method": "server/discover",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {
"name": "enterprise-agent",
"version": "1.0.0"
},
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}
服务端应当返回自己支持的协议版本、能力和身份信息。(Model Context Protocol)
理想的兼容流程如下:
flowchart LR
A[客户端访问 MCP 服务] --> B[发送 server/discover]
B --> C{服务端响应}
C -->|200| D[读取 supportedVersions]
D --> E[选择共同协议版本]
C -->|404 与 -32601| F[确认服务端不支持该方法]
F --> G[双时代客户端回退旧版 initialize]
C -->|400 与 -32022| H[读取服务端支持版本]
H --> E
C -->|500| I[协议探测中断]
I --> J[连接器或整次模型请求失败]
官方规范要求:
- 协议版本不支持时,返回 HTTP 400 和
UnsupportedProtocolVersionError; - RPC 方法不支持时,返回 HTTP 404 和 JSON-RPC 错误码
-32601; - 不应该因为未知方法直接返回 HTTP 500。(Model Context Protocol)
但 Java SDK 2.0.0 的一个公开问题显示,新版客户端发送 server/discover 后,当前无状态服务端可能返回 HTTP 500,并提示缺少对应处理器。
更严重的是,某些托管客户端会将这个 500 视为 MCP 连接失败,从而终止整次模型响应,即使用户的问题根本不需要调用工具。(GitHub)
所以这不是一个普通的“工具调用失败”,而是:
MCP 协议探测阶段已经失败,模型甚至还没有机会决定是否调用工具。
五、新旧协议的兼容关系
官方将实现分为三类:
- Legacy:只支持初始化握手协议;
- Modern:只支持每请求元数据协议;
- Dual-era:同时支持新旧两套协议。
常见组合如下:
| 客户端 | 服务端 | 结果 |
|---|---|---|
| Legacy 客户端 | Legacy 服务端 | 协议时代一致,通常可正常使用 |
| Modern 客户端 | Modern 服务端 | 正常使用新版协议 |
| Dual-era 客户端 | Legacy 服务端 | 服务端正确返回不支持后,可以回退 |
| Modern 客户端 | Legacy 服务端 | 通常无法直接兼容 |
| Legacy 客户端 | Modern-only 服务端 | 无法完成旧版初始化 |
| Modern 客户端 | 当前 Java SDK 2.0.0 服务端 | 存在 server/discover 兼容风险 |
这也是为什么单纯查看 /mcp 接口是否能够访问,并不能证明服务端兼容某个 MCP 客户端。(Model Context Protocol)
六、当前 Spring AI 项目的可用基线
在 Java SDK 正式完成新版协议实现之前,Spring AI 2.0 仍然可以作为旧版协议时代的无状态 MCP Server 使用。
Maven 依赖可以这样配置:
<properties>
<java.version>21</java.version>
<spring-ai.version>2.0.0</spring-ai.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
</dependencies>
服务端配置如下:
spring:
ai:
mcp:
server:
name: enterprise-tool-server
version: 1.0.0
type: SYNC
protocol: STATELESS
request-timeout: 20s
stateless:
mcp-endpoint: /mcp
Spring AI 文档显示,Stateless MCP Server 支持工具、资源和提示词能力,默认端点为 /mcp。不过 Stateless 模式不适合依赖连接上下文的设计,Tool Context 在该模式下也不适用。(Home)
需要强调的是:
这是一套当前可用的 Java 无状态部署基线,不应该对外宣称已经完整支持 2026-07-28 协议。
七、在 CI 中增加 MCP 协议兼容性测试
MCP 服务上线前,不能只测试 tools/list 和 tools/call。
至少还应该测试:
- 新版
server/discover请求是否导致 500; - 不支持的方法是否返回正确的协议错误;
- 请求头和
_meta中的协议版本是否一致; - 服务端到底声明支持哪些协议版本。
下面是一段基于 Java 21 HttpClient 和 JUnit 5 的协议探测测试:
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import org.junit.jupiter.api.Test;
class McpProtocolCompatibilityTest {
private static final URI MCP_ENDPOINT = URI.create(
System.getProperty(
"mcp.endpoint",
"http://localhost:8080/mcp"
)
);
private final HttpClient httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(3))
.build();
@Test
void discoverShouldReturnControlledProtocolResponse() throws Exception {
String requestBody = """
{
"jsonrpc": "2.0",
"id": "discover-1",
"method": "server/discover",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {
"name": "mcp-contract-test",
"version": "1.0.0"
},
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}
""";
HttpRequest request = HttpRequest.newBuilder()
.uri(MCP_ENDPOINT)
.timeout(Duration.ofSeconds(10))
.header("Content-Type", "application/json")
.header("Accept", "application/json, text/event-stream")
.header("MCP-Protocol-Version", "2026-07-28")
.header("Mcp-Method", "server/discover")
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
.build();
HttpResponse<String> response = httpClient.send(
request,
HttpResponse.BodyHandlers.ofString()
);
int status = response.statusCode();
String body = response.body();
switch (status) {
case 200 -> assertTrue(
body.contains("\"supportedVersions\""),
"200 响应必须包含 supportedVersions"
);
case 400 -> assertTrue(
body.contains("-32022"),
"400 响应应当包含 UnsupportedProtocolVersionError"
);
case 404 -> assertTrue(
body.contains("-32601"),
"404 响应应当包含 Method not found"
);
default -> fail(
"MCP 协议探测返回了不可控响应,status="
+ status
+ ", body="
+ body
);
}
}
}
对于当前存在兼容问题的 Java MCP Server,这个测试可能直接失败。
这正是测试存在的意义:在生产客户端替你发现问题之前,先在 CI 阶段阻断不兼容版本。
八、没有 Session,不代表业务不能保存状态
新版 MCP 删除的是协议级 Session,并不是禁止业务保存状态。
例如,一个报表生成流程可能需要:
- 创建报表任务;
- 返回任务 ID;
- 后续查询执行状态;
- 下载最终结果。
以前开发者可能把任务状态绑定在当前连接或 Session 中。
新版协议推荐服务端生成一个显式状态句柄,并要求客户端在后续工具调用时继续传递。(Model Context Protocol)
flowchart LR
A[调用 create_export_job] --> B[服务端创建任务]
B --> C[(Redis 或 PostgreSQL)]
B --> D[返回 jobId 与 expiresAt]
D --> E[模型保留 jobId]
E --> F[调用 get_export_job]
F --> G[携带 jobId]
G --> C
C --> H[校验用户 权限 有效期]
H --> I[返回任务状态]
Java 工具可以这样设计:
import java.time.Duration;
import java.time.Instant;
import java.util.Optional;
import java.util.UUID;
import org.springframework.ai.mcp.annotation.McpTool;
import org.springframework.ai.mcp.annotation.McpToolParam;
import org.springframework.stereotype.Component;
record ExportHandle(
String jobId,
Instant expiresAt
) {
}
record ExportStatus(
String jobId,
String status,
Instant expiresAt
) {
}
record ExportJob(
String jobId,
String ownerId,
String reportType,
String status,
Instant expiresAt
) {
}
interface ExportJobRepository {
void save(ExportJob job);
Optional<ExportJob> findById(String jobId);
}
interface CurrentCaller {
String requireUserId();
}
@Component
public class ExportTools {
private final ExportJobRepository repository;
private final CurrentCaller currentCaller;
public ExportTools(
ExportJobRepository repository,
CurrentCaller currentCaller
) {
this.repository = repository;
this.currentCaller = currentCaller;
}
@McpTool(
name = "create_export_job",
description = "创建异步报表任务,任务句柄有效期为两小时"
)
public ExportHandle createExportJob(
@McpToolParam(
description = "报表类型",
required = true
)
String reportType
) {
String jobId = "job_" + UUID.randomUUID();
String ownerId = currentCaller.requireUserId();
Instant expiresAt = Instant.now().plus(Duration.ofHours(2));
ExportJob job = new ExportJob(
jobId,
ownerId,
reportType,
"PENDING",
expiresAt
);
repository.save(job);
return new ExportHandle(jobId, expiresAt);
}
@McpTool(
name = "get_export_job",
description = "根据任务句柄查询报表生成状态"
)
public ExportStatus getExportJob(
@McpToolParam(
description = "create_export_job 返回的任务句柄",
required = true
)
String jobId
) {
String ownerId = currentCaller.requireUserId();
ExportJob job = repository.findById(jobId)
.filter(item -> item.ownerId().equals(ownerId))
.orElseThrow(() ->
new IllegalArgumentException(
"任务不存在或当前用户无权访问"
)
);
if (job.expiresAt().isBefore(Instant.now())) {
throw new IllegalStateException(
"任务句柄已经过期,请重新创建任务"
);
}
return new ExportStatus(
job.jobId(),
job.status(),
job.expiresAt()
);
}
}
实际生产环境中,ExportJobRepository 应由 Redis、PostgreSQL 或其他共享存储实现,而不是使用本机 ConcurrentHashMap。
因为只要服务部署了多个实例,本地内存状态就会产生以下问题:
- 创建任务的实例和查询任务的实例可能不同;
- 应用重启后任务状态丢失;
- 扩容、缩容时无法迁移状态;
- 无法统一设置过期时间;
- 无法进行跨实例幂等控制。
状态句柄还必须满足几个条件:
| 要求 | 说明 |
|---|---|
| 不透明 | 不在 ID 中暴露用户、数据库主键等内部结构 |
| 不可猜测 | 使用 UUID 或足够随机的标识 |
| 有有效期 | 明确 TTL,并在过期后返回可恢复错误 |
| 每次鉴权 | 不能拿到句柄就拥有数据访问权限 |
| 支持幂等 | 创建、支付、提交等操作需要业务幂等键 |
| 共享存储 | 不能绑定某一个应用实例 |
官方规范特别强调:对于认证服务,Handle 只是一个名称,不是权限凭证,服务端必须在每次调用时重新校验调用者是否有权访问该 Handle。(Model Context Protocol)
九、Java 项目应该怎样迁移
面对当前的协议断层,不建议直接将所有 MCP Server 一次性升级。
更稳妥的方案是分阶段迁移。
第一阶段:建立协议资产清单
先记录当前系统中:
- MCP 客户端名称和版本;
- MCP 服务端使用的 SDK;
- 使用的协议版本;
- Transport 类型;
- 是否依赖
initialize; - 是否依赖
Mcp-Session-Id; - 是否发送
server/discover; - 是否支持协议降级。
不要使用“客户端看起来能连上”作为兼容性依据。
第二阶段:固定当前生产基线
如果现有客户端和 Java SDK 2.0.0 工作正常,可以暂时固定在 2025-11-25 协议时代。
不要在没有完成验证的情况下,对外声明支持 2026-07-28。
协议版本应该像数据库版本一样被明确记录,而不是交给客户端和服务端自动猜测。
第三阶段:加入兼容性网关
企业内部拥有多个 MCP Server 时,可以增加一层协议兼容网关:
flowchart LR
A[AI Host 或 Agent 平台] --> B[MCP 兼容网关]
B --> C{协议版本}
C -->|2025-11-25| D[Spring AI 2.0 服务集群]
C -->|2026-07-28| E[新版 MCP 服务集群]
D --> F[统一工具服务层]
E --> F
F --> G[(Redis 或 PostgreSQL)]
F --> H[企业 API 与业务系统]
兼容网关可以负责:
- 记录客户端发送的协议版本;
- 校验 Header 和
_meta是否一致; - 将未知方法转换为正确的 JSON-RPC 错误;
- 根据版本路由到不同服务集群;
- 统计新旧协议使用比例;
- 对异常客户端进行限流和隔离。
但网关不应该伪造能力。
例如,后端并不支持 2026-07-28 时,网关不能返回一个假的 server/discover,声称服务端已经支持新版协议。
这样可能绕过连接检查,却会在后续 tools/list、MRTR 或订阅流程中产生更隐蔽的问题。
第四阶段:双栈灰度
当 Java SDK 正式提供完整的 2026-07-28 支持后,可以同时部署两组实例:
- Legacy 实例继续服务旧客户端;
- Modern 实例服务新版客户端;
- 网关根据协议版本进行路由;
- 逐步观察错误率和回退率;
- 最后再下线旧版协议。
不要在同一时间同时升级客户端、SDK、网关和业务工具。
否则出现问题时,很难判断是协议、传输、鉴权还是工具实现造成的。
十、MCP 无状态化之后,安全边界更重要
无状态协议意味着每个请求都必须独立完成身份识别和权限校验。
不能因为前一个请求已经认证,就默认后续请求仍然可信。
新版 MCP 的安全设计要求包括:
- Streamable HTTP 请求需要校验
Origin; - 本地服务应尽量只绑定
127.0.0.1; - 远程服务应使用可靠的身份认证;
- Token 必须校验 Audience;
- 禁止将上游 Token 直接透传给下游服务;
- 应优先使用短生命周期访问令牌;
- 工具参数必须进行完整校验;
- 工具调用应进行限流;
- 工具输出需要进行脱敏和清理;
- 敏感操作应要求用户确认;
- 工具调用需要留下审计记录。(Model Context Protocol)
对于 Java 服务来说,至少应该记录以下审计字段:
{
"requestId": "req_9cb71f",
"protocolVersion": "2026-07-28",
"clientName": "enterprise-agent",
"clientVersion": "1.0.0",
"method": "tools/call",
"toolName": "create_export_job",
"userId": "user_1024",
"tenantId": "tenant_001",
"status": "SUCCESS",
"durationMs": 126,
"timestamp": "2026-08-15T10:30:00Z"
}
需要注意,日志里不要直接记录:
- Access Token;
- Cookie;
- 身份证号;
- 手机号;
- 工具调用中的完整敏感参数;
- 数据库查询结果全文;
- 状态句柄对应的内部数据。
十一、建议增加的监控指标
除了普通接口监控,还应增加 MCP 协议维度的指标。
| 指标 | 作用 |
|---|---|
mcp_requests_total |
按协议版本、方法、状态码统计请求 |
mcp_discover_total |
统计 server/discover 调用结果 |
mcp_protocol_error_total |
统计协议不匹配和 Header 不一致 |
mcp_fallback_total |
统计客户端回退旧协议次数 |
mcp_tool_duration |
统计工具调用耗时 |
mcp_tool_error_total |
区分协议错误和业务执行错误 |
mcp_handle_lookup_total |
统计 Handle 查询、过期和不存在 |
mcp_auth_denied_total |
统计身份和权限拒绝 |
mcp_active_subscriptions |
统计长连接订阅数量 |
尤其需要关注以下告警:
server/discover出现 HTTP 500;- 某个客户端突然开始发送新协议版本;
- 协议降级率突然升高;
- 未知 RPC 方法数量异常增长;
- Handle 越权访问数量增加;
- 单个工具调用量突然暴涨;
- SSE 请求长期不释放。
十二、结语
MCP 2026-07-28 的核心变化,不是增加了几个新方法,而是将协议从“连接和会话驱动”改造成了“每请求自描述”。
这使 MCP Server 更容易横向扩容,更适合云原生部署,也减少了客户端连接状态与服务实例之间的隐式绑定。
但协议升级也带来了现实问题:
规范已经进入现代无状态时代,Java SDK 和 Spring AI 仍处于新旧协议交界处。
因此,当前 Java 团队最重要的工作不是盲目追新,而是先建立清晰的协议边界:
- 明确客户端和服务端支持的协议版本;
- 不把
STATELESS配置误认为完整支持新版协议; - 在 CI 中增加
server/discover兼容性测试; - 未知方法必须返回可控错误,不能直接 500;
- 将跨请求状态迁移为显式 Handle;
- 在每次调用中重新完成身份和权限校验;
- 通过双栈和灰度方式完成迁移。
MCP 最终会成为 AI 应用连接工具、数据和企业系统的重要协议。
而对于 Java 开发者来说,真正决定系统能否稳定运行的,往往不是能不能写出一个工具,而是能不能正确处理协议版本、状态边界和兼容性。

评论区 1