modelcontextprotocol / modelcontextprotocol/java-sdk

Streamable HTTP GET after initialize sends latest MCP-Protocol-Version instead of negotiated version

未关闭
#883 2 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

还没有人认领这个 Issue。

waiting for user
主要语言
Java
星标
3.7k
派生
1.1k
平均合并
1 天 15 小时
30 天内合并 PR
9

描述

Disclaimer: I am not a native English speaker. This issue was drafted with AI translation assistance. I apologize for any awkward phrasing.

Bug description

After a successful initialize handshake where the server negotiates down to an older protocol version (e.g. 2025-06-18), the subsequent GET request to open the SSE stream still sends the client's original latest version (2025-11-25) in the MCP-Protocol-Version HTTP header rather than the negotiated version. Servers that validate this header (such as rmcp) reject the request with 400 Bad Request.

Root cause

In both WebClientStreamableHttpTransport and HttpClientStreamableHttpTransport, the MCP-Protocol-Version header is resolved via Reactor Context:

ctx.getOrDefault(McpAsyncClient.NEGOTIATED_PROTOCOL_VERSION, this.latestSupportedProtocolVersion)

The negotiated version is written into the Reactor Context by LifecycleInitializer.withInitialization():

.flatMap(res -> operation.apply(res)
    .contextWrite(c -> c.put(McpAsyncClient.NEGOTIATED_PROTOCOL_VERSION,
        res.initializeResult().protocolVersion())));

However, the GET reconnect is triggered as a side effect inside sendMessage() when the transport session is first marked as initialized:

if (transportSession.markInitialized(response.headers()...getFirst(HttpHeaders.MCP_SESSION_ID))) {
    reconnect(null).contextWrite(sink.contextView()).subscribe();
}

At this point, sink.contextView() does not yet contain NEGOTIATED_PROTOCOL_VERSION because the contextWrite in LifecycleInitializer has not executed yet — it runs after the initialize request's sendMessage() completes. So the GET reconnect falls back to latestSupportedProtocolVersion (2025-11-25).

Timeline:

  1. LifecycleInitializer.doInitialize() → calls mcpClientSession.sendRequest("initialize", ...)transport.sendMessage()
  2. sendMessage() POSTs to /mcp with header MCP-Protocol-Version: 2025-11-25 ✅ (no negotiation yet, expected)
  3. Server responds with protocolVersion: "2025-06-18"
  4. Inside sendMessage(), transportSession.markInitialized(sessionId) returns trueimmediately calls reconnect(null) which fires a GET with MCP-Protocol-Version: 2025-11-25
  5. Later, LifecycleInitializer.withInitialization() runs .contextWrite(c -> c.put(NEGOTIATED_PROTOCOL_VERSION, "2025-06-18"))too late for the GET in step 4

Environment

  • Spring AI: 2.0.0-M3
  • MCP Java SDK (io.modelcontextprotocol.sdk:mcp-core): 1.1.0
  • Spring Boot: 4.0.4
  • Java: 25
  • Transport: WebClientStreamableHttpTransport (via spring-ai-starter-mcp-client-webflux)
  • MCP Server: rmcp 1.2.0 (Rust, Streamable HTTP, supporting protocol version 2025-06-18)

Steps to reproduce

  1. Set up an MCP server that supports protocolVersion: "2025-06-18" and validates the MCP-Protocol-Version HTTP header (rejecting unsupported versions).
  2. Configure a Spring AI MCP client with spring-ai-starter-mcp-client-webflux using default settings (no custom supportedProtocolVersions).
  3. Start the application and observe the HTTP traffic.

Observed:

POST /mcp  →  MCP-Protocol-Version: 2025-11-25  →  200 OK (negotiated to 2025-06-18)
GET  /mcp  →  MCP-Protocol-Version: 2025-11-25  →  400 Bad Request

Expected behavior

After the server responds with protocolVersion: "2025-06-18", all subsequent requests (including the GET SSE reconnect) should use MCP-Protocol-Version: 2025-06-18 in the HTTP header:

POST /mcp  →  MCP-Protocol-Version: 2025-11-25  →  200 OK (negotiated to 2025-06-18)
GET  /mcp  →  MCP-Protocol-Version: 2025-06-18  →  200 OK

Workaround

Register a McpClientCustomizer bean to remove 2025-11-25 from the supported versions list, so the fallback value aligns with the server:

@Component
public class McpTransportCustomizer implements McpClientCustomizer<WebClientStreamableHttpTransport.Builder> {

    @Override
    public void customize(String name, WebClientStreamableHttpTransport.Builder builder) {
        builder.supportedProtocolVersions(List.of(
                ProtocolVersions.MCP_2024_11_05,
                ProtocolVersions.MCP_2025_03_26,
                ProtocolVersions.MCP_2025_06_18
        ));
    }
}

贡献指南

打开贡献指南

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

调研方向

从 WebClientStreamableHttpTransport 和 HttpClientStreamableHttpTransport 开始,然后跟踪 sendMessage()、reconnect() 和 LifecycleInitializer.withInitialization(),以比较它们的 Reactor 上下文。使用一个协商 2025-06-18 并验证标头的服务器进行复现;当后续 GET 使用协商后的版本并成功,而不是返回 400 时,即完成。

由索引模型根据 Issue 内容生成。

评估

技术栈
java, spring, spring-boot
领域
api, backend, networking
Issue 类型
缺陷
难度
3/5
预计耗时
1-2 天
活跃度
活跃
描述清晰度
描述清楚
新手友好度
76/100

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。