Remove unused abstract test classes in mcp-core

Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
This commit is contained in:
Daniel Garnier-Moiroux 2026-02-12 10:44:03 +01:00
commit 82102a18f5
6 changed files with 0 additions and 4898 deletions

View file

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

View file

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

View file

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

View file

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

View file

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