Remove 2024-11-05 protocol from Streamable HTTP transport
Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
This commit is contained in:
parent
39c225e46c
commit
735a85190b
4 changed files with 115 additions and 75 deletions
|
|
@ -763,8 +763,8 @@ 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_2025_03_26,
|
||||||
ProtocolVersions.MCP_2025_03_26, ProtocolVersions.MCP_2025_06_18, ProtocolVersions.MCP_2025_11_25);
|
ProtocolVersions.MCP_2025_06_18, ProtocolVersions.MCP_2025_11_25);
|
||||||
|
|
||||||
private McpHttpClientTransportAuthorizationErrorHandler authorizationErrorHandler = McpHttpClientTransportAuthorizationErrorHandler.NOOP;
|
private McpHttpClientTransportAuthorizationErrorHandler authorizationErrorHandler = McpHttpClientTransportAuthorizationErrorHandler.NOOP;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import io.modelcontextprotocol.spec.McpSchema;
|
||||||
import io.modelcontextprotocol.spec.McpStreamableServerSession;
|
import io.modelcontextprotocol.spec.McpStreamableServerSession;
|
||||||
import io.modelcontextprotocol.spec.McpStreamableServerTransport;
|
import io.modelcontextprotocol.spec.McpStreamableServerTransport;
|
||||||
import io.modelcontextprotocol.spec.McpStreamableServerTransportProvider;
|
import io.modelcontextprotocol.spec.McpStreamableServerTransportProvider;
|
||||||
|
import io.modelcontextprotocol.spec.ProtocolVersions;
|
||||||
import io.modelcontextprotocol.util.Assert;
|
import io.modelcontextprotocol.util.Assert;
|
||||||
import io.modelcontextprotocol.util.KeepAliveScheduler;
|
import io.modelcontextprotocol.util.KeepAliveScheduler;
|
||||||
import jakarta.servlet.AsyncContext;
|
import jakarta.servlet.AsyncContext;
|
||||||
|
|
@ -820,6 +821,12 @@ public class HttpServletStreamableServerTransportProvider extends HttpServlet
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> protocolVersions() {
|
||||||
|
return List.of(ProtocolVersions.MCP_2025_03_26, ProtocolVersions.MCP_2025_06_18,
|
||||||
|
ProtocolVersions.MCP_2025_11_25);
|
||||||
|
}
|
||||||
|
|
||||||
public static Builder builder() {
|
public static Builder builder() {
|
||||||
return new Builder();
|
return new Builder();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,25 +17,30 @@ import io.modelcontextprotocol.server.McpSyncServerExchange;
|
||||||
import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider;
|
import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider;
|
||||||
import io.modelcontextprotocol.server.transport.McpTestRequestRecordingServletFilter;
|
import io.modelcontextprotocol.server.transport.McpTestRequestRecordingServletFilter;
|
||||||
import io.modelcontextprotocol.server.transport.TomcatTestUtil;
|
import io.modelcontextprotocol.server.transport.TomcatTestUtil;
|
||||||
|
import io.modelcontextprotocol.spec.McpError;
|
||||||
import io.modelcontextprotocol.spec.McpSchema;
|
import io.modelcontextprotocol.spec.McpSchema;
|
||||||
import io.modelcontextprotocol.spec.ProtocolVersions;
|
import io.modelcontextprotocol.spec.ProtocolVersions;
|
||||||
import org.apache.catalina.LifecycleException;
|
import org.apache.catalina.LifecycleException;
|
||||||
import org.apache.catalina.LifecycleState;
|
import org.apache.catalina.LifecycleState;
|
||||||
import org.apache.catalina.startup.Tomcat;
|
import org.apache.catalina.startup.Tomcat;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterAll;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.assertj.core.api.InstanceOfAssertFactories.type;
|
||||||
|
|
||||||
class HttpClientStreamableHttpVersionNegotiationIntegrationTests {
|
class HttpClientStreamableHttpVersionNegotiationIntegrationTests {
|
||||||
|
|
||||||
private Tomcat tomcat;
|
private static Tomcat tomcat;
|
||||||
|
|
||||||
private static final int PORT = TomcatTestUtil.findAvailablePort();
|
private static final int PORT = TomcatTestUtil.findAvailablePort();
|
||||||
|
|
||||||
private final McpTestRequestRecordingServletFilter requestRecordingFilter = new McpTestRequestRecordingServletFilter();
|
private static final McpTestRequestRecordingServletFilter requestRecordingFilter = new McpTestRequestRecordingServletFilter();
|
||||||
|
|
||||||
private final HttpServletStreamableServerTransportProvider transport = HttpServletStreamableServerTransportProvider
|
private static final HttpServletStreamableServerTransportProvider transport = HttpServletStreamableServerTransportProvider
|
||||||
.builder()
|
.builder()
|
||||||
.contextExtractor(
|
.contextExtractor(
|
||||||
req -> McpTransportContext.create(Map.of("protocol-version", req.getHeader("MCP-protocol-version"))))
|
req -> McpTransportContext.create(Map.of("protocol-version", req.getHeader("MCP-protocol-version"))))
|
||||||
|
|
@ -51,78 +56,13 @@ class HttpClientStreamableHttpVersionNegotiationIntegrationTests {
|
||||||
.isError(false)
|
.isError(false)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
McpSyncServer mcpServer = McpServer.sync(transport)
|
private final McpSyncServer mcpServer = McpServer.sync(transport)
|
||||||
.capabilities(McpSchema.ServerCapabilities.builder().tools(false).build())
|
.capabilities(McpSchema.ServerCapabilities.builder().tools(false).build())
|
||||||
.tools(McpServerFeatures.SyncToolSpecification.builder().tool(toolSpec).callHandler(toolHandler).build())
|
.tools(McpServerFeatures.SyncToolSpecification.builder().tool(toolSpec).callHandler(toolHandler).build())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@AfterEach
|
@BeforeAll
|
||||||
void tearDown() {
|
static void startTomcat() {
|
||||||
stopTomcat();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void usesLatestVersion() {
|
|
||||||
startTomcat();
|
|
||||||
|
|
||||||
var client = McpClient.sync(HttpClientStreamableHttpTransport.builder("http://localhost:" + PORT).build())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
client.initialize();
|
|
||||||
McpSchema.CallToolResult response = client
|
|
||||||
.callTool(McpSchema.CallToolRequest.builder("test-tool").arguments(Map.of()).build());
|
|
||||||
|
|
||||||
var calls = requestRecordingFilter.getCalls();
|
|
||||||
|
|
||||||
assertThat(calls).filteredOn(c -> !c.body().contains("\"method\":\"initialize\""))
|
|
||||||
// GET /mcp ; POST notification/initialized ; POST tools/call
|
|
||||||
.hasSize(3)
|
|
||||||
.map(McpTestRequestRecordingServletFilter.Call::headers)
|
|
||||||
.allSatisfy(headers -> assertThat(headers).containsEntry("mcp-protocol-version",
|
|
||||||
ProtocolVersions.MCP_2025_11_25));
|
|
||||||
|
|
||||||
assertThat(response).isNotNull();
|
|
||||||
assertThat(response.content()).hasSize(1)
|
|
||||||
.first()
|
|
||||||
.extracting(McpSchema.TextContent.class::cast)
|
|
||||||
.extracting(McpSchema.TextContent::text)
|
|
||||||
.isEqualTo(ProtocolVersions.MCP_2025_11_25);
|
|
||||||
mcpServer.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void usesServerSupportedVersion() {
|
|
||||||
startTomcat();
|
|
||||||
|
|
||||||
var transport = HttpClientStreamableHttpTransport.builder("http://localhost:" + PORT)
|
|
||||||
.supportedProtocolVersions(List.of(ProtocolVersions.MCP_2025_11_25, "2263-03-18"))
|
|
||||||
.build();
|
|
||||||
var client = McpClient.sync(transport).build();
|
|
||||||
|
|
||||||
client.initialize();
|
|
||||||
McpSchema.CallToolResult response = client
|
|
||||||
.callTool(McpSchema.CallToolRequest.builder("test-tool").arguments(Map.of()).build());
|
|
||||||
|
|
||||||
var calls = requestRecordingFilter.getCalls();
|
|
||||||
// Initialize tells the server the Client's latest supported version
|
|
||||||
// FIXME: Set the correct protocol version on GET /mcp
|
|
||||||
assertThat(calls).filteredOn(c -> c.method().equals("POST") && !c.body().contains("\"method\":\"initialize\""))
|
|
||||||
// POST notification/initialized ; POST tools/call
|
|
||||||
.hasSize(2)
|
|
||||||
.map(McpTestRequestRecordingServletFilter.Call::headers)
|
|
||||||
.allSatisfy(headers -> assertThat(headers).containsEntry("mcp-protocol-version",
|
|
||||||
ProtocolVersions.MCP_2025_11_25));
|
|
||||||
|
|
||||||
assertThat(response).isNotNull();
|
|
||||||
assertThat(response.content()).hasSize(1)
|
|
||||||
.first()
|
|
||||||
.extracting(McpSchema.TextContent.class::cast)
|
|
||||||
.extracting(McpSchema.TextContent::text)
|
|
||||||
.isEqualTo(ProtocolVersions.MCP_2025_11_25);
|
|
||||||
mcpServer.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void startTomcat() {
|
|
||||||
tomcat = TomcatTestUtil.createTomcatServer("", PORT, transport, requestRecordingFilter);
|
tomcat = TomcatTestUtil.createTomcatServer("", PORT, transport, requestRecordingFilter);
|
||||||
try {
|
try {
|
||||||
tomcat.start();
|
tomcat.start();
|
||||||
|
|
@ -133,7 +73,8 @@ class HttpClientStreamableHttpVersionNegotiationIntegrationTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void stopTomcat() {
|
@AfterAll
|
||||||
|
static void stopTomcat() {
|
||||||
if (tomcat != null) {
|
if (tomcat != null) {
|
||||||
try {
|
try {
|
||||||
tomcat.stop();
|
tomcat.stop();
|
||||||
|
|
@ -145,4 +86,92 @@ class HttpClientStreamableHttpVersionNegotiationIntegrationTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
requestRecordingFilter.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void usesLatestVersion() {
|
||||||
|
try (var client = McpClient.sync(HttpClientStreamableHttpTransport.builder("http://localhost:" + PORT).build())
|
||||||
|
.build()) {
|
||||||
|
client.initialize();
|
||||||
|
McpSchema.CallToolResult response = client
|
||||||
|
.callTool(McpSchema.CallToolRequest.builder("test-tool").arguments(Map.of()).build());
|
||||||
|
|
||||||
|
var calls = requestRecordingFilter.getCalls();
|
||||||
|
|
||||||
|
assertThat(calls).filteredOn(c -> !c.body().contains("\"method\":\"initialize\""))
|
||||||
|
// GET /mcp ; POST notification/initialized ; POST tools/call
|
||||||
|
.hasSize(3)
|
||||||
|
.map(McpTestRequestRecordingServletFilter.Call::headers)
|
||||||
|
.allSatisfy(headers -> assertThat(headers).containsEntry("mcp-protocol-version",
|
||||||
|
ProtocolVersions.MCP_2025_11_25));
|
||||||
|
|
||||||
|
assertThat(response).isNotNull();
|
||||||
|
assertThat(response.content()).hasSize(1)
|
||||||
|
.first()
|
||||||
|
.extracting(McpSchema.TextContent.class::cast)
|
||||||
|
.extracting(McpSchema.TextContent::text)
|
||||||
|
.isEqualTo(ProtocolVersions.MCP_2025_11_25);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void usesServerSupportedVersion() {
|
||||||
|
var transport = HttpClientStreamableHttpTransport.builder("http://localhost:" + PORT)
|
||||||
|
.supportedProtocolVersions(List.of(ProtocolVersions.MCP_2025_11_25, "2263-03-18"))
|
||||||
|
.build();
|
||||||
|
try (var client = McpClient.sync(transport).build()) {
|
||||||
|
client.initialize();
|
||||||
|
McpSchema.CallToolResult response = client
|
||||||
|
.callTool(McpSchema.CallToolRequest.builder("test-tool").arguments(Map.of()).build());
|
||||||
|
|
||||||
|
var calls = requestRecordingFilter.getCalls();
|
||||||
|
// Initialize tells the server the Client's latest supported version
|
||||||
|
// FIXME: Set the correct protocol version on GET /mcp
|
||||||
|
assertThat(calls)
|
||||||
|
.filteredOn(c -> c.method().equals("POST") && !c.body().contains("\"method\":\"initialize\""))
|
||||||
|
// POST notification/initialized ; POST tools/call
|
||||||
|
.hasSize(2)
|
||||||
|
.map(McpTestRequestRecordingServletFilter.Call::headers)
|
||||||
|
.allSatisfy(headers -> assertThat(headers).containsEntry("mcp-protocol-version",
|
||||||
|
ProtocolVersions.MCP_2025_11_25));
|
||||||
|
|
||||||
|
assertThat(response).isNotNull();
|
||||||
|
assertThat(response.content()).hasSize(1)
|
||||||
|
.first()
|
||||||
|
.extracting(McpSchema.TextContent.class::cast)
|
||||||
|
.extracting(McpSchema.TextContent::text)
|
||||||
|
.isEqualTo(ProtocolVersions.MCP_2025_11_25);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void clientDoesNotSupportProtocolVersion20241105ByDefault() {
|
||||||
|
var transport = HttpClientStreamableHttpTransport.builder("http://localhost:" + PORT).build();
|
||||||
|
|
||||||
|
// Testing it on the wire would require building a custom transport
|
||||||
|
// We trust the protocolVersions() accessor instead
|
||||||
|
assertThat(transport.protocolVersions()).containsExactly(ProtocolVersions.MCP_2025_03_26,
|
||||||
|
ProtocolVersions.MCP_2025_06_18, ProtocolVersions.MCP_2025_11_25);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void serverDoesNotSupportProtocolVersion20241105() {
|
||||||
|
var clientTransport = HttpClientStreamableHttpTransport.builder("http://localhost:" + PORT)
|
||||||
|
.supportedProtocolVersions(List.of(ProtocolVersions.MCP_2024_11_05))
|
||||||
|
.build();
|
||||||
|
try (var client = McpClient.sync(clientTransport).build()) {
|
||||||
|
assertThatThrownBy(client::initialize).rootCause()
|
||||||
|
.isInstanceOf(McpError.class)
|
||||||
|
.asInstanceOf(type(McpError.class))
|
||||||
|
.extracting(Throwable::getMessage)
|
||||||
|
.isEqualTo("Unsupported protocol version");
|
||||||
|
mcpServer.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,10 @@ public class McpTestRequestRecordingServletFilter implements Filter {
|
||||||
return List.copyOf(calls);
|
return List.copyOf(calls);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void clear() {
|
||||||
|
calls.clear();
|
||||||
|
}
|
||||||
|
|
||||||
public record Call(String method, Map<String, String> headers, String body) {
|
public record Call(String method, Map<String, String> headers, String body) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue