feat: WebClient Streamable HTTP support (#292)
An implementation of Streamable HTTP Client with WebFlux WebClient. Aside from implementing the specification, several improvements have been incorporated throughout the client-side of the architecture. The changes cover: - resilience tests using toxiproxy in testcontainers - integration tests using updated everything-server with streamableHttp support - improved logging - session invalidation handling (both transport session and JSON-RPC concept of session) - implicit initialization and burst protection (in case of concurrent `Mcp(Sync|Async)Client` use - more logging, e.g. stdio process lifecycle logs Related #72, #273, #253, #107, #105 Signed-off-by: Dariusz Jędrzejczyk <dariusz.jedrzejczyk@broadcom.com>
This commit is contained in:
parent
8a5a591d39
commit
2f944349cc
3 changed files with 1501 additions and 242 deletions
|
|
@ -0,0 +1,222 @@
|
|||
package io.modelcontextprotocol.client;
|
||||
|
||||
import eu.rekawek.toxiproxy.Proxy;
|
||||
import eu.rekawek.toxiproxy.ToxiproxyClient;
|
||||
import eu.rekawek.toxiproxy.model.ToxicDirection;
|
||||
import io.modelcontextprotocol.spec.McpClientTransport;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import io.modelcontextprotocol.spec.McpTransport;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.testcontainers.containers.GenericContainer;
|
||||
import org.testcontainers.containers.Network;
|
||||
import org.testcontainers.containers.ToxiproxyContainer;
|
||||
import org.testcontainers.containers.wait.strategy.Wait;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
|
||||
/**
|
||||
* Resiliency test suite for the {@link McpAsyncClient} that can be used with different
|
||||
* {@link McpTransport} implementations that support Streamable HTTP.
|
||||
*
|
||||
* The purpose of these tests is to allow validating the transport layer resiliency
|
||||
* instead of the functionality offered by the logical layer of MCP concepts such as
|
||||
* tools, resources, prompts, etc.
|
||||
*
|
||||
* @author Dariusz Jędrzejczyk
|
||||
*/
|
||||
public abstract class AbstractMcpAsyncClientResiliencyTests {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(AbstractMcpAsyncClientResiliencyTests.class);
|
||||
|
||||
static Network network = Network.newNetwork();
|
||||
static String host = "http://localhost:3001";
|
||||
|
||||
// Uses the https://github.com/tzolov/mcp-everything-server-docker-image
|
||||
@SuppressWarnings("resource")
|
||||
static GenericContainer<?> container = new GenericContainer<>("docker.io/tzolov/mcp-everything-server:v2")
|
||||
.withCommand("node dist/index.js streamableHttp")
|
||||
.withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String()))
|
||||
.withNetwork(network)
|
||||
.withNetworkAliases("everything-server")
|
||||
.withExposedPorts(3001)
|
||||
.waitingFor(Wait.forHttp("/").forStatusCode(404));
|
||||
|
||||
static ToxiproxyContainer toxiproxy = new ToxiproxyContainer("ghcr.io/shopify/toxiproxy:2.5.0").withNetwork(network)
|
||||
.withExposedPorts(8474, 3000);
|
||||
|
||||
static Proxy proxy;
|
||||
|
||||
static {
|
||||
container.start();
|
||||
|
||||
toxiproxy.start();
|
||||
|
||||
final ToxiproxyClient toxiproxyClient = new ToxiproxyClient(toxiproxy.getHost(), toxiproxy.getControlPort());
|
||||
try {
|
||||
proxy = toxiproxyClient.createProxy("everything-server", "0.0.0.0:3000", "everything-server:3001");
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new RuntimeException("Can't create proxy!", e);
|
||||
}
|
||||
|
||||
final String ipAddressViaToxiproxy = toxiproxy.getHost();
|
||||
final int portViaToxiproxy = toxiproxy.getMappedPort(3000);
|
||||
|
||||
host = "http://" + ipAddressViaToxiproxy + ":" + portViaToxiproxy;
|
||||
}
|
||||
|
||||
private static void disconnect() {
|
||||
long start = System.nanoTime();
|
||||
try {
|
||||
// disconnect
|
||||
// proxy.toxics().bandwidth("CUT_CONNECTION_DOWNSTREAM",
|
||||
// ToxicDirection.DOWNSTREAM, 0);
|
||||
// proxy.toxics().bandwidth("CUT_CONNECTION_UPSTREAM",
|
||||
// ToxicDirection.UPSTREAM, 0);
|
||||
proxy.toxics().resetPeer("RESET_DOWNSTREAM", ToxicDirection.DOWNSTREAM, 0);
|
||||
proxy.toxics().resetPeer("RESET_UPSTREAM", ToxicDirection.UPSTREAM, 0);
|
||||
logger.info("Disconnect took {} ms", Duration.ofNanos(System.nanoTime() - start).toMillis());
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new RuntimeException("Failed to disconnect", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void reconnect() {
|
||||
long start = System.nanoTime();
|
||||
try {
|
||||
proxy.toxics().get("RESET_UPSTREAM").remove();
|
||||
proxy.toxics().get("RESET_DOWNSTREAM").remove();
|
||||
// proxy.toxics().get("CUT_CONNECTION_DOWNSTREAM").remove();
|
||||
// proxy.toxics().get("CUT_CONNECTION_UPSTREAM").remove();
|
||||
logger.info("Reconnect took {} ms", Duration.ofNanos(System.nanoTime() - start).toMillis());
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new RuntimeException("Failed to reconnect", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void restartMcpServer() {
|
||||
container.stop();
|
||||
container.start();
|
||||
}
|
||||
|
||||
abstract McpClientTransport createMcpTransport();
|
||||
|
||||
protected Duration getRequestTimeout() {
|
||||
return Duration.ofSeconds(14);
|
||||
}
|
||||
|
||||
protected Duration getInitializationTimeout() {
|
||||
return Duration.ofSeconds(2);
|
||||
}
|
||||
|
||||
McpAsyncClient client(McpClientTransport transport) {
|
||||
return client(transport, Function.identity());
|
||||
}
|
||||
|
||||
McpAsyncClient client(McpClientTransport transport, Function<McpClient.AsyncSpec, McpClient.AsyncSpec> customizer) {
|
||||
AtomicReference<McpAsyncClient> client = new AtomicReference<>();
|
||||
|
||||
assertThatCode(() -> {
|
||||
McpClient.AsyncSpec builder = McpClient.async(transport)
|
||||
.requestTimeout(getRequestTimeout())
|
||||
.initializationTimeout(getInitializationTimeout())
|
||||
.capabilities(McpSchema.ClientCapabilities.builder().roots(true).build());
|
||||
builder = customizer.apply(builder);
|
||||
client.set(builder.build());
|
||||
}).doesNotThrowAnyException();
|
||||
|
||||
return client.get();
|
||||
}
|
||||
|
||||
void withClient(McpClientTransport transport, Consumer<McpAsyncClient> c) {
|
||||
withClient(transport, Function.identity(), c);
|
||||
}
|
||||
|
||||
void withClient(McpClientTransport transport, Function<McpClient.AsyncSpec, McpClient.AsyncSpec> customizer,
|
||||
Consumer<McpAsyncClient> c) {
|
||||
var client = client(transport, customizer);
|
||||
try {
|
||||
c.accept(client);
|
||||
}
|
||||
finally {
|
||||
StepVerifier.create(client.closeGracefully()).expectComplete().verify(Duration.ofSeconds(10));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPing() {
|
||||
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||
StepVerifier.create(mcpAsyncClient.initialize()).expectNextCount(1).verifyComplete();
|
||||
|
||||
disconnect();
|
||||
|
||||
StepVerifier.create(mcpAsyncClient.ping()).expectError().verify();
|
||||
|
||||
reconnect();
|
||||
|
||||
StepVerifier.create(mcpAsyncClient.ping()).expectNextCount(1).verifyComplete();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSessionInvalidation() {
|
||||
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||
StepVerifier.create(mcpAsyncClient.initialize()).expectNextCount(1).verifyComplete();
|
||||
|
||||
restartMcpServer();
|
||||
|
||||
// The first try will face the session mismatch exception and the second one
|
||||
// will go through the re-initialization process.
|
||||
StepVerifier.create(mcpAsyncClient.ping().retry(1)).expectNextCount(1).verifyComplete();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCallTool() {
|
||||
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||
AtomicReference<List<McpSchema.Tool>> tools = new AtomicReference<>();
|
||||
StepVerifier.create(mcpAsyncClient.initialize()).expectNextCount(1).verifyComplete();
|
||||
StepVerifier.create(mcpAsyncClient.listTools())
|
||||
.consumeNextWith(list -> tools.set(list.tools()))
|
||||
.verifyComplete();
|
||||
|
||||
disconnect();
|
||||
|
||||
String name = tools.get().get(0).name();
|
||||
// Assuming this is the echo tool
|
||||
McpSchema.CallToolRequest request = new McpSchema.CallToolRequest(name, Map.of("message", "hello"));
|
||||
StepVerifier.create(mcpAsyncClient.callTool(request)).expectError().verify();
|
||||
|
||||
reconnect();
|
||||
|
||||
StepVerifier.create(mcpAsyncClient.callTool(request)).expectNextCount(1).verifyComplete();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSessionClose() {
|
||||
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||
StepVerifier.create(mcpAsyncClient.initialize()).expectNextCount(1).verifyComplete();
|
||||
// In case of Streamable HTTP this call should issue a HTTP DELETE request
|
||||
// invalidating the session
|
||||
StepVerifier.create(mcpAsyncClient.closeGracefully()).expectComplete().verify();
|
||||
// The next use should immediately re-initialize with no issue and send the
|
||||
// request without any broken connections.
|
||||
StepVerifier.create(mcpAsyncClient.ping()).expectNextCount(1).verifyComplete();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -110,14 +110,16 @@ public abstract class AbstractMcpAsyncClientTests {
|
|||
onClose();
|
||||
}
|
||||
|
||||
<T> void verifyInitializationTimeout(Function<McpAsyncClient, Mono<T>> operation, String action) {
|
||||
<T> void verifyNotificationSucceedsWithImplicitInitialization(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();
|
||||
StepVerifier.create(operation.apply(mcpAsyncClient)).verifyComplete();
|
||||
});
|
||||
}
|
||||
|
||||
<T> void verifyCallSucceedsWithImplicitInitialization(Function<McpAsyncClient, Mono<T>> operation, String action) {
|
||||
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||
StepVerifier.create(operation.apply(mcpAsyncClient)).expectNextCount(1).verifyComplete();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -133,7 +135,7 @@ public abstract class AbstractMcpAsyncClientTests {
|
|||
|
||||
@Test
|
||||
void testListToolsWithoutInitialization() {
|
||||
verifyInitializationTimeout(client -> client.listTools(null), "listing tools");
|
||||
verifyCallSucceedsWithImplicitInitialization(client -> client.listTools(null), "listing tools");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -153,7 +155,7 @@ public abstract class AbstractMcpAsyncClientTests {
|
|||
|
||||
@Test
|
||||
void testPingWithoutInitialization() {
|
||||
verifyInitializationTimeout(client -> client.ping(), "pinging the server");
|
||||
verifyCallSucceedsWithImplicitInitialization(client -> client.ping(), "pinging the server");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -168,7 +170,7 @@ public abstract class AbstractMcpAsyncClientTests {
|
|||
@Test
|
||||
void testCallToolWithoutInitialization() {
|
||||
CallToolRequest callToolRequest = new CallToolRequest("echo", Map.of("message", ECHO_TEST_MESSAGE));
|
||||
verifyInitializationTimeout(client -> client.callTool(callToolRequest), "calling tools");
|
||||
verifyCallSucceedsWithImplicitInitialization(client -> client.callTool(callToolRequest), "calling tools");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -202,7 +204,7 @@ public abstract class AbstractMcpAsyncClientTests {
|
|||
|
||||
@Test
|
||||
void testListResourcesWithoutInitialization() {
|
||||
verifyInitializationTimeout(client -> client.listResources(null), "listing resources");
|
||||
verifyCallSucceedsWithImplicitInitialization(client -> client.listResources(null), "listing resources");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -233,7 +235,7 @@ public abstract class AbstractMcpAsyncClientTests {
|
|||
|
||||
@Test
|
||||
void testListPromptsWithoutInitialization() {
|
||||
verifyInitializationTimeout(client -> client.listPrompts(null), "listing " + "prompts");
|
||||
verifyCallSucceedsWithImplicitInitialization(client -> client.listPrompts(null), "listing " + "prompts");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -258,7 +260,7 @@ public abstract class AbstractMcpAsyncClientTests {
|
|||
@Test
|
||||
void testGetPromptWithoutInitialization() {
|
||||
GetPromptRequest request = new GetPromptRequest("simple_prompt", Map.of());
|
||||
verifyInitializationTimeout(client -> client.getPrompt(request), "getting " + "prompts");
|
||||
verifyCallSucceedsWithImplicitInitialization(client -> client.getPrompt(request), "getting " + "prompts");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -279,7 +281,7 @@ public abstract class AbstractMcpAsyncClientTests {
|
|||
|
||||
@Test
|
||||
void testRootsListChangedWithoutInitialization() {
|
||||
verifyInitializationTimeout(client -> client.rootsListChangedNotification(),
|
||||
verifyNotificationSucceedsWithImplicitInitialization(client -> client.rootsListChangedNotification(),
|
||||
"sending roots list changed notification");
|
||||
}
|
||||
|
||||
|
|
@ -354,7 +356,8 @@ public abstract class AbstractMcpAsyncClientTests {
|
|||
|
||||
@Test
|
||||
void testListResourceTemplatesWithoutInitialization() {
|
||||
verifyInitializationTimeout(client -> client.listResourceTemplates(), "listing resource templates");
|
||||
verifyCallSucceedsWithImplicitInitialization(client -> client.listResourceTemplates(),
|
||||
"listing resource templates");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -447,8 +450,8 @@ public abstract class AbstractMcpAsyncClientTests {
|
|||
|
||||
@Test
|
||||
void testLoggingLevelsWithoutInitialization() {
|
||||
verifyInitializationTimeout(client -> client.setLoggingLevel(McpSchema.LoggingLevel.DEBUG),
|
||||
"setting logging level");
|
||||
verifyNotificationSucceedsWithImplicitInitialization(
|
||||
client -> client.setLoggingLevel(McpSchema.LoggingLevel.DEBUG), "setting logging level");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
package io.modelcontextprotocol.client;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
|
@ -12,7 +13,6 @@ import java.util.function.Consumer;
|
|||
import java.util.function.Function;
|
||||
|
||||
import io.modelcontextprotocol.spec.McpClientTransport;
|
||||
import io.modelcontextprotocol.spec.McpError;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
|
||||
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
|
||||
|
|
@ -112,33 +112,18 @@ public abstract class AbstractMcpSyncClientTests {
|
|||
|
||||
static final Object DUMMY_RETURN_VALUE = new Object();
|
||||
|
||||
<T> void verifyNotificationTimesOut(Consumer<McpSyncClient> operation, String action) {
|
||||
verifyCallTimesOut(client -> {
|
||||
<T> void verifyNotificationSucceedsWithImplicitInitialization(Consumer<McpSyncClient> operation, String action) {
|
||||
verifyCallSucceedsWithImplicitInitialization(client -> {
|
||||
operation.accept(client);
|
||||
return DUMMY_RETURN_VALUE;
|
||||
}, action);
|
||||
}
|
||||
|
||||
<T> void verifyCallTimesOut(Function<McpSyncClient, T> blockingOperation, String action) {
|
||||
<T> void verifyCallSucceedsWithImplicitInitialization(Function<McpSyncClient, T> blockingOperation, 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(() -> blockingOperation.apply(mcpSyncClient))
|
||||
// Offload the blocking call to the real scheduler
|
||||
.subscribeOn(customScheduler))
|
||||
.expectSubscription()
|
||||
// This works without actually waiting but executes all the
|
||||
// tasks pending execution on the VirtualTimeScheduler.
|
||||
// It is possible to execute the blocking code from the operation
|
||||
// because it is blocked on a dedicated Scheduler and the main
|
||||
// flow is not blocked and uses the VirtualTimeScheduler.
|
||||
.thenAwait(getInitializationTimeout())
|
||||
.consumeErrorWith(e -> assertThat(e).isInstanceOf(McpError.class)
|
||||
.hasMessage("Client must be initialized before " + action))
|
||||
.verify();
|
||||
|
||||
customScheduler.dispose();
|
||||
StepVerifier.create(Mono.fromSupplier(() -> blockingOperation.apply(mcpSyncClient)))
|
||||
.expectNextCount(1)
|
||||
.verifyComplete();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -154,7 +139,7 @@ public abstract class AbstractMcpSyncClientTests {
|
|||
|
||||
@Test
|
||||
void testListToolsWithoutInitialization() {
|
||||
verifyCallTimesOut(client -> client.listTools(null), "listing tools");
|
||||
verifyCallSucceedsWithImplicitInitialization(client -> client.listTools(null), "listing tools");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -175,8 +160,8 @@ public abstract class AbstractMcpSyncClientTests {
|
|||
|
||||
@Test
|
||||
void testCallToolsWithoutInitialization() {
|
||||
verifyCallTimesOut(client -> client.callTool(new CallToolRequest("add", Map.of("a", 3, "b", 4))),
|
||||
"calling tools");
|
||||
verifyCallSucceedsWithImplicitInitialization(
|
||||
client -> client.callTool(new CallToolRequest("add", Map.of("a", 3, "b", 4))), "calling tools");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -200,7 +185,7 @@ public abstract class AbstractMcpSyncClientTests {
|
|||
|
||||
@Test
|
||||
void testPingWithoutInitialization() {
|
||||
verifyCallTimesOut(client -> client.ping(), "pinging the server");
|
||||
verifyCallSucceedsWithImplicitInitialization(client -> client.ping(), "pinging the server");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -214,7 +199,7 @@ public abstract class AbstractMcpSyncClientTests {
|
|||
@Test
|
||||
void testCallToolWithoutInitialization() {
|
||||
CallToolRequest callToolRequest = new CallToolRequest("echo", Map.of("message", TEST_MESSAGE));
|
||||
verifyCallTimesOut(client -> client.callTool(callToolRequest), "calling tools");
|
||||
verifyCallSucceedsWithImplicitInitialization(client -> client.callTool(callToolRequest), "calling tools");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -243,7 +228,7 @@ public abstract class AbstractMcpSyncClientTests {
|
|||
|
||||
@Test
|
||||
void testRootsListChangedWithoutInitialization() {
|
||||
verifyNotificationTimesOut(client -> client.rootsListChangedNotification(),
|
||||
verifyNotificationSucceedsWithImplicitInitialization(client -> client.rootsListChangedNotification(),
|
||||
"sending roots list changed notification");
|
||||
}
|
||||
|
||||
|
|
@ -257,7 +242,7 @@ public abstract class AbstractMcpSyncClientTests {
|
|||
|
||||
@Test
|
||||
void testListResourcesWithoutInitialization() {
|
||||
verifyCallTimesOut(client -> client.listResources(null), "listing resources");
|
||||
verifyCallSucceedsWithImplicitInitialization(client -> client.listResources(null), "listing resources");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -333,8 +318,14 @@ public abstract class AbstractMcpSyncClientTests {
|
|||
|
||||
@Test
|
||||
void testReadResourceWithoutInitialization() {
|
||||
Resource resource = new Resource("test://uri", "Test Resource", null, null, null);
|
||||
verifyCallTimesOut(client -> client.readResource(resource), "reading resources");
|
||||
AtomicReference<List<Resource>> resources = new AtomicReference<>();
|
||||
withClient(createMcpTransport(), mcpSyncClient -> {
|
||||
mcpSyncClient.initialize();
|
||||
resources.set(mcpSyncClient.listResources().resources());
|
||||
});
|
||||
|
||||
verifyCallSucceedsWithImplicitInitialization(client -> client.readResource(resources.get().get(0)),
|
||||
"reading resources");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -355,7 +346,8 @@ public abstract class AbstractMcpSyncClientTests {
|
|||
|
||||
@Test
|
||||
void testListResourceTemplatesWithoutInitialization() {
|
||||
verifyCallTimesOut(client -> client.listResourceTemplates(null), "listing resource templates");
|
||||
verifyCallSucceedsWithImplicitInitialization(client -> client.listResourceTemplates(null),
|
||||
"listing resource templates");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -413,8 +405,8 @@ public abstract class AbstractMcpSyncClientTests {
|
|||
|
||||
@Test
|
||||
void testLoggingLevelsWithoutInitialization() {
|
||||
verifyNotificationTimesOut(client -> client.setLoggingLevel(McpSchema.LoggingLevel.DEBUG),
|
||||
"setting logging level");
|
||||
verifyNotificationSucceedsWithImplicitInitialization(
|
||||
client -> client.setLoggingLevel(McpSchema.LoggingLevel.DEBUG), "setting logging level");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
Loading…
Reference in a new issue