modelcontextprotocol / modelcontextprotocol/java-sdk

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

Abierto
#883 2 comentarios 0 reacciones 0 asignados Ver en GitHub

Nadie ha tomado este issue todavía.

waiting for user
Lenguaje dominante
Java
Estrellas
3.7k
Forks
1.1k
Merge medio
1 d 15 h
PR fusionados (30 d)
9

Descripción

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
        ));
    }
}

Guía de contribución

Abrir la guía de contribución

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Línea de trabajo

Comienza en WebClientStreamableHttpTransport y HttpClientStreamableHttpTransport, y luego sigue sendMessage(), reconnect() y LifecycleInitializer.withInitialization() para comparar sus contextos de Reactor. Reprodúcelo con un servidor que negocie 2025-06-18 y valide el encabezado; se considera terminado cuando el GET posterior use la versión negociada y tenga éxito en lugar de devolver 400.

Escrito por el modelo de indexación a partir del texto del issue.

Evaluación

Stack tecnológico
java, spring, spring-boot
Área
api, backend, networking
Tipo de issue
Error
Dificultad
3/5
Tiempo estimado
1-2 días
Estado de actividad
Activo
Claridad
Bien especificado
Aptitud para principiantes
76/100

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.