Streamable HTTP GET after initialize sends latest MCP-Protocol-Version instead of negotiated version
まだ誰も着手していません。
評価
- 難易度
- 3/5
- 見積もり時間
- 1〜2日
- 初心者へのやさしさ
- 76/100
- issue の種類
- バグ
- 明瞭さ
- 明確に書かれている
- 活発さ
- 活発
- 技術スタック
- java, spring, spring-boot
- 領域
- api, backend, networking
調査の方向性
WebClientStreamableHttpTransport と HttpClientStreamableHttpTransport から始め、sendMessage()、reconnect()、LifecycleInitializer.withInitialization() を追って、それらの Reactor コンテキストを比較します。2025-06-18 をネゴシエートし、ヘッダーを検証するサーバーで再現します。後続の GET がネゴシエートされたバージョンを使用して成功し、400 を返さなくなれば完了です。
索引モデルが issue の本文から書いたものです。
説明
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:
LifecycleInitializer.doInitialize()→ callsmcpClientSession.sendRequest("initialize", ...)→transport.sendMessage()sendMessage()POSTs to/mcpwith headerMCP-Protocol-Version: 2025-11-25✅ (no negotiation yet, expected)- Server responds with
protocolVersion: "2025-06-18"✅ - Inside
sendMessage(),transportSession.markInitialized(sessionId)returnstrue→ immediately callsreconnect(null)which fires a GET withMCP-Protocol-Version: 2025-11-25❌ - 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(viaspring-ai-starter-mcp-client-webflux) - MCP Server: rmcp 1.2.0 (Rust, Streamable HTTP, supporting protocol version
2025-06-18)
Steps to reproduce
- Set up an MCP server that supports
protocolVersion: "2025-06-18"and validates theMCP-Protocol-VersionHTTP header (rejecting unsupported versions). - Configure a Spring AI MCP client with
spring-ai-starter-mcp-client-webfluxusing default settings (no customsupportedProtocolVersions). - 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
));
}
}
- 主要言語
- Java
- スター
- 3.7k
- フォーク
- 1.1k
- 平均マージ
- 1日 15時間
- マージ済み PR(30日)
- 9
コントリビューションガイド
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
modelcontextprotocol/java-sdk のほかの issue
-
area/transport bug P2
難易度 2/5 1〜3時間 初心者へのやさしさ 88/100
modelcontextprotocol/java-sdk#1136 ·
-
area/client bug P2
難易度 2/5 1〜3時間 初心者へのやさしさ 84/100
modelcontextprotocol/java-sdk#1124 · コメント 1 件 ·
-
ServerCapabilities.logging is added unconditionally, overriding the caller's explicit capabilities オープンbug P2 ready for work
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
modelcontextprotocol/java-sdk#1086 · コメント 1 件 ·
-
enhancement good first issue P3
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
modelcontextprotocol/java-sdk#1067 ·
-
bug P2 ready for work
難易度 2/5 1〜3時間 初心者へのやさしさ 74/100
modelcontextprotocol/java-sdk#898 · コメント 1 件 ·
modelcontextprotocol/java-sdk の issue をすべて見る
似ている issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
infinispan/infinispan#18150 ·
-
難易度 2/5 1〜3時間 初心者へのやさしさ 84/100
-
untriaged
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
opensearch-project/k-NN#3597 ·
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 88/100
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100