diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/AbstractMcpClientServerIntegrationTests.java b/mcp-test/src/main/java/io/modelcontextprotocol/AbstractMcpClientServerIntegrationTests.java index f6b547a..305319b 100644 --- a/mcp-test/src/main/java/io/modelcontextprotocol/AbstractMcpClientServerIntegrationTests.java +++ b/mcp-test/src/main/java/io/modelcontextprotocol/AbstractMcpClientServerIntegrationTests.java @@ -12,6 +12,7 @@ import java.time.Duration; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -49,10 +50,11 @@ import io.modelcontextprotocol.spec.McpSchema.Root; import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities; import io.modelcontextprotocol.spec.McpSchema.TextContent; import io.modelcontextprotocol.spec.McpSchema.Tool; -import io.modelcontextprotocol.spec.McpTransportException; import io.modelcontextprotocol.util.Utils; import net.javacrumbs.jsonunit.core.Option; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; import reactor.core.publisher.Mono; import static io.modelcontextprotocol.util.ToolsUtils.EMPTY_JSON_SCHEMA; @@ -72,20 +74,26 @@ public abstract class AbstractMcpClientServerIntegrationTests { protected static final int MAX_REQUEST_SIZE = 2048; + protected ConcurrentHashMap clientBuilders = new ConcurrentHashMap<>(); + + abstract protected void prepareClients(int port, String mcpEndpoint); + abstract protected McpServer.AsyncSpecification prepareAsyncServerBuilder(); abstract protected McpServer.SyncSpecification prepareSyncServerBuilder(); - abstract protected McpClient.SyncSpec getMcpClientBuilder(); + @ParameterizedTest(name = "{0} : {displayName} ") + @MethodSource("clientsForTesting") + void simple(String clientType) { + + var clientBuilder = clientBuilders.get(clientType); - @Test - void simple() { var server = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0") .requestTimeout(Duration.ofSeconds(1000)) .build(); try ( // Create client without sampling capabilities - var client = getMcpClientBuilder() + var client = clientBuilder .clientInfo(McpSchema.Implementation.builder("Sample " + "client", "0.0.0").build()) .requestTimeout(Duration.ofSeconds(1000)) .build()) { @@ -101,8 +109,12 @@ public abstract class AbstractMcpClientServerIntegrationTests { // --------------------------------------- // Sampling Tests // --------------------------------------- - @Test - void testCreateMessageWithoutSamplingCapabilities() { + @ParameterizedTest(name = "{0} : {displayName} ") + @MethodSource("clientsForTesting") + void testCreateMessageWithoutSamplingCapabilities(String clientType) { + + var clientBuilder = clientBuilders.get(clientType); + McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() .tool(Tool.builder("tool1", EMPTY_JSON_SCHEMA).description("tool1 description").build()) .callHandler((exchange, request) -> { @@ -115,7 +127,7 @@ public abstract class AbstractMcpClientServerIntegrationTests { try ( // Create client without sampling capabilities - var client = getMcpClientBuilder() + var client = clientBuilder .clientInfo(McpSchema.Implementation.builder("Sample " + "client", "0.0.0").build()) .build()) { @@ -134,8 +146,12 @@ public abstract class AbstractMcpClientServerIntegrationTests { } } - @Test - void testCreateMessageSuccess() { + @ParameterizedTest(name = "{0} : {displayName} ") + @MethodSource("clientsForTesting") + void testCreateMessageSuccess(String clientType) { + + var clientBuilder = clientBuilders.get(clientType); + Function samplingHandler = request -> { assertThat(request.messages()).hasSize(1); assertThat(request.messages().get(0).content()).isInstanceOf(McpSchema.TextContent.class); @@ -176,7 +192,7 @@ public abstract class AbstractMcpClientServerIntegrationTests { var mcpServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").tools(tool).build(); - try (var mcpClient = getMcpClientBuilder() + try (var mcpClient = clientBuilder .clientInfo(McpSchema.Implementation.builder("Sample client", "0.0.0").build()) .capabilities(ClientCapabilities.builder().sampling().build()) .sampling(samplingHandler) @@ -205,8 +221,14 @@ public abstract class AbstractMcpClientServerIntegrationTests { } } - @Test - void testCreateMessageWithRequestTimeoutSuccess() { + @ParameterizedTest(name = "{0} : {displayName} ") + @MethodSource("clientsForTesting") + void testCreateMessageWithRequestTimeoutSuccess(String clientType) throws InterruptedException { + + // Client + + var clientBuilder = clientBuilders.get(clientType); + Function samplingHandler = request -> { assertThat(request.messages()).hasSize(1); assertThat(request.messages().get(0).content()).isInstanceOf(McpSchema.TextContent.class); @@ -256,7 +278,7 @@ public abstract class AbstractMcpClientServerIntegrationTests { .requestTimeout(Duration.ofSeconds(4)) .tools(tool) .build(); - try (var mcpClient = getMcpClientBuilder() + try (var mcpClient = clientBuilder .clientInfo(McpSchema.Implementation.builder("Sample client", "0.0.0").build()) .capabilities(ClientCapabilities.builder().sampling().build()) .sampling(samplingHandler) @@ -285,8 +307,12 @@ public abstract class AbstractMcpClientServerIntegrationTests { } } - @Test - void testCreateMessageWithRequestTimeoutFail() { + @ParameterizedTest(name = "{0} : {displayName} ") + @MethodSource("clientsForTesting") + void testCreateMessageWithRequestTimeoutFail(String clientType) throws InterruptedException { + + var clientBuilder = clientBuilders.get(clientType); + Function samplingHandler = request -> { assertThat(request.messages()).hasSize(1); assertThat(request.messages().get(0).content()).isInstanceOf(McpSchema.TextContent.class); @@ -331,7 +357,7 @@ public abstract class AbstractMcpClientServerIntegrationTests { .tools(tool) .build(); - try (var mcpClient = getMcpClientBuilder() + try (var mcpClient = clientBuilder .clientInfo(McpSchema.Implementation.builder("Sample client", "0.0.0").build()) .capabilities(ClientCapabilities.builder().sampling().build()) .sampling(samplingHandler) @@ -352,8 +378,12 @@ public abstract class AbstractMcpClientServerIntegrationTests { // --------------------------------------- // Elicitation Tests // --------------------------------------- - @Test - void testCreateElicitationWithoutElicitationCapabilities() { + @ParameterizedTest(name = "{0} : {displayName} ") + @MethodSource("clientsForTesting") + void testCreateElicitationWithoutElicitationCapabilities(String clientType) { + + var clientBuilder = clientBuilders.get(clientType); + McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() .tool(Tool.builder("tool1", EMPTY_JSON_SCHEMA).description("tool1 description").build()) .callHandler((exchange, request) -> exchange.createElicitation(mock(McpSchema.ElicitFormRequest.class)) @@ -363,8 +393,7 @@ public abstract class AbstractMcpClientServerIntegrationTests { var server = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").tools(tool).build(); // Create client without elicitation capabilities - try (var client = getMcpClientBuilder() - .clientInfo(McpSchema.Implementation.builder("Sample client", "0.0.0").build()) + try (var client = clientBuilder.clientInfo(McpSchema.Implementation.builder("Sample client", "0.0.0").build()) .build()) { assertThat(client.initialize()).isNotNull(); @@ -382,8 +411,12 @@ public abstract class AbstractMcpClientServerIntegrationTests { } } - @Test - void testCreateElicitationSuccess() { + @ParameterizedTest(name = "{0} : {displayName} ") + @MethodSource("clientsForTesting") + void testCreateElicitationSuccess(String clientType) { + + var clientBuilder = clientBuilders.get(clientType); + Function formElicitationHandler = request -> { assertThat(request.message()).isNotEmpty(); assertThat(request.requestedSchema()).isNotNull(); @@ -416,7 +449,7 @@ public abstract class AbstractMcpClientServerIntegrationTests { var mcpServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").tools(tool).build(); - try (var mcpClient = getMcpClientBuilder() + try (var mcpClient = clientBuilder .clientInfo(McpSchema.Implementation.builder("Sample client", "0.0.0").build()) .capabilities(ClientCapabilities.builder().elicitation().build()) .elicitation(formElicitationHandler) @@ -441,8 +474,12 @@ public abstract class AbstractMcpClientServerIntegrationTests { } } - @Test - void testCreateElicitationWithApplyDefaults() { + @ParameterizedTest(name = "{0} : {displayName} ") + @MethodSource("clientsForTesting") + void testCreateElicitationWithApplyDefaults(String clientType) { + + var clientBuilder = clientBuilders.get(clientType); + // Client handler returns empty content — SDK should apply defaults Function elicitationHandler = request -> { assertThat(request.message()).isNotEmpty(); @@ -476,7 +513,7 @@ public abstract class AbstractMcpClientServerIntegrationTests { var mcpServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").tools(tool).build(); - try (var mcpClient = getMcpClientBuilder().clientInfo(new McpSchema.Implementation("Sample client", "0.0.0")) + try (var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0")) .capabilities(ClientCapabilities.builder().elicitation(true, false).build()) .elicitation(elicitationHandler) .applyElicitationDefaults(true) @@ -502,8 +539,12 @@ public abstract class AbstractMcpClientServerIntegrationTests { } } - @Test - void testCreateElicitationWithApplyDefaultsAndUnmodifiableMap() { + @ParameterizedTest(name = "{0} : {displayName} ") + @MethodSource("clientsForTesting") + void testCreateElicitationWithApplyDefaultsAndUnmodifiableMap(String clientType) { + + var clientBuilder = clientBuilders.get(clientType); + // Client handler returns an unmodifiable map (Map.of()) — SDK must copy into a // mutable map before applying defaults. Function elicitationHandler = request -> new McpSchema.ElicitResult( @@ -534,7 +575,7 @@ public abstract class AbstractMcpClientServerIntegrationTests { var mcpServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").tools(tool).build(); - try (var mcpClient = getMcpClientBuilder().clientInfo(new McpSchema.Implementation("Sample client", "0.0.0")) + try (var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0")) .capabilities(ClientCapabilities.builder().elicitation(true, false).build()) .elicitation(elicitationHandler) .applyElicitationDefaults(true) @@ -558,8 +599,12 @@ public abstract class AbstractMcpClientServerIntegrationTests { } } - @Test - void testCreateElicitationApplyDefaultsDisabledLeavesContentUntouched() { + @ParameterizedTest(name = "{0} : {displayName} ") + @MethodSource("clientsForTesting") + void testCreateElicitationApplyDefaultsDisabledLeavesContentUntouched(String clientType) { + + var clientBuilder = clientBuilders.get(clientType); + Function elicitationHandler = request -> new McpSchema.ElicitResult( McpSchema.ElicitResult.Action.ACCEPT, new HashMap<>()); @@ -586,7 +631,7 @@ public abstract class AbstractMcpClientServerIntegrationTests { var mcpServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").tools(tool).build(); // applyElicitationDefaults intentionally NOT called — default false. - try (var mcpClient = getMcpClientBuilder().clientInfo(new McpSchema.Implementation("Sample client", "0.0.0")) + try (var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0")) .capabilities(ClientCapabilities.builder().elicitation(true, false).build()) .elicitation(elicitationHandler) .build()) { @@ -608,8 +653,12 @@ public abstract class AbstractMcpClientServerIntegrationTests { } } - @Test - void testCreateElicitationApplyDefaultsSkippedOnDecline() { + @ParameterizedTest(name = "{0} : {displayName} ") + @MethodSource("clientsForTesting") + void testCreateElicitationApplyDefaultsSkippedOnDecline(String clientType) { + + var clientBuilder = clientBuilders.get(clientType); + Function elicitationHandler = request -> new McpSchema.ElicitResult( McpSchema.ElicitResult.Action.DECLINE, new HashMap<>()); @@ -635,7 +684,7 @@ public abstract class AbstractMcpClientServerIntegrationTests { var mcpServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").tools(tool).build(); - try (var mcpClient = getMcpClientBuilder().clientInfo(new McpSchema.Implementation("Sample client", "0.0.0")) + try (var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0")) .capabilities(ClientCapabilities.builder().elicitation(true, false).build()) .elicitation(elicitationHandler) .applyElicitationDefaults(true) @@ -658,8 +707,12 @@ public abstract class AbstractMcpClientServerIntegrationTests { } } - @Test - void testCreateElicitationApplyDefaultsPreservesMeta() { + @ParameterizedTest(name = "{0} : {displayName} ") + @MethodSource("clientsForTesting") + void testCreateElicitationApplyDefaultsPreservesMeta(String clientType) { + + var clientBuilder = clientBuilders.get(clientType); + Map meta = Map.of("trace-id", "abc-123"); Function elicitationHandler = request -> new McpSchema.ElicitResult( McpSchema.ElicitResult.Action.ACCEPT, new HashMap<>(), meta); @@ -686,7 +739,7 @@ public abstract class AbstractMcpClientServerIntegrationTests { var mcpServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").tools(tool).build(); - try (var mcpClient = getMcpClientBuilder().clientInfo(new McpSchema.Implementation("Sample client", "0.0.0")) + try (var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0")) .capabilities(ClientCapabilities.builder().elicitation(true, false).build()) .elicitation(elicitationHandler) .applyElicitationDefaults(true) @@ -710,8 +763,12 @@ public abstract class AbstractMcpClientServerIntegrationTests { } } - @Test - void testCreateElicitationWithRequestTimeoutSuccess() { + @ParameterizedTest(name = "{0} : {displayName} ") + @MethodSource("clientsForTesting") + void testCreateElicitationWithRequestTimeoutSuccess(String clientType) { + + var clientBuilder = clientBuilders.get(clientType); + Function elicitationHandler = request -> { assertThat(request.message()).isNotEmpty(); assertThat(((McpSchema.ElicitFormRequest) request).requestedSchema()).isNotNull(); @@ -746,7 +803,7 @@ public abstract class AbstractMcpClientServerIntegrationTests { .tools(tool) .build(); - try (var mcpClient = getMcpClientBuilder() + try (var mcpClient = clientBuilder .clientInfo(McpSchema.Implementation.builder("Sample client", "0.0.0").build()) .capabilities(ClientCapabilities.builder().elicitation().build()) .elicitation(elicitationHandler) @@ -771,10 +828,14 @@ public abstract class AbstractMcpClientServerIntegrationTests { } } - @Test - void testCreateElicitationWithRequestTimeoutFail() { + @ParameterizedTest(name = "{0} : {displayName} ") + @MethodSource("clientsForTesting") + void testCreateElicitationWithRequestTimeoutFail(String clientType) { + var latch = new CountDownLatch(1); + var clientBuilder = clientBuilders.get(clientType); + Function elicitationHandler = request -> { assertThat(request.message()).isNotEmpty(); assertThat(request.requestedSchema()).isNotNull(); @@ -818,7 +879,7 @@ public abstract class AbstractMcpClientServerIntegrationTests { .tools(tool) .build(); - try (var mcpClient = getMcpClientBuilder() + try (var mcpClient = clientBuilder .clientInfo(McpSchema.Implementation.builder("Sample client", "0.0.0").build()) .capabilities(ClientCapabilities.builder().elicitation().build()) .elicitation(elicitationHandler) @@ -839,12 +900,15 @@ public abstract class AbstractMcpClientServerIntegrationTests { } } - @Test - void testCreateUrlElicitationSuccess() { + @ParameterizedTest(name = "{0} : {displayName} ") + @MethodSource("clientsForTesting") + void testCreateUrlElicitationSuccess(String clientType) { var elicitationRequest = McpSchema.ElicitUrlRequest .builder("Test message", "https://example.com/auth", "elicitation-123") .build(); + var clientBuilder = clientBuilders.get(clientType); + Function urlElicitationHandler = request -> { assertThat(request.message()).isEqualTo("Test message"); assertThat(request.url()).isEqualTo("https://example.com/auth"); @@ -868,7 +932,7 @@ public abstract class AbstractMcpClientServerIntegrationTests { var mcpServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").tools(tool).build(); - try (var mcpClient = getMcpClientBuilder() + try (var mcpClient = clientBuilder .clientInfo(McpSchema.Implementation.builder("Sample client", "0.0.0").build()) .capabilities(ClientCapabilities.builder().elicitation(false, true).build()) .urlElicitation(urlElicitationHandler) @@ -888,8 +952,12 @@ public abstract class AbstractMcpClientServerIntegrationTests { } } - @Test - void testElicitationCompleteNotification() throws InterruptedException { + @ParameterizedTest(name = "{0} : {displayName} ") + @MethodSource("clientsForTesting") + void testElicitationCompleteNotification(String clientType) throws InterruptedException { + + var clientBuilder = clientBuilders.get(clientType); + CountDownLatch notificationLatch = new CountDownLatch(1); AtomicReference notificationRef = new AtomicReference<>(); AtomicReference sessionId = new AtomicReference<>(); @@ -914,7 +982,7 @@ public abstract class AbstractMcpClientServerIntegrationTests { var mcpServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").tools(tool).build(); - try (var mcpClient = getMcpClientBuilder() + try (var mcpClient = clientBuilder .clientInfo(McpSchema.Implementation.builder("Sample client", "0.0.0").build()) .elicitationCompleteConsumer(elicitationCompleteConsumer) // enable elicitation so that we can register an elicitation complete consumer @@ -940,8 +1008,12 @@ public abstract class AbstractMcpClientServerIntegrationTests { } } - @Test - void testElicitationRequiredError() { + @ParameterizedTest(name = "{0} : {displayName} ") + @MethodSource("clientsForTesting") + void testElicitationRequiredError(String clientType) throws InterruptedException { + + var clientBuilder = clientBuilders.get(clientType); + // Capture the session ID so we can trigger an "elicitation complete" notification McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() .tool(Tool.builder("tool1", EMPTY_JSON_SCHEMA).description("tool1 description").build()) @@ -957,7 +1029,7 @@ public abstract class AbstractMcpClientServerIntegrationTests { Function elicitationHandler = request -> ElicitResult .builder(ElicitResult.Action.ACCEPT) .build(); - try (var mcpClient = getMcpClientBuilder() + try (var mcpClient = clientBuilder .clientInfo(McpSchema.Implementation.builder("Sample client", "0.0.0").build()) .urlElicitation(elicitationHandler) .build()) { @@ -990,8 +1062,11 @@ public abstract class AbstractMcpClientServerIntegrationTests { // --------------------------------------- // Roots Tests // --------------------------------------- - @Test - void testRootsSuccess() { + @ParameterizedTest(name = "{0} : {displayName} ") + @MethodSource("clientsForTesting") + void testRootsSuccess(String clientType) { + var clientBuilder = clientBuilders.get(clientType); + List roots = List.of(Root.builder("uri1://").name("root1").build(), Root.builder("uri2://").name("root2").build()); @@ -1001,7 +1076,7 @@ public abstract class AbstractMcpClientServerIntegrationTests { .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) .build(); - try (var mcpClient = getMcpClientBuilder().capabilities(ClientCapabilities.builder().roots(true).build()) + try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) .roots(roots) .build()) { @@ -1036,8 +1111,12 @@ public abstract class AbstractMcpClientServerIntegrationTests { } } - @Test - void testRootsWithoutCapability() { + @ParameterizedTest(name = "{0} : {displayName} ") + @MethodSource("clientsForTesting") + void testRootsWithoutCapability(String clientType) { + + var clientBuilder = clientBuilders.get(clientType); + McpServerFeatures.SyncToolSpecification tool = McpServerFeatures.SyncToolSpecification.builder() .tool(Tool.builder("tool1", EMPTY_JSON_SCHEMA).description("tool1 description").build()) .callHandler((exchange, request) -> { @@ -1054,7 +1133,7 @@ public abstract class AbstractMcpClientServerIntegrationTests { try ( // Create client without roots capability // No roots capability - var mcpClient = getMcpClientBuilder().capabilities(ClientCapabilities.builder().build()).build()) { + var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().build()).build()) { assertThat(mcpClient.initialize()).isNotNull(); @@ -1071,15 +1150,19 @@ public abstract class AbstractMcpClientServerIntegrationTests { } } - @Test - void testRootsNotificationWithEmptyRootsList() { + @ParameterizedTest(name = "{0} : {displayName} ") + @MethodSource("clientsForTesting") + void testRootsNotificationWithEmptyRootsList(String clientType) { + + var clientBuilder = clientBuilders.get(clientType); + AtomicReference> rootsRef = new AtomicReference<>(); var mcpServer = prepareSyncServerBuilder() .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) .build(); - try (var mcpClient = getMcpClientBuilder().capabilities(ClientCapabilities.builder().roots(true).build()) + try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) .roots(List.of()) // Empty roots list .build()) { @@ -1097,8 +1180,12 @@ public abstract class AbstractMcpClientServerIntegrationTests { } } - @Test - void testRootsWithMultipleHandlers() { + @ParameterizedTest(name = "{0} : {displayName} ") + @MethodSource("clientsForTesting") + void testRootsWithMultipleHandlers(String clientType) { + + var clientBuilder = clientBuilders.get(clientType); + List roots = List.of(Root.builder("uri1://").name("root1").build()); AtomicReference> rootsRef1 = new AtomicReference<>(); @@ -1109,7 +1196,7 @@ public abstract class AbstractMcpClientServerIntegrationTests { .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef2.set(rootsUpdate)) .build(); - try (var mcpClient = getMcpClientBuilder().capabilities(ClientCapabilities.builder().roots(true).build()) + try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) .roots(roots) .build()) { @@ -1127,8 +1214,12 @@ public abstract class AbstractMcpClientServerIntegrationTests { } } - @Test - void testRootsServerCloseWithActiveSubscription() { + @ParameterizedTest(name = "{0} : {displayName} ") + @MethodSource("clientsForTesting") + void testRootsServerCloseWithActiveSubscription(String clientType) { + + var clientBuilder = clientBuilders.get(clientType); + List roots = List.of(Root.builder("uri1://").name("root1").build()); AtomicReference> rootsRef = new AtomicReference<>(); @@ -1137,7 +1228,7 @@ public abstract class AbstractMcpClientServerIntegrationTests { .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) .build(); - try (var mcpClient = getMcpClientBuilder().capabilities(ClientCapabilities.builder().roots(true).build()) + try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) .roots(roots) .build()) { @@ -1158,8 +1249,12 @@ public abstract class AbstractMcpClientServerIntegrationTests { // --------------------------------------- // Tools Tests // --------------------------------------- - @Test - void testToolCallSuccess() { + @ParameterizedTest(name = "{0} : {displayName} ") + @MethodSource("clientsForTesting") + void testToolCallSuccess(String clientType) { + + var clientBuilder = clientBuilders.get(clientType); + var responseBodyIsNullOrBlank = new AtomicBoolean(false); var callResponse = McpSchema.CallToolResult.builder() .addContent(McpSchema.TextContent.builder("CALL RESPONSE; ctx=importantValue").build()) @@ -1190,7 +1285,7 @@ public abstract class AbstractMcpClientServerIntegrationTests { .tools(tool1) .build(); - try (var mcpClient = getMcpClientBuilder().build()) { + try (var mcpClient = clientBuilder.build()) { InitializeResult initResult = mcpClient.initialize(); assertThat(initResult).isNotNull(); @@ -1208,8 +1303,12 @@ public abstract class AbstractMcpClientServerIntegrationTests { } } - @Test - void testThrowingToolCallIsCaughtBeforeTimeout() { + @ParameterizedTest(name = "{0} : {displayName} ") + @MethodSource("clientsForTesting") + void testThrowingToolCallIsCaughtBeforeTimeout(String clientType) { + + var clientBuilder = clientBuilders.get(clientType); + McpSyncServer mcpServer = prepareSyncServerBuilder() .capabilities(ServerCapabilities.builder().tools(true).build()) .tools(McpServerFeatures.SyncToolSpecification.builder() @@ -1222,7 +1321,7 @@ public abstract class AbstractMcpClientServerIntegrationTests { .build()) .build(); - try (var mcpClient = getMcpClientBuilder().requestTimeout(Duration.ofMillis(6666)).build()) { + try (var mcpClient = clientBuilder.requestTimeout(Duration.ofMillis(6666)).build()) { InitializeResult initResult = mcpClient.initialize(); assertThat(initResult).isNotNull(); @@ -1237,8 +1336,12 @@ public abstract class AbstractMcpClientServerIntegrationTests { } } - @Test - void testToolCallSuccessWithTranportContextExtraction() { + @ParameterizedTest(name = "{0} : {displayName} ") + @MethodSource("clientsForTesting") + void testToolCallSuccessWithTranportContextExtraction(String clientType) { + + var clientBuilder = clientBuilders.get(clientType); + var transportContextIsNull = new AtomicBoolean(false); var transportContextIsEmpty = new AtomicBoolean(false); var responseBodyIsNullOrBlank = new AtomicBoolean(false); @@ -1273,7 +1376,7 @@ public abstract class AbstractMcpClientServerIntegrationTests { .tools(tool1) .build(); - try (var mcpClient = getMcpClientBuilder().build()) { + try (var mcpClient = clientBuilder.build()) { InitializeResult initResult = mcpClient.initialize(); assertThat(initResult).isNotNull(); @@ -1293,8 +1396,11 @@ public abstract class AbstractMcpClientServerIntegrationTests { } } - @Test - void testToolWithNonAsciiCharacters() { + @ParameterizedTest(name = "{0} : {displayName} ") + @MethodSource("clientsForTesting") + void testToolWithNonAsciiCharacters(String clientType) { + var clientBuilder = clientBuilders.get(clientType); + String inputSchema = """ { "type": "object", @@ -1319,7 +1425,7 @@ public abstract class AbstractMcpClientServerIntegrationTests { .tools(nonAsciiTool) .build(); - try (var mcpClient = getMcpClientBuilder().build()) { + try (var mcpClient = clientBuilder.build()) { InitializeResult initResult = mcpClient.initialize(); assertThat(initResult).isNotNull(); @@ -1342,8 +1448,12 @@ public abstract class AbstractMcpClientServerIntegrationTests { } } - @Test - void testToolListChangeHandlingSuccess() { + @ParameterizedTest(name = "{0} : {displayName} ") + @MethodSource("clientsForTesting") + void testToolListChangeHandlingSuccess(String clientType) { + + var clientBuilder = clientBuilders.get(clientType); + var callResponse = McpSchema.CallToolResult.builder() .addContent(McpSchema.TextContent.builder("CALL RESPONSE").build()) .build(); @@ -1375,7 +1485,7 @@ public abstract class AbstractMcpClientServerIntegrationTests { .tools(tool1) .build(); - try (var mcpClient = getMcpClientBuilder().toolsChangeConsumer(toolsUpdate -> { + try (var mcpClient = clientBuilder.toolsChangeConsumer(toolsUpdate -> { // perform a blocking call to a remote service try { HttpResponse response = HttpClient.newHttpClient() @@ -1430,11 +1540,15 @@ public abstract class AbstractMcpClientServerIntegrationTests { } } - @Test - void testInitialize() { + @ParameterizedTest(name = "{0} : {displayName} ") + @MethodSource("clientsForTesting") + void testInitialize(String clientType) { + + var clientBuilder = clientBuilders.get(clientType); + var mcpServer = prepareSyncServerBuilder().build(); - try (var mcpClient = getMcpClientBuilder().build()) { + try (var mcpClient = clientBuilder.build()) { InitializeResult initResult = mcpClient.initialize(); assertThat(initResult).isNotNull(); @@ -1447,13 +1561,16 @@ public abstract class AbstractMcpClientServerIntegrationTests { // --------------------------------------- // Logging Tests // --------------------------------------- - @Test - void testLoggingNotification() throws InterruptedException { + @ParameterizedTest(name = "{0} : {displayName} ") + @MethodSource("clientsForTesting") + void testLoggingNotification(String clientType) throws InterruptedException { int expectedNotificationsCount = 3; CountDownLatch latch = new CountDownLatch(expectedNotificationsCount); // Create a list to store received logging notifications List receivedNotifications = new CopyOnWriteArrayList<>(); + var clientBuilder = clientBuilders.get(clientType); + // Create server with a tool that sends logging notifications McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() .tool(Tool.builder("logging-test", EMPTY_JSON_SCHEMA).description("Test logging notifications").build()) @@ -1502,7 +1619,7 @@ public abstract class AbstractMcpClientServerIntegrationTests { try ( // Create client with logging notification handler - var mcpClient = getMcpClientBuilder().loggingConsumer(notification -> { + var mcpClient = clientBuilder.loggingConsumer(notification -> { receivedNotifications.add(notification); latch.countDown(); }).build()) { @@ -1552,14 +1669,17 @@ public abstract class AbstractMcpClientServerIntegrationTests { // --------------------------------------- // Progress Tests // --------------------------------------- - @Test - void testProgressNotification() throws InterruptedException { + @ParameterizedTest(name = "{0} : {displayName} ") + @MethodSource("clientsForTesting") + void testProgressNotification(String clientType) throws InterruptedException { int expectedNotificationsCount = 4; // 3 notifications + 1 for another progress // token CountDownLatch latch = new CountDownLatch(expectedNotificationsCount); // Create a list to store received logging notifications List receivedNotifications = new CopyOnWriteArrayList<>(); + var clientBuilder = clientBuilders.get(clientType); + // Create server with a tool that sends logging notifications McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() .tool(McpSchema.Tool.builder("progress-test", EMPTY_JSON_SCHEMA) @@ -1602,7 +1722,7 @@ public abstract class AbstractMcpClientServerIntegrationTests { try ( // Create client with progress notification handler - var mcpClient = getMcpClientBuilder().progressConsumer(notification -> { + var mcpClient = clientBuilder.progressConsumer(notification -> { receivedNotifications.add(notification); latch.countDown(); }).build()) { @@ -1663,8 +1783,11 @@ public abstract class AbstractMcpClientServerIntegrationTests { // --------------------------------------- // Completion Tests // --------------------------------------- - @Test - void testCompletionShouldReturnExpectedSuggestions() { + @ParameterizedTest(name = "{0} : Completion call") + @MethodSource("clientsForTesting") + void testCompletionShouldReturnExpectedSuggestions(String clientType) { + var clientBuilder = clientBuilders.get(clientType); + var expectedValues = List.of("python", "pytorch", "pyside"); var completionResponse = new McpSchema.CompleteResult( new CompleteResult.CompleteCompletion(expectedValues, 10, true)); @@ -1690,7 +1813,7 @@ public abstract class AbstractMcpClientServerIntegrationTests { McpSchema.PromptReference.builder("code_review").title("Code review").build(), completionHandler)) .build(); - try (var mcpClient = getMcpClientBuilder().build()) { + try (var mcpClient = clientBuilder.build()) { InitializeResult initResult = mcpClient.initialize(); assertThat(initResult).isNotNull(); @@ -1716,8 +1839,12 @@ public abstract class AbstractMcpClientServerIntegrationTests { // --------------------------------------- // Ping Tests // --------------------------------------- - @Test - void testPingSuccess() { + @ParameterizedTest(name = "{0} : {displayName} ") + @MethodSource("clientsForTesting") + void testPingSuccess(String clientType) { + + var clientBuilder = clientBuilders.get(clientType); + // Create server with a tool that uses ping functionality AtomicReference executionOrder = new AtomicReference<>(""); @@ -1751,7 +1878,7 @@ public abstract class AbstractMcpClientServerIntegrationTests { .tools(tool) .build(); - try (var mcpClient = getMcpClientBuilder().build()) { + try (var mcpClient = clientBuilder.build()) { // Initialize client InitializeResult initResult = mcpClient.initialize(); @@ -1775,8 +1902,11 @@ public abstract class AbstractMcpClientServerIntegrationTests { // --------------------------------------- // Tool Structured Output Schema Tests // --------------------------------------- - @Test - void testStructuredOutputValidationSuccess() { + @ParameterizedTest(name = "{0} : {displayName} ") + @MethodSource("clientsForTesting") + void testStructuredOutputValidationSuccess(String clientType) { + var clientBuilder = clientBuilders.get(clientType); + // Create a tool with output schema Map outputSchema = Map.of( "type", "object", "properties", Map.of("result", Map.of("type", "number"), "operation", @@ -1805,7 +1935,7 @@ public abstract class AbstractMcpClientServerIntegrationTests { .tools(tool) .build(); - try (var mcpClient = getMcpClientBuilder().build()) { + try (var mcpClient = clientBuilder.build()) { InitializeResult initResult = mcpClient.initialize(); assertThat(initResult).isNotNull(); @@ -1845,8 +1975,11 @@ public abstract class AbstractMcpClientServerIntegrationTests { } } - @Test - void testStructuredOutputOfObjectArrayValidationSuccess() { + @ParameterizedTest(name = "{0} : {displayName} ") + @ValueSource(strings = { "httpclient" }) + void testStructuredOutputOfObjectArrayValidationSuccess(String clientType) { + var clientBuilder = clientBuilders.get(clientType); + // Create a tool with output schema that returns an array of objects Map outputSchema = Map .of( // @formatter:off @@ -1877,7 +2010,7 @@ public abstract class AbstractMcpClientServerIntegrationTests { .tools(tool) .build(); - try (var mcpClient = getMcpClientBuilder().build()) { + try (var mcpClient = clientBuilder.build()) { assertThat(mcpClient.initialize()).isNotNull(); // Call tool with valid structured output of type array @@ -1901,8 +2034,11 @@ public abstract class AbstractMcpClientServerIntegrationTests { } } - @Test - void testStructuredOutputWithInHandlerError() { + @ParameterizedTest(name = "{0} : {displayName} ") + @ValueSource(strings = { "httpclient" }) + void testStructuredOutputWithInHandlerError(String clientType) { + var clientBuilder = clientBuilders.get(clientType); + // Create a tool with output schema Map outputSchema = Map.of( "type", "object", "properties", Map.of("result", Map.of("type", "number"), "operation", @@ -1928,7 +2064,7 @@ public abstract class AbstractMcpClientServerIntegrationTests { .tools(tool) .build(); - try (var mcpClient = getMcpClientBuilder().build()) { + try (var mcpClient = clientBuilder.build()) { InitializeResult initResult = mcpClient.initialize(); assertThat(initResult).isNotNull(); @@ -1954,8 +2090,12 @@ public abstract class AbstractMcpClientServerIntegrationTests { } } - @Test - void testStructuredOutputValidationFailure() { + @ParameterizedTest(name = "{0} : {displayName} ") + @ValueSource(strings = { "httpclient" }) + void testStructuredOutputValidationFailure(String clientType) { + + var clientBuilder = clientBuilders.get(clientType); + // Create a tool with output schema Map outputSchema = Map.of("type", "object", "properties", Map.of("result", Map.of("type", "number"), "operation", Map.of("type", "string")), "required", @@ -1983,7 +2123,7 @@ public abstract class AbstractMcpClientServerIntegrationTests { .tools(tool) .build(); - try (var mcpClient = getMcpClientBuilder().build()) { + try (var mcpClient = clientBuilder.build()) { InitializeResult initResult = mcpClient.initialize(); assertThat(initResult).isNotNull(); @@ -2004,8 +2144,12 @@ public abstract class AbstractMcpClientServerIntegrationTests { } } - @Test - void testStructuredOutputMissingStructuredContent() { + @ParameterizedTest(name = "{0} : {displayName} ") + @MethodSource("clientsForTesting") + void testStructuredOutputMissingStructuredContent(String clientType) { + + var clientBuilder = clientBuilders.get(clientType); + // Create a tool with output schema Map outputSchema = Map.of("type", "object", "properties", Map.of("result", Map.of("type", "number")), "required", List.of("result")); @@ -2028,7 +2172,7 @@ public abstract class AbstractMcpClientServerIntegrationTests { .tools(tool) .build(); - try (var mcpClient = getMcpClientBuilder().build()) { + try (var mcpClient = clientBuilder.build()) { InitializeResult initResult = mcpClient.initialize(); assertThat(initResult).isNotNull(); @@ -2050,14 +2194,18 @@ public abstract class AbstractMcpClientServerIntegrationTests { } } - @Test - void testStructuredOutputRuntimeToolAddition() { + @ParameterizedTest(name = "{0} : {displayName} ") + @MethodSource("clientsForTesting") + void testStructuredOutputRuntimeToolAddition(String clientType) { + + var clientBuilder = clientBuilders.get(clientType); + // Start server without tools var mcpServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0") .capabilities(ServerCapabilities.builder().tools(true).build()) .build(); - try (var mcpClient = getMcpClientBuilder().build()) { + try (var mcpClient = clientBuilder.build()) { InitializeResult initResult = mcpClient.initialize(); assertThat(initResult).isNotNull(); @@ -2127,8 +2275,12 @@ public abstract class AbstractMcpClientServerIntegrationTests { // Resource Subscription Tests // --------------------------------------- - @Test - void testResourceSubscription() throws InterruptedException { + @ParameterizedTest(name = "{0} : {displayName} ") + @MethodSource("clientsForTesting") + void testResourceSubscription(String clientType) throws InterruptedException { + + var clientBuilder = clientBuilders.get(clientType); + String resourceUri = "test://subscribable-resource"; var receivedContents = new AtomicReference>(); var latch = new CountDownLatch(1); @@ -2146,7 +2298,7 @@ public abstract class AbstractMcpClientServerIntegrationTests { .resources(resourceSpec) .build(); - try (var mcpClient = getMcpClientBuilder().resourcesUpdateConsumer(contents -> { + try (var mcpClient = clientBuilder.resourcesUpdateConsumer(contents -> { receivedContents.set(contents); latch.countDown(); }).build()) { @@ -2167,8 +2319,11 @@ public abstract class AbstractMcpClientServerIntegrationTests { } } - @Test - void testResourceSubscription_afterUnsubscribe_noNotification() { + @ParameterizedTest(name = "{0} : {displayName} ") + @MethodSource("clientsForTesting") + void testResourceSubscription_afterUnsubscribe_noNotification(String clientType) throws InterruptedException { + var clientBuilder = clientBuilders.get(clientType); + String resourceUri = "test://subscribable-resource-unsub"; var notificationCount = new java.util.concurrent.atomic.AtomicInteger(0); @@ -2183,8 +2338,7 @@ public abstract class AbstractMcpClientServerIntegrationTests { .resources(resourceSpec) .build(); - try (var mcpClient = getMcpClientBuilder() - .resourcesUpdateConsumer(contents -> notificationCount.incrementAndGet()) + try (var mcpClient = clientBuilder.resourcesUpdateConsumer(contents -> notificationCount.incrementAndGet()) .build()) { mcpClient.initialize(); @@ -2202,8 +2356,11 @@ public abstract class AbstractMcpClientServerIntegrationTests { } // Bounded read - @Test - void testRejectsWhenContentLengthHeaderExceedsLimit() throws Exception { + @ParameterizedTest(name = "{0} : {displayName} ") + @MethodSource("clientsForTesting") + void testRejectsWhenContentLengthHeaderExceedsLimit(String clientType) throws Exception { + var clientBuilder = clientBuilders.get(clientType); + String inputSchema = """ { "type": "object", @@ -2227,7 +2384,7 @@ public abstract class AbstractMcpClientServerIntegrationTests { .tools(tool1) .build(); - try (var mcpClient = getMcpClientBuilder().build()) { + try (var mcpClient = clientBuilder.build()) { String oversizedBody = "a".repeat(MAX_REQUEST_SIZE + 1); mcpClient.initialize(); diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletSseIntegrationTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletSseIntegrationTests.java index c1ebe7b..7a07220 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletSseIntegrationTests.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletSseIntegrationTests.java @@ -70,6 +70,12 @@ class HttpServletSseIntegrationTests extends AbstractMcpClientServerIntegrationT catch (Exception e) { throw new RuntimeException("Failed to start Tomcat", e); } + + clientBuilders + .put("httpclient", + McpClient.sync(HttpClientSseClientTransport.builder("http://localhost:" + PORT) + .sseEndpoint(CUSTOM_SSE_ENDPOINT) + .build()).requestTimeout(Duration.ofHours(10))); } @Override @@ -82,15 +88,6 @@ class HttpServletSseIntegrationTests extends AbstractMcpClientServerIntegrationT return McpServer.sync(this.mcpServerTransportProvider); } - @Override - protected McpClient.SyncSpec getMcpClientBuilder() { - return McpClient - .sync(HttpClientSseClientTransport.builder("http://localhost:" + PORT) - .sseEndpoint(CUSTOM_SSE_ENDPOINT) - .build()) - .requestTimeout(Duration.ofHours(10)); - } - @AfterEach public void after() { if (mcpServerTransportProvider != null) { @@ -107,6 +104,10 @@ class HttpServletSseIntegrationTests extends AbstractMcpClientServerIntegrationT } } + @Override + protected void prepareClients(int port, String mcpEndpoint) { + } + @Test void rejectsWhenBodyBytesExceedLimitWithoutContentLengthHeader() throws Exception { var httpClient = HttpClient.newHttpClient(); diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStreamableIntegrationTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStreamableIntegrationTests.java index cfeb16b..281f3e0 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStreamableIntegrationTests.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStreamableIntegrationTests.java @@ -74,6 +74,12 @@ class HttpServletStreamableIntegrationTests extends AbstractMcpClientServerInteg catch (Exception e) { throw new RuntimeException("Failed to start Tomcat", e); } + + clientBuilders + .put("httpclient", + McpClient.sync(HttpClientStreamableHttpTransport.builder("http://localhost:" + PORT) + .endpoint(MESSAGE_ENDPOINT) + .build()).requestTimeout(Duration.ofHours(10))); } @Override @@ -86,15 +92,6 @@ class HttpServletStreamableIntegrationTests extends AbstractMcpClientServerInteg return McpServer.sync(this.mcpServerTransportProvider); } - @Override - protected McpClient.SyncSpec getMcpClientBuilder() { - return McpClient - .sync(HttpClientStreamableHttpTransport.builder("http://localhost:" + PORT) - .endpoint(MESSAGE_ENDPOINT) - .build()) - .requestTimeout(Duration.ofHours(10)); - } - @AfterEach public void after() { if (mcpServerTransportProvider != null) { @@ -149,7 +146,10 @@ class HttpServletStreamableIntegrationTests extends AbstractMcpClientServerInteg finally { mcpServer.close(); } + } + @Override + protected void prepareClients(int port, String mcpEndpoint) { } static McpTransportContextExtractor TEST_CONTEXT_EXTRACTOR = (r) -> McpTransportContext