Client transports: make #protocolVersions() configurable (#669)

Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
This commit is contained in:
Daniel Garnier-Moiroux 2025-11-18 14:53:50 +01:00 • committed by GitHub
commit 87bdf1ee49
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -11,6 +11,8 @@ import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandler;
import java.time.Duration;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletionException;
@ -18,17 +20,12 @@ import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Function;
import org.reactivestreams.Publisher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.modelcontextprotocol.json.TypeRef;
import io.modelcontextprotocol.json.McpJsonMapper;
import io.modelcontextprotocol.client.transport.ResponseSubscribers.ResponseEvent;
import io.modelcontextprotocol.client.transport.customizer.McpAsyncHttpClientRequestCustomizer;
import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpClientRequestCustomizer;
import io.modelcontextprotocol.client.transport.ResponseSubscribers.ResponseEvent;
import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.json.McpJsonMapper;
import io.modelcontextprotocol.json.TypeRef;
import io.modelcontextprotocol.spec.ClosedMcpTransportSession;
import io.modelcontextprotocol.spec.DefaultMcpTransportSession;
import io.modelcontextprotocol.spec.DefaultMcpTransportStream;
@ -42,6 +39,9 @@ import io.modelcontextprotocol.spec.McpTransportStream;
import io.modelcontextprotocol.spec.ProtocolVersions;
import io.modelcontextprotocol.util.Assert;
import io.modelcontextprotocol.util.Utils;
import org.reactivestreams.Publisher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.Disposable;
import reactor.core.publisher.Flux;
import reactor.core.publisher.FluxSink;
@ -78,8 +78,6 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport {
private static final Logger logger = LoggerFactory.getLogger(HttpClientStreamableHttpTransport.class);
private static final String MCP_PROTOCOL_VERSION = ProtocolVersions.MCP_2025_06_18;
private static final String DEFAULT_ENDPOINT = "/mcp";
/**
@ -125,9 +123,14 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport {
private final AtomicReference<Consumer<Throwable>> exceptionHandler = new AtomicReference<>();
private final List<String> supportedProtocolVersions;
private final String latestSupportedProtocolVersion;
private HttpClientStreamableHttpTransport(McpJsonMapper jsonMapper, HttpClient httpClient,
HttpRequest.Builder requestBuilder, String baseUri, String endpoint, boolean resumableStreams,
boolean openConnectionOnStartup, McpAsyncHttpClientRequestCustomizer httpRequestCustomizer) {
boolean openConnectionOnStartup, McpAsyncHttpClientRequestCustomizer httpRequestCustomizer,
List<String> supportedProtocolVersions) {
this.jsonMapper = jsonMapper;
this.httpClient = httpClient;
this.requestBuilder = requestBuilder;
@ -137,12 +140,16 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport {
this.openConnectionOnStartup = openConnectionOnStartup;
this.activeSession.set(createTransportSession());
this.httpRequestCustomizer = httpRequestCustomizer;
this.supportedProtocolVersions = Collections.unmodifiableList(supportedProtocolVersions);
this.latestSupportedProtocolVersion = this.supportedProtocolVersions.stream()
.sorted(Comparator.reverseOrder())
.findFirst()
.get();
}
@Override
public List<String> protocolVersions() {
return List.of(ProtocolVersions.MCP_2024_11_05, ProtocolVersions.MCP_2025_03_26,
ProtocolVersions.MCP_2025_06_18);
return supportedProtocolVersions;
}
public static Builder builder(String baseUri) {
@ -186,7 +193,7 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport {
.uri(uri)
.header("Cache-Control", "no-cache")
.header(HttpHeaders.MCP_SESSION_ID, sessionId)
.header(HttpHeaders.PROTOCOL_VERSION, MCP_PROTOCOL_VERSION)
.header(HttpHeaders.PROTOCOL_VERSION, this.latestSupportedProtocolVersion)
.DELETE();
var transportContext = ctx.getOrDefault(McpTransportContext.KEY, McpTransportContext.EMPTY);
return Mono.from(this.httpRequestCustomizer.customize(builder, "DELETE", uri, null, transportContext));
@ -257,7 +264,7 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport {
var builder = requestBuilder.uri(uri)
.header(HttpHeaders.ACCEPT, TEXT_EVENT_STREAM)
.header("Cache-Control", "no-cache")
.header(HttpHeaders.PROTOCOL_VERSION, MCP_PROTOCOL_VERSION)
.header(HttpHeaders.PROTOCOL_VERSION, this.latestSupportedProtocolVersion)
.GET();
var transportContext = connectionCtx.getOrDefault(McpTransportContext.KEY, McpTransportContext.EMPTY);
return Mono.from(this.httpRequestCustomizer.customize(builder, "GET", uri, null, transportContext));
@ -432,7 +439,7 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport {
.header(HttpHeaders.ACCEPT, APPLICATION_JSON + ", " + TEXT_EVENT_STREAM)
.header(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON)
.header(HttpHeaders.CACHE_CONTROL, "no-cache")
.header(HttpHeaders.PROTOCOL_VERSION, MCP_PROTOCOL_VERSION)
.header(HttpHeaders.PROTOCOL_VERSION, this.latestSupportedProtocolVersion)
.POST(HttpRequest.BodyPublishers.ofString(jsonBody));
var transportContext = ctx.getOrDefault(McpTransportContext.KEY, McpTransportContext.EMPTY);
return Mono
@ -624,6 +631,9 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport {
private Duration connectTimeout = Duration.ofSeconds(10);
private List<String> supportedProtocolVersions = List.of(ProtocolVersions.MCP_2024_11_05,
ProtocolVersions.MCP_2025_03_26, ProtocolVersions.MCP_2025_06_18);
/**
* Creates a new builder with the specified base URI.
* @param baseUri the base URI of the MCP server
@ -772,6 +782,30 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport {
return this;
}
/**
* Sets the list of supported protocol versions used in version negotiation. By
* default, the client will send the latest of those versions in the
* {@code MCP-Protocol-Version} header.
* <p>
* Setting this value only updates the values used in version negotiation, and
* does NOT impact the actual capabilities of the transport. It should only be
* used for compatibility with servers having strict requirements around the
* {@code MCP-Protocol-Version} header.
* @param supportedProtocolVersions protocol versions supported by this transport
* @return this builder
* @see <a href=
* "https://modelcontextprotocol.io/specification/2024-11-05/basic/lifecycle#version-negotiation">version
* negotiation specification</a>
* @see <a href=
* "https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#protocol-version-header">Protocol
* Version Header</a>
*/
public Builder supportedProtocolVersions(List<String> supportedProtocolVersions) {
Assert.notEmpty(supportedProtocolVersions, "supportedProtocolVersions must not be empty");
this.supportedProtocolVersions = Collections.unmodifiableList(supportedProtocolVersions);
return this;
}
/**
* Construct a fresh instance of {@link HttpClientStreamableHttpTransport} using
* the current builder configuration.
@ -781,7 +815,7 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport {
HttpClient httpClient = this.clientBuilder.connectTimeout(this.connectTimeout).build();
return new HttpClientStreamableHttpTransport(jsonMapper == null ? McpJsonMapper.getDefault() : jsonMapper,
httpClient, requestBuilder, baseUri, endpoint, resumableStreams, openConnectionOnStartup,
httpRequestCustomizer);
httpRequestCustomizer, supportedProtocolVersions);
}
}