Remove unused abstract test classes in mcp-core
Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
This commit is contained in:
parent
5e186a92f6
commit
82102a18f5
6 changed files with 0 additions and 4898 deletions
|
|
@ -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<McpClient.AsyncSpec, McpClient.AsyncSpec> customizer) {
|
|
||||||
AtomicReference<McpAsyncClient> 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<McpAsyncClient> c) {
|
|
||||||
withClient(transport, Function.identity(), c);
|
|
||||||
}
|
|
||||||
|
|
||||||
void withClient(McpClientTransport transport, Function<McpClient.AsyncSpec, McpClient.AsyncSpec> customizer,
|
|
||||||
Consumer<McpAsyncClient> 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<List<McpSchema.Tool>> 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();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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<McpClient.AsyncSpec, McpClient.AsyncSpec> customizer) {
|
|
||||||
AtomicReference<McpAsyncClient> 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<McpAsyncClient> c) {
|
|
||||||
withClient(transport, Function.identity(), c);
|
|
||||||
}
|
|
||||||
|
|
||||||
void withClient(McpClientTransport transport, Function<McpClient.AsyncSpec, McpClient.AsyncSpec> customizer,
|
|
||||||
Consumer<McpAsyncClient> c) {
|
|
||||||
var client = client(transport, customizer);
|
|
||||||
try {
|
|
||||||
c.accept(client);
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
StepVerifier.create(client.closeGracefully()).expectComplete().verify(Duration.ofSeconds(10));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
<T> void verifyNotificationSucceedsWithImplicitInitialization(Function<McpAsyncClient, Mono<T>> operation,
|
|
||||||
String action) {
|
|
||||||
withClient(createMcpTransport(), mcpAsyncClient -> {
|
|
||||||
StepVerifier.create(operation.apply(mcpAsyncClient)).verifyComplete();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
<T> void verifyCallSucceedsWithImplicitInitialization(Function<McpAsyncClient, Mono<T>> 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<McpSchema.ReadResourceResult> resources = client.initialize()
|
|
||||||
.then(client.listResources(null))
|
|
||||||
.flatMapMany(r -> {
|
|
||||||
List<Resource> 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<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler = request -> Mono
|
|
||||||
.just(CreateMessageResult.builder().message("test").model("test-model").build());
|
|
||||||
|
|
||||||
Function<ElicitRequest, Mono<ElicitResult>> 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<String> receivedPrompt = new AtomicReference<>();
|
|
||||||
AtomicReference<String> 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<McpSchema.ProgressNotification> sink = Sinks.many().unicast().onBackpressureBuffer();
|
|
||||||
List<McpSchema.ProgressNotification> 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");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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<McpClient.SyncSpec, McpClient.SyncSpec> customizer) {
|
|
||||||
AtomicReference<McpSyncClient> 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<McpSyncClient> c) {
|
|
||||||
withClient(transport, Function.identity(), c);
|
|
||||||
}
|
|
||||||
|
|
||||||
void withClient(McpClientTransport transport, Function<McpClient.SyncSpec, McpClient.SyncSpec> customizer,
|
|
||||||
Consumer<McpSyncClient> c) {
|
|
||||||
var client = client(transport, customizer);
|
|
||||||
try {
|
|
||||||
c.accept(client);
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
assertThat(client.closeGracefully()).isTrue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static final Object DUMMY_RETURN_VALUE = new Object();
|
|
||||||
|
|
||||||
<T> void verifyNotificationSucceedsWithImplicitInitialization(Consumer<McpSyncClient> operation, String action) {
|
|
||||||
verifyCallSucceedsWithImplicitInitialization(client -> {
|
|
||||||
operation.accept(client);
|
|
||||||
return DUMMY_RETURN_VALUE;
|
|
||||||
}, action);
|
|
||||||
}
|
|
||||||
|
|
||||||
<T> void verifyCallSucceedsWithImplicitInitialization(Function<McpSyncClient, T> 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<List<Resource>> 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<String> receivedPrompt = new AtomicReference<>();
|
|
||||||
AtomicReference<String> 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<McpSchema.ProgressNotification> 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");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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<McpServerFeatures.AsyncToolSpecification> 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<Resource>
|
|
||||||
// This appears to be a bug in the implementation that should return
|
|
||||||
// Flux<ResourceTemplate>
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -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<McpServerFeatures.SyncToolSpecification> 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<McpSchema.Resource> 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<McpSchema.ResourceTemplate> 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue