From 82102a18f5d5b7f99d047fdf140dac40d3e67c25 Mon Sep 17 00:00:00 2001 From: Daniel Garnier-Moiroux Date: Thu, 12 Feb 2026 10:44:03 +0100 Subject: [PATCH] Remove unused abstract test classes in mcp-core Signed-off-by: Daniel Garnier-Moiroux --- ...AbstractMcpAsyncClientResiliencyTests.java | 231 --- .../client/AbstractMcpAsyncClientTests.java | 829 -------- .../client/AbstractMcpSyncClientTests.java | 682 ------- .../server/AbstractMcpAsyncServerTests.java | 722 ------- ...stractMcpClientServerIntegrationTests.java | 1756 ----------------- .../server/AbstractMcpSyncServerTests.java | 678 ------- 6 files changed, 4898 deletions(-) delete mode 100644 mcp-core/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientResiliencyTests.java delete mode 100644 mcp-core/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java delete mode 100644 mcp-core/src/test/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java delete mode 100644 mcp-core/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java delete mode 100644 mcp-core/src/test/java/io/modelcontextprotocol/server/AbstractMcpClientServerIntegrationTests.java delete mode 100644 mcp-core/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientResiliencyTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientResiliencyTests.java deleted file mode 100644 index 18a5cb9..0000000 --- a/mcp-core/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientResiliencyTests.java +++ /dev/null @@ -1,231 +0,0 @@ -/* - * Copyright 2024-2024 the original author or authors. - */ - -package io.modelcontextprotocol.client; - -import eu.rekawek.toxiproxy.Proxy; -import eu.rekawek.toxiproxy.ToxiproxyClient; -import eu.rekawek.toxiproxy.model.ToxicDirection; -import io.modelcontextprotocol.spec.McpClientTransport; -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpTransport; -import io.modelcontextprotocol.spec.McpTransportSessionClosedException; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.testcontainers.containers.GenericContainer; -import org.testcontainers.containers.Network; -import org.testcontainers.containers.ToxiproxyContainer; -import org.testcontainers.containers.wait.strategy.Wait; -import reactor.test.StepVerifier; - -import java.io.IOException; -import java.time.Duration; -import java.util.List; -import java.util.Map; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; -import java.util.function.Function; - -import static org.assertj.core.api.Assertions.assertThatCode; - -/** - * Resiliency test suite for the {@link McpAsyncClient} that can be used with different - * {@link McpTransport} implementations that support Streamable HTTP. - * - * The purpose of these tests is to allow validating the transport layer resiliency - * instead of the functionality offered by the logical layer of MCP concepts such as - * tools, resources, prompts, etc. - * - * @author Dariusz Jędrzejczyk - */ -// KEEP IN SYNC with the class in mcp-test module -public abstract class AbstractMcpAsyncClientResiliencyTests { - - private static final Logger logger = LoggerFactory.getLogger(AbstractMcpAsyncClientResiliencyTests.class); - - static Network network = Network.newNetwork(); - static String host = "http://localhost:3001"; - - @SuppressWarnings("resource") - static GenericContainer container = new GenericContainer<>("docker.io/node:lts-alpine3.23") - .withCommand("npx -y @modelcontextprotocol/server-everything@2025.12.18 streamableHttp") - .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String())) - .withNetwork(network) - .withNetworkAliases("everything-server") - .withExposedPorts(3001) - .waitingFor(Wait.forHttp("/").forStatusCode(404)); - - static ToxiproxyContainer toxiproxy = new ToxiproxyContainer("ghcr.io/shopify/toxiproxy:2.5.0").withNetwork(network) - .withExposedPorts(8474, 3000); - - static Proxy proxy; - - static { - container.start(); - - toxiproxy.start(); - - final ToxiproxyClient toxiproxyClient = new ToxiproxyClient(toxiproxy.getHost(), toxiproxy.getControlPort()); - try { - proxy = toxiproxyClient.createProxy("everything-server", "0.0.0.0:3000", "everything-server:3001"); - } - catch (IOException e) { - throw new RuntimeException("Can't create proxy!", e); - } - - final String ipAddressViaToxiproxy = toxiproxy.getHost(); - final int portViaToxiproxy = toxiproxy.getMappedPort(3000); - - host = "http://" + ipAddressViaToxiproxy + ":" + portViaToxiproxy; - } - - static void disconnect() { - long start = System.nanoTime(); - try { - // disconnect - // proxy.toxics().bandwidth("CUT_CONNECTION_DOWNSTREAM", - // ToxicDirection.DOWNSTREAM, 0); - // proxy.toxics().bandwidth("CUT_CONNECTION_UPSTREAM", - // ToxicDirection.UPSTREAM, 0); - proxy.toxics().resetPeer("RESET_DOWNSTREAM", ToxicDirection.DOWNSTREAM, 0); - proxy.toxics().resetPeer("RESET_UPSTREAM", ToxicDirection.UPSTREAM, 0); - logger.info("Disconnect took {} ms", Duration.ofNanos(System.nanoTime() - start).toMillis()); - } - catch (IOException e) { - throw new RuntimeException("Failed to disconnect", e); - } - } - - static void reconnect() { - long start = System.nanoTime(); - try { - proxy.toxics().get("RESET_UPSTREAM").remove(); - proxy.toxics().get("RESET_DOWNSTREAM").remove(); - // proxy.toxics().get("CUT_CONNECTION_DOWNSTREAM").remove(); - // proxy.toxics().get("CUT_CONNECTION_UPSTREAM").remove(); - logger.info("Reconnect took {} ms", Duration.ofNanos(System.nanoTime() - start).toMillis()); - } - catch (IOException e) { - throw new RuntimeException("Failed to reconnect", e); - } - } - - static void restartMcpServer() { - container.stop(); - container.start(); - } - - abstract McpClientTransport createMcpTransport(); - - protected Duration getRequestTimeout() { - return Duration.ofSeconds(14); - } - - protected Duration getInitializationTimeout() { - return Duration.ofSeconds(2); - } - - McpAsyncClient client(McpClientTransport transport) { - return client(transport, Function.identity()); - } - - McpAsyncClient client(McpClientTransport transport, Function customizer) { - AtomicReference client = new AtomicReference<>(); - - assertThatCode(() -> { - // Do not advertise roots. Otherwise, the server will list roots during - // initialization. The client responds asynchronously, and there might be a - // rest condition in tests where we disconnect right after initialization. - McpClient.AsyncSpec builder = McpClient.async(transport) - .requestTimeout(getRequestTimeout()) - .initializationTimeout(getInitializationTimeout()) - .capabilities(McpSchema.ClientCapabilities.builder().build()); - builder = customizer.apply(builder); - client.set(builder.build()); - }).doesNotThrowAnyException(); - - return client.get(); - } - - void withClient(McpClientTransport transport, Consumer c) { - withClient(transport, Function.identity(), c); - } - - void withClient(McpClientTransport transport, Function customizer, - Consumer c) { - var client = client(transport, customizer); - try { - c.accept(client); - } - finally { - StepVerifier.create(client.closeGracefully()).expectComplete().verify(Duration.ofSeconds(10)); - } - } - - @Test - void testPing() { - withClient(createMcpTransport(), mcpAsyncClient -> { - StepVerifier.create(mcpAsyncClient.initialize()).expectNextCount(1).verifyComplete(); - - disconnect(); - - StepVerifier.create(mcpAsyncClient.ping()).expectError().verify(); - - reconnect(); - - StepVerifier.create(mcpAsyncClient.ping()).expectNextCount(1).verifyComplete(); - }); - } - - @Test - void testSessionInvalidation() { - withClient(createMcpTransport(), mcpAsyncClient -> { - StepVerifier.create(mcpAsyncClient.initialize()).expectNextCount(1).verifyComplete(); - - restartMcpServer(); - - // The first try will face the session mismatch exception and the second one - // will go through the re-initialization process. - StepVerifier.create(mcpAsyncClient.ping().retry(1)).expectNextCount(1).verifyComplete(); - }); - } - - @Test - void testCallTool() { - withClient(createMcpTransport(), mcpAsyncClient -> { - AtomicReference> tools = new AtomicReference<>(); - StepVerifier.create(mcpAsyncClient.initialize()).expectNextCount(1).verifyComplete(); - StepVerifier.create(mcpAsyncClient.listTools()) - .consumeNextWith(list -> tools.set(list.tools())) - .verifyComplete(); - - disconnect(); - - String name = tools.get().get(0).name(); - // Assuming this is the echo tool - McpSchema.CallToolRequest request = new McpSchema.CallToolRequest(name, Map.of("message", "hello")); - StepVerifier.create(mcpAsyncClient.callTool(request)).expectError().verify(); - - reconnect(); - - StepVerifier.create(mcpAsyncClient.callTool(request)).expectNextCount(1).verifyComplete(); - }); - } - - @Test - void testSessionClose() { - withClient(createMcpTransport(), mcpAsyncClient -> { - StepVerifier.create(mcpAsyncClient.initialize()).expectNextCount(1).verifyComplete(); - // In case of Streamable HTTP this call should issue a HTTP DELETE request - // invalidating the session - StepVerifier.create(mcpAsyncClient.closeGracefully()).expectComplete().verify(); - // The next tries to use the closed session and fails - StepVerifier.create(mcpAsyncClient.ping()) - .expectErrorMatches(err -> err.getCause() instanceof McpTransportSessionClosedException) - .verify(); - }); - } - -} diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java deleted file mode 100644 index 5b78779..0000000 --- a/mcp-core/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java +++ /dev/null @@ -1,829 +0,0 @@ -/* - * Copyright 2024-2024 the original author or authors. - */ - -package io.modelcontextprotocol.client; - -import static io.modelcontextprotocol.util.McpJsonMapperUtils.JSON_MAPPER; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.api.Assertions.fail; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; - -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; -import java.util.function.Function; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -import io.modelcontextprotocol.spec.McpClientTransport; -import io.modelcontextprotocol.spec.McpError; -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.BlobResourceContents; -import io.modelcontextprotocol.spec.McpSchema.CallToolRequest; -import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities; -import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest; -import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult; -import io.modelcontextprotocol.spec.McpSchema.ElicitRequest; -import io.modelcontextprotocol.spec.McpSchema.ElicitResult; -import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest; -import io.modelcontextprotocol.spec.McpSchema.Prompt; -import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult; -import io.modelcontextprotocol.spec.McpSchema.Resource; -import io.modelcontextprotocol.spec.McpSchema.ResourceContents; -import io.modelcontextprotocol.spec.McpSchema.Root; -import io.modelcontextprotocol.spec.McpSchema.SubscribeRequest; -import io.modelcontextprotocol.spec.McpSchema.TextResourceContents; -import io.modelcontextprotocol.spec.McpSchema.Tool; -import io.modelcontextprotocol.spec.McpSchema.UnsubscribeRequest; -import io.modelcontextprotocol.spec.McpTransport; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.core.publisher.Sinks; -import reactor.test.StepVerifier; - -/** - * Test suite for the {@link McpAsyncClient} that can be used with different - * {@link McpTransport} implementations. - * - * @author Christian Tzolov - * @author Dariusz Jędrzejczyk - */ -// KEEP IN SYNC with the class in mcp-test module -public abstract class AbstractMcpAsyncClientTests { - - private static final String ECHO_TEST_MESSAGE = "Hello MCP Spring AI!"; - - abstract protected McpClientTransport createMcpTransport(); - - protected Duration getRequestTimeout() { - return Duration.ofSeconds(14); - } - - protected Duration getInitializationTimeout() { - return Duration.ofSeconds(20); - } - - McpAsyncClient client(McpClientTransport transport) { - return client(transport, Function.identity()); - } - - McpAsyncClient client(McpClientTransport transport, Function customizer) { - AtomicReference client = new AtomicReference<>(); - - assertThatCode(() -> { - McpClient.AsyncSpec builder = McpClient.async(transport) - .requestTimeout(getRequestTimeout()) - .initializationTimeout(getInitializationTimeout()) - .sampling(req -> Mono.just(new CreateMessageResult(McpSchema.Role.USER, - new McpSchema.TextContent("Oh, hi!"), "modelId", CreateMessageResult.StopReason.END_TURN))) - .capabilities(ClientCapabilities.builder().roots(true).sampling().build()); - builder = customizer.apply(builder); - client.set(builder.build()); - }).doesNotThrowAnyException(); - - return client.get(); - } - - void withClient(McpClientTransport transport, Consumer c) { - withClient(transport, Function.identity(), c); - } - - void withClient(McpClientTransport transport, Function customizer, - Consumer c) { - var client = client(transport, customizer); - try { - c.accept(client); - } - finally { - StepVerifier.create(client.closeGracefully()).expectComplete().verify(Duration.ofSeconds(10)); - } - } - - void verifyNotificationSucceedsWithImplicitInitialization(Function> operation, - String action) { - withClient(createMcpTransport(), mcpAsyncClient -> { - StepVerifier.create(operation.apply(mcpAsyncClient)).verifyComplete(); - }); - } - - void verifyCallSucceedsWithImplicitInitialization(Function> operation, String action) { - withClient(createMcpTransport(), mcpAsyncClient -> { - StepVerifier.create(operation.apply(mcpAsyncClient)).expectNextCount(1).verifyComplete(); - }); - } - - @Test - void testConstructorWithInvalidArguments() { - assertThatThrownBy(() -> McpClient.async(null).build()).isInstanceOf(IllegalArgumentException.class) - .hasMessage("Transport must not be null"); - - assertThatThrownBy(() -> McpClient.async(createMcpTransport()).requestTimeout(null).build()) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Request timeout must not be null"); - } - - @Test - void testListToolsWithoutInitialization() { - verifyCallSucceedsWithImplicitInitialization(client -> client.listTools(McpSchema.FIRST_PAGE), "listing tools"); - } - - @Test - void testListTools() { - withClient(createMcpTransport(), mcpAsyncClient -> { - StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listTools(McpSchema.FIRST_PAGE))) - .consumeNextWith(result -> { - assertThat(result.tools()).isNotNull().isNotEmpty(); - - Tool firstTool = result.tools().get(0); - assertThat(firstTool.name()).isNotNull(); - assertThat(firstTool.description()).isNotNull(); - }) - .verifyComplete(); - }); - } - - @Test - void testListAllTools() { - withClient(createMcpTransport(), mcpAsyncClient -> { - StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listTools())) - .consumeNextWith(result -> { - assertThat(result.tools()).isNotNull().isNotEmpty(); - - Tool firstTool = result.tools().get(0); - assertThat(firstTool.name()).isNotNull(); - assertThat(firstTool.description()).isNotNull(); - }) - .verifyComplete(); - }); - } - - @Test - void testListAllToolsReturnsImmutableList() { - withClient(createMcpTransport(), mcpAsyncClient -> { - StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listTools())) - .consumeNextWith(result -> { - assertThat(result.tools()).isNotNull(); - // Verify that the returned list is immutable - assertThatThrownBy(() -> result.tools() - .add(Tool.builder() - .name("test") - .title("test") - .inputSchema(JSON_MAPPER, "{\"type\":\"object\"}") - .build())) - .isInstanceOf(UnsupportedOperationException.class); - }) - .verifyComplete(); - }); - } - - @Test - void testPingWithoutInitialization() { - verifyCallSucceedsWithImplicitInitialization(client -> client.ping(), "pinging the server"); - } - - @Test - void testPing() { - withClient(createMcpTransport(), mcpAsyncClient -> { - StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.ping())) - .expectNextCount(1) - .verifyComplete(); - }); - } - - @Test - void testCallToolWithoutInitialization() { - CallToolRequest callToolRequest = new CallToolRequest("echo", Map.of("message", ECHO_TEST_MESSAGE)); - verifyCallSucceedsWithImplicitInitialization(client -> client.callTool(callToolRequest), "calling tools"); - } - - @Test - void testCallTool() { - withClient(createMcpTransport(), mcpAsyncClient -> { - CallToolRequest callToolRequest = new CallToolRequest("echo", Map.of("message", ECHO_TEST_MESSAGE)); - - StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.callTool(callToolRequest))) - .consumeNextWith(callToolResult -> { - assertThat(callToolResult).isNotNull().satisfies(result -> { - assertThat(result.content()).isNotNull(); - assertThat(result.isError()).isNull(); - }); - }) - .verifyComplete(); - }); - } - - @Test - void testCallToolWithInvalidTool() { - withClient(createMcpTransport(), mcpAsyncClient -> { - CallToolRequest invalidRequest = new CallToolRequest("nonexistent_tool", - Map.of("message", ECHO_TEST_MESSAGE)); - - StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.callTool(invalidRequest))) - .consumeErrorWith( - e -> assertThat(e).isInstanceOf(McpError.class).hasMessage("Unknown tool: nonexistent_tool")) - .verify(); - }); - } - - @ParameterizedTest - @ValueSource(strings = { "success", "error", "debug" }) - void testCallToolWithMessageAnnotations(String messageType) { - McpClientTransport transport = createMcpTransport(); - - withClient(transport, mcpAsyncClient -> { - StepVerifier.create(mcpAsyncClient.initialize() - .then(mcpAsyncClient.callTool(new McpSchema.CallToolRequest("annotatedMessage", - Map.of("messageType", messageType, "includeImage", true))))) - .consumeNextWith(result -> { - assertThat(result).isNotNull(); - assertThat(result.isError()).isNotEqualTo(true); - assertThat(result.content()).isNotEmpty(); - assertThat(result.content()).allSatisfy(content -> { - switch (content.type()) { - case "text": - McpSchema.TextContent textContent = assertInstanceOf(McpSchema.TextContent.class, - content); - assertThat(textContent.text()).isNotEmpty(); - assertThat(textContent.annotations()).isNotNull(); - - switch (messageType) { - case "error": - assertThat(textContent.annotations().priority()).isEqualTo(1.0); - assertThat(textContent.annotations().audience()) - .containsOnly(McpSchema.Role.USER, McpSchema.Role.ASSISTANT); - break; - case "success": - assertThat(textContent.annotations().priority()).isEqualTo(0.7); - assertThat(textContent.annotations().audience()) - .containsExactly(McpSchema.Role.USER); - break; - case "debug": - assertThat(textContent.annotations().priority()).isEqualTo(0.3); - assertThat(textContent.annotations().audience()) - .containsExactly(McpSchema.Role.ASSISTANT); - break; - default: - throw new IllegalStateException("Unexpected value: " + content.type()); - } - break; - case "image": - McpSchema.ImageContent imageContent = assertInstanceOf(McpSchema.ImageContent.class, - content); - assertThat(imageContent.data()).isNotEmpty(); - assertThat(imageContent.annotations()).isNotNull(); - assertThat(imageContent.annotations().priority()).isEqualTo(0.5); - assertThat(imageContent.annotations().audience()).containsExactly(McpSchema.Role.USER); - break; - default: - fail("Unexpected content type: " + content.type()); - } - }); - }) - .verifyComplete(); - }); - } - - @Test - void testListResourcesWithoutInitialization() { - verifyCallSucceedsWithImplicitInitialization(client -> client.listResources(McpSchema.FIRST_PAGE), - "listing resources"); - } - - @Test - void testListResources() { - withClient(createMcpTransport(), mcpAsyncClient -> { - StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listResources(McpSchema.FIRST_PAGE))) - .consumeNextWith(resources -> { - assertThat(resources).isNotNull().satisfies(result -> { - assertThat(result.resources()).isNotNull(); - - if (!result.resources().isEmpty()) { - Resource firstResource = result.resources().get(0); - assertThat(firstResource.uri()).isNotNull(); - assertThat(firstResource.name()).isNotNull(); - } - }); - }) - .verifyComplete(); - }); - } - - @Test - void testListAllResources() { - withClient(createMcpTransport(), mcpAsyncClient -> { - StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listResources())) - .consumeNextWith(resources -> { - assertThat(resources).isNotNull().satisfies(result -> { - assertThat(result.resources()).isNotNull(); - - if (!result.resources().isEmpty()) { - Resource firstResource = result.resources().get(0); - assertThat(firstResource.uri()).isNotNull(); - assertThat(firstResource.name()).isNotNull(); - } - }); - }) - .verifyComplete(); - }); - } - - @Test - void testListAllResourcesReturnsImmutableList() { - withClient(createMcpTransport(), mcpAsyncClient -> { - StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listResources())) - .consumeNextWith(result -> { - assertThat(result.resources()).isNotNull(); - // Verify that the returned list is immutable - assertThatThrownBy( - () -> result.resources().add(Resource.builder().uri("test://uri").name("test").build())) - .isInstanceOf(UnsupportedOperationException.class); - }) - .verifyComplete(); - }); - } - - @Test - void testMcpAsyncClientState() { - withClient(createMcpTransport(), mcpAsyncClient -> { - assertThat(mcpAsyncClient).isNotNull(); - }); - } - - @Test - void testListPromptsWithoutInitialization() { - verifyCallSucceedsWithImplicitInitialization(client -> client.listPrompts(McpSchema.FIRST_PAGE), - "listing " + "prompts"); - } - - @Test - void testListPrompts() { - withClient(createMcpTransport(), mcpAsyncClient -> { - StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listPrompts(McpSchema.FIRST_PAGE))) - .consumeNextWith(prompts -> { - assertThat(prompts).isNotNull().satisfies(result -> { - assertThat(result.prompts()).isNotNull(); - - if (!result.prompts().isEmpty()) { - Prompt firstPrompt = result.prompts().get(0); - assertThat(firstPrompt.name()).isNotNull(); - assertThat(firstPrompt.description()).isNotNull(); - } - }); - }) - .verifyComplete(); - }); - } - - @Test - void testListAllPrompts() { - withClient(createMcpTransport(), mcpAsyncClient -> { - StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listPrompts())) - .consumeNextWith(prompts -> { - assertThat(prompts).isNotNull().satisfies(result -> { - assertThat(result.prompts()).isNotNull(); - - if (!result.prompts().isEmpty()) { - Prompt firstPrompt = result.prompts().get(0); - assertThat(firstPrompt.name()).isNotNull(); - assertThat(firstPrompt.description()).isNotNull(); - } - }); - }) - .verifyComplete(); - }); - } - - @Test - void testListAllPromptsReturnsImmutableList() { - withClient(createMcpTransport(), mcpAsyncClient -> { - StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listPrompts())) - .consumeNextWith(result -> { - assertThat(result.prompts()).isNotNull(); - // Verify that the returned list is immutable - assertThatThrownBy(() -> result.prompts().add(new Prompt("test", "test", "test", null))) - .isInstanceOf(UnsupportedOperationException.class); - }) - .verifyComplete(); - }); - } - - @Test - void testGetPromptWithoutInitialization() { - GetPromptRequest request = new GetPromptRequest("simple_prompt", Map.of()); - verifyCallSucceedsWithImplicitInitialization(client -> client.getPrompt(request), "getting " + "prompts"); - } - - @Test - void testGetPrompt() { - withClient(createMcpTransport(), mcpAsyncClient -> { - StepVerifier - .create(mcpAsyncClient.initialize() - .then(mcpAsyncClient.getPrompt(new GetPromptRequest("simple_prompt", Map.of())))) - .consumeNextWith(prompt -> { - assertThat(prompt).isNotNull().satisfies(result -> { - assertThat(result.messages()).isNotEmpty(); - assertThat(result.messages()).hasSize(1); - }); - }) - .verifyComplete(); - }); - } - - @Test - void testRootsListChangedWithoutInitialization() { - verifyNotificationSucceedsWithImplicitInitialization(client -> client.rootsListChangedNotification(), - "sending roots list changed notification"); - } - - @Test - void testRootsListChanged() { - withClient(createMcpTransport(), mcpAsyncClient -> { - StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.rootsListChangedNotification())) - .verifyComplete(); - }); - } - - @Test - void testInitializeWithRootsListProviders() { - withClient(createMcpTransport(), builder -> builder.roots(new Root("file:///test/path", "test-root")), - client -> { - StepVerifier.create(client.initialize().then(client.closeGracefully())).verifyComplete(); - }); - } - - @Test - void testAddRoot() { - withClient(createMcpTransport(), mcpAsyncClient -> { - Root newRoot = new Root("file:///new/test/path", "new-test-root"); - StepVerifier.create(mcpAsyncClient.addRoot(newRoot)).verifyComplete(); - }); - } - - @Test - void testAddRootWithNullValue() { - withClient(createMcpTransport(), mcpAsyncClient -> { - StepVerifier.create(mcpAsyncClient.addRoot(null)) - .consumeErrorWith(e -> assertThat(e).isInstanceOf(IllegalArgumentException.class) - .hasMessage("Root must not be null")) - .verify(); - }); - } - - @Test - void testRemoveRoot() { - withClient(createMcpTransport(), mcpAsyncClient -> { - Root root = new Root("file:///test/path/to/remove", "root-to-remove"); - StepVerifier.create(mcpAsyncClient.addRoot(root)).verifyComplete(); - - StepVerifier.create(mcpAsyncClient.removeRoot(root.uri())).verifyComplete(); - }); - } - - @Test - void testRemoveNonExistentRoot() { - withClient(createMcpTransport(), mcpAsyncClient -> { - StepVerifier.create(mcpAsyncClient.removeRoot("nonexistent-uri")) - .consumeErrorWith(e -> assertThat(e).isInstanceOf(IllegalStateException.class) - .hasMessage("Root with uri 'nonexistent-uri' not found")) - .verify(); - }); - } - - @Test - void testReadResource() { - AtomicInteger resourceCount = new AtomicInteger(); - withClient(createMcpTransport(), client -> { - Flux resources = client.initialize() - .then(client.listResources(null)) - .flatMapMany(r -> { - List l = r.resources(); - resourceCount.set(l.size()); - return Flux.fromIterable(l); - }) - .flatMap(r -> client.readResource(r)); - - StepVerifier.create(resources) - .recordWith(ArrayList::new) - .thenConsumeWhile(res -> true) - .consumeRecordedWith(readResourceResults -> { - assertThat(readResourceResults.size()).isEqualTo(resourceCount.get()); - for (ReadResourceResult result : readResourceResults) { - - assertThat(result).isNotNull(); - assertThat(result.contents()).isNotNull().isNotEmpty(); - - // Validate each content item - for (ResourceContents content : result.contents()) { - assertThat(content).isNotNull(); - assertThat(content.uri()).isNotNull().isNotEmpty(); - assertThat(content.mimeType()).isNotNull().isNotEmpty(); - - // Validate content based on its type with more comprehensive - // checks - switch (content.mimeType()) { - case "text/plain" -> { - TextResourceContents textContent = assertInstanceOf(TextResourceContents.class, - content); - assertThat(textContent.text()).isNotNull().isNotEmpty(); - assertThat(textContent.uri()).isNotEmpty(); - } - case "application/octet-stream" -> { - BlobResourceContents blobContent = assertInstanceOf(BlobResourceContents.class, - content); - assertThat(blobContent.blob()).isNotNull().isNotEmpty(); - assertThat(blobContent.uri()).isNotNull().isNotEmpty(); - // Validate base64 encoding format - assertThat(blobContent.blob()).matches("^[A-Za-z0-9+/]*={0,2}$"); - } - default -> { - - // Still validate basic properties - if (content instanceof TextResourceContents textContent) { - assertThat(textContent.text()).isNotNull(); - } - else if (content instanceof BlobResourceContents blobContent) { - assertThat(blobContent.blob()).isNotNull(); - } - } - } - } - } - }) - .verifyComplete(); - }); - } - - @Test - void testListResourceTemplatesWithoutInitialization() { - verifyCallSucceedsWithImplicitInitialization(client -> client.listResourceTemplates(McpSchema.FIRST_PAGE), - "listing resource templates"); - } - - @Test - void testListResourceTemplates() { - withClient(createMcpTransport(), mcpAsyncClient -> { - StepVerifier - .create(mcpAsyncClient.initialize().then(mcpAsyncClient.listResourceTemplates(McpSchema.FIRST_PAGE))) - .consumeNextWith(result -> { - assertThat(result).isNotNull(); - assertThat(result.resourceTemplates()).isNotNull(); - }) - .verifyComplete(); - }); - } - - @Test - void testListAllResourceTemplates() { - withClient(createMcpTransport(), mcpAsyncClient -> { - StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listResourceTemplates())) - .consumeNextWith(result -> { - assertThat(result).isNotNull(); - assertThat(result.resourceTemplates()).isNotNull(); - }) - .verifyComplete(); - }); - } - - @Test - void testListAllResourceTemplatesReturnsImmutableList() { - withClient(createMcpTransport(), mcpAsyncClient -> { - StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listResourceTemplates())) - .consumeNextWith(result -> { - assertThat(result.resourceTemplates()).isNotNull(); - // Verify that the returned list is immutable - assertThatThrownBy(() -> result.resourceTemplates() - .add(new McpSchema.ResourceTemplate("test://template", "test", "test", null, null, null))) - .isInstanceOf(UnsupportedOperationException.class); - }) - .verifyComplete(); - }); - } - - // @Test - void testResourceSubscription() { - withClient(createMcpTransport(), mcpAsyncClient -> { - StepVerifier.create(mcpAsyncClient.listResources()).consumeNextWith(resources -> { - if (!resources.resources().isEmpty()) { - Resource firstResource = resources.resources().get(0); - - // Test subscribe - StepVerifier.create(mcpAsyncClient.subscribeResource(new SubscribeRequest(firstResource.uri()))) - .verifyComplete(); - - // Test unsubscribe - StepVerifier.create(mcpAsyncClient.unsubscribeResource(new UnsubscribeRequest(firstResource.uri()))) - .verifyComplete(); - } - }).verifyComplete(); - }); - } - - @Test - void testNotificationHandlers() { - AtomicBoolean toolsNotificationReceived = new AtomicBoolean(false); - AtomicBoolean resourcesNotificationReceived = new AtomicBoolean(false); - AtomicBoolean promptsNotificationReceived = new AtomicBoolean(false); - - withClient(createMcpTransport(), - builder -> builder - .toolsChangeConsumer(tools -> Mono.fromRunnable(() -> toolsNotificationReceived.set(true))) - .resourcesChangeConsumer( - resources -> Mono.fromRunnable(() -> resourcesNotificationReceived.set(true))) - .promptsChangeConsumer(prompts -> Mono.fromRunnable(() -> promptsNotificationReceived.set(true))), - mcpAsyncClient -> { - StepVerifier.create(mcpAsyncClient.initialize()) - .expectNextMatches(Objects::nonNull) - .verifyComplete(); - }); - } - - @Test - void testInitializeWithSamplingCapability() { - ClientCapabilities capabilities = ClientCapabilities.builder().sampling().build(); - CreateMessageResult createMessageResult = CreateMessageResult.builder() - .message("test") - .model("test-model") - .build(); - withClient(createMcpTransport(), - builder -> builder.capabilities(capabilities).sampling(request -> Mono.just(createMessageResult)), - client -> { - StepVerifier.create(client.initialize()).expectNextMatches(Objects::nonNull).verifyComplete(); - }); - } - - @Test - void testInitializeWithElicitationCapability() { - ClientCapabilities capabilities = ClientCapabilities.builder().elicitation().build(); - ElicitResult elicitResult = ElicitResult.builder() - .message(ElicitResult.Action.ACCEPT) - .content(Map.of("foo", "bar")) - .build(); - withClient(createMcpTransport(), - builder -> builder.capabilities(capabilities).elicitation(request -> Mono.just(elicitResult)), - client -> { - StepVerifier.create(client.initialize()).expectNextMatches(Objects::nonNull).verifyComplete(); - }); - } - - @Test - void testInitializeWithAllCapabilities() { - var capabilities = ClientCapabilities.builder() - .experimental(Map.of("feature", Map.of("featureFlag", true))) - .roots(true) - .sampling() - .build(); - - Function> samplingHandler = request -> Mono - .just(CreateMessageResult.builder().message("test").model("test-model").build()); - - Function> elicitationHandler = request -> Mono - .just(ElicitResult.builder().message(ElicitResult.Action.ACCEPT).content(Map.of("foo", "bar")).build()); - - withClient(createMcpTransport(), - builder -> builder.capabilities(capabilities).sampling(samplingHandler).elicitation(elicitationHandler), - client -> - - StepVerifier.create(client.initialize()).assertNext(result -> { - assertThat(result).isNotNull(); - assertThat(result.capabilities()).isNotNull(); - }).verifyComplete()); - } - // --------------------------------------- - // Logging Tests - // --------------------------------------- - - @Test - void testLoggingLevelsWithoutInitialization() { - verifyNotificationSucceedsWithImplicitInitialization( - client -> client.setLoggingLevel(McpSchema.LoggingLevel.DEBUG), "setting logging level"); - } - - @Test - void testLoggingLevels() { - withClient(createMcpTransport(), mcpAsyncClient -> { - StepVerifier - .create(mcpAsyncClient.initialize() - .thenMany(Flux.fromArray(McpSchema.LoggingLevel.values()).flatMap(mcpAsyncClient::setLoggingLevel))) - .verifyComplete(); - }); - } - - @Test - void testLoggingConsumer() { - AtomicBoolean logReceived = new AtomicBoolean(false); - - withClient(createMcpTransport(), - builder -> builder.loggingConsumer(notification -> Mono.fromRunnable(() -> logReceived.set(true))), - client -> { - StepVerifier.create(client.initialize()).expectNextMatches(Objects::nonNull).verifyComplete(); - StepVerifier.create(client.closeGracefully()).verifyComplete(); - - }); - - } - - @Test - void testLoggingWithNullNotification() { - withClient(createMcpTransport(), mcpAsyncClient -> { - StepVerifier.create(mcpAsyncClient.setLoggingLevel(null)) - .expectErrorMatches(error -> error.getMessage().contains("Logging level must not be null")) - .verify(); - }); - } - - @Test - void testSampling() { - McpClientTransport transport = createMcpTransport(); - - final String message = "Hello, world!"; - final String response = "Goodbye, world!"; - final int maxTokens = 100; - - AtomicReference receivedPrompt = new AtomicReference<>(); - AtomicReference receivedMessage = new AtomicReference<>(); - AtomicInteger receivedMaxTokens = new AtomicInteger(); - - withClient(transport, spec -> spec.capabilities(McpSchema.ClientCapabilities.builder().sampling().build()) - .sampling(request -> { - McpSchema.TextContent messageText = assertInstanceOf(McpSchema.TextContent.class, - request.messages().get(0).content()); - receivedPrompt.set(request.systemPrompt()); - receivedMessage.set(messageText.text()); - receivedMaxTokens.set(request.maxTokens()); - - return Mono - .just(new McpSchema.CreateMessageResult(McpSchema.Role.USER, new McpSchema.TextContent(response), - "modelId", McpSchema.CreateMessageResult.StopReason.END_TURN)); - }), client -> { - StepVerifier.create(client.initialize()).expectNextMatches(Objects::nonNull).verifyComplete(); - - StepVerifier.create(client.callTool( - new McpSchema.CallToolRequest("sampleLLM", Map.of("prompt", message, "maxTokens", maxTokens)))) - .consumeNextWith(result -> { - // Verify tool response to ensure our sampling response was passed - // through - assertThat(result.content()).hasAtLeastOneElementOfType(McpSchema.TextContent.class); - assertThat(result.content()).allSatisfy(content -> { - if (!(content instanceof McpSchema.TextContent text)) - return; - - assertThat(text.text()).contains(response); - }); - - // Verify sampling request parameters received in our callback - assertThat(receivedPrompt.get()).isNotEmpty(); - assertThat(receivedMessage.get()).endsWith(message); // Prefixed - assertThat(receivedMaxTokens.get()).isEqualTo(maxTokens); - }) - .verifyComplete(); - }); - } - - // --------------------------------------- - // Progress Notification Tests - // --------------------------------------- - - @Test - void testProgressConsumer() { - Sinks.Many sink = Sinks.many().unicast().onBackpressureBuffer(); - List receivedNotifications = new CopyOnWriteArrayList<>(); - - withClient(createMcpTransport(), builder -> builder.progressConsumer(notification -> { - receivedNotifications.add(notification); - sink.tryEmitNext(notification); - return Mono.empty(); - }), client -> { - StepVerifier.create(client.initialize()).expectNextMatches(Objects::nonNull).verifyComplete(); - - // Call a tool that sends progress notifications - CallToolRequest request = CallToolRequest.builder() - .name("longRunningOperation") - .arguments(Map.of("duration", 1, "steps", 2)) - .progressToken("test-token") - .build(); - - StepVerifier.create(client.callTool(request)).consumeNextWith(result -> { - assertThat(result).isNotNull(); - }).verifyComplete(); - - // Use StepVerifier to verify the progress notifications via the sink - StepVerifier.create(sink.asFlux()).expectNextCount(2).thenCancel().verify(Duration.ofSeconds(3)); - - assertThat(receivedNotifications).hasSize(2); - assertThat(receivedNotifications.get(0).progressToken()).isEqualTo("test-token"); - }); - } - -} diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java deleted file mode 100644 index c67fa86..0000000 --- a/mcp-core/src/test/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java +++ /dev/null @@ -1,682 +0,0 @@ -/* - * Copyright 2024-2024 the original author or authors. - */ - -package io.modelcontextprotocol.client; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.api.Assertions.fail; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; - -import java.time.Duration; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; -import java.util.function.Function; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import io.modelcontextprotocol.spec.McpClientTransport; -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.BlobResourceContents; -import io.modelcontextprotocol.spec.McpSchema.CallToolRequest; -import io.modelcontextprotocol.spec.McpSchema.CallToolResult; -import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities; -import io.modelcontextprotocol.spec.McpSchema.ListResourceTemplatesResult; -import io.modelcontextprotocol.spec.McpSchema.ListResourcesResult; -import io.modelcontextprotocol.spec.McpSchema.ListToolsResult; -import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult; -import io.modelcontextprotocol.spec.McpSchema.Resource; -import io.modelcontextprotocol.spec.McpSchema.ResourceContents; -import io.modelcontextprotocol.spec.McpSchema.Root; -import io.modelcontextprotocol.spec.McpSchema.SubscribeRequest; -import io.modelcontextprotocol.spec.McpSchema.TextContent; -import io.modelcontextprotocol.spec.McpSchema.TextResourceContents; -import io.modelcontextprotocol.spec.McpSchema.Tool; -import io.modelcontextprotocol.spec.McpSchema.UnsubscribeRequest; -import reactor.core.publisher.Mono; -import reactor.core.scheduler.Schedulers; -import reactor.test.StepVerifier; - -/** - * Unit tests for MCP Client Session functionality. - * - * @author Christian Tzolov - * @author Dariusz Jędrzejczyk - */ -// KEEP IN SYNC with the class in mcp-test module -public abstract class AbstractMcpSyncClientTests { - - private static final Logger logger = LoggerFactory.getLogger(AbstractMcpSyncClientTests.class); - - private static final String TEST_MESSAGE = "Hello MCP Spring AI!"; - - abstract protected McpClientTransport createMcpTransport(); - - protected Duration getRequestTimeout() { - return Duration.ofSeconds(14); - } - - protected Duration getInitializationTimeout() { - return Duration.ofSeconds(2); - } - - McpSyncClient client(McpClientTransport transport) { - return client(transport, Function.identity()); - } - - McpSyncClient client(McpClientTransport transport, Function customizer) { - AtomicReference client = new AtomicReference<>(); - - assertThatCode(() -> { - McpClient.SyncSpec builder = McpClient.sync(transport) - .requestTimeout(getRequestTimeout()) - .initializationTimeout(getInitializationTimeout()) - .capabilities(ClientCapabilities.builder().roots(true).build()); - builder = customizer.apply(builder); - client.set(builder.build()); - }).doesNotThrowAnyException(); - - return client.get(); - } - - void withClient(McpClientTransport transport, Consumer c) { - withClient(transport, Function.identity(), c); - } - - void withClient(McpClientTransport transport, Function customizer, - Consumer c) { - var client = client(transport, customizer); - try { - c.accept(client); - } - finally { - assertThat(client.closeGracefully()).isTrue(); - } - } - - static final Object DUMMY_RETURN_VALUE = new Object(); - - void verifyNotificationSucceedsWithImplicitInitialization(Consumer operation, String action) { - verifyCallSucceedsWithImplicitInitialization(client -> { - operation.accept(client); - return DUMMY_RETURN_VALUE; - }, action); - } - - void verifyCallSucceedsWithImplicitInitialization(Function blockingOperation, String action) { - withClient(createMcpTransport(), mcpSyncClient -> { - StepVerifier.create(Mono.fromSupplier(() -> blockingOperation.apply(mcpSyncClient)) - // Offload the blocking call to the real scheduler - .subscribeOn(Schedulers.boundedElastic())).expectNextCount(1).verifyComplete(); - }); - } - - @Test - void testConstructorWithInvalidArguments() { - assertThatThrownBy(() -> McpClient.sync(null).build()).isInstanceOf(IllegalArgumentException.class) - .hasMessage("Transport must not be null"); - - assertThatThrownBy(() -> McpClient.sync(createMcpTransport()).requestTimeout(null).build()) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Request timeout must not be null"); - } - - @Test - void testListToolsWithoutInitialization() { - verifyCallSucceedsWithImplicitInitialization(client -> client.listTools(McpSchema.FIRST_PAGE), "listing tools"); - } - - @Test - void testListTools() { - withClient(createMcpTransport(), mcpSyncClient -> { - mcpSyncClient.initialize(); - ListToolsResult tools = mcpSyncClient.listTools(McpSchema.FIRST_PAGE); - - assertThat(tools).isNotNull().satisfies(result -> { - assertThat(result.tools()).isNotNull().isNotEmpty(); - - Tool firstTool = result.tools().get(0); - assertThat(firstTool.name()).isNotNull(); - assertThat(firstTool.description()).isNotNull(); - }); - }); - } - - @Test - void testListAllTools() { - withClient(createMcpTransport(), mcpSyncClient -> { - mcpSyncClient.initialize(); - ListToolsResult tools = mcpSyncClient.listTools(); - - assertThat(tools).isNotNull().satisfies(result -> { - assertThat(result.tools()).isNotNull().isNotEmpty(); - - Tool firstTool = result.tools().get(0); - assertThat(firstTool.name()).isNotNull(); - assertThat(firstTool.description()).isNotNull(); - }); - }); - } - - @Test - void testCallToolsWithoutInitialization() { - verifyCallSucceedsWithImplicitInitialization( - client -> client.callTool(new CallToolRequest("add", Map.of("a", 3, "b", 4))), "calling tools"); - } - - @Test - void testCallTools() { - withClient(createMcpTransport(), mcpSyncClient -> { - mcpSyncClient.initialize(); - CallToolResult toolResult = mcpSyncClient.callTool(new CallToolRequest("add", Map.of("a", 3, "b", 4))); - - assertThat(toolResult).isNotNull().satisfies(result -> { - - assertThat(result.content()).hasSize(1); - - TextContent content = (TextContent) result.content().get(0); - - assertThat(content).isNotNull(); - assertThat(content.text()).isNotNull(); - assertThat(content.text()).contains("7"); - }); - }); - } - - @Test - void testPingWithoutInitialization() { - verifyCallSucceedsWithImplicitInitialization(client -> client.ping(), "pinging the server"); - } - - @Test - void testPing() { - withClient(createMcpTransport(), mcpSyncClient -> { - mcpSyncClient.initialize(); - assertThatCode(() -> mcpSyncClient.ping()).doesNotThrowAnyException(); - }); - } - - @Test - void testCallToolWithoutInitialization() { - CallToolRequest callToolRequest = new CallToolRequest("echo", Map.of("message", TEST_MESSAGE)); - verifyCallSucceedsWithImplicitInitialization(client -> client.callTool(callToolRequest), "calling tools"); - } - - @Test - void testCallTool() { - withClient(createMcpTransport(), mcpSyncClient -> { - mcpSyncClient.initialize(); - CallToolRequest callToolRequest = new CallToolRequest("echo", Map.of("message", TEST_MESSAGE)); - - CallToolResult callToolResult = mcpSyncClient.callTool(callToolRequest); - - assertThat(callToolResult).isNotNull().satisfies(result -> { - assertThat(result.content()).isNotNull(); - assertThat(result.isError()).isNull(); - }); - }); - } - - @Test - void testCallToolWithInvalidTool() { - withClient(createMcpTransport(), mcpSyncClient -> { - CallToolRequest invalidRequest = new CallToolRequest("nonexistent_tool", Map.of("message", TEST_MESSAGE)); - - assertThatThrownBy(() -> mcpSyncClient.callTool(invalidRequest)).isInstanceOf(Exception.class); - }); - } - - @ParameterizedTest - @ValueSource(strings = { "success", "error", "debug" }) - void testCallToolWithMessageAnnotations(String messageType) { - McpClientTransport transport = createMcpTransport(); - - withClient(transport, client -> { - client.initialize(); - - McpSchema.CallToolResult result = client.callTool(new McpSchema.CallToolRequest("annotatedMessage", - Map.of("messageType", messageType, "includeImage", true))); - - assertThat(result).isNotNull(); - assertThat(result.isError()).isNotEqualTo(true); - assertThat(result.content()).isNotEmpty(); - assertThat(result.content()).allSatisfy(content -> { - switch (content.type()) { - case "text": - McpSchema.TextContent textContent = assertInstanceOf(McpSchema.TextContent.class, content); - assertThat(textContent.text()).isNotEmpty(); - assertThat(textContent.annotations()).isNotNull(); - - switch (messageType) { - case "error": - assertThat(textContent.annotations().priority()).isEqualTo(1.0); - assertThat(textContent.annotations().audience()).containsOnly(McpSchema.Role.USER, - McpSchema.Role.ASSISTANT); - break; - case "success": - assertThat(textContent.annotations().priority()).isEqualTo(0.7); - assertThat(textContent.annotations().audience()).containsExactly(McpSchema.Role.USER); - break; - case "debug": - assertThat(textContent.annotations().priority()).isEqualTo(0.3); - assertThat(textContent.annotations().audience()) - .containsExactly(McpSchema.Role.ASSISTANT); - break; - default: - throw new IllegalStateException("Unexpected value: " + content.type()); - } - break; - case "image": - McpSchema.ImageContent imageContent = assertInstanceOf(McpSchema.ImageContent.class, content); - assertThat(imageContent.data()).isNotEmpty(); - assertThat(imageContent.annotations()).isNotNull(); - assertThat(imageContent.annotations().priority()).isEqualTo(0.5); - assertThat(imageContent.annotations().audience()).containsExactly(McpSchema.Role.USER); - break; - default: - fail("Unexpected content type: " + content.type()); - } - }); - }); - } - - @Test - void testRootsListChangedWithoutInitialization() { - verifyNotificationSucceedsWithImplicitInitialization(client -> client.rootsListChangedNotification(), - "sending roots list changed notification"); - } - - @Test - void testRootsListChanged() { - withClient(createMcpTransport(), mcpSyncClient -> { - mcpSyncClient.initialize(); - assertThatCode(() -> mcpSyncClient.rootsListChangedNotification()).doesNotThrowAnyException(); - }); - } - - @Test - void testListResourcesWithoutInitialization() { - verifyCallSucceedsWithImplicitInitialization(client -> client.listResources(McpSchema.FIRST_PAGE), - "listing resources"); - } - - @Test - void testListResources() { - withClient(createMcpTransport(), mcpSyncClient -> { - mcpSyncClient.initialize(); - ListResourcesResult resources = mcpSyncClient.listResources(McpSchema.FIRST_PAGE); - - assertThat(resources).isNotNull().satisfies(result -> { - assertThat(result.resources()).isNotNull(); - - if (!result.resources().isEmpty()) { - Resource firstResource = result.resources().get(0); - assertThat(firstResource.uri()).isNotNull(); - assertThat(firstResource.name()).isNotNull(); - } - }); - }); - } - - @Test - void testListAllResources() { - withClient(createMcpTransport(), mcpSyncClient -> { - mcpSyncClient.initialize(); - ListResourcesResult resources = mcpSyncClient.listResources(); - - assertThat(resources).isNotNull().satisfies(result -> { - assertThat(result.resources()).isNotNull(); - - if (!result.resources().isEmpty()) { - Resource firstResource = result.resources().get(0); - assertThat(firstResource.uri()).isNotNull(); - assertThat(firstResource.name()).isNotNull(); - } - }); - }); - } - - @Test - void testClientSessionState() { - withClient(createMcpTransport(), mcpSyncClient -> { - assertThat(mcpSyncClient).isNotNull(); - }); - } - - @Test - void testInitializeWithRootsListProviders() { - withClient(createMcpTransport(), builder -> builder.roots(new Root("file:///test/path", "test-root")), - mcpSyncClient -> { - - assertThatCode(() -> { - mcpSyncClient.initialize(); - mcpSyncClient.close(); - }).doesNotThrowAnyException(); - }); - } - - @Test - void testAddRoot() { - withClient(createMcpTransport(), mcpSyncClient -> { - Root newRoot = new Root("file:///new/test/path", "new-test-root"); - assertThatCode(() -> mcpSyncClient.addRoot(newRoot)).doesNotThrowAnyException(); - }); - } - - @Test - void testAddRootWithNullValue() { - withClient(createMcpTransport(), mcpSyncClient -> { - assertThatThrownBy(() -> mcpSyncClient.addRoot(null)).hasMessageContaining("Root must not be null"); - }); - } - - @Test - void testRemoveRoot() { - withClient(createMcpTransport(), mcpSyncClient -> { - Root root = new Root("file:///test/path/to/remove", "root-to-remove"); - assertThatCode(() -> { - mcpSyncClient.addRoot(root); - mcpSyncClient.removeRoot(root.uri()); - }).doesNotThrowAnyException(); - }); - } - - @Test - void testRemoveNonExistentRoot() { - withClient(createMcpTransport(), mcpSyncClient -> { - assertThatThrownBy(() -> mcpSyncClient.removeRoot("nonexistent-uri")) - .hasMessageContaining("Root with uri 'nonexistent-uri' not found"); - }); - } - - @Test - void testReadResourceWithoutInitialization() { - AtomicReference> resources = new AtomicReference<>(); - withClient(createMcpTransport(), mcpSyncClient -> { - mcpSyncClient.initialize(); - resources.set(mcpSyncClient.listResources().resources()); - }); - - verifyCallSucceedsWithImplicitInitialization(client -> client.readResource(resources.get().get(0)), - "reading resources"); - } - - @Test - void testReadResource() { - withClient(createMcpTransport(), mcpSyncClient -> { - - int readResourceCount = 0; - - mcpSyncClient.initialize(); - ListResourcesResult resources = mcpSyncClient.listResources(null); - - assertThat(resources).isNotNull(); - assertThat(resources.resources()).isNotNull(); - - assertThat(resources.resources()).isNotNull().isNotEmpty(); - - // Test reading each resource individually for better error isolation - for (Resource resource : resources.resources()) { - ReadResourceResult result = mcpSyncClient.readResource(resource); - - assertThat(result).isNotNull(); - assertThat(result.contents()).isNotNull().isNotEmpty(); - - readResourceCount++; - - // Validate each content item - for (ResourceContents content : result.contents()) { - assertThat(content).isNotNull(); - assertThat(content.uri()).isNotNull().isNotEmpty(); - assertThat(content.mimeType()).isNotNull().isNotEmpty(); - - // Validate content based on its type with more comprehensive - // checks - switch (content.mimeType()) { - case "text/plain" -> { - TextResourceContents textContent = assertInstanceOf(TextResourceContents.class, content); - assertThat(textContent.text()).isNotNull().isNotEmpty(); - // Verify URI consistency - assertThat(textContent.uri()).isEqualTo(resource.uri()); - } - case "application/octet-stream" -> { - BlobResourceContents blobContent = assertInstanceOf(BlobResourceContents.class, content); - assertThat(blobContent.blob()).isNotNull().isNotEmpty(); - // Verify URI consistency - assertThat(blobContent.uri()).isEqualTo(resource.uri()); - // Validate base64 encoding format - assertThat(blobContent.blob()).matches("^[A-Za-z0-9+/]*={0,2}$"); - } - default -> { - // More flexible handling of additional MIME types - // Log the unexpected type for debugging but don't fail - // the test - logger.warn("Warning: Encountered unexpected MIME type: {} for resource: {}", - content.mimeType(), resource.uri()); - - // Still validate basic properties - if (content instanceof TextResourceContents textContent) { - assertThat(textContent.text()).isNotNull(); - } - else if (content instanceof BlobResourceContents blobContent) { - assertThat(blobContent.blob()).isNotNull(); - } - } - } - } - } - - // Assert that we read exactly 10 resources - assertThat(readResourceCount).isEqualTo(10); - }); - } - - @Test - void testListResourceTemplatesWithoutInitialization() { - verifyCallSucceedsWithImplicitInitialization(client -> client.listResourceTemplates(McpSchema.FIRST_PAGE), - "listing resource templates"); - } - - @Test - void testListResourceTemplates() { - withClient(createMcpTransport(), mcpSyncClient -> { - mcpSyncClient.initialize(); - ListResourceTemplatesResult result = mcpSyncClient.listResourceTemplates(McpSchema.FIRST_PAGE); - - assertThat(result).isNotNull(); - assertThat(result.resourceTemplates()).isNotNull(); - }); - } - - @Test - void testListAllResourceTemplates() { - withClient(createMcpTransport(), mcpSyncClient -> { - mcpSyncClient.initialize(); - ListResourceTemplatesResult result = mcpSyncClient.listResourceTemplates(); - - assertThat(result).isNotNull(); - assertThat(result.resourceTemplates()).isNotNull(); - }); - } - - // @Test - void testResourceSubscription() { - withClient(createMcpTransport(), mcpSyncClient -> { - ListResourcesResult resources = mcpSyncClient.listResources(null); - - if (!resources.resources().isEmpty()) { - Resource firstResource = resources.resources().get(0); - - // Test subscribe - assertThatCode(() -> mcpSyncClient.subscribeResource(new SubscribeRequest(firstResource.uri()))) - .doesNotThrowAnyException(); - - // Test unsubscribe - assertThatCode(() -> mcpSyncClient.unsubscribeResource(new UnsubscribeRequest(firstResource.uri()))) - .doesNotThrowAnyException(); - } - }); - } - - @Test - void testNotificationHandlers() { - AtomicBoolean toolsNotificationReceived = new AtomicBoolean(false); - AtomicBoolean resourcesNotificationReceived = new AtomicBoolean(false); - AtomicBoolean promptsNotificationReceived = new AtomicBoolean(false); - AtomicBoolean resourcesUpdatedNotificationReceived = new AtomicBoolean(false); - - withClient(createMcpTransport(), - builder -> builder.toolsChangeConsumer(tools -> toolsNotificationReceived.set(true)) - .resourcesChangeConsumer(resources -> resourcesNotificationReceived.set(true)) - .promptsChangeConsumer(prompts -> promptsNotificationReceived.set(true)) - .resourcesUpdateConsumer(resources -> resourcesUpdatedNotificationReceived.set(true)), - client -> { - - assertThatCode(() -> { - client.initialize(); - client.close(); - }).doesNotThrowAnyException(); - }); - } - - // --------------------------------------- - // Logging Tests - // --------------------------------------- - - @Test - void testLoggingLevelsWithoutInitialization() { - verifyNotificationSucceedsWithImplicitInitialization( - client -> client.setLoggingLevel(McpSchema.LoggingLevel.DEBUG), "setting logging level"); - } - - @Test - void testLoggingLevels() { - withClient(createMcpTransport(), mcpSyncClient -> { - mcpSyncClient.initialize(); - // Test all logging levels - for (McpSchema.LoggingLevel level : McpSchema.LoggingLevel.values()) { - assertThatCode(() -> mcpSyncClient.setLoggingLevel(level)).doesNotThrowAnyException(); - } - }); - } - - @Test - void testLoggingConsumer() { - AtomicBoolean logReceived = new AtomicBoolean(false); - withClient(createMcpTransport(), builder -> builder.requestTimeout(getRequestTimeout()) - .loggingConsumer(notification -> logReceived.set(true)), client -> { - assertThatCode(() -> { - client.initialize(); - client.close(); - }).doesNotThrowAnyException(); - }); - } - - @Test - void testLoggingWithNullNotification() { - withClient(createMcpTransport(), mcpSyncClient -> assertThatThrownBy(() -> mcpSyncClient.setLoggingLevel(null)) - .hasMessageContaining("Logging level must not be null")); - } - - @Test - void testSampling() { - McpClientTransport transport = createMcpTransport(); - - final String message = "Hello, world!"; - final String response = "Goodbye, world!"; - final int maxTokens = 100; - - AtomicReference receivedPrompt = new AtomicReference<>(); - AtomicReference receivedMessage = new AtomicReference<>(); - AtomicInteger receivedMaxTokens = new AtomicInteger(); - - withClient(transport, spec -> spec.capabilities(McpSchema.ClientCapabilities.builder().sampling().build()) - .sampling(request -> { - McpSchema.TextContent messageText = assertInstanceOf(McpSchema.TextContent.class, - request.messages().get(0).content()); - receivedPrompt.set(request.systemPrompt()); - receivedMessage.set(messageText.text()); - receivedMaxTokens.set(request.maxTokens()); - - return new McpSchema.CreateMessageResult(McpSchema.Role.USER, new McpSchema.TextContent(response), - "modelId", McpSchema.CreateMessageResult.StopReason.END_TURN); - }), client -> { - client.initialize(); - - McpSchema.CallToolResult result = client.callTool( - new McpSchema.CallToolRequest("sampleLLM", Map.of("prompt", message, "maxTokens", maxTokens))); - - // Verify tool response to ensure our sampling response was passed through - assertThat(result.content()).hasAtLeastOneElementOfType(McpSchema.TextContent.class); - assertThat(result.content()).allSatisfy(content -> { - if (!(content instanceof McpSchema.TextContent text)) - return; - - assertThat(text.text()).contains(response); - }); - - // Verify sampling request parameters received in our callback - assertThat(receivedPrompt.get()).isNotEmpty(); - assertThat(receivedMessage.get()).endsWith(message); // Prefixed - assertThat(receivedMaxTokens.get()).isEqualTo(maxTokens); - }); - } - - // --------------------------------------- - // Progress Notification Tests - // --------------------------------------- - - @Test - void testProgressConsumer() { - AtomicInteger progressNotificationCount = new AtomicInteger(0); - List receivedNotifications = new CopyOnWriteArrayList<>(); - CountDownLatch latch = new CountDownLatch(2); - - withClient(createMcpTransport(), builder -> builder.progressConsumer(notification -> { - System.out.println("Received progress notification: " + notification); - receivedNotifications.add(notification); - progressNotificationCount.incrementAndGet(); - latch.countDown(); - }), client -> { - client.initialize(); - - // Call a tool that sends progress notifications - CallToolRequest request = CallToolRequest.builder() - .name("longRunningOperation") - .arguments(Map.of("duration", 1, "steps", 2)) - .progressToken("test-token") - .build(); - - CallToolResult result = client.callTool(request); - - assertThat(result).isNotNull(); - - try { - // Wait for progress notifications to be processed - latch.await(3, TimeUnit.SECONDS); - } - catch (InterruptedException e) { - e.printStackTrace(); - } - - assertThat(progressNotificationCount.get()).isEqualTo(2); - - assertThat(receivedNotifications).isNotEmpty(); - assertThat(receivedNotifications.get(0).progressToken()).isEqualTo("test-token"); - }); - } - -} diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java deleted file mode 100644 index 0907102..0000000 --- a/mcp-core/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java +++ /dev/null @@ -1,722 +0,0 @@ -/* - * Copyright 2024-2024 the original author or authors. - */ - -package io.modelcontextprotocol.server; - -import java.time.Duration; -import java.util.List; - -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.CallToolResult; -import io.modelcontextprotocol.spec.McpSchema.GetPromptResult; -import io.modelcontextprotocol.spec.McpSchema.Prompt; -import io.modelcontextprotocol.spec.McpSchema.PromptMessage; -import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult; -import io.modelcontextprotocol.spec.McpSchema.Resource; -import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities; -import io.modelcontextprotocol.spec.McpSchema.Tool; -import io.modelcontextprotocol.spec.McpServerTransportProvider; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - -import static io.modelcontextprotocol.util.ToolsUtils.EMPTY_JSON_SCHEMA; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -/** - * Test suite for the {@link McpAsyncServer} that can be used with different - * {@link io.modelcontextprotocol.spec.McpServerTransportProvider} implementations. - * - * @author Christian Tzolov - */ -// KEEP IN SYNC with the class in mcp-test module -public abstract class AbstractMcpAsyncServerTests { - - private static final String TEST_TOOL_NAME = "test-tool"; - - private static final String TEST_RESOURCE_URI = "test://resource"; - - private static final String TEST_PROMPT_NAME = "test-prompt"; - - abstract protected McpServer.AsyncSpecification prepareAsyncServerBuilder(); - - protected void onStart() { - } - - protected void onClose() { - } - - @BeforeEach - void setUp() { - } - - @AfterEach - void tearDown() { - onClose(); - } - - // --------------------------------------- - // Server Lifecycle Tests - // --------------------------------------- - void testConstructorWithInvalidArguments() { - assertThatThrownBy(() -> McpServer.async((McpServerTransportProvider) null)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Transport provider must not be null"); - - assertThatThrownBy(() -> prepareAsyncServerBuilder().serverInfo((McpSchema.Implementation) null)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Server info must not be null"); - } - - @Test - void testGracefulShutdown() { - McpServer.AsyncSpecification builder = prepareAsyncServerBuilder(); - var mcpAsyncServer = builder.serverInfo("test-server", "1.0.0").build(); - - StepVerifier.create(mcpAsyncServer.closeGracefully()).verifyComplete(); - } - - @Test - void testImmediateClose() { - var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").build(); - - assertThatCode(mcpAsyncServer::close).doesNotThrowAnyException(); - } - - // --------------------------------------- - // Tools Tests - // --------------------------------------- - @Test - @Deprecated - void testAddTool() { - Tool newTool = McpSchema.Tool.builder() - .name("new-tool") - .title("New test tool") - .inputSchema(EMPTY_JSON_SCHEMA) - .build(); - var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .build(); - - StepVerifier - .create(mcpAsyncServer.addTool(new McpServerFeatures.AsyncToolSpecification(newTool, - (exchange, args) -> Mono.just(CallToolResult.builder().content(List.of()).isError(false).build())))) - .verifyComplete(); - - assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); - } - - @Test - void testAddToolCall() { - Tool newTool = McpSchema.Tool.builder() - .name("new-tool") - .title("New test tool") - .inputSchema(EMPTY_JSON_SCHEMA) - .build(); - - var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .build(); - - StepVerifier.create(mcpAsyncServer.addTool(McpServerFeatures.AsyncToolSpecification.builder() - .tool(newTool) - .callHandler((exchange, request) -> Mono - .just(CallToolResult.builder().content(List.of()).isError(false).build())) - .build())).verifyComplete(); - - assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); - } - - @Test - @Deprecated - void testAddDuplicateTool() { - Tool duplicateTool = McpSchema.Tool.builder() - .name(TEST_TOOL_NAME) - .title("Duplicate tool") - .inputSchema(EMPTY_JSON_SCHEMA) - .build(); - - var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .tool(duplicateTool, - (exchange, args) -> Mono.just(CallToolResult.builder().content(List.of()).isError(false).build())) - .build(); - - StepVerifier - .create(mcpAsyncServer.addTool(new McpServerFeatures.AsyncToolSpecification(duplicateTool, - (exchange, args) -> Mono.just(CallToolResult.builder().content(List.of()).isError(false).build())))) - .verifyComplete(); - - assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); - } - - @Test - void testAddDuplicateToolCall() { - Tool duplicateTool = McpSchema.Tool.builder() - .name(TEST_TOOL_NAME) - .title("Duplicate tool") - .inputSchema(EMPTY_JSON_SCHEMA) - .build(); - - var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .toolCall(duplicateTool, - (exchange, request) -> Mono - .just(CallToolResult.builder().content(List.of()).isError(false).build())) - .build(); - - StepVerifier.create(mcpAsyncServer.addTool(McpServerFeatures.AsyncToolSpecification.builder() - .tool(duplicateTool) - .callHandler((exchange, request) -> Mono - .just(CallToolResult.builder().content(List.of()).isError(false).build())) - .build())).verifyComplete(); - - assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); - } - - @Test - void testDuplicateToolCallDuringBuilding() { - Tool duplicateTool = McpSchema.Tool.builder() - .name("duplicate-build-toolcall") - .title("Duplicate toolcall during building") - .inputSchema(EMPTY_JSON_SCHEMA) - .build(); - - assertThatThrownBy(() -> prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .toolCall(duplicateTool, - (exchange, request) -> Mono - .just(CallToolResult.builder().content(List.of()).isError(false).build())) - .toolCall(duplicateTool, - (exchange, request) -> Mono - .just(CallToolResult.builder().content(List.of()).isError(false).build())) // Duplicate! - .build()).isInstanceOf(IllegalArgumentException.class) - .hasMessage("Tool with name 'duplicate-build-toolcall' is already registered."); - } - - @Test - void testDuplicateToolsInBatchListRegistration() { - Tool duplicateTool = McpSchema.Tool.builder() - .name("batch-list-tool") - .title("Duplicate tool in batch list") - .inputSchema(EMPTY_JSON_SCHEMA) - .build(); - - List specs = List.of( - McpServerFeatures.AsyncToolSpecification.builder() - .tool(duplicateTool) - .callHandler((exchange, request) -> Mono - .just(CallToolResult.builder().content(List.of()).isError(false).build())) - .build(), - McpServerFeatures.AsyncToolSpecification.builder() - .tool(duplicateTool) - .callHandler((exchange, request) -> Mono - .just(CallToolResult.builder().content(List.of()).isError(false).build())) - .build() // Duplicate! - ); - - assertThatThrownBy(() -> prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .tools(specs) - .build()).isInstanceOf(IllegalArgumentException.class) - .hasMessage("Tool with name 'batch-list-tool' is already registered."); - } - - @Test - void testDuplicateToolsInBatchVarargsRegistration() { - Tool duplicateTool = McpSchema.Tool.builder() - .name("batch-varargs-tool") - .title("Duplicate tool in batch varargs") - .inputSchema(EMPTY_JSON_SCHEMA) - .build(); - - assertThatThrownBy(() -> prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .tools(McpServerFeatures.AsyncToolSpecification.builder() - .tool(duplicateTool) - .callHandler((exchange, request) -> Mono - .just(CallToolResult.builder().content(List.of()).isError(false).build())) - .build(), - McpServerFeatures.AsyncToolSpecification.builder() - .tool(duplicateTool) - .callHandler((exchange, request) -> Mono - .just(CallToolResult.builder().content(List.of()).isError(false).build())) - .build() // Duplicate! - ) - .build()).isInstanceOf(IllegalArgumentException.class) - .hasMessage("Tool with name 'batch-varargs-tool' is already registered."); - } - - @Test - void testRemoveTool() { - Tool too = McpSchema.Tool.builder() - .name(TEST_TOOL_NAME) - .title("Duplicate tool") - .inputSchema(EMPTY_JSON_SCHEMA) - .build(); - - var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .toolCall(too, - (exchange, request) -> Mono - .just(CallToolResult.builder().content(List.of()).isError(false).build())) - .build(); - - StepVerifier.create(mcpAsyncServer.removeTool(TEST_TOOL_NAME)).verifyComplete(); - - assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); - } - - @Test - void testRemoveNonexistentTool() { - var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .build(); - - StepVerifier.create(mcpAsyncServer.removeTool("nonexistent-tool")).verifyComplete(); - - assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); - } - - @Test - void testNotifyToolsListChanged() { - Tool too = McpSchema.Tool.builder() - .name(TEST_TOOL_NAME) - .title("Duplicate tool") - .inputSchema(EMPTY_JSON_SCHEMA) - .build(); - - var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .toolCall(too, - (exchange, args) -> Mono.just(CallToolResult.builder().content(List.of()).isError(false).build())) - .build(); - - StepVerifier.create(mcpAsyncServer.notifyToolsListChanged()).verifyComplete(); - - assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); - } - - // --------------------------------------- - // Resources Tests - // --------------------------------------- - - @Test - void testNotifyResourcesListChanged() { - var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").build(); - - StepVerifier.create(mcpAsyncServer.notifyResourcesListChanged()).verifyComplete(); - - assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); - } - - @Test - void testNotifyResourcesUpdated() { - var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").build(); - - StepVerifier - .create(mcpAsyncServer - .notifyResourcesUpdated(new McpSchema.ResourcesUpdatedNotification(TEST_RESOURCE_URI))) - .verifyComplete(); - - assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); - } - - @Test - void testAddResource() { - var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().resources(true, false).build()) - .build(); - - Resource resource = Resource.builder() - .uri(TEST_RESOURCE_URI) - .name("Test Resource") - .title("Test Resource") - .mimeType("text/plain") - .description("Test resource description") - .build(); - McpServerFeatures.AsyncResourceSpecification specification = new McpServerFeatures.AsyncResourceSpecification( - resource, (exchange, req) -> Mono.just(new ReadResourceResult(List.of()))); - - StepVerifier.create(mcpAsyncServer.addResource(specification)).verifyComplete(); - - assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); - } - - @Test - void testAddResourceWithNullSpecification() { - var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().resources(true, false).build()) - .build(); - - StepVerifier.create(mcpAsyncServer.addResource((McpServerFeatures.AsyncResourceSpecification) null)) - .verifyErrorSatisfies(error -> { - assertThat(error).isInstanceOf(IllegalArgumentException.class).hasMessage("Resource must not be null"); - }); - - assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); - } - - @Test - void testAddResourceWithoutCapability() { - // Create a server without resource capabilities - McpAsyncServer serverWithoutResources = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").build(); - - Resource resource = Resource.builder() - .uri(TEST_RESOURCE_URI) - .name("Test Resource") - .title("Test Resource") - .mimeType("text/plain") - .description("Test resource description") - .build(); - McpServerFeatures.AsyncResourceSpecification specification = new McpServerFeatures.AsyncResourceSpecification( - resource, (exchange, req) -> Mono.just(new ReadResourceResult(List.of()))); - - StepVerifier.create(serverWithoutResources.addResource(specification)).verifyErrorSatisfies(error -> { - assertThat(error).isInstanceOf(IllegalStateException.class) - .hasMessageContaining("Server must be configured with resource capabilities"); - }); - } - - @Test - void testRemoveResourceWithoutCapability() { - // Create a server without resource capabilities - McpAsyncServer serverWithoutResources = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").build(); - - StepVerifier.create(serverWithoutResources.removeResource(TEST_RESOURCE_URI)).verifyErrorSatisfies(error -> { - assertThat(error).isInstanceOf(IllegalStateException.class) - .hasMessageContaining("Server must be configured with resource capabilities"); - }); - } - - @Test - void testListResources() { - var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().resources(true, false).build()) - .build(); - - Resource resource = Resource.builder() - .uri(TEST_RESOURCE_URI) - .name("Test Resource") - .title("Test Resource") - .mimeType("text/plain") - .description("Test resource description") - .build(); - McpServerFeatures.AsyncResourceSpecification specification = new McpServerFeatures.AsyncResourceSpecification( - resource, (exchange, req) -> Mono.just(new ReadResourceResult(List.of()))); - - StepVerifier - .create(mcpAsyncServer.addResource(specification).then(mcpAsyncServer.listResources().collectList())) - .expectNextMatches(resources -> resources.size() == 1 && resources.get(0).uri().equals(TEST_RESOURCE_URI)) - .verifyComplete(); - - assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); - } - - @Test - void testRemoveResource() { - var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().resources(true, false).build()) - .build(); - - Resource resource = Resource.builder() - .uri(TEST_RESOURCE_URI) - .name("Test Resource") - .title("Test Resource") - .mimeType("text/plain") - .description("Test resource description") - .build(); - McpServerFeatures.AsyncResourceSpecification specification = new McpServerFeatures.AsyncResourceSpecification( - resource, (exchange, req) -> Mono.just(new ReadResourceResult(List.of()))); - - StepVerifier - .create(mcpAsyncServer.addResource(specification).then(mcpAsyncServer.removeResource(TEST_RESOURCE_URI))) - .verifyComplete(); - - assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); - } - - @Test - void testRemoveNonexistentResource() { - var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().resources(true, false).build()) - .build(); - - // Removing a non-existent resource should complete successfully (no error) - // as per the new implementation that just logs a warning - StepVerifier.create(mcpAsyncServer.removeResource("nonexistent://resource")).verifyComplete(); - - assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); - } - - // --------------------------------------- - // Resource Template Tests - // --------------------------------------- - - @Test - void testAddResourceTemplate() { - var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().resources(true, false).build()) - .build(); - - McpSchema.ResourceTemplate template = McpSchema.ResourceTemplate.builder() - .uriTemplate("test://template/{id}") - .name("test-template") - .description("Test resource template") - .mimeType("text/plain") - .build(); - - McpServerFeatures.AsyncResourceTemplateSpecification specification = new McpServerFeatures.AsyncResourceTemplateSpecification( - template, (exchange, req) -> Mono.just(new ReadResourceResult(List.of()))); - - StepVerifier.create(mcpAsyncServer.addResourceTemplate(specification)).verifyComplete(); - - assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); - } - - @Test - void testAddResourceTemplateWithoutCapability() { - // Create a server without resource capabilities - McpAsyncServer serverWithoutResources = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").build(); - - McpSchema.ResourceTemplate template = McpSchema.ResourceTemplate.builder() - .uriTemplate("test://template/{id}") - .name("test-template") - .description("Test resource template") - .mimeType("text/plain") - .build(); - - McpServerFeatures.AsyncResourceTemplateSpecification specification = new McpServerFeatures.AsyncResourceTemplateSpecification( - template, (exchange, req) -> Mono.just(new ReadResourceResult(List.of()))); - - StepVerifier.create(serverWithoutResources.addResourceTemplate(specification)).verifyErrorSatisfies(error -> { - assertThat(error).isInstanceOf(IllegalStateException.class) - .hasMessageContaining("Server must be configured with resource capabilities"); - }); - } - - @Test - void testRemoveResourceTemplate() { - McpSchema.ResourceTemplate template = McpSchema.ResourceTemplate.builder() - .uriTemplate("test://template/{id}") - .name("test-template") - .description("Test resource template") - .mimeType("text/plain") - .build(); - - McpServerFeatures.AsyncResourceTemplateSpecification specification = new McpServerFeatures.AsyncResourceTemplateSpecification( - template, (exchange, req) -> Mono.just(new ReadResourceResult(List.of()))); - - var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().resources(true, false).build()) - .resourceTemplates(specification) - .build(); - - StepVerifier.create(mcpAsyncServer.removeResourceTemplate("test://template/{id}")).verifyComplete(); - - assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); - } - - @Test - void testRemoveResourceTemplateWithoutCapability() { - // Create a server without resource capabilities - McpAsyncServer serverWithoutResources = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").build(); - - StepVerifier.create(serverWithoutResources.removeResourceTemplate("test://template/{id}")) - .verifyErrorSatisfies(error -> { - assertThat(error).isInstanceOf(IllegalStateException.class) - .hasMessageContaining("Server must be configured with resource capabilities"); - }); - } - - @Test - void testRemoveNonexistentResourceTemplate() { - var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().resources(true, false).build()) - .build(); - - StepVerifier.create(mcpAsyncServer.removeResourceTemplate("nonexistent://template/{id}")).verifyComplete(); - - assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); - } - - @Test - void testListResourceTemplates() { - McpSchema.ResourceTemplate template = McpSchema.ResourceTemplate.builder() - .uriTemplate("test://template/{id}") - .name("test-template") - .description("Test resource template") - .mimeType("text/plain") - .build(); - - McpServerFeatures.AsyncResourceTemplateSpecification specification = new McpServerFeatures.AsyncResourceTemplateSpecification( - template, (exchange, req) -> Mono.just(new ReadResourceResult(List.of()))); - - var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().resources(true, false).build()) - .resourceTemplates(specification) - .build(); - - // Note: Based on the current implementation, listResourceTemplates() returns - // Flux - // This appears to be a bug in the implementation that should return - // Flux - StepVerifier.create(mcpAsyncServer.listResourceTemplates().collectList()) - .expectNextMatches(resources -> resources.size() >= 0) // Just verify it - // doesn't error - .verifyComplete(); - - assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); - } - - // --------------------------------------- - // Prompts Tests - // --------------------------------------- - - @Test - void testNotifyPromptsListChanged() { - var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").build(); - - StepVerifier.create(mcpAsyncServer.notifyPromptsListChanged()).verifyComplete(); - - assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); - } - - @Test - void testAddPromptWithNullSpecification() { - var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().prompts(false).build()) - .build(); - - StepVerifier.create(mcpAsyncServer.addPrompt((McpServerFeatures.AsyncPromptSpecification) null)) - .verifyErrorSatisfies(error -> { - assertThat(error).isInstanceOf(IllegalArgumentException.class) - .hasMessage("Prompt specification must not be null"); - }); - } - - @Test - void testAddPromptWithoutCapability() { - // Create a server without prompt capabilities - McpAsyncServer serverWithoutPrompts = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").build(); - - Prompt prompt = new Prompt(TEST_PROMPT_NAME, "Test Prompt", "Test Prompt", List.of()); - McpServerFeatures.AsyncPromptSpecification specification = new McpServerFeatures.AsyncPromptSpecification( - prompt, (exchange, req) -> Mono.just(new GetPromptResult("Test prompt description", List - .of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content")))))); - - StepVerifier.create(serverWithoutPrompts.addPrompt(specification)).verifyErrorSatisfies(error -> { - assertThat(error).isInstanceOf(IllegalStateException.class) - .hasMessage("Server must be configured with prompt capabilities"); - }); - } - - @Test - void testRemovePromptWithoutCapability() { - // Create a server without prompt capabilities - McpAsyncServer serverWithoutPrompts = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").build(); - - StepVerifier.create(serverWithoutPrompts.removePrompt(TEST_PROMPT_NAME)).verifyErrorSatisfies(error -> { - assertThat(error).isInstanceOf(IllegalStateException.class) - .hasMessage("Server must be configured with prompt capabilities"); - }); - } - - @Test - void testRemovePrompt() { - String TEST_PROMPT_NAME_TO_REMOVE = "TEST_PROMPT_NAME678"; - - Prompt prompt = new Prompt(TEST_PROMPT_NAME_TO_REMOVE, "Test Prompt", "Test Prompt", List.of()); - McpServerFeatures.AsyncPromptSpecification specification = new McpServerFeatures.AsyncPromptSpecification( - prompt, (exchange, req) -> Mono.just(new GetPromptResult("Test prompt description", List - .of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content")))))); - - var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().prompts(true).build()) - .prompts(specification) - .build(); - - StepVerifier.create(mcpAsyncServer.removePrompt(TEST_PROMPT_NAME_TO_REMOVE)).verifyComplete(); - - assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); - } - - @Test - void testRemoveNonexistentPrompt() { - var mcpAsyncServer2 = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().prompts(true).build()) - .build(); - - StepVerifier.create(mcpAsyncServer2.removePrompt("nonexistent-prompt")).verifyComplete(); - - assertThatCode(() -> mcpAsyncServer2.closeGracefully().block(Duration.ofSeconds(10))) - .doesNotThrowAnyException(); - } - - // --------------------------------------- - // Roots Tests - // --------------------------------------- - - @Test - void testRootsChangeHandlers() { - // Test with single consumer - var rootsReceived = new McpSchema.Root[1]; - var consumerCalled = new boolean[1]; - - var singleConsumerServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0") - .rootsChangeHandlers(List.of((exchange, roots) -> Mono.fromRunnable(() -> { - consumerCalled[0] = true; - if (!roots.isEmpty()) { - rootsReceived[0] = roots.get(0); - } - }))) - .build(); - - assertThat(singleConsumerServer).isNotNull(); - assertThatCode(() -> singleConsumerServer.closeGracefully().block(Duration.ofSeconds(10))) - .doesNotThrowAnyException(); - onClose(); - - // Test with multiple consumers - var consumer1Called = new boolean[1]; - var consumer2Called = new boolean[1]; - var rootsContent = new List[1]; - - var multipleConsumersServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0") - .rootsChangeHandlers(List.of((exchange, roots) -> Mono.fromRunnable(() -> { - consumer1Called[0] = true; - rootsContent[0] = roots; - }), (exchange, roots) -> Mono.fromRunnable(() -> consumer2Called[0] = true))) - .build(); - - assertThat(multipleConsumersServer).isNotNull(); - assertThatCode(() -> multipleConsumersServer.closeGracefully().block(Duration.ofSeconds(10))) - .doesNotThrowAnyException(); - onClose(); - - // Test error handling - var errorHandlingServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0") - .rootsChangeHandlers(List.of((exchange, roots) -> { - throw new RuntimeException("Test error"); - })) - .build(); - - assertThat(errorHandlingServer).isNotNull(); - assertThatCode(() -> errorHandlingServer.closeGracefully().block(Duration.ofSeconds(10))) - .doesNotThrowAnyException(); - onClose(); - - // Test without consumers - var noConsumersServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").build(); - - assertThat(noConsumersServer).isNotNull(); - assertThatCode(() -> noConsumersServer.closeGracefully().block(Duration.ofSeconds(10))) - .doesNotThrowAnyException(); - } - -} diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/server/AbstractMcpClientServerIntegrationTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/server/AbstractMcpClientServerIntegrationTests.java deleted file mode 100644 index 1f5387f..0000000 --- a/mcp-core/src/test/java/io/modelcontextprotocol/server/AbstractMcpClientServerIntegrationTests.java +++ /dev/null @@ -1,1756 +0,0 @@ -/* - * Copyright 2024 - 2024 the original author or authors. - */ - -package io.modelcontextprotocol.server; - -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.time.Duration; -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; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.BiFunction; -import java.util.function.Function; -import java.util.stream.Collectors; - -import io.modelcontextprotocol.client.McpClient; -import io.modelcontextprotocol.common.McpTransportContext; -import io.modelcontextprotocol.spec.McpError; -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.CallToolResult; -import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities; -import io.modelcontextprotocol.spec.McpSchema.CompleteRequest; -import io.modelcontextprotocol.spec.McpSchema.CompleteResult; -import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest; -import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult; -import io.modelcontextprotocol.spec.McpSchema.ElicitRequest; -import io.modelcontextprotocol.spec.McpSchema.ElicitResult; -import io.modelcontextprotocol.spec.McpSchema.InitializeResult; -import io.modelcontextprotocol.spec.McpSchema.ModelPreferences; -import io.modelcontextprotocol.spec.McpSchema.Prompt; -import io.modelcontextprotocol.spec.McpSchema.PromptArgument; -import io.modelcontextprotocol.spec.McpSchema.PromptReference; -import io.modelcontextprotocol.spec.McpSchema.Role; -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.util.Utils; -import net.javacrumbs.jsonunit.core.Option; -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 reactor.test.StepVerifier; - -import static io.modelcontextprotocol.util.ToolsUtils.EMPTY_JSON_SCHEMA; -import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; -import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertWith; -import static org.awaitility.Awaitility.await; -import static org.mockito.Mockito.mock; - -public abstract class AbstractMcpClientServerIntegrationTests { - - protected ConcurrentHashMap clientBuilders = new ConcurrentHashMap<>(); - - abstract protected void prepareClients(int port, String mcpEndpoint); - - abstract protected McpServer.AsyncSpecification prepareAsyncServerBuilder(); - - abstract protected McpServer.SyncSpecification prepareSyncServerBuilder(); - - @ParameterizedTest(name = "{0} : {displayName} ") - @MethodSource("clientsForTesting") - void simple(String clientType) { - - var clientBuilder = clientBuilders.get(clientType); - - var server = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0") - .requestTimeout(Duration.ofSeconds(1000)) - .build(); - try ( - // Create client without sampling capabilities - var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample " + "client", "0.0.0")) - .requestTimeout(Duration.ofSeconds(1000)) - .build()) { - - assertThat(client.initialize()).isNotNull(); - - } - finally { - server.closeGracefully().block(); - } - } - - // --------------------------------------- - // Sampling Tests - // --------------------------------------- - @ParameterizedTest(name = "{0} : {displayName} ") - @MethodSource("clientsForTesting") - void testCreateMessageWithoutSamplingCapabilities(String clientType) { - - var clientBuilder = clientBuilders.get(clientType); - - McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() - .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) - .callHandler((exchange, request) -> { - return exchange.createMessage(mock(McpSchema.CreateMessageRequest.class)) - .then(Mono.just(mock(CallToolResult.class))); - }) - .build(); - - var server = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").tools(tool).build(); - - try ( - // Create client without sampling capabilities - var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample " + "client", "0.0.0")) - .build()) { - - assertThat(client.initialize()).isNotNull(); - - try { - client.callTool(new McpSchema.CallToolRequest("tool1", Map.of())); - } - catch (McpError e) { - assertThat(e).isInstanceOf(McpError.class) - .hasMessage("Client must be configured with sampling capabilities"); - } - } - finally { - server.closeGracefully().block(); - } - } - - @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); - - return new CreateMessageResult(Role.USER, new McpSchema.TextContent("Test message"), "MockModelName", - CreateMessageResult.StopReason.STOP_SEQUENCE); - }; - - CallToolResult callResponse = McpSchema.CallToolResult.builder() - .addContent(new McpSchema.TextContent("CALL RESPONSE")) - .build(); - - AtomicReference samplingResult = new AtomicReference<>(); - - McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() - .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) - .callHandler((exchange, request) -> { - - var createMessageRequest = McpSchema.CreateMessageRequest.builder() - .messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, - new McpSchema.TextContent("Test message")))) - .modelPreferences(ModelPreferences.builder() - .hints(List.of()) - .costPriority(1.0) - .speedPriority(1.0) - .intelligencePriority(1.0) - .build()) - .build(); - - return exchange.createMessage(createMessageRequest) - .doOnNext(samplingResult::set) - .thenReturn(callResponse); - }) - .build(); - - var mcpServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").tools(tool).build(); - - try (var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0")) - .capabilities(ClientCapabilities.builder().sampling().build()) - .sampling(samplingHandler) - .build()) { - - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); - - CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of())); - - assertThat(response).isNotNull(); - assertThat(response).isEqualTo(callResponse); - - assertWith(samplingResult.get(), result -> { - assertThat(result).isNotNull(); - assertThat(result.role()).isEqualTo(Role.USER); - assertThat(result.content()).isInstanceOf(McpSchema.TextContent.class); - assertThat(((McpSchema.TextContent) result.content()).text()).isEqualTo("Test message"); - assertThat(result.model()).isEqualTo("MockModelName"); - assertThat(result.stopReason()).isEqualTo(CreateMessageResult.StopReason.STOP_SEQUENCE); - }); - } - finally { - mcpServer.closeGracefully().block(); - } - } - - @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); - try { - TimeUnit.SECONDS.sleep(2); - } - catch (InterruptedException e) { - throw new RuntimeException(e); - } - return new CreateMessageResult(Role.USER, new McpSchema.TextContent("Test message"), "MockModelName", - CreateMessageResult.StopReason.STOP_SEQUENCE); - }; - - // Server - - CallToolResult callResponse = McpSchema.CallToolResult.builder() - .addContent(new McpSchema.TextContent("CALL RESPONSE")) - .build(); - - AtomicReference samplingResult = new AtomicReference<>(); - - McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() - .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) - .callHandler((exchange, request) -> { - - var createMessageRequest = McpSchema.CreateMessageRequest.builder() - .messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, - new McpSchema.TextContent("Test message")))) - .modelPreferences(ModelPreferences.builder() - .hints(List.of()) - .costPriority(1.0) - .speedPriority(1.0) - .intelligencePriority(1.0) - .build()) - .build(); - - return exchange.createMessage(createMessageRequest) - .doOnNext(samplingResult::set) - .thenReturn(callResponse); - }) - .build(); - - var mcpServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0") - .requestTimeout(Duration.ofSeconds(4)) - .tools(tool) - .build(); - try (var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0")) - .capabilities(ClientCapabilities.builder().sampling().build()) - .sampling(samplingHandler) - .build()) { - - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); - - CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of())); - - assertThat(response).isNotNull(); - assertThat(response).isEqualTo(callResponse); - - assertWith(samplingResult.get(), result -> { - assertThat(result).isNotNull(); - assertThat(result.role()).isEqualTo(Role.USER); - assertThat(result.content()).isInstanceOf(McpSchema.TextContent.class); - assertThat(((McpSchema.TextContent) result.content()).text()).isEqualTo("Test message"); - assertThat(result.model()).isEqualTo("MockModelName"); - assertThat(result.stopReason()).isEqualTo(CreateMessageResult.StopReason.STOP_SEQUENCE); - }); - } - finally { - mcpServer.closeGracefully().block(); - } - } - - @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); - try { - TimeUnit.SECONDS.sleep(2); - } - catch (InterruptedException e) { - throw new RuntimeException(e); - } - return new CreateMessageResult(Role.USER, new McpSchema.TextContent("Test message"), "MockModelName", - CreateMessageResult.StopReason.STOP_SEQUENCE); - }; - - CallToolResult callResponse = McpSchema.CallToolResult.builder() - .addContent(new McpSchema.TextContent("CALL RESPONSE")) - .build(); - - McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() - .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) - .callHandler((exchange, request) -> { - - var createMessageRequest = McpSchema.CreateMessageRequest.builder() - .messages(List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, - new McpSchema.TextContent("Test message")))) - .modelPreferences(ModelPreferences.builder() - .hints(List.of()) - .costPriority(1.0) - .speedPriority(1.0) - .intelligencePriority(1.0) - .build()) - .build(); - - return exchange.createMessage(createMessageRequest).thenReturn(callResponse); - }) - .build(); - - var mcpServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0") - .requestTimeout(Duration.ofSeconds(1)) - .tools(tool) - .build(); - - try (var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0")) - .capabilities(ClientCapabilities.builder().sampling().build()) - .sampling(samplingHandler) - .build()) { - - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); - - assertThatExceptionOfType(McpError.class).isThrownBy(() -> { - mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of())); - }).withMessageContaining("1000ms"); - } - finally { - mcpServer.closeGracefully().block(); - } - } - - // --------------------------------------- - // Elicitation Tests - // --------------------------------------- - @ParameterizedTest(name = "{0} : {displayName} ") - @MethodSource("clientsForTesting") - void testCreateElicitationWithoutElicitationCapabilities(String clientType) { - - var clientBuilder = clientBuilders.get(clientType); - - McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() - .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) - .callHandler((exchange, request) -> exchange.createElicitation(mock(ElicitRequest.class)) - .then(Mono.just(mock(CallToolResult.class)))) - .build(); - - var server = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").tools(tool).build(); - - // Create client without elicitation capabilities - try (var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0")).build()) { - - assertThat(client.initialize()).isNotNull(); - - try { - client.callTool(new McpSchema.CallToolRequest("tool1", Map.of())); - } - catch (McpError e) { - assertThat(e).isInstanceOf(McpError.class) - .hasMessage("Client must be configured with elicitation capabilities"); - } - } - finally { - server.closeGracefully().block(); - } - } - - @ParameterizedTest(name = "{0} : {displayName} ") - @MethodSource("clientsForTesting") - void testCreateElicitationSuccess(String clientType) { - - var clientBuilder = clientBuilders.get(clientType); - - Function elicitationHandler = request -> { - assertThat(request.message()).isNotEmpty(); - assertThat(request.requestedSchema()).isNotNull(); - - return new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT, - Map.of("message", request.message())); - }; - - CallToolResult callResponse = McpSchema.CallToolResult.builder() - .addContent(new McpSchema.TextContent("CALL RESPONSE")) - .build(); - - McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() - .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) - .callHandler((exchange, request) -> { - - var elicitationRequest = McpSchema.ElicitRequest.builder() - .message("Test message") - .requestedSchema( - Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string")))) - .build(); - - StepVerifier.create(exchange.createElicitation(elicitationRequest)).consumeNextWith(result -> { - assertThat(result).isNotNull(); - assertThat(result.action()).isEqualTo(McpSchema.ElicitResult.Action.ACCEPT); - assertThat(result.content().get("message")).isEqualTo("Test message"); - }).verifyComplete(); - - return Mono.just(callResponse); - }) - .build(); - - var mcpServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").tools(tool).build(); - - try (var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0")) - .capabilities(ClientCapabilities.builder().elicitation().build()) - .elicitation(elicitationHandler) - .build()) { - - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); - - CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of())); - - assertThat(response).isNotNull(); - assertThat(response).isEqualTo(callResponse); - } - finally { - mcpServer.closeGracefully().block(); - } - } - - @ParameterizedTest(name = "{0} : {displayName} ") - @MethodSource("clientsForTesting") - void testCreateElicitationWithRequestTimeoutSuccess(String clientType) { - - var clientBuilder = clientBuilders.get(clientType); - - Function elicitationHandler = request -> { - assertThat(request.message()).isNotEmpty(); - assertThat(request.requestedSchema()).isNotNull(); - return new ElicitResult(ElicitResult.Action.ACCEPT, Map.of("message", request.message())); - }; - - CallToolResult callResponse = McpSchema.CallToolResult.builder() - .addContent(new McpSchema.TextContent("CALL RESPONSE")) - .build(); - - AtomicReference resultRef = new AtomicReference<>(); - - McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() - .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) - .callHandler((exchange, request) -> { - - var elicitationRequest = McpSchema.ElicitRequest.builder() - .message("Test message") - .requestedSchema( - Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string")))) - .build(); - - return exchange.createElicitation(elicitationRequest) - .doOnNext(resultRef::set) - .then(Mono.just(callResponse)); - }) - .build(); - - var mcpServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0") - .requestTimeout(Duration.ofSeconds(3)) - .tools(tool) - .build(); - - try (var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0")) - .capabilities(ClientCapabilities.builder().elicitation().build()) - .elicitation(elicitationHandler) - .build()) { - - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); - - CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of())); - - assertThat(response).isNotNull(); - assertThat(response).isEqualTo(callResponse); - assertWith(resultRef.get(), result -> { - assertThat(result).isNotNull(); - assertThat(result.action()).isEqualTo(McpSchema.ElicitResult.Action.ACCEPT); - assertThat(result.content().get("message")).isEqualTo("Test message"); - }); - } - finally { - mcpServer.closeGracefully().block(); - } - } - - @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(); - - try { - if (!latch.await(2, TimeUnit.SECONDS)) { - throw new RuntimeException("Timeout waiting for elicitation processing"); - } - } - catch (InterruptedException e) { - throw new RuntimeException(e); - } - return new ElicitResult(ElicitResult.Action.ACCEPT, Map.of("message", request.message())); - }; - - CallToolResult callResponse = CallToolResult.builder().addContent(new TextContent("CALL RESPONSE")).build(); - - AtomicReference resultRef = new AtomicReference<>(); - - McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() - .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) - .callHandler((exchange, request) -> { - - var elicitationRequest = ElicitRequest.builder() - .message("Test message") - .requestedSchema( - Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string")))) - .build(); - - return exchange.createElicitation(elicitationRequest) - .doOnNext(resultRef::set) - .then(Mono.just(callResponse)); - }) - .build(); - - var mcpServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0") - .requestTimeout(Duration.ofSeconds(1)) // 1 second. - .tools(tool) - .build(); - - try (var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0")) - .capabilities(ClientCapabilities.builder().elicitation().build()) - .elicitation(elicitationHandler) - .build()) { - - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); - - assertThatExceptionOfType(McpError.class).isThrownBy(() -> { - mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of())); - }).withMessageContaining("within 1000ms"); - - ElicitResult elicitResult = resultRef.get(); - assertThat(elicitResult).isNull(); - } - finally { - mcpServer.closeGracefully().block(); - } - } - - // --------------------------------------- - // Roots Tests - // --------------------------------------- - @ParameterizedTest(name = "{0} : {displayName} ") - @MethodSource("clientsForTesting") - void testRootsSuccess(String clientType) { - var clientBuilder = clientBuilders.get(clientType); - - List roots = List.of(new Root("uri1://", "root1"), new Root("uri2://", "root2")); - - AtomicReference> rootsRef = new AtomicReference<>(); - - var mcpServer = prepareSyncServerBuilder() - .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) - .build(); - - try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) - .roots(roots) - .build()) { - - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); - - assertThat(rootsRef.get()).isNull(); - - mcpClient.rootsListChangedNotification(); - - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).containsAll(roots); - }); - - // Remove a root - mcpClient.removeRoot(roots.get(0).uri()); - - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).containsAll(List.of(roots.get(1))); - }); - - // Add a new root - var root3 = new Root("uri3://", "root3"); - mcpClient.addRoot(root3); - - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).containsAll(List.of(roots.get(1), root3)); - }); - } - finally { - mcpServer.closeGracefully(); - } - } - - @ParameterizedTest(name = "{0} : {displayName} ") - @MethodSource("clientsForTesting") - void testRootsWithoutCapability(String clientType) { - - var clientBuilder = clientBuilders.get(clientType); - - McpServerFeatures.SyncToolSpecification tool = McpServerFeatures.SyncToolSpecification.builder() - .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) - .callHandler((exchange, request) -> { - - exchange.listRoots(); // try to list roots - - return mock(CallToolResult.class); - }) - .build(); - - var mcpServer = prepareSyncServerBuilder().rootsChangeHandler((exchange, rootsUpdate) -> { - }).tools(tool).build(); - - try ( - // Create client without roots capability - // No roots capability - var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().build()).build()) { - - assertThat(mcpClient.initialize()).isNotNull(); - - // Attempt to list roots should fail - try { - mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of())); - } - catch (McpError e) { - assertThat(e).isInstanceOf(McpError.class).hasMessage("Roots not supported"); - } - } - finally { - mcpServer.closeGracefully(); - } - } - - @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 = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) - .roots(List.of()) // Empty roots list - .build()) { - - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); - - mcpClient.rootsListChangedNotification(); - - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).isEmpty(); - }); - } - finally { - mcpServer.closeGracefully(); - } - } - - @ParameterizedTest(name = "{0} : {displayName} ") - @MethodSource("clientsForTesting") - void testRootsWithMultipleHandlers(String clientType) { - - var clientBuilder = clientBuilders.get(clientType); - - List roots = List.of(new Root("uri1://", "root1")); - - AtomicReference> rootsRef1 = new AtomicReference<>(); - AtomicReference> rootsRef2 = new AtomicReference<>(); - - var mcpServer = prepareSyncServerBuilder() - .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef1.set(rootsUpdate)) - .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef2.set(rootsUpdate)) - .build(); - - try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) - .roots(roots) - .build()) { - - assertThat(mcpClient.initialize()).isNotNull(); - - mcpClient.rootsListChangedNotification(); - - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef1.get()).containsAll(roots); - assertThat(rootsRef2.get()).containsAll(roots); - }); - } - finally { - mcpServer.closeGracefully(); - } - } - - @ParameterizedTest(name = "{0} : {displayName} ") - @MethodSource("clientsForTesting") - void testRootsServerCloseWithActiveSubscription(String clientType) { - - var clientBuilder = clientBuilders.get(clientType); - - List roots = List.of(new Root("uri1://", "root1")); - - AtomicReference> rootsRef = new AtomicReference<>(); - - var mcpServer = prepareSyncServerBuilder() - .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) - .build(); - - try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) - .roots(roots) - .build()) { - - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); - - mcpClient.rootsListChangedNotification(); - - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).containsAll(roots); - }); - } - finally { - mcpServer.closeGracefully(); - } - } - - // --------------------------------------- - // Tools Tests - // --------------------------------------- - @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(new McpSchema.TextContent("CALL RESPONSE; ctx=importantValue")) - .build(); - McpServerFeatures.SyncToolSpecification tool1 = McpServerFeatures.SyncToolSpecification.builder() - .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) - .callHandler((exchange, request) -> { - - try { - HttpResponse response = HttpClient.newHttpClient() - .send(HttpRequest.newBuilder() - .uri(URI.create( - "https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md")) - .GET() - .build(), HttpResponse.BodyHandlers.ofString()); - String responseBody = response.body(); - responseBodyIsNullOrBlank.set(!Utils.hasText(responseBody)); - } - catch (Exception e) { - e.printStackTrace(); - } - - return callResponse; - }) - .build(); - - var mcpServer = prepareSyncServerBuilder().capabilities(ServerCapabilities.builder().tools(true).build()) - .tools(tool1) - .build(); - - try (var mcpClient = clientBuilder.build()) { - - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); - - assertThat(mcpClient.listTools().tools()).contains(tool1.tool()); - - CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of())); - - assertThat(responseBodyIsNullOrBlank.get()).isFalse(); - assertThat(response).isNotNull().isEqualTo(callResponse); - } - finally { - mcpServer.closeGracefully(); - } - } - - @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() - .tool(Tool.builder() - .name("tool1") - .description("tool1 description") - .inputSchema(EMPTY_JSON_SCHEMA) - .build()) - .callHandler((exchange, request) -> { - // We trigger a timeout on blocking read, raising an exception - Mono.never().block(Duration.ofSeconds(1)); - return null; - }) - .build()) - .build(); - - try (var mcpClient = clientBuilder.requestTimeout(Duration.ofMillis(6666)).build()) { - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); - - // We expect the tool call to fail immediately with the exception raised by - // the offending tool instead of getting back a timeout. - assertThatExceptionOfType(McpError.class) - .isThrownBy(() -> mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()))) - .withMessageContaining("Timeout on blocking read"); - } - finally { - mcpServer.closeGracefully(); - } - } - - @ParameterizedTest(name = "{0} : {displayName} ") - @MethodSource("clientsForTesting") - void testToolCallSuccessWithTransportContextExtraction(String clientType) { - - var clientBuilder = clientBuilders.get(clientType); - - var transportContextIsNull = new AtomicBoolean(false); - var transportContextIsEmpty = new AtomicBoolean(false); - var responseBodyIsNullOrBlank = new AtomicBoolean(false); - - var expectedCallResponse = McpSchema.CallToolResult.builder() - .addContent(new McpSchema.TextContent("CALL RESPONSE; ctx=value")) - .build(); - McpServerFeatures.SyncToolSpecification tool1 = McpServerFeatures.SyncToolSpecification.builder() - .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) - .callHandler((exchange, request) -> { - - McpTransportContext transportContext = exchange.transportContext(); - transportContextIsNull.set(transportContext == null); - transportContextIsEmpty.set(transportContext.equals(McpTransportContext.EMPTY)); - String ctxValue = (String) transportContext.get("important"); - - try { - String responseBody = "TOOL RESPONSE"; - responseBodyIsNullOrBlank.set(!Utils.hasText(responseBody)); - } - catch (Exception e) { - e.printStackTrace(); - } - - return McpSchema.CallToolResult.builder() - .addContent(new McpSchema.TextContent("CALL RESPONSE; ctx=" + ctxValue)) - .build(); - }) - .build(); - - var mcpServer = prepareSyncServerBuilder().capabilities(ServerCapabilities.builder().tools(true).build()) - .tools(tool1) - .build(); - - try (var mcpClient = clientBuilder.build()) { - - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); - - assertThat(mcpClient.listTools().tools()).contains(tool1.tool()); - - CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of())); - - assertThat(transportContextIsNull.get()).isFalse(); - assertThat(transportContextIsEmpty.get()).isFalse(); - assertThat(responseBodyIsNullOrBlank.get()).isFalse(); - assertThat(response).isNotNull().isEqualTo(expectedCallResponse); - } - finally { - mcpServer.closeGracefully(); - } - } - - @ParameterizedTest(name = "{0} : {displayName} ") - @MethodSource("clientsForTesting") - void testToolListChangeHandlingSuccess(String clientType) { - - var clientBuilder = clientBuilders.get(clientType); - - var callResponse = McpSchema.CallToolResult.builder() - .addContent(new McpSchema.TextContent("CALL RESPONSE")) - .build(); - - McpServerFeatures.SyncToolSpecification tool1 = McpServerFeatures.SyncToolSpecification.builder() - .tool(Tool.builder().name("tool1").description("tool1 description").inputSchema(EMPTY_JSON_SCHEMA).build()) - .callHandler((exchange, request) -> { - // perform a blocking call to a remote service - try { - HttpResponse response = HttpClient.newHttpClient() - .send(HttpRequest.newBuilder() - .uri(URI.create( - "https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md")) - .GET() - .build(), HttpResponse.BodyHandlers.ofString()); - String responseBody = response.body(); - assertThat(responseBody).isNotBlank(); - } - catch (Exception e) { - e.printStackTrace(); - } - return callResponse; - }) - .build(); - - AtomicReference> toolsRef = new AtomicReference<>(); - - var mcpServer = prepareSyncServerBuilder().capabilities(ServerCapabilities.builder().tools(true).build()) - .tools(tool1) - .build(); - - try (var mcpClient = clientBuilder.toolsChangeConsumer(toolsUpdate -> { - // perform a blocking call to a remote service - try { - HttpResponse response = HttpClient.newHttpClient() - .send(HttpRequest.newBuilder() - .uri(URI.create( - "https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md")) - .GET() - .build(), HttpResponse.BodyHandlers.ofString()); - String responseBody = response.body(); - assertThat(responseBody).isNotBlank(); - toolsRef.set(toolsUpdate); - } - catch (Exception e) { - e.printStackTrace(); - } - }).build()) { - - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); - - assertThat(toolsRef.get()).isNull(); - - assertThat(mcpClient.listTools().tools()).contains(tool1.tool()); - - mcpServer.notifyToolsListChanged(); - - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(toolsRef.get()).containsAll(List.of(tool1.tool())); - }); - - // Remove a tool - mcpServer.removeTool("tool1"); - - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(toolsRef.get()).isEmpty(); - }); - - // Add a new tool - McpServerFeatures.SyncToolSpecification tool2 = McpServerFeatures.SyncToolSpecification.builder() - .tool(Tool.builder() - .name("tool2") - .description("tool2 description") - .inputSchema(EMPTY_JSON_SCHEMA) - .build()) - .callHandler((exchange, request) -> callResponse) - .build(); - - mcpServer.addTool(tool2); - - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(toolsRef.get()).containsAll(List.of(tool2.tool())); - }); - } - finally { - mcpServer.closeGracefully(); - } - } - - @ParameterizedTest(name = "{0} : {displayName} ") - @MethodSource("clientsForTesting") - void testInitialize(String clientType) { - - var clientBuilder = clientBuilders.get(clientType); - - var mcpServer = prepareSyncServerBuilder().build(); - - try (var mcpClient = clientBuilder.build()) { - - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); - } - finally { - mcpServer.closeGracefully(); - } - } - - // --------------------------------------- - // Logging Tests - // --------------------------------------- - @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() - .name("logging-test") - .description("Test logging notifications") - .inputSchema(EMPTY_JSON_SCHEMA) - .build()) - .callHandler((exchange, request) -> { - - // Create and send notifications with different levels - - //@formatter:off - return exchange // This should be filtered out (DEBUG < NOTICE) - .loggingNotification(McpSchema.LoggingMessageNotification.builder() - .level(McpSchema.LoggingLevel.DEBUG) - .logger("test-logger") - .data("Debug message") - .build()) - .then(exchange // This should be sent (NOTICE >= NOTICE) - .loggingNotification(McpSchema.LoggingMessageNotification.builder() - .level(McpSchema.LoggingLevel.NOTICE) - .logger("test-logger") - .data("Notice message") - .build())) - .then(exchange // This should be sent (ERROR > NOTICE) - .loggingNotification(McpSchema.LoggingMessageNotification.builder() - .level(McpSchema.LoggingLevel.ERROR) - .logger("test-logger") - .data("Error message") - .build())) - .then(exchange // This should be filtered out (INFO < NOTICE) - .loggingNotification(McpSchema.LoggingMessageNotification.builder() - .level(McpSchema.LoggingLevel.INFO) - .logger("test-logger") - .data("Another info message") - .build())) - .then(exchange // This should be sent (ERROR >= NOTICE) - .loggingNotification(McpSchema.LoggingMessageNotification.builder() - .level(McpSchema.LoggingLevel.ERROR) - .logger("test-logger") - .data("Another error message") - .build())) - .thenReturn(CallToolResult.builder() - .content(List.of(new McpSchema.TextContent("Logging test completed"))) - .isError(false) - .build()); - //@formatter:on - }) - .build(); - - var mcpServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .tools(tool) - .build(); - - try ( - // Create client with logging notification handler - var mcpClient = clientBuilder.loggingConsumer(notification -> { - receivedNotifications.add(notification); - latch.countDown(); - }).build()) { - - // Initialize client - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); - - // Set minimum logging level to NOTICE - mcpClient.setLoggingLevel(McpSchema.LoggingLevel.NOTICE); - - // Call the tool that sends logging notifications - CallToolResult result = mcpClient.callTool(new McpSchema.CallToolRequest("logging-test", Map.of())); - assertThat(result).isNotNull(); - assertThat(result.content().get(0)).isInstanceOf(McpSchema.TextContent.class); - assertThat(((McpSchema.TextContent) result.content().get(0)).text()).isEqualTo("Logging test completed"); - - assertThat(latch.await(5, TimeUnit.SECONDS)).as("Should receive notifications in reasonable time").isTrue(); - - // Should have received 3 notifications (1 NOTICE and 2 ERROR) - assertThat(receivedNotifications).hasSize(expectedNotificationsCount); - - Map notificationMap = receivedNotifications.stream() - .collect(Collectors.toMap(n -> n.data(), n -> n)); - - // First notification should be NOTICE level - assertThat(notificationMap.get("Notice message").level()).isEqualTo(McpSchema.LoggingLevel.NOTICE); - assertThat(notificationMap.get("Notice message").logger()).isEqualTo("test-logger"); - assertThat(notificationMap.get("Notice message").data()).isEqualTo("Notice message"); - - // Second notification should be ERROR level - assertThat(notificationMap.get("Error message").level()).isEqualTo(McpSchema.LoggingLevel.ERROR); - assertThat(notificationMap.get("Error message").logger()).isEqualTo("test-logger"); - assertThat(notificationMap.get("Error message").data()).isEqualTo("Error message"); - - // Third notification should be ERROR level - assertThat(notificationMap.get("Another error message").level()).isEqualTo(McpSchema.LoggingLevel.ERROR); - assertThat(notificationMap.get("Another error message").logger()).isEqualTo("test-logger"); - assertThat(notificationMap.get("Another error message").data()).isEqualTo("Another error message"); - } - finally { - mcpServer.closeGracefully().block(); - } - } - - // --------------------------------------- - // Progress Tests - // --------------------------------------- - @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() - .name("progress-test") - .description("Test progress notifications") - .inputSchema(EMPTY_JSON_SCHEMA) - .build()) - .callHandler((exchange, request) -> { - - // Create and send notifications - var progressToken = (String) request.meta().get("progressToken"); - - return exchange - .progressNotification( - new McpSchema.ProgressNotification(progressToken, 0.0, 1.0, "Processing started")) - .then(exchange.progressNotification( - new McpSchema.ProgressNotification(progressToken, 0.5, 1.0, "Processing data"))) - .then(// Send a progress notification with another progress value - // should - exchange.progressNotification(new McpSchema.ProgressNotification("another-progress-token", - 0.0, 1.0, "Another processing started"))) - .then(exchange.progressNotification( - new McpSchema.ProgressNotification(progressToken, 1.0, 1.0, "Processing completed"))) - .thenReturn(CallToolResult.builder() - .content(List.of(new McpSchema.TextContent("Progress test completed"))) - .isError(false) - .build()); - }) - .build(); - - var mcpServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .tools(tool) - .build(); - - try ( - // Create client with progress notification handler - var mcpClient = clientBuilder.progressConsumer(notification -> { - receivedNotifications.add(notification); - latch.countDown(); - }).build()) { - - // Initialize client - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); - - // Call the tool that sends progress notifications - McpSchema.CallToolRequest callToolRequest = McpSchema.CallToolRequest.builder() - .name("progress-test") - .meta(Map.of("progressToken", "test-progress-token")) - .build(); - CallToolResult result = mcpClient.callTool(callToolRequest); - assertThat(result).isNotNull(); - assertThat(result.content().get(0)).isInstanceOf(McpSchema.TextContent.class); - assertThat(((McpSchema.TextContent) result.content().get(0)).text()).isEqualTo("Progress test completed"); - - assertThat(latch.await(5, TimeUnit.SECONDS)).as("Should receive notifications in reasonable time").isTrue(); - - // Should have received 3 notifications - assertThat(receivedNotifications).hasSize(expectedNotificationsCount); - - Map notificationMap = receivedNotifications.stream() - .collect(Collectors.toMap(n -> n.message(), n -> n)); - - // First notification should be 0.0/1.0 progress - assertThat(notificationMap.get("Processing started").progressToken()).isEqualTo("test-progress-token"); - assertThat(notificationMap.get("Processing started").progress()).isEqualTo(0.0); - assertThat(notificationMap.get("Processing started").total()).isEqualTo(1.0); - assertThat(notificationMap.get("Processing started").message()).isEqualTo("Processing started"); - - // Second notification should be 0.5/1.0 progress - assertThat(notificationMap.get("Processing data").progressToken()).isEqualTo("test-progress-token"); - assertThat(notificationMap.get("Processing data").progress()).isEqualTo(0.5); - assertThat(notificationMap.get("Processing data").total()).isEqualTo(1.0); - assertThat(notificationMap.get("Processing data").message()).isEqualTo("Processing data"); - - // Third notification should be another progress token with 0.0/1.0 progress - assertThat(notificationMap.get("Another processing started").progressToken()) - .isEqualTo("another-progress-token"); - assertThat(notificationMap.get("Another processing started").progress()).isEqualTo(0.0); - assertThat(notificationMap.get("Another processing started").total()).isEqualTo(1.0); - assertThat(notificationMap.get("Another processing started").message()) - .isEqualTo("Another processing started"); - - // Fourth notification should be 1.0/1.0 progress - assertThat(notificationMap.get("Processing completed").progressToken()).isEqualTo("test-progress-token"); - assertThat(notificationMap.get("Processing completed").progress()).isEqualTo(1.0); - assertThat(notificationMap.get("Processing completed").total()).isEqualTo(1.0); - assertThat(notificationMap.get("Processing completed").message()).isEqualTo("Processing completed"); - } - finally { - mcpServer.closeGracefully().block(); - } - } - - // --------------------------------------- - // Completion Tests - // --------------------------------------- - @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, // total - true // hasMore - )); - - AtomicReference samplingRequest = new AtomicReference<>(); - BiFunction completionHandler = (mcpSyncServerExchange, - request) -> { - samplingRequest.set(request); - return completionResponse; - }; - - var mcpServer = prepareSyncServerBuilder().capabilities(ServerCapabilities.builder().completions().build()) - .prompts(new McpServerFeatures.SyncPromptSpecification( - new Prompt("code_review", "Code review", "this is code review prompt", - List.of(new PromptArgument("language", "Language", "string", false))), - (mcpSyncServerExchange, getPromptRequest) -> null)) - .completions(new McpServerFeatures.SyncCompletionSpecification( - new McpSchema.PromptReference(PromptReference.TYPE, "code_review", "Code review"), - completionHandler)) - .build(); - - try (var mcpClient = clientBuilder.build()) { - - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); - - CompleteRequest request = new CompleteRequest( - new PromptReference(PromptReference.TYPE, "code_review", "Code review"), - new CompleteRequest.CompleteArgument("language", "py")); - - CompleteResult result = mcpClient.completeCompletion(request); - - assertThat(result).isNotNull(); - - assertThat(samplingRequest.get().argument().name()).isEqualTo("language"); - assertThat(samplingRequest.get().argument().value()).isEqualTo("py"); - assertThat(samplingRequest.get().ref().type()).isEqualTo(PromptReference.TYPE); - } - finally { - mcpServer.closeGracefully(); - } - } - - // --------------------------------------- - // Ping Tests - // --------------------------------------- - @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<>(""); - - McpServerFeatures.AsyncToolSpecification tool = McpServerFeatures.AsyncToolSpecification.builder() - .tool(Tool.builder() - .name("ping-async-test") - .description("Test ping async behavior") - .inputSchema(EMPTY_JSON_SCHEMA) - .build()) - .callHandler((exchange, request) -> { - - executionOrder.set(executionOrder.get() + "1"); - - // Test async ping behavior - return exchange.ping().doOnNext(result -> { - - assertThat(result).isNotNull(); - // Ping should return an empty object or map - assertThat(result).isInstanceOf(Map.class); - - executionOrder.set(executionOrder.get() + "2"); - assertThat(result).isNotNull(); - }).then(Mono.fromCallable(() -> { - executionOrder.set(executionOrder.get() + "3"); - return CallToolResult.builder() - .content(List.of(new McpSchema.TextContent("Async ping test completed"))) - .isError(false) - .build(); - })); - }) - .build(); - - var mcpServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .tools(tool) - .build(); - - try (var mcpClient = clientBuilder.build()) { - - // Initialize client - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); - - // Call the tool that tests ping async behavior - CallToolResult result = mcpClient.callTool(new McpSchema.CallToolRequest("ping-async-test", Map.of())); - assertThat(result).isNotNull(); - assertThat(result.content().get(0)).isInstanceOf(McpSchema.TextContent.class); - assertThat(((McpSchema.TextContent) result.content().get(0)).text()).isEqualTo("Async ping test completed"); - - // Verify execution order - assertThat(executionOrder.get()).isEqualTo("123"); - } - finally { - mcpServer.closeGracefully().block(); - } - } - - // --------------------------------------- - // Tool Structured Output Schema Tests - // --------------------------------------- - @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", - Map.of("type", "string"), "timestamp", Map.of("type", "string")), - "required", List.of("result", "operation")); - - Tool calculatorTool = Tool.builder() - .name("calculator") - .description("Performs mathematical calculations") - .outputSchema(outputSchema) - .build(); - - McpServerFeatures.SyncToolSpecification tool = McpServerFeatures.SyncToolSpecification.builder() - .tool(calculatorTool) - .callHandler((exchange, request) -> { - String expression = (String) request.arguments().getOrDefault("expression", "2 + 3"); - double result = evaluateExpression(expression); - return CallToolResult.builder() - .structuredContent( - Map.of("result", result, "operation", expression, "timestamp", "2024-01-01T10:00:00Z")) - .build(); - }) - .build(); - - var mcpServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .tools(tool) - .build(); - - try (var mcpClient = clientBuilder.build()) { - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); - - // Verify tool is listed with output schema - var toolsList = mcpClient.listTools(); - assertThat(toolsList.tools()).hasSize(1); - assertThat(toolsList.tools().get(0).name()).isEqualTo("calculator"); - // Note: outputSchema might be null in sync server, but validation still works - - // Call tool with valid structured output - CallToolResult response = mcpClient - .callTool(new McpSchema.CallToolRequest("calculator", Map.of("expression", "2 + 3"))); - - assertThat(response).isNotNull(); - assertThat(response.isError()).isFalse(); - - // In WebMVC, structured content is returned properly - if (response.structuredContent() != null) { - assertThat((Map) response.structuredContent()).containsEntry("result", 5.0) - .containsEntry("operation", "2 + 3") - .containsEntry("timestamp", "2024-01-01T10:00:00Z"); - } - else { - // Fallback to checking content if structured content is not available - assertThat(response.content()).isNotEmpty(); - } - - assertThat(response.structuredContent()).isNotNull(); - assertThatJson(response.structuredContent()).when(Option.IGNORING_ARRAY_ORDER) - .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) - .isObject() - .isEqualTo(json(""" - {"result":5.0,"operation":"2 + 3","timestamp":"2024-01-01T10:00:00Z"}""")); - } - finally { - mcpServer.closeGracefully(); - } - } - - @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 - "type", "array", - "items", Map.of( - "type", "object", - "properties", Map.of( - "name", Map.of("type", "string"), - "age", Map.of("type", "number")), - "required", List.of("name", "age"))); // @formatter:on - - Tool calculatorTool = Tool.builder() - .name("getMembers") - .description("Returns a list of members") - .outputSchema(outputSchema) - .build(); - - McpServerFeatures.SyncToolSpecification tool = McpServerFeatures.SyncToolSpecification.builder() - .tool(calculatorTool) - .callHandler((exchange, request) -> { - return CallToolResult.builder() - .structuredContent(List.of(Map.of("name", "John", "age", 30), Map.of("name", "Peter", "age", 25))) - .build(); - }) - .build(); - - var mcpServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .tools(tool) - .build(); - - try (var mcpClient = clientBuilder.build()) { - assertThat(mcpClient.initialize()).isNotNull(); - - // Call tool with valid structured output of type array - CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("getMembers", Map.of())); - - assertThat(response).isNotNull(); - assertThat(response.isError()).isFalse(); - - assertThat(response.structuredContent()).isNotNull(); - assertThatJson(response.structuredContent()).when(Option.IGNORING_ARRAY_ORDER) - .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) - .isArray() - .hasSize(2) - .containsExactlyInAnyOrder(json(""" - {"name":"John","age":30}"""), json(""" - {"name":"Peter","age":25}""")); - } - finally { - mcpServer.closeGracefully(); - } - } - - @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", - Map.of("type", "string"), "timestamp", Map.of("type", "string")), - "required", List.of("result", "operation")); - - Tool calculatorTool = Tool.builder() - .name("calculator") - .description("Performs mathematical calculations") - .outputSchema(outputSchema) - .build(); - - // Handler that returns an error result - McpServerFeatures.SyncToolSpecification tool = McpServerFeatures.SyncToolSpecification.builder() - .tool(calculatorTool) - .callHandler((exchange, request) -> CallToolResult.builder() - .isError(true) - .content(List.of(new TextContent("Error calling tool: Simulated in-handler error"))) - .build()) - .build(); - - var mcpServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .tools(tool) - .build(); - - try (var mcpClient = clientBuilder.build()) { - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); - - // Verify tool is listed with output schema - var toolsList = mcpClient.listTools(); - assertThat(toolsList.tools()).hasSize(1); - assertThat(toolsList.tools().get(0).name()).isEqualTo("calculator"); - // Note: outputSchema might be null in sync server, but validation still works - - // Call tool with valid structured output - CallToolResult response = mcpClient - .callTool(new McpSchema.CallToolRequest("calculator", Map.of("expression", "2 + 3"))); - - assertThat(response).isNotNull(); - assertThat(response.isError()).isTrue(); - assertThat(response.content()).isNotEmpty(); - assertThat(response.content()) - .containsExactly(new McpSchema.TextContent("Error calling tool: Simulated in-handler error")); - assertThat(response.structuredContent()).isNull(); - } - finally { - mcpServer.closeGracefully(); - } - } - - @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", - List.of("result", "operation")); - - Tool calculatorTool = Tool.builder() - .name("calculator") - .description("Performs mathematical calculations") - .outputSchema(outputSchema) - .build(); - - McpServerFeatures.SyncToolSpecification tool = McpServerFeatures.SyncToolSpecification.builder() - .tool(calculatorTool) - .callHandler((exchange, request) -> { - // Return invalid structured output. Result should be number, missing - // operation - return CallToolResult.builder() - .addTextContent("Invalid calculation") - .structuredContent(Map.of("result", "not-a-number", "extra", "field")) - .build(); - }) - .build(); - - var mcpServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .tools(tool) - .build(); - - try (var mcpClient = clientBuilder.build()) { - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); - - // Call tool with invalid structured output - CallToolResult response = mcpClient - .callTool(new McpSchema.CallToolRequest("calculator", Map.of("expression", "2 + 3"))); - - assertThat(response).isNotNull(); - assertThat(response.isError()).isTrue(); - assertThat(response.content()).hasSize(1); - assertThat(response.content().get(0)).isInstanceOf(McpSchema.TextContent.class); - - String errorMessage = ((McpSchema.TextContent) response.content().get(0)).text(); - assertThat(errorMessage).contains("Validation failed"); - } - finally { - mcpServer.closeGracefully(); - } - } - - @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")); - - Tool calculatorTool = Tool.builder() - .name("calculator") - .description("Performs mathematical calculations") - .outputSchema(outputSchema) - .build(); - - McpServerFeatures.SyncToolSpecification tool = McpServerFeatures.SyncToolSpecification.builder() - .tool(calculatorTool) - .callHandler((exchange, request) -> { - // Return result without structured content but tool has output schema - return CallToolResult.builder().addTextContent("Calculation completed").build(); - }) - .build(); - - var mcpServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .tools(tool) - .build(); - - try (var mcpClient = clientBuilder.build()) { - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); - - // Call tool that should return structured content but doesn't - CallToolResult response = mcpClient - .callTool(new McpSchema.CallToolRequest("calculator", Map.of("expression", "2 + 3"))); - - assertThat(response).isNotNull(); - assertThat(response.isError()).isTrue(); - assertThat(response.content()).hasSize(1); - assertThat(response.content().get(0)).isInstanceOf(McpSchema.TextContent.class); - - String errorMessage = ((McpSchema.TextContent) response.content().get(0)).text(); - assertThat(errorMessage).isEqualTo( - "Response missing structured content which is expected when calling tool with non-empty outputSchema"); - } - finally { - mcpServer.closeGracefully(); - } - } - - @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 = clientBuilder.build()) { - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); - - // Initially no tools - assertThat(mcpClient.listTools().tools()).isEmpty(); - - // Add tool with output schema at runtime - Map outputSchema = Map.of("type", "object", "properties", - Map.of("message", Map.of("type", "string"), "count", Map.of("type", "integer")), "required", - List.of("message", "count")); - - Tool dynamicTool = Tool.builder() - .name("dynamic-tool") - .description("Dynamically added tool") - .outputSchema(outputSchema) - .build(); - - McpServerFeatures.SyncToolSpecification toolSpec = McpServerFeatures.SyncToolSpecification.builder() - .tool(dynamicTool) - .callHandler((exchange, request) -> { - int count = (Integer) request.arguments().getOrDefault("count", 1); - return CallToolResult.builder() - .addTextContent("Dynamic tool executed " + count + " times") - .structuredContent(Map.of("message", "Dynamic execution", "count", count)) - .build(); - }) - .build(); - - // Add tool to server - mcpServer.addTool(toolSpec); - - // Wait for tool list change notification - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(mcpClient.listTools().tools()).hasSize(1); - }); - - // Verify tool was added with output schema - var toolsList = mcpClient.listTools(); - assertThat(toolsList.tools()).hasSize(1); - assertThat(toolsList.tools().get(0).name()).isEqualTo("dynamic-tool"); - // Note: outputSchema might be null in sync server, but validation still works - - // Call dynamically added tool - CallToolResult response = mcpClient - .callTool(new McpSchema.CallToolRequest("dynamic-tool", Map.of("count", 3))); - - assertThat(response).isNotNull(); - assertThat(response.isError()).isFalse(); - - assertThat(response.content()).hasSize(1); - assertThat(response.content().get(0)).isInstanceOf(McpSchema.TextContent.class); - assertThat(((McpSchema.TextContent) response.content().get(0)).text()) - .isEqualTo("Dynamic tool executed 3 times"); - - assertThat(response.structuredContent()).isNotNull(); - assertThatJson(response.structuredContent()).when(Option.IGNORING_ARRAY_ORDER) - .when(Option.IGNORING_EXTRA_ARRAY_ITEMS) - .isObject() - .isEqualTo(json(""" - {"count":3,"message":"Dynamic execution"}""")); - } - finally { - mcpServer.closeGracefully(); - } - } - - private double evaluateExpression(String expression) { - // Simple expression evaluator for testing - return switch (expression) { - case "2 + 3" -> 5.0; - case "10 * 2" -> 20.0; - case "7 + 8" -> 15.0; - case "5 + 3" -> 8.0; - default -> 0.0; - }; - } - -} diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java deleted file mode 100644 index 915c658..0000000 --- a/mcp-core/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java +++ /dev/null @@ -1,678 +0,0 @@ -/* - * Copyright 2024-2024 the original author or authors. - */ - -package io.modelcontextprotocol.server; - -import java.util.List; - -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.CallToolResult; -import io.modelcontextprotocol.spec.McpSchema.GetPromptResult; -import io.modelcontextprotocol.spec.McpSchema.Prompt; -import io.modelcontextprotocol.spec.McpSchema.PromptMessage; -import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult; -import io.modelcontextprotocol.spec.McpSchema.Resource; -import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities; -import io.modelcontextprotocol.spec.McpSchema.Tool; -import io.modelcontextprotocol.spec.McpServerTransportProvider; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import static io.modelcontextprotocol.util.ToolsUtils.EMPTY_JSON_SCHEMA; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -/** - * Test suite for the {@link McpSyncServer} that can be used with different - * {@link McpServerTransportProvider} implementations. - * - * @author Christian Tzolov - */ -// KEEP IN SYNC with the class in mcp-test module -public abstract class AbstractMcpSyncServerTests { - - private static final String TEST_TOOL_NAME = "test-tool"; - - private static final String TEST_RESOURCE_URI = "test://resource"; - - private static final String TEST_PROMPT_NAME = "test-prompt"; - - abstract protected McpServer.SyncSpecification prepareSyncServerBuilder(); - - protected void onStart() { - } - - protected void onClose() { - } - - @BeforeEach - void setUp() { - // onStart(); - } - - @AfterEach - void tearDown() { - onClose(); - } - - // --------------------------------------- - // Server Lifecycle Tests - // --------------------------------------- - - @Test - void testConstructorWithInvalidArguments() { - assertThatThrownBy(() -> McpServer.sync((McpServerTransportProvider) null)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Transport provider must not be null"); - - assertThatThrownBy(() -> prepareSyncServerBuilder().serverInfo(null)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Server info must not be null"); - } - - @Test - void testGracefulShutdown() { - var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0").build(); - - assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException(); - } - - @Test - void testImmediateClose() { - var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0").build(); - - assertThatCode(mcpSyncServer::close).doesNotThrowAnyException(); - } - - @Test - void testGetAsyncServer() { - var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0").build(); - - assertThat(mcpSyncServer.getAsyncServer()).isNotNull(); - - assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException(); - } - - // --------------------------------------- - // Tools Tests - // --------------------------------------- - - @Test - @Deprecated - void testAddTool() { - var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .build(); - - Tool newTool = McpSchema.Tool.builder() - .name("new-tool") - .title("New test tool") - .inputSchema(EMPTY_JSON_SCHEMA) - .build(); - assertThatCode(() -> mcpSyncServer.addTool(new McpServerFeatures.SyncToolSpecification(newTool, - (exchange, args) -> CallToolResult.builder().content(List.of()).isError(false).build()))) - .doesNotThrowAnyException(); - - assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException(); - } - - @Test - void testAddToolCall() { - var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .build(); - - Tool newTool = McpSchema.Tool.builder() - .name("new-tool") - .title("New test tool") - .inputSchema(EMPTY_JSON_SCHEMA) - .build(); - - assertThatCode(() -> mcpSyncServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(newTool) - .callHandler((exchange, request) -> CallToolResult.builder().content(List.of()).isError(false).build()) - .build())).doesNotThrowAnyException(); - - assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException(); - } - - @Test - @Deprecated - void testAddDuplicateTool() { - Tool duplicateTool = McpSchema.Tool.builder() - .name(TEST_TOOL_NAME) - .title("Duplicate tool") - .inputSchema(EMPTY_JSON_SCHEMA) - .build(); - - var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .tool(duplicateTool, (exchange, args) -> CallToolResult.builder().content(List.of()).isError(false).build()) - .build(); - - assertThatCode(() -> mcpSyncServer.addTool(new McpServerFeatures.SyncToolSpecification(duplicateTool, - (exchange, args) -> CallToolResult.builder().content(List.of()).isError(false).build()))) - .doesNotThrowAnyException(); - - assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException(); - } - - @Test - void testAddDuplicateToolCall() { - Tool duplicateTool = McpSchema.Tool.builder() - .name(TEST_TOOL_NAME) - .title("Duplicate tool") - .inputSchema(EMPTY_JSON_SCHEMA) - .build(); - - var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .toolCall(duplicateTool, - (exchange, request) -> CallToolResult.builder().content(List.of()).isError(false).build()) - .build(); - - assertThatCode(() -> mcpSyncServer.addTool(McpServerFeatures.SyncToolSpecification.builder() - .tool(duplicateTool) - .callHandler((exchange, request) -> CallToolResult.builder().content(List.of()).isError(false).build()) - .build())).doesNotThrowAnyException(); - - assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException(); - } - - @Test - void testDuplicateToolCallDuringBuilding() { - Tool duplicateTool = McpSchema.Tool.builder() - .name("duplicate-build-toolcall") - .title("Duplicate toolcall during building") - .inputSchema(EMPTY_JSON_SCHEMA) - .build(); - - assertThatThrownBy(() -> prepareSyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .toolCall(duplicateTool, - (exchange, request) -> CallToolResult.builder().content(List.of()).isError(false).build()) - .toolCall(duplicateTool, - (exchange, request) -> CallToolResult.builder().content(List.of()).isError(false).build()) // Duplicate! - .build()).isInstanceOf(IllegalArgumentException.class) - .hasMessage("Tool with name 'duplicate-build-toolcall' is already registered."); - } - - @Test - void testDuplicateToolsInBatchListRegistration() { - Tool duplicateTool = McpSchema.Tool.builder() - .name("batch-list-tool") - .title("Duplicate tool in batch list") - .inputSchema(EMPTY_JSON_SCHEMA) - .build(); - List specs = List.of( - McpServerFeatures.SyncToolSpecification.builder() - .tool(duplicateTool) - .callHandler( - (exchange, request) -> CallToolResult.builder().content(List.of()).isError(false).build()) - .build(), - McpServerFeatures.SyncToolSpecification.builder() - .tool(duplicateTool) - .callHandler( - (exchange, request) -> CallToolResult.builder().content(List.of()).isError(false).build()) - .build() // Duplicate! - ); - - assertThatThrownBy(() -> prepareSyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .tools(specs) - .build()).isInstanceOf(IllegalArgumentException.class) - .hasMessage("Tool with name 'batch-list-tool' is already registered."); - } - - @Test - void testDuplicateToolsInBatchVarargsRegistration() { - Tool duplicateTool = McpSchema.Tool.builder() - .name("batch-varargs-tool") - .title("Duplicate tool in batch varargs") - .inputSchema(EMPTY_JSON_SCHEMA) - .build(); - - assertThatThrownBy(() -> prepareSyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .tools(McpServerFeatures.SyncToolSpecification.builder() - .tool(duplicateTool) - .callHandler((exchange, request) -> CallToolResult.builder().content(List.of()).isError(false).build()) - .build(), - McpServerFeatures.SyncToolSpecification.builder() - .tool(duplicateTool) - .callHandler((exchange, - request) -> CallToolResult.builder().content(List.of()).isError(false).build()) - .build() // Duplicate! - ) - .build()).isInstanceOf(IllegalArgumentException.class) - .hasMessage("Tool with name 'batch-varargs-tool' is already registered."); - } - - @Test - void testRemoveTool() { - Tool tool = McpSchema.Tool.builder() - .name(TEST_TOOL_NAME) - .title("Test tool") - .inputSchema(EMPTY_JSON_SCHEMA) - .build(); - - var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .toolCall(tool, (exchange, args) -> CallToolResult.builder().content(List.of()).isError(false).build()) - .build(); - - assertThatCode(() -> mcpSyncServer.removeTool(TEST_TOOL_NAME)).doesNotThrowAnyException(); - - assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException(); - } - - @Test - void testRemoveNonexistentTool() { - var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .build(); - - assertThatCode(() -> mcpSyncServer.removeTool("nonexistent-tool")).doesNotThrowAnyException(); - - assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException(); - } - - @Test - void testNotifyToolsListChanged() { - var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0").build(); - - assertThatCode(mcpSyncServer::notifyToolsListChanged).doesNotThrowAnyException(); - - assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException(); - } - - // --------------------------------------- - // Resources Tests - // --------------------------------------- - - @Test - void testNotifyResourcesListChanged() { - var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0").build(); - - assertThatCode(mcpSyncServer::notifyResourcesListChanged).doesNotThrowAnyException(); - - assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException(); - } - - @Test - void testNotifyResourcesUpdated() { - var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0").build(); - - assertThatCode(() -> mcpSyncServer - .notifyResourcesUpdated(new McpSchema.ResourcesUpdatedNotification(TEST_RESOURCE_URI))) - .doesNotThrowAnyException(); - - assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException(); - } - - @Test - void testAddResource() { - var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().resources(true, false).build()) - .build(); - - Resource resource = Resource.builder() - .uri(TEST_RESOURCE_URI) - .name("Test Resource") - .mimeType("text/plain") - .description("Test resource description") - .build(); - McpServerFeatures.SyncResourceSpecification specification = new McpServerFeatures.SyncResourceSpecification( - resource, (exchange, req) -> new ReadResourceResult(List.of())); - - assertThatCode(() -> mcpSyncServer.addResource(specification)).doesNotThrowAnyException(); - - assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException(); - } - - @Test - void testAddResourceWithNullSpecification() { - var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().resources(true, false).build()) - .build(); - - assertThatThrownBy(() -> mcpSyncServer.addResource((McpServerFeatures.SyncResourceSpecification) null)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Resource must not be null"); - - assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException(); - } - - @Test - void testAddResourceWithoutCapability() { - var serverWithoutResources = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0").build(); - - Resource resource = Resource.builder() - .uri(TEST_RESOURCE_URI) - .name("Test Resource") - .mimeType("text/plain") - .description("Test resource description") - .build(); - McpServerFeatures.SyncResourceSpecification specification = new McpServerFeatures.SyncResourceSpecification( - resource, (exchange, req) -> new ReadResourceResult(List.of())); - - assertThatThrownBy(() -> serverWithoutResources.addResource(specification)) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("Server must be configured with resource capabilities"); - } - - @Test - void testRemoveResourceWithoutCapability() { - var serverWithoutResources = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0").build(); - - assertThatThrownBy(() -> serverWithoutResources.removeResource(TEST_RESOURCE_URI)) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("Server must be configured with resource capabilities"); - } - - @Test - void testListResources() { - var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().resources(true, false).build()) - .build(); - - Resource resource = Resource.builder() - .uri(TEST_RESOURCE_URI) - .name("Test Resource") - .mimeType("text/plain") - .description("Test resource description") - .build(); - McpServerFeatures.SyncResourceSpecification specification = new McpServerFeatures.SyncResourceSpecification( - resource, (exchange, req) -> new ReadResourceResult(List.of())); - - mcpSyncServer.addResource(specification); - List resources = mcpSyncServer.listResources(); - - assertThat(resources).hasSize(1); - assertThat(resources.get(0).uri()).isEqualTo(TEST_RESOURCE_URI); - - assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException(); - } - - @Test - void testRemoveResource() { - var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().resources(true, false).build()) - .build(); - - Resource resource = Resource.builder() - .uri(TEST_RESOURCE_URI) - .name("Test Resource") - .mimeType("text/plain") - .description("Test resource description") - .build(); - McpServerFeatures.SyncResourceSpecification specification = new McpServerFeatures.SyncResourceSpecification( - resource, (exchange, req) -> new ReadResourceResult(List.of())); - - mcpSyncServer.addResource(specification); - assertThatCode(() -> mcpSyncServer.removeResource(TEST_RESOURCE_URI)).doesNotThrowAnyException(); - - assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException(); - } - - @Test - void testRemoveNonexistentResource() { - var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().resources(true, false).build()) - .build(); - - // Removing a non-existent resource should complete successfully (no error) - // as per the new implementation that just logs a warning - assertThatCode(() -> mcpSyncServer.removeResource("nonexistent://resource")).doesNotThrowAnyException(); - - assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException(); - } - - // --------------------------------------- - // Resource Template Tests - // --------------------------------------- - - @Test - void testAddResourceTemplate() { - var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().resources(true, false).build()) - .build(); - - McpSchema.ResourceTemplate template = McpSchema.ResourceTemplate.builder() - .uriTemplate("test://template/{id}") - .name("test-template") - .description("Test resource template") - .mimeType("text/plain") - .build(); - - McpServerFeatures.SyncResourceTemplateSpecification specification = new McpServerFeatures.SyncResourceTemplateSpecification( - template, (exchange, req) -> new ReadResourceResult(List.of())); - - assertThatCode(() -> mcpSyncServer.addResourceTemplate(specification)).doesNotThrowAnyException(); - - assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException(); - } - - @Test - void testAddResourceTemplateWithoutCapability() { - // Create a server without resource capabilities - var serverWithoutResources = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0").build(); - - McpSchema.ResourceTemplate template = McpSchema.ResourceTemplate.builder() - .uriTemplate("test://template/{id}") - .name("test-template") - .description("Test resource template") - .mimeType("text/plain") - .build(); - - McpServerFeatures.SyncResourceTemplateSpecification specification = new McpServerFeatures.SyncResourceTemplateSpecification( - template, (exchange, req) -> new ReadResourceResult(List.of())); - - assertThatThrownBy(() -> serverWithoutResources.addResourceTemplate(specification)) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("Server must be configured with resource capabilities"); - } - - @Test - void testRemoveResourceTemplate() { - McpSchema.ResourceTemplate template = McpSchema.ResourceTemplate.builder() - .uriTemplate("test://template/{id}") - .name("test-template") - .description("Test resource template") - .mimeType("text/plain") - .build(); - - McpServerFeatures.SyncResourceTemplateSpecification specification = new McpServerFeatures.SyncResourceTemplateSpecification( - template, (exchange, req) -> new ReadResourceResult(List.of())); - - var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().resources(true, false).build()) - .resourceTemplates(specification) - .build(); - - assertThatCode(() -> mcpSyncServer.removeResourceTemplate("test://template/{id}")).doesNotThrowAnyException(); - - assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException(); - } - - @Test - void testRemoveResourceTemplateWithoutCapability() { - // Create a server without resource capabilities - var serverWithoutResources = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0").build(); - - assertThatThrownBy(() -> serverWithoutResources.removeResourceTemplate("test://template/{id}")) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("Server must be configured with resource capabilities"); - } - - @Test - void testRemoveNonexistentResourceTemplate() { - var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().resources(true, false).build()) - .build(); - - assertThatCode(() -> mcpSyncServer.removeResourceTemplate("nonexistent://template/{id}")) - .doesNotThrowAnyException(); - - assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException(); - } - - @Test - void testListResourceTemplates() { - McpSchema.ResourceTemplate template = McpSchema.ResourceTemplate.builder() - .uriTemplate("test://template/{id}") - .name("test-template") - .description("Test resource template") - .mimeType("text/plain") - .build(); - - McpServerFeatures.SyncResourceTemplateSpecification specification = new McpServerFeatures.SyncResourceTemplateSpecification( - template, (exchange, req) -> new ReadResourceResult(List.of())); - - var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().resources(true, false).build()) - .resourceTemplates(specification) - .build(); - - List templates = mcpSyncServer.listResourceTemplates(); - - assertThat(templates).isNotNull(); - - assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException(); - } - - // --------------------------------------- - // Prompts Tests - // --------------------------------------- - - @Test - void testNotifyPromptsListChanged() { - var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0").build(); - - assertThatCode(mcpSyncServer::notifyPromptsListChanged).doesNotThrowAnyException(); - - assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException(); - } - - @Test - void testAddPromptWithNullSpecification() { - var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().prompts(false).build()) - .build(); - - assertThatThrownBy(() -> mcpSyncServer.addPrompt((McpServerFeatures.SyncPromptSpecification) null)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Prompt specification must not be null"); - } - - @Test - void testAddPromptWithoutCapability() { - var serverWithoutPrompts = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0").build(); - - Prompt prompt = new Prompt(TEST_PROMPT_NAME, "Test Prompt", "Test Prompt", List.of()); - McpServerFeatures.SyncPromptSpecification specification = new McpServerFeatures.SyncPromptSpecification(prompt, - (exchange, req) -> new GetPromptResult("Test prompt description", List - .of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content"))))); - - assertThatThrownBy(() -> serverWithoutPrompts.addPrompt(specification)) - .isInstanceOf(IllegalStateException.class) - .hasMessage("Server must be configured with prompt capabilities"); - } - - @Test - void testRemovePromptWithoutCapability() { - var serverWithoutPrompts = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0").build(); - - assertThatThrownBy(() -> serverWithoutPrompts.removePrompt(TEST_PROMPT_NAME)) - .isInstanceOf(IllegalStateException.class) - .hasMessage("Server must be configured with prompt capabilities"); - } - - @Test - void testRemovePrompt() { - Prompt prompt = new Prompt(TEST_PROMPT_NAME, "Test Prompt", "Test Prompt", List.of()); - McpServerFeatures.SyncPromptSpecification specification = new McpServerFeatures.SyncPromptSpecification(prompt, - (exchange, req) -> new GetPromptResult("Test prompt description", List - .of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content"))))); - - var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().prompts(true).build()) - .prompts(specification) - .build(); - - assertThatCode(() -> mcpSyncServer.removePrompt(TEST_PROMPT_NAME)).doesNotThrowAnyException(); - - assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException(); - } - - @Test - void testRemoveNonexistentPrompt() { - var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().prompts(true).build()) - .build(); - - assertThatCode(() -> mcpSyncServer.removePrompt("nonexistent://template/{id}")).doesNotThrowAnyException(); - - assertThatCode(mcpSyncServer::closeGracefully).doesNotThrowAnyException(); - } - - // --------------------------------------- - // Roots Tests - // --------------------------------------- - - @Test - void testRootsChangeHandlers() { - // Test with single consumer - var rootsReceived = new McpSchema.Root[1]; - var consumerCalled = new boolean[1]; - - var singleConsumerServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0") - .rootsChangeHandlers(List.of((exchange, roots) -> { - consumerCalled[0] = true; - if (!roots.isEmpty()) { - rootsReceived[0] = roots.get(0); - } - })) - .build(); - assertThat(singleConsumerServer).isNotNull(); - assertThatCode(singleConsumerServer::closeGracefully).doesNotThrowAnyException(); - onClose(); - - // Test with multiple consumers - var consumer1Called = new boolean[1]; - var consumer2Called = new boolean[1]; - var rootsContent = new List[1]; - - var multipleConsumersServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0") - .rootsChangeHandlers(List.of((exchange, roots) -> { - consumer1Called[0] = true; - rootsContent[0] = roots; - }, (exchange, roots) -> consumer2Called[0] = true)) - .build(); - - assertThat(multipleConsumersServer).isNotNull(); - assertThatCode(multipleConsumersServer::closeGracefully).doesNotThrowAnyException(); - onClose(); - - // Test error handling - var errorHandlingServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0") - .rootsChangeHandlers(List.of((exchange, roots) -> { - throw new RuntimeException("Test error"); - })) - .build(); - - assertThat(errorHandlingServer).isNotNull(); - assertThatCode(errorHandlingServer::closeGracefully).doesNotThrowAnyException(); - onClose(); - - // Test without consumers - var noConsumersServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0").build(); - - assertThat(noConsumersServer).isNotNull(); - assertThatCode(noConsumersServer::closeGracefully).doesNotThrowAnyException(); - } - -}