MCP 2026-07-28 无状态协议大改版:Spring AI 2.1 多轮往返请求与网关路由落地

今天 CSDN 头条是《Agent 能搭出来,为什么还做不成事?六位专家拆解智能体生产化的六道关》。这个标题很扎心:搭 Agent 早就不难了,选个模型、接上知识库和工具,一下午就能跑起来。真正卡住企业的是------能不能稳定、安全、持续地把事做成。

而就在 2026 年 7 月 28 日,MCP 发布了官方口中「自发布以来最大的一次修订」。这次修订没有加什么炫酷的新能力,它干的事情恰恰是六道关里最不性感、但最要命的那一半:把 MCP 从一个有状态的双向协议,改成一个无状态的请求-响应协议。

对 Java 后端来说,这次改版简直像一次「回家」:状态不再靠长连接持有,而是靠请求自带、显式句柄、幂等重放。这不就是我们做了二十年的无状态服务、分布式会话、幂等接口、网关路由吗?

Spring AI 2.1.0-RC2(2026-11-05 发布)是第一个把 MODERN 协议搬进 Java 生态的版本。这篇文章只讲一件事:MCP 2026-07-28 到底改了什么,以及 Java 团队怎么在 Spring AI 里把它落地成生产可用。


一、先看清改版的动机:不是加功能,是重新铺管道

老版 MCP 有三个隐含假设,全都建立在「连接会一直在」上:

  1. initialize 握手协商一次,之后所有请求默认沿用这个协商结果;
  2. Mcp-Session-Id 让服务端能记住「这个客户端是谁、上次聊到哪」;
  3. 服务端可以反向发请求给客户端(sampling/createMessage、elicitation/create、roots/list),因为流还挂着。

这三个假设在 Demo 阶段无比方便,在 K8s 里就是灾难。你的 MCP Server 一旦横向扩容到 3 个 Pod、前面挂个普通负载均衡,会话状态就分裂了;用户在你等审批的 30 秒里,重试请求落到了另一个 Pod,之前收集的上下文全丢。

2026-07-28 的解法非常工程化:把所有隐含状态显式化。

  • 想要协商结果?每次请求自己带 _meta;
  • 想要跨调用状态?服务端签发一个显式句柄,让模型当成普通 tool 参数带回来;
  • 想要中途问用户?别反向发了,返回一个「我还缺信息」的结果,让客户端带着答案重放原请求。

这套逻辑,和「把 session 从 Tomcat 内存搬到 Redis + 令牌里带租户上下文」是同一个思路。所以我说 Java 团队看这版协议应该感到亲切。


二、协议变更全景:9 条大改,12 条小改

2.1 九条破坏性变更

# 变更 SEP 影响面
1 移除协议级 session 与 Mcp-Session-Id;tools/list、resources/list、prompts/list 不再随连接变化 SEP-2567 状态存储全废
2 移除 initialize / notifications/initialized;版本与能力改由每次请求的 _meta 携带 SEP-2575 客户端全改
3 新增 server/discover,服务端 MUST 实现 SEP-2575 必做新方法
4 subscriptions/listen 取代 HTTP GET 流与 resources/subscribe/unsubscribe SEP-2575 通知通道全换
5 移除 ping、logging/setLevel、notifications/roots/list_changed SEP-2575 健康检查失效
6 Tasks 从核心移出成扩展 io.modelcontextprotocol/tasks;tasks/result 换成 tasks/get 轮询 + tasks/update,删除 tasks/list SEP-2663 长任务全改
7 MRTR :服务端不再反向发请求,改为返回 InputRequiredResult,客户端重试带 inputResponses SEP-2322 交互式中断
8 所有 result 必须带 resultType:complete 或 input_required SEP-2322 序列化契约
9 移除 SSE 断点续传(Last-Event-ID 与事件 ID);断流即丢请求,客户端须用新 ID 重发 SEP-2575 可靠性语义

2.2 五条最容易被忽略、但最先炸的小改

  1. _meta 成为新的握手信封 :io.modelcontextprotocol/protocolVersion、io.modelcontextprotocol/clientCapabilities 必带;clientInfo SHOULD 带;服务端在 result 的 _meta 里回 serverInfo。
  2. Streamable HTTP 强制路由头 :每个 POST 必须带 Mcp-Method,tools/call / resources/read / prompts/get 还必须带 Mcp-Name。工具参数可以用 x-mcp-header 提升成 Mcp-Param-{Name} 头。
  3. ttlMs + cacheScope :tools/list、prompts/list、resources/list、resources/read、resources/templates/list 的返回都要带缓存提示,cacheScope 取 public 或 private。
  4. 错误码重新分区 :-32000~-32019 留给实现,-32020~-32099 归规范。于是 HeaderMismatch 从 -32001 变 -32020 ,MissingRequiredClientCapability 从 -32003 变 -32021 ,UnsupportedProtocolVersion 从 -32004 变 -32022 。另外 resource not found 从 -32002 改成 -32602。
  5. tools/list 要求确定性顺序:这条看起来最无害,其实是为了 LLM prompt cache 命中率------列表顺序一抖,整段前缀缓存全失效。

三、源码级:一次 tools/call 的完整旅程

3.1 一个能跑通的 MODERN 请求长什么样

先别看 SDK,手写一次 curl,把协议看清:

bash 复制代码
curl -s -X POST http://127.0.0.1:8787/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -H 'MCP-Protocol-Version: 2026-07-28' \
  -H 'Mcp-Method: server/discover' \
  -d '{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{
        "io.modelcontextprotocol/protocolVersion":"2026-07-28",
        "io.modelcontextprotocol/clientCapabilities":{}}}}'

注意这里有两份版本号 :header 里一份 MCP-Protocol-Version: 2026-07-28,body 的 _meta 里又一份 io.modelcontextprotocol/protocolVersion。很多人第一反应是「重复了,删一个」------删哪个都会挂:

  • 只留 _meta、不带 header → HTTP 400 + -32020(规范明确要求 2026-07-28 必须带 header);
  • 只带 header、_meta 缺失或格式错 → -32602(Missing envelope)。

3.2 最阴的一个坑:server/discover 报 -32601

调试时你会遇到这种诡异现象:

