feat: implement MCP protocol version per transport support (#404)

- Add protocolVersion() method to transport and transport provider interfaces and implementations
- Replace hardcoded HTTP header strings with HttpHeaders constants
- Update protocol versions to be transport-specific rather than global
- Deprecate McpSchema.LATEST_PROTOCOL_VERSION in favor of transport-specific versions
- Standardize header names: MCP-Protocol-Version, mcp-session-id, last-event-id
- Update clients and servers to use transport.protocolVersion() for initialization
- Refactor tests to use transport-specific protocol versions
- Include MCP-Protocol-Version header in all GET/POST/DELETE requests
- Update WebClientStreamableHttpTransport, WebFluxSseClientTransport,
  HttpClientSseClientTransport, and HttpClientStreamableHttpTransport

Related to #398 , #363 , #250

Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
This commit is contained in:
Christian Tzolov 2025-07-30 23:54:28 +02:00 • committed by GitHub
commit 9d58621918
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 169 additions and 34 deletions

View file

@ -23,6 +23,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.spec.DefaultMcpTransportSession;
import io.modelcontextprotocol.spec.DefaultMcpTransportStream;
import io.modelcontextprotocol.spec.HttpHeaders;
import io.modelcontextprotocol.spec.McpClientTransport;
import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema;
@ -66,6 +67,8 @@ public class WebClientStreamableHttpTransport implements McpClientTransport {
private static final Logger logger = LoggerFactory.getLogger(WebClientStreamableHttpTransport.class);
private static final String MCP_PROTOCOL_VERSION = "2025-03-26";
private static final String DEFAULT_ENDPOINT = "/mcp";
/**
@ -103,6 +106,11 @@ public class WebClientStreamableHttpTransport implements McpClientTransport {
this.activeSession.set(createTransportSession());
}
@Override
public String protocolVersion() {
return MCP_PROTOCOL_VERSION;
}
/**
* Create a stateful builder for creating {@link WebClientStreamableHttpTransport}
* instances.
@ -128,12 +136,20 @@ public class WebClientStreamableHttpTransport implements McpClientTransport {
private DefaultMcpTransportSession createTransportSession() {
Function<String, Publisher<Void>> onClose = sessionId -> sessionId == null ? Mono.empty()
: webClient.delete().uri(this.endpoint).headers(httpHeaders -> {
httpHeaders.add("mcp-session-id", sessionId);
}).retrieve().toBodilessEntity().onErrorComplete(e -> {
logger.warn("Got error when closing transport", e);
return true;
}).then();
: webClient.delete()
.uri(this.endpoint)
.header(HttpHeaders.PROTOCOL_VERSION, MCP_PROTOCOL_VERSION)
.headers(httpHeaders -> {
httpHeaders.add(HttpHeaders.MCP_SESSION_ID, sessionId);
httpHeaders.add(HttpHeaders.PROTOCOL_VERSION, MCP_PROTOCOL_VERSION);
})
.retrieve()
.toBodilessEntity()
.onErrorComplete(e -> {
logger.warn("Got error when closing transport", e);
return true;
})
.then();
return new DefaultMcpTransportSession(onClose);
}
@ -186,10 +202,11 @@ public class WebClientStreamableHttpTransport implements McpClientTransport {
Disposable connection = webClient.get()
.uri(this.endpoint)
.accept(MediaType.TEXT_EVENT_STREAM)
.header(HttpHeaders.PROTOCOL_VERSION, MCP_PROTOCOL_VERSION)
.headers(httpHeaders -> {
transportSession.sessionId().ifPresent(id -> httpHeaders.add("mcp-session-id", id));
transportSession.sessionId().ifPresent(id -> httpHeaders.add(HttpHeaders.MCP_SESSION_ID, id));
if (stream != null) {
stream.lastId().ifPresent(id -> httpHeaders.add("last-event-id", id));
stream.lastId().ifPresent(id -> httpHeaders.add(HttpHeaders.LAST_EVENT_ID, id));
}
})
.exchangeToFlux(response -> {
@ -246,13 +263,14 @@ public class WebClientStreamableHttpTransport implements McpClientTransport {
Disposable connection = webClient.post()
.uri(this.endpoint)
.accept(MediaType.APPLICATION_JSON, MediaType.TEXT_EVENT_STREAM)
.header(HttpHeaders.PROTOCOL_VERSION, MCP_PROTOCOL_VERSION)
.headers(httpHeaders -> {
transportSession.sessionId().ifPresent(id -> httpHeaders.add("mcp-session-id", id));
transportSession.sessionId().ifPresent(id -> httpHeaders.add(HttpHeaders.MCP_SESSION_ID, id));
})
.bodyValue(message)
.exchangeToFlux(response -> {
if (transportSession
.markInitialized(response.headers().asHttpHeaders().getFirst("mcp-session-id"))) {
.markInitialized(response.headers().asHttpHeaders().getFirst(HttpHeaders.MCP_SESSION_ID))) {
// Once we have a session, we try to open an async stream for
// the server to send notifications and requests out-of-band.
reconnect(null).contextWrite(sink.contextView()).subscribe();

View file

@ -9,6 +9,8 @@ import java.util.function.Function;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.spec.HttpHeaders;
import io.modelcontextprotocol.spec.McpClientTransport;
import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema;
@ -62,6 +64,8 @@ public class WebFluxSseClientTransport implements McpClientTransport {
private static final Logger logger = LoggerFactory.getLogger(WebFluxSseClientTransport.class);
private static final String MCP_PROTOCOL_VERSION = "2024-11-05";
/**
* Event type for JSON-RPC messages received through the SSE connection. The server
* sends messages with this event type to transmit JSON-RPC protocol data.
@ -166,6 +170,11 @@ public class WebFluxSseClientTransport implements McpClientTransport {
this.sseEndpoint = sseEndpoint;
}
@Override
public String protocolVersion() {
return MCP_PROTOCOL_VERSION;
}
/**
* Establishes a connection to the MCP server using Server-Sent Events (SSE). This
* method initiates the SSE connection and sets up the message processing pipeline.
@ -250,6 +259,7 @@ public class WebFluxSseClientTransport implements McpClientTransport {
return webClient.post()
.uri(messageEndpointUri)
.contentType(MediaType.APPLICATION_JSON)
.header(HttpHeaders.PROTOCOL_VERSION, MCP_PROTOCOL_VERSION)
.bodyValue(jsonText)
.retrieve()
.toBodilessEntity()
@ -282,6 +292,7 @@ public class WebFluxSseClientTransport implements McpClientTransport {
.get()
.uri(this.sseEndpoint)
.accept(MediaType.TEXT_EVENT_STREAM)
.header(HttpHeaders.PROTOCOL_VERSION, MCP_PROTOCOL_VERSION)
.retrieve()
.bodyToFlux(SSE_TYPE)
.retryWhen(Retry.from(retrySignal -> retrySignal.handle(inboundRetryHandler)));

View file

@ -79,6 +79,8 @@ public class WebFluxSseServerTransportProvider implements McpServerTransportProv
*/
public static final String ENDPOINT_EVENT_TYPE = "endpoint";
private static final String MCP_PROTOCOL_VERSION = "2025-06-18";
/**
* Default SSE endpoint path as specified by the MCP transport specification.
*/
@ -212,6 +214,11 @@ public class WebFluxSseServerTransportProvider implements McpServerTransportProv
}
}
@Override
public String protocolVersion() {
return "2024-11-05";
}
@Override
public void setSessionFactory(McpServerSession.Factory sessionFactory) {
this.sessionFactory = sessionFactory;

View file

@ -21,7 +21,6 @@ import reactor.core.publisher.Mono;
import java.io.IOException;
import java.util.List;
import java.util.function.Function;
/**
* Implementation of a WebFlux based {@link McpStatelessServerTransport}.

View file

@ -95,6 +95,11 @@ public class WebFluxStreamableServerTransportProvider implements McpStreamableSe
}
@Override
public String protocolVersion() {
return "2025-03-26";
}
@Override
public void setSessionFactory(McpStreamableServerSession.Factory sessionFactory) {
this.sessionFactory = sessionFactory;