From baa1d8939e09d8fb67349040070276533f527fb3 Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Wed, 9 Apr 2025 11:15:16 +0200 Subject: [PATCH 1/8] feat(mcp): refactor logging to use exchange for targeted client notifications Refactors the MCP logging system to use the exchange mechanism for sending logging notifications only to specific client sessions rather than broadcasting to all clients. - Move logging notification delivery from server-wide broadcast to per-session exchange - Implement per-session minimum logging level tracking and filtering - Change setLoggingLevel from notification to request/response pattern - Deprecate global server.loggingNotification in favor of exchange.loggingNotification - Add integration test demonstrating filtered logging notifications Resolves #131 Signed-off-by: Christian Tzolov --- .../WebFluxSseIntegrationTests.java | 128 ++++++++++++++++-- .../client/AbstractMcpAsyncClientTests.java | 6 +- .../server/AbstractMcpAsyncServerTests.java | 49 ------- .../server/AbstractMcpSyncServerTests.java | 49 ------- .../client/McpAsyncClient.java | 7 +- .../client/McpSyncClient.java | 4 +- .../server/McpAsyncServer.java | 33 ++++- .../server/McpAsyncServerExchange.java | 34 +++++ .../server/McpSyncServer.java | 11 -- .../server/McpSyncServerExchange.java | 23 +++- .../modelcontextprotocol/spec/McpSchema.java | 5 + .../spec/McpServerSession.java | 18 +++ .../client/AbstractMcpAsyncClientTests.java | 6 +- .../server/AbstractMcpAsyncServerTests.java | 49 ------- .../server/AbstractMcpSyncServerTests.java | 49 ------- ...rverTransportProviderIntegrationTests.java | 118 +++++++++++++++- 16 files changed, 353 insertions(+), 236 deletions(-) diff --git a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java index ac487b6f..a43e676f 100644 --- a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java +++ b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java @@ -4,6 +4,7 @@ package io.modelcontextprotocol; import java.time.Duration; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -12,6 +13,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.client.McpClient; +import io.modelcontextprotocol.client.McpSyncClient; import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport; import io.modelcontextprotocol.client.transport.WebFluxSseClientTransport; import io.modelcontextprotocol.server.McpServer; @@ -111,7 +113,7 @@ void testCreateMessageWithoutSamplingCapabilities(String clientType) { return Mono.just(mock(CallToolResult.class)); }); - McpServer.async(mcpServerTransportProvider).serverInfo("test-server", "1.0.0").tools(tool).build(); + var server = McpServer.async(mcpServerTransportProvider).serverInfo("test-server", "1.0.0").tools(tool).build(); // Create client without sampling capabilities var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample " + "client", "0.0.0")).build(); @@ -125,6 +127,9 @@ void testCreateMessageWithoutSamplingCapabilities(String clientType) { assertThat(e).isInstanceOf(McpError.class) .hasMessage("Client must be configured with sampling capabilities"); } + + server.close(); + client.close(); } @ParameterizedTest(name = "{0} : {displayName} ") @@ -293,8 +298,7 @@ void testRootsNotifciationWithEmptyRootsList(String clientType) { .roots(List.of()) // Empty roots list .build(); - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); + assertThat(mcpClient.initialize()).isNotNull(); mcpClient.rootsListChangedNotification(); @@ -309,6 +313,7 @@ void testRootsNotifciationWithEmptyRootsList(String clientType) { @ParameterizedTest(name = "{0} : {displayName} ") @ValueSource(strings = { "httpclient", "webflux" }) void testRootsWithMultipleHandlers(String clientType) { + var clientBuilder = clientBuilders.get(clientType); List roots = List.of(new Root("uri1://", "root1")); @@ -339,8 +344,8 @@ void testRootsWithMultipleHandlers(String clientType) { mcpServer.close(); } - @ParameterizedTest(name = "{0} : {displayName} ") - @ValueSource(strings = { "httpclient", "webflux" }) + // @ParameterizedTest(name = "{0} : {displayName} ") + // @ValueSource(strings = { "httpclient", "webflux" }) void testRootsServerCloseWithActiveSubscription(String clientType) { var clientBuilder = clientBuilders.get(clientType); @@ -365,10 +370,7 @@ void testRootsServerCloseWithActiveSubscription(String clientType) { assertThat(rootsRef.get()).containsAll(roots); }); - // Close server while subscription is active mcpServer.close(); - - // Verify client can handle server closure gracefully mcpClient.close(); } @@ -378,9 +380,9 @@ void testRootsServerCloseWithActiveSubscription(String clientType) { String emptyJsonSchema = """ { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": {} + "": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": {} } """; @@ -511,4 +513,108 @@ void testInitialize(String clientType) { mcpServer.close(); } + // --------------------------------------- + // Logging Tests + // --------------------------------------- + + @ParameterizedTest(name = "{0} : {displayName} ") + @ValueSource(strings = { "httpclient", "webflux" }) + void testLoggingNotification(String clientType) { + // Create a list to store received logging notifications + List receivedNotifications = new ArrayList<>(); + + var clientBuilder = clientBuilders.get(clientType); + + // Create server with a tool that sends logging notifications + McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification( + new McpSchema.Tool("logging-test", "Test logging notifications", emptyJsonSchema), + (exchange, request) -> { + + // Create and send notifications with different levels + + //@formatter:off + return exchange // This should be filtered out (DEBUG < NOTICE) + .loggingNotification(McpSchema.LoggingMessageNotification.builder() + .level(McpSchema.LoggingLevel.DEBUG) + .logger("test-logger") + .data("Debug message") + .build()) + .then(exchange // This should be sent (NOTICE >= NOTICE) + .loggingNotification(McpSchema.LoggingMessageNotification.builder() + .level(McpSchema.LoggingLevel.NOTICE) + .logger("test-logger") + .data("Notice message") + .build())) + .then(exchange // This should be sent (ERROR > NOTICE) + .loggingNotification(McpSchema.LoggingMessageNotification.builder() + .level(McpSchema.LoggingLevel.ERROR) + .logger("test-logger") + .data("Error message") + .build())) + .then(exchange // This should be filtered out (INFO < NOTICE) + .loggingNotification(McpSchema.LoggingMessageNotification.builder() + .level(McpSchema.LoggingLevel.INFO) + .logger("test-logger") + .data("Another info message") + .build())) + .then(exchange // This should be sent (ERROR >= NOTICE) + .loggingNotification(McpSchema.LoggingMessageNotification.builder() + .level(McpSchema.LoggingLevel.ERROR) + .logger("test-logger") + .data("Another error message") + .build())) + .thenReturn(new CallToolResult("Logging test completed", false)); + //@formatter:on + }); + + var mcpServer = McpServer.async(mcpServerTransportProvider) + .serverInfo("test-server", "1.0.0") + .capabilities(ServerCapabilities.builder().logging().tools(true).build()) + .tools(tool) + .build(); + + // Create client with logging notification handler + McpSyncClient mcpClient = clientBuilder.loggingConsumer(notification -> { + receivedNotifications.add(notification); + }).build(); + + // Initialize client + InitializeResult initResult = mcpClient.initialize(); + assertThat(initResult).isNotNull(); + + // Set minimum logging level to NOTICE + mcpClient.setLoggingLevel(McpSchema.LoggingLevel.NOTICE); + + // Call the tool that sends logging notifications + CallToolResult result = mcpClient.callTool(new McpSchema.CallToolRequest("logging-test", Map.of())); + assertThat(result).isNotNull(); + assertThat(result.content().get(0)).isInstanceOf(McpSchema.TextContent.class); + assertThat(((McpSchema.TextContent) result.content().get(0)).text()).isEqualTo("Logging test completed"); + + // Wait for notifications to be processed + await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { + + // Should have received 3 notifications (1 NOTICE and 2 ERROR) + assertThat(receivedNotifications).hasSize(3); + + // First notification should be NOTICE level + assertThat(receivedNotifications.get(0).level()).isEqualTo(McpSchema.LoggingLevel.NOTICE); + assertThat(receivedNotifications.get(0).logger()).isEqualTo("test-logger"); + assertThat(receivedNotifications.get(0).data()).isEqualTo("Notice message"); + + // Second notification should be ERROR level + assertThat(receivedNotifications.get(1).level()).isEqualTo(McpSchema.LoggingLevel.ERROR); + assertThat(receivedNotifications.get(1).logger()).isEqualTo("test-logger"); + assertThat(receivedNotifications.get(1).data()).isEqualTo("Error message"); + + // Third notification should be ERROR level + assertThat(receivedNotifications.get(2).level()).isEqualTo(McpSchema.LoggingLevel.ERROR); + assertThat(receivedNotifications.get(2).logger()).isEqualTo("test-logger"); + assertThat(receivedNotifications.get(2).data()).isEqualTo("Another error message"); + }); + + mcpClient.close(); + mcpServer.close(); + } + } diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java b/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java index 71356351..c7892b17 100644 --- a/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java +++ b/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java @@ -453,15 +453,15 @@ void testLoggingLevelsWithoutInitialization() { @Test void testLoggingLevels() { withClient(createMcpTransport(), mcpAsyncClient -> { - Mono testAllLevels = mcpAsyncClient.initialize().then(Mono.defer(() -> { - Mono chain = Mono.empty(); + Mono testAllLevels = mcpAsyncClient.initialize().then(Mono.defer(() -> { + Mono chain = Mono.empty(); for (McpSchema.LoggingLevel level : McpSchema.LoggingLevel.values()) { chain = chain.then(mcpAsyncClient.setLoggingLevel(level)); } return chain; })); - StepVerifier.create(testAllLevels).verifyComplete(); + StepVerifier.create(testAllLevels).expectNextCount(1).verifyComplete(); }); } diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java b/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java index 7bcb9a8b..a91632c6 100644 --- a/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java +++ b/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java @@ -416,53 +416,4 @@ void testRootsChangeHandlers() { .doesNotThrowAnyException(); } - // --------------------------------------- - // Logging Tests - // --------------------------------------- - - @Test - void testLoggingLevels() { - var mcpAsyncServer = McpServer.async(createMcpTransportProvider()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().logging().build()) - .build(); - - // Test all logging levels - for (McpSchema.LoggingLevel level : McpSchema.LoggingLevel.values()) { - var notification = McpSchema.LoggingMessageNotification.builder() - .level(level) - .logger("test-logger") - .data("Test message with level " + level) - .build(); - - StepVerifier.create(mcpAsyncServer.loggingNotification(notification)).verifyComplete(); - } - } - - @Test - void testLoggingWithoutCapability() { - var mcpAsyncServer = McpServer.async(createMcpTransportProvider()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().build()) // No logging capability - .build(); - - var notification = McpSchema.LoggingMessageNotification.builder() - .level(McpSchema.LoggingLevel.INFO) - .logger("test-logger") - .data("Test log message") - .build(); - - StepVerifier.create(mcpAsyncServer.loggingNotification(notification)).verifyComplete(); - } - - @Test - void testLoggingWithNullNotification() { - var mcpAsyncServer = McpServer.async(createMcpTransportProvider()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().logging().build()) - .build(); - - StepVerifier.create(mcpAsyncServer.loggingNotification(null)).verifyError(McpError.class); - } - } diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java b/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java index 7846e053..9a63143c 100644 --- a/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java +++ b/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java @@ -388,53 +388,4 @@ void testRootsChangeHandlers() { assertThatCode(() -> noConsumersServer.closeGracefully()).doesNotThrowAnyException(); } - // --------------------------------------- - // Logging Tests - // --------------------------------------- - - @Test - void testLoggingLevels() { - var mcpSyncServer = McpServer.sync(createMcpTransportProvider()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().logging().build()) - .build(); - - // Test all logging levels - for (McpSchema.LoggingLevel level : McpSchema.LoggingLevel.values()) { - var notification = McpSchema.LoggingMessageNotification.builder() - .level(level) - .logger("test-logger") - .data("Test message with level " + level) - .build(); - - assertThatCode(() -> mcpSyncServer.loggingNotification(notification)).doesNotThrowAnyException(); - } - } - - @Test - void testLoggingWithoutCapability() { - var mcpSyncServer = McpServer.sync(createMcpTransportProvider()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().build()) // No logging capability - .build(); - - var notification = McpSchema.LoggingMessageNotification.builder() - .level(McpSchema.LoggingLevel.INFO) - .logger("test-logger") - .data("Test log message") - .build(); - - assertThatCode(() -> mcpSyncServer.loggingNotification(notification)).doesNotThrowAnyException(); - } - - @Test - void testLoggingWithNullNotification() { - var mcpSyncServer = McpServer.sync(createMcpTransportProvider()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().logging().build()) - .build(); - - assertThatThrownBy(() -> mcpSyncServer.loggingNotification(null)).isInstanceOf(McpError.class); - } - } diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java index ce49b0a5..81580b93 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java +++ b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java @@ -780,16 +780,15 @@ private NotificationHandler asyncLoggingNotificationHandler( * @return A Mono that completes when the logging level is set. * @see McpSchema.LoggingLevel */ - public Mono setLoggingLevel(LoggingLevel loggingLevel) { + public Mono setLoggingLevel(LoggingLevel loggingLevel) { if (loggingLevel == null) { return Mono.error(new McpError("Logging level must not be null")); } return this.withInitializationCheck("setting logging level", initializedResult -> { - String levelName = this.transport.unmarshalFrom(loggingLevel, new TypeReference() { + var params = new McpSchema.SetLevelRequest(loggingLevel); + return this.mcpSession.sendRequest(McpSchema.METHOD_LOGGING_SET_LEVEL, params, new TypeReference() { }); - Map params = Map.of("level", levelName); - return this.mcpSession.sendNotification(McpSchema.METHOD_LOGGING_SET_LEVEL, params); }); } diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java b/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java index 071d7646..ba911fc4 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java +++ b/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java @@ -322,8 +322,8 @@ public GetPromptResult getPrompt(GetPromptRequest getPromptRequest) { * Client can set the minimum logging level it wants to receive from the server. * @param loggingLevel the min logging level */ - public void setLoggingLevel(McpSchema.LoggingLevel loggingLevel) { - this.delegate.setLoggingLevel(loggingLevel).block(); + public Object setLoggingLevel(McpSchema.LoggingLevel loggingLevel) { + return this.delegate.setLoggingLevel(loggingLevel).block(); } } diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java index ec2a04c9..e761f8fc 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java @@ -21,6 +21,7 @@ import io.modelcontextprotocol.spec.McpSchema.CallToolResult; import io.modelcontextprotocol.spec.McpSchema.LoggingLevel; import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification; +import io.modelcontextprotocol.spec.McpSchema.SetLevelRequest; import io.modelcontextprotocol.spec.McpSchema.Tool; import io.modelcontextprotocol.spec.McpServerSession; import io.modelcontextprotocol.spec.McpServerTransportProvider; @@ -257,6 +258,9 @@ private static class AsyncServerImpl extends McpAsyncServer { private final ConcurrentHashMap prompts = new ConcurrentHashMap<>(); + // TODO: this field is deprecated and should be remvoed together with the + // broadcasting loggingNotification. + @Deprecated private LoggingLevel minLoggingLevel = LoggingLevel.DEBUG; private List protocolVersions = List.of(McpSchema.LATEST_PROTOCOL_VERSION); @@ -662,7 +666,17 @@ private McpServerSession.RequestHandler promptsGetReq // Logging Management // --------------------------------------- + /** + * This implementation would, incorrectly, broadcast the logging message to all + * connected clients, using a single minLoggingLevel for all of them. Similar to + * the sampling and roots, the logging level should be set per client session and + * use the ServerExchange to send the logging message to the right client. + * @deprecated Use + * {@link McpAsyncServerExchange#loggingNotification(LoggingMessageNotification)} + * instead. + */ @Override + @Deprecated public Mono loggingNotification(LoggingMessageNotification loggingMessageNotification) { if (loggingMessageNotification == null) { @@ -677,12 +691,23 @@ public Mono loggingNotification(LoggingMessageNotification loggingMessageN loggingMessageNotification); } - private McpServerSession.RequestHandler setLoggerRequestHandler() { + private McpServerSession.RequestHandler setLoggerRequestHandler() { return (exchange, params) -> { - this.minLoggingLevel = objectMapper.convertValue(params, new TypeReference() { - }); + return Mono.defer(() -> { - return Mono.empty(); + SetLevelRequest newMinLoggingLevel = objectMapper.convertValue(params, + new TypeReference() { + }); + + exchange.setMinLoggingLevel(newMinLoggingLevel.level()); + + // TODO: this field is deprecated and should be remvoed together with + // the + // broadcasting loggingNotification. + this.minLoggingLevel = newMinLoggingLevel.level(); + + return Mono.just(Map.of()); + }); }; } diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java index 65862844..fee84c96 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java @@ -1,9 +1,16 @@ +/* + * Copyright 2024-2024 the original author or authors. + */ + package io.modelcontextprotocol.server; import com.fasterxml.jackson.core.type.TypeReference; import io.modelcontextprotocol.spec.McpError; import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.spec.McpSchema.LoggingLevel; +import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification; import io.modelcontextprotocol.spec.McpServerSession; +import io.modelcontextprotocol.util.Assert; import reactor.core.publisher.Mono; /** @@ -11,6 +18,7 @@ * exchange provides methods to interact with the client and query its capabilities. * * @author Dariusz Jędrzejczyk + * @author Christian Tzolov */ public class McpAsyncServerExchange { @@ -101,4 +109,30 @@ public Mono listRoots(String cursor) { LIST_ROOTS_RESULT_TYPE_REF); } + /** + * Send a logging message notification to all connected clients. Messages below the + * current minimum logging level will be filtered out. + * @param loggingMessageNotification The logging message to send + * @return A Mono that completes when the notification has been sent + */ + public Mono loggingNotification(LoggingMessageNotification loggingMessageNotification) { + + if (loggingMessageNotification == null) { + return Mono.error(new McpError("Logging message must not be null")); + } + + return Mono.defer(() -> { + if (loggingMessageNotification.level().level() < this.session.getMinLoggingLevel().level()) { + return Mono.empty(); + } + + return this.session.sendNotification(McpSchema.METHOD_NOTIFICATION_MESSAGE, loggingMessageNotification); + }); + } + + public void setMinLoggingLevel(LoggingLevel minLoggingLevel) { + Assert.notNull(minLoggingLevel, "minLoggingLevel must not be null"); + this.session.setMinLoggingLevel(minLoggingLevel); + } + } diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServer.java index 72eba8b8..2ee0cc75 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServer.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServer.java @@ -4,10 +4,7 @@ package io.modelcontextprotocol.server; -import io.modelcontextprotocol.spec.McpError; import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities; -import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification; import io.modelcontextprotocol.util.Assert; /** @@ -150,14 +147,6 @@ public void notifyPromptsListChanged() { this.asyncServer.notifyPromptsListChanged().block(); } - /** - * Send a logging message notification to all clients. - * @param loggingMessageNotification The logging message notification to send - */ - public void loggingNotification(LoggingMessageNotification loggingMessageNotification) { - this.asyncServer.loggingNotification(loggingMessageNotification).block(); - } - /** * Close the server gracefully. */ diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServerExchange.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServerExchange.java index f121db55..406ac13a 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServerExchange.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServerExchange.java @@ -1,13 +1,20 @@ +/* + * Copyright 2024-2024 the original author or authors. + */ + package io.modelcontextprotocol.server; -import com.fasterxml.jackson.core.type.TypeReference; import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.spec.McpSchema.LoggingLevel; +import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification; +import reactor.core.publisher.Mono; /** * Represents a synchronous exchange with a Model Context Protocol (MCP) client. The * exchange provides methods to interact with the client and query its capabilities. * * @author Dariusz Jędrzejczyk + * @author Christian Tzolov */ public class McpSyncServerExchange { @@ -75,4 +82,18 @@ public McpSchema.ListRootsResult listRoots(String cursor) { return this.exchange.listRoots(cursor).block(); } + /** + * Send a logging message notification to all connected clients. Messages below the + * current minimum logging level will be filtered out. + * @param loggingMessageNotification The logging message to send + * @return A Mono that completes when the notification has been sent + */ + public Mono loggingNotification(LoggingMessageNotification loggingMessageNotification) { + return this.exchange.loggingNotification(loggingMessageNotification); + } + + void setMinLoggingLevel(LoggingLevel minLoggingLevel) { + this.exchange.setMinLoggingLevel(minLoggingLevel); + } + } diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java index 4c596b62..e621ac19 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpSchema.java @@ -1165,6 +1165,11 @@ public int level() { } // @formatter:on + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public record SetLevelRequest(@JsonProperty("level") LoggingLevel level) { + } + // --------------------------- // Autocomplete // --------------------------- diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java index 46014af8..f0abecd4 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java @@ -9,6 +9,7 @@ import com.fasterxml.jackson.core.type.TypeReference; import io.modelcontextprotocol.server.McpAsyncServerExchange; +import io.modelcontextprotocol.spec.McpSchema.LoggingLevel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Mono; @@ -53,6 +54,8 @@ public class McpServerSession implements McpSession { private final AtomicInteger state = new AtomicInteger(STATE_UNINITIALIZED); + private final AtomicReference minLoggingLevel = new AtomicReference<>(LoggingLevel.INFO); + /** * Creates a new server session with the given parameters and the transport to use. * @param id session id @@ -84,6 +87,14 @@ public String getId() { return this.id; } + public LoggingLevel getMinLoggingLevel() { + return this.minLoggingLevel.get(); + } + + public void setMinLoggingLevel(LoggingLevel minLoggingLevel) { + this.minLoggingLevel.set(minLoggingLevel); + } + /** * Called upon successful initialization sequence between the client and the server * with the client capabilities and information. @@ -138,6 +149,13 @@ public Mono sendNotification(String method, Object params) { return this.transport.sendMessage(jsonrpcNotification); } + public Mono sendNotification(String method, Object params) { + McpSchema.JSONRPCNotification jsonrpcNotification = new McpSchema.JSONRPCNotification(McpSchema.JSONRPC_VERSION, + method, this.transport.unmarshalFrom(params, new TypeReference>() { + })); + return this.transport.sendMessage(jsonrpcNotification); + } + /** * Called by the {@link McpServerTransportProvider} once the session is determined. * The purpose of this method is to dispatch the message to an appropriate handler as diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java b/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java index ac7b9e5e..541dadf6 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java @@ -454,15 +454,15 @@ void testLoggingLevelsWithoutInitialization() { @Test void testLoggingLevels() { withClient(createMcpTransport(), mcpAsyncClient -> { - Mono testAllLevels = mcpAsyncClient.initialize().then(Mono.defer(() -> { - Mono chain = Mono.empty(); + Mono testAllLevels = mcpAsyncClient.initialize().then(Mono.defer(() -> { + Mono chain = Mono.empty(); for (McpSchema.LoggingLevel level : McpSchema.LoggingLevel.values()) { chain = chain.then(mcpAsyncClient.setLoggingLevel(level)); } return chain; })); - StepVerifier.create(testAllLevels).verifyComplete(); + StepVerifier.create(testAllLevels).expectNextCount(1).verifyComplete(); }); } diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java index 4b4fc434..c7c69b52 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java @@ -415,53 +415,4 @@ void testRootsChangeHandlers() { .doesNotThrowAnyException(); } - // --------------------------------------- - // Logging Tests - // --------------------------------------- - - @Test - void testLoggingLevels() { - var mcpAsyncServer = McpServer.async(createMcpTransportProvider()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().logging().build()) - .build(); - - // Test all logging levels - for (McpSchema.LoggingLevel level : McpSchema.LoggingLevel.values()) { - var notification = McpSchema.LoggingMessageNotification.builder() - .level(level) - .logger("test-logger") - .data("Test message with level " + level) - .build(); - - StepVerifier.create(mcpAsyncServer.loggingNotification(notification)).verifyComplete(); - } - } - - @Test - void testLoggingWithoutCapability() { - var mcpAsyncServer = McpServer.async(createMcpTransportProvider()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().build()) // No logging capability - .build(); - - var notification = McpSchema.LoggingMessageNotification.builder() - .level(McpSchema.LoggingLevel.INFO) - .logger("test-logger") - .data("Test log message") - .build(); - - StepVerifier.create(mcpAsyncServer.loggingNotification(notification)).verifyComplete(); - } - - @Test - void testLoggingWithNullNotification() { - var mcpAsyncServer = McpServer.async(createMcpTransportProvider()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().logging().build()) - .build(); - - StepVerifier.create(mcpAsyncServer.loggingNotification(null)).verifyError(McpError.class); - } - } diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java index 17feb36e..8c9328cc 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java @@ -387,53 +387,4 @@ void testRootsChangeHandlers() { assertThatCode(() -> noConsumersServer.closeGracefully()).doesNotThrowAnyException(); } - // --------------------------------------- - // Logging Tests - // --------------------------------------- - - @Test - void testLoggingLevels() { - var mcpSyncServer = McpServer.sync(createMcpTransportProvider()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().logging().build()) - .build(); - - // Test all logging levels - for (McpSchema.LoggingLevel level : McpSchema.LoggingLevel.values()) { - var notification = McpSchema.LoggingMessageNotification.builder() - .level(level) - .logger("test-logger") - .data("Test message with level " + level) - .build(); - - assertThatCode(() -> mcpSyncServer.loggingNotification(notification)).doesNotThrowAnyException(); - } - } - - @Test - void testLoggingWithoutCapability() { - var mcpSyncServer = McpServer.sync(createMcpTransportProvider()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().build()) // No logging capability - .build(); - - var notification = McpSchema.LoggingMessageNotification.builder() - .level(McpSchema.LoggingLevel.INFO) - .logger("test-logger") - .data("Test log message") - .build(); - - assertThatCode(() -> mcpSyncServer.loggingNotification(notification)).doesNotThrowAnyException(); - } - - @Test - void testLoggingWithNullNotification() { - var mcpSyncServer = McpServer.sync(createMcpTransportProvider()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().logging().build()) - .build(); - - assertThatThrownBy(() -> mcpSyncServer.loggingNotification(null)).isInstanceOf(McpError.class); - } - } diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java index e34baf9d..f662389e 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java @@ -4,6 +4,7 @@ package io.modelcontextprotocol.server.transport; import java.time.Duration; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; @@ -44,7 +45,7 @@ public class HttpServletSseServerTransportProviderIntegrationTests { - private static final int PORT = 8185; + private static final int PORT = 8189; private static final String CUSTOM_SSE_ENDPOINT = "/somePath/sse"; @@ -483,4 +484,119 @@ void testInitialize() { mcpServer.close(); } + // --------------------------------------- + // Logging Tests + // --------------------------------------- + @Test + void testLoggingNotification() { + // Create a list to store received logging notifications + List receivedNotifications = new ArrayList<>(); + + // Create server with a tool that sends logging notifications + McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification( + new McpSchema.Tool("logging-test", "Test logging notifications", emptyJsonSchema), + (exchange, request) -> { + + // Create and send notifications with different levels + + // This should be filtered out (DEBUG < NOTICE) + exchange + .loggingNotification(McpSchema.LoggingMessageNotification.builder() + .level(McpSchema.LoggingLevel.DEBUG) + .logger("test-logger") + .data("Debug message") + .build()) + .block(); + + // This should be sent (NOTICE >= NOTICE) + exchange + .loggingNotification(McpSchema.LoggingMessageNotification.builder() + .level(McpSchema.LoggingLevel.NOTICE) + .logger("test-logger") + .data("Notice message") + .build()) + .block(); + + // This should be sent (ERROR > NOTICE) + exchange + .loggingNotification(McpSchema.LoggingMessageNotification.builder() + .level(McpSchema.LoggingLevel.ERROR) + .logger("test-logger") + .data("Error message") + .build()) + .block(); + + // This should be filtered out (INFO < NOTICE) + exchange + .loggingNotification(McpSchema.LoggingMessageNotification.builder() + .level(McpSchema.LoggingLevel.INFO) + .logger("test-logger") + .data("Another info message") + .build()) + .block(); + + // This should be sent (ERROR >= NOTICE) + exchange + .loggingNotification(McpSchema.LoggingMessageNotification.builder() + .level(McpSchema.LoggingLevel.ERROR) + .logger("test-logger") + .data("Another error message") + .build()) + .block(); + + return Mono.just(new CallToolResult("Logging test completed", false)); + }); + + var mcpServer = McpServer.async(mcpServerTransportProvider) + .serverInfo("test-server", "1.0.0") + .capabilities(ServerCapabilities.builder().logging().tools(true).build()) + .tools(tool) + .build(); + + // Create client with logging notification handler + var mcpClient = clientBuilder.loggingConsumer(notification -> { + receivedNotifications.add(notification); + }).build(); + + // Initialize client + InitializeResult initResult = mcpClient.initialize(); + assertThat(initResult).isNotNull(); + + // Set minimum logging level to NOTICE + mcpClient.setLoggingLevel(McpSchema.LoggingLevel.NOTICE); + + // Call the tool that sends logging notifications + CallToolResult result = mcpClient.callTool(new McpSchema.CallToolRequest("logging-test", Map.of())); + assertThat(result).isNotNull(); + assertThat(result.content().get(0)).isInstanceOf(McpSchema.TextContent.class); + assertThat(((McpSchema.TextContent) result.content().get(0)).text()).isEqualTo("Logging test completed"); + + // Wait for notifications to be processed + await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { + + System.out.println("Received notifications: " + receivedNotifications); + + // Should have received 3 notifications (1 NOTICE and 2 ERROR) + assertThat(receivedNotifications).hasSize(3); + + // First notification should be NOTICE level + assertThat(receivedNotifications.get(0).level()).isEqualTo(McpSchema.LoggingLevel.NOTICE); + assertThat(receivedNotifications.get(0).logger()).isEqualTo("test-logger"); + assertThat(receivedNotifications.get(0).data()).isEqualTo("Notice message"); + + // Second notification should be ERROR level + assertThat(receivedNotifications.get(1).level()).isEqualTo(McpSchema.LoggingLevel.ERROR); + assertThat(receivedNotifications.get(1).logger()).isEqualTo("test-logger"); + assertThat(receivedNotifications.get(1).data()).isEqualTo("Error message"); + + // Third notification should be ERROR level + assertThat(receivedNotifications.get(2).level()).isEqualTo(McpSchema.LoggingLevel.ERROR); + assertThat(receivedNotifications.get(2).logger()).isEqualTo("test-logger"); + assertThat(receivedNotifications.get(2).data()).isEqualTo("Another error message"); + }); + + mcpClient.close(); + mcpServer.close(); + } + } From a4c07e77dcbeadc60748401518f093fd05639e95 Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Wed, 9 Apr 2025 14:51:32 +0200 Subject: [PATCH 2/8] Address review comments Signed-off-by: Christian Tzolov --- .../WebFluxSseIntegrationTests.java | 6 ++--- .../client/McpSyncClient.java | 2 +- .../server/McpAsyncServerExchange.java | 7 +++++- .../spec/McpServerSession.java | 22 +++++++++++++++---- 4 files changed, 28 insertions(+), 9 deletions(-) diff --git a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java index a43e676f..96fd9237 100644 --- a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java +++ b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java @@ -344,8 +344,8 @@ void testRootsWithMultipleHandlers(String clientType) { mcpServer.close(); } - // @ParameterizedTest(name = "{0} : {displayName} ") - // @ValueSource(strings = { "httpclient", "webflux" }) + @ParameterizedTest(name = "{0} : {displayName} ") + @ValueSource(strings = { "httpclient", "webflux" }) void testRootsServerCloseWithActiveSubscription(String clientType) { var clientBuilder = clientBuilders.get(clientType); @@ -380,7 +380,7 @@ void testRootsServerCloseWithActiveSubscription(String clientType) { String emptyJsonSchema = """ { - "": "http://json-schema.org/draft-07/schema#", + "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": {} } diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java b/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java index ba911fc4..5748b825 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java +++ b/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java @@ -6,7 +6,6 @@ import java.time.Duration; -import io.modelcontextprotocol.spec.McpClientTransport; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities; import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest; @@ -321,6 +320,7 @@ public GetPromptResult getPrompt(GetPromptRequest getPromptRequest) { /** * Client can set the minimum logging level it wants to receive from the server. * @param loggingLevel the min logging level + * @return empty response */ public Object setLoggingLevel(McpSchema.LoggingLevel loggingLevel) { return this.delegate.setLoggingLevel(loggingLevel).block(); diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java index fee84c96..1e50506c 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java @@ -122,7 +122,7 @@ public Mono loggingNotification(LoggingMessageNotification loggingMessageN } return Mono.defer(() -> { - if (loggingMessageNotification.level().level() < this.session.getMinLoggingLevel().level()) { + if (!this.session.isLoingLevelEnabled(loggingMessageNotification.level())) { return Mono.empty(); } @@ -130,6 +130,11 @@ public Mono loggingNotification(LoggingMessageNotification loggingMessageN }); } + /** + * Set the minimum logging level for the client. Messages below this level will be + * filtered out. + * @param minLoggingLevel The minimum logging level + */ public void setMinLoggingLevel(LoggingLevel minLoggingLevel) { Assert.notNull(minLoggingLevel, "minLoggingLevel must not be null"); this.session.setMinLoggingLevel(minLoggingLevel); diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java index f0abecd4..ecf519bb 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.core.type.TypeReference; import io.modelcontextprotocol.server.McpAsyncServerExchange; import io.modelcontextprotocol.spec.McpSchema.LoggingLevel; +import io.modelcontextprotocol.util.Assert; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Mono; @@ -54,7 +55,7 @@ public class McpServerSession implements McpSession { private final AtomicInteger state = new AtomicInteger(STATE_UNINITIALIZED); - private final AtomicReference minLoggingLevel = new AtomicReference<>(LoggingLevel.INFO); + private volatile LoggingLevel minLoggingLevel = LoggingLevel.INFO; /** * Creates a new server session with the given parameters and the transport to use. @@ -87,12 +88,21 @@ public String getId() { return this.id; } - public LoggingLevel getMinLoggingLevel() { - return this.minLoggingLevel.get(); + /** + * Checks if the logging level bigger or equal to the minimum set logging level. + * @return true if the logging level is enabled, false otherwise + */ + public boolean isLoingLevelEnabled(LoggingLevel loggingLevel) { + return loggingLevel.level() >= this.minLoggingLevel.level(); } + /** + * Set the minimum logging level for this session. + * @param minLoggingLevel the minimum logging level + */ public void setMinLoggingLevel(LoggingLevel minLoggingLevel) { - this.minLoggingLevel.set(minLoggingLevel); + Assert.notNull(minLoggingLevel, "minLoggingLevel can't be null"); + this.minLoggingLevel = minLoggingLevel; } /** @@ -149,6 +159,10 @@ public Mono sendNotification(String method, Object params) { return this.transport.sendMessage(jsonrpcNotification); } + // NOTE: This is a workaround for the fact that the {@link #sendNotification(String, + // Map)} method doesn't accept types like LoggingMessageNotification + // TODO investigate if this method can replace the {@link #sendNotification(String, + // Map)} - Breaking change. public Mono sendNotification(String method, Object params) { McpSchema.JSONRPCNotification jsonrpcNotification = new McpSchema.JSONRPCNotification(McpSchema.JSONRPC_VERSION, method, this.transport.unmarshalFrom(params, new TypeReference>() { From b2b5b19ab69d1270156b0c598777f4e26e6a08f5 Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Thu, 10 Apr 2025 10:26:42 +0200 Subject: [PATCH 3/8] feat: implement AutoCloseable for MCP client and server classes - Implement AutoCloseable interface for McpAsyncClient, McpAsyncServer, and McpSyncServer - Refactor test methods to use try-with-resources for automatic resource cleanup - Add missing Javadoc for McpSyncServerExchange.setMinLoggingLevel - Change visibility of McpAsyncServerExchange.setMinLoggingLevel to package-private Signed-off-by: Christian Tzolov --- .../WebFluxSseIntegrationTests.java | 450 +++++++++-------- .../server/WebMvcSseIntegrationTests.java | 354 +++++++------- .../client/McpAsyncClient.java | 3 +- .../server/McpAsyncServer.java | 3 +- .../server/McpAsyncServerExchange.java | 2 +- .../server/McpSyncServer.java | 3 +- .../server/McpSyncServerExchange.java | 5 + ...ervletSseServerCustomContextPathTests.java | 10 +- ...rverTransportProviderIntegrationTests.java | 455 +++++++++--------- 9 files changed, 643 insertions(+), 642 deletions(-) diff --git a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java index 96fd9237..4e660c20 100644 --- a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java +++ b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java @@ -13,7 +13,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.client.McpClient; -import io.modelcontextprotocol.client.McpSyncClient; import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport; import io.modelcontextprotocol.client.transport.WebFluxSseClientTransport; import io.modelcontextprotocol.server.McpServer; @@ -113,30 +112,33 @@ void testCreateMessageWithoutSamplingCapabilities(String clientType) { return Mono.just(mock(CallToolResult.class)); }); - var server = McpServer.async(mcpServerTransportProvider).serverInfo("test-server", "1.0.0").tools(tool).build(); + try (//@formatter:off + var server = McpServer.async( + mcpServerTransportProvider) + .serverInfo("test-server", "1.0.0") + .tools(tool) + .build(); - // Create client without sampling capabilities - var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample " + "client", "0.0.0")).build(); + var client = clientBuilder + .clientInfo(new McpSchema.Implementation("Sample " + "client", "0.0.0")) + .build();) { - assertThat(client.initialize()).isNotNull(); + assertThat(client.initialize()).isNotNull(); - try { - client.callTool(new McpSchema.CallToolRequest("tool1", Map.of())); - } - catch (McpError e) { - assertThat(e).isInstanceOf(McpError.class) - .hasMessage("Client must be configured with sampling capabilities"); - } - - server.close(); - client.close(); + try { + client.callTool(new McpSchema.CallToolRequest("tool1", Map.of())); + } + catch (McpError e) { + assertThat(e).isInstanceOf(McpError.class) + .hasMessage("Client must be configured with sampling capabilities"); + } + } //@formatter:on } @ParameterizedTest(name = "{0} : {displayName} ") @ValueSource(strings = { "httpclient", "webflux" }) void testCreateMessageSuccess(String clientType) throws InterruptedException { - // Client var clientBuilder = clientBuilders.get(clientType); Function samplingHandler = request -> { @@ -147,13 +149,6 @@ void testCreateMessageSuccess(String clientType) throws InterruptedException { CreateMessageResult.StopReason.STOP_SEQUENCE); }; - var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0")) - .capabilities(ClientCapabilities.builder().sampling().build()) - .sampling(samplingHandler) - .build(); - - // Server - CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); @@ -183,21 +178,26 @@ void testCreateMessageSuccess(String clientType) throws InterruptedException { return Mono.just(callResponse); }); - var mcpServer = McpServer.async(mcpServerTransportProvider) - .serverInfo("test-server", "1.0.0") - .tools(tool) - .build(); + try (var mcpServer = McpServer + .async(//@formatter:off + mcpServerTransportProvider) + .serverInfo("test-server", "1.0.0") + .tools(tool) + .build(); - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); + var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0")) + .capabilities(ClientCapabilities.builder().sampling().build()) + .sampling(samplingHandler) + .build()) {// @formatter:on - CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of())); + InitializeResult initResult = mcpClient.initialize(); + assertThat(initResult).isNotNull(); - assertThat(response).isNotNull(); - assertThat(response).isEqualTo(callResponse); + CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of())); - mcpClient.close(); - mcpServer.close(); + assertThat(response).isNotNull(); + assertThat(response).isEqualTo(callResponse); + } } // --------------------------------------- @@ -211,42 +211,42 @@ void testRootsSuccess(String clientType) { List roots = List.of(new Root("uri1://", "root1"), new Root("uri2://", "root2")); AtomicReference> rootsRef = new AtomicReference<>(); - var mcpServer = McpServer.sync(mcpServerTransportProvider) - .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) - .build(); - var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) - .roots(roots) - .build(); + try (// @formatter:off + var mcpServer = McpServer.sync(mcpServerTransportProvider) + .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) + .build(); - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); + var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) + .roots(roots) + .build()) { // @formatter:on - assertThat(rootsRef.get()).isNull(); + InitializeResult initResult = mcpClient.initialize(); + assertThat(initResult).isNotNull(); - mcpClient.rootsListChangedNotification(); + assertThat(rootsRef.get()).isNull(); - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).containsAll(roots); - }); + mcpClient.rootsListChangedNotification(); - // Remove a root - mcpClient.removeRoot(roots.get(0).uri()); + await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { + assertThat(rootsRef.get()).containsAll(roots); + }); - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).containsAll(List.of(roots.get(1))); - }); + // Remove a root + mcpClient.removeRoot(roots.get(0).uri()); - // Add a new root - var root3 = new Root("uri3://", "root3"); - mcpClient.addRoot(root3); + await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { + assertThat(rootsRef.get()).containsAll(List.of(roots.get(1))); + }); - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).containsAll(List.of(roots.get(1), root3)); - }); + // Add a new root + var root3 = new Root("uri3://", "root3"); + mcpClient.addRoot(root3); - mcpClient.close(); - mcpServer.close(); + await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { + assertThat(rootsRef.get()).containsAll(List.of(roots.get(1), root3)); + }); + } } @ParameterizedTest(name = "{0} : {displayName} ") @@ -263,25 +263,27 @@ void testRootsWithoutCapability(String clientType) { return mock(CallToolResult.class); }); - var mcpServer = McpServer.sync(mcpServerTransportProvider).rootsChangeHandler((exchange, rootsUpdate) -> { - }).tools(tool).build(); + try (// @formatter:off + var mcpServer = McpServer.sync(mcpServerTransportProvider) + .rootsChangeHandler((exchange, rootsUpdate) -> {}) + .tools(tool) + .build(); - // Create client without roots capability - // No roots capability - var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().build()).build(); + // Create client without roots capability + var mcpClient = clientBuilder + .capabilities(ClientCapabilities.builder().build()) + .build()) { // @formatter:on} - assertThat(mcpClient.initialize()).isNotNull(); + assertThat(mcpClient.initialize()).isNotNull(); - // Attempt to list roots should fail - try { - mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of())); - } - catch (McpError e) { - assertThat(e).isInstanceOf(McpError.class).hasMessage("Roots not supported"); + // Attempt to list roots should fail + try { + mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of())); + } + catch (McpError e) { + assertThat(e).isInstanceOf(McpError.class).hasMessage("Roots not supported"); + } } - - mcpClient.close(); - mcpServer.close(); } @ParameterizedTest(name = "{0} : {displayName} ") @@ -290,24 +292,24 @@ void testRootsNotifciationWithEmptyRootsList(String clientType) { var clientBuilder = clientBuilders.get(clientType); AtomicReference> rootsRef = new AtomicReference<>(); - var mcpServer = McpServer.sync(mcpServerTransportProvider) - .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) - .build(); - var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) - .roots(List.of()) // Empty roots list - .build(); + try ( //@formatter:off + var mcpServer = McpServer.sync(mcpServerTransportProvider) + .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) + .build(); - assertThat(mcpClient.initialize()).isNotNull(); + var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) + .roots(List.of()) // Empty roots list + .build()) { // @formatter:on - mcpClient.rootsListChangedNotification(); + assertThat(mcpClient.initialize()).isNotNull(); - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).isEmpty(); - }); + mcpClient.rootsListChangedNotification(); - mcpClient.close(); - mcpServer.close(); + await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { + assertThat(rootsRef.get()).isEmpty(); + }); + } } @ParameterizedTest(name = "{0} : {displayName} ") @@ -321,27 +323,26 @@ void testRootsWithMultipleHandlers(String clientType) { AtomicReference> rootsRef1 = new AtomicReference<>(); AtomicReference> rootsRef2 = new AtomicReference<>(); - var mcpServer = McpServer.sync(mcpServerTransportProvider) - .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef1.set(rootsUpdate)) - .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef2.set(rootsUpdate)) - .build(); - - var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) - .roots(roots) - .build(); + try ( //@formatter:off + var mcpServer = McpServer.sync(mcpServerTransportProvider) + .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef1.set(rootsUpdate)) + .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef2.set(rootsUpdate)) + .build(); - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); + var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) + .roots(roots) + .build()) { // @formatter:on - mcpClient.rootsListChangedNotification(); + InitializeResult initResult = mcpClient.initialize(); + assertThat(initResult).isNotNull(); - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef1.get()).containsAll(roots); - assertThat(rootsRef2.get()).containsAll(roots); - }); + mcpClient.rootsListChangedNotification(); - mcpClient.close(); - mcpServer.close(); + await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { + assertThat(rootsRef1.get()).containsAll(roots); + assertThat(rootsRef2.get()).containsAll(roots); + }); + } } @ParameterizedTest(name = "{0} : {displayName} ") @@ -353,25 +354,25 @@ void testRootsServerCloseWithActiveSubscription(String clientType) { List roots = List.of(new Root("uri1://", "root1")); AtomicReference> rootsRef = new AtomicReference<>(); - var mcpServer = McpServer.sync(mcpServerTransportProvider) - .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) - .build(); - var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) - .roots(roots) - .build(); + try ( //@formatter:off + var mcpServer = McpServer.sync(mcpServerTransportProvider) + .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) + .build(); - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); + var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) + .roots(roots) + .build()) { // @formatter:on - mcpClient.rootsListChangedNotification(); + InitializeResult initResult = mcpClient.initialize(); + assertThat(initResult).isNotNull(); - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).containsAll(roots); - }); + mcpClient.rootsListChangedNotification(); - mcpServer.close(); - mcpClient.close(); + await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { + assertThat(rootsRef.get()).containsAll(roots); + }); + } } // --------------------------------------- @@ -405,25 +406,24 @@ void testToolCallSuccess(String clientType) { return callResponse; }); - var mcpServer = McpServer.sync(mcpServerTransportProvider) - .capabilities(ServerCapabilities.builder().tools(true).build()) - .tools(tool1) - .build(); - - var mcpClient = clientBuilder.build(); + try ( //@formatter:off + var mcpServer = McpServer.sync(mcpServerTransportProvider) + .capabilities(ServerCapabilities.builder().tools(true).build()) + .tools(tool1) + .build(); - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); + var mcpClient = clientBuilder.build()) { // @formatter:on - assertThat(mcpClient.listTools().tools()).contains(tool1.tool()); + InitializeResult initResult = mcpClient.initialize(); + assertThat(initResult).isNotNull(); - CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of())); + assertThat(mcpClient.listTools().tools()).contains(tool1.tool()); - assertThat(response).isNotNull(); - assertThat(response).isEqualTo(callResponse); + CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of())); - mcpClient.close(); - mcpServer.close(); + assertThat(response).isNotNull(); + assertThat(response).isEqualTo(callResponse); + } } @ParameterizedTest(name = "{0} : {displayName} ") @@ -445,55 +445,56 @@ void testToolListChangeHandlingSuccess(String clientType) { return callResponse; }); - var mcpServer = McpServer.sync(mcpServerTransportProvider) - .capabilities(ServerCapabilities.builder().tools(true).build()) - .tools(tool1) - .build(); - AtomicReference> rootsRef = new AtomicReference<>(); - var mcpClient = clientBuilder.toolsChangeConsumer(toolsUpdate -> { - // perform a blocking call to a remote service - String response = RestClient.create() - .get() - .uri("https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md") - .retrieve() - .body(String.class); - assertThat(response).isNotBlank(); - rootsRef.set(toolsUpdate); - }).build(); - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); + try ( //@formatter:off + var mcpServer = McpServer.sync(mcpServerTransportProvider) + .capabilities(ServerCapabilities.builder().tools(true).build()) + .tools(tool1) + .build(); + + var mcpClient = clientBuilder.toolsChangeConsumer(toolsUpdate -> { + // perform a blocking call to a remote service + String response = RestClient.create() + .get() + .uri("https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md") + .retrieve() + .body(String.class); + assertThat(response).isNotBlank(); + rootsRef.set(toolsUpdate); + }).build()) { // @formatter:on - assertThat(rootsRef.get()).isNull(); + InitializeResult initResult = mcpClient.initialize(); + assertThat(initResult).isNotNull(); - assertThat(mcpClient.listTools().tools()).contains(tool1.tool()); + assertThat(rootsRef.get()).isNull(); - mcpServer.notifyToolsListChanged(); + assertThat(mcpClient.listTools().tools()).contains(tool1.tool()); - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).containsAll(List.of(tool1.tool())); - }); + mcpServer.notifyToolsListChanged(); - // Remove a tool - mcpServer.removeTool("tool1"); + await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { + assertThat(rootsRef.get()).containsAll(List.of(tool1.tool())); + }); - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).isEmpty(); - }); + // Remove a tool + mcpServer.removeTool("tool1"); - // Add a new tool - McpServerFeatures.SyncToolSpecification tool2 = new McpServerFeatures.SyncToolSpecification( - new McpSchema.Tool("tool2", "tool2 description", emptyJsonSchema), (exchange, request) -> callResponse); + await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { + assertThat(rootsRef.get()).isEmpty(); + }); - mcpServer.addTool(tool2); + // Add a new tool + McpServerFeatures.SyncToolSpecification tool2 = new McpServerFeatures.SyncToolSpecification( + new McpSchema.Tool("tool2", "tool2 description", emptyJsonSchema), + (exchange, request) -> callResponse); - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).containsAll(List.of(tool2.tool())); - }); + mcpServer.addTool(tool2); - mcpClient.close(); - mcpServer.close(); + await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { + assertThat(rootsRef.get()).containsAll(List.of(tool2.tool())); + }); + } } @ParameterizedTest(name = "{0} : {displayName} ") @@ -502,15 +503,13 @@ void testInitialize(String clientType) { var clientBuilder = clientBuilders.get(clientType); - var mcpServer = McpServer.sync(mcpServerTransportProvider).build(); + try (// @formatter:off + var mcpServer = McpServer.sync(mcpServerTransportProvider).build(); + var mcpClient = clientBuilder.build()) { // @formatter:on - var mcpClient = clientBuilder.build(); - - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); - - mcpClient.close(); - mcpServer.close(); + InitializeResult initResult = mcpClient.initialize(); + assertThat(initResult).isNotNull(); + } } // --------------------------------------- @@ -567,54 +566,53 @@ void testLoggingNotification(String clientType) { //@formatter:on }); - var mcpServer = McpServer.async(mcpServerTransportProvider) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().logging().tools(true).build()) - .tools(tool) - .build(); - - // Create client with logging notification handler - McpSyncClient mcpClient = clientBuilder.loggingConsumer(notification -> { - receivedNotifications.add(notification); - }).build(); - - // Initialize client - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); - - // Set minimum logging level to NOTICE - mcpClient.setLoggingLevel(McpSchema.LoggingLevel.NOTICE); - - // Call the tool that sends logging notifications - CallToolResult result = mcpClient.callTool(new McpSchema.CallToolRequest("logging-test", Map.of())); - assertThat(result).isNotNull(); - assertThat(result.content().get(0)).isInstanceOf(McpSchema.TextContent.class); - assertThat(((McpSchema.TextContent) result.content().get(0)).text()).isEqualTo("Logging test completed"); - - // Wait for notifications to be processed - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - - // Should have received 3 notifications (1 NOTICE and 2 ERROR) - assertThat(receivedNotifications).hasSize(3); - - // First notification should be NOTICE level - assertThat(receivedNotifications.get(0).level()).isEqualTo(McpSchema.LoggingLevel.NOTICE); - assertThat(receivedNotifications.get(0).logger()).isEqualTo("test-logger"); - assertThat(receivedNotifications.get(0).data()).isEqualTo("Notice message"); - - // Second notification should be ERROR level - assertThat(receivedNotifications.get(1).level()).isEqualTo(McpSchema.LoggingLevel.ERROR); - assertThat(receivedNotifications.get(1).logger()).isEqualTo("test-logger"); - assertThat(receivedNotifications.get(1).data()).isEqualTo("Error message"); - - // Third notification should be ERROR level - assertThat(receivedNotifications.get(2).level()).isEqualTo(McpSchema.LoggingLevel.ERROR); - assertThat(receivedNotifications.get(2).logger()).isEqualTo("test-logger"); - assertThat(receivedNotifications.get(2).data()).isEqualTo("Another error message"); - }); - - mcpClient.close(); - mcpServer.close(); + try ( //@formatter:off + var mcpServer = McpServer.async(mcpServerTransportProvider) + .serverInfo("test-server", "1.0.0") + .capabilities(ServerCapabilities.builder().logging().tools(true).build()) + .tools(tool) + .build(); + + // Create client with logging notification handler + var mcpClient = clientBuilder + .loggingConsumer(notification -> { receivedNotifications.add(notification); }) + .build()) { // @formatter:on + + // Initialize client + InitializeResult initResult = mcpClient.initialize(); + assertThat(initResult).isNotNull(); + + // Set minimum logging level to NOTICE + mcpClient.setLoggingLevel(McpSchema.LoggingLevel.NOTICE); + + // Call the tool that sends logging notifications + CallToolResult result = mcpClient.callTool(new McpSchema.CallToolRequest("logging-test", Map.of())); + assertThat(result).isNotNull(); + assertThat(result.content().get(0)).isInstanceOf(McpSchema.TextContent.class); + assertThat(((McpSchema.TextContent) result.content().get(0)).text()).isEqualTo("Logging test completed"); + + // Wait for notifications to be processed + await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { + + // Should have received 3 notifications (1 NOTICE and 2 ERROR) + assertThat(receivedNotifications).hasSize(3); + + // First notification should be NOTICE level + assertThat(receivedNotifications.get(0).level()).isEqualTo(McpSchema.LoggingLevel.NOTICE); + assertThat(receivedNotifications.get(0).logger()).isEqualTo("test-logger"); + assertThat(receivedNotifications.get(0).data()).isEqualTo("Notice message"); + + // Second notification should be ERROR level + assertThat(receivedNotifications.get(1).level()).isEqualTo(McpSchema.LoggingLevel.ERROR); + assertThat(receivedNotifications.get(1).logger()).isEqualTo("test-logger"); + assertThat(receivedNotifications.get(1).data()).isEqualTo("Error message"); + + // Third notification should be ERROR level + assertThat(receivedNotifications.get(2).level()).isEqualTo(McpSchema.LoggingLevel.ERROR); + assertThat(receivedNotifications.get(2).logger()).isEqualTo("test-logger"); + assertThat(receivedNotifications.get(2).data()).isEqualTo("Another error message"); + }); + } } } diff --git a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java index d5c9f90f..0115200b 100644 --- a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java +++ b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java @@ -125,27 +125,32 @@ void testCreateMessageWithoutSamplingCapabilities() { return Mono.just(mock(CallToolResult.class)); }); - McpServer.async(mcpServerTransportProvider).serverInfo("test-server", "1.0.0").tools(tool).build(); + //@formatter:off + try (var server = McpServer.async(mcpServerTransportProvider) + .serverInfo("test-server", "1.0.0") + .tools(tool) + .build(); + + // Create client without sampling capabilities + var client = clientBuilder + .clientInfo(new McpSchema.Implementation("Sample " + "client", "0.0.0")) + .build()) {//@formatter:on + + assertThat(client.initialize()).isNotNull(); - // Create client without sampling capabilities - var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample " + "client", "0.0.0")).build(); - - assertThat(client.initialize()).isNotNull(); - - try { - client.callTool(new McpSchema.CallToolRequest("tool1", Map.of())); - } - catch (McpError e) { - assertThat(e).isInstanceOf(McpError.class) - .hasMessage("Client must be configured with sampling capabilities"); + try { + client.callTool(new McpSchema.CallToolRequest("tool1", Map.of())); + } + catch (McpError e) { + assertThat(e).isInstanceOf(McpError.class) + .hasMessage("Client must be configured with sampling capabilities"); + } } } @Test void testCreateMessageSuccess() throws InterruptedException { - // Client - Function samplingHandler = request -> { assertThat(request.messages()).hasSize(1); assertThat(request.messages().get(0).content()).isInstanceOf(McpSchema.TextContent.class); @@ -154,13 +159,6 @@ void testCreateMessageSuccess() throws InterruptedException { CreateMessageResult.StopReason.STOP_SEQUENCE); }; - var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0")) - .capabilities(ClientCapabilities.builder().sampling().build()) - .sampling(samplingHandler) - .build(); - - // Server - CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); @@ -190,20 +188,24 @@ void testCreateMessageSuccess() throws InterruptedException { return Mono.just(callResponse); }); - var mcpServer = McpServer.async(mcpServerTransportProvider) - .serverInfo("test-server", "1.0.0") - .tools(tool) - .build(); + //@formatter:off + try (var mcpServer = McpServer.async(mcpServerTransportProvider) + .serverInfo("test-server", "1.0.0") + .tools(tool) + .build(); - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); + var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0")) + .capabilities(ClientCapabilities.builder().sampling().build()) + .sampling(samplingHandler) + .build()) {//@formatter:on - CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of())); + InitializeResult initResult = mcpClient.initialize(); + assertThat(initResult).isNotNull(); - assertThat(response).isNotNull().isEqualTo(callResponse); + CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of())); - mcpClient.close(); - mcpServer.close(); + assertThat(response).isNotNull().isEqualTo(callResponse); + } } // --------------------------------------- @@ -214,42 +216,42 @@ void testRootsSuccess() { List roots = List.of(new Root("uri1://", "root1"), new Root("uri2://", "root2")); AtomicReference> rootsRef = new AtomicReference<>(); - var mcpServer = McpServer.sync(mcpServerTransportProvider) - .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) - .build(); - var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) - .roots(roots) - .build(); + //@formatter:off + try (var mcpServer = McpServer.sync(mcpServerTransportProvider) + .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) + .build(); - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); + var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) + .roots(roots) + .build()) {//@formatter:on - assertThat(rootsRef.get()).isNull(); + InitializeResult initResult = mcpClient.initialize(); + assertThat(initResult).isNotNull(); - mcpClient.rootsListChangedNotification(); + assertThat(rootsRef.get()).isNull(); - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).containsAll(roots); - }); + mcpClient.rootsListChangedNotification(); - // Remove a root - mcpClient.removeRoot(roots.get(0).uri()); + await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { + assertThat(rootsRef.get()).containsAll(roots); + }); - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).containsAll(List.of(roots.get(1))); - }); + // Remove a root + mcpClient.removeRoot(roots.get(0).uri()); - // Add a new root - var root3 = new Root("uri3://", "root3"); - mcpClient.addRoot(root3); + await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { + assertThat(rootsRef.get()).containsAll(List.of(roots.get(1))); + }); - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).containsAll(List.of(roots.get(1), root3)); - }); + // Add a new root + var root3 = new Root("uri3://", "root3"); + mcpClient.addRoot(root3); - mcpClient.close(); - mcpServer.close(); + await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { + assertThat(rootsRef.get()).containsAll(List.of(roots.get(1), root3)); + }); + } } @Test @@ -263,50 +265,52 @@ void testRootsWithoutCapability() { return mock(CallToolResult.class); }); - var mcpServer = McpServer.sync(mcpServerTransportProvider).rootsChangeHandler((exchange, rootsUpdate) -> { - }).tools(tool).build(); + //@formatter:off + try (var mcpServer = McpServer.sync(mcpServerTransportProvider) + .rootsChangeHandler((exchange, rootsUpdate) -> {}) + .tools(tool) + .build(); - // Create client without roots capability - // No roots capability - var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().build()).build(); + // Create client without roots capability + // No roots capability + var mcpClient = clientBuilder + .capabilities(ClientCapabilities.builder().build()) + .build()) {//@formatter:on - assertThat(mcpClient.initialize()).isNotNull(); + assertThat(mcpClient.initialize()).isNotNull(); - // Attempt to list roots should fail - try { - mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of())); - } - catch (McpError e) { - assertThat(e).isInstanceOf(McpError.class).hasMessage("Roots not supported"); + // Attempt to list roots should fail + try { + mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of())); + } + catch (McpError e) { + assertThat(e).isInstanceOf(McpError.class).hasMessage("Roots not supported"); + } } - - mcpClient.close(); - mcpServer.close(); } @Test void testRootsNotifciationWithEmptyRootsList() { AtomicReference> rootsRef = new AtomicReference<>(); - var mcpServer = McpServer.sync(mcpServerTransportProvider) - .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) - .build(); + //@formatter:off + try (var mcpServer = McpServer.sync(mcpServerTransportProvider) + .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) + .build(); - var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) - .roots(List.of()) // Empty roots list - .build(); + var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) + .roots(List.of()) // Empty roots list + .build()) {//@formatter:on - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); + InitializeResult initResult = mcpClient.initialize(); + assertThat(initResult).isNotNull(); - mcpClient.rootsListChangedNotification(); + mcpClient.rootsListChangedNotification(); - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).isEmpty(); - }); - - mcpClient.close(); - mcpServer.close(); + await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { + assertThat(rootsRef.get()).isEmpty(); + }); + } } @Test @@ -316,26 +320,25 @@ void testRootsWithMultipleHandlers() { AtomicReference> rootsRef1 = new AtomicReference<>(); AtomicReference> rootsRef2 = new AtomicReference<>(); - var mcpServer = McpServer.sync(mcpServerTransportProvider) - .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef1.set(rootsUpdate)) - .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef2.set(rootsUpdate)) - .build(); + //@formatter:off + try (var mcpServer = McpServer.sync(mcpServerTransportProvider) + .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef1.set(rootsUpdate)) + .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef2.set(rootsUpdate)) + .build(); - var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) - .roots(roots) - .build(); + var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) + .roots(roots) + .build()) {//@formatter:on - assertThat(mcpClient.initialize()).isNotNull(); + assertThat(mcpClient.initialize()).isNotNull(); - mcpClient.rootsListChangedNotification(); + mcpClient.rootsListChangedNotification(); - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef1.get()).containsAll(roots); - assertThat(rootsRef2.get()).containsAll(roots); - }); - - mcpClient.close(); - mcpServer.close(); + await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { + assertThat(rootsRef1.get()).containsAll(roots); + assertThat(rootsRef2.get()).containsAll(roots); + }); + } } @Test @@ -343,28 +346,25 @@ void testRootsServerCloseWithActiveSubscription() { List roots = List.of(new Root("uri1://", "root1")); AtomicReference> rootsRef = new AtomicReference<>(); - var mcpServer = McpServer.sync(mcpServerTransportProvider) - .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) - .build(); - var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) - .roots(roots) - .build(); + //@formatter:off + try (var mcpServer = McpServer.sync(mcpServerTransportProvider) + .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) + .build(); - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); + var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) + .roots(roots) + .build()) {//@formatter:on - mcpClient.rootsListChangedNotification(); + InitializeResult initResult = mcpClient.initialize(); + assertThat(initResult).isNotNull(); - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).containsAll(roots); - }); + mcpClient.rootsListChangedNotification(); - // Close server while subscription is active - mcpServer.close(); - - // Verify client can handle server closure gracefully - mcpClient.close(); + await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { + assertThat(rootsRef.get()).containsAll(roots); + }); + } } // --------------------------------------- @@ -395,24 +395,23 @@ void testToolCallSuccess() { return callResponse; }); - var mcpServer = McpServer.sync(mcpServerTransportProvider) - .capabilities(ServerCapabilities.builder().tools(true).build()) - .tools(tool1) - .build(); + //@formatter:off + try (var mcpServer = McpServer.sync(mcpServerTransportProvider) + .capabilities(ServerCapabilities.builder().tools(true).build()) + .tools(tool1) + .build(); - var mcpClient = clientBuilder.build(); + var mcpClient = clientBuilder.build()) {//@formatter:on - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); + InitializeResult initResult = mcpClient.initialize(); + assertThat(initResult).isNotNull(); - assertThat(mcpClient.listTools().tools()).contains(tool1.tool()); + assertThat(mcpClient.listTools().tools()).contains(tool1.tool()); - CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of())); + CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of())); - assertThat(response).isNotNull().isEqualTo(callResponse); - - mcpClient.close(); - mcpServer.close(); + assertThat(response).isNotNull().isEqualTo(callResponse); + } } @Test @@ -431,69 +430,68 @@ void testToolListChangeHandlingSuccess() { return callResponse; }); - var mcpServer = McpServer.sync(mcpServerTransportProvider) - .capabilities(ServerCapabilities.builder().tools(true).build()) - .tools(tool1) - .build(); - AtomicReference> rootsRef = new AtomicReference<>(); - var mcpClient = clientBuilder.toolsChangeConsumer(toolsUpdate -> { - // perform a blocking call to a remote service - String response = RestClient.create() - .get() - .uri("https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md") - .retrieve() - .body(String.class); - assertThat(response).isNotBlank(); - rootsRef.set(toolsUpdate); - }).build(); - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); + //@formatter:off + try (var mcpServer = McpServer.sync(mcpServerTransportProvider) + .capabilities(ServerCapabilities.builder().tools(true).build()) + .tools(tool1) + .build(); + + var mcpClient = clientBuilder.toolsChangeConsumer(toolsUpdate -> { + // perform a blocking call to a remote service + String response = RestClient.create() + .get() + .uri("https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md") + .retrieve() + .body(String.class); + assertThat(response).isNotBlank(); + rootsRef.set(toolsUpdate); + }).build()) {//@formatter:on - assertThat(rootsRef.get()).isNull(); + InitializeResult initResult = mcpClient.initialize(); + assertThat(initResult).isNotNull(); - assertThat(mcpClient.listTools().tools()).contains(tool1.tool()); + assertThat(rootsRef.get()).isNull(); - mcpServer.notifyToolsListChanged(); + assertThat(mcpClient.listTools().tools()).contains(tool1.tool()); - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).containsAll(List.of(tool1.tool())); - }); + mcpServer.notifyToolsListChanged(); - // Remove a tool - mcpServer.removeTool("tool1"); + await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { + assertThat(rootsRef.get()).containsAll(List.of(tool1.tool())); + }); - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).isEmpty(); - }); + // Remove a tool + mcpServer.removeTool("tool1"); - // Add a new tool - McpServerFeatures.SyncToolSpecification tool2 = new McpServerFeatures.SyncToolSpecification( - new McpSchema.Tool("tool2", "tool2 description", emptyJsonSchema), (exchange, request) -> callResponse); + await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { + assertThat(rootsRef.get()).isEmpty(); + }); - mcpServer.addTool(tool2); + // Add a new tool + McpServerFeatures.SyncToolSpecification tool2 = new McpServerFeatures.SyncToolSpecification( + new McpSchema.Tool("tool2", "tool2 description", emptyJsonSchema), + (exchange, request) -> callResponse); - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).containsAll(List.of(tool2.tool())); - }); + mcpServer.addTool(tool2); - mcpClient.close(); - mcpServer.close(); + await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { + assertThat(rootsRef.get()).containsAll(List.of(tool2.tool())); + }); + } } @Test void testInitialize() { - var mcpServer = McpServer.sync(mcpServerTransportProvider).build(); - - var mcpClient = clientBuilder.build(); + //@formatter:off + try (var mcpServer = McpServer.sync(mcpServerTransportProvider).build(); + var mcpClient = clientBuilder.build()) {//@formatter:on - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); - - mcpClient.close(); - mcpServer.close(); + InitializeResult initResult = mcpClient.initialize(); + assertThat(initResult).isNotNull(); + } } } diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java index 81580b93..2aae3cee 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java +++ b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java @@ -75,7 +75,7 @@ * @see McpSchema * @see McpClientSession */ -public class McpAsyncClient { +public class McpAsyncClient implements AutoCloseable { private static final Logger logger = LoggerFactory.getLogger(McpAsyncClient.class); @@ -275,6 +275,7 @@ public McpSchema.Implementation getClientInfo() { /** * Closes the client connection immediately. */ + @Override public void close() { this.mcpSession.close(); } diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java index e761f8fc..3de9ed17 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java @@ -72,7 +72,7 @@ * @see McpSchema * @see McpClientSession */ -public class McpAsyncServer { +public class McpAsyncServer implements AutoCloseable { private static final Logger logger = LoggerFactory.getLogger(McpAsyncServer.class); @@ -121,6 +121,7 @@ public Mono closeGracefully() { /** * Close the server immediately. */ + @Override public void close() { this.delegate.close(); } diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java index 1e50506c..f6a3ef37 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java @@ -135,7 +135,7 @@ public Mono loggingNotification(LoggingMessageNotification loggingMessageN * filtered out. * @param minLoggingLevel The minimum logging level */ - public void setMinLoggingLevel(LoggingLevel minLoggingLevel) { + void setMinLoggingLevel(LoggingLevel minLoggingLevel) { Assert.notNull(minLoggingLevel, "minLoggingLevel must not be null"); this.session.setMinLoggingLevel(minLoggingLevel); } diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServer.java index 2ee0cc75..10311959 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServer.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServer.java @@ -46,7 +46,7 @@ * @see McpAsyncServer * @see McpSchema */ -public class McpSyncServer { +public class McpSyncServer implements AutoCloseable { /** * The async server to wrap. @@ -157,6 +157,7 @@ public void closeGracefully() { /** * Close the server immediately. */ + @Override public void close() { this.asyncServer.close(); } diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServerExchange.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServerExchange.java index 406ac13a..32dfe2b2 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServerExchange.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServerExchange.java @@ -92,6 +92,11 @@ public Mono loggingNotification(LoggingMessageNotification loggingMessageN return this.exchange.loggingNotification(loggingMessageNotification); } + /** + * Set the minimum logging level for the client. Messages below this level will be + * filtered out. + * @param minLoggingLevel The minimum logging level + */ void setMinLoggingLevel(LoggingLevel minLoggingLevel) { this.exchange.setMinLoggingLevel(minLoggingLevel); } diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerCustomContextPathTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerCustomContextPathTests.java index 1254e2ad..0120c097 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerCustomContextPathTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerCustomContextPathTests.java @@ -8,7 +8,6 @@ import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport; import io.modelcontextprotocol.server.McpServer; import io.modelcontextprotocol.spec.McpSchema; -import org.apache.catalina.Context; import org.apache.catalina.LifecycleException; import org.apache.catalina.LifecycleState; import org.apache.catalina.startup.Tomcat; @@ -78,9 +77,12 @@ public void after() { @Test void testCustomContextPath() { - McpServer.async(mcpServerTransportProvider).serverInfo("test-server", "1.0.0").build(); - var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample " + "client", "0.0.0")).build(); - assertThat(client.initialize()).isNotNull(); + try (//@formatter:off + var server = McpServer.async(mcpServerTransportProvider).serverInfo("test-server", "1.0.0").build(); + var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample " + "client", "0.0.0")) .build()) { //@formatter:on + + assertThat(client.initialize()).isNotNull(); + } } } diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java index f662389e..2fe93896 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java @@ -111,27 +111,32 @@ void testCreateMessageWithoutSamplingCapabilities() { return Mono.just(mock(CallToolResult.class)); }); - McpServer.async(mcpServerTransportProvider).serverInfo("test-server", "1.0.0").tools(tool).build(); + //@formatter:off + try (var server = McpServer.async(mcpServerTransportProvider) + .serverInfo("test-server", "1.0.0") + .tools(tool) + .build(); + + // Create client without sampling capabilities + var client = clientBuilder + .clientInfo(new McpSchema.Implementation("Sample " + "client", "0.0.0")) + .build()) {//@formatter:on + + assertThat(client.initialize()).isNotNull(); - // Create client without sampling capabilities - var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample " + "client", "0.0.0")).build(); - - assertThat(client.initialize()).isNotNull(); - - try { - client.callTool(new McpSchema.CallToolRequest("tool1", Map.of())); - } - catch (McpError e) { - assertThat(e).isInstanceOf(McpError.class) - .hasMessage("Client must be configured with sampling capabilities"); + try { + client.callTool(new McpSchema.CallToolRequest("tool1", Map.of())); + } + catch (McpError e) { + assertThat(e).isInstanceOf(McpError.class) + .hasMessage("Client must be configured with sampling capabilities"); + } } } @Test void testCreateMessageSuccess() throws InterruptedException { - // Client - Function samplingHandler = request -> { assertThat(request.messages()).hasSize(1); assertThat(request.messages().get(0).content()).isInstanceOf(McpSchema.TextContent.class); @@ -140,13 +145,6 @@ void testCreateMessageSuccess() throws InterruptedException { CreateMessageResult.StopReason.STOP_SEQUENCE); }; - var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0")) - .capabilities(ClientCapabilities.builder().sampling().build()) - .sampling(samplingHandler) - .build(); - - // Server - CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); @@ -176,21 +174,25 @@ void testCreateMessageSuccess() throws InterruptedException { return Mono.just(callResponse); }); - var mcpServer = McpServer.async(mcpServerTransportProvider) - .serverInfo("test-server", "1.0.0") - .tools(tool) - .build(); + //@formatter:off + try (var mcpServer = McpServer.async(mcpServerTransportProvider) + .serverInfo("test-server", "1.0.0") + .tools(tool) + .build(); - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); + var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0")) + .capabilities(ClientCapabilities.builder().sampling().build()) + .sampling(samplingHandler) + .build()) {//@formatter:on - CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of())); + InitializeResult initResult = mcpClient.initialize(); + assertThat(initResult).isNotNull(); - assertThat(response).isNotNull(); - assertThat(response).isEqualTo(callResponse); + CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of())); - mcpClient.close(); - mcpServer.close(); + assertThat(response).isNotNull(); + assertThat(response).isEqualTo(callResponse); + } } // --------------------------------------- @@ -201,42 +203,42 @@ void testRootsSuccess() { List roots = List.of(new Root("uri1://", "root1"), new Root("uri2://", "root2")); AtomicReference> rootsRef = new AtomicReference<>(); - var mcpServer = McpServer.sync(mcpServerTransportProvider) - .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) - .build(); - var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) - .roots(roots) - .build(); + //@formatter:off + try (var mcpServer = McpServer.sync(mcpServerTransportProvider) + .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) + .build(); - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); + var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) + .roots(roots) + .build()) {//@formatter:on - assertThat(rootsRef.get()).isNull(); + InitializeResult initResult = mcpClient.initialize(); + assertThat(initResult).isNotNull(); - mcpClient.rootsListChangedNotification(); + assertThat(rootsRef.get()).isNull(); - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).containsAll(roots); - }); + mcpClient.rootsListChangedNotification(); - // Remove a root - mcpClient.removeRoot(roots.get(0).uri()); + await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { + assertThat(rootsRef.get()).containsAll(roots); + }); - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).containsAll(List.of(roots.get(1))); - }); + // Remove a root + mcpClient.removeRoot(roots.get(0).uri()); - // Add a new root - var root3 = new Root("uri3://", "root3"); - mcpClient.addRoot(root3); + await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { + assertThat(rootsRef.get()).containsAll(List.of(roots.get(1))); + }); - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).containsAll(List.of(roots.get(1), root3)); - }); + // Add a new root + var root3 = new Root("uri3://", "root3"); + mcpClient.addRoot(root3); - mcpClient.close(); - mcpServer.close(); + await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { + assertThat(rootsRef.get()).containsAll(List.of(roots.get(1), root3)); + }); + } } @Test @@ -250,50 +252,51 @@ void testRootsWithoutCapability() { return mock(CallToolResult.class); }); - var mcpServer = McpServer.sync(mcpServerTransportProvider).rootsChangeHandler((exchange, rootsUpdate) -> { - }).tools(tool).build(); + //@formatter:off + try (var mcpServer = McpServer.sync(mcpServerTransportProvider) + .rootsChangeHandler((exchange, rootsUpdate) -> {}) + .tools(tool) + .build(); - // Create client without roots capability - // No roots capability - var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().build()).build(); + // Create client without roots capability + var mcpClient = clientBuilder + .capabilities(ClientCapabilities.builder().build()) + .build()) {//@formatter:on - assertThat(mcpClient.initialize()).isNotNull(); + assertThat(mcpClient.initialize()).isNotNull(); - // Attempt to list roots should fail - try { - mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of())); - } - catch (McpError e) { - assertThat(e).isInstanceOf(McpError.class).hasMessage("Roots not supported"); + // Attempt to list roots should fail + try { + mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of())); + } + catch (McpError e) { + assertThat(e).isInstanceOf(McpError.class).hasMessage("Roots not supported"); + } } - - mcpClient.close(); - mcpServer.close(); } @Test void testRootsNotifciationWithEmptyRootsList() { AtomicReference> rootsRef = new AtomicReference<>(); - var mcpServer = McpServer.sync(mcpServerTransportProvider) - .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) - .build(); - - var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) - .roots(List.of()) // Empty roots list - .build(); + //@formatter:off + try (var mcpServer = McpServer.sync(mcpServerTransportProvider) + .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) + .build(); - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); + var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) + .roots(List.of()) // Empty roots list + .build()) {//@formatter:on - mcpClient.rootsListChangedNotification(); + InitializeResult initResult = mcpClient.initialize(); + assertThat(initResult).isNotNull(); - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).isEmpty(); - }); + mcpClient.rootsListChangedNotification(); - mcpClient.close(); - mcpServer.close(); + await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { + assertThat(rootsRef.get()).isEmpty(); + }); + } } @Test @@ -303,26 +306,25 @@ void testRootsWithMultipleHandlers() { AtomicReference> rootsRef1 = new AtomicReference<>(); AtomicReference> rootsRef2 = new AtomicReference<>(); - var mcpServer = McpServer.sync(mcpServerTransportProvider) - .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef1.set(rootsUpdate)) - .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef2.set(rootsUpdate)) - .build(); - - var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) - .roots(roots) - .build(); + //@formatter:off + try (var mcpServer = McpServer.sync(mcpServerTransportProvider) + .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef1.set(rootsUpdate)) + .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef2.set(rootsUpdate)) + .build(); - assertThat(mcpClient.initialize()).isNotNull(); + var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) + .roots(roots) + .build()) {//@formatter:on - mcpClient.rootsListChangedNotification(); + assertThat(mcpClient.initialize()).isNotNull(); - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef1.get()).containsAll(roots); - assertThat(rootsRef2.get()).containsAll(roots); - }); + mcpClient.rootsListChangedNotification(); - mcpClient.close(); - mcpServer.close(); + await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { + assertThat(rootsRef1.get()).containsAll(roots); + assertThat(rootsRef2.get()).containsAll(roots); + }); + } } @Test @@ -330,28 +332,25 @@ void testRootsServerCloseWithActiveSubscription() { List roots = List.of(new Root("uri1://", "root1")); AtomicReference> rootsRef = new AtomicReference<>(); - var mcpServer = McpServer.sync(mcpServerTransportProvider) - .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) - .build(); - - var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) - .roots(roots) - .build(); - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); + //@formatter:off + try (var mcpServer = McpServer.sync(mcpServerTransportProvider) + .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) + .build(); - mcpClient.rootsListChangedNotification(); + var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) + .roots(roots) + .build()) {//@formatter:on - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).containsAll(roots); - }); + InitializeResult initResult = mcpClient.initialize(); + assertThat(initResult).isNotNull(); - // Close server while subscription is active - mcpServer.close(); + mcpClient.rootsListChangedNotification(); - // Verify client can handle server closure gracefully - mcpClient.close(); + await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { + assertThat(rootsRef.get()).containsAll(roots); + }); + } } // --------------------------------------- @@ -382,25 +381,24 @@ void testToolCallSuccess() { return callResponse; }); - var mcpServer = McpServer.sync(mcpServerTransportProvider) - .capabilities(ServerCapabilities.builder().tools(true).build()) - .tools(tool1) - .build(); - - var mcpClient = clientBuilder.build(); + //@formatter:off + try (var mcpServer = McpServer.sync(mcpServerTransportProvider) + .capabilities(ServerCapabilities.builder().tools(true).build()) + .tools(tool1) + .build(); - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); + var mcpClient = clientBuilder.build()) {//@formatter:on - assertThat(mcpClient.listTools().tools()).contains(tool1.tool()); + InitializeResult initResult = mcpClient.initialize(); + assertThat(initResult).isNotNull(); - CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of())); + assertThat(mcpClient.listTools().tools()).contains(tool1.tool()); - assertThat(response).isNotNull(); - assertThat(response).isEqualTo(callResponse); + CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of())); - mcpClient.close(); - mcpServer.close(); + assertThat(response).isNotNull(); + assertThat(response).isEqualTo(callResponse); + } } @Test @@ -419,69 +417,67 @@ void testToolListChangeHandlingSuccess() { return callResponse; }); - var mcpServer = McpServer.sync(mcpServerTransportProvider) - .capabilities(ServerCapabilities.builder().tools(true).build()) - .tools(tool1) - .build(); - AtomicReference> rootsRef = new AtomicReference<>(); - var mcpClient = clientBuilder.toolsChangeConsumer(toolsUpdate -> { - // perform a blocking call to a remote service - String response = RestClient.create() - .get() - .uri("https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md") - .retrieve() - .body(String.class); - assertThat(response).isNotBlank(); - rootsRef.set(toolsUpdate); - }).build(); - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); + //@formatter:off + try (var mcpServer = McpServer.sync(mcpServerTransportProvider) + .capabilities(ServerCapabilities.builder().tools(true).build()) + .tools(tool1) + .build(); + + var mcpClient = clientBuilder.toolsChangeConsumer(toolsUpdate -> { + // perform a blocking call to a remote service + String response = RestClient.create() + .get() + .uri("https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md") + .retrieve() + .body(String.class); + assertThat(response).isNotBlank(); + rootsRef.set(toolsUpdate); + }).build()) {//@formatter:on - assertThat(rootsRef.get()).isNull(); + InitializeResult initResult = mcpClient.initialize(); + assertThat(initResult).isNotNull(); - assertThat(mcpClient.listTools().tools()).contains(tool1.tool()); + assertThat(rootsRef.get()).isNull(); - mcpServer.notifyToolsListChanged(); + assertThat(mcpClient.listTools().tools()).contains(tool1.tool()); - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).containsAll(List.of(tool1.tool())); - }); + mcpServer.notifyToolsListChanged(); - // Remove a tool - mcpServer.removeTool("tool1"); + await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { + assertThat(rootsRef.get()).containsAll(List.of(tool1.tool())); + }); - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).isEmpty(); - }); + // Remove a tool + mcpServer.removeTool("tool1"); - // Add a new tool - McpServerFeatures.SyncToolSpecification tool2 = new McpServerFeatures.SyncToolSpecification( - new McpSchema.Tool("tool2", "tool2 description", emptyJsonSchema), (exchange, request) -> callResponse); + await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { + assertThat(rootsRef.get()).isEmpty(); + }); - mcpServer.addTool(tool2); + // Add a new tool + McpServerFeatures.SyncToolSpecification tool2 = new McpServerFeatures.SyncToolSpecification( + new McpSchema.Tool("tool2", "tool2 description", emptyJsonSchema), + (exchange, request) -> callResponse); - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).containsAll(List.of(tool2.tool())); - }); + mcpServer.addTool(tool2); - mcpClient.close(); - mcpServer.close(); + await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { + assertThat(rootsRef.get()).containsAll(List.of(tool2.tool())); + }); + } } @Test void testInitialize() { - var mcpServer = McpServer.sync(mcpServerTransportProvider).build(); - - var mcpClient = clientBuilder.build(); - - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); + try (var mcpServer = McpServer.sync(mcpServerTransportProvider).build(); + var mcpClient = clientBuilder.build()) { - mcpClient.close(); - mcpServer.close(); + InitializeResult initResult = mcpClient.initialize(); + assertThat(initResult).isNotNull(); + } } // --------------------------------------- @@ -547,56 +543,55 @@ void testLoggingNotification() { return Mono.just(new CallToolResult("Logging test completed", false)); }); - var mcpServer = McpServer.async(mcpServerTransportProvider) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().logging().tools(true).build()) - .tools(tool) - .build(); - - // Create client with logging notification handler - var mcpClient = clientBuilder.loggingConsumer(notification -> { - receivedNotifications.add(notification); - }).build(); - - // Initialize client - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); - - // Set minimum logging level to NOTICE - mcpClient.setLoggingLevel(McpSchema.LoggingLevel.NOTICE); - - // Call the tool that sends logging notifications - CallToolResult result = mcpClient.callTool(new McpSchema.CallToolRequest("logging-test", Map.of())); - assertThat(result).isNotNull(); - assertThat(result.content().get(0)).isInstanceOf(McpSchema.TextContent.class); - assertThat(((McpSchema.TextContent) result.content().get(0)).text()).isEqualTo("Logging test completed"); - - // Wait for notifications to be processed - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - - System.out.println("Received notifications: " + receivedNotifications); - - // Should have received 3 notifications (1 NOTICE and 2 ERROR) - assertThat(receivedNotifications).hasSize(3); - - // First notification should be NOTICE level - assertThat(receivedNotifications.get(0).level()).isEqualTo(McpSchema.LoggingLevel.NOTICE); - assertThat(receivedNotifications.get(0).logger()).isEqualTo("test-logger"); - assertThat(receivedNotifications.get(0).data()).isEqualTo("Notice message"); - - // Second notification should be ERROR level - assertThat(receivedNotifications.get(1).level()).isEqualTo(McpSchema.LoggingLevel.ERROR); - assertThat(receivedNotifications.get(1).logger()).isEqualTo("test-logger"); - assertThat(receivedNotifications.get(1).data()).isEqualTo("Error message"); - - // Third notification should be ERROR level - assertThat(receivedNotifications.get(2).level()).isEqualTo(McpSchema.LoggingLevel.ERROR); - assertThat(receivedNotifications.get(2).logger()).isEqualTo("test-logger"); - assertThat(receivedNotifications.get(2).data()).isEqualTo("Another error message"); - }); - - mcpClient.close(); - mcpServer.close(); + //@formatter:off + try (var mcpServer = McpServer.async(mcpServerTransportProvider) + .serverInfo("test-server", "1.0.0") + .capabilities(ServerCapabilities.builder().logging().tools(true).build()) + .tools(tool) + .build(); + + // Create client with logging notification handler + var mcpClient = clientBuilder.loggingConsumer(notification -> { + receivedNotifications.add(notification); + }).build()) {//@formatter:on + + // Initialize client + InitializeResult initResult = mcpClient.initialize(); + assertThat(initResult).isNotNull(); + + // Set minimum logging level to NOTICE + mcpClient.setLoggingLevel(McpSchema.LoggingLevel.NOTICE); + + // Call the tool that sends logging notifications + CallToolResult result = mcpClient.callTool(new McpSchema.CallToolRequest("logging-test", Map.of())); + assertThat(result).isNotNull(); + assertThat(result.content().get(0)).isInstanceOf(McpSchema.TextContent.class); + assertThat(((McpSchema.TextContent) result.content().get(0)).text()).isEqualTo("Logging test completed"); + + // Wait for notifications to be processed + await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { + + System.out.println("Received notifications: " + receivedNotifications); + + // Should have received 3 notifications (1 NOTICE and 2 ERROR) + assertThat(receivedNotifications).hasSize(3); + + // First notification should be NOTICE level + assertThat(receivedNotifications.get(0).level()).isEqualTo(McpSchema.LoggingLevel.NOTICE); + assertThat(receivedNotifications.get(0).logger()).isEqualTo("test-logger"); + assertThat(receivedNotifications.get(0).data()).isEqualTo("Notice message"); + + // Second notification should be ERROR level + assertThat(receivedNotifications.get(1).level()).isEqualTo(McpSchema.LoggingLevel.ERROR); + assertThat(receivedNotifications.get(1).logger()).isEqualTo("test-logger"); + assertThat(receivedNotifications.get(1).data()).isEqualTo("Error message"); + + // Third notification should be ERROR level + assertThat(receivedNotifications.get(2).level()).isEqualTo(McpSchema.LoggingLevel.ERROR); + assertThat(receivedNotifications.get(2).logger()).isEqualTo("test-logger"); + assertThat(receivedNotifications.get(2).data()).isEqualTo("Another error message"); + }); + } } } From 86c520812161ba9e5f398fd78cf7b325018479ad Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Thu, 10 Apr 2025 12:53:21 +0200 Subject: [PATCH 4/8] refactor: Move logging level management from McpServerSession to McpAsyncServerExchange Signed-off-by: Christian Tzolov --- .../server/McpAsyncServerExchange.java | 19 +++++++++--- .../spec/McpServerSession.java | 31 ------------------- 2 files changed, 14 insertions(+), 36 deletions(-) diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java index f6a3ef37..f6246d1a 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java @@ -28,6 +28,8 @@ public class McpAsyncServerExchange { private final McpSchema.Implementation clientInfo; + private volatile LoggingLevel minLoggingLevel = LoggingLevel.INFO; + private static final TypeReference CREATE_MESSAGE_RESULT_TYPE_REF = new TypeReference<>() { }; @@ -122,11 +124,10 @@ public Mono loggingNotification(LoggingMessageNotification loggingMessageN } return Mono.defer(() -> { - if (!this.session.isLoingLevelEnabled(loggingMessageNotification.level())) { - return Mono.empty(); + if (this.isNotificationForLevelAllowed(loggingMessageNotification.level())) { + return this.session.sendNotification(McpSchema.METHOD_NOTIFICATION_MESSAGE, loggingMessageNotification); } - - return this.session.sendNotification(McpSchema.METHOD_NOTIFICATION_MESSAGE, loggingMessageNotification); + return Mono.empty(); }); } @@ -137,7 +138,15 @@ public Mono loggingNotification(LoggingMessageNotification loggingMessageN */ void setMinLoggingLevel(LoggingLevel minLoggingLevel) { Assert.notNull(minLoggingLevel, "minLoggingLevel must not be null"); - this.session.setMinLoggingLevel(minLoggingLevel); + this.minLoggingLevel = minLoggingLevel; + } + + /** + * Checks if the logging level bigger or equal to the minimum set logging level. + * @return true if the logging level is enabled, false otherwise + */ + private boolean isNotificationForLevelAllowed(LoggingLevel loggingLevel) { + return loggingLevel.level() >= this.minLoggingLevel.level(); } } diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java index ecf519bb..691ae41b 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java @@ -9,8 +9,6 @@ import com.fasterxml.jackson.core.type.TypeReference; import io.modelcontextprotocol.server.McpAsyncServerExchange; -import io.modelcontextprotocol.spec.McpSchema.LoggingLevel; -import io.modelcontextprotocol.util.Assert; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Mono; @@ -55,8 +53,6 @@ public class McpServerSession implements McpSession { private final AtomicInteger state = new AtomicInteger(STATE_UNINITIALIZED); - private volatile LoggingLevel minLoggingLevel = LoggingLevel.INFO; - /** * Creates a new server session with the given parameters and the transport to use. * @param id session id @@ -88,23 +84,6 @@ public String getId() { return this.id; } - /** - * Checks if the logging level bigger or equal to the minimum set logging level. - * @return true if the logging level is enabled, false otherwise - */ - public boolean isLoingLevelEnabled(LoggingLevel loggingLevel) { - return loggingLevel.level() >= this.minLoggingLevel.level(); - } - - /** - * Set the minimum logging level for this session. - * @param minLoggingLevel the minimum logging level - */ - public void setMinLoggingLevel(LoggingLevel minLoggingLevel) { - Assert.notNull(minLoggingLevel, "minLoggingLevel can't be null"); - this.minLoggingLevel = minLoggingLevel; - } - /** * Called upon successful initialization sequence between the client and the server * with the client capabilities and information. @@ -153,16 +132,6 @@ public Mono sendRequest(String method, Object requestParams, TypeReferenc } @Override - public Mono sendNotification(String method, Object params) { - McpSchema.JSONRPCNotification jsonrpcNotification = new McpSchema.JSONRPCNotification(McpSchema.JSONRPC_VERSION, - method, params); - return this.transport.sendMessage(jsonrpcNotification); - } - - // NOTE: This is a workaround for the fact that the {@link #sendNotification(String, - // Map)} method doesn't accept types like LoggingMessageNotification - // TODO investigate if this method can replace the {@link #sendNotification(String, - // Map)} - Breaking change. public Mono sendNotification(String method, Object params) { McpSchema.JSONRPCNotification jsonrpcNotification = new McpSchema.JSONRPCNotification(McpSchema.JSONRPC_VERSION, method, this.transport.unmarshalFrom(params, new TypeReference>() { From 17804b6da1b6b79904dc33365b13d0ddfdf14543 Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Thu, 10 Apr 2025 13:31:21 +0200 Subject: [PATCH 5/8] revert the auto-closablefor async client/server Signed-off-by: Christian Tzolov --- .../WebFluxSseIntegrationTests.java | 48 +++++++++++-------- .../server/WebMvcSseIntegrationTests.java | 11 +++-- .../client/McpAsyncClient.java | 3 +- .../server/McpAsyncServer.java | 3 +- ...ervletSseServerCustomContextPathTests.java | 3 +- ...rverTransportProviderIntegrationTests.java | 16 +++++-- 6 files changed, 50 insertions(+), 34 deletions(-) diff --git a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java index 4e660c20..57aa45c9 100644 --- a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java +++ b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java @@ -112,13 +112,14 @@ void testCreateMessageWithoutSamplingCapabilities(String clientType) { return Mono.just(mock(CallToolResult.class)); }); - try (//@formatter:off - var server = McpServer.async( - mcpServerTransportProvider) - .serverInfo("test-server", "1.0.0") - .tools(tool) - .build(); - + //@formatter:off + var server = McpServer.async( + mcpServerTransportProvider) + .serverInfo("test-server", "1.0.0") + .tools(tool) + .build(); + + try ( var client = clientBuilder .clientInfo(new McpSchema.Implementation("Sample " + "client", "0.0.0")) .build();) { @@ -132,7 +133,8 @@ void testCreateMessageWithoutSamplingCapabilities(String clientType) { assertThat(e).isInstanceOf(McpError.class) .hasMessage("Client must be configured with sampling capabilities"); } - } //@formatter:on + } //@formatter:on + server.close(); } @ParameterizedTest(name = "{0} : {displayName} ") @@ -178,13 +180,13 @@ void testCreateMessageSuccess(String clientType) throws InterruptedException { return Mono.just(callResponse); }); - try (var mcpServer = McpServer - .async(//@formatter:off - mcpServerTransportProvider) - .serverInfo("test-server", "1.0.0") - .tools(tool) - .build(); - + //@formatter:off + var mcpServer = McpServer.async(mcpServerTransportProvider) + .serverInfo("test-server", "1.0.0") + .tools(tool) + .build(); + + try ( var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0")) .capabilities(ClientCapabilities.builder().sampling().build()) .sampling(samplingHandler) @@ -198,6 +200,7 @@ void testCreateMessageSuccess(String clientType) throws InterruptedException { assertThat(response).isNotNull(); assertThat(response).isEqualTo(callResponse); } + mcpServer.close(); } // --------------------------------------- @@ -566,12 +569,14 @@ void testLoggingNotification(String clientType) { //@formatter:on }); - try ( //@formatter:off - var mcpServer = McpServer.async(mcpServerTransportProvider) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().logging().tools(true).build()) - .tools(tool) - .build(); + //@formatter:off + var mcpServer = McpServer.async(mcpServerTransportProvider) + .serverInfo("test-server", "1.0.0") + .capabilities(ServerCapabilities.builder().logging().tools(true).build()) + .tools(tool) + .build(); + + try ( // Create client with logging notification handler var mcpClient = clientBuilder @@ -613,6 +618,7 @@ void testLoggingNotification(String clientType) { assertThat(receivedNotifications.get(2).data()).isEqualTo("Another error message"); }); } + mcpServer.close(); } } diff --git a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java index 0115200b..fe2a9f52 100644 --- a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java +++ b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java @@ -30,6 +30,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.testcontainers.shaded.org.checkerframework.checker.units.qual.s; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -126,11 +127,12 @@ void testCreateMessageWithoutSamplingCapabilities() { }); //@formatter:off - try (var server = McpServer.async(mcpServerTransportProvider) + var server = McpServer.async(mcpServerTransportProvider) .serverInfo("test-server", "1.0.0") .tools(tool) .build(); - + + try ( // Create client without sampling capabilities var client = clientBuilder .clientInfo(new McpSchema.Implementation("Sample " + "client", "0.0.0")) @@ -146,6 +148,7 @@ void testCreateMessageWithoutSamplingCapabilities() { .hasMessage("Client must be configured with sampling capabilities"); } } + server.close(); } @Test @@ -189,11 +192,12 @@ void testCreateMessageSuccess() throws InterruptedException { }); //@formatter:off - try (var mcpServer = McpServer.async(mcpServerTransportProvider) + var mcpServer = McpServer.async(mcpServerTransportProvider) .serverInfo("test-server", "1.0.0") .tools(tool) .build(); + try ( var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0")) .capabilities(ClientCapabilities.builder().sampling().build()) .sampling(samplingHandler) @@ -206,6 +210,7 @@ void testCreateMessageSuccess() throws InterruptedException { assertThat(response).isNotNull().isEqualTo(callResponse); } + mcpServer.close(); } // --------------------------------------- diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java index 2aae3cee..81580b93 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java +++ b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java @@ -75,7 +75,7 @@ * @see McpSchema * @see McpClientSession */ -public class McpAsyncClient implements AutoCloseable { +public class McpAsyncClient { private static final Logger logger = LoggerFactory.getLogger(McpAsyncClient.class); @@ -275,7 +275,6 @@ public McpSchema.Implementation getClientInfo() { /** * Closes the client connection immediately. */ - @Override public void close() { this.mcpSession.close(); } diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java index 3de9ed17..e761f8fc 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java @@ -72,7 +72,7 @@ * @see McpSchema * @see McpClientSession */ -public class McpAsyncServer implements AutoCloseable { +public class McpAsyncServer { private static final Logger logger = LoggerFactory.getLogger(McpAsyncServer.class); @@ -121,7 +121,6 @@ public Mono closeGracefully() { /** * Close the server immediately. */ - @Override public void close() { this.delegate.close(); } diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerCustomContextPathTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerCustomContextPathTests.java index 0120c097..212a3c95 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerCustomContextPathTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerCustomContextPathTests.java @@ -77,12 +77,13 @@ public void after() { @Test void testCustomContextPath() { + var server = McpServer.async(mcpServerTransportProvider).serverInfo("test-server", "1.0.0").build(); try (//@formatter:off - var server = McpServer.async(mcpServerTransportProvider).serverInfo("test-server", "1.0.0").build(); var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample " + "client", "0.0.0")) .build()) { //@formatter:on assertThat(client.initialize()).isNotNull(); } + server.close(); } } diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java index 2fe93896..9bbb1c4f 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java @@ -34,6 +34,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.testcontainers.shaded.org.checkerframework.checker.units.qual.m; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -112,11 +113,12 @@ void testCreateMessageWithoutSamplingCapabilities() { }); //@formatter:off - try (var server = McpServer.async(mcpServerTransportProvider) + var server = McpServer.async(mcpServerTransportProvider) .serverInfo("test-server", "1.0.0") .tools(tool) .build(); - + + try ( // Create client without sampling capabilities var client = clientBuilder .clientInfo(new McpSchema.Implementation("Sample " + "client", "0.0.0")) @@ -132,6 +134,7 @@ void testCreateMessageWithoutSamplingCapabilities() { .hasMessage("Client must be configured with sampling capabilities"); } } + server.close(); } @Test @@ -175,11 +178,12 @@ void testCreateMessageSuccess() throws InterruptedException { }); //@formatter:off - try (var mcpServer = McpServer.async(mcpServerTransportProvider) + var mcpServer = McpServer.async(mcpServerTransportProvider) .serverInfo("test-server", "1.0.0") .tools(tool) .build(); + try ( var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0")) .capabilities(ClientCapabilities.builder().sampling().build()) .sampling(samplingHandler) @@ -193,6 +197,7 @@ void testCreateMessageSuccess() throws InterruptedException { assertThat(response).isNotNull(); assertThat(response).isEqualTo(callResponse); } + mcpServer.close(); } // --------------------------------------- @@ -544,12 +549,12 @@ void testLoggingNotification() { }); //@formatter:off - try (var mcpServer = McpServer.async(mcpServerTransportProvider) + var mcpServer = McpServer.async(mcpServerTransportProvider) .serverInfo("test-server", "1.0.0") .capabilities(ServerCapabilities.builder().logging().tools(true).build()) .tools(tool) .build(); - + try ( // Create client with logging notification handler var mcpClient = clientBuilder.loggingConsumer(notification -> { receivedNotifications.add(notification); @@ -592,6 +597,7 @@ void testLoggingNotification() { assertThat(receivedNotifications.get(2).data()).isEqualTo("Another error message"); }); } + mcpServer.close(); } } From 2e49583196f024222dfdb56f031ab4cdac114784 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Thu, 10 Apr 2025 15:07:32 +0200 Subject: [PATCH 6/8] Polish and fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Dariusz Jędrzejczyk --- .../WebFluxSseIntegrationTests.java | 115 +++++----- .../client/AbstractMcpAsyncClientTests.java | 16 +- .../client/McpAsyncClient.java | 4 +- .../client/McpSyncClient.java | 5 +- .../server/McpAsyncServer.java | 28 +-- .../server/McpAsyncServerExchange.java | 4 - .../server/McpSyncServer.java | 19 +- .../server/McpSyncServerExchange.java | 16 +- .../spec/McpServerSession.java | 3 +- .../client/AbstractMcpAsyncClientTests.java | 210 +++++++++--------- ...rverTransportProviderIntegrationTests.java | 118 +++++----- 11 files changed, 267 insertions(+), 271 deletions(-) diff --git a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java index 57aa45c9..15b5765e 100644 --- a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java +++ b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java @@ -112,15 +112,13 @@ void testCreateMessageWithoutSamplingCapabilities(String clientType) { return Mono.just(mock(CallToolResult.class)); }); - //@formatter:off var server = McpServer.async( mcpServerTransportProvider) .serverInfo("test-server", "1.0.0") .tools(tool) .build(); - try ( - var client = clientBuilder + try (var client = clientBuilder .clientInfo(new McpSchema.Implementation("Sample " + "client", "0.0.0")) .build();) { @@ -133,7 +131,7 @@ void testCreateMessageWithoutSamplingCapabilities(String clientType) { assertThat(e).isInstanceOf(McpError.class) .hasMessage("Client must be configured with sampling capabilities"); } - } //@formatter:on + } server.close(); } @@ -180,7 +178,6 @@ void testCreateMessageSuccess(String clientType) throws InterruptedException { return Mono.just(callResponse); }); - //@formatter:off var mcpServer = McpServer.async(mcpServerTransportProvider) .serverInfo("test-server", "1.0.0") .tools(tool) @@ -190,7 +187,7 @@ void testCreateMessageSuccess(String clientType) throws InterruptedException { var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0")) .capabilities(ClientCapabilities.builder().sampling().build()) .sampling(samplingHandler) - .build()) {// @formatter:on + .build()) { InitializeResult initResult = mcpClient.initialize(); assertThat(initResult).isNotNull(); @@ -215,14 +212,13 @@ void testRootsSuccess(String clientType) { AtomicReference> rootsRef = new AtomicReference<>(); - try (// @formatter:off - var mcpServer = McpServer.sync(mcpServerTransportProvider) - .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) - .build(); + var mcpServer = McpServer.sync(mcpServerTransportProvider) + .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) + .build(); - var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) + try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) .roots(roots) - .build()) { // @formatter:on + .build()) { InitializeResult initResult = mcpClient.initialize(); assertThat(initResult).isNotNull(); @@ -250,6 +246,8 @@ void testRootsSuccess(String clientType) { assertThat(rootsRef.get()).containsAll(List.of(roots.get(1), root3)); }); } + + mcpServer.close(); } @ParameterizedTest(name = "{0} : {displayName} ") @@ -266,16 +264,16 @@ void testRootsWithoutCapability(String clientType) { return mock(CallToolResult.class); }); - try (// @formatter:off - var mcpServer = McpServer.sync(mcpServerTransportProvider) - .rootsChangeHandler((exchange, rootsUpdate) -> {}) - .tools(tool) - .build(); + var mcpServer = McpServer.sync(mcpServerTransportProvider) + .rootsChangeHandler((exchange, rootsUpdate) -> {}) + .tools(tool) + .build(); + try ( // Create client without roots capability var mcpClient = clientBuilder .capabilities(ClientCapabilities.builder().build()) - .build()) { // @formatter:on} + .build()) { assertThat(mcpClient.initialize()).isNotNull(); @@ -287,6 +285,8 @@ void testRootsWithoutCapability(String clientType) { assertThat(e).isInstanceOf(McpError.class).hasMessage("Roots not supported"); } } + + mcpServer.close(); } @ParameterizedTest(name = "{0} : {displayName} ") @@ -296,14 +296,13 @@ void testRootsNotifciationWithEmptyRootsList(String clientType) { AtomicReference> rootsRef = new AtomicReference<>(); - try ( //@formatter:off - var mcpServer = McpServer.sync(mcpServerTransportProvider) - .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) - .build(); + var mcpServer = McpServer.sync(mcpServerTransportProvider) + .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) + .build(); - var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) + try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) .roots(List.of()) // Empty roots list - .build()) { // @formatter:on + .build()) { assertThat(mcpClient.initialize()).isNotNull(); @@ -313,6 +312,8 @@ void testRootsNotifciationWithEmptyRootsList(String clientType) { assertThat(rootsRef.get()).isEmpty(); }); } + + mcpServer.close(); } @ParameterizedTest(name = "{0} : {displayName} ") @@ -326,15 +327,14 @@ void testRootsWithMultipleHandlers(String clientType) { AtomicReference> rootsRef1 = new AtomicReference<>(); AtomicReference> rootsRef2 = new AtomicReference<>(); - try ( //@formatter:off - var mcpServer = McpServer.sync(mcpServerTransportProvider) - .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef1.set(rootsUpdate)) - .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef2.set(rootsUpdate)) - .build(); + var mcpServer = McpServer.sync(mcpServerTransportProvider) + .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef1.set(rootsUpdate)) + .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef2.set(rootsUpdate)) + .build(); - var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) + try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) .roots(roots) - .build()) { // @formatter:on + .build()) { InitializeResult initResult = mcpClient.initialize(); assertThat(initResult).isNotNull(); @@ -346,6 +346,8 @@ void testRootsWithMultipleHandlers(String clientType) { assertThat(rootsRef2.get()).containsAll(roots); }); } + + mcpServer.close(); } @ParameterizedTest(name = "{0} : {displayName} ") @@ -358,14 +360,13 @@ void testRootsServerCloseWithActiveSubscription(String clientType) { AtomicReference> rootsRef = new AtomicReference<>(); - try ( //@formatter:off - var mcpServer = McpServer.sync(mcpServerTransportProvider) - .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) - .build(); + var mcpServer = McpServer.sync(mcpServerTransportProvider) + .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) + .build(); - var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) + try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) .roots(roots) - .build()) { // @formatter:on + .build()) { InitializeResult initResult = mcpClient.initialize(); assertThat(initResult).isNotNull(); @@ -376,6 +377,8 @@ void testRootsServerCloseWithActiveSubscription(String clientType) { assertThat(rootsRef.get()).containsAll(roots); }); } + + mcpServer.close(); } // --------------------------------------- @@ -409,13 +412,12 @@ void testToolCallSuccess(String clientType) { return callResponse; }); - try ( //@formatter:off - var mcpServer = McpServer.sync(mcpServerTransportProvider) - .capabilities(ServerCapabilities.builder().tools(true).build()) - .tools(tool1) - .build(); + var mcpServer = McpServer.sync(mcpServerTransportProvider) + .capabilities(ServerCapabilities.builder().tools(true).build()) + .tools(tool1) + .build(); - var mcpClient = clientBuilder.build()) { // @formatter:on + try (var mcpClient = clientBuilder.build()) { InitializeResult initResult = mcpClient.initialize(); assertThat(initResult).isNotNull(); @@ -427,6 +429,8 @@ void testToolCallSuccess(String clientType) { assertThat(response).isNotNull(); assertThat(response).isEqualTo(callResponse); } + + mcpServer.close(); } @ParameterizedTest(name = "{0} : {displayName} ") @@ -450,12 +454,12 @@ void testToolListChangeHandlingSuccess(String clientType) { AtomicReference> rootsRef = new AtomicReference<>(); - try ( //@formatter:off - var mcpServer = McpServer.sync(mcpServerTransportProvider) - .capabilities(ServerCapabilities.builder().tools(true).build()) - .tools(tool1) - .build(); + var mcpServer = McpServer.sync(mcpServerTransportProvider) + .capabilities(ServerCapabilities.builder().tools(true).build()) + .tools(tool1) + .build(); + try ( var mcpClient = clientBuilder.toolsChangeConsumer(toolsUpdate -> { // perform a blocking call to a remote service String response = RestClient.create() @@ -465,7 +469,7 @@ void testToolListChangeHandlingSuccess(String clientType) { .body(String.class); assertThat(response).isNotBlank(); rootsRef.set(toolsUpdate); - }).build()) { // @formatter:on + }).build()) { InitializeResult initResult = mcpClient.initialize(); assertThat(initResult).isNotNull(); @@ -498,6 +502,8 @@ void testToolListChangeHandlingSuccess(String clientType) { assertThat(rootsRef.get()).containsAll(List.of(tool2.tool())); }); } + + mcpServer.close(); } @ParameterizedTest(name = "{0} : {displayName} ") @@ -506,13 +512,14 @@ void testInitialize(String clientType) { var clientBuilder = clientBuilders.get(clientType); - try (// @formatter:off - var mcpServer = McpServer.sync(mcpServerTransportProvider).build(); - var mcpClient = clientBuilder.build()) { // @formatter:on + var mcpServer = McpServer.sync(mcpServerTransportProvider).build(); + try (var mcpClient = clientBuilder.build()) { InitializeResult initResult = mcpClient.initialize(); assertThat(initResult).isNotNull(); } + + mcpServer.close(); } // --------------------------------------- @@ -569,7 +576,6 @@ void testLoggingNotification(String clientType) { //@formatter:on }); - //@formatter:off var mcpServer = McpServer.async(mcpServerTransportProvider) .serverInfo("test-server", "1.0.0") .capabilities(ServerCapabilities.builder().logging().tools(true).build()) @@ -577,11 +583,10 @@ void testLoggingNotification(String clientType) { .build(); try ( - // Create client with logging notification handler var mcpClient = clientBuilder .loggingConsumer(notification -> { receivedNotifications.add(notification); }) - .build()) { // @formatter:on + .build()) { // Initialize client InitializeResult initResult = mcpClient.initialize(); diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java b/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java index c7892b17..f9f5b356 100644 --- a/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java +++ b/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java @@ -31,6 +31,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -453,15 +454,12 @@ void testLoggingLevelsWithoutInitialization() { @Test void testLoggingLevels() { withClient(createMcpTransport(), mcpAsyncClient -> { - Mono testAllLevels = mcpAsyncClient.initialize().then(Mono.defer(() -> { - Mono chain = Mono.empty(); - for (McpSchema.LoggingLevel level : McpSchema.LoggingLevel.values()) { - chain = chain.then(mcpAsyncClient.setLoggingLevel(level)); - } - return chain; - })); - - StepVerifier.create(testAllLevels).expectNextCount(1).verifyComplete(); + StepVerifier.create(mcpAsyncClient.initialize() + .thenMany(Flux.fromArray(McpSchema.LoggingLevel.values()) + .flatMap(mcpAsyncClient::setLoggingLevel)) + .collectList()) + .assertNext(l -> assertThat(l).hasSize(McpSchema.LoggingLevel.values().length)) + .verifyComplete(); }); } diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java index 81580b93..df099836 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java +++ b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java @@ -780,7 +780,7 @@ private NotificationHandler asyncLoggingNotificationHandler( * @return A Mono that completes when the logging level is set. * @see McpSchema.LoggingLevel */ - public Mono setLoggingLevel(LoggingLevel loggingLevel) { + public Mono setLoggingLevel(LoggingLevel loggingLevel) { if (loggingLevel == null) { return Mono.error(new McpError("Logging level must not be null")); } @@ -788,7 +788,7 @@ public Mono setLoggingLevel(LoggingLevel loggingLevel) { return this.withInitializationCheck("setting logging level", initializedResult -> { var params = new McpSchema.SetLevelRequest(loggingLevel); return this.mcpSession.sendRequest(McpSchema.METHOD_LOGGING_SET_LEVEL, params, new TypeReference() { - }); + }).then(); }); } diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java b/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java index 5748b825..32cf325e 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java +++ b/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java @@ -320,10 +320,9 @@ public GetPromptResult getPrompt(GetPromptRequest getPromptRequest) { /** * Client can set the minimum logging level it wants to receive from the server. * @param loggingLevel the min logging level - * @return empty response */ - public Object setLoggingLevel(McpSchema.LoggingLevel loggingLevel) { - return this.delegate.setLoggingLevel(loggingLevel).block(); + public void setLoggingLevel(McpSchema.LoggingLevel loggingLevel) { + this.delegate.setLoggingLevel(loggingLevel).block(); } } diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java index e761f8fc..4d0a7248 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java @@ -217,11 +217,17 @@ public Mono notifyPromptsListChanged() { // --------------------------------------- /** - * Send a logging message notification to all connected clients. Messages below the - * current minimum logging level will be filtered out. + * This implementation would, incorrectly, broadcast the logging message to all + * connected clients, using a single minLoggingLevel for all of them. Similar to + * the sampling and roots, the logging level should be set per client session and + * use the ServerExchange to send the logging message to the right client. * @param loggingMessageNotification The logging message to send * @return A Mono that completes when the notification has been sent + * @deprecated Use + * {@link McpAsyncServerExchange#loggingNotification(LoggingMessageNotification)} + * instead. */ + @Deprecated public Mono loggingNotification(LoggingMessageNotification loggingMessageNotification) { return this.delegate.loggingNotification(loggingMessageNotification); } @@ -258,9 +264,8 @@ private static class AsyncServerImpl extends McpAsyncServer { private final ConcurrentHashMap prompts = new ConcurrentHashMap<>(); - // TODO: this field is deprecated and should be remvoed together with the + // FIXME: this field is deprecated and should be remvoed together with the // broadcasting loggingNotification. - @Deprecated private LoggingLevel minLoggingLevel = LoggingLevel.DEBUG; private List protocolVersions = List.of(McpSchema.LATEST_PROTOCOL_VERSION); @@ -666,17 +671,7 @@ private McpServerSession.RequestHandler promptsGetReq // Logging Management // --------------------------------------- - /** - * This implementation would, incorrectly, broadcast the logging message to all - * connected clients, using a single minLoggingLevel for all of them. Similar to - * the sampling and roots, the logging level should be set per client session and - * use the ServerExchange to send the logging message to the right client. - * @deprecated Use - * {@link McpAsyncServerExchange#loggingNotification(LoggingMessageNotification)} - * instead. - */ @Override - @Deprecated public Mono loggingNotification(LoggingMessageNotification loggingMessageNotification) { if (loggingMessageNotification == null) { @@ -701,9 +696,8 @@ private McpServerSession.RequestHandler setLoggerRequestHandler() { exchange.setMinLoggingLevel(newMinLoggingLevel.level()); - // TODO: this field is deprecated and should be remvoed together with - // the - // broadcasting loggingNotification. + // FIXME: this field is deprecated and should be removed together + // with the broadcasting loggingNotification. this.minLoggingLevel = newMinLoggingLevel.level(); return Mono.just(Map.of()); diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java index f6246d1a..889dc66d 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java @@ -141,10 +141,6 @@ void setMinLoggingLevel(LoggingLevel minLoggingLevel) { this.minLoggingLevel = minLoggingLevel; } - /** - * Checks if the logging level bigger or equal to the minimum set logging level. - * @return true if the logging level is enabled, false otherwise - */ private boolean isNotificationForLevelAllowed(LoggingLevel loggingLevel) { return loggingLevel.level() >= this.minLoggingLevel.level(); } diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServer.java index 10311959..cad27c4b 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServer.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServer.java @@ -5,6 +5,7 @@ package io.modelcontextprotocol.server; import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification; import io.modelcontextprotocol.util.Assert; /** @@ -46,7 +47,7 @@ * @see McpAsyncServer * @see McpSchema */ -public class McpSyncServer implements AutoCloseable { +public class McpSyncServer { /** * The async server to wrap. @@ -147,6 +148,21 @@ public void notifyPromptsListChanged() { this.asyncServer.notifyPromptsListChanged().block(); } + /** + * This implementation would, incorrectly, broadcast the logging message to all + * connected clients, using a single minLoggingLevel for all of them. Similar to + * the sampling and roots, the logging level should be set per client session and + * use the ServerExchange to send the logging message to the right client. + * @param loggingMessageNotification The logging message to send + * @deprecated Use + * {@link McpSyncServerExchange#loggingNotification(LoggingMessageNotification)} + * instead. + */ + @Deprecated + public void loggingNotification(LoggingMessageNotification loggingMessageNotification) { + this.asyncServer.loggingNotification(loggingMessageNotification).block(); + } + /** * Close the server gracefully. */ @@ -157,7 +173,6 @@ public void closeGracefully() { /** * Close the server immediately. */ - @Override public void close() { this.asyncServer.close(); } diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServerExchange.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServerExchange.java index 32dfe2b2..a3fbf42f 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServerExchange.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServerExchange.java @@ -7,7 +7,6 @@ import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema.LoggingLevel; import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification; -import reactor.core.publisher.Mono; /** * Represents a synchronous exchange with a Model Context Protocol (MCP) client. The @@ -86,19 +85,8 @@ public McpSchema.ListRootsResult listRoots(String cursor) { * Send a logging message notification to all connected clients. Messages below the * current minimum logging level will be filtered out. * @param loggingMessageNotification The logging message to send - * @return A Mono that completes when the notification has been sent */ - public Mono loggingNotification(LoggingMessageNotification loggingMessageNotification) { - return this.exchange.loggingNotification(loggingMessageNotification); + public void loggingNotification(LoggingMessageNotification loggingMessageNotification) { + this.exchange.loggingNotification(loggingMessageNotification).block(); } - - /** - * Set the minimum logging level for the client. Messages below this level will be - * filtered out. - * @param minLoggingLevel The minimum logging level - */ - void setMinLoggingLevel(LoggingLevel minLoggingLevel) { - this.exchange.setMinLoggingLevel(minLoggingLevel); - } - } diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java index 691ae41b..46014af8 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpServerSession.java @@ -134,8 +134,7 @@ public Mono sendRequest(String method, Object requestParams, TypeReferenc @Override public Mono sendNotification(String method, Object params) { McpSchema.JSONRPCNotification jsonrpcNotification = new McpSchema.JSONRPCNotification(McpSchema.JSONRPC_VERSION, - method, this.transport.unmarshalFrom(params, new TypeReference>() { - })); + method, params); return this.transport.sendMessage(jsonrpcNotification); } diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java b/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java index 541dadf6..9bf7de91 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java @@ -31,6 +31,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -75,9 +76,9 @@ McpAsyncClient client(McpClientTransport transport, Function { McpClient.AsyncSpec builder = McpClient.async(transport) - .requestTimeout(getRequestTimeout()) - .initializationTimeout(getInitializationTimeout()) - .capabilities(ClientCapabilities.builder().roots(true).build()); + .requestTimeout(getRequestTimeout()) + .initializationTimeout(getInitializationTimeout()) + .capabilities(ClientCapabilities.builder().roots(true).build()); builder = customizer.apply(builder); client.set(builder.build()); }).doesNotThrowAnyException(); @@ -113,22 +114,22 @@ void tearDown() { void verifyInitializationTimeout(Function> 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(); + .expectSubscription() + .thenAwait(getInitializationTimeout()) + .consumeErrorWith(e -> assertThat(e).isInstanceOf(McpError.class) + .hasMessage("Client must be initialized before " + action)) + .verify(); }); } @Test void testConstructorWithInvalidArguments() { assertThatThrownBy(() -> McpClient.async(null).build()).isInstanceOf(IllegalArgumentException.class) - .hasMessage("Transport must not be null"); + .hasMessage("Transport must not be null"); assertThatThrownBy(() -> McpClient.async(createMcpTransport()).requestTimeout(null).build()) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Request timeout must not be null"); + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Request timeout must not be null"); } @Test @@ -140,14 +141,14 @@ void testListToolsWithoutInitialization() { void testListTools() { withClient(createMcpTransport(), mcpAsyncClient -> { StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listTools(null))) - .consumeNextWith(result -> { - assertThat(result.tools()).isNotNull().isNotEmpty(); - - Tool firstTool = result.tools().get(0); - assertThat(firstTool.name()).isNotNull(); - assertThat(firstTool.description()).isNotNull(); - }) - .verifyComplete(); + .consumeNextWith(result -> { + assertThat(result.tools()).isNotNull().isNotEmpty(); + + Tool firstTool = result.tools().get(0); + assertThat(firstTool.name()).isNotNull(); + assertThat(firstTool.description()).isNotNull(); + }) + .verifyComplete(); }); } @@ -160,8 +161,8 @@ void testPingWithoutInitialization() { void testPing() { withClient(createMcpTransport(), mcpAsyncClient -> { StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.ping())) - .expectNextCount(1) - .verifyComplete(); + .expectNextCount(1) + .verifyComplete(); }); } @@ -177,13 +178,13 @@ void testCallTool() { CallToolRequest callToolRequest = new CallToolRequest("echo", Map.of("message", ECHO_TEST_MESSAGE)); StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.callTool(callToolRequest))) - .consumeNextWith(callToolResult -> { - assertThat(callToolResult).isNotNull().satisfies(result -> { - assertThat(result.content()).isNotNull(); - assertThat(result.isError()).isNull(); - }); - }) - .verifyComplete(); + .consumeNextWith(callToolResult -> { + assertThat(callToolResult).isNotNull().satisfies(result -> { + assertThat(result.content()).isNotNull(); + assertThat(result.isError()).isNull(); + }); + }) + .verifyComplete(); }); } @@ -194,9 +195,9 @@ void testCallToolWithInvalidTool() { Map.of("message", ECHO_TEST_MESSAGE)); StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.callTool(invalidRequest))) - .consumeErrorWith( - e -> assertThat(e).isInstanceOf(McpError.class).hasMessage("Unknown tool: nonexistent_tool")) - .verify(); + .consumeErrorWith( + e -> assertThat(e).isInstanceOf(McpError.class).hasMessage("Unknown tool: nonexistent_tool")) + .verify(); }); } @@ -209,18 +210,18 @@ void testListResourcesWithoutInitialization() { void testListResources() { withClient(createMcpTransport(), mcpAsyncClient -> { StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listResources(null))) - .consumeNextWith(resources -> { - assertThat(resources).isNotNull().satisfies(result -> { - assertThat(result.resources()).isNotNull(); - - if (!result.resources().isEmpty()) { - Resource firstResource = result.resources().get(0); - assertThat(firstResource.uri()).isNotNull(); - assertThat(firstResource.name()).isNotNull(); - } - }); - }) - .verifyComplete(); + .consumeNextWith(resources -> { + assertThat(resources).isNotNull().satisfies(result -> { + assertThat(result.resources()).isNotNull(); + + if (!result.resources().isEmpty()) { + Resource firstResource = result.resources().get(0); + assertThat(firstResource.uri()).isNotNull(); + assertThat(firstResource.name()).isNotNull(); + } + }); + }) + .verifyComplete(); }); } @@ -240,18 +241,18 @@ void testListPromptsWithoutInitialization() { void testListPrompts() { withClient(createMcpTransport(), mcpAsyncClient -> { StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listPrompts(null))) - .consumeNextWith(prompts -> { - assertThat(prompts).isNotNull().satisfies(result -> { - assertThat(result.prompts()).isNotNull(); - - if (!result.prompts().isEmpty()) { - Prompt firstPrompt = result.prompts().get(0); - assertThat(firstPrompt.name()).isNotNull(); - assertThat(firstPrompt.description()).isNotNull(); - } - }); - }) - .verifyComplete(); + .consumeNextWith(prompts -> { + assertThat(prompts).isNotNull().satisfies(result -> { + assertThat(result.prompts()).isNotNull(); + + if (!result.prompts().isEmpty()) { + Prompt firstPrompt = result.prompts().get(0); + assertThat(firstPrompt.name()).isNotNull(); + assertThat(firstPrompt.description()).isNotNull(); + } + }); + }) + .verifyComplete(); }); } @@ -265,15 +266,15 @@ void testGetPromptWithoutInitialization() { void testGetPrompt() { withClient(createMcpTransport(), mcpAsyncClient -> { StepVerifier - .create(mcpAsyncClient.initialize() - .then(mcpAsyncClient.getPrompt(new GetPromptRequest("simple_prompt", Map.of())))) - .consumeNextWith(prompt -> { - assertThat(prompt).isNotNull().satisfies(result -> { - assertThat(result.messages()).isNotEmpty(); - assertThat(result.messages()).hasSize(1); - }); - }) - .verifyComplete(); + .create(mcpAsyncClient.initialize() + .then(mcpAsyncClient.getPrompt(new GetPromptRequest("simple_prompt", Map.of())))) + .consumeNextWith(prompt -> { + assertThat(prompt).isNotNull().satisfies(result -> { + assertThat(result.messages()).isNotEmpty(); + assertThat(result.messages()).hasSize(1); + }); + }) + .verifyComplete(); }); } @@ -287,7 +288,7 @@ void testRootsListChangedWithoutInitialization() { void testRootsListChanged() { withClient(createMcpTransport(), mcpAsyncClient -> { StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.rootsListChangedNotification())) - .verifyComplete(); + .verifyComplete(); }); } @@ -311,8 +312,8 @@ void testAddRoot() { void testAddRootWithNullValue() { withClient(createMcpTransport(), mcpAsyncClient -> { StepVerifier.create(mcpAsyncClient.addRoot(null)) - .consumeErrorWith(e -> assertThat(e).isInstanceOf(McpError.class).hasMessage("Root must not be null")) - .verify(); + .consumeErrorWith(e -> assertThat(e).isInstanceOf(McpError.class).hasMessage("Root must not be null")) + .verify(); }); } @@ -330,9 +331,9 @@ void testRemoveRoot() { void testRemoveNonExistentRoot() { withClient(createMcpTransport(), mcpAsyncClient -> { StepVerifier.create(mcpAsyncClient.removeRoot("nonexistent-uri")) - .consumeErrorWith(e -> assertThat(e).isInstanceOf(McpError.class) - .hasMessage("Root with uri 'nonexistent-uri' not found")) - .verify(); + .consumeErrorWith(e -> assertThat(e).isInstanceOf(McpError.class) + .hasMessage("Root with uri 'nonexistent-uri' not found")) + .verify(); }); } @@ -361,11 +362,11 @@ void testListResourceTemplatesWithoutInitialization() { void testListResourceTemplates() { withClient(createMcpTransport(), mcpAsyncClient -> { StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listResourceTemplates())) - .consumeNextWith(result -> { - assertThat(result).isNotNull(); - assertThat(result.resourceTemplates()).isNotNull(); - }) - .verifyComplete(); + .consumeNextWith(result -> { + assertThat(result).isNotNull(); + assertThat(result.resourceTemplates()).isNotNull(); + }) + .verifyComplete(); }); } @@ -378,11 +379,11 @@ void testResourceSubscription() { // Test subscribe StepVerifier.create(mcpAsyncClient.subscribeResource(new SubscribeRequest(firstResource.uri()))) - .verifyComplete(); + .verifyComplete(); // Test unsubscribe StepVerifier.create(mcpAsyncClient.unsubscribeResource(new UnsubscribeRequest(firstResource.uri()))) - .verifyComplete(); + .verifyComplete(); } }).verifyComplete(); }); @@ -396,14 +397,14 @@ void testNotificationHandlers() { withClient(createMcpTransport(), builder -> builder - .toolsChangeConsumer(tools -> Mono.fromRunnable(() -> toolsNotificationReceived.set(true))) - .resourcesChangeConsumer( - resources -> Mono.fromRunnable(() -> resourcesNotificationReceived.set(true))) - .promptsChangeConsumer(prompts -> Mono.fromRunnable(() -> promptsNotificationReceived.set(true))), + .toolsChangeConsumer(tools -> Mono.fromRunnable(() -> toolsNotificationReceived.set(true))) + .resourcesChangeConsumer( + resources -> Mono.fromRunnable(() -> resourcesNotificationReceived.set(true))) + .promptsChangeConsumer(prompts -> Mono.fromRunnable(() -> promptsNotificationReceived.set(true))), mcpAsyncClient -> { StepVerifier.create(mcpAsyncClient.initialize()) - .expectNextMatches(Objects::nonNull) - .verifyComplete(); + .expectNextMatches(Objects::nonNull) + .verifyComplete(); }); } @@ -411,9 +412,9 @@ void testNotificationHandlers() { void testInitializeWithSamplingCapability() { ClientCapabilities capabilities = ClientCapabilities.builder().sampling().build(); CreateMessageResult createMessageResult = CreateMessageResult.builder() - .message("test") - .model("test-model") - .build(); + .message("test") + .model("test-model") + .build(); withClient(createMcpTransport(), builder -> builder.capabilities(capabilities).sampling(request -> Mono.just(createMessageResult)), client -> { @@ -424,21 +425,21 @@ void testInitializeWithSamplingCapability() { @Test void testInitializeWithAllCapabilities() { var capabilities = ClientCapabilities.builder() - .experimental(Map.of("feature", "test")) - .roots(true) - .sampling() - .build(); + .experimental(Map.of("feature", "test")) + .roots(true) + .sampling() + .build(); Function> samplingHandler = request -> Mono - .just(CreateMessageResult.builder().message("test").model("test-model").build()); + .just(CreateMessageResult.builder().message("test").model("test-model").build()); withClient(createMcpTransport(), builder -> builder.capabilities(capabilities).sampling(samplingHandler), client -> - StepVerifier.create(client.initialize()).assertNext(result -> { - assertThat(result).isNotNull(); - assertThat(result.capabilities()).isNotNull(); - }).verifyComplete()); + StepVerifier.create(client.initialize()).assertNext(result -> { + assertThat(result).isNotNull(); + assertThat(result.capabilities()).isNotNull(); + }).verifyComplete()); } // --------------------------------------- @@ -454,15 +455,12 @@ void testLoggingLevelsWithoutInitialization() { @Test void testLoggingLevels() { withClient(createMcpTransport(), mcpAsyncClient -> { - Mono testAllLevels = mcpAsyncClient.initialize().then(Mono.defer(() -> { - Mono chain = Mono.empty(); - for (McpSchema.LoggingLevel level : McpSchema.LoggingLevel.values()) { - chain = chain.then(mcpAsyncClient.setLoggingLevel(level)); - } - return chain; - })); - - StepVerifier.create(testAllLevels).expectNextCount(1).verifyComplete(); + StepVerifier.create(mcpAsyncClient.initialize() + .thenMany(Flux.fromArray(McpSchema.LoggingLevel.values()) + .flatMap(mcpAsyncClient::setLoggingLevel)) + .collectList()) + .assertNext(l -> assertThat(l).hasSize(McpSchema.LoggingLevel.values().length)) + .verifyComplete(); }); } @@ -484,8 +482,8 @@ void testLoggingConsumer() { void testLoggingWithNullNotification() { withClient(createMcpTransport(), mcpAsyncClient -> { StepVerifier.create(mcpAsyncClient.setLoggingLevel(null)) - .expectErrorMatches(error -> error.getMessage().contains("Logging level must not be null")) - .verify(); + .expectErrorMatches(error -> error.getMessage().contains("Logging level must not be null")) + .verify(); }); } diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java index 9bbb1c4f..d10cfeab 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java @@ -112,17 +112,16 @@ void testCreateMessageWithoutSamplingCapabilities() { return Mono.just(mock(CallToolResult.class)); }); - //@formatter:off var server = McpServer.async(mcpServerTransportProvider) .serverInfo("test-server", "1.0.0") .tools(tool) .build(); - - try ( + + try ( // Create client without sampling capabilities var client = clientBuilder .clientInfo(new McpSchema.Implementation("Sample " + "client", "0.0.0")) - .build()) {//@formatter:on + .build()) { assertThat(client.initialize()).isNotNull(); @@ -177,17 +176,15 @@ void testCreateMessageSuccess() throws InterruptedException { return Mono.just(callResponse); }); - //@formatter:off var mcpServer = McpServer.async(mcpServerTransportProvider) .serverInfo("test-server", "1.0.0") .tools(tool) .build(); - try ( - var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0")) + try (var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0")) .capabilities(ClientCapabilities.builder().sampling().build()) .sampling(samplingHandler) - .build()) {//@formatter:on + .build()) { InitializeResult initResult = mcpClient.initialize(); assertThat(initResult).isNotNull(); @@ -209,14 +206,14 @@ void testRootsSuccess() { AtomicReference> rootsRef = new AtomicReference<>(); - //@formatter:off - try (var mcpServer = McpServer.sync(mcpServerTransportProvider) + var mcpServer = McpServer.sync(mcpServerTransportProvider) .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) .build(); - var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) + try (var mcpClient = + clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) .roots(roots) - .build()) {//@formatter:on + .build()) { InitializeResult initResult = mcpClient.initialize(); assertThat(initResult).isNotNull(); @@ -243,6 +240,8 @@ void testRootsSuccess() { await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { assertThat(rootsRef.get()).containsAll(List.of(roots.get(1), root3)); }); + + mcpServer.close(); } } @@ -257,16 +256,14 @@ void testRootsWithoutCapability() { return mock(CallToolResult.class); }); - //@formatter:off - try (var mcpServer = McpServer.sync(mcpServerTransportProvider) - .rootsChangeHandler((exchange, rootsUpdate) -> {}) - .tools(tool) - .build(); + var mcpServer = McpServer.sync(mcpServerTransportProvider) + .rootsChangeHandler((exchange, rootsUpdate) -> {}) + .tools(tool) + .build(); - // Create client without roots capability - var mcpClient = clientBuilder + try (var mcpClient = clientBuilder .capabilities(ClientCapabilities.builder().build()) - .build()) {//@formatter:on + .build()) { assertThat(mcpClient.initialize()).isNotNull(); @@ -278,20 +275,21 @@ void testRootsWithoutCapability() { assertThat(e).isInstanceOf(McpError.class).hasMessage("Roots not supported"); } } + + mcpServer.close(); } @Test void testRootsNotifciationWithEmptyRootsList() { AtomicReference> rootsRef = new AtomicReference<>(); - //@formatter:off - try (var mcpServer = McpServer.sync(mcpServerTransportProvider) - .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) - .build(); + var mcpServer = McpServer.sync(mcpServerTransportProvider) + .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) + .build(); - var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) + try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) .roots(List.of()) // Empty roots list - .build()) {//@formatter:on + .build()) { InitializeResult initResult = mcpClient.initialize(); assertThat(initResult).isNotNull(); @@ -302,6 +300,8 @@ void testRootsNotifciationWithEmptyRootsList() { assertThat(rootsRef.get()).isEmpty(); }); } + + mcpServer.close(); } @Test @@ -311,15 +311,14 @@ void testRootsWithMultipleHandlers() { AtomicReference> rootsRef1 = new AtomicReference<>(); AtomicReference> rootsRef2 = new AtomicReference<>(); - //@formatter:off - try (var mcpServer = McpServer.sync(mcpServerTransportProvider) - .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef1.set(rootsUpdate)) - .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef2.set(rootsUpdate)) - .build(); + var mcpServer = McpServer.sync(mcpServerTransportProvider) + .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef1.set(rootsUpdate)) + .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef2.set(rootsUpdate)) + .build(); - var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) + try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) .roots(roots) - .build()) {//@formatter:on + .build()) { assertThat(mcpClient.initialize()).isNotNull(); @@ -330,6 +329,8 @@ void testRootsWithMultipleHandlers() { assertThat(rootsRef2.get()).containsAll(roots); }); } + + mcpServer.close(); } @Test @@ -338,14 +339,13 @@ void testRootsServerCloseWithActiveSubscription() { AtomicReference> rootsRef = new AtomicReference<>(); - //@formatter:off - try (var mcpServer = McpServer.sync(mcpServerTransportProvider) - .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) - .build(); + var mcpServer = McpServer.sync(mcpServerTransportProvider) + .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) + .build(); - var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) + try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) .roots(roots) - .build()) {//@formatter:on + .build()) { InitializeResult initResult = mcpClient.initialize(); assertThat(initResult).isNotNull(); @@ -356,6 +356,8 @@ void testRootsServerCloseWithActiveSubscription() { assertThat(rootsRef.get()).containsAll(roots); }); } + + mcpServer.close(); } // --------------------------------------- @@ -386,14 +388,12 @@ void testToolCallSuccess() { return callResponse; }); - //@formatter:off - try (var mcpServer = McpServer.sync(mcpServerTransportProvider) - .capabilities(ServerCapabilities.builder().tools(true).build()) - .tools(tool1) - .build(); - - var mcpClient = clientBuilder.build()) {//@formatter:on + var mcpServer = McpServer.sync(mcpServerTransportProvider) + .capabilities(ServerCapabilities.builder().tools(true).build()) + .tools(tool1) + .build(); + try (var mcpClient = clientBuilder.build()) { InitializeResult initResult = mcpClient.initialize(); assertThat(initResult).isNotNull(); @@ -404,6 +404,8 @@ void testToolCallSuccess() { assertThat(response).isNotNull(); assertThat(response).isEqualTo(callResponse); } + + mcpServer.close(); } @Test @@ -424,13 +426,12 @@ void testToolListChangeHandlingSuccess() { AtomicReference> rootsRef = new AtomicReference<>(); - //@formatter:off - try (var mcpServer = McpServer.sync(mcpServerTransportProvider) - .capabilities(ServerCapabilities.builder().tools(true).build()) - .tools(tool1) - .build(); + var mcpServer = McpServer.sync(mcpServerTransportProvider) + .capabilities(ServerCapabilities.builder().tools(true).build()) + .tools(tool1) + .build(); - var mcpClient = clientBuilder.toolsChangeConsumer(toolsUpdate -> { + try (var mcpClient = clientBuilder.toolsChangeConsumer(toolsUpdate -> { // perform a blocking call to a remote service String response = RestClient.create() .get() @@ -439,7 +440,7 @@ void testToolListChangeHandlingSuccess() { .body(String.class); assertThat(response).isNotBlank(); rootsRef.set(toolsUpdate); - }).build()) {//@formatter:on + }).build()) { InitializeResult initResult = mcpClient.initialize(); assertThat(initResult).isNotNull(); @@ -472,17 +473,21 @@ void testToolListChangeHandlingSuccess() { assertThat(rootsRef.get()).containsAll(List.of(tool2.tool())); }); } + + mcpServer.close(); } @Test void testInitialize() { + var mcpServer = McpServer.sync(mcpServerTransportProvider).build(); - try (var mcpServer = McpServer.sync(mcpServerTransportProvider).build(); - var mcpClient = clientBuilder.build()) { + try (var mcpClient = clientBuilder.build()) { InitializeResult initResult = mcpClient.initialize(); assertThat(initResult).isNotNull(); } + + mcpServer.close(); } // --------------------------------------- @@ -548,7 +553,6 @@ void testLoggingNotification() { return Mono.just(new CallToolResult("Logging test completed", false)); }); - //@formatter:off var mcpServer = McpServer.async(mcpServerTransportProvider) .serverInfo("test-server", "1.0.0") .capabilities(ServerCapabilities.builder().logging().tools(true).build()) @@ -558,7 +562,7 @@ void testLoggingNotification() { // Create client with logging notification handler var mcpClient = clientBuilder.loggingConsumer(notification -> { receivedNotifications.add(notification); - }).build()) {//@formatter:on + }).build()) { // Initialize client InitializeResult initResult = mcpClient.initialize(); From 96569aeb28e0f9912400201501babf729b0ddf7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Thu, 10 Apr 2025 15:08:25 +0200 Subject: [PATCH 7/8] Formatting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Dariusz Jędrzejczyk --- .../WebFluxSseIntegrationTests.java | 111 +++++----- .../client/AbstractMcpAsyncClientTests.java | 12 +- .../server/McpAsyncServer.java | 8 +- .../server/McpSyncServer.java | 6 +- .../server/McpSyncServerExchange.java | 1 + .../client/AbstractMcpAsyncClientTests.java | 206 +++++++++--------- ...rverTransportProviderIntegrationTests.java | 117 +++++----- 7 files changed, 221 insertions(+), 240 deletions(-) diff --git a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java index 15b5765e..d71fe1ab 100644 --- a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java +++ b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java @@ -112,15 +112,10 @@ void testCreateMessageWithoutSamplingCapabilities(String clientType) { return Mono.just(mock(CallToolResult.class)); }); - var server = McpServer.async( - mcpServerTransportProvider) - .serverInfo("test-server", "1.0.0") - .tools(tool) - .build(); - - try (var client = clientBuilder - .clientInfo(new McpSchema.Implementation("Sample " + "client", "0.0.0")) - .build();) { + var server = McpServer.async(mcpServerTransportProvider).serverInfo("test-server", "1.0.0").tools(tool).build(); + + try (var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample " + "client", "0.0.0")) + .build();) { assertThat(client.initialize()).isNotNull(); @@ -182,12 +177,11 @@ void testCreateMessageSuccess(String clientType) throws InterruptedException { .serverInfo("test-server", "1.0.0") .tools(tool) .build(); - - try ( - var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0")) - .capabilities(ClientCapabilities.builder().sampling().build()) - .sampling(samplingHandler) - .build()) { + + try (var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0")) + .capabilities(ClientCapabilities.builder().sampling().build()) + .sampling(samplingHandler) + .build()) { InitializeResult initResult = mcpClient.initialize(); assertThat(initResult).isNotNull(); @@ -213,12 +207,12 @@ void testRootsSuccess(String clientType) { AtomicReference> rootsRef = new AtomicReference<>(); var mcpServer = McpServer.sync(mcpServerTransportProvider) - .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) - .build(); + .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) + .build(); try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) - .roots(roots) - .build()) { + .roots(roots) + .build()) { InitializeResult initResult = mcpClient.initialize(); assertThat(initResult).isNotNull(); @@ -264,16 +258,12 @@ void testRootsWithoutCapability(String clientType) { return mock(CallToolResult.class); }); - var mcpServer = McpServer.sync(mcpServerTransportProvider) - .rootsChangeHandler((exchange, rootsUpdate) -> {}) - .tools(tool) - .build(); + var mcpServer = McpServer.sync(mcpServerTransportProvider).rootsChangeHandler((exchange, rootsUpdate) -> { + }).tools(tool).build(); try ( - // Create client without roots capability - var mcpClient = clientBuilder - .capabilities(ClientCapabilities.builder().build()) - .build()) { + // Create client without roots capability + var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().build()).build()) { assertThat(mcpClient.initialize()).isNotNull(); @@ -297,12 +287,12 @@ void testRootsNotifciationWithEmptyRootsList(String clientType) { AtomicReference> rootsRef = new AtomicReference<>(); var mcpServer = McpServer.sync(mcpServerTransportProvider) - .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) - .build(); + .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) + .build(); try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) - .roots(List.of()) // Empty roots list - .build()) { + .roots(List.of()) // Empty roots list + .build()) { assertThat(mcpClient.initialize()).isNotNull(); @@ -328,13 +318,13 @@ void testRootsWithMultipleHandlers(String clientType) { AtomicReference> rootsRef2 = new AtomicReference<>(); var mcpServer = McpServer.sync(mcpServerTransportProvider) - .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef1.set(rootsUpdate)) - .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef2.set(rootsUpdate)) - .build(); + .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef1.set(rootsUpdate)) + .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef2.set(rootsUpdate)) + .build(); try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) - .roots(roots) - .build()) { + .roots(roots) + .build()) { InitializeResult initResult = mcpClient.initialize(); assertThat(initResult).isNotNull(); @@ -361,12 +351,12 @@ void testRootsServerCloseWithActiveSubscription(String clientType) { AtomicReference> rootsRef = new AtomicReference<>(); var mcpServer = McpServer.sync(mcpServerTransportProvider) - .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) - .build(); + .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) + .build(); try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) - .roots(roots) - .build()) { + .roots(roots) + .build()) { InitializeResult initResult = mcpClient.initialize(); assertThat(initResult).isNotNull(); @@ -413,9 +403,9 @@ void testToolCallSuccess(String clientType) { }); var mcpServer = McpServer.sync(mcpServerTransportProvider) - .capabilities(ServerCapabilities.builder().tools(true).build()) - .tools(tool1) - .build(); + .capabilities(ServerCapabilities.builder().tools(true).build()) + .tools(tool1) + .build(); try (var mcpClient = clientBuilder.build()) { @@ -455,21 +445,20 @@ void testToolListChangeHandlingSuccess(String clientType) { AtomicReference> rootsRef = new AtomicReference<>(); var mcpServer = McpServer.sync(mcpServerTransportProvider) - .capabilities(ServerCapabilities.builder().tools(true).build()) - .tools(tool1) - .build(); + .capabilities(ServerCapabilities.builder().tools(true).build()) + .tools(tool1) + .build(); - try ( - var mcpClient = clientBuilder.toolsChangeConsumer(toolsUpdate -> { - // perform a blocking call to a remote service - String response = RestClient.create() - .get() - .uri("https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md") - .retrieve() - .body(String.class); - assertThat(response).isNotBlank(); - rootsRef.set(toolsUpdate); - }).build()) { + try (var mcpClient = clientBuilder.toolsChangeConsumer(toolsUpdate -> { + // perform a blocking call to a remote service + String response = RestClient.create() + .get() + .uri("https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md") + .retrieve() + .body(String.class); + assertThat(response).isNotBlank(); + rootsRef.set(toolsUpdate); + }).build()) { InitializeResult initResult = mcpClient.initialize(); assertThat(initResult).isNotNull(); @@ -583,10 +572,10 @@ void testLoggingNotification(String clientType) { .build(); try ( - // Create client with logging notification handler - var mcpClient = clientBuilder - .loggingConsumer(notification -> { receivedNotifications.add(notification); }) - .build()) { + // Create client with logging notification handler + var mcpClient = clientBuilder.loggingConsumer(notification -> { + receivedNotifications.add(notification); + }).build()) { // Initialize client InitializeResult initResult = mcpClient.initialize(); diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java b/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java index f9f5b356..03b28599 100644 --- a/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java +++ b/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java @@ -454,12 +454,12 @@ void testLoggingLevelsWithoutInitialization() { @Test void testLoggingLevels() { withClient(createMcpTransport(), mcpAsyncClient -> { - StepVerifier.create(mcpAsyncClient.initialize() - .thenMany(Flux.fromArray(McpSchema.LoggingLevel.values()) - .flatMap(mcpAsyncClient::setLoggingLevel)) - .collectList()) - .assertNext(l -> assertThat(l).hasSize(McpSchema.LoggingLevel.values().length)) - .verifyComplete(); + StepVerifier + .create(mcpAsyncClient.initialize() + .thenMany(Flux.fromArray(McpSchema.LoggingLevel.values()).flatMap(mcpAsyncClient::setLoggingLevel)) + .collectList()) + .assertNext(l -> assertThat(l).hasSize(McpSchema.LoggingLevel.values().length)) + .verifyComplete(); }); } diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java index 4d0a7248..062de13e 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java @@ -218,9 +218,9 @@ public Mono notifyPromptsListChanged() { /** * This implementation would, incorrectly, broadcast the logging message to all - * connected clients, using a single minLoggingLevel for all of them. Similar to - * the sampling and roots, the logging level should be set per client session and - * use the ServerExchange to send the logging message to the right client. + * connected clients, using a single minLoggingLevel for all of them. Similar to the + * sampling and roots, the logging level should be set per client session and use the + * ServerExchange to send the logging message to the right client. * @param loggingMessageNotification The logging message to send * @return A Mono that completes when the notification has been sent * @deprecated Use @@ -697,7 +697,7 @@ private McpServerSession.RequestHandler setLoggerRequestHandler() { exchange.setMinLoggingLevel(newMinLoggingLevel.level()); // FIXME: this field is deprecated and should be removed together - // with the broadcasting loggingNotification. + // with the broadcasting loggingNotification. this.minLoggingLevel = newMinLoggingLevel.level(); return Mono.just(Map.of()); diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServer.java index cad27c4b..bf310450 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServer.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServer.java @@ -150,9 +150,9 @@ public void notifyPromptsListChanged() { /** * This implementation would, incorrectly, broadcast the logging message to all - * connected clients, using a single minLoggingLevel for all of them. Similar to - * the sampling and roots, the logging level should be set per client session and - * use the ServerExchange to send the logging message to the right client. + * connected clients, using a single minLoggingLevel for all of them. Similar to the + * sampling and roots, the logging level should be set per client session and use the + * ServerExchange to send the logging message to the right client. * @param loggingMessageNotification The logging message to send * @deprecated Use * {@link McpSyncServerExchange#loggingNotification(LoggingMessageNotification)} diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServerExchange.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServerExchange.java index a3fbf42f..52360e54 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServerExchange.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServerExchange.java @@ -89,4 +89,5 @@ public McpSchema.ListRootsResult listRoots(String cursor) { public void loggingNotification(LoggingMessageNotification loggingMessageNotification) { this.exchange.loggingNotification(loggingMessageNotification).block(); } + } diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java b/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java index 9bf7de91..3e78fe42 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java @@ -76,9 +76,9 @@ McpAsyncClient client(McpClientTransport transport, Function { McpClient.AsyncSpec builder = McpClient.async(transport) - .requestTimeout(getRequestTimeout()) - .initializationTimeout(getInitializationTimeout()) - .capabilities(ClientCapabilities.builder().roots(true).build()); + .requestTimeout(getRequestTimeout()) + .initializationTimeout(getInitializationTimeout()) + .capabilities(ClientCapabilities.builder().roots(true).build()); builder = customizer.apply(builder); client.set(builder.build()); }).doesNotThrowAnyException(); @@ -114,22 +114,22 @@ void tearDown() { void verifyInitializationTimeout(Function> 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(); + .expectSubscription() + .thenAwait(getInitializationTimeout()) + .consumeErrorWith(e -> assertThat(e).isInstanceOf(McpError.class) + .hasMessage("Client must be initialized before " + action)) + .verify(); }); } @Test void testConstructorWithInvalidArguments() { assertThatThrownBy(() -> McpClient.async(null).build()).isInstanceOf(IllegalArgumentException.class) - .hasMessage("Transport must not be null"); + .hasMessage("Transport must not be null"); assertThatThrownBy(() -> McpClient.async(createMcpTransport()).requestTimeout(null).build()) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Request timeout must not be null"); + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Request timeout must not be null"); } @Test @@ -141,14 +141,14 @@ void testListToolsWithoutInitialization() { void testListTools() { withClient(createMcpTransport(), mcpAsyncClient -> { StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listTools(null))) - .consumeNextWith(result -> { - assertThat(result.tools()).isNotNull().isNotEmpty(); - - Tool firstTool = result.tools().get(0); - assertThat(firstTool.name()).isNotNull(); - assertThat(firstTool.description()).isNotNull(); - }) - .verifyComplete(); + .consumeNextWith(result -> { + assertThat(result.tools()).isNotNull().isNotEmpty(); + + Tool firstTool = result.tools().get(0); + assertThat(firstTool.name()).isNotNull(); + assertThat(firstTool.description()).isNotNull(); + }) + .verifyComplete(); }); } @@ -161,8 +161,8 @@ void testPingWithoutInitialization() { void testPing() { withClient(createMcpTransport(), mcpAsyncClient -> { StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.ping())) - .expectNextCount(1) - .verifyComplete(); + .expectNextCount(1) + .verifyComplete(); }); } @@ -178,13 +178,13 @@ void testCallTool() { CallToolRequest callToolRequest = new CallToolRequest("echo", Map.of("message", ECHO_TEST_MESSAGE)); StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.callTool(callToolRequest))) - .consumeNextWith(callToolResult -> { - assertThat(callToolResult).isNotNull().satisfies(result -> { - assertThat(result.content()).isNotNull(); - assertThat(result.isError()).isNull(); - }); - }) - .verifyComplete(); + .consumeNextWith(callToolResult -> { + assertThat(callToolResult).isNotNull().satisfies(result -> { + assertThat(result.content()).isNotNull(); + assertThat(result.isError()).isNull(); + }); + }) + .verifyComplete(); }); } @@ -195,9 +195,9 @@ void testCallToolWithInvalidTool() { Map.of("message", ECHO_TEST_MESSAGE)); StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.callTool(invalidRequest))) - .consumeErrorWith( - e -> assertThat(e).isInstanceOf(McpError.class).hasMessage("Unknown tool: nonexistent_tool")) - .verify(); + .consumeErrorWith( + e -> assertThat(e).isInstanceOf(McpError.class).hasMessage("Unknown tool: nonexistent_tool")) + .verify(); }); } @@ -210,18 +210,18 @@ void testListResourcesWithoutInitialization() { void testListResources() { withClient(createMcpTransport(), mcpAsyncClient -> { StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listResources(null))) - .consumeNextWith(resources -> { - assertThat(resources).isNotNull().satisfies(result -> { - assertThat(result.resources()).isNotNull(); - - if (!result.resources().isEmpty()) { - Resource firstResource = result.resources().get(0); - assertThat(firstResource.uri()).isNotNull(); - assertThat(firstResource.name()).isNotNull(); - } - }); - }) - .verifyComplete(); + .consumeNextWith(resources -> { + assertThat(resources).isNotNull().satisfies(result -> { + assertThat(result.resources()).isNotNull(); + + if (!result.resources().isEmpty()) { + Resource firstResource = result.resources().get(0); + assertThat(firstResource.uri()).isNotNull(); + assertThat(firstResource.name()).isNotNull(); + } + }); + }) + .verifyComplete(); }); } @@ -241,18 +241,18 @@ void testListPromptsWithoutInitialization() { void testListPrompts() { withClient(createMcpTransport(), mcpAsyncClient -> { StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listPrompts(null))) - .consumeNextWith(prompts -> { - assertThat(prompts).isNotNull().satisfies(result -> { - assertThat(result.prompts()).isNotNull(); - - if (!result.prompts().isEmpty()) { - Prompt firstPrompt = result.prompts().get(0); - assertThat(firstPrompt.name()).isNotNull(); - assertThat(firstPrompt.description()).isNotNull(); - } - }); - }) - .verifyComplete(); + .consumeNextWith(prompts -> { + assertThat(prompts).isNotNull().satisfies(result -> { + assertThat(result.prompts()).isNotNull(); + + if (!result.prompts().isEmpty()) { + Prompt firstPrompt = result.prompts().get(0); + assertThat(firstPrompt.name()).isNotNull(); + assertThat(firstPrompt.description()).isNotNull(); + } + }); + }) + .verifyComplete(); }); } @@ -266,15 +266,15 @@ void testGetPromptWithoutInitialization() { void testGetPrompt() { withClient(createMcpTransport(), mcpAsyncClient -> { StepVerifier - .create(mcpAsyncClient.initialize() - .then(mcpAsyncClient.getPrompt(new GetPromptRequest("simple_prompt", Map.of())))) - .consumeNextWith(prompt -> { - assertThat(prompt).isNotNull().satisfies(result -> { - assertThat(result.messages()).isNotEmpty(); - assertThat(result.messages()).hasSize(1); - }); - }) - .verifyComplete(); + .create(mcpAsyncClient.initialize() + .then(mcpAsyncClient.getPrompt(new GetPromptRequest("simple_prompt", Map.of())))) + .consumeNextWith(prompt -> { + assertThat(prompt).isNotNull().satisfies(result -> { + assertThat(result.messages()).isNotEmpty(); + assertThat(result.messages()).hasSize(1); + }); + }) + .verifyComplete(); }); } @@ -288,7 +288,7 @@ void testRootsListChangedWithoutInitialization() { void testRootsListChanged() { withClient(createMcpTransport(), mcpAsyncClient -> { StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.rootsListChangedNotification())) - .verifyComplete(); + .verifyComplete(); }); } @@ -312,8 +312,8 @@ void testAddRoot() { void testAddRootWithNullValue() { withClient(createMcpTransport(), mcpAsyncClient -> { StepVerifier.create(mcpAsyncClient.addRoot(null)) - .consumeErrorWith(e -> assertThat(e).isInstanceOf(McpError.class).hasMessage("Root must not be null")) - .verify(); + .consumeErrorWith(e -> assertThat(e).isInstanceOf(McpError.class).hasMessage("Root must not be null")) + .verify(); }); } @@ -331,9 +331,9 @@ void testRemoveRoot() { void testRemoveNonExistentRoot() { withClient(createMcpTransport(), mcpAsyncClient -> { StepVerifier.create(mcpAsyncClient.removeRoot("nonexistent-uri")) - .consumeErrorWith(e -> assertThat(e).isInstanceOf(McpError.class) - .hasMessage("Root with uri 'nonexistent-uri' not found")) - .verify(); + .consumeErrorWith(e -> assertThat(e).isInstanceOf(McpError.class) + .hasMessage("Root with uri 'nonexistent-uri' not found")) + .verify(); }); } @@ -362,11 +362,11 @@ void testListResourceTemplatesWithoutInitialization() { void testListResourceTemplates() { withClient(createMcpTransport(), mcpAsyncClient -> { StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listResourceTemplates())) - .consumeNextWith(result -> { - assertThat(result).isNotNull(); - assertThat(result.resourceTemplates()).isNotNull(); - }) - .verifyComplete(); + .consumeNextWith(result -> { + assertThat(result).isNotNull(); + assertThat(result.resourceTemplates()).isNotNull(); + }) + .verifyComplete(); }); } @@ -379,11 +379,11 @@ void testResourceSubscription() { // Test subscribe StepVerifier.create(mcpAsyncClient.subscribeResource(new SubscribeRequest(firstResource.uri()))) - .verifyComplete(); + .verifyComplete(); // Test unsubscribe StepVerifier.create(mcpAsyncClient.unsubscribeResource(new UnsubscribeRequest(firstResource.uri()))) - .verifyComplete(); + .verifyComplete(); } }).verifyComplete(); }); @@ -397,14 +397,14 @@ void testNotificationHandlers() { withClient(createMcpTransport(), builder -> builder - .toolsChangeConsumer(tools -> Mono.fromRunnable(() -> toolsNotificationReceived.set(true))) - .resourcesChangeConsumer( - resources -> Mono.fromRunnable(() -> resourcesNotificationReceived.set(true))) - .promptsChangeConsumer(prompts -> Mono.fromRunnable(() -> promptsNotificationReceived.set(true))), + .toolsChangeConsumer(tools -> Mono.fromRunnable(() -> toolsNotificationReceived.set(true))) + .resourcesChangeConsumer( + resources -> Mono.fromRunnable(() -> resourcesNotificationReceived.set(true))) + .promptsChangeConsumer(prompts -> Mono.fromRunnable(() -> promptsNotificationReceived.set(true))), mcpAsyncClient -> { StepVerifier.create(mcpAsyncClient.initialize()) - .expectNextMatches(Objects::nonNull) - .verifyComplete(); + .expectNextMatches(Objects::nonNull) + .verifyComplete(); }); } @@ -412,9 +412,9 @@ void testNotificationHandlers() { void testInitializeWithSamplingCapability() { ClientCapabilities capabilities = ClientCapabilities.builder().sampling().build(); CreateMessageResult createMessageResult = CreateMessageResult.builder() - .message("test") - .model("test-model") - .build(); + .message("test") + .model("test-model") + .build(); withClient(createMcpTransport(), builder -> builder.capabilities(capabilities).sampling(request -> Mono.just(createMessageResult)), client -> { @@ -425,21 +425,21 @@ void testInitializeWithSamplingCapability() { @Test void testInitializeWithAllCapabilities() { var capabilities = ClientCapabilities.builder() - .experimental(Map.of("feature", "test")) - .roots(true) - .sampling() - .build(); + .experimental(Map.of("feature", "test")) + .roots(true) + .sampling() + .build(); Function> samplingHandler = request -> Mono - .just(CreateMessageResult.builder().message("test").model("test-model").build()); + .just(CreateMessageResult.builder().message("test").model("test-model").build()); withClient(createMcpTransport(), builder -> builder.capabilities(capabilities).sampling(samplingHandler), client -> - StepVerifier.create(client.initialize()).assertNext(result -> { - assertThat(result).isNotNull(); - assertThat(result.capabilities()).isNotNull(); - }).verifyComplete()); + StepVerifier.create(client.initialize()).assertNext(result -> { + assertThat(result).isNotNull(); + assertThat(result.capabilities()).isNotNull(); + }).verifyComplete()); } // --------------------------------------- @@ -455,12 +455,12 @@ void testLoggingLevelsWithoutInitialization() { @Test void testLoggingLevels() { withClient(createMcpTransport(), mcpAsyncClient -> { - StepVerifier.create(mcpAsyncClient.initialize() - .thenMany(Flux.fromArray(McpSchema.LoggingLevel.values()) - .flatMap(mcpAsyncClient::setLoggingLevel)) - .collectList()) - .assertNext(l -> assertThat(l).hasSize(McpSchema.LoggingLevel.values().length)) - .verifyComplete(); + StepVerifier + .create(mcpAsyncClient.initialize() + .thenMany(Flux.fromArray(McpSchema.LoggingLevel.values()).flatMap(mcpAsyncClient::setLoggingLevel)) + .collectList()) + .assertNext(l -> assertThat(l).hasSize(McpSchema.LoggingLevel.values().length)) + .verifyComplete(); }); } @@ -482,8 +482,8 @@ void testLoggingConsumer() { void testLoggingWithNullNotification() { withClient(createMcpTransport(), mcpAsyncClient -> { StepVerifier.create(mcpAsyncClient.setLoggingLevel(null)) - .expectErrorMatches(error -> error.getMessage().contains("Logging level must not be null")) - .verify(); + .expectErrorMatches(error -> error.getMessage().contains("Logging level must not be null")) + .verify(); }); } diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java index d10cfeab..a7b61064 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportProviderIntegrationTests.java @@ -112,16 +112,12 @@ void testCreateMessageWithoutSamplingCapabilities() { return Mono.just(mock(CallToolResult.class)); }); - var server = McpServer.async(mcpServerTransportProvider) - .serverInfo("test-server", "1.0.0") - .tools(tool) - .build(); + var server = McpServer.async(mcpServerTransportProvider).serverInfo("test-server", "1.0.0").tools(tool).build(); try ( - // Create client without sampling capabilities - var client = clientBuilder - .clientInfo(new McpSchema.Implementation("Sample " + "client", "0.0.0")) - .build()) { + // Create client without sampling capabilities + var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample " + "client", "0.0.0")) + .build()) { assertThat(client.initialize()).isNotNull(); @@ -177,14 +173,14 @@ void testCreateMessageSuccess() throws InterruptedException { }); var mcpServer = McpServer.async(mcpServerTransportProvider) - .serverInfo("test-server", "1.0.0") - .tools(tool) - .build(); + .serverInfo("test-server", "1.0.0") + .tools(tool) + .build(); try (var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0")) - .capabilities(ClientCapabilities.builder().sampling().build()) - .sampling(samplingHandler) - .build()) { + .capabilities(ClientCapabilities.builder().sampling().build()) + .sampling(samplingHandler) + .build()) { InitializeResult initResult = mcpClient.initialize(); assertThat(initResult).isNotNull(); @@ -207,13 +203,12 @@ void testRootsSuccess() { AtomicReference> rootsRef = new AtomicReference<>(); var mcpServer = McpServer.sync(mcpServerTransportProvider) - .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) - .build(); + .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) + .build(); - try (var mcpClient = - clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) - .roots(roots) - .build()) { + try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) + .roots(roots) + .build()) { InitializeResult initResult = mcpClient.initialize(); assertThat(initResult).isNotNull(); @@ -256,14 +251,10 @@ void testRootsWithoutCapability() { return mock(CallToolResult.class); }); - var mcpServer = McpServer.sync(mcpServerTransportProvider) - .rootsChangeHandler((exchange, rootsUpdate) -> {}) - .tools(tool) - .build(); + var mcpServer = McpServer.sync(mcpServerTransportProvider).rootsChangeHandler((exchange, rootsUpdate) -> { + }).tools(tool).build(); - try (var mcpClient = clientBuilder - .capabilities(ClientCapabilities.builder().build()) - .build()) { + try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().build()).build()) { assertThat(mcpClient.initialize()).isNotNull(); @@ -284,12 +275,12 @@ void testRootsNotifciationWithEmptyRootsList() { AtomicReference> rootsRef = new AtomicReference<>(); var mcpServer = McpServer.sync(mcpServerTransportProvider) - .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) - .build(); + .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) + .build(); try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) - .roots(List.of()) // Empty roots list - .build()) { + .roots(List.of()) // Empty roots list + .build()) { InitializeResult initResult = mcpClient.initialize(); assertThat(initResult).isNotNull(); @@ -312,13 +303,13 @@ void testRootsWithMultipleHandlers() { AtomicReference> rootsRef2 = new AtomicReference<>(); var mcpServer = McpServer.sync(mcpServerTransportProvider) - .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef1.set(rootsUpdate)) - .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef2.set(rootsUpdate)) - .build(); + .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef1.set(rootsUpdate)) + .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef2.set(rootsUpdate)) + .build(); try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) - .roots(roots) - .build()) { + .roots(roots) + .build()) { assertThat(mcpClient.initialize()).isNotNull(); @@ -340,12 +331,12 @@ void testRootsServerCloseWithActiveSubscription() { AtomicReference> rootsRef = new AtomicReference<>(); var mcpServer = McpServer.sync(mcpServerTransportProvider) - .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) - .build(); + .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) + .build(); try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) - .roots(roots) - .build()) { + .roots(roots) + .build()) { InitializeResult initResult = mcpClient.initialize(); assertThat(initResult).isNotNull(); @@ -389,9 +380,9 @@ void testToolCallSuccess() { }); var mcpServer = McpServer.sync(mcpServerTransportProvider) - .capabilities(ServerCapabilities.builder().tools(true).build()) - .tools(tool1) - .build(); + .capabilities(ServerCapabilities.builder().tools(true).build()) + .tools(tool1) + .build(); try (var mcpClient = clientBuilder.build()) { InitializeResult initResult = mcpClient.initialize(); @@ -427,20 +418,20 @@ void testToolListChangeHandlingSuccess() { AtomicReference> rootsRef = new AtomicReference<>(); var mcpServer = McpServer.sync(mcpServerTransportProvider) - .capabilities(ServerCapabilities.builder().tools(true).build()) - .tools(tool1) - .build(); + .capabilities(ServerCapabilities.builder().tools(true).build()) + .tools(tool1) + .build(); try (var mcpClient = clientBuilder.toolsChangeConsumer(toolsUpdate -> { - // perform a blocking call to a remote service - String response = RestClient.create() - .get() - .uri("https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md") - .retrieve() - .body(String.class); - assertThat(response).isNotBlank(); - rootsRef.set(toolsUpdate); - }).build()) { + // perform a blocking call to a remote service + String response = RestClient.create() + .get() + .uri("https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md") + .retrieve() + .body(String.class); + assertThat(response).isNotBlank(); + rootsRef.set(toolsUpdate); + }).build()) { InitializeResult initResult = mcpClient.initialize(); assertThat(initResult).isNotNull(); @@ -554,15 +545,15 @@ void testLoggingNotification() { }); var mcpServer = McpServer.async(mcpServerTransportProvider) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().logging().tools(true).build()) - .tools(tool) - .build(); + .serverInfo("test-server", "1.0.0") + .capabilities(ServerCapabilities.builder().logging().tools(true).build()) + .tools(tool) + .build(); try ( - // Create client with logging notification handler - var mcpClient = clientBuilder.loggingConsumer(notification -> { - receivedNotifications.add(notification); - }).build()) { + // Create client with logging notification handler + var mcpClient = clientBuilder.loggingConsumer(notification -> { + receivedNotifications.add(notification); + }).build()) { // Initialize client InitializeResult initResult = mcpClient.initialize(); From 81992a91a761f090d8cf2df7f691083bc508ba72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20J=C4=99drzejczyk?= Date: Thu, 10 Apr 2025 15:35:06 +0200 Subject: [PATCH 8/8] Polish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Dariusz Jędrzejczyk --- .../server/WebMvcSseIntegrationTests.java | 134 +++++++++--------- .../client/AbstractMcpAsyncClientTests.java | 4 +- .../client/AbstractMcpAsyncClientTests.java | 4 +- 3 files changed, 72 insertions(+), 70 deletions(-) diff --git a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java index fe2a9f52..ceac4fa3 100644 --- a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java +++ b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java @@ -222,14 +222,13 @@ void testRootsSuccess() { AtomicReference> rootsRef = new AtomicReference<>(); - //@formatter:off - try (var mcpServer = McpServer.sync(mcpServerTransportProvider) - .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) - .build(); + var mcpServer = McpServer.sync(mcpServerTransportProvider) + .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) + .build(); - var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) - .roots(roots) - .build()) {//@formatter:on + try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) + .roots(roots) + .build()) { InitializeResult initResult = mcpClient.initialize(); assertThat(initResult).isNotNull(); @@ -257,6 +256,8 @@ void testRootsSuccess() { assertThat(rootsRef.get()).containsAll(List.of(roots.get(1), root3)); }); } + + mcpServer.close(); } @Test @@ -270,17 +271,13 @@ void testRootsWithoutCapability() { return mock(CallToolResult.class); }); - //@formatter:off - try (var mcpServer = McpServer.sync(mcpServerTransportProvider) - .rootsChangeHandler((exchange, rootsUpdate) -> {}) - .tools(tool) - .build(); + var mcpServer = McpServer.sync(mcpServerTransportProvider).rootsChangeHandler((exchange, rootsUpdate) -> { + }).tools(tool).build(); - // Create client without roots capability - // No roots capability - var mcpClient = clientBuilder - .capabilities(ClientCapabilities.builder().build()) - .build()) {//@formatter:on + try ( + // Create client without roots capability + // No roots capability + var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().build()).build()) { assertThat(mcpClient.initialize()).isNotNull(); @@ -292,20 +289,21 @@ void testRootsWithoutCapability() { assertThat(e).isInstanceOf(McpError.class).hasMessage("Roots not supported"); } } + + mcpServer.close(); } @Test void testRootsNotifciationWithEmptyRootsList() { AtomicReference> rootsRef = new AtomicReference<>(); - //@formatter:off - try (var mcpServer = McpServer.sync(mcpServerTransportProvider) - .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) - .build(); + var mcpServer = McpServer.sync(mcpServerTransportProvider) + .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) + .build(); - var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) - .roots(List.of()) // Empty roots list - .build()) {//@formatter:on + try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) + .roots(List.of()) // Empty roots list + .build()) { InitializeResult initResult = mcpClient.initialize(); assertThat(initResult).isNotNull(); @@ -316,6 +314,8 @@ void testRootsNotifciationWithEmptyRootsList() { assertThat(rootsRef.get()).isEmpty(); }); } + + mcpServer.close(); } @Test @@ -325,15 +325,14 @@ void testRootsWithMultipleHandlers() { AtomicReference> rootsRef1 = new AtomicReference<>(); AtomicReference> rootsRef2 = new AtomicReference<>(); - //@formatter:off - try (var mcpServer = McpServer.sync(mcpServerTransportProvider) - .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef1.set(rootsUpdate)) - .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef2.set(rootsUpdate)) - .build(); + var mcpServer = McpServer.sync(mcpServerTransportProvider) + .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef1.set(rootsUpdate)) + .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef2.set(rootsUpdate)) + .build(); - var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) - .roots(roots) - .build()) {//@formatter:on + try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) + .roots(roots) + .build()) { assertThat(mcpClient.initialize()).isNotNull(); @@ -344,6 +343,8 @@ void testRootsWithMultipleHandlers() { assertThat(rootsRef2.get()).containsAll(roots); }); } + + mcpServer.close(); } @Test @@ -352,14 +353,13 @@ void testRootsServerCloseWithActiveSubscription() { AtomicReference> rootsRef = new AtomicReference<>(); - //@formatter:off - try (var mcpServer = McpServer.sync(mcpServerTransportProvider) - .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) - .build(); + var mcpServer = McpServer.sync(mcpServerTransportProvider) + .rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate)) + .build(); - var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) - .roots(roots) - .build()) {//@formatter:on + try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) + .roots(roots) + .build()) { InitializeResult initResult = mcpClient.initialize(); assertThat(initResult).isNotNull(); @@ -370,6 +370,8 @@ void testRootsServerCloseWithActiveSubscription() { assertThat(rootsRef.get()).containsAll(roots); }); } + + mcpServer.close(); } // --------------------------------------- @@ -400,13 +402,12 @@ void testToolCallSuccess() { return callResponse; }); - //@formatter:off - try (var mcpServer = McpServer.sync(mcpServerTransportProvider) - .capabilities(ServerCapabilities.builder().tools(true).build()) - .tools(tool1) - .build(); + var mcpServer = McpServer.sync(mcpServerTransportProvider) + .capabilities(ServerCapabilities.builder().tools(true).build()) + .tools(tool1) + .build(); - var mcpClient = clientBuilder.build()) {//@formatter:on + try (var mcpClient = clientBuilder.build()) { InitializeResult initResult = mcpClient.initialize(); assertThat(initResult).isNotNull(); @@ -417,6 +418,8 @@ void testToolCallSuccess() { assertThat(response).isNotNull().isEqualTo(callResponse); } + + mcpServer.close(); } @Test @@ -437,22 +440,21 @@ void testToolListChangeHandlingSuccess() { AtomicReference> rootsRef = new AtomicReference<>(); - //@formatter:off - try (var mcpServer = McpServer.sync(mcpServerTransportProvider) - .capabilities(ServerCapabilities.builder().tools(true).build()) - .tools(tool1) - .build(); - - var mcpClient = clientBuilder.toolsChangeConsumer(toolsUpdate -> { - // perform a blocking call to a remote service - String response = RestClient.create() - .get() - .uri("https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md") - .retrieve() - .body(String.class); - assertThat(response).isNotBlank(); - rootsRef.set(toolsUpdate); - }).build()) {//@formatter:on + var mcpServer = McpServer.sync(mcpServerTransportProvider) + .capabilities(ServerCapabilities.builder().tools(true).build()) + .tools(tool1) + .build(); + + try (var mcpClient = clientBuilder.toolsChangeConsumer(toolsUpdate -> { + // perform a blocking call to a remote service + String response = RestClient.create() + .get() + .uri("https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md") + .retrieve() + .body(String.class); + assertThat(response).isNotBlank(); + rootsRef.set(toolsUpdate); + }).build()) { InitializeResult initResult = mcpClient.initialize(); assertThat(initResult).isNotNull(); @@ -485,18 +487,22 @@ void testToolListChangeHandlingSuccess() { assertThat(rootsRef.get()).containsAll(List.of(tool2.tool())); }); } + + mcpServer.close(); } @Test void testInitialize() { - //@formatter:off - try (var mcpServer = McpServer.sync(mcpServerTransportProvider).build(); - var mcpClient = clientBuilder.build()) {//@formatter:on + var mcpServer = McpServer.sync(mcpServerTransportProvider).build(); + + try (var mcpClient = clientBuilder.build()) { InitializeResult initResult = mcpClient.initialize(); assertThat(initResult).isNotNull(); } + + mcpServer.close(); } } diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java b/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java index 03b28599..5452c8ea 100644 --- a/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java +++ b/mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java @@ -456,9 +456,7 @@ void testLoggingLevels() { withClient(createMcpTransport(), mcpAsyncClient -> { StepVerifier .create(mcpAsyncClient.initialize() - .thenMany(Flux.fromArray(McpSchema.LoggingLevel.values()).flatMap(mcpAsyncClient::setLoggingLevel)) - .collectList()) - .assertNext(l -> assertThat(l).hasSize(McpSchema.LoggingLevel.values().length)) + .thenMany(Flux.fromArray(McpSchema.LoggingLevel.values()).flatMap(mcpAsyncClient::setLoggingLevel))) .verifyComplete(); }); } diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java b/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java index 3e78fe42..72b409af 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java @@ -457,9 +457,7 @@ void testLoggingLevels() { withClient(createMcpTransport(), mcpAsyncClient -> { StepVerifier .create(mcpAsyncClient.initialize() - .thenMany(Flux.fromArray(McpSchema.LoggingLevel.values()).flatMap(mcpAsyncClient::setLoggingLevel)) - .collectList()) - .assertNext(l -> assertThat(l).hasSize(McpSchema.LoggingLevel.values().length)) + .thenMany(Flux.fromArray(McpSchema.LoggingLevel.values()).flatMap(mcpAsyncClient::setLoggingLevel))) .verifyComplete(); }); }