From e85158219ca86cd9ad8c6d5d53ba749787941edd Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Wed, 16 Jul 2025 17:49:06 +0200 Subject: [PATCH 1/2] feat: implement MCP protocol version header support Add MCP-Protocol-Version header to all HTTP requests as required by MCP specification 2025-06-18. This enables proper server-side version negotiation and backwards compatibility handling. - Add protocol version constants (2025-06-18 for streamable HTTP, 2024-11-05 for SSE) - Include MCP-Protocol-Version header in all GET/POST/DELETE requests - Update WebClientStreamableHttpTransport, WebFluxSseClientTransport, HttpClientSseClientTransport, and HttpClientStreamableHttpTransport Complies with MCP specification requirement that HTTP clients MUST include protocol version header on all requests to MCP servers. Related to #398 , #363 , #250 Signed-off-by: Christian Tzolov --- .../WebClientStreamableHttpTransport.java | 26 ++++++++++++++----- .../transport/WebFluxSseClientTransport.java | 6 +++++ .../HttpClientSseClientTransport.java | 10 ++++++- .../HttpClientStreamableHttpTransport.java | 8 ++++++ 4 files changed, 43 insertions(+), 7 deletions(-) diff --git a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/client/transport/WebClientStreamableHttpTransport.java b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/client/transport/WebClientStreamableHttpTransport.java index 6fa76cc2e..9a8f25338 100644 --- a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/client/transport/WebClientStreamableHttpTransport.java +++ b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/client/transport/WebClientStreamableHttpTransport.java @@ -66,6 +66,10 @@ public class WebClientStreamableHttpTransport implements McpClientTransport { private static final Logger logger = LoggerFactory.getLogger(WebClientStreamableHttpTransport.class); + private static final String MCP_PROTOCOL_VERSION = "2025-06-18"; + + private static final String MCP_PROTOCOL_VERSION_HEADER_NAME = "MCP-Protocol-Version"; + private static final String DEFAULT_ENDPOINT = "/mcp"; /** @@ -128,12 +132,20 @@ public Mono connect(Function, Mono> 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(MCP_PROTOCOL_VERSION_HEADER_NAME, MCP_PROTOCOL_VERSION) + .headers(httpHeaders -> { + httpHeaders.add("mcp-session-id", sessionId); + httpHeaders.add(MCP_PROTOCOL_VERSION_HEADER_NAME, MCP_PROTOCOL_VERSION); + }) + .retrieve() + .toBodilessEntity() + .onErrorComplete(e -> { + logger.warn("Got error when closing transport", e); + return true; + }) + .then(); return new DefaultMcpTransportSession(onClose); } @@ -186,6 +198,7 @@ private Mono reconnect(McpTransportStream stream) { Disposable connection = webClient.get() .uri(this.endpoint) .accept(MediaType.TEXT_EVENT_STREAM) + .header(MCP_PROTOCOL_VERSION_HEADER_NAME, MCP_PROTOCOL_VERSION) .headers(httpHeaders -> { transportSession.sessionId().ifPresent(id -> httpHeaders.add("mcp-session-id", id)); if (stream != null) { @@ -246,6 +259,7 @@ public Mono sendMessage(McpSchema.JSONRPCMessage message) { Disposable connection = webClient.post() .uri(this.endpoint) .accept(MediaType.APPLICATION_JSON, MediaType.TEXT_EVENT_STREAM) + .header(MCP_PROTOCOL_VERSION_HEADER_NAME, MCP_PROTOCOL_VERSION) .headers(httpHeaders -> { transportSession.sessionId().ifPresent(id -> httpHeaders.add("mcp-session-id", id)); }) diff --git a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/client/transport/WebFluxSseClientTransport.java b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/client/transport/WebFluxSseClientTransport.java index 59385b54a..f3c3a6933 100644 --- a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/client/transport/WebFluxSseClientTransport.java +++ b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/client/transport/WebFluxSseClientTransport.java @@ -62,6 +62,10 @@ public class WebFluxSseClientTransport implements McpClientTransport { private static final Logger logger = LoggerFactory.getLogger(WebFluxSseClientTransport.class); + private static final String MCP_PROTOCOL_VERSION = "2024-11-05"; + + private static final String MCP_PROTOCOL_VERSION_HEADER_NAME = "MCP-Protocol-Version"; + /** * 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. @@ -250,6 +254,7 @@ public Mono sendMessage(JSONRPCMessage message) { return webClient.post() .uri(messageEndpointUri) .contentType(MediaType.APPLICATION_JSON) + .header(MCP_PROTOCOL_VERSION_HEADER_NAME, MCP_PROTOCOL_VERSION) .bodyValue(jsonText) .retrieve() .toBodilessEntity() @@ -282,6 +287,7 @@ protected Flux> eventStream() {// @formatter:off .get() .uri(this.sseEndpoint) .accept(MediaType.TEXT_EVENT_STREAM) + .header(MCP_PROTOCOL_VERSION_HEADER_NAME, MCP_PROTOCOL_VERSION) .retrieve() .bodyToFlux(SSE_TYPE) .retryWhen(Retry.from(retrySignal -> retrySignal.handle(inboundRetryHandler))); diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java b/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java index 39fb0d461..ebca4d50e 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java +++ b/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java @@ -61,6 +61,10 @@ */ public class HttpClientSseClientTransport implements McpClientTransport { + private static final String MCP_PROTOCOL_VERSION = "2024-11-05"; + + private static final String MCP_PROTOCOL_VERSION_HEADER_NAME = "MCP-Protocol-Version"; + private static final Logger logger = LoggerFactory.getLogger(HttpClientSseClientTransport.class); /** SSE event type for JSON-RPC messages */ @@ -391,6 +395,7 @@ public Mono connect(Function, Mono> h .uri(uri) .header("Accept", "text/event-stream") .header("Cache-Control", "no-cache") + .header(MCP_PROTOCOL_VERSION_HEADER_NAME, MCP_PROTOCOL_VERSION) .GET(); return Mono.from(this.httpRequestCustomizer.customize(builder, "GET", uri, null)); }).flatMap(requestBuilder -> Mono.create(sink -> { @@ -516,7 +521,10 @@ private Mono serializeMessage(final JSONRPCMessage message) { private Mono> sendHttpPost(final String endpoint, final String body) { final URI requestUri = Utils.resolveUri(baseUri, endpoint); return Mono.defer(() -> { - var builder = this.requestBuilder.copy().uri(requestUri).POST(HttpRequest.BodyPublishers.ofString(body)); + var builder = this.requestBuilder.copy() + .uri(requestUri) + .header(MCP_PROTOCOL_VERSION_HEADER_NAME, MCP_PROTOCOL_VERSION) + .POST(HttpRequest.BodyPublishers.ofString(body)); return Mono.from(this.httpRequestCustomizer.customize(builder, "POST", requestUri, body)); }).flatMap(customizedBuilder -> { var request = customizedBuilder.build(); diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java b/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java index 799716584..973f17a4e 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java +++ b/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java @@ -72,6 +72,10 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport { private static final Logger logger = LoggerFactory.getLogger(HttpClientStreamableHttpTransport.class); + private static final String MCP_PROTOCOL_VERSION = "2025-06-18"; + + private static final String MCP_PROTOCOL_VERSION_HEADER_NAME = "MCP-Protocol-Version"; + private static final String DEFAULT_ENDPOINT = "/mcp"; /** @@ -157,12 +161,14 @@ private DefaultMcpTransportSession createTransportSession() { } private Publisher createDelete(String sessionId) { + var uri = Utils.resolveUri(this.baseUri, this.endpoint); return Mono.defer(() -> { var builder = this.requestBuilder.copy() .uri(uri) .header("Cache-Control", "no-cache") .header("mcp-session-id", sessionId) + .header(MCP_PROTOCOL_VERSION_HEADER_NAME, MCP_PROTOCOL_VERSION) .DELETE(); return Mono.from(this.httpRequestCustomizer.customize(builder, "DELETE", uri, null)); }).flatMap(requestBuilder -> { @@ -231,6 +237,7 @@ private Mono reconnect(McpTransportStream stream) { var builder = requestBuilder.uri(uri) .header("Accept", TEXT_EVENT_STREAM) .header("Cache-Control", "no-cache") + .header(MCP_PROTOCOL_VERSION_HEADER_NAME, MCP_PROTOCOL_VERSION) .GET(); return Mono.from(this.httpRequestCustomizer.customize(builder, "GET", uri, null)); }) @@ -384,6 +391,7 @@ public Mono sendMessage(McpSchema.JSONRPCMessage sentMessage) { .header("Accept", APPLICATION_JSON + ", " + TEXT_EVENT_STREAM) .header("Content-Type", APPLICATION_JSON) .header("Cache-Control", "no-cache") + .header(MCP_PROTOCOL_VERSION_HEADER_NAME, MCP_PROTOCOL_VERSION) .POST(HttpRequest.BodyPublishers.ofString(jsonBody)); return Mono.from(this.httpRequestCustomizer.customize(builder, "GET", uri, jsonBody)); }).flatMapMany(requestBuilder -> Flux.create(responseEventSink -> { From c477064713d1cb7d865455515317a5551dacc497 Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Wed, 30 Jul 2025 22:21:56 +0200 Subject: [PATCH 2/2] refactor: standardize protocol version handling and HTTP headers - 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 This change improves maintainability by centralizing header name constants and allows different transports to support different protocol versions independently. Signed-off-by: Christian Tzolov --- .../WebClientStreamableHttpTransport.java | 28 +++++++++++-------- .../transport/WebFluxSseClientTransport.java | 13 ++++++--- .../WebFluxSseServerTransportProvider.java | 7 +++++ .../WebFluxStatelessServerTransport.java | 1 - ...FluxStreamableServerTransportProvider.java | 5 ++++ .../WebMvcSseServerTransportProvider.java | 5 ++++ ...bMvcStreamableServerTransportProvider.java | 5 ++++ .../WebMvcSseCustomContextPathTests.java | 11 ++++++-- .../client/McpAsyncClient.java | 5 ++-- .../HttpClientSseClientTransport.java | 5 ++++ .../HttpClientStreamableHttpTransport.java | 26 ++++++++++------- .../server/McpAsyncServer.java | 6 +++- .../server/McpStatelessAsyncServer.java | 4 ++- ...HttpServletSseServerTransportProvider.java | 5 ++++ ...vletStreamableServerTransportProvider.java | 5 ++++ .../StdioServerTransportProvider.java | 5 ++++ .../spec/HttpHeaders.java | 5 ++++ .../modelcontextprotocol/spec/McpSchema.java | 1 + .../spec/McpServerTransportProviderBase.java | 8 ++++++ .../spec/McpStatelessServerTransport.java | 4 +++ .../McpStreamableServerTransportProvider.java | 2 -- .../spec/McpTransport.java | 4 +++ .../MockMcpClientTransport.java | 11 ++++++++ .../McpAsyncClientResponseHandlerTests.java | 4 +-- .../client/McpAsyncClientTests.java | 4 +-- .../client/McpClientProtocolVersionTests.java | 6 ++-- .../server/McpServerProtocolVersionTests.java | 4 +-- 27 files changed, 144 insertions(+), 45 deletions(-) diff --git a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/client/transport/WebClientStreamableHttpTransport.java b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/client/transport/WebClientStreamableHttpTransport.java index 9a8f25338..d7f7f9bfb 100644 --- a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/client/transport/WebClientStreamableHttpTransport.java +++ b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/client/transport/WebClientStreamableHttpTransport.java @@ -23,6 +23,7 @@ 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,9 +67,7 @@ public class WebClientStreamableHttpTransport implements McpClientTransport { private static final Logger logger = LoggerFactory.getLogger(WebClientStreamableHttpTransport.class); - private static final String MCP_PROTOCOL_VERSION = "2025-06-18"; - - private static final String MCP_PROTOCOL_VERSION_HEADER_NAME = "MCP-Protocol-Version"; + private static final String MCP_PROTOCOL_VERSION = "2025-03-26"; private static final String DEFAULT_ENDPOINT = "/mcp"; @@ -107,6 +106,11 @@ private WebClientStreamableHttpTransport(ObjectMapper objectMapper, WebClient.Bu this.activeSession.set(createTransportSession()); } + @Override + public String protocolVersion() { + return MCP_PROTOCOL_VERSION; + } + /** * Create a stateful builder for creating {@link WebClientStreamableHttpTransport} * instances. @@ -134,10 +138,10 @@ private DefaultMcpTransportSession createTransportSession() { Function> onClose = sessionId -> sessionId == null ? Mono.empty() : webClient.delete() .uri(this.endpoint) - .header(MCP_PROTOCOL_VERSION_HEADER_NAME, MCP_PROTOCOL_VERSION) + .header(HttpHeaders.PROTOCOL_VERSION, MCP_PROTOCOL_VERSION) .headers(httpHeaders -> { - httpHeaders.add("mcp-session-id", sessionId); - httpHeaders.add(MCP_PROTOCOL_VERSION_HEADER_NAME, MCP_PROTOCOL_VERSION); + httpHeaders.add(HttpHeaders.MCP_SESSION_ID, sessionId); + httpHeaders.add(HttpHeaders.PROTOCOL_VERSION, MCP_PROTOCOL_VERSION); }) .retrieve() .toBodilessEntity() @@ -198,11 +202,11 @@ private Mono reconnect(McpTransportStream stream) { Disposable connection = webClient.get() .uri(this.endpoint) .accept(MediaType.TEXT_EVENT_STREAM) - .header(MCP_PROTOCOL_VERSION_HEADER_NAME, MCP_PROTOCOL_VERSION) + .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 -> { @@ -259,14 +263,14 @@ public Mono sendMessage(McpSchema.JSONRPCMessage message) { Disposable connection = webClient.post() .uri(this.endpoint) .accept(MediaType.APPLICATION_JSON, MediaType.TEXT_EVENT_STREAM) - .header(MCP_PROTOCOL_VERSION_HEADER_NAME, MCP_PROTOCOL_VERSION) + .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(); diff --git a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/client/transport/WebFluxSseClientTransport.java b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/client/transport/WebFluxSseClientTransport.java index f3c3a6933..fe6b07a6d 100644 --- a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/client/transport/WebFluxSseClientTransport.java +++ b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/client/transport/WebFluxSseClientTransport.java @@ -9,6 +9,8 @@ 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; @@ -64,8 +66,6 @@ public class WebFluxSseClientTransport implements McpClientTransport { private static final String MCP_PROTOCOL_VERSION = "2024-11-05"; - private static final String MCP_PROTOCOL_VERSION_HEADER_NAME = "MCP-Protocol-Version"; - /** * 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. @@ -170,6 +170,11 @@ public WebFluxSseClientTransport(WebClient.Builder webClientBuilder, ObjectMappe 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. @@ -254,7 +259,7 @@ public Mono sendMessage(JSONRPCMessage message) { return webClient.post() .uri(messageEndpointUri) .contentType(MediaType.APPLICATION_JSON) - .header(MCP_PROTOCOL_VERSION_HEADER_NAME, MCP_PROTOCOL_VERSION) + .header(HttpHeaders.PROTOCOL_VERSION, MCP_PROTOCOL_VERSION) .bodyValue(jsonText) .retrieve() .toBodilessEntity() @@ -287,7 +292,7 @@ protected Flux> eventStream() {// @formatter:off .get() .uri(this.sseEndpoint) .accept(MediaType.TEXT_EVENT_STREAM) - .header(MCP_PROTOCOL_VERSION_HEADER_NAME, MCP_PROTOCOL_VERSION) + .header(HttpHeaders.PROTOCOL_VERSION, MCP_PROTOCOL_VERSION) .retrieve() .bodyToFlux(SSE_TYPE) .retryWhen(Retry.from(retrySignal -> retrySignal.handle(inboundRetryHandler))); diff --git a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransportProvider.java b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransportProvider.java index b1b5246c8..67810fb56 100644 --- a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransportProvider.java +++ b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransportProvider.java @@ -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 WebFluxSseServerTransportProvider(ObjectMapper objectMapper, String baseU } } + @Override + public String protocolVersion() { + return "2024-11-05"; + } + @Override public void setSessionFactory(McpServerSession.Factory sessionFactory) { this.sessionFactory = sessionFactory; diff --git a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxStatelessServerTransport.java b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxStatelessServerTransport.java index e75e9262d..c514f2dff 100644 --- a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxStatelessServerTransport.java +++ b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxStatelessServerTransport.java @@ -21,7 +21,6 @@ import java.io.IOException; import java.util.List; -import java.util.function.Function; /** * Implementation of a WebFlux based {@link McpStatelessServerTransport}. diff --git a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxStreamableServerTransportProvider.java b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxStreamableServerTransportProvider.java index 79224a57d..00ec68c5d 100644 --- a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxStreamableServerTransportProvider.java +++ b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxStreamableServerTransportProvider.java @@ -95,6 +95,11 @@ private WebFluxStreamableServerTransportProvider(ObjectMapper objectMapper, Stri } + @Override + public String protocolVersion() { + return "2025-03-26"; + } + @Override public void setSessionFactory(McpStreamableServerSession.Factory sessionFactory) { this.sessionFactory = sessionFactory; diff --git a/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcSseServerTransportProvider.java b/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcSseServerTransportProvider.java index b90f9fb3d..a3898006d 100644 --- a/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcSseServerTransportProvider.java +++ b/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcSseServerTransportProvider.java @@ -209,6 +209,11 @@ public WebMvcSseServerTransportProvider(ObjectMapper objectMapper, String baseUr } } + @Override + public String protocolVersion() { + return "2024-11-05"; + } + @Override public void setSessionFactory(McpServerSession.Factory sessionFactory) { this.sessionFactory = sessionFactory; diff --git a/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcStreamableServerTransportProvider.java b/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcStreamableServerTransportProvider.java index 391aa3e8d..2f94d5c11 100644 --- a/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcStreamableServerTransportProvider.java +++ b/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcStreamableServerTransportProvider.java @@ -147,6 +147,11 @@ private WebMvcStreamableServerTransportProvider(ObjectMapper objectMapper, Strin } } + @Override + public String protocolVersion() { + return "2025-03-26"; + } + @Override public void setSessionFactory(McpStreamableServerSession.Factory sessionFactory) { this.sessionFactory = sessionFactory; diff --git a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseCustomContextPathTests.java b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseCustomContextPathTests.java index 1b5218cc5..cce36d191 100644 --- a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseCustomContextPathTests.java +++ b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseCustomContextPathTests.java @@ -91,8 +91,15 @@ static class TestConfig { @Bean public WebMvcSseServerTransportProvider webMvcSseServerTransportProvider() { - return new WebMvcSseServerTransportProvider(new ObjectMapper(), CUSTOM_CONTEXT_PATH, MESSAGE_ENDPOINT, - WebMvcSseServerTransportProvider.DEFAULT_SSE_ENDPOINT); + return WebMvcSseServerTransportProvider.builder() + .objectMapper(new ObjectMapper()) + .baseUrl(CUSTOM_CONTEXT_PATH) + .messageEndpoint(MESSAGE_ENDPOINT) + .sseEndpoint(WebMvcSseServerTransportProvider.DEFAULT_SSE_ENDPOINT) + .build(); + // return new WebMvcSseServerTransportProvider(new ObjectMapper(), + // CUSTOM_CONTEXT_PATH, MESSAGE_ENDPOINT, + // WebMvcSseServerTransportProvider.DEFAULT_SSE_ENDPOINT); } @Bean diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java index 22c0ee1d2..73765122f 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java +++ b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java @@ -272,9 +272,8 @@ public class McpAsyncClient { asyncProgressNotificationHandler(progressConsumersFinal)); this.initializer = new LifecycleInitializer(clientCapabilities, clientInfo, - List.of(McpSchema.LATEST_PROTOCOL_VERSION), initializationTimeout, - ctx -> new McpClientSession(requestTimeout, transport, requestHandlers, notificationHandlers, - con -> con.contextWrite(ctx))); + List.of(transport.protocolVersion()), initializationTimeout, ctx -> new McpClientSession(requestTimeout, + transport, requestHandlers, notificationHandlers, con -> con.contextWrite(ctx))); this.transport.setExceptionHandler(this.initializer::handleException); } diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java b/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java index ebca4d50e..3fe88fec8 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java +++ b/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientSseClientTransport.java @@ -215,6 +215,11 @@ public HttpClientSseClientTransport(HttpClient.Builder clientBuilder, HttpReques this.httpRequestCustomizer = httpRequestCustomizer; } + @Override + public String protocolVersion() { + return MCP_PROTOCOL_VERSION; + } + /** * Creates a new builder for {@link HttpClientSseClientTransport}. * @param baseUri the base URI of the MCP server diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java b/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java index 973f17a4e..dadb09abc 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java +++ b/mcp/src/main/java/io/modelcontextprotocol/client/transport/HttpClientStreamableHttpTransport.java @@ -28,6 +28,7 @@ import io.modelcontextprotocol.client.transport.ResponseSubscribers.ResponseEvent; 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; @@ -72,9 +73,7 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport { private static final Logger logger = LoggerFactory.getLogger(HttpClientStreamableHttpTransport.class); - private static final String MCP_PROTOCOL_VERSION = "2025-06-18"; - - private static final String MCP_PROTOCOL_VERSION_HEADER_NAME = "MCP-Protocol-Version"; + private static final String MCP_PROTOCOL_VERSION = "2025-03-26"; private static final String DEFAULT_ENDPOINT = "/mcp"; @@ -135,6 +134,11 @@ private HttpClientStreamableHttpTransport(ObjectMapper objectMapper, HttpClient this.httpRequestCustomizer = httpRequestCustomizer; } + @Override + public String protocolVersion() { + return MCP_PROTOCOL_VERSION; + } + public static Builder builder(String baseUri) { return new Builder(baseUri); } @@ -167,8 +171,8 @@ private Publisher createDelete(String sessionId) { var builder = this.requestBuilder.copy() .uri(uri) .header("Cache-Control", "no-cache") - .header("mcp-session-id", sessionId) - .header(MCP_PROTOCOL_VERSION_HEADER_NAME, MCP_PROTOCOL_VERSION) + .header(HttpHeaders.MCP_SESSION_ID, sessionId) + .header(HttpHeaders.PROTOCOL_VERSION, MCP_PROTOCOL_VERSION) .DELETE(); return Mono.from(this.httpRequestCustomizer.customize(builder, "DELETE", uri, null)); }).flatMap(requestBuilder -> { @@ -227,17 +231,18 @@ private Mono reconnect(McpTransportStream stream) { HttpRequest.Builder requestBuilder = this.requestBuilder.copy(); if (transportSession != null && transportSession.sessionId().isPresent()) { - requestBuilder = requestBuilder.header("mcp-session-id", transportSession.sessionId().get()); + requestBuilder = requestBuilder.header(HttpHeaders.MCP_SESSION_ID, + transportSession.sessionId().get()); } if (stream != null && stream.lastId().isPresent()) { - requestBuilder = requestBuilder.header("last-event-id", stream.lastId().get()); + requestBuilder = requestBuilder.header(HttpHeaders.LAST_EVENT_ID, stream.lastId().get()); } var builder = requestBuilder.uri(uri) .header("Accept", TEXT_EVENT_STREAM) .header("Cache-Control", "no-cache") - .header(MCP_PROTOCOL_VERSION_HEADER_NAME, MCP_PROTOCOL_VERSION) + .header(HttpHeaders.PROTOCOL_VERSION, MCP_PROTOCOL_VERSION) .GET(); return Mono.from(this.httpRequestCustomizer.customize(builder, "GET", uri, null)); }) @@ -384,14 +389,15 @@ public Mono sendMessage(McpSchema.JSONRPCMessage sentMessage) { HttpRequest.Builder requestBuilder = this.requestBuilder.copy(); if (transportSession != null && transportSession.sessionId().isPresent()) { - requestBuilder = requestBuilder.header("mcp-session-id", transportSession.sessionId().get()); + requestBuilder = requestBuilder.header(HttpHeaders.MCP_SESSION_ID, + transportSession.sessionId().get()); } var builder = requestBuilder.uri(uri) .header("Accept", APPLICATION_JSON + ", " + TEXT_EVENT_STREAM) .header("Content-Type", APPLICATION_JSON) .header("Cache-Control", "no-cache") - .header(MCP_PROTOCOL_VERSION_HEADER_NAME, MCP_PROTOCOL_VERSION) + .header(HttpHeaders.PROTOCOL_VERSION, MCP_PROTOCOL_VERSION) .POST(HttpRequest.BodyPublishers.ofString(jsonBody)); return Mono.from(this.httpRequestCustomizer.customize(builder, "GET", uri, jsonBody)); }).flatMapMany(requestBuilder -> Flux.create(responseEventSink -> { diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java index 1b3eee3c8..9605fb3f2 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java @@ -115,7 +115,7 @@ public class McpAsyncServer { private final ConcurrentHashMap completions = new ConcurrentHashMap<>(); - private List protocolVersions = List.of(McpSchema.LATEST_PROTOCOL_VERSION); + private List protocolVersions; private McpUriTemplateManagerFactory uriTemplateManagerFactory = new DeafaultMcpUriTemplateManagerFactory(); @@ -145,6 +145,8 @@ public class McpAsyncServer { Map> requestHandlers = prepareRequestHandlers(); Map notificationHandlers = prepareNotificationHandlers(features); + this.protocolVersions = List.of(mcpTransportProvider.protocolVersion()); + mcpTransportProvider.setSessionFactory(transport -> new McpServerSession(UUID.randomUUID().toString(), requestTimeout, transport, this::asyncInitializeRequestHandler, requestHandlers, notificationHandlers)); } @@ -168,6 +170,8 @@ public class McpAsyncServer { Map> requestHandlers = prepareRequestHandlers(); Map notificationHandlers = prepareNotificationHandlers(features); + this.protocolVersions = List.of(mcpTransportProvider.protocolVersion()); + mcpTransportProvider.setSessionFactory(new DefaultMcpStreamableServerSessionFactory(requestTimeout, this::asyncInitializeRequestHandler, requestHandlers, notificationHandlers)); } diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java index 63fefa31d..565c53f13 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java @@ -63,7 +63,7 @@ public class McpStatelessAsyncServer { private final ConcurrentHashMap completions = new ConcurrentHashMap<>(); - private List protocolVersions = List.of(McpSchema.LATEST_PROTOCOL_VERSION); + private List protocolVersions; private McpUriTemplateManagerFactory uriTemplateManagerFactory = new DeafaultMcpUriTemplateManagerFactory(); @@ -118,6 +118,8 @@ public class McpStatelessAsyncServer { requestHandlers.put(McpSchema.METHOD_COMPLETION_COMPLETE, completionCompleteRequestHandler()); } + this.protocolVersions = List.of(mcpTransport.protocolVersion()); + McpStatelessServerHandler handler = new DefaultMcpStatelessServerHandler(requestHandlers, Map.of()); mcpTransport.setMcpHandler(handler); } diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java b/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java index 5c0b85f26..24e749fc3 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProvider.java @@ -178,6 +178,11 @@ public HttpServletSseServerTransportProvider(ObjectMapper objectMapper, String b } } + @Override + public String protocolVersion() { + return "2024-11-05"; + } + /** * Creates a new HttpServletSseServerTransportProvider instance with the default SSE * endpoint. diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java b/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java index 211a9c052..6805bf194 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStreamableServerTransportProvider.java @@ -154,6 +154,11 @@ private HttpServletStreamableServerTransportProvider(ObjectMapper objectMapper, } + @Override + public String protocolVersion() { + return "2025-03-26"; + } + @Override public void setSessionFactory(McpStreamableServerSession.Factory sessionFactory) { this.sessionFactory = sessionFactory; diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/transport/StdioServerTransportProvider.java b/mcp/src/main/java/io/modelcontextprotocol/server/transport/StdioServerTransportProvider.java index 9ef9c7829..d2943b31d 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/transport/StdioServerTransportProvider.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/transport/StdioServerTransportProvider.java @@ -88,6 +88,11 @@ public StdioServerTransportProvider(ObjectMapper objectMapper, InputStream input this.outputStream = outputStream; } + @Override + public String protocolVersion() { + return "2024-11-05"; + } + @Override public void setSessionFactory(McpServerSession.Factory sessionFactory) { // Create a single session for the stdio connection diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/HttpHeaders.java b/mcp/src/main/java/io/modelcontextprotocol/spec/HttpHeaders.java index 2e8084915..c1c4c7a7d 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/HttpHeaders.java +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/HttpHeaders.java @@ -17,4 +17,9 @@ public interface HttpHeaders { */ String LAST_EVENT_ID = "last-event-id"; + /** + * Identifies the MCP protocol version. + */ + String PROTOCOL_VERSION = "MCP-Protocol-Version"; + } diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java index a3812dbc2..fb4baabfb 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java @@ -44,6 +44,7 @@ public final class McpSchema { private McpSchema() { } + @Deprecated public static final String LATEST_PROTOCOL_VERSION = "2025-03-26"; public static final String JSONRPC_VERSION = "2.0"; diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerTransportProviderBase.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerTransportProviderBase.java index 87e7d6441..798575017 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerTransportProviderBase.java +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerTransportProviderBase.java @@ -55,4 +55,12 @@ default void close() { */ Mono closeGracefully(); + /** + * Returns the protocol version supported by this transport provider. + * @return the protocol version as a string + */ + default String protocolVersion() { + return "2024-11-05"; + } + } diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpStatelessServerTransport.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpStatelessServerTransport.java index b6211fe3b..329908469 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpStatelessServerTransport.java +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpStatelessServerTransport.java @@ -22,4 +22,8 @@ default void close() { */ Mono closeGracefully(); + default String protocolVersion() { + return "2025-03-26"; + } + } diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpStreamableServerTransportProvider.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpStreamableServerTransportProvider.java index 87574e8ab..b75081096 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpStreamableServerTransportProvider.java +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpStreamableServerTransportProvider.java @@ -2,8 +2,6 @@ import reactor.core.publisher.Mono; -import java.util.Map; - /** * The core building block providing the server-side MCP transport for Streamable HTTP * servers. Implement this interface to bridge between a particular server-side technology diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpTransport.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpTransport.java index 40d9ba7ac..49c485059 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpTransport.java +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpTransport.java @@ -77,4 +77,8 @@ default void close() { */ T unmarshalFrom(Object data, TypeReference typeRef); + default String protocolVersion() { + return "2024-11-05"; + } + } diff --git a/mcp/src/test/java/io/modelcontextprotocol/MockMcpClientTransport.java b/mcp/src/test/java/io/modelcontextprotocol/MockMcpClientTransport.java index 482d0aac6..b531d5739 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/MockMcpClientTransport.java +++ b/mcp/src/test/java/io/modelcontextprotocol/MockMcpClientTransport.java @@ -29,6 +29,8 @@ public class MockMcpClientTransport implements McpClientTransport { private final BiConsumer interceptor; + private String protocolVersion = McpSchema.LATEST_PROTOCOL_VERSION; + public MockMcpClientTransport() { this((t, msg) -> { }); @@ -38,6 +40,15 @@ public MockMcpClientTransport(BiConsumer { - assertThat(result.protocolVersion()).isEqualTo(McpSchema.LATEST_PROTOCOL_VERSION); + assertThat(result.protocolVersion()).isEqualTo(transport.protocolVersion()); }).verifyComplete(); } diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/McpServerProtocolVersionTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/McpServerProtocolVersionTests.java index f643f1ba3..95086ee81 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/server/McpServerProtocolVersionTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/server/McpServerProtocolVersionTests.java @@ -45,7 +45,7 @@ void shouldUseLatestVersionByDefault() { assertThat(jsonResponse.id()).isEqualTo(requestId); assertThat(jsonResponse.result()).isInstanceOf(McpSchema.InitializeResult.class); McpSchema.InitializeResult result = (McpSchema.InitializeResult) jsonResponse.result(); - assertThat(result.protocolVersion()).isEqualTo(McpSchema.LATEST_PROTOCOL_VERSION); + assertThat(result.protocolVersion()).isEqualTo(transportProvider.protocolVersion()); server.closeGracefully().subscribe(); } @@ -93,7 +93,7 @@ void shouldSuggestLatestVersionForUnsupportedVersion() { assertThat(jsonResponse.id()).isEqualTo(requestId); assertThat(jsonResponse.result()).isInstanceOf(McpSchema.InitializeResult.class); McpSchema.InitializeResult result = (McpSchema.InitializeResult) jsonResponse.result(); - assertThat(result.protocolVersion()).isEqualTo(McpSchema.LATEST_PROTOCOL_VERSION); + assertThat(result.protocolVersion()).isEqualTo(transportProvider.protocolVersion()); server.closeGracefully().subscribe(); }