json 复制代码
{"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"Method not found"}}

明明规范说服务端 MUST 实现 server/discover,为什么没有?因为没带 _meta 的请求会被当成 2025 时代的开场 ,走 legacy 路径,而 legacy 路径里根本没有这个方法。加上 _meta 之后,同一个服务立刻正常返回。

这个坑的杀伤力在于:它让你误判「SDK 没实现新协议」,然后你去降级、去改配置、去翻 issue,浪费半天。真相是版本探测本身就是协议的一部分:客户端就是靠「带版本 → 成功 / 不带版本 → legacy」这套行为来感知服务端支持到哪一代。

3.3 头与 body 必须一致,这是安全属性不是格式要求

Mcp-Method / Mcp-Name 的存在不是为了好看,而是为了让网关能在不解析 body 的前提下做路由、限流和鉴权。这就带来一个必须守住的不变量:

处理 body 的服务端必须校验 header 与 body 一致,不一致直接 400 + -32020。

为什么不能睁一只眼闭一只眼?假设你的网关按 Mcp-Name: read_file 做了权限校验然后放行,body 里写的却是 delete_file------如果服务端不校验,你就完成了一次教科书级的越权。网关信任 header 的前提,是这条链路上有人把 header 和 body 钉死。

推而广之,任何中间件(API 网关、Sidecar、WAF)如果改写了 body,就必须同步改写 header;不能只改一个。而网关自己也必须先校验再信任------拿着未经校验的 header 去路由,等于把安全边界建在沙子上。

一个黑盒测试就能验证你的服务端有没有做这件事:header 写 Mcp-Name: read_file,body 里写一个根本不存在的工具名。正确行为是什么都不执行 ,返回 400 + -32020。


四、MRTR:把「服务端反向请求」翻译成「幂等重放」

这是整版协议里工程含量最高的一块,也是最能体现 Java 后端经验的地方。

4.1 老做法为什么必须死

老版里,一个工具执行到一半想问用户「你确定要删这个任务吗?」,服务端直接反向发一个 elicitation/create 给客户端,然后在当前这条流上等着。这在无状态世界里根本不成立:没有任何一条流是「你的」流。

4.2 新契约:input_required + requestState

新做法是一次协议层的中断-重放:

复制代码
第 1 轮  客户端 → tools/call
         服务端 → { resultType: "input_required",
                    inputRequests: { confirm: { method: "elicitation/create", params: {...} } },
                    requestState: "<不透明句柄>" }

第 2 轮  客户端 → tools/call(同一个请求,新 id)
                   arguments 不变
                   inputResponses: { confirm: { content: {...} } }
                   requestState: "<原样回传>"
         服务端 → { resultType: "complete", content: [...] }

关键点在于 确定性重放(deterministic replay) :服务端不保留任何跨轮记忆,第 2 轮请求进来时,handler 从第一行重新跑一遍 ,靠 ctx.inputResponse("confirm") 读到答案、靠回传的 requestState 恢复中间态。

这正是 Java 里「无状态服务 + 客户端回传上下文」的标准模式。Spring AI 官方给的例子非常直白:

java 复制代码
@McpTool(name = "delete_task",
         description = "Deletes a task after asking the user to confirm",
         requiredCapabilities = McpClientCapability.ELICITATION_FORM)
public CallToolOutcome deleteTask(McpSyncNotifier notifier,
                                  McpModernSyncRequestContext ctx,
                                  @McpToolParam(description = "Task id") String id) {
    // 第一轮:客户端还没答,这里是 null
    Map answer = ctx.inputResponse("confirm", Map.class);
    if (answer == null) {
        notifier.progress(10, 100.0, "asking for confirmation");
        Map schema = Map.of(
            "type", "object",
            "properties", Map.of("confirmed", Map.of("type", "boolean")));
        return InputRequiredResult.builder()
            .elicit("confirm", ElicitFormRequest.builder("Delete task " + id + "?", schema).build())
            .requestState(id)
            .build();
    }
    // 第二轮:从头上重跑到这里,answer 有了
    return CallToolOutcome.of("deleted " + id);
}

注意 requiredCapabilities = McpClientCapability.ELICITATION_FORM:服务端在方法执行之前 就检查客户端是否声明了表单交互能力,没声明直接返回 -32021,而不是跑一半才发现没法问。

4.3 requestState 是客户端可控输入,必须密封

这是最容易出事的一点。requestState 在客户端手里过了一趟,回来的时候已经不可信了。

如果你把 "taskId=42&approved=true" 明文塞进去,攻击者改成 approved=false 就能绕过审批。所以规范推荐的姿势是:AES-256-GCM 密封 + TTL + 绑定来源 method、target、arguments 。参考实现(Ruby SDK)直接给了个 RequestStateSecurity.new(key:),用 32 字节密钥自动封解,TTL 和绑定关系全透明。多进程部署时必须共享同一把密钥,否则一半请求解封失败。

Java 侧自己封一个不难,Spring AI 里可以包成一个组件:

java 复制代码
@Component
public class RequestStateSealer {

    private static final String TTL = "ts";
    private static final String BIND = "bind";

    private final SecretKey key;
    private final Duration ttl;

    public RequestStateSealer(@Value("${mcp.request-state.key}") String base64Key,
                              @Value("${mcp.request-state.ttl:PT10M}") Duration ttl) {
        this.key = new SecretKeySpec(Base64.getDecoder().decode(base64Key), "AES");
        this.ttl = ttl;
    }

    /** 密封:把业务状态 + 时间戳 + 绑定指纹一起塞进 GCM */
    public String seal(Object payload, String method, String target, Map<String, Object> args) {
        try {
            byte[] iv = new byte[12];
            SecureRandom.getInstanceStrong().nextBytes(iv);
            byte[] plain = JsonMapper.builder().build().writeValueAsBytes(
                Map.of("p", payload,
                       TTL, Instant.now().getEpochSecond(),
                       BIND, fingerprint(method, target, args)));
            Cipher c = Cipher.getInstance("AES/GCM/NoPadding");
            c.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, iv));
            byte[] ct = c.doFinal(plain);
            byte[] out = new byte[iv.length + ct.length];
            System.arraycopy(iv, 0, out, 0, iv.length);
            System.arraycopy(ct, 0, out, iv.length, ct.length);
            return Base64.getUrlEncoder().withoutPadding().encodeToString(out);
        } catch (Exception e) {
            throw new IllegalStateException("seal requestState failed", e);
        }
    }

    /** 解封:TTL 过期、绑定指纹不符,一律视为无效(fail-fast) */
    public <T> T unseal(String token, Class<T> type, String method, String target,
                        Map<String, Object> args) {
        try {
            byte[] all = Base64.getUrlDecoder().decode(token);
            Cipher c = Cipher.getInstance("AES/GCM/NoPadding");
            c.init(Cipher.DECRYPT_MODE, key,
                   new GCMParameterSpec(128, all, 0, 12));
            JsonNode node = JsonMapper.builder().build()
                .readTree(c.doFinal(all, 12, all.length - 12));

            long issuedAt = node.get(TTL).asLong();
            if (Instant.now().getEpochSecond() - issuedAt > ttl.getSeconds()) {
                throw new RequestStateExpiredException("requestState expired");
            }
            if (!fingerprint(method, target, args).equals(node.get(BIND).asText())) {
                throw new RequestStateTamperedException("requestState bound to another request");
            }
            return JsonMapper.builder().build()
                .treeToValue(node.get("p"), type);
        } catch (RequestStateExpiredException | RequestStateTamperedException e) {
            throw e;
        } catch (Exception e) {
            throw new RequestStateTamperedException("requestState invalid", e);
        }
    }

    private String fingerprint(String method, String target, Map<String, Object> args) {
        String raw = method + '|' + target + '|' + new TreeMap<>(args);
        try {
            byte[] d = MessageDigest.getInstance("SHA-256")
                .digest(raw.getBytes(StandardCharsets.UTF_8));
            return HexFormat.of().formatHex(d);
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException(e);
        }
    }
}

