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() → calls mcpClientSession.sendRequest("initialize", ...) → transport.sendMessage()
sendMessage() POSTs to /mcp with header MCP-Protocol-Version: 2025-11-25 ✅ (no negotiation yet, expected)
- Server responds with
protocolVersion: "2025-06-18" ✅
- Inside
sendMessage(), transportSession.markInitialized(sessionId) returns true → immediately calls reconnect(null) which fires a GET with MCP-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 (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
- Set up an MCP server that supports
protocolVersion: "2025-06-18" and validates the MCP-Protocol-Version HTTP header (rejecting unsupported versions).
- Configure a Spring AI MCP client with
spring-ai-starter-mcp-client-webflux using default settings (no custom supportedProtocolVersions).
- 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
));
}
}
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
initializehandshake 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 theMCP-Protocol-VersionHTTP header rather than the negotiated version. Servers that validate this header (such as rmcp) reject the request with400 Bad Request.Root cause
In both
WebClientStreamableHttpTransportandHttpClientStreamableHttpTransport, theMCP-Protocol-Versionheader is resolved via Reactor Context:The negotiated version is written into the Reactor Context by
LifecycleInitializer.withInitialization():However, the GET reconnect is triggered as a side effect inside
sendMessage()when the transport session is first marked as initialized:At this point,
sink.contextView()does not yet containNEGOTIATED_PROTOCOL_VERSIONbecause thecontextWriteinLifecycleInitializerhas not executed yet — it runs after theinitializerequest'ssendMessage()completes. So the GET reconnect falls back tolatestSupportedProtocolVersion(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)protocolVersion: "2025-06-18"✅sendMessage(),transportSession.markInitialized(sessionId)returnstrue→ immediately callsreconnect(null)which fires a GET withMCP-Protocol-Version: 2025-11-25❌LifecycleInitializer.withInitialization()runs.contextWrite(c -> c.put(NEGOTIATED_PROTOCOL_VERSION, "2025-06-18"))— too late for the GET in step 4Environment
io.modelcontextprotocol.sdk:mcp-core): 1.1.0WebClientStreamableHttpTransport(viaspring-ai-starter-mcp-client-webflux)2025-06-18)Steps to reproduce
protocolVersion: "2025-06-18"and validates theMCP-Protocol-VersionHTTP header (rejecting unsupported versions).spring-ai-starter-mcp-client-webfluxusing default settings (no customsupportedProtocolVersions).Observed:
Expected behavior
After the server responds with
protocolVersion: "2025-06-18", all subsequent requests (including the GET SSE reconnect) should useMCP-Protocol-Version: 2025-06-18in the HTTP header:Workaround
Register a
McpClientCustomizerbean to remove2025-11-25from the supported versions list, so the fallback value aligns with the server: