Improve client test reliability and execution time
This change uses VirtualTimeScheduler and pretends enough time has passed to trigger a timeout on the initialization. Another problem with reliability of the tests was that the used testcontainer for the SSE server does not support multiple clients and the existence of both the global client for the entire suite and some customized local clients in some tests caused responses to be delivered to the other client at some racing situations. Now each test creates a dedicated client and performs cleanup locally. While these tests were improved, two other issues were found and fixed. The first one is that the closeGracefully of DefaultMcpSession was not lazy and would trigger connection disposal before the returned Mono was subscribed. The second one was dealing with closing the StdIo client before the process was started. In such a case there should not be an error but rather a warning and successful completion.
This commit is contained in:
parent
92ec67a965
commit
37120f2ae5
7 changed files with 1016 additions and 718 deletions
|
|
@ -6,7 +6,10 @@ package io.modelcontextprotocol.client;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
import io.modelcontextprotocol.spec.ClientMcpTransport;
|
import io.modelcontextprotocol.spec.ClientMcpTransport;
|
||||||
|
|
@ -44,10 +47,6 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
*/
|
*/
|
||||||
public abstract class AbstractMcpAsyncClientTests {
|
public abstract class AbstractMcpAsyncClientTests {
|
||||||
|
|
||||||
private McpAsyncClient mcpAsyncClient;
|
|
||||||
|
|
||||||
protected ClientMcpTransport mcpTransport;
|
|
||||||
|
|
||||||
private static final String ECHO_TEST_MESSAGE = "Hello MCP Spring AI!";
|
private static final String ECHO_TEST_MESSAGE = "Hello MCP Spring AI!";
|
||||||
|
|
||||||
abstract protected ClientMcpTransport createMcpTransport();
|
abstract protected ClientMcpTransport createMcpTransport();
|
||||||
|
|
@ -66,25 +65,47 @@ public abstract class AbstractMcpAsyncClientTests {
|
||||||
return Duration.ofSeconds(2);
|
return Duration.ofSeconds(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
McpAsyncClient client(ClientMcpTransport transport) {
|
||||||
|
return client(transport, Function.identity());
|
||||||
|
}
|
||||||
|
|
||||||
|
McpAsyncClient client(ClientMcpTransport transport, Function<McpClient.AsyncSpec, McpClient.AsyncSpec> customizer) {
|
||||||
|
AtomicReference<McpAsyncClient> client = new AtomicReference<>();
|
||||||
|
|
||||||
|
assertThatCode(() -> {
|
||||||
|
McpClient.AsyncSpec builder = McpClient.async(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(ClientMcpTransport transport, Consumer<McpAsyncClient> c) {
|
||||||
|
withClient(transport, Function.identity(), c);
|
||||||
|
}
|
||||||
|
|
||||||
|
void withClient(ClientMcpTransport 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
onStart();
|
onStart();
|
||||||
this.mcpTransport = createMcpTransport();
|
|
||||||
|
|
||||||
assertThatCode(() -> {
|
|
||||||
mcpAsyncClient = McpClient.async(mcpTransport)
|
|
||||||
.requestTimeout(getRequestTimeout())
|
|
||||||
.initializationTimeout(getInitializationTimeout())
|
|
||||||
.capabilities(ClientCapabilities.builder().roots(true).build())
|
|
||||||
.build();
|
|
||||||
}).doesNotThrowAnyException();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
void tearDown() {
|
void tearDown() {
|
||||||
if (mcpAsyncClient != null) {
|
|
||||||
StepVerifier.create(mcpAsyncClient.closeGracefully()).verifyComplete();
|
|
||||||
}
|
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -93,258 +114,323 @@ public abstract class AbstractMcpAsyncClientTests {
|
||||||
assertThatThrownBy(() -> McpClient.async(null).build()).isInstanceOf(IllegalArgumentException.class)
|
assertThatThrownBy(() -> McpClient.async(null).build()).isInstanceOf(IllegalArgumentException.class)
|
||||||
.hasMessage("Transport must not be null");
|
.hasMessage("Transport must not be null");
|
||||||
|
|
||||||
assertThatThrownBy(() -> McpClient.async(mcpTransport).requestTimeout(null).build())
|
assertThatThrownBy(() -> McpClient.async(createMcpTransport()).requestTimeout(null).build())
|
||||||
.isInstanceOf(IllegalArgumentException.class)
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
.hasMessage("Request timeout must not be null");
|
.hasMessage("Request timeout must not be null");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testListToolsWithoutInitialization() {
|
void testListToolsWithoutInitialization() {
|
||||||
StepVerifier.create(mcpAsyncClient.listTools(null)).expectErrorSatisfies(error -> {
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
assertThat(error).isInstanceOf(McpError.class)
|
StepVerifier.withVirtualTime(() -> mcpAsyncClient.listTools(null))
|
||||||
.hasMessage("Client must be initialized before listing tools");
|
.expectSubscription()
|
||||||
}).verify();
|
.thenAwait(getInitializationTimeout())
|
||||||
|
.consumeErrorWith(e -> assertThat(e).isInstanceOf(McpError.class)
|
||||||
|
.hasMessage("Client must be initialized before listing tools"))
|
||||||
|
.verify();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testListTools() {
|
void testListTools() {
|
||||||
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listTools(null)))
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
.consumeNextWith(result -> {
|
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listTools(null)))
|
||||||
assertThat(result.tools()).isNotNull().isNotEmpty();
|
.consumeNextWith(result -> {
|
||||||
|
assertThat(result.tools()).isNotNull().isNotEmpty();
|
||||||
|
|
||||||
Tool firstTool = result.tools().get(0);
|
Tool firstTool = result.tools().get(0);
|
||||||
assertThat(firstTool.name()).isNotNull();
|
assertThat(firstTool.name()).isNotNull();
|
||||||
assertThat(firstTool.description()).isNotNull();
|
assertThat(firstTool.description()).isNotNull();
|
||||||
})
|
})
|
||||||
.verifyComplete();
|
.verifyComplete();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testPingWithoutInitialization() {
|
void testPingWithoutInitialization() {
|
||||||
StepVerifier.create(mcpAsyncClient.ping())
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
.expectErrorMatches(error -> error instanceof McpError
|
StepVerifier.withVirtualTime(() -> mcpAsyncClient.ping())
|
||||||
&& error.getMessage().equals("Client must be initialized before pinging the server"))
|
.expectSubscription()
|
||||||
.verify();
|
.thenAwait(getInitializationTimeout())
|
||||||
|
.consumeErrorWith(e -> assertThat(e).isInstanceOf(McpError.class)
|
||||||
|
.hasMessage("Client must be initialized before pinging the " + "server"))
|
||||||
|
.verify();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testPing() {
|
void testPing() {
|
||||||
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.ping())).consumeNextWith(callToolResult -> {
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
}).verifyComplete();
|
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.ping()))
|
||||||
|
.expectNextCount(1)
|
||||||
|
.verifyComplete();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testCallToolWithoutInitialization() {
|
void testCallToolWithoutInitialization() {
|
||||||
CallToolRequest callToolRequest = new CallToolRequest("echo", Map.of("message", ECHO_TEST_MESSAGE));
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
|
CallToolRequest callToolRequest = new CallToolRequest("echo", Map.of("message", ECHO_TEST_MESSAGE));
|
||||||
|
|
||||||
StepVerifier.create(mcpAsyncClient.callTool(callToolRequest))
|
StepVerifier.withVirtualTime(() -> mcpAsyncClient.callTool(callToolRequest))
|
||||||
.expectErrorMatches(error -> error instanceof McpError
|
.expectSubscription()
|
||||||
&& error.getMessage().equals("Client must be initialized before calling tools"))
|
.thenAwait(getInitializationTimeout())
|
||||||
.verify();
|
.consumeErrorWith(e -> assertThat(e).isInstanceOf(McpError.class)
|
||||||
|
.hasMessage("Client must be initialized before calling tools"))
|
||||||
|
.verify();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testCallTool() {
|
void testCallTool() {
|
||||||
CallToolRequest callToolRequest = new CallToolRequest("echo", Map.of("message", ECHO_TEST_MESSAGE));
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
|
CallToolRequest callToolRequest = new CallToolRequest("echo", Map.of("message", ECHO_TEST_MESSAGE));
|
||||||
|
|
||||||
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.callTool(callToolRequest)))
|
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.callTool(callToolRequest)))
|
||||||
.consumeNextWith(callToolResult -> {
|
.consumeNextWith(callToolResult -> {
|
||||||
assertThat(callToolResult).isNotNull();
|
assertThat(callToolResult).isNotNull().satisfies(result -> {
|
||||||
assertThat(callToolResult.content()).isNotNull();
|
assertThat(result.content()).isNotNull();
|
||||||
assertThat(callToolResult.isError()).isNull();
|
assertThat(result.isError()).isNull();
|
||||||
})
|
});
|
||||||
.verifyComplete();
|
})
|
||||||
|
.verifyComplete();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testCallToolWithInvalidTool() {
|
void testCallToolWithInvalidTool() {
|
||||||
CallToolRequest invalidRequest = new CallToolRequest("nonexistent_tool", Map.of("message", ECHO_TEST_MESSAGE));
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
|
CallToolRequest invalidRequest = new CallToolRequest("nonexistent_tool",
|
||||||
|
Map.of("message", ECHO_TEST_MESSAGE));
|
||||||
|
|
||||||
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.callTool(invalidRequest)))
|
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.callTool(invalidRequest)))
|
||||||
.expectError(Exception.class)
|
.consumeErrorWith(
|
||||||
.verify();
|
e -> assertThat(e).isInstanceOf(McpError.class).hasMessage("Unknown tool: nonexistent_tool"))
|
||||||
|
.verify();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testListResourcesWithoutInitialization() {
|
void testListResourcesWithoutInitialization() {
|
||||||
StepVerifier.create(mcpAsyncClient.listResources(null))
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
.expectErrorMatches(error -> error instanceof McpError
|
StepVerifier.withVirtualTime(() -> mcpAsyncClient.listResources(null))
|
||||||
&& error.getMessage().equals("Client must be initialized before listing resources"))
|
.expectSubscription()
|
||||||
.verify();
|
.thenAwait(getInitializationTimeout())
|
||||||
|
.consumeErrorWith(e -> assertThat(e).isInstanceOf(McpError.class)
|
||||||
|
.hasMessage("Client must be initialized before listing resources"))
|
||||||
|
.verify();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testListResources() {
|
void testListResources() {
|
||||||
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listResources(null)))
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
.consumeNextWith(resources -> {
|
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listResources(null)))
|
||||||
assertThat(resources).isNotNull().satisfies(result -> {
|
.consumeNextWith(resources -> {
|
||||||
assertThat(result.resources()).isNotNull();
|
assertThat(resources).isNotNull().satisfies(result -> {
|
||||||
|
assertThat(result.resources()).isNotNull();
|
||||||
|
|
||||||
if (!result.resources().isEmpty()) {
|
if (!result.resources().isEmpty()) {
|
||||||
Resource firstResource = result.resources().get(0);
|
Resource firstResource = result.resources().get(0);
|
||||||
assertThat(firstResource.uri()).isNotNull();
|
assertThat(firstResource.uri()).isNotNull();
|
||||||
assertThat(firstResource.name()).isNotNull();
|
assertThat(firstResource.name()).isNotNull();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.verifyComplete();
|
.verifyComplete();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testMcpAsyncClientState() {
|
void testMcpAsyncClientState() {
|
||||||
assertThat(mcpAsyncClient).isNotNull();
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
|
assertThat(mcpAsyncClient).isNotNull();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testListPromptsWithoutInitialization() {
|
void testListPromptsWithoutInitialization() {
|
||||||
StepVerifier.create(mcpAsyncClient.listPrompts(null))
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
.expectErrorMatches(error -> error instanceof McpError
|
StepVerifier.withVirtualTime(() -> mcpAsyncClient.listPrompts(null))
|
||||||
&& error.getMessage().equals("Client must be initialized before listing prompts"))
|
.expectSubscription()
|
||||||
.verify();
|
.thenAwait(getInitializationTimeout())
|
||||||
|
.consumeErrorWith(e -> assertThat(e).isInstanceOf(McpError.class)
|
||||||
|
.hasMessage("Client must be initialized before listing prompts"))
|
||||||
|
.verify();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testListPrompts() {
|
void testListPrompts() {
|
||||||
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listPrompts(null)))
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
.consumeNextWith(prompts -> {
|
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listPrompts(null)))
|
||||||
assertThat(prompts).isNotNull().satisfies(result -> {
|
.consumeNextWith(prompts -> {
|
||||||
assertThat(result.prompts()).isNotNull();
|
assertThat(prompts).isNotNull().satisfies(result -> {
|
||||||
|
assertThat(result.prompts()).isNotNull();
|
||||||
|
|
||||||
if (!result.prompts().isEmpty()) {
|
if (!result.prompts().isEmpty()) {
|
||||||
Prompt firstPrompt = result.prompts().get(0);
|
Prompt firstPrompt = result.prompts().get(0);
|
||||||
assertThat(firstPrompt.name()).isNotNull();
|
assertThat(firstPrompt.name()).isNotNull();
|
||||||
assertThat(firstPrompt.description()).isNotNull();
|
assertThat(firstPrompt.description()).isNotNull();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.verifyComplete();
|
.verifyComplete();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testGetPromptWithoutInitialization() {
|
void testGetPromptWithoutInitialization() {
|
||||||
GetPromptRequest request = new GetPromptRequest("simple_prompt", Map.of());
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
|
GetPromptRequest request = new GetPromptRequest("simple_prompt", Map.of());
|
||||||
|
|
||||||
StepVerifier.create(mcpAsyncClient.getPrompt(request))
|
StepVerifier.withVirtualTime(() -> mcpAsyncClient.getPrompt(request))
|
||||||
.expectErrorMatches(error -> error instanceof McpError
|
.expectSubscription()
|
||||||
&& error.getMessage().equals("Client must be initialized before getting prompts"))
|
.thenAwait(getInitializationTimeout())
|
||||||
.verify();
|
.consumeErrorWith(e -> assertThat(e).isInstanceOf(McpError.class)
|
||||||
|
.hasMessage("Client must be initialized before getting prompts"))
|
||||||
|
.verify();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testGetPrompt() {
|
void testGetPrompt() {
|
||||||
GetPromptRequest request = new GetPromptRequest("simple_prompt", Map.of());
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
|
StepVerifier
|
||||||
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.getPrompt(request)))
|
.create(mcpAsyncClient.initialize()
|
||||||
.consumeNextWith(prompt -> {
|
.then(mcpAsyncClient.getPrompt(new GetPromptRequest("simple_prompt", Map.of()))))
|
||||||
assertThat(prompt).isNotNull().satisfies(result -> {
|
.consumeNextWith(prompt -> {
|
||||||
assertThat(result.messages()).isNotEmpty();
|
assertThat(prompt).isNotNull().satisfies(result -> {
|
||||||
assertThat(result.messages()).hasSize(1);
|
assertThat(result.messages()).isNotEmpty();
|
||||||
});
|
assertThat(result.messages()).hasSize(1);
|
||||||
})
|
});
|
||||||
.verifyComplete();
|
})
|
||||||
|
.verifyComplete();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testRootsListChangedWithoutInitialization() {
|
void testRootsListChangedWithoutInitialization() {
|
||||||
StepVerifier.create(mcpAsyncClient.rootsListChangedNotification())
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
.expectErrorMatches(error -> error instanceof McpError && error.getMessage()
|
StepVerifier.withVirtualTime(() -> mcpAsyncClient.rootsListChangedNotification())
|
||||||
.equals("Client must be initialized before sending roots list changed notification"))
|
.expectSubscription()
|
||||||
.verify();
|
.thenAwait(getInitializationTimeout())
|
||||||
|
.consumeErrorWith(e -> assertThat(e).isInstanceOf(McpError.class)
|
||||||
|
.hasMessage("Client must be initialized before sending roots list changed notification"))
|
||||||
|
.verify();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testRootsListChanged() {
|
void testRootsListChanged() {
|
||||||
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.rootsListChangedNotification()))
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
.verifyComplete();
|
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.rootsListChangedNotification()))
|
||||||
|
.verifyComplete();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testInitializeWithRootsListProviders() {
|
void testInitializeWithRootsListProviders() {
|
||||||
var transport = createMcpTransport();
|
withClient(createMcpTransport(), builder -> builder.roots(new Root("file:///test/path", "test-root")),
|
||||||
|
client -> {
|
||||||
var client = McpClient.async(transport)
|
StepVerifier.create(client.initialize().then(client.closeGracefully())).verifyComplete();
|
||||||
.requestTimeout(getRequestTimeout())
|
});
|
||||||
.roots(new Root("file:///test/path", "test-root"))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
StepVerifier.create(client.initialize().then(client.closeGracefully())).verifyComplete();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testAddRoot() {
|
void testAddRoot() {
|
||||||
Root newRoot = new Root("file:///new/test/path", "new-test-root");
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
|
Root newRoot = new Root("file:///new/test/path", "new-test-root");
|
||||||
StepVerifier.create(mcpAsyncClient.addRoot(newRoot)).verifyComplete();
|
StepVerifier.create(mcpAsyncClient.addRoot(newRoot)).verifyComplete();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testAddRootWithNullValue() {
|
void testAddRootWithNullValue() {
|
||||||
StepVerifier.create(mcpAsyncClient.addRoot(null))
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
.expectErrorMatches(error -> error.getMessage().contains("Root must not be null"))
|
StepVerifier.create(mcpAsyncClient.addRoot(null))
|
||||||
.verify();
|
.consumeErrorWith(e -> assertThat(e).isInstanceOf(McpError.class).hasMessage("Root must not be null"))
|
||||||
|
.verify();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testRemoveRoot() {
|
void testRemoveRoot() {
|
||||||
Root root = new Root("file:///test/path/to/remove", "root-to-remove");
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
|
Root root = new Root("file:///test/path/to/remove", "root-to-remove");
|
||||||
|
StepVerifier.create(mcpAsyncClient.addRoot(root)).verifyComplete();
|
||||||
|
|
||||||
StepVerifier.create(mcpAsyncClient.addRoot(root).then(mcpAsyncClient.removeRoot(root.uri()))).verifyComplete();
|
StepVerifier.create(mcpAsyncClient.removeRoot(root.uri())).verifyComplete();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testRemoveNonExistentRoot() {
|
void testRemoveNonExistentRoot() {
|
||||||
StepVerifier.create(mcpAsyncClient.removeRoot("nonexistent-uri"))
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
.expectErrorMatches(error -> error.getMessage().contains("Root with uri 'nonexistent-uri' not found"))
|
StepVerifier.create(mcpAsyncClient.removeRoot("nonexistent-uri"))
|
||||||
.verify();
|
.consumeErrorWith(e -> assertThat(e).isInstanceOf(McpError.class)
|
||||||
|
.hasMessage("Root with uri 'nonexistent-uri' not found"))
|
||||||
|
.verify();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Disabled
|
@Disabled
|
||||||
void testReadResource() {
|
void testReadResource() {
|
||||||
StepVerifier.create(mcpAsyncClient.listResources()).consumeNextWith(resources -> {
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
if (!resources.resources().isEmpty()) {
|
StepVerifier.create(mcpAsyncClient.listResources()).consumeNextWith(resources -> {
|
||||||
Resource firstResource = resources.resources().get(0);
|
if (!resources.resources().isEmpty()) {
|
||||||
StepVerifier.create(mcpAsyncClient.readResource(firstResource)).consumeNextWith(result -> {
|
Resource firstResource = resources.resources().get(0);
|
||||||
assertThat(result).isNotNull();
|
StepVerifier.create(mcpAsyncClient.readResource(firstResource)).consumeNextWith(result -> {
|
||||||
assertThat(result.contents()).isNotNull();
|
assertThat(result).isNotNull();
|
||||||
}).verifyComplete();
|
assertThat(result.contents()).isNotNull();
|
||||||
}
|
}).verifyComplete();
|
||||||
}).verifyComplete();
|
}
|
||||||
|
}).verifyComplete();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testListResourceTemplatesWithoutInitialization() {
|
void testListResourceTemplatesWithoutInitialization() {
|
||||||
StepVerifier.create(mcpAsyncClient.listResourceTemplates())
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
.expectErrorMatches(error -> error instanceof McpError
|
StepVerifier.withVirtualTime(() -> mcpAsyncClient.listResourceTemplates())
|
||||||
&& error.getMessage().equals("Client must be initialized before listing resource templates"))
|
.expectSubscription()
|
||||||
.verify();
|
.thenAwait(getInitializationTimeout())
|
||||||
|
.consumeErrorWith(e -> assertThat(e).isInstanceOf(McpError.class)
|
||||||
|
.hasMessage("Client must be initialized before listing resource templates"))
|
||||||
|
.verify();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testListResourceTemplates() {
|
void testListResourceTemplates() {
|
||||||
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listResourceTemplates()))
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
.consumeNextWith(result -> {
|
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listResourceTemplates()))
|
||||||
assertThat(result).isNotNull();
|
.consumeNextWith(result -> {
|
||||||
assertThat(result.resourceTemplates()).isNotNull();
|
assertThat(result).isNotNull();
|
||||||
})
|
assertThat(result.resourceTemplates()).isNotNull();
|
||||||
.verifyComplete();
|
})
|
||||||
|
.verifyComplete();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Test
|
// @Test
|
||||||
void testResourceSubscription() {
|
void testResourceSubscription() {
|
||||||
StepVerifier.create(mcpAsyncClient.listResources()).consumeNextWith(resources -> {
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
if (!resources.resources().isEmpty()) {
|
StepVerifier.create(mcpAsyncClient.listResources()).consumeNextWith(resources -> {
|
||||||
Resource firstResource = resources.resources().get(0);
|
if (!resources.resources().isEmpty()) {
|
||||||
|
Resource firstResource = resources.resources().get(0);
|
||||||
|
|
||||||
// Test subscribe
|
// Test subscribe
|
||||||
StepVerifier.create(mcpAsyncClient.subscribeResource(new SubscribeRequest(firstResource.uri())))
|
StepVerifier.create(mcpAsyncClient.subscribeResource(new SubscribeRequest(firstResource.uri())))
|
||||||
.verifyComplete();
|
.verifyComplete();
|
||||||
|
|
||||||
// Test unsubscribe
|
// Test unsubscribe
|
||||||
StepVerifier.create(mcpAsyncClient.unsubscribeResource(new UnsubscribeRequest(firstResource.uri())))
|
StepVerifier.create(mcpAsyncClient.unsubscribeResource(new UnsubscribeRequest(firstResource.uri())))
|
||||||
.verifyComplete();
|
.verifyComplete();
|
||||||
}
|
}
|
||||||
}).verifyComplete();
|
}).verifyComplete();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -353,36 +439,44 @@ public abstract class AbstractMcpAsyncClientTests {
|
||||||
AtomicBoolean resourcesNotificationReceived = new AtomicBoolean(false);
|
AtomicBoolean resourcesNotificationReceived = new AtomicBoolean(false);
|
||||||
AtomicBoolean promptsNotificationReceived = new AtomicBoolean(false);
|
AtomicBoolean promptsNotificationReceived = new AtomicBoolean(false);
|
||||||
|
|
||||||
var transport = createMcpTransport();
|
withClient(createMcpTransport(),
|
||||||
var client = McpClient.async(transport)
|
builder -> builder
|
||||||
.requestTimeout(getRequestTimeout())
|
.toolsChangeConsumer(tools -> Mono.fromRunnable(() -> toolsNotificationReceived.set(true)))
|
||||||
.toolsChangeConsumer(tools -> Mono.fromRunnable(() -> toolsNotificationReceived.set(true)))
|
.resourcesChangeConsumer(
|
||||||
.resourcesChangeConsumer(resources -> Mono.fromRunnable(() -> resourcesNotificationReceived.set(true)))
|
resources -> Mono.fromRunnable(() -> resourcesNotificationReceived.set(true)))
|
||||||
.promptsChangeConsumer(prompts -> Mono.fromRunnable(() -> promptsNotificationReceived.set(true)))
|
.promptsChangeConsumer(prompts -> Mono.fromRunnable(() -> promptsNotificationReceived.set(true))),
|
||||||
.build();
|
mcpAsyncClient -> {
|
||||||
|
|
||||||
StepVerifier.create(client.initialize().then(client.closeGracefully())).verifyComplete();
|
var transport = createMcpTransport();
|
||||||
|
var client = McpClient.async(transport)
|
||||||
|
.requestTimeout(getRequestTimeout())
|
||||||
|
.toolsChangeConsumer(tools -> Mono.fromRunnable(() -> toolsNotificationReceived.set(true)))
|
||||||
|
.resourcesChangeConsumer(
|
||||||
|
resources -> Mono.fromRunnable(() -> resourcesNotificationReceived.set(true)))
|
||||||
|
.promptsChangeConsumer(
|
||||||
|
prompts -> Mono.fromRunnable(() -> promptsNotificationReceived.set(true)))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
StepVerifier.create(client.initialize()).expectNextMatches(Objects::nonNull).verifyComplete();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testInitializeWithSamplingCapability() {
|
void testInitializeWithSamplingCapability() {
|
||||||
var transport = createMcpTransport();
|
ClientCapabilities capabilities = ClientCapabilities.builder().sampling().build();
|
||||||
|
CreateMessageResult createMessageResult = CreateMessageResult.builder()
|
||||||
var capabilities = ClientCapabilities.builder().sampling().build();
|
.message("test")
|
||||||
|
.model("test-model")
|
||||||
var client = McpClient.async(transport)
|
|
||||||
.requestTimeout(getRequestTimeout())
|
|
||||||
.capabilities(capabilities)
|
|
||||||
.sampling(request -> Mono.just(CreateMessageResult.builder().message("test").model("test-model").build()))
|
|
||||||
.build();
|
.build();
|
||||||
|
withClient(createMcpTransport(),
|
||||||
StepVerifier.create(client.initialize().then(client.closeGracefully())).verifyComplete();
|
builder -> builder.capabilities(capabilities).sampling(request -> Mono.just(createMessageResult)),
|
||||||
|
client -> {
|
||||||
|
StepVerifier.create(client.initialize()).expectNextMatches(Objects::nonNull).verifyComplete();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testInitializeWithAllCapabilities() {
|
void testInitializeWithAllCapabilities() {
|
||||||
var transport = createMcpTransport();
|
|
||||||
|
|
||||||
var capabilities = ClientCapabilities.builder()
|
var capabilities = ClientCapabilities.builder()
|
||||||
.experimental(Map.of("feature", "test"))
|
.experimental(Map.of("feature", "test"))
|
||||||
.roots(true)
|
.roots(true)
|
||||||
|
|
@ -391,18 +485,14 @@ public abstract class AbstractMcpAsyncClientTests {
|
||||||
|
|
||||||
Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler = request -> Mono
|
Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler = request -> Mono
|
||||||
.just(CreateMessageResult.builder().message("test").model("test-model").build());
|
.just(CreateMessageResult.builder().message("test").model("test-model").build());
|
||||||
var client = McpClient.async(transport)
|
|
||||||
.requestTimeout(getRequestTimeout())
|
|
||||||
.capabilities(capabilities)
|
|
||||||
.sampling(samplingHandler)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
StepVerifier.create(client.initialize()).consumeNextWith(result -> {
|
withClient(createMcpTransport(), builder -> builder.capabilities(capabilities).sampling(samplingHandler),
|
||||||
assertThat(result).isNotNull();
|
client ->
|
||||||
assertThat(result.capabilities()).isNotNull();
|
|
||||||
}).verifyComplete();
|
|
||||||
|
|
||||||
StepVerifier.create(client.closeGracefully()).verifyComplete();
|
StepVerifier.create(client.initialize()).assertNext(result -> {
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.capabilities()).isNotNull();
|
||||||
|
}).verifyComplete());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------
|
// ---------------------------------------
|
||||||
|
|
@ -411,43 +501,52 @@ public abstract class AbstractMcpAsyncClientTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testLoggingLevelsWithoutInitialization() {
|
void testLoggingLevelsWithoutInitialization() {
|
||||||
StepVerifier.create(mcpAsyncClient.setLoggingLevel(McpSchema.LoggingLevel.DEBUG))
|
withClient(createMcpTransport(),
|
||||||
.expectErrorMatches(error -> error instanceof McpError
|
mcpAsyncClient -> StepVerifier
|
||||||
&& error.getMessage().equals("Client must be initialized before setting logging level"))
|
.withVirtualTime(() -> mcpAsyncClient.setLoggingLevel(McpSchema.LoggingLevel.DEBUG))
|
||||||
.verify();
|
.expectSubscription()
|
||||||
|
.thenAwait(getInitializationTimeout())
|
||||||
|
.consumeErrorWith(e -> assertThat(e).isInstanceOf(McpError.class)
|
||||||
|
.hasMessage("Client must be initialized before setting logging level"))
|
||||||
|
.verify());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testLoggingLevels() {
|
void testLoggingLevels() {
|
||||||
Mono<Void> testAllLevels = mcpAsyncClient.initialize().then(Mono.defer(() -> {
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
Mono<Void> chain = Mono.empty();
|
Mono<Void> testAllLevels = mcpAsyncClient.initialize().then(Mono.defer(() -> {
|
||||||
for (McpSchema.LoggingLevel level : McpSchema.LoggingLevel.values()) {
|
Mono<Void> chain = Mono.empty();
|
||||||
chain = chain.then(mcpAsyncClient.setLoggingLevel(level));
|
for (McpSchema.LoggingLevel level : McpSchema.LoggingLevel.values()) {
|
||||||
}
|
chain = chain.then(mcpAsyncClient.setLoggingLevel(level));
|
||||||
return chain;
|
}
|
||||||
}));
|
return chain;
|
||||||
|
}));
|
||||||
|
|
||||||
StepVerifier.create(testAllLevels).verifyComplete();
|
StepVerifier.create(testAllLevels).verifyComplete();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testLoggingConsumer() {
|
void testLoggingConsumer() {
|
||||||
AtomicBoolean logReceived = new AtomicBoolean(false);
|
AtomicBoolean logReceived = new AtomicBoolean(false);
|
||||||
var transport = createMcpTransport();
|
|
||||||
|
|
||||||
var client = McpClient.async(transport)
|
withClient(createMcpTransport(),
|
||||||
.requestTimeout(getRequestTimeout())
|
builder -> builder.loggingConsumer(notification -> Mono.fromRunnable(() -> logReceived.set(true))),
|
||||||
.loggingConsumer(notification -> Mono.fromRunnable(() -> logReceived.set(true)))
|
client -> {
|
||||||
.build();
|
StepVerifier.create(client.initialize()).expectNextMatches(Objects::nonNull).verifyComplete();
|
||||||
|
StepVerifier.create(client.closeGracefully()).verifyComplete();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
StepVerifier.create(client.initialize().then(client.closeGracefully())).verifyComplete();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testLoggingWithNullNotification() {
|
void testLoggingWithNullNotification() {
|
||||||
StepVerifier.create(mcpAsyncClient.setLoggingLevel(null))
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
.expectErrorMatches(error -> error.getMessage().contains("Logging level must not be null"))
|
StepVerifier.create(mcpAsyncClient.setLoggingLevel(null))
|
||||||
.verify();
|
.expectErrorMatches(error -> error.getMessage().contains("Logging level must not be null"))
|
||||||
|
.verify();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,9 @@ package io.modelcontextprotocol.client;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
import io.modelcontextprotocol.spec.ClientMcpTransport;
|
import io.modelcontextprotocol.spec.ClientMcpTransport;
|
||||||
import io.modelcontextprotocol.spec.McpError;
|
import io.modelcontextprotocol.spec.McpError;
|
||||||
|
|
@ -27,6 +30,10 @@ import io.modelcontextprotocol.spec.McpSchema.UnsubscribeRequest;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.core.scheduler.Scheduler;
|
||||||
|
import reactor.core.scheduler.Schedulers;
|
||||||
|
import reactor.test.StepVerifier;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||||
|
|
@ -40,12 +47,8 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
*/
|
*/
|
||||||
public abstract class AbstractMcpSyncClientTests {
|
public abstract class AbstractMcpSyncClientTests {
|
||||||
|
|
||||||
private McpSyncClient mcpSyncClient;
|
|
||||||
|
|
||||||
private static final String TEST_MESSAGE = "Hello MCP Spring AI!";
|
private static final String TEST_MESSAGE = "Hello MCP Spring AI!";
|
||||||
|
|
||||||
protected ClientMcpTransport mcpTransport;
|
|
||||||
|
|
||||||
abstract protected ClientMcpTransport createMcpTransport();
|
abstract protected ClientMcpTransport createMcpTransport();
|
||||||
|
|
||||||
protected void onStart() {
|
protected void onStart() {
|
||||||
|
|
@ -62,254 +65,322 @@ public abstract class AbstractMcpSyncClientTests {
|
||||||
return Duration.ofSeconds(2);
|
return Duration.ofSeconds(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
McpSyncClient client(ClientMcpTransport transport) {
|
||||||
|
return client(transport, Function.identity());
|
||||||
|
}
|
||||||
|
|
||||||
|
McpSyncClient client(ClientMcpTransport 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(ClientMcpTransport transport, Consumer<McpSyncClient> c) {
|
||||||
|
withClient(transport, Function.identity(), c);
|
||||||
|
}
|
||||||
|
|
||||||
|
void withClient(ClientMcpTransport transport, Function<McpClient.SyncSpec, McpClient.SyncSpec> customizer,
|
||||||
|
Consumer<McpSyncClient> c) {
|
||||||
|
var client = client(transport, customizer);
|
||||||
|
try {
|
||||||
|
c.accept(client);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
assertThat(client.closeGracefully()).isTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
onStart();
|
onStart();
|
||||||
this.mcpTransport = createMcpTransport();
|
|
||||||
|
|
||||||
assertThatCode(() -> {
|
|
||||||
mcpSyncClient = McpClient.sync(mcpTransport)
|
|
||||||
.requestTimeout(getRequestTimeout())
|
|
||||||
.initializationTimeout(getInitializationTimeout())
|
|
||||||
.capabilities(ClientCapabilities.builder().roots(true).build())
|
|
||||||
.build();
|
|
||||||
}).doesNotThrowAnyException();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
void tearDown() {
|
void tearDown() {
|
||||||
if (mcpSyncClient != null) {
|
|
||||||
assertThatCode(() -> mcpSyncClient.close()).doesNotThrowAnyException();
|
|
||||||
}
|
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static final Object DUMMY_RETURN_VALUE = new Object();
|
||||||
|
|
||||||
|
<T> void verifyNotificationTimesOut(Consumer<McpSyncClient> operation, String action) {
|
||||||
|
verifyCallTimesOut(client -> {
|
||||||
|
operation.accept(client);
|
||||||
|
return DUMMY_RETURN_VALUE;
|
||||||
|
}, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
<T> void verifyCallTimesOut(Function<McpSyncClient, T> operation, String action) {
|
||||||
|
withClient(createMcpTransport(), mcpSyncClient -> {
|
||||||
|
// This scheduler is not replaced by virtual time scheduler
|
||||||
|
Scheduler customScheduler = Schedulers.newBoundedElastic(1, 1, "actualBoundedElastic");
|
||||||
|
|
||||||
|
StepVerifier.withVirtualTime(() -> Mono.fromSupplier(() -> operation.apply(mcpSyncClient))
|
||||||
|
// offload the blocking call to the real scheduler
|
||||||
|
.subscribeOn(customScheduler))
|
||||||
|
.expectSubscription()
|
||||||
|
.thenAwait(getInitializationTimeout())
|
||||||
|
.consumeErrorWith(e -> assertThat(e).isInstanceOf(McpError.class)
|
||||||
|
.hasMessage("Client must be initialized before " + action))
|
||||||
|
.verify();
|
||||||
|
|
||||||
|
customScheduler.dispose();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testConstructorWithInvalidArguments() {
|
void testConstructorWithInvalidArguments() {
|
||||||
assertThatThrownBy(() -> McpClient.sync(null).build()).isInstanceOf(IllegalArgumentException.class)
|
assertThatThrownBy(() -> McpClient.sync(null).build()).isInstanceOf(IllegalArgumentException.class)
|
||||||
.hasMessage("Transport must not be null");
|
.hasMessage("Transport must not be null");
|
||||||
|
|
||||||
assertThatThrownBy(() -> McpClient.sync(mcpTransport).requestTimeout(null).build())
|
assertThatThrownBy(() -> McpClient.sync(createMcpTransport()).requestTimeout(null).build())
|
||||||
.isInstanceOf(IllegalArgumentException.class)
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
.hasMessage("Request timeout must not be null");
|
.hasMessage("Request timeout must not be null");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testListToolsWithoutInitialization() {
|
void testListToolsWithoutInitialization() {
|
||||||
assertThatThrownBy(() -> mcpSyncClient.listTools(null)).isInstanceOf(McpError.class)
|
verifyCallTimesOut(client -> client.listTools(null), "listing tools");
|
||||||
.hasMessage("Client must be initialized before listing tools");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testListTools() {
|
void testListTools() {
|
||||||
mcpSyncClient.initialize();
|
withClient(createMcpTransport(), mcpSyncClient -> {
|
||||||
ListToolsResult tools = mcpSyncClient.listTools(null);
|
mcpSyncClient.initialize();
|
||||||
|
ListToolsResult tools = mcpSyncClient.listTools(null);
|
||||||
|
|
||||||
assertThat(tools).isNotNull().satisfies(result -> {
|
assertThat(tools).isNotNull().satisfies(result -> {
|
||||||
assertThat(result.tools()).isNotNull().isNotEmpty();
|
assertThat(result.tools()).isNotNull().isNotEmpty();
|
||||||
|
|
||||||
Tool firstTool = result.tools().get(0);
|
Tool firstTool = result.tools().get(0);
|
||||||
assertThat(firstTool.name()).isNotNull();
|
assertThat(firstTool.name()).isNotNull();
|
||||||
assertThat(firstTool.description()).isNotNull();
|
assertThat(firstTool.description()).isNotNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testCallToolsWithoutInitialization() {
|
void testCallToolsWithoutInitialization() {
|
||||||
assertThatThrownBy(() -> mcpSyncClient.callTool(new CallToolRequest("add", Map.of("a", 3, "b", 4))))
|
verifyCallTimesOut(client -> client.callTool(new CallToolRequest("add", Map.of("a", 3, "b", 4))),
|
||||||
.isInstanceOf(McpError.class)
|
"calling tools");
|
||||||
.hasMessage("Client must be initialized before calling tools");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testCallTools() {
|
void testCallTools() {
|
||||||
mcpSyncClient.initialize();
|
withClient(createMcpTransport(), mcpSyncClient -> {
|
||||||
CallToolResult toolResult = mcpSyncClient.callTool(new CallToolRequest("add", Map.of("a", 3, "b", 4)));
|
mcpSyncClient.initialize();
|
||||||
|
CallToolResult toolResult = mcpSyncClient.callTool(new CallToolRequest("add", Map.of("a", 3, "b", 4)));
|
||||||
|
|
||||||
assertThat(toolResult).isNotNull().satisfies(result -> {
|
assertThat(toolResult).isNotNull().satisfies(result -> {
|
||||||
|
|
||||||
assertThat(result.content()).hasSize(1);
|
assertThat(result.content()).hasSize(1);
|
||||||
|
|
||||||
TextContent content = (TextContent) result.content().get(0);
|
TextContent content = (TextContent) result.content().get(0);
|
||||||
|
|
||||||
assertThat(content).isNotNull();
|
assertThat(content).isNotNull();
|
||||||
assertThat(content.text()).isNotNull();
|
assertThat(content.text()).isNotNull();
|
||||||
assertThat(content.text()).contains("7");
|
assertThat(content.text()).contains("7");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testPingWithoutInitialization() {
|
void testPingWithoutInitialization() {
|
||||||
assertThatThrownBy(() -> mcpSyncClient.ping()).isInstanceOf(McpError.class)
|
verifyCallTimesOut(client -> client.ping(), "pinging the server");
|
||||||
.hasMessage("Client must be initialized before pinging the server");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testPing() {
|
void testPing() {
|
||||||
mcpSyncClient.initialize();
|
withClient(createMcpTransport(), mcpSyncClient -> {
|
||||||
assertThatCode(() -> mcpSyncClient.ping()).doesNotThrowAnyException();
|
mcpSyncClient.initialize();
|
||||||
|
assertThatCode(() -> mcpSyncClient.ping()).doesNotThrowAnyException();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testCallToolWithoutInitialization() {
|
void testCallToolWithoutInitialization() {
|
||||||
CallToolRequest callToolRequest = new CallToolRequest("echo", Map.of("message", TEST_MESSAGE));
|
CallToolRequest callToolRequest = new CallToolRequest("echo", Map.of("message", TEST_MESSAGE));
|
||||||
|
verifyCallTimesOut(client -> client.callTool(callToolRequest), "calling tools");
|
||||||
assertThatThrownBy(() -> mcpSyncClient.callTool(callToolRequest)).isInstanceOf(McpError.class)
|
|
||||||
.hasMessage("Client must be initialized before calling tools");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testCallTool() {
|
void testCallTool() {
|
||||||
mcpSyncClient.initialize();
|
withClient(createMcpTransport(), mcpSyncClient -> {
|
||||||
CallToolRequest callToolRequest = new CallToolRequest("echo", Map.of("message", TEST_MESSAGE));
|
mcpSyncClient.initialize();
|
||||||
|
CallToolRequest callToolRequest = new CallToolRequest("echo", Map.of("message", TEST_MESSAGE));
|
||||||
|
|
||||||
CallToolResult callToolResult = mcpSyncClient.callTool(callToolRequest);
|
CallToolResult callToolResult = mcpSyncClient.callTool(callToolRequest);
|
||||||
|
|
||||||
assertThat(callToolResult).isNotNull().satisfies(result -> {
|
assertThat(callToolResult).isNotNull().satisfies(result -> {
|
||||||
assertThat(result.content()).isNotNull();
|
assertThat(result.content()).isNotNull();
|
||||||
assertThat(result.isError()).isNull();
|
assertThat(result.isError()).isNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testCallToolWithInvalidTool() {
|
void testCallToolWithInvalidTool() {
|
||||||
CallToolRequest invalidRequest = new CallToolRequest("nonexistent_tool", Map.of("message", TEST_MESSAGE));
|
withClient(createMcpTransport(), mcpSyncClient -> {
|
||||||
|
CallToolRequest invalidRequest = new CallToolRequest("nonexistent_tool", Map.of("message", TEST_MESSAGE));
|
||||||
|
|
||||||
assertThatThrownBy(() -> mcpSyncClient.callTool(invalidRequest)).isInstanceOf(Exception.class);
|
assertThatThrownBy(() -> mcpSyncClient.callTool(invalidRequest)).isInstanceOf(Exception.class);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testRootsListChangedWithoutInitialization() {
|
void testRootsListChangedWithoutInitialization() {
|
||||||
assertThatThrownBy(() -> mcpSyncClient.rootsListChangedNotification()).isInstanceOf(McpError.class)
|
verifyNotificationTimesOut(client -> client.rootsListChangedNotification(),
|
||||||
.hasMessage("Client must be initialized before sending roots list changed notification");
|
"sending roots list changed notification");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testRootsListChanged() {
|
void testRootsListChanged() {
|
||||||
mcpSyncClient.initialize();
|
withClient(createMcpTransport(), mcpSyncClient -> {
|
||||||
assertThatCode(() -> mcpSyncClient.rootsListChangedNotification()).doesNotThrowAnyException();
|
mcpSyncClient.initialize();
|
||||||
|
assertThatCode(() -> mcpSyncClient.rootsListChangedNotification()).doesNotThrowAnyException();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testListResourcesWithoutInitialization() {
|
void testListResourcesWithoutInitialization() {
|
||||||
assertThatThrownBy(() -> mcpSyncClient.listResources(null)).isInstanceOf(McpError.class)
|
verifyCallTimesOut(client -> client.listResources(null), "listing resources");
|
||||||
.hasMessage("Client must be initialized before listing resources");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testListResources() {
|
void testListResources() {
|
||||||
mcpSyncClient.initialize();
|
withClient(createMcpTransport(), mcpSyncClient -> {
|
||||||
ListResourcesResult resources = mcpSyncClient.listResources(null);
|
mcpSyncClient.initialize();
|
||||||
|
ListResourcesResult resources = mcpSyncClient.listResources(null);
|
||||||
|
|
||||||
assertThat(resources).isNotNull().satisfies(result -> {
|
assertThat(resources).isNotNull().satisfies(result -> {
|
||||||
assertThat(result.resources()).isNotNull();
|
assertThat(result.resources()).isNotNull();
|
||||||
|
|
||||||
if (!result.resources().isEmpty()) {
|
if (!result.resources().isEmpty()) {
|
||||||
Resource firstResource = result.resources().get(0);
|
Resource firstResource = result.resources().get(0);
|
||||||
assertThat(firstResource.uri()).isNotNull();
|
assertThat(firstResource.uri()).isNotNull();
|
||||||
assertThat(firstResource.name()).isNotNull();
|
assertThat(firstResource.name()).isNotNull();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testClientSessionState() {
|
void testClientSessionState() {
|
||||||
assertThat(mcpSyncClient).isNotNull();
|
withClient(createMcpTransport(), mcpSyncClient -> {
|
||||||
|
assertThat(mcpSyncClient).isNotNull();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testInitializeWithRootsListProviders() {
|
void testInitializeWithRootsListProviders() {
|
||||||
var transport = createMcpTransport();
|
withClient(createMcpTransport(), builder -> builder.roots(new Root("file:///test/path", "test-root")),
|
||||||
|
mcpSyncClient -> {
|
||||||
|
|
||||||
var client = McpClient.sync(transport)
|
assertThatCode(() -> {
|
||||||
.requestTimeout(getRequestTimeout())
|
mcpSyncClient.initialize();
|
||||||
.roots(new Root("file:///test/path", "test-root"))
|
mcpSyncClient.close();
|
||||||
.build();
|
}).doesNotThrowAnyException();
|
||||||
|
});
|
||||||
assertThatCode(() -> {
|
|
||||||
client.initialize();
|
|
||||||
client.close();
|
|
||||||
}).doesNotThrowAnyException();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testAddRoot() {
|
void testAddRoot() {
|
||||||
Root newRoot = new Root("file:///new/test/path", "new-test-root");
|
withClient(createMcpTransport(), mcpSyncClient -> {
|
||||||
assertThatCode(() -> mcpSyncClient.addRoot(newRoot)).doesNotThrowAnyException();
|
Root newRoot = new Root("file:///new/test/path", "new-test-root");
|
||||||
|
assertThatCode(() -> mcpSyncClient.addRoot(newRoot)).doesNotThrowAnyException();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testAddRootWithNullValue() {
|
void testAddRootWithNullValue() {
|
||||||
assertThatThrownBy(() -> mcpSyncClient.addRoot(null)).hasMessageContaining("Root must not be null");
|
withClient(createMcpTransport(), mcpSyncClient -> {
|
||||||
|
assertThatThrownBy(() -> mcpSyncClient.addRoot(null)).hasMessageContaining("Root must not be null");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testRemoveRoot() {
|
void testRemoveRoot() {
|
||||||
Root root = new Root("file:///test/path/to/remove", "root-to-remove");
|
withClient(createMcpTransport(), mcpSyncClient -> {
|
||||||
assertThatCode(() -> {
|
Root root = new Root("file:///test/path/to/remove", "root-to-remove");
|
||||||
mcpSyncClient.addRoot(root);
|
assertThatCode(() -> {
|
||||||
mcpSyncClient.removeRoot(root.uri());
|
mcpSyncClient.addRoot(root);
|
||||||
}).doesNotThrowAnyException();
|
mcpSyncClient.removeRoot(root.uri());
|
||||||
|
}).doesNotThrowAnyException();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testRemoveNonExistentRoot() {
|
void testRemoveNonExistentRoot() {
|
||||||
assertThatThrownBy(() -> mcpSyncClient.removeRoot("nonexistent-uri"))
|
withClient(createMcpTransport(), mcpSyncClient -> {
|
||||||
.hasMessageContaining("Root with uri 'nonexistent-uri' not found");
|
assertThatThrownBy(() -> mcpSyncClient.removeRoot("nonexistent-uri"))
|
||||||
|
.hasMessageContaining("Root with uri 'nonexistent-uri' not found");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testReadResourceWithoutInitialization() {
|
void testReadResourceWithoutInitialization() {
|
||||||
assertThatThrownBy(() -> {
|
Resource resource = new Resource("test://uri", "Test Resource", null, null, null);
|
||||||
Resource resource = new Resource("test://uri", "Test Resource", null, null, null);
|
verifyCallTimesOut(client -> client.readResource(resource), "reading resources");
|
||||||
mcpSyncClient.readResource(resource);
|
|
||||||
}).isInstanceOf(McpError.class).hasMessage("Client must be initialized before reading resources");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testReadResource() {
|
void testReadResource() {
|
||||||
mcpSyncClient.initialize();
|
withClient(createMcpTransport(), mcpSyncClient -> {
|
||||||
ListResourcesResult resources = mcpSyncClient.listResources(null);
|
mcpSyncClient.initialize();
|
||||||
|
ListResourcesResult resources = mcpSyncClient.listResources(null);
|
||||||
|
|
||||||
if (!resources.resources().isEmpty()) {
|
if (!resources.resources().isEmpty()) {
|
||||||
Resource firstResource = resources.resources().get(0);
|
Resource firstResource = resources.resources().get(0);
|
||||||
ReadResourceResult result = mcpSyncClient.readResource(firstResource);
|
ReadResourceResult result = mcpSyncClient.readResource(firstResource);
|
||||||
|
|
||||||
assertThat(result).isNotNull();
|
assertThat(result).isNotNull();
|
||||||
assertThat(result.contents()).isNotNull();
|
assertThat(result.contents()).isNotNull();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testListResourceTemplatesWithoutInitialization() {
|
void testListResourceTemplatesWithoutInitialization() {
|
||||||
assertThatThrownBy(() -> mcpSyncClient.listResourceTemplates(null)).isInstanceOf(McpError.class)
|
verifyCallTimesOut(client -> client.listResourceTemplates(null), "listing resource templates");
|
||||||
.hasMessage("Client must be initialized before listing resource templates");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testListResourceTemplates() {
|
void testListResourceTemplates() {
|
||||||
mcpSyncClient.initialize();
|
withClient(createMcpTransport(), mcpSyncClient -> {
|
||||||
ListResourceTemplatesResult result = mcpSyncClient.listResourceTemplates(null);
|
mcpSyncClient.initialize();
|
||||||
|
ListResourceTemplatesResult result = mcpSyncClient.listResourceTemplates(null);
|
||||||
|
|
||||||
assertThat(result).isNotNull();
|
assertThat(result).isNotNull();
|
||||||
assertThat(result.resourceTemplates()).isNotNull();
|
assertThat(result.resourceTemplates()).isNotNull();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Test
|
// @Test
|
||||||
void testResourceSubscription() {
|
void testResourceSubscription() {
|
||||||
ListResourcesResult resources = mcpSyncClient.listResources(null);
|
withClient(createMcpTransport(), mcpSyncClient -> {
|
||||||
|
ListResourcesResult resources = mcpSyncClient.listResources(null);
|
||||||
|
|
||||||
if (!resources.resources().isEmpty()) {
|
if (!resources.resources().isEmpty()) {
|
||||||
Resource firstResource = resources.resources().get(0);
|
Resource firstResource = resources.resources().get(0);
|
||||||
|
|
||||||
// Test subscribe
|
// Test subscribe
|
||||||
assertThatCode(() -> mcpSyncClient.subscribeResource(new SubscribeRequest(firstResource.uri())))
|
assertThatCode(() -> mcpSyncClient.subscribeResource(new SubscribeRequest(firstResource.uri())))
|
||||||
.doesNotThrowAnyException();
|
.doesNotThrowAnyException();
|
||||||
|
|
||||||
// Test unsubscribe
|
// Test unsubscribe
|
||||||
assertThatCode(() -> mcpSyncClient.unsubscribeResource(new UnsubscribeRequest(firstResource.uri())))
|
assertThatCode(() -> mcpSyncClient.unsubscribeResource(new UnsubscribeRequest(firstResource.uri())))
|
||||||
.doesNotThrowAnyException();
|
.doesNotThrowAnyException();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -318,18 +389,17 @@ public abstract class AbstractMcpSyncClientTests {
|
||||||
AtomicBoolean resourcesNotificationReceived = new AtomicBoolean(false);
|
AtomicBoolean resourcesNotificationReceived = new AtomicBoolean(false);
|
||||||
AtomicBoolean promptsNotificationReceived = new AtomicBoolean(false);
|
AtomicBoolean promptsNotificationReceived = new AtomicBoolean(false);
|
||||||
|
|
||||||
var transport = createMcpTransport();
|
withClient(createMcpTransport(),
|
||||||
var client = McpClient.sync(transport)
|
builder -> builder.toolsChangeConsumer(tools -> toolsNotificationReceived.set(true))
|
||||||
.requestTimeout(getRequestTimeout())
|
.resourcesChangeConsumer(resources -> resourcesNotificationReceived.set(true))
|
||||||
.toolsChangeConsumer(tools -> toolsNotificationReceived.set(true))
|
.promptsChangeConsumer(prompts -> promptsNotificationReceived.set(true)),
|
||||||
.resourcesChangeConsumer(resources -> resourcesNotificationReceived.set(true))
|
client -> {
|
||||||
.promptsChangeConsumer(prompts -> promptsNotificationReceived.set(true))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
assertThatCode(() -> {
|
assertThatCode(() -> {
|
||||||
client.initialize();
|
client.initialize();
|
||||||
client.close();
|
client.close();
|
||||||
}).doesNotThrowAnyException();
|
}).doesNotThrowAnyException();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------
|
// ---------------------------------------
|
||||||
|
|
@ -338,40 +408,37 @@ public abstract class AbstractMcpSyncClientTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testLoggingLevelsWithoutInitialization() {
|
void testLoggingLevelsWithoutInitialization() {
|
||||||
assertThatThrownBy(() -> mcpSyncClient.setLoggingLevel(McpSchema.LoggingLevel.DEBUG))
|
verifyNotificationTimesOut(client -> client.setLoggingLevel(McpSchema.LoggingLevel.DEBUG),
|
||||||
.isInstanceOf(McpError.class)
|
"setting logging level");
|
||||||
.hasMessage("Client must be initialized before setting logging level");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testLoggingLevels() {
|
void testLoggingLevels() {
|
||||||
mcpSyncClient.initialize();
|
withClient(createMcpTransport(), mcpSyncClient -> {
|
||||||
// Test all logging levels
|
mcpSyncClient.initialize();
|
||||||
for (McpSchema.LoggingLevel level : McpSchema.LoggingLevel.values()) {
|
// Test all logging levels
|
||||||
assertThatCode(() -> mcpSyncClient.setLoggingLevel(level)).doesNotThrowAnyException();
|
for (McpSchema.LoggingLevel level : McpSchema.LoggingLevel.values()) {
|
||||||
}
|
assertThatCode(() -> mcpSyncClient.setLoggingLevel(level)).doesNotThrowAnyException();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testLoggingConsumer() {
|
void testLoggingConsumer() {
|
||||||
AtomicBoolean logReceived = new AtomicBoolean(false);
|
AtomicBoolean logReceived = new AtomicBoolean(false);
|
||||||
var transport = createMcpTransport();
|
withClient(createMcpTransport(), builder -> builder.requestTimeout(getRequestTimeout())
|
||||||
|
.loggingConsumer(notification -> logReceived.set(true)), client -> {
|
||||||
var client = McpClient.sync(transport)
|
assertThatCode(() -> {
|
||||||
.requestTimeout(getRequestTimeout())
|
client.initialize();
|
||||||
.loggingConsumer(notification -> logReceived.set(true))
|
client.close();
|
||||||
.build();
|
}).doesNotThrowAnyException();
|
||||||
|
});
|
||||||
assertThatCode(() -> {
|
|
||||||
client.initialize();
|
|
||||||
client.close();
|
|
||||||
}).doesNotThrowAnyException();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testLoggingWithNullNotification() {
|
void testLoggingWithNullNotification() {
|
||||||
assertThatThrownBy(() -> mcpSyncClient.setLoggingLevel(null))
|
withClient(createMcpTransport(), mcpSyncClient -> assertThatThrownBy(() -> mcpSyncClient.setLoggingLevel(null))
|
||||||
.hasMessageContaining("Logging level must not be null");
|
.hasMessageContaining("Logging level must not be null"));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -353,14 +353,15 @@ public class StdioClientTransport implements ClientMcpTransport {
|
||||||
|
|
||||||
// Give a short time for any pending messages to be processed
|
// Give a short time for any pending messages to be processed
|
||||||
return Mono.delay(Duration.ofMillis(100));
|
return Mono.delay(Duration.ofMillis(100));
|
||||||
})).then(Mono.fromFuture(() -> {
|
})).then(Mono.defer(() -> {
|
||||||
logger.debug("Sending TERM to process");
|
logger.debug("Sending TERM to process");
|
||||||
if (this.process != null) {
|
if (this.process != null) {
|
||||||
this.process.destroy();
|
this.process.destroy();
|
||||||
return process.onExit();
|
return Mono.fromFuture(process.onExit());
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return CompletableFuture.failedFuture(new RuntimeException("Process not started"));
|
logger.warn("Process not started");
|
||||||
|
return Mono.empty();
|
||||||
}
|
}
|
||||||
})).doOnNext(process -> {
|
})).doOnNext(process -> {
|
||||||
if (process.exitValue() != 0) {
|
if (process.exitValue() != 0) {
|
||||||
|
|
|
||||||
|
|
@ -270,8 +270,10 @@ public class DefaultMcpSession implements McpSession {
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public Mono<Void> closeGracefully() {
|
public Mono<Void> closeGracefully() {
|
||||||
this.connection.dispose();
|
return Mono.defer(() -> {
|
||||||
return transport.closeGracefully();
|
this.connection.dispose();
|
||||||
|
return transport.closeGracefully();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,12 @@ package io.modelcontextprotocol.client;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import io.modelcontextprotocol.spec.ClientMcpTransport;
|
import io.modelcontextprotocol.spec.ClientMcpTransport;
|
||||||
import io.modelcontextprotocol.spec.McpError;
|
import io.modelcontextprotocol.spec.McpError;
|
||||||
|
|
@ -45,10 +49,6 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
// KEEP IN SYNC with the class in mcp-test module
|
// KEEP IN SYNC with the class in mcp-test module
|
||||||
public abstract class AbstractMcpAsyncClientTests {
|
public abstract class AbstractMcpAsyncClientTests {
|
||||||
|
|
||||||
private McpAsyncClient mcpAsyncClient;
|
|
||||||
|
|
||||||
protected ClientMcpTransport mcpTransport;
|
|
||||||
|
|
||||||
private static final String ECHO_TEST_MESSAGE = "Hello MCP Spring AI!";
|
private static final String ECHO_TEST_MESSAGE = "Hello MCP Spring AI!";
|
||||||
|
|
||||||
abstract protected ClientMcpTransport createMcpTransport();
|
abstract protected ClientMcpTransport createMcpTransport();
|
||||||
|
|
@ -67,285 +67,326 @@ public abstract class AbstractMcpAsyncClientTests {
|
||||||
return Duration.ofSeconds(2);
|
return Duration.ofSeconds(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
McpAsyncClient client(ClientMcpTransport transport) {
|
||||||
|
return client(transport, Function.identity());
|
||||||
|
}
|
||||||
|
|
||||||
|
McpAsyncClient client(ClientMcpTransport transport, Function<McpClient.AsyncSpec, McpClient.AsyncSpec> customizer) {
|
||||||
|
AtomicReference<McpAsyncClient> client = new AtomicReference<>();
|
||||||
|
|
||||||
|
assertThatCode(() -> {
|
||||||
|
McpClient.AsyncSpec builder = McpClient.async(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(ClientMcpTransport transport, Consumer<McpAsyncClient> c) {
|
||||||
|
withClient(transport, Function.identity(), c);
|
||||||
|
}
|
||||||
|
|
||||||
|
void withClient(ClientMcpTransport 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
onStart();
|
onStart();
|
||||||
this.mcpTransport = createMcpTransport();
|
|
||||||
|
|
||||||
assertThatCode(() -> {
|
|
||||||
mcpAsyncClient = McpClient.async(mcpTransport)
|
|
||||||
.requestTimeout(getRequestTimeout())
|
|
||||||
.initializationTimeout(getInitializationTimeout())
|
|
||||||
.capabilities(ClientCapabilities.builder().roots(true).build())
|
|
||||||
.build();
|
|
||||||
}).doesNotThrowAnyException();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
void tearDown() {
|
void tearDown() {
|
||||||
if (mcpAsyncClient != null) {
|
|
||||||
StepVerifier.create(mcpAsyncClient.closeGracefully()).verifyComplete();
|
|
||||||
}
|
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<T> void verifyInitializationTimeout(Function<McpAsyncClient, Mono<T>> operation, String action) {
|
||||||
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
|
StepVerifier.withVirtualTime(() -> operation.apply(mcpAsyncClient))
|
||||||
|
.expectSubscription()
|
||||||
|
.thenAwait(getInitializationTimeout())
|
||||||
|
.consumeErrorWith(e -> assertThat(e).isInstanceOf(McpError.class)
|
||||||
|
.hasMessage("Client must be initialized before " + action))
|
||||||
|
.verify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testConstructorWithInvalidArguments() {
|
void testConstructorWithInvalidArguments() {
|
||||||
assertThatThrownBy(() -> McpClient.async(null).build()).isInstanceOf(IllegalArgumentException.class)
|
assertThatThrownBy(() -> McpClient.async(null).build()).isInstanceOf(IllegalArgumentException.class)
|
||||||
.hasMessage("Transport must not be null");
|
.hasMessage("Transport must not be null");
|
||||||
|
|
||||||
assertThatThrownBy(() -> McpClient.async(mcpTransport).requestTimeout(null).build())
|
assertThatThrownBy(() -> McpClient.async(createMcpTransport()).requestTimeout(null).build())
|
||||||
.isInstanceOf(IllegalArgumentException.class)
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
.hasMessage("Request timeout must not be null");
|
.hasMessage("Request timeout must not be null");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testListToolsWithoutInitialization() {
|
void testListToolsWithoutInitialization() {
|
||||||
StepVerifier.create(mcpAsyncClient.listTools(null)).expectErrorSatisfies(error -> {
|
verifyInitializationTimeout(client -> client.listTools(null), "listing tools");
|
||||||
assertThat(error).isInstanceOf(McpError.class)
|
|
||||||
.hasMessage("Client must be initialized before listing tools");
|
|
||||||
}).verify();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testListTools() {
|
void testListTools() {
|
||||||
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listTools(null)))
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
.consumeNextWith(result -> {
|
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listTools(null)))
|
||||||
assertThat(result.tools()).isNotNull().isNotEmpty();
|
.consumeNextWith(result -> {
|
||||||
|
assertThat(result.tools()).isNotNull().isNotEmpty();
|
||||||
|
|
||||||
Tool firstTool = result.tools().get(0);
|
Tool firstTool = result.tools().get(0);
|
||||||
assertThat(firstTool.name()).isNotNull();
|
assertThat(firstTool.name()).isNotNull();
|
||||||
assertThat(firstTool.description()).isNotNull();
|
assertThat(firstTool.description()).isNotNull();
|
||||||
})
|
})
|
||||||
.verifyComplete();
|
.verifyComplete();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testPingWithoutInitialization() {
|
void testPingWithoutInitialization() {
|
||||||
StepVerifier.create(mcpAsyncClient.ping())
|
verifyInitializationTimeout(client -> client.ping(), "pinging the server");
|
||||||
.expectErrorMatches(error -> error instanceof McpError
|
|
||||||
&& error.getMessage().equals("Client must be initialized before pinging the server"))
|
|
||||||
.verify();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testPing() {
|
void testPing() {
|
||||||
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.ping())).consumeNextWith(callToolResult -> {
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
}).verifyComplete();
|
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.ping()))
|
||||||
|
.expectNextCount(1)
|
||||||
|
.verifyComplete();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testCallToolWithoutInitialization() {
|
void testCallToolWithoutInitialization() {
|
||||||
CallToolRequest callToolRequest = new CallToolRequest("echo", Map.of("message", ECHO_TEST_MESSAGE));
|
CallToolRequest callToolRequest = new CallToolRequest("echo", Map.of("message", ECHO_TEST_MESSAGE));
|
||||||
|
verifyInitializationTimeout(client -> client.callTool(callToolRequest), "calling tools");
|
||||||
StepVerifier.create(mcpAsyncClient.callTool(callToolRequest))
|
|
||||||
.expectErrorMatches(error -> error instanceof McpError
|
|
||||||
&& error.getMessage().equals("Client must be initialized before calling tools"))
|
|
||||||
.verify();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testCallTool() {
|
void testCallTool() {
|
||||||
CallToolRequest callToolRequest = new CallToolRequest("echo", Map.of("message", ECHO_TEST_MESSAGE));
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
|
CallToolRequest callToolRequest = new CallToolRequest("echo", Map.of("message", ECHO_TEST_MESSAGE));
|
||||||
|
|
||||||
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.callTool(callToolRequest)))
|
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.callTool(callToolRequest)))
|
||||||
.consumeNextWith(callToolResult -> {
|
.consumeNextWith(callToolResult -> {
|
||||||
assertThat(callToolResult).isNotNull();
|
assertThat(callToolResult).isNotNull().satisfies(result -> {
|
||||||
assertThat(callToolResult.content()).isNotNull();
|
assertThat(result.content()).isNotNull();
|
||||||
assertThat(callToolResult.isError()).isNull();
|
assertThat(result.isError()).isNull();
|
||||||
})
|
});
|
||||||
.verifyComplete();
|
})
|
||||||
|
.verifyComplete();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testCallToolWithInvalidTool() {
|
void testCallToolWithInvalidTool() {
|
||||||
CallToolRequest invalidRequest = new CallToolRequest("nonexistent_tool", Map.of("message", ECHO_TEST_MESSAGE));
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
|
CallToolRequest invalidRequest = new CallToolRequest("nonexistent_tool",
|
||||||
|
Map.of("message", ECHO_TEST_MESSAGE));
|
||||||
|
|
||||||
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.callTool(invalidRequest)))
|
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.callTool(invalidRequest)))
|
||||||
.expectError(Exception.class)
|
.consumeErrorWith(
|
||||||
.verify();
|
e -> assertThat(e).isInstanceOf(McpError.class).hasMessage("Unknown tool: nonexistent_tool"))
|
||||||
|
.verify();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testListResourcesWithoutInitialization() {
|
void testListResourcesWithoutInitialization() {
|
||||||
StepVerifier.create(mcpAsyncClient.listResources(null))
|
verifyInitializationTimeout(client -> client.listResources(null), "listing resources");
|
||||||
.expectErrorMatches(error -> error instanceof McpError
|
|
||||||
&& error.getMessage().equals("Client must be initialized before listing resources"))
|
|
||||||
.verify();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testListResources() {
|
void testListResources() {
|
||||||
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listResources(null)))
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
.consumeNextWith(resources -> {
|
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listResources(null)))
|
||||||
assertThat(resources).isNotNull().satisfies(result -> {
|
.consumeNextWith(resources -> {
|
||||||
assertThat(result.resources()).isNotNull();
|
assertThat(resources).isNotNull().satisfies(result -> {
|
||||||
|
assertThat(result.resources()).isNotNull();
|
||||||
|
|
||||||
if (!result.resources().isEmpty()) {
|
if (!result.resources().isEmpty()) {
|
||||||
Resource firstResource = result.resources().get(0);
|
Resource firstResource = result.resources().get(0);
|
||||||
assertThat(firstResource.uri()).isNotNull();
|
assertThat(firstResource.uri()).isNotNull();
|
||||||
assertThat(firstResource.name()).isNotNull();
|
assertThat(firstResource.name()).isNotNull();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.verifyComplete();
|
.verifyComplete();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testMcpAsyncClientState() {
|
void testMcpAsyncClientState() {
|
||||||
assertThat(mcpAsyncClient).isNotNull();
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
|
assertThat(mcpAsyncClient).isNotNull();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testListPromptsWithoutInitialization() {
|
void testListPromptsWithoutInitialization() {
|
||||||
StepVerifier.create(mcpAsyncClient.listPrompts(null))
|
verifyInitializationTimeout(client -> client.listPrompts(null), "listing " + "prompts");
|
||||||
.expectErrorMatches(error -> error instanceof McpError
|
|
||||||
&& error.getMessage().equals("Client must be initialized before listing prompts"))
|
|
||||||
.verify();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testListPrompts() {
|
void testListPrompts() {
|
||||||
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listPrompts(null)))
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
.consumeNextWith(prompts -> {
|
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listPrompts(null)))
|
||||||
assertThat(prompts).isNotNull().satisfies(result -> {
|
.consumeNextWith(prompts -> {
|
||||||
assertThat(result.prompts()).isNotNull();
|
assertThat(prompts).isNotNull().satisfies(result -> {
|
||||||
|
assertThat(result.prompts()).isNotNull();
|
||||||
|
|
||||||
if (!result.prompts().isEmpty()) {
|
if (!result.prompts().isEmpty()) {
|
||||||
Prompt firstPrompt = result.prompts().get(0);
|
Prompt firstPrompt = result.prompts().get(0);
|
||||||
assertThat(firstPrompt.name()).isNotNull();
|
assertThat(firstPrompt.name()).isNotNull();
|
||||||
assertThat(firstPrompt.description()).isNotNull();
|
assertThat(firstPrompt.description()).isNotNull();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.verifyComplete();
|
.verifyComplete();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testGetPromptWithoutInitialization() {
|
void testGetPromptWithoutInitialization() {
|
||||||
GetPromptRequest request = new GetPromptRequest("simple_prompt", Map.of());
|
GetPromptRequest request = new GetPromptRequest("simple_prompt", Map.of());
|
||||||
|
verifyInitializationTimeout(client -> client.getPrompt(request), "getting " + "prompts");
|
||||||
StepVerifier.create(mcpAsyncClient.getPrompt(request))
|
|
||||||
.expectErrorMatches(error -> error instanceof McpError
|
|
||||||
&& error.getMessage().equals("Client must be initialized before getting prompts"))
|
|
||||||
.verify();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testGetPrompt() {
|
void testGetPrompt() {
|
||||||
GetPromptRequest request = new GetPromptRequest("simple_prompt", Map.of());
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
|
StepVerifier
|
||||||
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.getPrompt(request)))
|
.create(mcpAsyncClient.initialize()
|
||||||
.consumeNextWith(prompt -> {
|
.then(mcpAsyncClient.getPrompt(new GetPromptRequest("simple_prompt", Map.of()))))
|
||||||
assertThat(prompt).isNotNull().satisfies(result -> {
|
.consumeNextWith(prompt -> {
|
||||||
assertThat(result.messages()).isNotEmpty();
|
assertThat(prompt).isNotNull().satisfies(result -> {
|
||||||
assertThat(result.messages()).hasSize(1);
|
assertThat(result.messages()).isNotEmpty();
|
||||||
});
|
assertThat(result.messages()).hasSize(1);
|
||||||
})
|
});
|
||||||
.verifyComplete();
|
})
|
||||||
|
.verifyComplete();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testRootsListChangedWithoutInitialization() {
|
void testRootsListChangedWithoutInitialization() {
|
||||||
StepVerifier.create(mcpAsyncClient.rootsListChangedNotification())
|
verifyInitializationTimeout(client -> client.rootsListChangedNotification(),
|
||||||
.expectErrorMatches(error -> error instanceof McpError && error.getMessage()
|
"sending roots list changed notification");
|
||||||
.equals("Client must be initialized before sending roots list changed notification"))
|
|
||||||
.verify();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testRootsListChanged() {
|
void testRootsListChanged() {
|
||||||
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.rootsListChangedNotification()))
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
.verifyComplete();
|
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.rootsListChangedNotification()))
|
||||||
|
.verifyComplete();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testInitializeWithRootsListProviders() {
|
void testInitializeWithRootsListProviders() {
|
||||||
var transport = createMcpTransport();
|
withClient(createMcpTransport(), builder -> builder.roots(new Root("file:///test/path", "test-root")),
|
||||||
|
client -> {
|
||||||
var client = McpClient.async(transport)
|
StepVerifier.create(client.initialize().then(client.closeGracefully())).verifyComplete();
|
||||||
.requestTimeout(getRequestTimeout())
|
});
|
||||||
.roots(new Root("file:///test/path", "test-root"))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
StepVerifier.create(client.initialize().then(client.closeGracefully())).verifyComplete();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testAddRoot() {
|
void testAddRoot() {
|
||||||
Root newRoot = new Root("file:///new/test/path", "new-test-root");
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
|
Root newRoot = new Root("file:///new/test/path", "new-test-root");
|
||||||
StepVerifier.create(mcpAsyncClient.addRoot(newRoot)).verifyComplete();
|
StepVerifier.create(mcpAsyncClient.addRoot(newRoot)).verifyComplete();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testAddRootWithNullValue() {
|
void testAddRootWithNullValue() {
|
||||||
StepVerifier.create(mcpAsyncClient.addRoot(null))
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
.expectErrorMatches(error -> error.getMessage().contains("Root must not be null"))
|
StepVerifier.create(mcpAsyncClient.addRoot(null))
|
||||||
.verify();
|
.consumeErrorWith(e -> assertThat(e).isInstanceOf(McpError.class).hasMessage("Root must not be null"))
|
||||||
|
.verify();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testRemoveRoot() {
|
void testRemoveRoot() {
|
||||||
Root root = new Root("file:///test/path/to/remove", "root-to-remove");
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
|
Root root = new Root("file:///test/path/to/remove", "root-to-remove");
|
||||||
|
StepVerifier.create(mcpAsyncClient.addRoot(root)).verifyComplete();
|
||||||
|
|
||||||
StepVerifier.create(mcpAsyncClient.addRoot(root).then(mcpAsyncClient.removeRoot(root.uri()))).verifyComplete();
|
StepVerifier.create(mcpAsyncClient.removeRoot(root.uri())).verifyComplete();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testRemoveNonExistentRoot() {
|
void testRemoveNonExistentRoot() {
|
||||||
StepVerifier.create(mcpAsyncClient.removeRoot("nonexistent-uri"))
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
.expectErrorMatches(error -> error.getMessage().contains("Root with uri 'nonexistent-uri' not found"))
|
StepVerifier.create(mcpAsyncClient.removeRoot("nonexistent-uri"))
|
||||||
.verify();
|
.consumeErrorWith(e -> assertThat(e).isInstanceOf(McpError.class)
|
||||||
|
.hasMessage("Root with uri 'nonexistent-uri' not found"))
|
||||||
|
.verify();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Disabled
|
@Disabled
|
||||||
void testReadResource() {
|
void testReadResource() {
|
||||||
StepVerifier.create(mcpAsyncClient.listResources()).consumeNextWith(resources -> {
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
if (!resources.resources().isEmpty()) {
|
StepVerifier.create(mcpAsyncClient.listResources()).consumeNextWith(resources -> {
|
||||||
Resource firstResource = resources.resources().get(0);
|
if (!resources.resources().isEmpty()) {
|
||||||
StepVerifier.create(mcpAsyncClient.readResource(firstResource)).consumeNextWith(result -> {
|
Resource firstResource = resources.resources().get(0);
|
||||||
assertThat(result).isNotNull();
|
StepVerifier.create(mcpAsyncClient.readResource(firstResource)).consumeNextWith(result -> {
|
||||||
assertThat(result.contents()).isNotNull();
|
assertThat(result).isNotNull();
|
||||||
}).verifyComplete();
|
assertThat(result.contents()).isNotNull();
|
||||||
}
|
}).verifyComplete();
|
||||||
}).verifyComplete();
|
}
|
||||||
|
}).verifyComplete();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testListResourceTemplatesWithoutInitialization() {
|
void testListResourceTemplatesWithoutInitialization() {
|
||||||
StepVerifier.create(mcpAsyncClient.listResourceTemplates())
|
verifyInitializationTimeout(client -> client.listResourceTemplates(), "listing resource templates");
|
||||||
.expectErrorMatches(error -> error instanceof McpError
|
|
||||||
&& error.getMessage().equals("Client must be initialized before listing resource templates"))
|
|
||||||
.verify();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testListResourceTemplates() {
|
void testListResourceTemplates() {
|
||||||
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listResourceTemplates()))
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
.consumeNextWith(result -> {
|
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listResourceTemplates()))
|
||||||
assertThat(result).isNotNull();
|
.consumeNextWith(result -> {
|
||||||
assertThat(result.resourceTemplates()).isNotNull();
|
assertThat(result).isNotNull();
|
||||||
})
|
assertThat(result.resourceTemplates()).isNotNull();
|
||||||
.verifyComplete();
|
})
|
||||||
|
.verifyComplete();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Test
|
// @Test
|
||||||
void testResourceSubscription() {
|
void testResourceSubscription() {
|
||||||
StepVerifier.create(mcpAsyncClient.listResources()).consumeNextWith(resources -> {
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
if (!resources.resources().isEmpty()) {
|
StepVerifier.create(mcpAsyncClient.listResources()).consumeNextWith(resources -> {
|
||||||
Resource firstResource = resources.resources().get(0);
|
if (!resources.resources().isEmpty()) {
|
||||||
|
Resource firstResource = resources.resources().get(0);
|
||||||
|
|
||||||
// Test subscribe
|
// Test subscribe
|
||||||
StepVerifier.create(mcpAsyncClient.subscribeResource(new SubscribeRequest(firstResource.uri())))
|
StepVerifier.create(mcpAsyncClient.subscribeResource(new SubscribeRequest(firstResource.uri())))
|
||||||
.verifyComplete();
|
.verifyComplete();
|
||||||
|
|
||||||
// Test unsubscribe
|
// Test unsubscribe
|
||||||
StepVerifier.create(mcpAsyncClient.unsubscribeResource(new UnsubscribeRequest(firstResource.uri())))
|
StepVerifier.create(mcpAsyncClient.unsubscribeResource(new UnsubscribeRequest(firstResource.uri())))
|
||||||
.verifyComplete();
|
.verifyComplete();
|
||||||
}
|
}
|
||||||
}).verifyComplete();
|
}).verifyComplete();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -354,36 +395,44 @@ public abstract class AbstractMcpAsyncClientTests {
|
||||||
AtomicBoolean resourcesNotificationReceived = new AtomicBoolean(false);
|
AtomicBoolean resourcesNotificationReceived = new AtomicBoolean(false);
|
||||||
AtomicBoolean promptsNotificationReceived = new AtomicBoolean(false);
|
AtomicBoolean promptsNotificationReceived = new AtomicBoolean(false);
|
||||||
|
|
||||||
var transport = createMcpTransport();
|
withClient(createMcpTransport(),
|
||||||
var client = McpClient.async(transport)
|
builder -> builder
|
||||||
.requestTimeout(getRequestTimeout())
|
.toolsChangeConsumer(tools -> Mono.fromRunnable(() -> toolsNotificationReceived.set(true)))
|
||||||
.toolsChangeConsumer(tools -> Mono.fromRunnable(() -> toolsNotificationReceived.set(true)))
|
.resourcesChangeConsumer(
|
||||||
.resourcesChangeConsumer(resources -> Mono.fromRunnable(() -> resourcesNotificationReceived.set(true)))
|
resources -> Mono.fromRunnable(() -> resourcesNotificationReceived.set(true)))
|
||||||
.promptsChangeConsumer(prompts -> Mono.fromRunnable(() -> promptsNotificationReceived.set(true)))
|
.promptsChangeConsumer(prompts -> Mono.fromRunnable(() -> promptsNotificationReceived.set(true))),
|
||||||
.build();
|
mcpAsyncClient -> {
|
||||||
|
|
||||||
StepVerifier.create(client.initialize().then(client.closeGracefully())).verifyComplete();
|
var transport = createMcpTransport();
|
||||||
|
var client = McpClient.async(transport)
|
||||||
|
.requestTimeout(getRequestTimeout())
|
||||||
|
.toolsChangeConsumer(tools -> Mono.fromRunnable(() -> toolsNotificationReceived.set(true)))
|
||||||
|
.resourcesChangeConsumer(
|
||||||
|
resources -> Mono.fromRunnable(() -> resourcesNotificationReceived.set(true)))
|
||||||
|
.promptsChangeConsumer(
|
||||||
|
prompts -> Mono.fromRunnable(() -> promptsNotificationReceived.set(true)))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
StepVerifier.create(client.initialize()).expectNextMatches(Objects::nonNull).verifyComplete();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testInitializeWithSamplingCapability() {
|
void testInitializeWithSamplingCapability() {
|
||||||
var transport = createMcpTransport();
|
ClientCapabilities capabilities = ClientCapabilities.builder().sampling().build();
|
||||||
|
CreateMessageResult createMessageResult = CreateMessageResult.builder()
|
||||||
var capabilities = ClientCapabilities.builder().sampling().build();
|
.message("test")
|
||||||
|
.model("test-model")
|
||||||
var client = McpClient.async(transport)
|
|
||||||
.requestTimeout(getRequestTimeout())
|
|
||||||
.capabilities(capabilities)
|
|
||||||
.sampling(request -> Mono.just(CreateMessageResult.builder().message("test").model("test-model").build()))
|
|
||||||
.build();
|
.build();
|
||||||
|
withClient(createMcpTransport(),
|
||||||
StepVerifier.create(client.initialize().then(client.closeGracefully())).verifyComplete();
|
builder -> builder.capabilities(capabilities).sampling(request -> Mono.just(createMessageResult)),
|
||||||
|
client -> {
|
||||||
|
StepVerifier.create(client.initialize()).expectNextMatches(Objects::nonNull).verifyComplete();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testInitializeWithAllCapabilities() {
|
void testInitializeWithAllCapabilities() {
|
||||||
var transport = createMcpTransport();
|
|
||||||
|
|
||||||
var capabilities = ClientCapabilities.builder()
|
var capabilities = ClientCapabilities.builder()
|
||||||
.experimental(Map.of("feature", "test"))
|
.experimental(Map.of("feature", "test"))
|
||||||
.roots(true)
|
.roots(true)
|
||||||
|
|
@ -392,18 +441,14 @@ public abstract class AbstractMcpAsyncClientTests {
|
||||||
|
|
||||||
Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler = request -> Mono
|
Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler = request -> Mono
|
||||||
.just(CreateMessageResult.builder().message("test").model("test-model").build());
|
.just(CreateMessageResult.builder().message("test").model("test-model").build());
|
||||||
var client = McpClient.async(transport)
|
|
||||||
.requestTimeout(getRequestTimeout())
|
|
||||||
.capabilities(capabilities)
|
|
||||||
.sampling(samplingHandler)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
StepVerifier.create(client.initialize()).consumeNextWith(result -> {
|
withClient(createMcpTransport(), builder -> builder.capabilities(capabilities).sampling(samplingHandler),
|
||||||
assertThat(result).isNotNull();
|
client ->
|
||||||
assertThat(result.capabilities()).isNotNull();
|
|
||||||
}).verifyComplete();
|
|
||||||
|
|
||||||
StepVerifier.create(client.closeGracefully()).verifyComplete();
|
StepVerifier.create(client.initialize()).assertNext(result -> {
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.capabilities()).isNotNull();
|
||||||
|
}).verifyComplete());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------
|
// ---------------------------------------
|
||||||
|
|
@ -412,43 +457,46 @@ public abstract class AbstractMcpAsyncClientTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testLoggingLevelsWithoutInitialization() {
|
void testLoggingLevelsWithoutInitialization() {
|
||||||
StepVerifier.create(mcpAsyncClient.setLoggingLevel(McpSchema.LoggingLevel.DEBUG))
|
verifyInitializationTimeout(client -> client.setLoggingLevel(McpSchema.LoggingLevel.DEBUG),
|
||||||
.expectErrorMatches(error -> error instanceof McpError
|
"setting logging level");
|
||||||
&& error.getMessage().equals("Client must be initialized before setting logging level"))
|
|
||||||
.verify();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testLoggingLevels() {
|
void testLoggingLevels() {
|
||||||
Mono<Void> testAllLevels = mcpAsyncClient.initialize().then(Mono.defer(() -> {
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
Mono<Void> chain = Mono.empty();
|
Mono<Void> testAllLevels = mcpAsyncClient.initialize().then(Mono.defer(() -> {
|
||||||
for (McpSchema.LoggingLevel level : McpSchema.LoggingLevel.values()) {
|
Mono<Void> chain = Mono.empty();
|
||||||
chain = chain.then(mcpAsyncClient.setLoggingLevel(level));
|
for (McpSchema.LoggingLevel level : McpSchema.LoggingLevel.values()) {
|
||||||
}
|
chain = chain.then(mcpAsyncClient.setLoggingLevel(level));
|
||||||
return chain;
|
}
|
||||||
}));
|
return chain;
|
||||||
|
}));
|
||||||
|
|
||||||
StepVerifier.create(testAllLevels).verifyComplete();
|
StepVerifier.create(testAllLevels).verifyComplete();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testLoggingConsumer() {
|
void testLoggingConsumer() {
|
||||||
AtomicBoolean logReceived = new AtomicBoolean(false);
|
AtomicBoolean logReceived = new AtomicBoolean(false);
|
||||||
var transport = createMcpTransport();
|
|
||||||
|
|
||||||
var client = McpClient.async(transport)
|
withClient(createMcpTransport(),
|
||||||
.requestTimeout(getRequestTimeout())
|
builder -> builder.loggingConsumer(notification -> Mono.fromRunnable(() -> logReceived.set(true))),
|
||||||
.loggingConsumer(notification -> Mono.fromRunnable(() -> logReceived.set(true)))
|
client -> {
|
||||||
.build();
|
StepVerifier.create(client.initialize()).expectNextMatches(Objects::nonNull).verifyComplete();
|
||||||
|
StepVerifier.create(client.closeGracefully()).verifyComplete();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
StepVerifier.create(client.initialize().then(client.closeGracefully())).verifyComplete();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testLoggingWithNullNotification() {
|
void testLoggingWithNullNotification() {
|
||||||
StepVerifier.create(mcpAsyncClient.setLoggingLevel(null))
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
.expectErrorMatches(error -> error.getMessage().contains("Logging level must not be null"))
|
StepVerifier.create(mcpAsyncClient.setLoggingLevel(null))
|
||||||
.verify();
|
.expectErrorMatches(error -> error.getMessage().contains("Logging level must not be null"))
|
||||||
|
.verify();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,9 @@ package io.modelcontextprotocol.client;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
import io.modelcontextprotocol.spec.ClientMcpTransport;
|
import io.modelcontextprotocol.spec.ClientMcpTransport;
|
||||||
import io.modelcontextprotocol.spec.McpError;
|
import io.modelcontextprotocol.spec.McpError;
|
||||||
|
|
@ -27,6 +30,10 @@ import io.modelcontextprotocol.spec.McpSchema.UnsubscribeRequest;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.core.scheduler.Scheduler;
|
||||||
|
import reactor.core.scheduler.Schedulers;
|
||||||
|
import reactor.test.StepVerifier;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||||
|
|
@ -41,12 +48,8 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
// KEEP IN SYNC with the class in mcp-test module
|
// KEEP IN SYNC with the class in mcp-test module
|
||||||
public abstract class AbstractMcpSyncClientTests {
|
public abstract class AbstractMcpSyncClientTests {
|
||||||
|
|
||||||
private McpSyncClient mcpSyncClient;
|
|
||||||
|
|
||||||
private static final String TEST_MESSAGE = "Hello MCP Spring AI!";
|
private static final String TEST_MESSAGE = "Hello MCP Spring AI!";
|
||||||
|
|
||||||
protected ClientMcpTransport mcpTransport;
|
|
||||||
|
|
||||||
abstract protected ClientMcpTransport createMcpTransport();
|
abstract protected ClientMcpTransport createMcpTransport();
|
||||||
|
|
||||||
protected void onStart() {
|
protected void onStart() {
|
||||||
|
|
@ -63,254 +66,322 @@ public abstract class AbstractMcpSyncClientTests {
|
||||||
return Duration.ofSeconds(2);
|
return Duration.ofSeconds(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
McpSyncClient client(ClientMcpTransport transport) {
|
||||||
|
return client(transport, Function.identity());
|
||||||
|
}
|
||||||
|
|
||||||
|
McpSyncClient client(ClientMcpTransport 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(ClientMcpTransport transport, Consumer<McpSyncClient> c) {
|
||||||
|
withClient(transport, Function.identity(), c);
|
||||||
|
}
|
||||||
|
|
||||||
|
void withClient(ClientMcpTransport transport, Function<McpClient.SyncSpec, McpClient.SyncSpec> customizer,
|
||||||
|
Consumer<McpSyncClient> c) {
|
||||||
|
var client = client(transport, customizer);
|
||||||
|
try {
|
||||||
|
c.accept(client);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
assertThat(client.closeGracefully()).isTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
onStart();
|
onStart();
|
||||||
this.mcpTransport = createMcpTransport();
|
|
||||||
|
|
||||||
assertThatCode(() -> {
|
|
||||||
mcpSyncClient = McpClient.sync(mcpTransport)
|
|
||||||
.requestTimeout(getRequestTimeout())
|
|
||||||
.initializationTimeout(getInitializationTimeout())
|
|
||||||
.capabilities(ClientCapabilities.builder().roots(true).build())
|
|
||||||
.build();
|
|
||||||
}).doesNotThrowAnyException();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
void tearDown() {
|
void tearDown() {
|
||||||
if (mcpSyncClient != null) {
|
|
||||||
assertThatCode(() -> mcpSyncClient.close()).doesNotThrowAnyException();
|
|
||||||
}
|
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static final Object DUMMY_RETURN_VALUE = new Object();
|
||||||
|
|
||||||
|
<T> void verifyNotificationTimesOut(Consumer<McpSyncClient> operation, String action) {
|
||||||
|
verifyCallTimesOut(client -> {
|
||||||
|
operation.accept(client);
|
||||||
|
return DUMMY_RETURN_VALUE;
|
||||||
|
}, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
<T> void verifyCallTimesOut(Function<McpSyncClient, T> operation, String action) {
|
||||||
|
withClient(createMcpTransport(), mcpSyncClient -> {
|
||||||
|
// This scheduler is not replaced by virtual time scheduler
|
||||||
|
Scheduler customScheduler = Schedulers.newBoundedElastic(1, 1, "actualBoundedElastic");
|
||||||
|
|
||||||
|
StepVerifier.withVirtualTime(() -> Mono.fromSupplier(() -> operation.apply(mcpSyncClient))
|
||||||
|
// offload the blocking call to the real scheduler
|
||||||
|
.subscribeOn(customScheduler))
|
||||||
|
.expectSubscription()
|
||||||
|
.thenAwait(getInitializationTimeout())
|
||||||
|
.consumeErrorWith(e -> assertThat(e).isInstanceOf(McpError.class)
|
||||||
|
.hasMessage("Client must be initialized before " + action))
|
||||||
|
.verify();
|
||||||
|
|
||||||
|
customScheduler.dispose();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testConstructorWithInvalidArguments() {
|
void testConstructorWithInvalidArguments() {
|
||||||
assertThatThrownBy(() -> McpClient.sync(null).build()).isInstanceOf(IllegalArgumentException.class)
|
assertThatThrownBy(() -> McpClient.sync(null).build()).isInstanceOf(IllegalArgumentException.class)
|
||||||
.hasMessage("Transport must not be null");
|
.hasMessage("Transport must not be null");
|
||||||
|
|
||||||
assertThatThrownBy(() -> McpClient.sync(mcpTransport).requestTimeout(null).build())
|
assertThatThrownBy(() -> McpClient.sync(createMcpTransport()).requestTimeout(null).build())
|
||||||
.isInstanceOf(IllegalArgumentException.class)
|
.isInstanceOf(IllegalArgumentException.class)
|
||||||
.hasMessage("Request timeout must not be null");
|
.hasMessage("Request timeout must not be null");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testListToolsWithoutInitialization() {
|
void testListToolsWithoutInitialization() {
|
||||||
assertThatThrownBy(() -> mcpSyncClient.listTools(null)).isInstanceOf(McpError.class)
|
verifyCallTimesOut(client -> client.listTools(null), "listing tools");
|
||||||
.hasMessage("Client must be initialized before listing tools");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testListTools() {
|
void testListTools() {
|
||||||
mcpSyncClient.initialize();
|
withClient(createMcpTransport(), mcpSyncClient -> {
|
||||||
ListToolsResult tools = mcpSyncClient.listTools(null);
|
mcpSyncClient.initialize();
|
||||||
|
ListToolsResult tools = mcpSyncClient.listTools(null);
|
||||||
|
|
||||||
assertThat(tools).isNotNull().satisfies(result -> {
|
assertThat(tools).isNotNull().satisfies(result -> {
|
||||||
assertThat(result.tools()).isNotNull().isNotEmpty();
|
assertThat(result.tools()).isNotNull().isNotEmpty();
|
||||||
|
|
||||||
Tool firstTool = result.tools().get(0);
|
Tool firstTool = result.tools().get(0);
|
||||||
assertThat(firstTool.name()).isNotNull();
|
assertThat(firstTool.name()).isNotNull();
|
||||||
assertThat(firstTool.description()).isNotNull();
|
assertThat(firstTool.description()).isNotNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testCallToolsWithoutInitialization() {
|
void testCallToolsWithoutInitialization() {
|
||||||
assertThatThrownBy(() -> mcpSyncClient.callTool(new CallToolRequest("add", Map.of("a", 3, "b", 4))))
|
verifyCallTimesOut(client -> client.callTool(new CallToolRequest("add", Map.of("a", 3, "b", 4))),
|
||||||
.isInstanceOf(McpError.class)
|
"calling tools");
|
||||||
.hasMessage("Client must be initialized before calling tools");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testCallTools() {
|
void testCallTools() {
|
||||||
mcpSyncClient.initialize();
|
withClient(createMcpTransport(), mcpSyncClient -> {
|
||||||
CallToolResult toolResult = mcpSyncClient.callTool(new CallToolRequest("add", Map.of("a", 3, "b", 4)));
|
mcpSyncClient.initialize();
|
||||||
|
CallToolResult toolResult = mcpSyncClient.callTool(new CallToolRequest("add", Map.of("a", 3, "b", 4)));
|
||||||
|
|
||||||
assertThat(toolResult).isNotNull().satisfies(result -> {
|
assertThat(toolResult).isNotNull().satisfies(result -> {
|
||||||
|
|
||||||
assertThat(result.content()).hasSize(1);
|
assertThat(result.content()).hasSize(1);
|
||||||
|
|
||||||
TextContent content = (TextContent) result.content().get(0);
|
TextContent content = (TextContent) result.content().get(0);
|
||||||
|
|
||||||
assertThat(content).isNotNull();
|
assertThat(content).isNotNull();
|
||||||
assertThat(content.text()).isNotNull();
|
assertThat(content.text()).isNotNull();
|
||||||
assertThat(content.text()).contains("7");
|
assertThat(content.text()).contains("7");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testPingWithoutInitialization() {
|
void testPingWithoutInitialization() {
|
||||||
assertThatThrownBy(() -> mcpSyncClient.ping()).isInstanceOf(McpError.class)
|
verifyCallTimesOut(client -> client.ping(), "pinging the server");
|
||||||
.hasMessage("Client must be initialized before pinging the server");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testPing() {
|
void testPing() {
|
||||||
mcpSyncClient.initialize();
|
withClient(createMcpTransport(), mcpSyncClient -> {
|
||||||
assertThatCode(() -> mcpSyncClient.ping()).doesNotThrowAnyException();
|
mcpSyncClient.initialize();
|
||||||
|
assertThatCode(() -> mcpSyncClient.ping()).doesNotThrowAnyException();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testCallToolWithoutInitialization() {
|
void testCallToolWithoutInitialization() {
|
||||||
CallToolRequest callToolRequest = new CallToolRequest("echo", Map.of("message", TEST_MESSAGE));
|
CallToolRequest callToolRequest = new CallToolRequest("echo", Map.of("message", TEST_MESSAGE));
|
||||||
|
verifyCallTimesOut(client -> client.callTool(callToolRequest), "calling tools");
|
||||||
assertThatThrownBy(() -> mcpSyncClient.callTool(callToolRequest)).isInstanceOf(McpError.class)
|
|
||||||
.hasMessage("Client must be initialized before calling tools");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testCallTool() {
|
void testCallTool() {
|
||||||
mcpSyncClient.initialize();
|
withClient(createMcpTransport(), mcpSyncClient -> {
|
||||||
CallToolRequest callToolRequest = new CallToolRequest("echo", Map.of("message", TEST_MESSAGE));
|
mcpSyncClient.initialize();
|
||||||
|
CallToolRequest callToolRequest = new CallToolRequest("echo", Map.of("message", TEST_MESSAGE));
|
||||||
|
|
||||||
CallToolResult callToolResult = mcpSyncClient.callTool(callToolRequest);
|
CallToolResult callToolResult = mcpSyncClient.callTool(callToolRequest);
|
||||||
|
|
||||||
assertThat(callToolResult).isNotNull().satisfies(result -> {
|
assertThat(callToolResult).isNotNull().satisfies(result -> {
|
||||||
assertThat(result.content()).isNotNull();
|
assertThat(result.content()).isNotNull();
|
||||||
assertThat(result.isError()).isNull();
|
assertThat(result.isError()).isNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testCallToolWithInvalidTool() {
|
void testCallToolWithInvalidTool() {
|
||||||
CallToolRequest invalidRequest = new CallToolRequest("nonexistent_tool", Map.of("message", TEST_MESSAGE));
|
withClient(createMcpTransport(), mcpSyncClient -> {
|
||||||
|
CallToolRequest invalidRequest = new CallToolRequest("nonexistent_tool", Map.of("message", TEST_MESSAGE));
|
||||||
|
|
||||||
assertThatThrownBy(() -> mcpSyncClient.callTool(invalidRequest)).isInstanceOf(Exception.class);
|
assertThatThrownBy(() -> mcpSyncClient.callTool(invalidRequest)).isInstanceOf(Exception.class);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testRootsListChangedWithoutInitialization() {
|
void testRootsListChangedWithoutInitialization() {
|
||||||
assertThatThrownBy(() -> mcpSyncClient.rootsListChangedNotification()).isInstanceOf(McpError.class)
|
verifyNotificationTimesOut(client -> client.rootsListChangedNotification(),
|
||||||
.hasMessage("Client must be initialized before sending roots list changed notification");
|
"sending roots list changed notification");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testRootsListChanged() {
|
void testRootsListChanged() {
|
||||||
mcpSyncClient.initialize();
|
withClient(createMcpTransport(), mcpSyncClient -> {
|
||||||
assertThatCode(() -> mcpSyncClient.rootsListChangedNotification()).doesNotThrowAnyException();
|
mcpSyncClient.initialize();
|
||||||
|
assertThatCode(() -> mcpSyncClient.rootsListChangedNotification()).doesNotThrowAnyException();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testListResourcesWithoutInitialization() {
|
void testListResourcesWithoutInitialization() {
|
||||||
assertThatThrownBy(() -> mcpSyncClient.listResources(null)).isInstanceOf(McpError.class)
|
verifyCallTimesOut(client -> client.listResources(null), "listing resources");
|
||||||
.hasMessage("Client must be initialized before listing resources");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testListResources() {
|
void testListResources() {
|
||||||
mcpSyncClient.initialize();
|
withClient(createMcpTransport(), mcpSyncClient -> {
|
||||||
ListResourcesResult resources = mcpSyncClient.listResources(null);
|
mcpSyncClient.initialize();
|
||||||
|
ListResourcesResult resources = mcpSyncClient.listResources(null);
|
||||||
|
|
||||||
assertThat(resources).isNotNull().satisfies(result -> {
|
assertThat(resources).isNotNull().satisfies(result -> {
|
||||||
assertThat(result.resources()).isNotNull();
|
assertThat(result.resources()).isNotNull();
|
||||||
|
|
||||||
if (!result.resources().isEmpty()) {
|
if (!result.resources().isEmpty()) {
|
||||||
Resource firstResource = result.resources().get(0);
|
Resource firstResource = result.resources().get(0);
|
||||||
assertThat(firstResource.uri()).isNotNull();
|
assertThat(firstResource.uri()).isNotNull();
|
||||||
assertThat(firstResource.name()).isNotNull();
|
assertThat(firstResource.name()).isNotNull();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testClientSessionState() {
|
void testClientSessionState() {
|
||||||
assertThat(mcpSyncClient).isNotNull();
|
withClient(createMcpTransport(), mcpSyncClient -> {
|
||||||
|
assertThat(mcpSyncClient).isNotNull();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testInitializeWithRootsListProviders() {
|
void testInitializeWithRootsListProviders() {
|
||||||
var transport = createMcpTransport();
|
withClient(createMcpTransport(), builder -> builder.roots(new Root("file:///test/path", "test-root")),
|
||||||
|
mcpSyncClient -> {
|
||||||
|
|
||||||
var client = McpClient.sync(transport)
|
assertThatCode(() -> {
|
||||||
.requestTimeout(getRequestTimeout())
|
mcpSyncClient.initialize();
|
||||||
.roots(new Root("file:///test/path", "test-root"))
|
mcpSyncClient.close();
|
||||||
.build();
|
}).doesNotThrowAnyException();
|
||||||
|
});
|
||||||
assertThatCode(() -> {
|
|
||||||
client.initialize();
|
|
||||||
client.close();
|
|
||||||
}).doesNotThrowAnyException();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testAddRoot() {
|
void testAddRoot() {
|
||||||
Root newRoot = new Root("file:///new/test/path", "new-test-root");
|
withClient(createMcpTransport(), mcpSyncClient -> {
|
||||||
assertThatCode(() -> mcpSyncClient.addRoot(newRoot)).doesNotThrowAnyException();
|
Root newRoot = new Root("file:///new/test/path", "new-test-root");
|
||||||
|
assertThatCode(() -> mcpSyncClient.addRoot(newRoot)).doesNotThrowAnyException();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testAddRootWithNullValue() {
|
void testAddRootWithNullValue() {
|
||||||
assertThatThrownBy(() -> mcpSyncClient.addRoot(null)).hasMessageContaining("Root must not be null");
|
withClient(createMcpTransport(), mcpSyncClient -> {
|
||||||
|
assertThatThrownBy(() -> mcpSyncClient.addRoot(null)).hasMessageContaining("Root must not be null");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testRemoveRoot() {
|
void testRemoveRoot() {
|
||||||
Root root = new Root("file:///test/path/to/remove", "root-to-remove");
|
withClient(createMcpTransport(), mcpSyncClient -> {
|
||||||
assertThatCode(() -> {
|
Root root = new Root("file:///test/path/to/remove", "root-to-remove");
|
||||||
mcpSyncClient.addRoot(root);
|
assertThatCode(() -> {
|
||||||
mcpSyncClient.removeRoot(root.uri());
|
mcpSyncClient.addRoot(root);
|
||||||
}).doesNotThrowAnyException();
|
mcpSyncClient.removeRoot(root.uri());
|
||||||
|
}).doesNotThrowAnyException();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testRemoveNonExistentRoot() {
|
void testRemoveNonExistentRoot() {
|
||||||
assertThatThrownBy(() -> mcpSyncClient.removeRoot("nonexistent-uri"))
|
withClient(createMcpTransport(), mcpSyncClient -> {
|
||||||
.hasMessageContaining("Root with uri 'nonexistent-uri' not found");
|
assertThatThrownBy(() -> mcpSyncClient.removeRoot("nonexistent-uri"))
|
||||||
|
.hasMessageContaining("Root with uri 'nonexistent-uri' not found");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testReadResourceWithoutInitialization() {
|
void testReadResourceWithoutInitialization() {
|
||||||
assertThatThrownBy(() -> {
|
Resource resource = new Resource("test://uri", "Test Resource", null, null, null);
|
||||||
Resource resource = new Resource("test://uri", "Test Resource", null, null, null);
|
verifyCallTimesOut(client -> client.readResource(resource), "reading resources");
|
||||||
mcpSyncClient.readResource(resource);
|
|
||||||
}).isInstanceOf(McpError.class).hasMessage("Client must be initialized before reading resources");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testReadResource() {
|
void testReadResource() {
|
||||||
mcpSyncClient.initialize();
|
withClient(createMcpTransport(), mcpSyncClient -> {
|
||||||
ListResourcesResult resources = mcpSyncClient.listResources(null);
|
mcpSyncClient.initialize();
|
||||||
|
ListResourcesResult resources = mcpSyncClient.listResources(null);
|
||||||
|
|
||||||
if (!resources.resources().isEmpty()) {
|
if (!resources.resources().isEmpty()) {
|
||||||
Resource firstResource = resources.resources().get(0);
|
Resource firstResource = resources.resources().get(0);
|
||||||
ReadResourceResult result = mcpSyncClient.readResource(firstResource);
|
ReadResourceResult result = mcpSyncClient.readResource(firstResource);
|
||||||
|
|
||||||
assertThat(result).isNotNull();
|
assertThat(result).isNotNull();
|
||||||
assertThat(result.contents()).isNotNull();
|
assertThat(result.contents()).isNotNull();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testListResourceTemplatesWithoutInitialization() {
|
void testListResourceTemplatesWithoutInitialization() {
|
||||||
assertThatThrownBy(() -> mcpSyncClient.listResourceTemplates(null)).isInstanceOf(McpError.class)
|
verifyCallTimesOut(client -> client.listResourceTemplates(null), "listing resource templates");
|
||||||
.hasMessage("Client must be initialized before listing resource templates");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testListResourceTemplates() {
|
void testListResourceTemplates() {
|
||||||
mcpSyncClient.initialize();
|
withClient(createMcpTransport(), mcpSyncClient -> {
|
||||||
ListResourceTemplatesResult result = mcpSyncClient.listResourceTemplates(null);
|
mcpSyncClient.initialize();
|
||||||
|
ListResourceTemplatesResult result = mcpSyncClient.listResourceTemplates(null);
|
||||||
|
|
||||||
assertThat(result).isNotNull();
|
assertThat(result).isNotNull();
|
||||||
assertThat(result.resourceTemplates()).isNotNull();
|
assertThat(result.resourceTemplates()).isNotNull();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Test
|
// @Test
|
||||||
void testResourceSubscription() {
|
void testResourceSubscription() {
|
||||||
ListResourcesResult resources = mcpSyncClient.listResources(null);
|
withClient(createMcpTransport(), mcpSyncClient -> {
|
||||||
|
ListResourcesResult resources = mcpSyncClient.listResources(null);
|
||||||
|
|
||||||
if (!resources.resources().isEmpty()) {
|
if (!resources.resources().isEmpty()) {
|
||||||
Resource firstResource = resources.resources().get(0);
|
Resource firstResource = resources.resources().get(0);
|
||||||
|
|
||||||
// Test subscribe
|
// Test subscribe
|
||||||
assertThatCode(() -> mcpSyncClient.subscribeResource(new SubscribeRequest(firstResource.uri())))
|
assertThatCode(() -> mcpSyncClient.subscribeResource(new SubscribeRequest(firstResource.uri())))
|
||||||
.doesNotThrowAnyException();
|
.doesNotThrowAnyException();
|
||||||
|
|
||||||
// Test unsubscribe
|
// Test unsubscribe
|
||||||
assertThatCode(() -> mcpSyncClient.unsubscribeResource(new UnsubscribeRequest(firstResource.uri())))
|
assertThatCode(() -> mcpSyncClient.unsubscribeResource(new UnsubscribeRequest(firstResource.uri())))
|
||||||
.doesNotThrowAnyException();
|
.doesNotThrowAnyException();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -319,18 +390,17 @@ public abstract class AbstractMcpSyncClientTests {
|
||||||
AtomicBoolean resourcesNotificationReceived = new AtomicBoolean(false);
|
AtomicBoolean resourcesNotificationReceived = new AtomicBoolean(false);
|
||||||
AtomicBoolean promptsNotificationReceived = new AtomicBoolean(false);
|
AtomicBoolean promptsNotificationReceived = new AtomicBoolean(false);
|
||||||
|
|
||||||
var transport = createMcpTransport();
|
withClient(createMcpTransport(),
|
||||||
var client = McpClient.sync(transport)
|
builder -> builder.toolsChangeConsumer(tools -> toolsNotificationReceived.set(true))
|
||||||
.requestTimeout(getRequestTimeout())
|
.resourcesChangeConsumer(resources -> resourcesNotificationReceived.set(true))
|
||||||
.toolsChangeConsumer(tools -> toolsNotificationReceived.set(true))
|
.promptsChangeConsumer(prompts -> promptsNotificationReceived.set(true)),
|
||||||
.resourcesChangeConsumer(resources -> resourcesNotificationReceived.set(true))
|
client -> {
|
||||||
.promptsChangeConsumer(prompts -> promptsNotificationReceived.set(true))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
assertThatCode(() -> {
|
assertThatCode(() -> {
|
||||||
client.initialize();
|
client.initialize();
|
||||||
client.close();
|
client.close();
|
||||||
}).doesNotThrowAnyException();
|
}).doesNotThrowAnyException();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------
|
// ---------------------------------------
|
||||||
|
|
@ -339,40 +409,37 @@ public abstract class AbstractMcpSyncClientTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testLoggingLevelsWithoutInitialization() {
|
void testLoggingLevelsWithoutInitialization() {
|
||||||
assertThatThrownBy(() -> mcpSyncClient.setLoggingLevel(McpSchema.LoggingLevel.DEBUG))
|
verifyNotificationTimesOut(client -> client.setLoggingLevel(McpSchema.LoggingLevel.DEBUG),
|
||||||
.isInstanceOf(McpError.class)
|
"setting logging level");
|
||||||
.hasMessage("Client must be initialized before setting logging level");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testLoggingLevels() {
|
void testLoggingLevels() {
|
||||||
mcpSyncClient.initialize();
|
withClient(createMcpTransport(), mcpSyncClient -> {
|
||||||
// Test all logging levels
|
mcpSyncClient.initialize();
|
||||||
for (McpSchema.LoggingLevel level : McpSchema.LoggingLevel.values()) {
|
// Test all logging levels
|
||||||
assertThatCode(() -> mcpSyncClient.setLoggingLevel(level)).doesNotThrowAnyException();
|
for (McpSchema.LoggingLevel level : McpSchema.LoggingLevel.values()) {
|
||||||
}
|
assertThatCode(() -> mcpSyncClient.setLoggingLevel(level)).doesNotThrowAnyException();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testLoggingConsumer() {
|
void testLoggingConsumer() {
|
||||||
AtomicBoolean logReceived = new AtomicBoolean(false);
|
AtomicBoolean logReceived = new AtomicBoolean(false);
|
||||||
var transport = createMcpTransport();
|
withClient(createMcpTransport(), builder -> builder.requestTimeout(getRequestTimeout())
|
||||||
|
.loggingConsumer(notification -> logReceived.set(true)), client -> {
|
||||||
var client = McpClient.sync(transport)
|
assertThatCode(() -> {
|
||||||
.requestTimeout(getRequestTimeout())
|
client.initialize();
|
||||||
.loggingConsumer(notification -> logReceived.set(true))
|
client.close();
|
||||||
.build();
|
}).doesNotThrowAnyException();
|
||||||
|
});
|
||||||
assertThatCode(() -> {
|
|
||||||
client.initialize();
|
|
||||||
client.close();
|
|
||||||
}).doesNotThrowAnyException();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testLoggingWithNullNotification() {
|
void testLoggingWithNullNotification() {
|
||||||
assertThatThrownBy(() -> mcpSyncClient.setLoggingLevel(null))
|
withClient(createMcpTransport(), mcpSyncClient -> assertThatThrownBy(() -> mcpSyncClient.setLoggingLevel(null))
|
||||||
.hasMessageContaining("Logging level must not be null");
|
.hasMessageContaining("Logging level must not be null"));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@
|
||||||
package io.modelcontextprotocol.client;
|
package io.modelcontextprotocol.client;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
import io.modelcontextprotocol.client.transport.ServerParameters;
|
import io.modelcontextprotocol.client.transport.ServerParameters;
|
||||||
|
|
@ -13,6 +15,7 @@ import io.modelcontextprotocol.spec.ClientMcpTransport;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.Timeout;
|
import org.junit.jupiter.api.Timeout;
|
||||||
import reactor.core.publisher.Sinks;
|
import reactor.core.publisher.Sinks;
|
||||||
|
import reactor.test.StepVerifier;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
|
@ -35,15 +38,26 @@ class StdioMcpSyncClientTests extends AbstractMcpSyncClientTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void customErrorHandlerShouldReceiveErrors() {
|
void customErrorHandlerShouldReceiveErrors() throws InterruptedException {
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
AtomicReference<String> receivedError = new AtomicReference<>();
|
AtomicReference<String> receivedError = new AtomicReference<>();
|
||||||
|
|
||||||
((StdioClientTransport) mcpTransport).setStdErrorHandler(error -> receivedError.set(error));
|
ClientMcpTransport transport = createMcpTransport();
|
||||||
|
StepVerifier.create(transport.connect(msg -> msg)).verifyComplete();
|
||||||
|
|
||||||
|
((StdioClientTransport) transport).setStdErrorHandler(error -> {
|
||||||
|
receivedError.set(error);
|
||||||
|
latch.countDown();
|
||||||
|
});
|
||||||
|
|
||||||
String errorMessage = "Test error";
|
String errorMessage = "Test error";
|
||||||
((StdioClientTransport) mcpTransport).getErrorSink().emitNext(errorMessage, Sinks.EmitFailureHandler.FAIL_FAST);
|
((StdioClientTransport) transport).getErrorSink().emitNext(errorMessage, Sinks.EmitFailureHandler.FAIL_FAST);
|
||||||
|
|
||||||
|
assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue();
|
||||||
|
|
||||||
assertThat(receivedError.get()).isNotNull().isEqualTo(errorMessage);
|
assertThat(receivedError.get()).isNotNull().isEqualTo(errorMessage);
|
||||||
|
|
||||||
|
StepVerifier.create(transport.closeGracefully()).expectComplete().verify(Duration.ofSeconds(5));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Duration getInitializationTimeout() {
|
protected Duration getInitializationTimeout() {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue