feat: broadcast 2025-11-25 as latest supported client version (#758)
* feat: broadcast 2025-11-25 as latest supported client version * fix: ignore empty SSE events
This commit is contained in:
parent
5ed6063a07
commit
ef8399c1cb
10 changed files with 43 additions and 22 deletions
|
|
@ -295,12 +295,23 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport {
|
||||||
if (statusCode >= 200 && statusCode < 300) {
|
if (statusCode >= 200 && statusCode < 300) {
|
||||||
|
|
||||||
if (MESSAGE_EVENT_TYPE.equals(responseEvent.sseEvent().event())) {
|
if (MESSAGE_EVENT_TYPE.equals(responseEvent.sseEvent().event())) {
|
||||||
|
String data = responseEvent.sseEvent().data();
|
||||||
|
// Per 2025-11-25 spec (SEP-1699), servers may
|
||||||
|
// send SSE events
|
||||||
|
// with empty data to prime the client for
|
||||||
|
// reconnection.
|
||||||
|
// Skip these events as they contain no JSON-RPC
|
||||||
|
// message.
|
||||||
|
if (data == null || data.isBlank()) {
|
||||||
|
logger.debug("Skipping SSE event with empty data (stream primer)");
|
||||||
|
return Flux.empty();
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
// We don't support batching ATM and probably
|
// We don't support batching ATM and probably
|
||||||
// won't since the next version considers
|
// won't since the next version considers
|
||||||
// removing it.
|
// removing it.
|
||||||
McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(
|
McpSchema.JSONRPCMessage message = McpSchema
|
||||||
this.jsonMapper, responseEvent.sseEvent().data());
|
.deserializeJsonRpcMessage(this.jsonMapper, data);
|
||||||
|
|
||||||
Tuple2<Optional<String>, Iterable<McpSchema.JSONRPCMessage>> idWithMessages = Tuples
|
Tuple2<Optional<String>, Iterable<McpSchema.JSONRPCMessage>> idWithMessages = Tuples
|
||||||
.of(Optional.ofNullable(responseEvent.sseEvent().id()),
|
.of(Optional.ofNullable(responseEvent.sseEvent().id()),
|
||||||
|
|
@ -503,13 +514,22 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport {
|
||||||
else if (contentType.contains(TEXT_EVENT_STREAM)) {
|
else if (contentType.contains(TEXT_EVENT_STREAM)) {
|
||||||
return Flux.just(((ResponseSubscribers.SseResponseEvent) responseEvent).sseEvent())
|
return Flux.just(((ResponseSubscribers.SseResponseEvent) responseEvent).sseEvent())
|
||||||
.flatMap(sseEvent -> {
|
.flatMap(sseEvent -> {
|
||||||
|
String data = sseEvent.data();
|
||||||
|
// Per 2025-11-25 spec (SEP-1699), servers may send SSE
|
||||||
|
// events
|
||||||
|
// with empty data to prime the client for reconnection.
|
||||||
|
// Skip these events as they contain no JSON-RPC message.
|
||||||
|
if (data == null || data.isBlank()) {
|
||||||
|
logger.debug("Skipping SSE event with empty data (stream primer)");
|
||||||
|
return Flux.empty();
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
// We don't support batching ATM and probably
|
// We don't support batching ATM and probably
|
||||||
// won't
|
// won't
|
||||||
// since the
|
// since the
|
||||||
// next version considers removing it.
|
// next version considers removing it.
|
||||||
McpSchema.JSONRPCMessage message = McpSchema
|
McpSchema.JSONRPCMessage message = McpSchema
|
||||||
.deserializeJsonRpcMessage(this.jsonMapper, sseEvent.data());
|
.deserializeJsonRpcMessage(this.jsonMapper, data);
|
||||||
|
|
||||||
Tuple2<Optional<String>, Iterable<McpSchema.JSONRPCMessage>> idWithMessages = Tuples
|
Tuple2<Optional<String>, Iterable<McpSchema.JSONRPCMessage>> idWithMessages = Tuples
|
||||||
.of(Optional.ofNullable(sseEvent.id()), List.of(message));
|
.of(Optional.ofNullable(sseEvent.id()), List.of(message));
|
||||||
|
|
@ -641,7 +661,7 @@ public class HttpClientStreamableHttpTransport implements McpClientTransport {
|
||||||
private Duration connectTimeout = Duration.ofSeconds(10);
|
private Duration connectTimeout = Duration.ofSeconds(10);
|
||||||
|
|
||||||
private List<String> supportedProtocolVersions = List.of(ProtocolVersions.MCP_2024_11_05,
|
private List<String> supportedProtocolVersions = List.of(ProtocolVersions.MCP_2024_11_05,
|
||||||
ProtocolVersions.MCP_2025_03_26, ProtocolVersions.MCP_2025_06_18);
|
ProtocolVersions.MCP_2025_03_26, ProtocolVersions.MCP_2025_06_18, ProtocolVersions.MCP_2025_11_25);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new builder with the specified base URI.
|
* Creates a new builder with the specified base URI.
|
||||||
|
|
|
||||||
|
|
@ -171,7 +171,7 @@ public class HttpServletStreamableServerTransportProvider extends HttpServlet
|
||||||
@Override
|
@Override
|
||||||
public List<String> protocolVersions() {
|
public List<String> protocolVersions() {
|
||||||
return List.of(ProtocolVersions.MCP_2024_11_05, ProtocolVersions.MCP_2025_03_26,
|
return List.of(ProtocolVersions.MCP_2024_11_05, ProtocolVersions.MCP_2025_03_26,
|
||||||
ProtocolVersions.MCP_2025_06_18);
|
ProtocolVersions.MCP_2025_06_18, ProtocolVersions.MCP_2025_11_25);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ public final class McpSchema {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated
|
@Deprecated
|
||||||
public static final String LATEST_PROTOCOL_VERSION = ProtocolVersions.MCP_2025_06_18;
|
public static final String LATEST_PROTOCOL_VERSION = ProtocolVersions.MCP_2025_11_25;
|
||||||
|
|
||||||
public static final String JSONRPC_VERSION = "2.0";
|
public static final String JSONRPC_VERSION = "2.0";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,8 @@ public interface McpStatelessServerTransport {
|
||||||
Mono<Void> closeGracefully();
|
Mono<Void> closeGracefully();
|
||||||
|
|
||||||
default List<String> protocolVersions() {
|
default List<String> protocolVersions() {
|
||||||
return List.of(ProtocolVersions.MCP_2025_03_26, ProtocolVersions.MCP_2025_06_18);
|
return List.of(ProtocolVersions.MCP_2025_03_26, ProtocolVersions.MCP_2025_06_18,
|
||||||
|
ProtocolVersions.MCP_2025_11_25);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ class HttpClientStreamableHttpTransportTest {
|
||||||
|
|
||||||
// Verify the customizer was called
|
// Verify the customizer was called
|
||||||
verify(mockRequestCustomizer, atLeastOnce()).customize(any(), eq("POST"), eq(uri), eq(
|
verify(mockRequestCustomizer, atLeastOnce()).customize(any(), eq("POST"), eq(uri), eq(
|
||||||
"{\"jsonrpc\":\"2.0\",\"method\":\"initialize\",\"id\":\"test-id\",\"params\":{\"protocolVersion\":\"2025-06-18\",\"capabilities\":{\"roots\":{\"listChanged\":true}},\"clientInfo\":{\"name\":\"MCP Client\",\"version\":\"0.3.1\"}}}"),
|
"{\"jsonrpc\":\"2.0\",\"method\":\"initialize\",\"id\":\"test-id\",\"params\":{\"protocolVersion\":\"2025-11-25\",\"capabilities\":{\"roots\":{\"listChanged\":true}},\"clientInfo\":{\"name\":\"MCP Client\",\"version\":\"0.3.1\"}}}"),
|
||||||
eq(context));
|
eq(context));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -120,7 +120,7 @@ class HttpClientStreamableHttpTransportTest {
|
||||||
|
|
||||||
// Verify the customizer was called
|
// Verify the customizer was called
|
||||||
verify(mockRequestCustomizer, atLeastOnce()).customize(any(), eq("POST"), eq(uri), eq(
|
verify(mockRequestCustomizer, atLeastOnce()).customize(any(), eq("POST"), eq(uri), eq(
|
||||||
"{\"jsonrpc\":\"2.0\",\"method\":\"initialize\",\"id\":\"test-id\",\"params\":{\"protocolVersion\":\"2025-06-18\",\"capabilities\":{\"roots\":{\"listChanged\":true}},\"clientInfo\":{\"name\":\"MCP Client\",\"version\":\"0.3.1\"}}}"),
|
"{\"jsonrpc\":\"2.0\",\"method\":\"initialize\",\"id\":\"test-id\",\"params\":{\"protocolVersion\":\"2025-11-25\",\"capabilities\":{\"roots\":{\"listChanged\":true}},\"clientInfo\":{\"name\":\"MCP Client\",\"version\":\"0.3.1\"}}}"),
|
||||||
eq(context));
|
eq(context));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -77,14 +77,14 @@ class HttpClientStreamableHttpVersionNegotiationIntegrationTests {
|
||||||
.hasSize(3)
|
.hasSize(3)
|
||||||
.map(McpTestRequestRecordingServletFilter.Call::headers)
|
.map(McpTestRequestRecordingServletFilter.Call::headers)
|
||||||
.allSatisfy(headers -> assertThat(headers).containsEntry("mcp-protocol-version",
|
.allSatisfy(headers -> assertThat(headers).containsEntry("mcp-protocol-version",
|
||||||
ProtocolVersions.MCP_2025_06_18));
|
ProtocolVersions.MCP_2025_11_25));
|
||||||
|
|
||||||
assertThat(response).isNotNull();
|
assertThat(response).isNotNull();
|
||||||
assertThat(response.content()).hasSize(1)
|
assertThat(response.content()).hasSize(1)
|
||||||
.first()
|
.first()
|
||||||
.extracting(McpSchema.TextContent.class::cast)
|
.extracting(McpSchema.TextContent.class::cast)
|
||||||
.extracting(McpSchema.TextContent::text)
|
.extracting(McpSchema.TextContent::text)
|
||||||
.isEqualTo(ProtocolVersions.MCP_2025_06_18);
|
.isEqualTo(ProtocolVersions.MCP_2025_11_25);
|
||||||
mcpServer.close();
|
mcpServer.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -93,7 +93,7 @@ class HttpClientStreamableHttpVersionNegotiationIntegrationTests {
|
||||||
startTomcat();
|
startTomcat();
|
||||||
|
|
||||||
var transport = HttpClientStreamableHttpTransport.builder("http://localhost:" + PORT)
|
var transport = HttpClientStreamableHttpTransport.builder("http://localhost:" + PORT)
|
||||||
.supportedProtocolVersions(List.of(ProtocolVersions.MCP_2025_06_18, "2263-03-18"))
|
.supportedProtocolVersions(List.of(ProtocolVersions.MCP_2025_11_25, "2263-03-18"))
|
||||||
.build();
|
.build();
|
||||||
var client = McpClient.sync(transport).build();
|
var client = McpClient.sync(transport).build();
|
||||||
|
|
||||||
|
|
@ -108,14 +108,14 @@ class HttpClientStreamableHttpVersionNegotiationIntegrationTests {
|
||||||
.hasSize(2)
|
.hasSize(2)
|
||||||
.map(McpTestRequestRecordingServletFilter.Call::headers)
|
.map(McpTestRequestRecordingServletFilter.Call::headers)
|
||||||
.allSatisfy(headers -> assertThat(headers).containsEntry("mcp-protocol-version",
|
.allSatisfy(headers -> assertThat(headers).containsEntry("mcp-protocol-version",
|
||||||
ProtocolVersions.MCP_2025_06_18));
|
ProtocolVersions.MCP_2025_11_25));
|
||||||
|
|
||||||
assertThat(response).isNotNull();
|
assertThat(response).isNotNull();
|
||||||
assertThat(response.content()).hasSize(1)
|
assertThat(response.content()).hasSize(1)
|
||||||
.first()
|
.first()
|
||||||
.extracting(McpSchema.TextContent.class::cast)
|
.extracting(McpSchema.TextContent.class::cast)
|
||||||
.extracting(McpSchema.TextContent::text)
|
.extracting(McpSchema.TextContent::text)
|
||||||
.isEqualTo(ProtocolVersions.MCP_2025_06_18);
|
.isEqualTo(ProtocolVersions.MCP_2025_11_25);
|
||||||
mcpServer.close();
|
mcpServer.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -518,7 +518,7 @@ public class WebClientStreamableHttpTransport implements McpClientTransport {
|
||||||
private boolean openConnectionOnStartup = false;
|
private boolean openConnectionOnStartup = false;
|
||||||
|
|
||||||
private List<String> supportedProtocolVersions = List.of(ProtocolVersions.MCP_2024_11_05,
|
private List<String> supportedProtocolVersions = List.of(ProtocolVersions.MCP_2024_11_05,
|
||||||
ProtocolVersions.MCP_2025_03_26, ProtocolVersions.MCP_2025_06_18);
|
ProtocolVersions.MCP_2025_03_26, ProtocolVersions.MCP_2025_06_18, ProtocolVersions.MCP_2025_11_25);
|
||||||
|
|
||||||
private Builder(WebClient.Builder webClientBuilder) {
|
private Builder(WebClient.Builder webClientBuilder) {
|
||||||
Assert.notNull(webClientBuilder, "WebClient.Builder must not be null");
|
Assert.notNull(webClientBuilder, "WebClient.Builder must not be null");
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,7 @@ public class WebFluxStreamableServerTransportProvider implements McpStreamableSe
|
||||||
@Override
|
@Override
|
||||||
public List<String> protocolVersions() {
|
public List<String> protocolVersions() {
|
||||||
return List.of(ProtocolVersions.MCP_2024_11_05, ProtocolVersions.MCP_2025_03_26,
|
return List.of(ProtocolVersions.MCP_2024_11_05, ProtocolVersions.MCP_2025_03_26,
|
||||||
ProtocolVersions.MCP_2025_06_18);
|
ProtocolVersions.MCP_2025_06_18, ProtocolVersions.MCP_2025_11_25);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -105,14 +105,14 @@ class WebFluxStreamableHttpVersionNegotiationIntegrationTests {
|
||||||
.hasSize(3)
|
.hasSize(3)
|
||||||
.map(McpTestRequestRecordingExchangeFilterFunction.Call::headers)
|
.map(McpTestRequestRecordingExchangeFilterFunction.Call::headers)
|
||||||
.allSatisfy(headers -> assertThat(headers).containsEntry("mcp-protocol-version",
|
.allSatisfy(headers -> assertThat(headers).containsEntry("mcp-protocol-version",
|
||||||
ProtocolVersions.MCP_2025_06_18));
|
ProtocolVersions.MCP_2025_11_25));
|
||||||
|
|
||||||
assertThat(response).isNotNull();
|
assertThat(response).isNotNull();
|
||||||
assertThat(response.content()).hasSize(1)
|
assertThat(response.content()).hasSize(1)
|
||||||
.first()
|
.first()
|
||||||
.extracting(McpSchema.TextContent.class::cast)
|
.extracting(McpSchema.TextContent.class::cast)
|
||||||
.extracting(McpSchema.TextContent::text)
|
.extracting(McpSchema.TextContent::text)
|
||||||
.isEqualTo(ProtocolVersions.MCP_2025_06_18);
|
.isEqualTo(ProtocolVersions.MCP_2025_11_25);
|
||||||
mcpServer.close();
|
mcpServer.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -120,7 +120,7 @@ class WebFluxStreamableHttpVersionNegotiationIntegrationTests {
|
||||||
void usesServerSupportedVersion() {
|
void usesServerSupportedVersion() {
|
||||||
var transport = WebClientStreamableHttpTransport
|
var transport = WebClientStreamableHttpTransport
|
||||||
.builder(WebClient.builder().baseUrl("http://localhost:" + PORT))
|
.builder(WebClient.builder().baseUrl("http://localhost:" + PORT))
|
||||||
.supportedProtocolVersions(List.of(ProtocolVersions.MCP_2025_06_18, "2263-03-18"))
|
.supportedProtocolVersions(List.of(ProtocolVersions.MCP_2025_11_25, "2263-03-18"))
|
||||||
.build();
|
.build();
|
||||||
var client = McpClient.sync(transport).requestTimeout(Duration.ofHours(10)).build();
|
var client = McpClient.sync(transport).requestTimeout(Duration.ofHours(10)).build();
|
||||||
|
|
||||||
|
|
@ -137,14 +137,14 @@ class WebFluxStreamableHttpVersionNegotiationIntegrationTests {
|
||||||
.hasSize(2)
|
.hasSize(2)
|
||||||
.map(McpTestRequestRecordingExchangeFilterFunction.Call::headers)
|
.map(McpTestRequestRecordingExchangeFilterFunction.Call::headers)
|
||||||
.allSatisfy(headers -> assertThat(headers).containsEntry("mcp-protocol-version",
|
.allSatisfy(headers -> assertThat(headers).containsEntry("mcp-protocol-version",
|
||||||
ProtocolVersions.MCP_2025_06_18));
|
ProtocolVersions.MCP_2025_11_25));
|
||||||
|
|
||||||
assertThat(response).isNotNull();
|
assertThat(response).isNotNull();
|
||||||
assertThat(response.content()).hasSize(1)
|
assertThat(response.content()).hasSize(1)
|
||||||
.first()
|
.first()
|
||||||
.extracting(McpSchema.TextContent.class::cast)
|
.extracting(McpSchema.TextContent.class::cast)
|
||||||
.extracting(McpSchema.TextContent::text)
|
.extracting(McpSchema.TextContent::text)
|
||||||
.isEqualTo(ProtocolVersions.MCP_2025_06_18);
|
.isEqualTo(ProtocolVersions.MCP_2025_11_25);
|
||||||
mcpServer.close();
|
mcpServer.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -157,7 +157,7 @@ public class WebMvcStreamableServerTransportProvider implements McpStreamableSer
|
||||||
@Override
|
@Override
|
||||||
public List<String> protocolVersions() {
|
public List<String> protocolVersions() {
|
||||||
return List.of(ProtocolVersions.MCP_2024_11_05, ProtocolVersions.MCP_2025_03_26,
|
return List.of(ProtocolVersions.MCP_2024_11_05, ProtocolVersions.MCP_2025_03_26,
|
||||||
ProtocolVersions.MCP_2025_06_18);
|
ProtocolVersions.MCP_2025_06_18, ProtocolVersions.MCP_2025_11_25);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue