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