fingerprint 里用 TreeMap 是为了参数顺序不影响哈希;绑定 method|target|args 是为了防止「审批 A 操作的 token 被拿去执行 B 操作」------这跟签名 URL 绑定资源是同一个套路。

4.4 没有 exactly-once,副作用必须自己协调

规范说得很清楚:MRTR 的重试可能落到另一个服务实例 ,而且 MCP 不提供 exactly-once 保证。

所以第一轮里「收集输入之前」做过的任何副作用,都必须在第二轮里有对账逻辑。落地到 Java,就是老几样:

  • 执行类工具的副作用走 Outbox 表 + 状态机 ,用 requestId 做幂等键;
  • 中断点之前只做只读准备(查库存、算价格、生成预览),真正写库放在答案回来之后;
  • 状态机用条件更新(UPDATE ... WHERE status = 'PENDING')抢占,避免两个实例都执行。

4.5 legacy shim:双时代兼容不是免费的午餐

好消息是 SDK 给了过渡方案:用 2026 风格写出来的 handler,遇到 2025 时代的客户端时,服务端会替客户端把 inputRequests 翻译成真实的 elicitation/create 等反向请求 ,收集完答案再重跑 handler------同一套确定性重放契约。这个 shim 默认开启,上限 8 轮 ,可以用 input_required_legacy_shim: false 关掉、恢复对 legacy 请求的严格拒绝。

坏消息是:八轮上限、双路径分支、两套测试矩阵,全都是技术债。给 legacy 路径定一个下线日期,不然它会跟着你到 2028。


五、Spring AI 2.1.0-RC2 落地:一行配置切到 MODERN

Spring AI 2.1.0-RC2 的 MCP 服务端现在能说 2026-07-28 这一代,与原有 STDIO、SSE、STREAMABLE、STATELESS 并列:

yaml 复制代码
spring:
  ai:
    mcp:
      server:
        protocol: MODERN          # 关键开关
        name: approval-mcp
        version: 1.0.0
        modern:
          # 见 spring.ai.mcp.server.modern.* 命名空间
          legacy-compat: true     # 过渡期保留双时代
          legacy-removal-date: 2027-07-31

底层是 MCP Java SDK 2.2 的 modern API(当前 2.2.0-M2)。它带来的能力可以归成四块:

① 传输层收敛 :WebMVC 和 WebFlux 两种容器内传输,都通过单一 POST 端点提供该代协议,配合 SSE 做流式;同时支持 STDIO。

② 内存仓储可运行时变更:tools / prompts / resources / completions 的内存仓储支持运行时增删、变更广播和分页。这一条在「工具集要热更新」的场景里是关键------工具不上线重启就能加。

③ 注解回调现代化 :@McpTool、@McpResource、@McpPrompt、@McpComplete 四个注解都有 modern 回调路径。方法可以额外声明两个参数:

  • McpModernSyncRequestContext:读请求数据 + 读 MRTR 的 inputResponse(s) + requestState;
  • McpSyncNotifier:上报进度。

④ 声明式能力门槛与元数据:

  • requiredCapabilities(三个注解上都有):方法依赖的客户端能力,方法执行前就报错;
  • @McpIcon、@McpArg(title)、@McpResource(size, ttlMs, cacheScope)、@McpToolParam(header = true)。

@McpToolParam(header = true) 直接对应协议里的 x-mcp-header:参数会被镜像成 Mcp-Param-{Name} 请求头。最经典的用法是把 region 参数提升成路由头,网关据此把请求打到对应地域的集群,全程不用解析 body。

5.1 实战:企业变更审批 MCP Server

我们做一个真实场景:运维通过 Agent 发起数据库变更,高风险操作需要人工审批。

java 复制代码
@Configuration
@RequiredArgsConstructor
public class ApprovalMcpServerConfig {

    private final ChangeRequestService changeService;
    private final RequestStateSealer sealer;

    @McpTool(
        name = "create_change_request",
        description = "创建数据库变更申请。高危操作(DROP/ALTER)需要人工审批。",
        requiredCapabilities = McpClientCapability.ELICITATION_FORM)
    public CallToolOutcome createChange(
            McpSyncNotifier notifier,
            McpModernSyncRequestContext ctx,
            @McpToolParam(description = "目标库,如 order_db") String database,
            @McpToolParam(description = "变更 SQL") String sql,
            @McpToolParam(description = "执行地域", header = true) String region) {   // 提升为 Mcp-Param-Region

        notifier.progress(5, 100.0, "risk assessment");

        RiskLevel risk = RiskAssessor.assess(sql);          // 只读判断,无副作用
        if (risk == RiskLevel.HIGH) {
            // 第二轮从头上重跑时,这里能读到审批结果
            ApprovalAnswer approved = ctx.inputResponse("approve", ApprovalAnswer.class);
            if (approved == null) {
                notifier.progress(30, 100.0, "high risk, awaiting approval");
                String state = sealer.seal(
                        new PendingState(database, region, risk),   // 只放必要信息
                        "tools/call", "create_change_request",
                        Map.of("database", database, "sql", sql, "region", region));
                return InputRequiredResult.builder()
                    .elicit("approve", ElicitFormRequest.builder(
                            "该变更被判为高风险(" + risk + "),是否批准执行?",
                            ApprovalSchema.FORM)
                        .build())
                    .requestState(state)
                    .build();
            }
            if (!approved.confirmed()) {
                return CallToolOutcome.of("已拒绝,变更未提交");
            }
        }

        // 只有在审批通过(或低风险)之后,才产生副作用
        String requestId = changeService.submit(
                new ChangeRequest(database, sql, region, risk,
                                  ctx.requestId(), ctx.clientInfo().name()));
        notifier.progress(100, 100.0, "submitted");
        return CallToolOutcome.of(Map.of(
                "requestId", requestId,
                "risk", risk.name(),
                "status", "PENDING_REVIEW"));
    }

    @McpTool(
        name = "list_change_requests",
        description = "按状态查询变更单列表",
        requiredCapabilities = McpClientCapability.ELICITATION_FORM)
    public CallToolOutcome listChanges(
            McpModernSyncRequestContext ctx,
            @McpToolParam(description = "状态:PENDING/APPROVED/EXECUTED") String status,
            @McpToolParam(description = "分页游标") String cursor) {
        var page = changeService.page(status, cursor, 20);
        return CallToolOutcome.of(page);
    }
}

两个细节值得单独说:

第一,评审在中断点之前。 RiskAssessor.assess(sql) 是纯函数式只读判断,放在 input_required 之前安全;changeService.submit(...) 是写操作,必须放在答案回来之后。这样即使第二轮落到另一个实例、重跑一遍 handler,也不会重复写库。这就是「确定性重放友好」的代码结构。

第二,ctx.requestId() 做幂等键。 MRTR 的重试在协议层是「同一个请求的新一轮」,但网络层可能是全新的 HTTP 请求。用服务端生成的 requestId 而不是客户端 ID,能保证同一个逻辑请求的多次重放收敛到同一条变更单。

5.2 让 tools/list 支持缓存

协议要求 tools/list 返回确定性顺序 + ttlMs/cacheScope。Spring AI 里用注解直接声明:

java 复制代码
@McpResource(
    uri = "config://approval/rules",
    name = "approval-rules",
    description = "审批规则配置(按租户隔离)",
    size = 12_400,
    ttlMs = 60_000,          // 客户端可缓存 1 分钟
    cacheScope = "private")  // 租户数据,禁止共享中间件缓存
public String rules(@McpModernSyncRequestContext ctx) {
    // 按请求自带的身份解析租户,而不是靠 session
    return ruleRepo.load(ctx.clientInfo().name(), ctx.tenantId()).toJson();
}

cacheScope = "private" 这里绝不能图省事写 public------一旦中间层把它当公共内容缓存,A 租户就会读到 B 租户的审批规则。缓存提示写得越激进,泄漏面越大。


六、生产踩坑清单(10 条,按被坑概率排序)

1. 版本只在 _meta 里写、没带 MCP-Protocol-Version 头。

2026-07-28 明确要求 header,缺了就是 400 + -32020。这是升级后第一个撞墙的点。

2. server/discover 报 -32601 就以为服务端没实现。

其实是不带 _meta 的请求走了 legacy 路径。加上 _meta.io.modelcontextprotocol/protocolVersion 再试,同一个服务端立刻返回。别急着降级。

3. Mcp-Name 与 body 里的工具名不一致。

服务端不做校验 → 400 + -32020 只是表面;真正的风险是网关按 header 鉴权、服务端按 body 执行,直接越权。上线前做一次黑盒测试:header 写 A、body 写未注册的工具,确认「什么都没执行」。

4. 中间件改写 body 却忘了改写 header。

流式网关、日志脱敏、参数注入这类组件最容易犯。规则只有一条:要么都改,要么都不改;网关必须先拿 body 校验 header 再信任它。

5. MRTR 重试落到别的实例,内存中间态全丢。

老代码里「先把上下文塞进 ConcurrentHashMap<sessionId, Context>」的地方,无状态化后 100% 失效。要么全塞进 requestState(注意体积),要么落库用 handle 取。

6. requestState 明文回传。

最危险的坑。客户端可控 → 可篡改 → 审批绕过。必须 AES-GCM 密封 + TTL + 绑定 method|target|args。

7. 多实例部署 requestState 密钥不共享。

表现是「大约一半的审批重试解封失败」,随机且难复现。密钥走 KMS / Vault 下发,别写死在 application.yml。

8. 把业务状态藏在 session 里。

典型三种:文件上传游标、「这个用户已经付过钱」、分页上下文。无状态化后全丢。改法:要么每次请求付费、要么签发带 scope 和过期的显式 handle------注意这种 handle 本质是 bearer 凭据,必须自带作用域和有效期。

9. cacheScope 对租户数据写了 public。

跨租户数据泄漏,而且是通过缓存中间件泄漏的,日志里一点痕迹都没有。

10. 继续拿 ping 做健康检查。

ping 已被移除,K8s liveness probe 会 404 / -32601,然后 Pod 被反复重启。换成 server/discover,或者干脆用独立的 /health 端点,别让探针依赖协议方法。

补充两条:

  • SSE 断点续传没了。 Last-Event-ID 与 SSE 事件 ID 一并移除,断流即丢请求。客户端必须用新 request id 重发;如果无脑复用旧 id,会撞上「同 id 不同内容」的语义冲突。
  • 双时代服务器没有下线日期。 legacy 路径今天很方便,明年就是包袱。上线时就写进 ADR:legacy 保留到 2027-07-31,到期删除。

七、迁移路线图:五步走,不要一次到位

阶段 动作 验收标准
1. 盘点 找出所有依赖 session 的地方:会话存储、内存态、ping 探针、resources/subscribe 输出一份「状态清单」,每条注明改成 handle 还是 durable store
2. 双时代并行 protocol: MODERN + 保留 legacy shim,modern 走主路 modern / legacy 两套协议都能跑通同一批工具
3. 状态外化 handle + 持久化;MRTR handler 改成「只读准备 → 中断 → 写库」结构 故意 kill 掉第一轮实例,第二轮换实例仍能正确重放
4. 网关前置 打开 Mcp-Method/Mcp-Name 路由与按工具限流;x-mcp-header 提升地域等路由参数 网关不解析 body 也能正确路由与限流;header/body 校验生效
5. 摘掉 legacy 按 ADR 日期删除 legacy 路径与 shim 只有一条代码路径、一套测试矩阵

顺序不能乱。第 3 步没做完就切到第 4 步,网关会放行一批注定失败的请求;第 4 步没做完就摘 legacy,你连出问题的是哪一代协议都分不清。


八、总结:这次改版把 Agent 的工程化往前推了一大步

回头看六道关里的「企业级 Runtime」和「异步通信底座」两道,MCP 2026-07-28 给的答案其实很朴素:

  • 无状态,所以任何实例都能处理任何请求,扩容不再需要粘性会话;
  • 显式句柄,所以跨调用状态是可审计、可过期的,而不是藏在进程内存里;
  • 幂等重放,所以交互式中断在分布式环境下也可恢复,代价是你要自己保证副作用可对账;
  • 头部路由,所以网关能在不解析 body 的前提下做治理,而校验 header/body 一致性成了新的安全底线。

这些没有一个是在发明新概念。它们是分布式系统里被验证过二十年的老手艺:无状态、显式上下文、幂等、网关治理、fail-fast、可观测。

所以对 Java 后端来说,这版协议不需要你「转型」什么。你需要的只是把做微服务时那套「状态在哪、谁保证幂等、边界怎么防」的自觉,从 HTTP 服务平移到 Agent 的工具层。

模型负责决定做什么,协议负责把这件事可靠地送达,而系统负责敢让人少盯着一点。这三件事里,后两件一直是 Java 工程师的主场。

相关推荐
SL_staff1 小时前
JVS-BI 实战:业务人员如何用零代码 ELT + 可视化配置构建实时经营分析闭环
java·spring·全栈
SL_staff2 小时前
企业AI落地难?JVS-AI渐进式实践:安全可控、小切口验证、标准化沉淀与规模化复制
java·开源·aigc
进击的雷神3 小时前
我用 second-brain-skills 把 Claude Code 升级成了“第二大脑”:PPT、品牌手册、SOP 一人搞定
ai·claude·ppt·sop
Alluxio3 小时前
Alluxio + OCI:为 AI 工作负载实现亚毫秒访问延迟与可扩展吞吐
运维·数据库·人工智能·分布式·缓存·ai·语言模型
SL_staff3 小时前
JVS数字底座实践:构建企业级统一内容管理平台的技术路径
java·产品
nice_lcj5203 小时前
【从零理解 Spring AI · 12】MCP 的两种角色:做客户端还是做服务端
java·后端·spring·spring ai·ai应用开发
桦说编程4 小时前
TransmittableThreadLocal 解读:从 CRR 机制到上下文所有权
java·后端·源码
wflynn5 小时前
Genie 4新增原生音频,实时世界模型加入声音生成
ai·技术
xys_6785 小时前
JavaEE通关手册:初识Java
java·开发语言