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:
parent
67de84b300
commit
9d58621918
19 changed files with 169 additions and 34 deletions
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -61,6 +61,10 @@ import reactor.core.publisher.Sinks;
|
|||
*/
|
||||
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 */
|
||||
|
|
@ -211,6 +215,11 @@ public class HttpClientSseClientTransport implements McpClientTransport {
|
|||
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
|
||||
|
|
@ -391,6 +400,7 @@ public class HttpClientSseClientTransport implements McpClientTransport {
|
|||
.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 +526,10 @@ public class HttpClientSseClientTransport implements McpClientTransport {
|
|||
private Mono<HttpResponse<String>> 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();
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||
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,6 +73,8 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport {
|
|||
|
||||
private static final Logger logger = LoggerFactory.getLogger(HttpClientStreamableHttpTransport.class);
|
||||
|
||||
private static final String MCP_PROTOCOL_VERSION = "2025-03-26";
|
||||
|
||||
private static final String DEFAULT_ENDPOINT = "/mcp";
|
||||
|
||||
/**
|
||||
|
|
@ -131,6 +134,11 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport {
|
|||
this.httpRequestCustomizer = httpRequestCustomizer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String protocolVersion() {
|
||||
return MCP_PROTOCOL_VERSION;
|
||||
}
|
||||
|
||||
public static Builder builder(String baseUri) {
|
||||
return new Builder(baseUri);
|
||||
}
|
||||
|
|
@ -157,12 +165,14 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport {
|
|||
}
|
||||
|
||||
private Publisher<Void> 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(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 -> {
|
||||
|
|
@ -221,16 +231,18 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport {
|
|||
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(HttpHeaders.PROTOCOL_VERSION, MCP_PROTOCOL_VERSION)
|
||||
.GET();
|
||||
return Mono.from(this.httpRequestCustomizer.customize(builder, "GET", uri, null));
|
||||
})
|
||||
|
|
@ -377,13 +389,15 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport {
|
|||
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(HttpHeaders.PROTOCOL_VERSION, MCP_PROTOCOL_VERSION)
|
||||
.POST(HttpRequest.BodyPublishers.ofString(jsonBody));
|
||||
return Mono.from(this.httpRequestCustomizer.customize(builder, "GET", uri, jsonBody));
|
||||
}).flatMapMany(requestBuilder -> Flux.<ResponseEvent>create(responseEventSink -> {
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ public class McpAsyncServer {
|
|||
|
||||
private final ConcurrentHashMap<McpSchema.CompleteReference, McpServerFeatures.AsyncCompletionSpecification> completions = new ConcurrentHashMap<>();
|
||||
|
||||
private List<String> protocolVersions = List.of(McpSchema.LATEST_PROTOCOL_VERSION);
|
||||
private List<String> protocolVersions;
|
||||
|
||||
private McpUriTemplateManagerFactory uriTemplateManagerFactory = new DeafaultMcpUriTemplateManagerFactory();
|
||||
|
||||
|
|
@ -145,6 +145,8 @@ public class McpAsyncServer {
|
|||
Map<String, McpRequestHandler<?>> requestHandlers = prepareRequestHandlers();
|
||||
Map<String, McpNotificationHandler> 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<String, McpRequestHandler<?>> requestHandlers = prepareRequestHandlers();
|
||||
Map<String, McpNotificationHandler> notificationHandlers = prepareNotificationHandlers(features);
|
||||
|
||||
this.protocolVersions = List.of(mcpTransportProvider.protocolVersion());
|
||||
|
||||
mcpTransportProvider.setSessionFactory(new DefaultMcpStreamableServerSessionFactory(requestTimeout,
|
||||
this::asyncInitializeRequestHandler, requestHandlers, notificationHandlers));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ public class McpStatelessAsyncServer {
|
|||
|
||||
private final ConcurrentHashMap<McpSchema.CompleteReference, McpStatelessServerFeatures.AsyncCompletionSpecification> completions = new ConcurrentHashMap<>();
|
||||
|
||||
private List<String> protocolVersions = List.of(McpSchema.LATEST_PROTOCOL_VERSION);
|
||||
private List<String> 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -178,6 +178,11 @@ public class HttpServletSseServerTransportProvider extends HttpServlet implement
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String protocolVersion() {
|
||||
return "2024-11-05";
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new HttpServletSseServerTransportProvider instance with the default SSE
|
||||
* endpoint.
|
||||
|
|
|
|||
|
|
@ -154,6 +154,11 @@ public class HttpServletStreamableServerTransportProvider extends HttpServlet
|
|||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String protocolVersion() {
|
||||
return "2025-03-26";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSessionFactory(McpStreamableServerSession.Factory sessionFactory) {
|
||||
this.sessionFactory = sessionFactory;
|
||||
|
|
|
|||
|
|
@ -88,6 +88,11 @@ public class StdioServerTransportProvider implements McpServerTransportProvider
|
|||
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
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -55,4 +55,12 @@ public interface McpServerTransportProviderBase {
|
|||
*/
|
||||
Mono<Void> closeGracefully();
|
||||
|
||||
/**
|
||||
* Returns the protocol version supported by this transport provider.
|
||||
* @return the protocol version as a string
|
||||
*/
|
||||
default String protocolVersion() {
|
||||
return "2024-11-05";
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,4 +22,8 @@ public interface McpStatelessServerTransport {
|
|||
*/
|
||||
Mono<Void> closeGracefully();
|
||||
|
||||
default String protocolVersion() {
|
||||
return "2025-03-26";
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@ package io.modelcontextprotocol.spec;
|
|||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -77,4 +77,8 @@ public interface McpTransport {
|
|||
*/
|
||||
<T> T unmarshalFrom(Object data, TypeReference<T> typeRef);
|
||||
|
||||
default String protocolVersion() {
|
||||
return "2024-11-05";
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ public class MockMcpClientTransport implements McpClientTransport {
|
|||
|
||||
private final BiConsumer<MockMcpClientTransport, McpSchema.JSONRPCMessage> interceptor;
|
||||
|
||||
private String protocolVersion = McpSchema.LATEST_PROTOCOL_VERSION;
|
||||
|
||||
public MockMcpClientTransport() {
|
||||
this((t, msg) -> {
|
||||
});
|
||||
|
|
@ -38,6 +40,15 @@ public class MockMcpClientTransport implements McpClientTransport {
|
|||
this.interceptor = interceptor;
|
||||
}
|
||||
|
||||
public MockMcpClientTransport withProtocolVersion(String protocolVersion) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String protocolVersion() {
|
||||
return protocolVersion;
|
||||
}
|
||||
|
||||
public void simulateIncomingMessage(McpSchema.JSONRPCMessage message) {
|
||||
if (inbound.tryEmitNext(message).isFailure()) {
|
||||
throw new RuntimeException("Failed to process incoming message " + message);
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ class McpAsyncClientResponseHandlerTests {
|
|||
r.id(), mockInitResult, null);
|
||||
t.simulateIncomingMessage(initResponse);
|
||||
}
|
||||
});
|
||||
}).withProtocolVersion(McpSchema.LATEST_PROTOCOL_VERSION);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -79,7 +79,7 @@ class McpAsyncClientResponseHandlerTests {
|
|||
|
||||
// Verify initialization result
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.protocolVersion()).isEqualTo(McpSchema.LATEST_PROTOCOL_VERSION);
|
||||
assertThat(result.protocolVersion()).isEqualTo(transport.protocolVersion());
|
||||
assertThat(result.capabilities()).isEqualTo(serverCapabilities);
|
||||
assertThat(result.serverInfo()).isEqualTo(serverInfo);
|
||||
assertThat(result.instructions()).isEqualTo("Test instructions");
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ class McpAsyncClientTests {
|
|||
public static final McpSchema.ServerCapabilities MOCK_SERVER_CAPABILITIES = McpSchema.ServerCapabilities.builder()
|
||||
.build();
|
||||
|
||||
public static final McpSchema.InitializeResult MOCK_INIT_RESULT = new McpSchema.InitializeResult(
|
||||
McpSchema.LATEST_PROTOCOL_VERSION, MOCK_SERVER_CAPABILITIES, MOCK_SERVER_INFO, "Test instructions");
|
||||
public static final McpSchema.InitializeResult MOCK_INIT_RESULT = new McpSchema.InitializeResult("2024-11-05",
|
||||
MOCK_SERVER_CAPABILITIES, MOCK_SERVER_INFO, "Test instructions");
|
||||
|
||||
private static final String CONTEXT_KEY = "context.key";
|
||||
|
||||
|
|
|
|||
|
|
@ -41,14 +41,14 @@ class McpClientProtocolVersionTests {
|
|||
McpSchema.JSONRPCRequest request = transport.getLastSentMessageAsRequest();
|
||||
assertThat(request.params()).isInstanceOf(McpSchema.InitializeRequest.class);
|
||||
McpSchema.InitializeRequest initRequest = (McpSchema.InitializeRequest) request.params();
|
||||
assertThat(initRequest.protocolVersion()).isEqualTo(McpSchema.LATEST_PROTOCOL_VERSION);
|
||||
assertThat(initRequest.protocolVersion()).isEqualTo(transport.protocolVersion());
|
||||
|
||||
transport.simulateIncomingMessage(new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(),
|
||||
new McpSchema.InitializeResult(McpSchema.LATEST_PROTOCOL_VERSION, null,
|
||||
new McpSchema.InitializeResult(transport.protocolVersion(), null,
|
||||
new McpSchema.Implementation("test-server", "1.0.0"), null),
|
||||
null));
|
||||
}).assertNext(result -> {
|
||||
assertThat(result.protocolVersion()).isEqualTo(McpSchema.LATEST_PROTOCOL_VERSION);
|
||||
assertThat(result.protocolVersion()).isEqualTo(transport.protocolVersion());
|
||||
}).verifyComplete();
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ class McpServerProtocolVersionTests {
|
|||
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 @@ class McpServerProtocolVersionTests {
|
|||
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();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue