Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -763,8 +763,8 @@ public static class Builder {

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, ProtocolVersions.MCP_2025_11_25);
private List<String> supportedProtocolVersions = List.of(ProtocolVersions.MCP_2025_03_26,
ProtocolVersions.MCP_2025_06_18, ProtocolVersions.MCP_2025_11_25);

private McpHttpClientTransportAuthorizationErrorHandler authorizationErrorHandler = McpHttpClientTransportAuthorizationErrorHandler.NOOP;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import io.modelcontextprotocol.spec.McpStreamableServerSession;
import io.modelcontextprotocol.spec.McpStreamableServerTransport;
import io.modelcontextprotocol.spec.McpStreamableServerTransportProvider;
import io.modelcontextprotocol.spec.ProtocolVersions;
import io.modelcontextprotocol.util.Assert;
import io.modelcontextprotocol.util.KeepAliveScheduler;
import jakarta.servlet.AsyncContext;
Expand Down Expand Up @@ -820,6 +821,12 @@ public void close() {

}

@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() {
return new Builder();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,30 @@
import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider;
import io.modelcontextprotocol.server.transport.McpTestRequestRecordingServletFilter;
import io.modelcontextprotocol.server.transport.TomcatTestUtil;
import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.ProtocolVersions;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.LifecycleState;
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 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 {

private Tomcat tomcat;
private static Tomcat tomcat;

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()
.contextExtractor(
req -> McpTransportContext.create(Map.of("protocol-version", req.getHeader("MCP-protocol-version"))))
Expand All @@ -51,78 +56,13 @@ class HttpClientStreamableHttpVersionNegotiationIntegrationTests {
.isError(false)
.build();

McpSyncServer mcpServer = McpServer.sync(transport)
private final McpSyncServer mcpServer = McpServer.sync(transport)
.capabilities(McpSchema.ServerCapabilities.builder().tools(false).build())
.tools(McpServerFeatures.SyncToolSpecification.builder().tool(toolSpec).callHandler(toolHandler).build())
.build();

@AfterEach
void tearDown() {
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() {
@BeforeAll
static void startTomcat() {
tomcat = TomcatTestUtil.createTomcatServer("", PORT, transport, requestRecordingFilter);
try {
tomcat.start();
Expand All @@ -133,7 +73,8 @@ private void startTomcat() {
}
}

private void stopTomcat() {
@AfterAll
static void stopTomcat() {
if (tomcat != null) {
try {
tomcat.stop();
Expand All @@ -145,4 +86,92 @@ private void stopTomcat() {
}
}

@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();
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ public List<Call> getCalls() {
return List.copyOf(calls);
}

public void clear() {
calls.clear();
}

public record Call(String method, Map<String, String> headers, String body) {

}
Expand Down
Loading