modelcontextprotocol / modelcontextprotocol/java-sdk

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

Aperta
#883 2 commenti 0 reazioni 0 assegnatari Vedi su GitHub

Nessuno ha ancora preso questa issue.

waiting for user
Lingua principale
Java
Stelle
3.7k
Fork
1.1k
Merge medio
1g 15h
PR unite (30g)
9

Descrizione

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

Guida per i contributori

Apri la guida per i contributori

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Direzione di ricerca

Inizia da WebClientStreamableHttpTransport e HttpClientStreamableHttpTransport, quindi segui sendMessage(), reconnect() e LifecycleInitializer.withInitialization() per confrontare i relativi contesti Reactor. Riproduci il problema con un server che negozia 2025-06-18 e convalida l’header; il lavoro è completato quando il GET successivo usa la versione negoziata e ha esito positivo invece di restituire 400.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Valutazione

Stack tecnologico
java, spring, spring-boot
Ambito
api, backend, networking
Tipo di issue
Bug
Difficoltà
3/5
Tempo stimato
1-2 giorni
Stato di attività
Attiva
Chiarezza
Specificata chiaramente
Idoneità per principianti
76/100

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.