Basic version negotiation capabilities
- We do not support full version negotiation, but this commit allows the client to the send the correct version to match that of the server. - There is one limitation: the GET /mcp request happens too early, before we have deserialized the initialization response, so we do not know the negotiated version yet. This request will have the client latest version as Mcp-Protocol-Version value. Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
This commit is contained in:
parent
7e950ebb6c
commit
e6045f7287
3 changed files with 53 additions and 27 deletions
|
|
@ -24,6 +24,7 @@ import org.springframework.web.reactive.function.client.ClientResponse;
|
|||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.springframework.web.reactive.function.client.WebClientResponseException;
|
||||
|
||||
import io.modelcontextprotocol.client.McpAsyncClient;
|
||||
import io.modelcontextprotocol.json.McpJsonMapper;
|
||||
import io.modelcontextprotocol.json.TypeRef;
|
||||
import io.modelcontextprotocol.spec.ClosedMcpTransportSession;
|
||||
|
|
@ -225,7 +226,9 @@ public class WebClientStreamableHttpTransport implements McpClientTransport {
|
|||
Disposable connection = webClient.get()
|
||||
.uri(this.endpoint)
|
||||
.accept(MediaType.TEXT_EVENT_STREAM)
|
||||
.header(HttpHeaders.PROTOCOL_VERSION, this.latestSupportedProtocolVersion)
|
||||
.header(HttpHeaders.PROTOCOL_VERSION,
|
||||
ctx.getOrDefault(McpAsyncClient.NEGOTIATED_PROTOCOL_VERSION,
|
||||
this.latestSupportedProtocolVersion))
|
||||
.headers(httpHeaders -> {
|
||||
transportSession.sessionId().ifPresent(id -> httpHeaders.add(HttpHeaders.MCP_SESSION_ID, id));
|
||||
if (stream != null) {
|
||||
|
|
@ -288,10 +291,12 @@ public class WebClientStreamableHttpTransport implements McpClientTransport {
|
|||
final AtomicReference<Disposable> disposableRef = new AtomicReference<>();
|
||||
final McpTransportSession<Disposable> transportSession = this.activeSession.get();
|
||||
|
||||
Disposable connection = webClient.post()
|
||||
Disposable connection = Flux.deferContextual(ctx -> webClient.post()
|
||||
.uri(this.endpoint)
|
||||
.accept(MediaType.APPLICATION_JSON, MediaType.TEXT_EVENT_STREAM)
|
||||
.header(HttpHeaders.PROTOCOL_VERSION, this.latestSupportedProtocolVersion)
|
||||
.header(HttpHeaders.PROTOCOL_VERSION,
|
||||
ctx.getOrDefault(McpAsyncClient.NEGOTIATED_PROTOCOL_VERSION,
|
||||
this.latestSupportedProtocolVersion))
|
||||
.headers(httpHeaders -> {
|
||||
transportSession.sessionId().ifPresent(id -> httpHeaders.add(HttpHeaders.MCP_SESSION_ID, id));
|
||||
})
|
||||
|
|
@ -350,7 +355,7 @@ public class WebClientStreamableHttpTransport implements McpClientTransport {
|
|||
}
|
||||
return this.extractError(response, sessionRepresentation);
|
||||
}
|
||||
})
|
||||
}))
|
||||
.flatMap(jsonRpcMessage -> this.handler.get().apply(Mono.just(jsonRpcMessage)))
|
||||
.onErrorComplete(t -> {
|
||||
// handle the error first
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import org.junit.jupiter.api.Test;
|
|||
import reactor.netty.DisposableServer;
|
||||
import reactor.netty.http.server.HttpServer;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.server.reactive.HttpHandler;
|
||||
import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
|
@ -116,7 +117,7 @@ class WebFluxStreamableHttpVersionNegotiationIntegrationTests {
|
|||
}
|
||||
|
||||
@Test
|
||||
void usesCustomLatestVersion() {
|
||||
void usesServerSupportedVersion() {
|
||||
var transport = WebClientStreamableHttpTransport
|
||||
.builder(WebClient.builder().baseUrl("http://localhost:" + PORT))
|
||||
.supportedProtocolVersions(List.of(ProtocolVersions.MCP_2025_06_18, "2263-03-18"))
|
||||
|
|
@ -128,18 +129,22 @@ class WebFluxStreamableHttpVersionNegotiationIntegrationTests {
|
|||
McpSchema.CallToolResult response = client.callTool(new McpSchema.CallToolRequest("test-tool", Map.of()));
|
||||
|
||||
var calls = recordingFilterFunction.getCalls();
|
||||
assertThat(calls).filteredOn(c -> !c.body().contains("\"method\":\"initialize\""))
|
||||
// GET /mcp ; POST notification/initialized ; POST tools/call
|
||||
.hasSize(3)
|
||||
// Initialize tells the server the Client's latest supported version
|
||||
// FIXME: Set the correct protocol version on GET /mcp
|
||||
assertThat(calls)
|
||||
.filteredOn(c -> !c.body().contains("\"method\":\"initialize\"") && c.method().equals(HttpMethod.POST))
|
||||
// POST notification/initialized ; POST tools/call
|
||||
.hasSize(2)
|
||||
.map(McpTestRequestRecordingExchangeFilterFunction.Call::headers)
|
||||
.allSatisfy(headers -> assertThat(headers).containsEntry("mcp-protocol-version", "2263-03-18"));
|
||||
.allSatisfy(headers -> assertThat(headers).containsEntry("mcp-protocol-version",
|
||||
ProtocolVersions.MCP_2025_06_18));
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.content()).hasSize(1)
|
||||
.first()
|
||||
.extracting(McpSchema.TextContent.class::cast)
|
||||
.extracting(McpSchema.TextContent::text)
|
||||
.isEqualTo("2263-03-18");
|
||||
.isEqualTo(ProtocolVersions.MCP_2025_06_18);
|
||||
mcpServer.close();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import java.util.stream.Collectors;
|
|||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.web.reactive.function.server.HandlerFilterFunction;
|
||||
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
|
|
@ -34,7 +35,7 @@ public class McpTestRequestRecordingExchangeFilterFunction implements HandlerFil
|
|||
.collect(Collectors.toMap(String::toLowerCase, k -> String.join(",", request.headers().header(k))));
|
||||
|
||||
var cr = request.bodyToMono(String.class).defaultIfEmpty("").map(body -> {
|
||||
this.calls.add(new Call(headers, body));
|
||||
this.calls.add(new Call(request.method(), headers, body));
|
||||
return ServerRequest.from(request).body(body).build();
|
||||
});
|
||||
|
||||
|
|
@ -46,7 +47,7 @@ public class McpTestRequestRecordingExchangeFilterFunction implements HandlerFil
|
|||
return List.copyOf(calls);
|
||||
}
|
||||
|
||||
public record Call(Map<String, String> headers, String body) {
|
||||
public record Call(HttpMethod method, Map<String, String> headers, String body) {
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue