feat(mcp): refactor logging to use exchange for targeted client notifications (#132)
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 - Add proper logging level filtering at the exchange level - Change setLoggingLevel from notification to request/response pattern (breaking change) - Deprecate global server.loggingNotification in favor of exchange.loggingNotification - Add SetLevelRequest record to McpSchema - Add integration test demonstrating filtered logging notifications Resolves #131 Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com> Co-authored-by: Dariusz Jędrzejczyk <dariusz.jedrzejczyk@broadcom.com>
This commit is contained in:
parent
2895d1589a
commit
c88ac937f3
17 changed files with 721 additions and 613 deletions
|
|
@ -4,6 +4,7 @@
|
||||||
package io.modelcontextprotocol;
|
package io.modelcontextprotocol;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
@ -111,10 +112,10 @@ public class WebFluxSseIntegrationTests {
|
||||||
return Mono.just(mock(CallToolResult.class));
|
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
|
try (var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample " + "client", "0.0.0"))
|
||||||
var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample " + "client", "0.0.0")).build();
|
.build();) {
|
||||||
|
|
||||||
assertThat(client.initialize()).isNotNull();
|
assertThat(client.initialize()).isNotNull();
|
||||||
|
|
||||||
|
|
@ -126,12 +127,13 @@ public class WebFluxSseIntegrationTests {
|
||||||
.hasMessage("Client must be configured with sampling capabilities");
|
.hasMessage("Client must be configured with sampling capabilities");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
server.close();
|
||||||
|
}
|
||||||
|
|
||||||
@ParameterizedTest(name = "{0} : {displayName} ")
|
@ParameterizedTest(name = "{0} : {displayName} ")
|
||||||
@ValueSource(strings = { "httpclient", "webflux" })
|
@ValueSource(strings = { "httpclient", "webflux" })
|
||||||
void testCreateMessageSuccess(String clientType) throws InterruptedException {
|
void testCreateMessageSuccess(String clientType) throws InterruptedException {
|
||||||
|
|
||||||
// Client
|
|
||||||
var clientBuilder = clientBuilders.get(clientType);
|
var clientBuilder = clientBuilders.get(clientType);
|
||||||
|
|
||||||
Function<CreateMessageRequest, CreateMessageResult> samplingHandler = request -> {
|
Function<CreateMessageRequest, CreateMessageResult> samplingHandler = request -> {
|
||||||
|
|
@ -142,13 +144,6 @@ public class WebFluxSseIntegrationTests {
|
||||||
CreateMessageResult.StopReason.STOP_SEQUENCE);
|
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")),
|
CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")),
|
||||||
null);
|
null);
|
||||||
|
|
||||||
|
|
@ -183,6 +178,11 @@ public class WebFluxSseIntegrationTests {
|
||||||
.tools(tool)
|
.tools(tool)
|
||||||
.build();
|
.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();
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
assertThat(initResult).isNotNull();
|
assertThat(initResult).isNotNull();
|
||||||
|
|
||||||
|
|
@ -190,8 +190,7 @@ public class WebFluxSseIntegrationTests {
|
||||||
|
|
||||||
assertThat(response).isNotNull();
|
assertThat(response).isNotNull();
|
||||||
assertThat(response).isEqualTo(callResponse);
|
assertThat(response).isEqualTo(callResponse);
|
||||||
|
}
|
||||||
mcpClient.close();
|
|
||||||
mcpServer.close();
|
mcpServer.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -206,13 +205,14 @@ public class WebFluxSseIntegrationTests {
|
||||||
List<Root> roots = List.of(new Root("uri1://", "root1"), new Root("uri2://", "root2"));
|
List<Root> roots = List.of(new Root("uri1://", "root1"), new Root("uri2://", "root2"));
|
||||||
|
|
||||||
AtomicReference<List<Root>> rootsRef = new AtomicReference<>();
|
AtomicReference<List<Root>> rootsRef = new AtomicReference<>();
|
||||||
|
|
||||||
var mcpServer = McpServer.sync(mcpServerTransportProvider)
|
var mcpServer = McpServer.sync(mcpServerTransportProvider)
|
||||||
.rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate))
|
.rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
|
try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
|
||||||
.roots(roots)
|
.roots(roots)
|
||||||
.build();
|
.build()) {
|
||||||
|
|
||||||
InitializeResult initResult = mcpClient.initialize();
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
assertThat(initResult).isNotNull();
|
assertThat(initResult).isNotNull();
|
||||||
|
|
@ -239,8 +239,8 @@ public class WebFluxSseIntegrationTests {
|
||||||
await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
|
await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
|
||||||
assertThat(rootsRef.get()).containsAll(List.of(roots.get(1), root3));
|
assertThat(rootsRef.get()).containsAll(List.of(roots.get(1), root3));
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
mcpClient.close();
|
|
||||||
mcpServer.close();
|
mcpServer.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -261,9 +261,9 @@ public class WebFluxSseIntegrationTests {
|
||||||
var mcpServer = McpServer.sync(mcpServerTransportProvider).rootsChangeHandler((exchange, rootsUpdate) -> {
|
var mcpServer = McpServer.sync(mcpServerTransportProvider).rootsChangeHandler((exchange, rootsUpdate) -> {
|
||||||
}).tools(tool).build();
|
}).tools(tool).build();
|
||||||
|
|
||||||
|
try (
|
||||||
// Create client without roots capability
|
// Create client without roots capability
|
||||||
// No roots capability
|
var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().build()).build()) {
|
||||||
var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().build()).build();
|
|
||||||
|
|
||||||
assertThat(mcpClient.initialize()).isNotNull();
|
assertThat(mcpClient.initialize()).isNotNull();
|
||||||
|
|
||||||
|
|
@ -274,8 +274,8 @@ public class WebFluxSseIntegrationTests {
|
||||||
catch (McpError e) {
|
catch (McpError e) {
|
||||||
assertThat(e).isInstanceOf(McpError.class).hasMessage("Roots not supported");
|
assertThat(e).isInstanceOf(McpError.class).hasMessage("Roots not supported");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mcpClient.close();
|
|
||||||
mcpServer.close();
|
mcpServer.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -285,30 +285,31 @@ public class WebFluxSseIntegrationTests {
|
||||||
var clientBuilder = clientBuilders.get(clientType);
|
var clientBuilder = clientBuilders.get(clientType);
|
||||||
|
|
||||||
AtomicReference<List<Root>> rootsRef = new AtomicReference<>();
|
AtomicReference<List<Root>> rootsRef = new AtomicReference<>();
|
||||||
|
|
||||||
var mcpServer = McpServer.sync(mcpServerTransportProvider)
|
var mcpServer = McpServer.sync(mcpServerTransportProvider)
|
||||||
.rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate))
|
.rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate))
|
||||||
.build();
|
.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
|
.roots(List.of()) // Empty roots list
|
||||||
.build();
|
.build()) {
|
||||||
|
|
||||||
InitializeResult initResult = mcpClient.initialize();
|
assertThat(mcpClient.initialize()).isNotNull();
|
||||||
assertThat(initResult).isNotNull();
|
|
||||||
|
|
||||||
mcpClient.rootsListChangedNotification();
|
mcpClient.rootsListChangedNotification();
|
||||||
|
|
||||||
await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
|
await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
|
||||||
assertThat(rootsRef.get()).isEmpty();
|
assertThat(rootsRef.get()).isEmpty();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
mcpClient.close();
|
|
||||||
mcpServer.close();
|
mcpServer.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest(name = "{0} : {displayName} ")
|
@ParameterizedTest(name = "{0} : {displayName} ")
|
||||||
@ValueSource(strings = { "httpclient", "webflux" })
|
@ValueSource(strings = { "httpclient", "webflux" })
|
||||||
void testRootsWithMultipleHandlers(String clientType) {
|
void testRootsWithMultipleHandlers(String clientType) {
|
||||||
|
|
||||||
var clientBuilder = clientBuilders.get(clientType);
|
var clientBuilder = clientBuilders.get(clientType);
|
||||||
|
|
||||||
List<Root> roots = List.of(new Root("uri1://", "root1"));
|
List<Root> roots = List.of(new Root("uri1://", "root1"));
|
||||||
|
|
@ -321,9 +322,9 @@ public class WebFluxSseIntegrationTests {
|
||||||
.rootsChangeHandler((exchange, rootsUpdate) -> rootsRef2.set(rootsUpdate))
|
.rootsChangeHandler((exchange, rootsUpdate) -> rootsRef2.set(rootsUpdate))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
|
try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
|
||||||
.roots(roots)
|
.roots(roots)
|
||||||
.build();
|
.build()) {
|
||||||
|
|
||||||
InitializeResult initResult = mcpClient.initialize();
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
assertThat(initResult).isNotNull();
|
assertThat(initResult).isNotNull();
|
||||||
|
|
@ -334,8 +335,8 @@ public class WebFluxSseIntegrationTests {
|
||||||
assertThat(rootsRef1.get()).containsAll(roots);
|
assertThat(rootsRef1.get()).containsAll(roots);
|
||||||
assertThat(rootsRef2.get()).containsAll(roots);
|
assertThat(rootsRef2.get()).containsAll(roots);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
mcpClient.close();
|
|
||||||
mcpServer.close();
|
mcpServer.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -348,13 +349,14 @@ public class WebFluxSseIntegrationTests {
|
||||||
List<Root> roots = List.of(new Root("uri1://", "root1"));
|
List<Root> roots = List.of(new Root("uri1://", "root1"));
|
||||||
|
|
||||||
AtomicReference<List<Root>> rootsRef = new AtomicReference<>();
|
AtomicReference<List<Root>> rootsRef = new AtomicReference<>();
|
||||||
|
|
||||||
var mcpServer = McpServer.sync(mcpServerTransportProvider)
|
var mcpServer = McpServer.sync(mcpServerTransportProvider)
|
||||||
.rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate))
|
.rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
|
try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
|
||||||
.roots(roots)
|
.roots(roots)
|
||||||
.build();
|
.build()) {
|
||||||
|
|
||||||
InitializeResult initResult = mcpClient.initialize();
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
assertThat(initResult).isNotNull();
|
assertThat(initResult).isNotNull();
|
||||||
|
|
@ -364,12 +366,9 @@ public class WebFluxSseIntegrationTests {
|
||||||
await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
|
await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
|
||||||
assertThat(rootsRef.get()).containsAll(roots);
|
assertThat(rootsRef.get()).containsAll(roots);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Close server while subscription is active
|
|
||||||
mcpServer.close();
|
mcpServer.close();
|
||||||
|
|
||||||
// Verify client can handle server closure gracefully
|
|
||||||
mcpClient.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------
|
// ---------------------------------------
|
||||||
|
|
@ -408,7 +407,7 @@ public class WebFluxSseIntegrationTests {
|
||||||
.tools(tool1)
|
.tools(tool1)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
var mcpClient = clientBuilder.build();
|
try (var mcpClient = clientBuilder.build()) {
|
||||||
|
|
||||||
InitializeResult initResult = mcpClient.initialize();
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
assertThat(initResult).isNotNull();
|
assertThat(initResult).isNotNull();
|
||||||
|
|
@ -419,8 +418,8 @@ public class WebFluxSseIntegrationTests {
|
||||||
|
|
||||||
assertThat(response).isNotNull();
|
assertThat(response).isNotNull();
|
||||||
assertThat(response).isEqualTo(callResponse);
|
assertThat(response).isEqualTo(callResponse);
|
||||||
|
}
|
||||||
|
|
||||||
mcpClient.close();
|
|
||||||
mcpServer.close();
|
mcpServer.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -443,13 +442,14 @@ public class WebFluxSseIntegrationTests {
|
||||||
return callResponse;
|
return callResponse;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
AtomicReference<List<Tool>> rootsRef = new AtomicReference<>();
|
||||||
|
|
||||||
var mcpServer = McpServer.sync(mcpServerTransportProvider)
|
var mcpServer = McpServer.sync(mcpServerTransportProvider)
|
||||||
.capabilities(ServerCapabilities.builder().tools(true).build())
|
.capabilities(ServerCapabilities.builder().tools(true).build())
|
||||||
.tools(tool1)
|
.tools(tool1)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
AtomicReference<List<Tool>> rootsRef = new AtomicReference<>();
|
try (var mcpClient = clientBuilder.toolsChangeConsumer(toolsUpdate -> {
|
||||||
var mcpClient = clientBuilder.toolsChangeConsumer(toolsUpdate -> {
|
|
||||||
// perform a blocking call to a remote service
|
// perform a blocking call to a remote service
|
||||||
String response = RestClient.create()
|
String response = RestClient.create()
|
||||||
.get()
|
.get()
|
||||||
|
|
@ -458,7 +458,7 @@ public class WebFluxSseIntegrationTests {
|
||||||
.body(String.class);
|
.body(String.class);
|
||||||
assertThat(response).isNotBlank();
|
assertThat(response).isNotBlank();
|
||||||
rootsRef.set(toolsUpdate);
|
rootsRef.set(toolsUpdate);
|
||||||
}).build();
|
}).build()) {
|
||||||
|
|
||||||
InitializeResult initResult = mcpClient.initialize();
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
assertThat(initResult).isNotNull();
|
assertThat(initResult).isNotNull();
|
||||||
|
|
@ -482,15 +482,16 @@ public class WebFluxSseIntegrationTests {
|
||||||
|
|
||||||
// Add a new tool
|
// Add a new tool
|
||||||
McpServerFeatures.SyncToolSpecification tool2 = new McpServerFeatures.SyncToolSpecification(
|
McpServerFeatures.SyncToolSpecification tool2 = new McpServerFeatures.SyncToolSpecification(
|
||||||
new McpSchema.Tool("tool2", "tool2 description", emptyJsonSchema), (exchange, request) -> callResponse);
|
new McpSchema.Tool("tool2", "tool2 description", emptyJsonSchema),
|
||||||
|
(exchange, request) -> callResponse);
|
||||||
|
|
||||||
mcpServer.addTool(tool2);
|
mcpServer.addTool(tool2);
|
||||||
|
|
||||||
await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
|
await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
|
||||||
assertThat(rootsRef.get()).containsAll(List.of(tool2.tool()));
|
assertThat(rootsRef.get()).containsAll(List.of(tool2.tool()));
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
mcpClient.close();
|
|
||||||
mcpServer.close();
|
mcpServer.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -502,12 +503,115 @@ public class WebFluxSseIntegrationTests {
|
||||||
|
|
||||||
var mcpServer = McpServer.sync(mcpServerTransportProvider).build();
|
var mcpServer = McpServer.sync(mcpServerTransportProvider).build();
|
||||||
|
|
||||||
var mcpClient = clientBuilder.build();
|
try (var mcpClient = clientBuilder.build()) {
|
||||||
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
|
assertThat(initResult).isNotNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
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<McpSchema.LoggingMessageNotification> 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();
|
||||||
|
|
||||||
|
try (
|
||||||
|
// Create client with logging notification handler
|
||||||
|
var mcpClient = clientBuilder.loggingConsumer(notification -> {
|
||||||
|
receivedNotifications.add(notification);
|
||||||
|
}).build()) {
|
||||||
|
|
||||||
|
// Initialize client
|
||||||
InitializeResult initResult = mcpClient.initialize();
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
assertThat(initResult).isNotNull();
|
assertThat(initResult).isNotNull();
|
||||||
|
|
||||||
mcpClient.close();
|
// 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");
|
||||||
|
});
|
||||||
|
}
|
||||||
mcpServer.close();
|
mcpServer.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -125,10 +125,17 @@ class WebMvcSseIntegrationTests {
|
||||||
return Mono.just(mock(CallToolResult.class));
|
return Mono.just(mock(CallToolResult.class));
|
||||||
});
|
});
|
||||||
|
|
||||||
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 (
|
||||||
// Create client without sampling capabilities
|
// 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()) {//@formatter:on
|
||||||
|
|
||||||
assertThat(client.initialize()).isNotNull();
|
assertThat(client.initialize()).isNotNull();
|
||||||
|
|
||||||
|
|
@ -140,12 +147,12 @@ class WebMvcSseIntegrationTests {
|
||||||
.hasMessage("Client must be configured with sampling capabilities");
|
.hasMessage("Client must be configured with sampling capabilities");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
server.close();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testCreateMessageSuccess() throws InterruptedException {
|
void testCreateMessageSuccess() throws InterruptedException {
|
||||||
|
|
||||||
// Client
|
|
||||||
|
|
||||||
Function<CreateMessageRequest, CreateMessageResult> samplingHandler = request -> {
|
Function<CreateMessageRequest, CreateMessageResult> samplingHandler = request -> {
|
||||||
assertThat(request.messages()).hasSize(1);
|
assertThat(request.messages()).hasSize(1);
|
||||||
assertThat(request.messages().get(0).content()).isInstanceOf(McpSchema.TextContent.class);
|
assertThat(request.messages().get(0).content()).isInstanceOf(McpSchema.TextContent.class);
|
||||||
|
|
@ -154,13 +161,6 @@ class WebMvcSseIntegrationTests {
|
||||||
CreateMessageResult.StopReason.STOP_SEQUENCE);
|
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")),
|
CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")),
|
||||||
null);
|
null);
|
||||||
|
|
||||||
|
|
@ -190,19 +190,25 @@ class WebMvcSseIntegrationTests {
|
||||||
return Mono.just(callResponse);
|
return Mono.just(callResponse);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//@formatter:off
|
||||||
var mcpServer = McpServer.async(mcpServerTransportProvider)
|
var mcpServer = McpServer.async(mcpServerTransportProvider)
|
||||||
.serverInfo("test-server", "1.0.0")
|
.serverInfo("test-server", "1.0.0")
|
||||||
.tools(tool)
|
.tools(tool)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
try (
|
||||||
|
var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0"))
|
||||||
|
.capabilities(ClientCapabilities.builder().sampling().build())
|
||||||
|
.sampling(samplingHandler)
|
||||||
|
.build()) {//@formatter:on
|
||||||
|
|
||||||
InitializeResult initResult = mcpClient.initialize();
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
assertThat(initResult).isNotNull();
|
assertThat(initResult).isNotNull();
|
||||||
|
|
||||||
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);
|
assertThat(response).isNotNull().isEqualTo(callResponse);
|
||||||
|
}
|
||||||
mcpClient.close();
|
|
||||||
mcpServer.close();
|
mcpServer.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -214,13 +220,14 @@ class WebMvcSseIntegrationTests {
|
||||||
List<Root> roots = List.of(new Root("uri1://", "root1"), new Root("uri2://", "root2"));
|
List<Root> roots = List.of(new Root("uri1://", "root1"), new Root("uri2://", "root2"));
|
||||||
|
|
||||||
AtomicReference<List<Root>> rootsRef = new AtomicReference<>();
|
AtomicReference<List<Root>> rootsRef = new AtomicReference<>();
|
||||||
|
|
||||||
var mcpServer = McpServer.sync(mcpServerTransportProvider)
|
var mcpServer = McpServer.sync(mcpServerTransportProvider)
|
||||||
.rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate))
|
.rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
|
try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
|
||||||
.roots(roots)
|
.roots(roots)
|
||||||
.build();
|
.build()) {
|
||||||
|
|
||||||
InitializeResult initResult = mcpClient.initialize();
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
assertThat(initResult).isNotNull();
|
assertThat(initResult).isNotNull();
|
||||||
|
|
@ -247,8 +254,8 @@ class WebMvcSseIntegrationTests {
|
||||||
await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
|
await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
|
||||||
assertThat(rootsRef.get()).containsAll(List.of(roots.get(1), root3));
|
assertThat(rootsRef.get()).containsAll(List.of(roots.get(1), root3));
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
mcpClient.close();
|
|
||||||
mcpServer.close();
|
mcpServer.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -266,9 +273,10 @@ class WebMvcSseIntegrationTests {
|
||||||
var mcpServer = McpServer.sync(mcpServerTransportProvider).rootsChangeHandler((exchange, rootsUpdate) -> {
|
var mcpServer = McpServer.sync(mcpServerTransportProvider).rootsChangeHandler((exchange, rootsUpdate) -> {
|
||||||
}).tools(tool).build();
|
}).tools(tool).build();
|
||||||
|
|
||||||
|
try (
|
||||||
// Create client without roots capability
|
// Create client without roots capability
|
||||||
// No roots capability
|
// No roots capability
|
||||||
var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().build()).build();
|
var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().build()).build()) {
|
||||||
|
|
||||||
assertThat(mcpClient.initialize()).isNotNull();
|
assertThat(mcpClient.initialize()).isNotNull();
|
||||||
|
|
||||||
|
|
@ -279,8 +287,8 @@ class WebMvcSseIntegrationTests {
|
||||||
catch (McpError e) {
|
catch (McpError e) {
|
||||||
assertThat(e).isInstanceOf(McpError.class).hasMessage("Roots not supported");
|
assertThat(e).isInstanceOf(McpError.class).hasMessage("Roots not supported");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mcpClient.close();
|
|
||||||
mcpServer.close();
|
mcpServer.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -292,9 +300,9 @@ class WebMvcSseIntegrationTests {
|
||||||
.rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate))
|
.rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate))
|
||||||
.build();
|
.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
|
.roots(List.of()) // Empty roots list
|
||||||
.build();
|
.build()) {
|
||||||
|
|
||||||
InitializeResult initResult = mcpClient.initialize();
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
assertThat(initResult).isNotNull();
|
assertThat(initResult).isNotNull();
|
||||||
|
|
@ -304,8 +312,8 @@ class WebMvcSseIntegrationTests {
|
||||||
await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
|
await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
|
||||||
assertThat(rootsRef.get()).isEmpty();
|
assertThat(rootsRef.get()).isEmpty();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
mcpClient.close();
|
|
||||||
mcpServer.close();
|
mcpServer.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -321,9 +329,9 @@ class WebMvcSseIntegrationTests {
|
||||||
.rootsChangeHandler((exchange, rootsUpdate) -> rootsRef2.set(rootsUpdate))
|
.rootsChangeHandler((exchange, rootsUpdate) -> rootsRef2.set(rootsUpdate))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
|
try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
|
||||||
.roots(roots)
|
.roots(roots)
|
||||||
.build();
|
.build()) {
|
||||||
|
|
||||||
assertThat(mcpClient.initialize()).isNotNull();
|
assertThat(mcpClient.initialize()).isNotNull();
|
||||||
|
|
||||||
|
|
@ -333,8 +341,8 @@ class WebMvcSseIntegrationTests {
|
||||||
assertThat(rootsRef1.get()).containsAll(roots);
|
assertThat(rootsRef1.get()).containsAll(roots);
|
||||||
assertThat(rootsRef2.get()).containsAll(roots);
|
assertThat(rootsRef2.get()).containsAll(roots);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
mcpClient.close();
|
|
||||||
mcpServer.close();
|
mcpServer.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -343,13 +351,14 @@ class WebMvcSseIntegrationTests {
|
||||||
List<Root> roots = List.of(new Root("uri1://", "root1"));
|
List<Root> roots = List.of(new Root("uri1://", "root1"));
|
||||||
|
|
||||||
AtomicReference<List<Root>> rootsRef = new AtomicReference<>();
|
AtomicReference<List<Root>> rootsRef = new AtomicReference<>();
|
||||||
|
|
||||||
var mcpServer = McpServer.sync(mcpServerTransportProvider)
|
var mcpServer = McpServer.sync(mcpServerTransportProvider)
|
||||||
.rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate))
|
.rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
|
try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
|
||||||
.roots(roots)
|
.roots(roots)
|
||||||
.build();
|
.build()) {
|
||||||
|
|
||||||
InitializeResult initResult = mcpClient.initialize();
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
assertThat(initResult).isNotNull();
|
assertThat(initResult).isNotNull();
|
||||||
|
|
@ -359,12 +368,9 @@ class WebMvcSseIntegrationTests {
|
||||||
await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
|
await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
|
||||||
assertThat(rootsRef.get()).containsAll(roots);
|
assertThat(rootsRef.get()).containsAll(roots);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Close server while subscription is active
|
|
||||||
mcpServer.close();
|
mcpServer.close();
|
||||||
|
|
||||||
// Verify client can handle server closure gracefully
|
|
||||||
mcpClient.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------
|
// ---------------------------------------
|
||||||
|
|
@ -400,7 +406,7 @@ class WebMvcSseIntegrationTests {
|
||||||
.tools(tool1)
|
.tools(tool1)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
var mcpClient = clientBuilder.build();
|
try (var mcpClient = clientBuilder.build()) {
|
||||||
|
|
||||||
InitializeResult initResult = mcpClient.initialize();
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
assertThat(initResult).isNotNull();
|
assertThat(initResult).isNotNull();
|
||||||
|
|
@ -410,8 +416,8 @@ class WebMvcSseIntegrationTests {
|
||||||
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);
|
assertThat(response).isNotNull().isEqualTo(callResponse);
|
||||||
|
}
|
||||||
|
|
||||||
mcpClient.close();
|
|
||||||
mcpServer.close();
|
mcpServer.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -431,13 +437,14 @@ class WebMvcSseIntegrationTests {
|
||||||
return callResponse;
|
return callResponse;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
AtomicReference<List<Tool>> rootsRef = new AtomicReference<>();
|
||||||
|
|
||||||
var mcpServer = McpServer.sync(mcpServerTransportProvider)
|
var mcpServer = McpServer.sync(mcpServerTransportProvider)
|
||||||
.capabilities(ServerCapabilities.builder().tools(true).build())
|
.capabilities(ServerCapabilities.builder().tools(true).build())
|
||||||
.tools(tool1)
|
.tools(tool1)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
AtomicReference<List<Tool>> rootsRef = new AtomicReference<>();
|
try (var mcpClient = clientBuilder.toolsChangeConsumer(toolsUpdate -> {
|
||||||
var mcpClient = clientBuilder.toolsChangeConsumer(toolsUpdate -> {
|
|
||||||
// perform a blocking call to a remote service
|
// perform a blocking call to a remote service
|
||||||
String response = RestClient.create()
|
String response = RestClient.create()
|
||||||
.get()
|
.get()
|
||||||
|
|
@ -446,7 +453,7 @@ class WebMvcSseIntegrationTests {
|
||||||
.body(String.class);
|
.body(String.class);
|
||||||
assertThat(response).isNotBlank();
|
assertThat(response).isNotBlank();
|
||||||
rootsRef.set(toolsUpdate);
|
rootsRef.set(toolsUpdate);
|
||||||
}).build();
|
}).build()) {
|
||||||
|
|
||||||
InitializeResult initResult = mcpClient.initialize();
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
assertThat(initResult).isNotNull();
|
assertThat(initResult).isNotNull();
|
||||||
|
|
@ -470,15 +477,16 @@ class WebMvcSseIntegrationTests {
|
||||||
|
|
||||||
// Add a new tool
|
// Add a new tool
|
||||||
McpServerFeatures.SyncToolSpecification tool2 = new McpServerFeatures.SyncToolSpecification(
|
McpServerFeatures.SyncToolSpecification tool2 = new McpServerFeatures.SyncToolSpecification(
|
||||||
new McpSchema.Tool("tool2", "tool2 description", emptyJsonSchema), (exchange, request) -> callResponse);
|
new McpSchema.Tool("tool2", "tool2 description", emptyJsonSchema),
|
||||||
|
(exchange, request) -> callResponse);
|
||||||
|
|
||||||
mcpServer.addTool(tool2);
|
mcpServer.addTool(tool2);
|
||||||
|
|
||||||
await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
|
await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
|
||||||
assertThat(rootsRef.get()).containsAll(List.of(tool2.tool()));
|
assertThat(rootsRef.get()).containsAll(List.of(tool2.tool()));
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
mcpClient.close();
|
|
||||||
mcpServer.close();
|
mcpServer.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -487,12 +495,12 @@ class WebMvcSseIntegrationTests {
|
||||||
|
|
||||||
var mcpServer = McpServer.sync(mcpServerTransportProvider).build();
|
var mcpServer = McpServer.sync(mcpServerTransportProvider).build();
|
||||||
|
|
||||||
var mcpClient = clientBuilder.build();
|
try (var mcpClient = clientBuilder.build()) {
|
||||||
|
|
||||||
InitializeResult initResult = mcpClient.initialize();
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
assertThat(initResult).isNotNull();
|
assertThat(initResult).isNotNull();
|
||||||
|
}
|
||||||
|
|
||||||
mcpClient.close();
|
|
||||||
mcpServer.close();
|
mcpServer.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Disabled;
|
import org.junit.jupiter.api.Disabled;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.test.StepVerifier;
|
import reactor.test.StepVerifier;
|
||||||
|
|
||||||
|
|
@ -453,15 +454,10 @@ public abstract class AbstractMcpAsyncClientTests {
|
||||||
@Test
|
@Test
|
||||||
void testLoggingLevels() {
|
void testLoggingLevels() {
|
||||||
withClient(createMcpTransport(), mcpAsyncClient -> {
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
Mono<Void> testAllLevels = mcpAsyncClient.initialize().then(Mono.defer(() -> {
|
StepVerifier
|
||||||
Mono<Void> chain = Mono.empty();
|
.create(mcpAsyncClient.initialize()
|
||||||
for (McpSchema.LoggingLevel level : McpSchema.LoggingLevel.values()) {
|
.thenMany(Flux.fromArray(McpSchema.LoggingLevel.values()).flatMap(mcpAsyncClient::setLoggingLevel)))
|
||||||
chain = chain.then(mcpAsyncClient.setLoggingLevel(level));
|
.verifyComplete();
|
||||||
}
|
|
||||||
return chain;
|
|
||||||
}));
|
|
||||||
|
|
||||||
StepVerifier.create(testAllLevels).verifyComplete();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -416,53 +416,4 @@ public abstract class AbstractMcpAsyncServerTests {
|
||||||
.doesNotThrowAnyException();
|
.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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -388,53 +388,4 @@ public abstract class AbstractMcpSyncServerTests {
|
||||||
assertThatCode(() -> noConsumersServer.closeGracefully()).doesNotThrowAnyException();
|
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -786,10 +786,9 @@ public class McpAsyncClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.withInitializationCheck("setting logging level", initializedResult -> {
|
return this.withInitializationCheck("setting logging level", initializedResult -> {
|
||||||
String levelName = this.transport.unmarshalFrom(loggingLevel, new TypeReference<String>() {
|
var params = new McpSchema.SetLevelRequest(loggingLevel);
|
||||||
});
|
return this.mcpSession.sendRequest(McpSchema.METHOD_LOGGING_SET_LEVEL, params, new TypeReference<Object>() {
|
||||||
Map<String, Object> params = Map.of("level", levelName);
|
}).then();
|
||||||
return this.mcpSession.sendNotification(McpSchema.METHOD_LOGGING_SET_LEVEL, params);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ package io.modelcontextprotocol.client;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
|
||||||
import io.modelcontextprotocol.spec.McpClientTransport;
|
|
||||||
import io.modelcontextprotocol.spec.McpSchema;
|
import io.modelcontextprotocol.spec.McpSchema;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
|
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
|
import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import io.modelcontextprotocol.spec.McpSchema;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
|
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.LoggingLevel;
|
import io.modelcontextprotocol.spec.McpSchema.LoggingLevel;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
|
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema.SetLevelRequest;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.Tool;
|
import io.modelcontextprotocol.spec.McpSchema.Tool;
|
||||||
import io.modelcontextprotocol.spec.McpServerSession;
|
import io.modelcontextprotocol.spec.McpServerSession;
|
||||||
import io.modelcontextprotocol.spec.McpServerTransportProvider;
|
import io.modelcontextprotocol.spec.McpServerTransportProvider;
|
||||||
|
|
@ -216,11 +217,17 @@ public class McpAsyncServer {
|
||||||
// ---------------------------------------
|
// ---------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a logging message notification to all connected clients. Messages below the
|
* This implementation would, incorrectly, broadcast the logging message to all
|
||||||
* current minimum logging level will be filtered out.
|
* 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
|
* @param loggingMessageNotification The logging message to send
|
||||||
* @return A Mono that completes when the notification has been sent
|
* @return A Mono that completes when the notification has been sent
|
||||||
|
* @deprecated Use
|
||||||
|
* {@link McpAsyncServerExchange#loggingNotification(LoggingMessageNotification)}
|
||||||
|
* instead.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public Mono<Void> loggingNotification(LoggingMessageNotification loggingMessageNotification) {
|
public Mono<Void> loggingNotification(LoggingMessageNotification loggingMessageNotification) {
|
||||||
return this.delegate.loggingNotification(loggingMessageNotification);
|
return this.delegate.loggingNotification(loggingMessageNotification);
|
||||||
}
|
}
|
||||||
|
|
@ -257,6 +264,8 @@ public class McpAsyncServer {
|
||||||
|
|
||||||
private final ConcurrentHashMap<String, McpServerFeatures.AsyncPromptSpecification> prompts = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<String, McpServerFeatures.AsyncPromptSpecification> prompts = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
// FIXME: this field is deprecated and should be remvoed together with the
|
||||||
|
// broadcasting loggingNotification.
|
||||||
private LoggingLevel minLoggingLevel = LoggingLevel.DEBUG;
|
private LoggingLevel minLoggingLevel = LoggingLevel.DEBUG;
|
||||||
|
|
||||||
private List<String> protocolVersions = List.of(McpSchema.LATEST_PROTOCOL_VERSION);
|
private List<String> protocolVersions = List.of(McpSchema.LATEST_PROTOCOL_VERSION);
|
||||||
|
|
@ -677,12 +686,22 @@ public class McpAsyncServer {
|
||||||
loggingMessageNotification);
|
loggingMessageNotification);
|
||||||
}
|
}
|
||||||
|
|
||||||
private McpServerSession.RequestHandler<Void> setLoggerRequestHandler() {
|
private McpServerSession.RequestHandler<Object> setLoggerRequestHandler() {
|
||||||
return (exchange, params) -> {
|
return (exchange, params) -> {
|
||||||
this.minLoggingLevel = objectMapper.convertValue(params, new TypeReference<LoggingLevel>() {
|
return Mono.defer(() -> {
|
||||||
|
|
||||||
|
SetLevelRequest newMinLoggingLevel = objectMapper.convertValue(params,
|
||||||
|
new TypeReference<SetLevelRequest>() {
|
||||||
});
|
});
|
||||||
|
|
||||||
return Mono.empty();
|
exchange.setMinLoggingLevel(newMinLoggingLevel.level());
|
||||||
|
|
||||||
|
// FIXME: this field is deprecated and should be removed together
|
||||||
|
// with the broadcasting loggingNotification.
|
||||||
|
this.minLoggingLevel = newMinLoggingLevel.level();
|
||||||
|
|
||||||
|
return Mono.just(Map.of());
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,16 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024-2024 the original author or authors.
|
||||||
|
*/
|
||||||
|
|
||||||
package io.modelcontextprotocol.server;
|
package io.modelcontextprotocol.server;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import io.modelcontextprotocol.spec.McpError;
|
import io.modelcontextprotocol.spec.McpError;
|
||||||
import io.modelcontextprotocol.spec.McpSchema;
|
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.spec.McpServerSession;
|
||||||
|
import io.modelcontextprotocol.util.Assert;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -11,6 +18,7 @@ import reactor.core.publisher.Mono;
|
||||||
* exchange provides methods to interact with the client and query its capabilities.
|
* exchange provides methods to interact with the client and query its capabilities.
|
||||||
*
|
*
|
||||||
* @author Dariusz Jędrzejczyk
|
* @author Dariusz Jędrzejczyk
|
||||||
|
* @author Christian Tzolov
|
||||||
*/
|
*/
|
||||||
public class McpAsyncServerExchange {
|
public class McpAsyncServerExchange {
|
||||||
|
|
||||||
|
|
@ -20,6 +28,8 @@ public class McpAsyncServerExchange {
|
||||||
|
|
||||||
private final McpSchema.Implementation clientInfo;
|
private final McpSchema.Implementation clientInfo;
|
||||||
|
|
||||||
|
private volatile LoggingLevel minLoggingLevel = LoggingLevel.INFO;
|
||||||
|
|
||||||
private static final TypeReference<McpSchema.CreateMessageResult> CREATE_MESSAGE_RESULT_TYPE_REF = new TypeReference<>() {
|
private static final TypeReference<McpSchema.CreateMessageResult> CREATE_MESSAGE_RESULT_TYPE_REF = new TypeReference<>() {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -101,4 +111,38 @@ public class McpAsyncServerExchange {
|
||||||
LIST_ROOTS_RESULT_TYPE_REF);
|
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<Void> loggingNotification(LoggingMessageNotification loggingMessageNotification) {
|
||||||
|
|
||||||
|
if (loggingMessageNotification == null) {
|
||||||
|
return Mono.error(new McpError("Logging message must not be null"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Mono.defer(() -> {
|
||||||
|
if (this.isNotificationForLevelAllowed(loggingMessageNotification.level())) {
|
||||||
|
return this.session.sendNotification(McpSchema.METHOD_NOTIFICATION_MESSAGE, loggingMessageNotification);
|
||||||
|
}
|
||||||
|
return Mono.empty();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
|
Assert.notNull(minLoggingLevel, "minLoggingLevel must not be null");
|
||||||
|
this.minLoggingLevel = minLoggingLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isNotificationForLevelAllowed(LoggingLevel loggingLevel) {
|
||||||
|
return loggingLevel.level() >= this.minLoggingLevel.level();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,7 @@
|
||||||
|
|
||||||
package io.modelcontextprotocol.server;
|
package io.modelcontextprotocol.server;
|
||||||
|
|
||||||
import io.modelcontextprotocol.spec.McpError;
|
|
||||||
import io.modelcontextprotocol.spec.McpSchema;
|
import io.modelcontextprotocol.spec.McpSchema;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
|
|
||||||
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
|
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
|
||||||
import io.modelcontextprotocol.util.Assert;
|
import io.modelcontextprotocol.util.Assert;
|
||||||
|
|
||||||
|
|
@ -151,9 +149,16 @@ public class McpSyncServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a logging message notification to all clients.
|
* This implementation would, incorrectly, broadcast the logging message to all
|
||||||
* @param loggingMessageNotification The logging message notification to send
|
* 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) {
|
public void loggingNotification(LoggingMessageNotification loggingMessageNotification) {
|
||||||
this.asyncServer.loggingNotification(loggingMessageNotification).block();
|
this.asyncServer.loggingNotification(loggingMessageNotification).block();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,19 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024-2024 the original author or authors.
|
||||||
|
*/
|
||||||
|
|
||||||
package io.modelcontextprotocol.server;
|
package io.modelcontextprotocol.server;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
|
||||||
import io.modelcontextprotocol.spec.McpSchema;
|
import io.modelcontextprotocol.spec.McpSchema;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema.LoggingLevel;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a synchronous exchange with a Model Context Protocol (MCP) client. The
|
* Represents a synchronous exchange with a Model Context Protocol (MCP) client. The
|
||||||
* exchange provides methods to interact with the client and query its capabilities.
|
* exchange provides methods to interact with the client and query its capabilities.
|
||||||
*
|
*
|
||||||
* @author Dariusz Jędrzejczyk
|
* @author Dariusz Jędrzejczyk
|
||||||
|
* @author Christian Tzolov
|
||||||
*/
|
*/
|
||||||
public class McpSyncServerExchange {
|
public class McpSyncServerExchange {
|
||||||
|
|
||||||
|
|
@ -75,4 +81,13 @@ public class McpSyncServerExchange {
|
||||||
return this.exchange.listRoots(cursor).block();
|
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
|
||||||
|
*/
|
||||||
|
public void loggingNotification(LoggingMessageNotification loggingMessageNotification) {
|
||||||
|
this.exchange.loggingNotification(loggingMessageNotification).block();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1165,6 +1165,11 @@ public final class McpSchema {
|
||||||
|
|
||||||
} // @formatter:on
|
} // @formatter:on
|
||||||
|
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_ABSENT)
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public record SetLevelRequest(@JsonProperty("level") LoggingLevel level) {
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
// Autocomplete
|
// Autocomplete
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Disabled;
|
import org.junit.jupiter.api.Disabled;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.test.StepVerifier;
|
import reactor.test.StepVerifier;
|
||||||
|
|
||||||
|
|
@ -454,15 +455,10 @@ public abstract class AbstractMcpAsyncClientTests {
|
||||||
@Test
|
@Test
|
||||||
void testLoggingLevels() {
|
void testLoggingLevels() {
|
||||||
withClient(createMcpTransport(), mcpAsyncClient -> {
|
withClient(createMcpTransport(), mcpAsyncClient -> {
|
||||||
Mono<Void> testAllLevels = mcpAsyncClient.initialize().then(Mono.defer(() -> {
|
StepVerifier
|
||||||
Mono<Void> chain = Mono.empty();
|
.create(mcpAsyncClient.initialize()
|
||||||
for (McpSchema.LoggingLevel level : McpSchema.LoggingLevel.values()) {
|
.thenMany(Flux.fromArray(McpSchema.LoggingLevel.values()).flatMap(mcpAsyncClient::setLoggingLevel)))
|
||||||
chain = chain.then(mcpAsyncClient.setLoggingLevel(level));
|
.verifyComplete();
|
||||||
}
|
|
||||||
return chain;
|
|
||||||
}));
|
|
||||||
|
|
||||||
StepVerifier.create(testAllLevels).verifyComplete();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -415,53 +415,4 @@ public abstract class AbstractMcpAsyncServerTests {
|
||||||
.doesNotThrowAnyException();
|
.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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -387,53 +387,4 @@ public abstract class AbstractMcpSyncServerTests {
|
||||||
assertThatCode(() -> noConsumersServer.closeGracefully()).doesNotThrowAnyException();
|
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import io.modelcontextprotocol.client.McpClient;
|
||||||
import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;
|
import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;
|
||||||
import io.modelcontextprotocol.server.McpServer;
|
import io.modelcontextprotocol.server.McpServer;
|
||||||
import io.modelcontextprotocol.spec.McpSchema;
|
import io.modelcontextprotocol.spec.McpSchema;
|
||||||
import org.apache.catalina.Context;
|
|
||||||
import org.apache.catalina.LifecycleException;
|
import org.apache.catalina.LifecycleException;
|
||||||
import org.apache.catalina.LifecycleState;
|
import org.apache.catalina.LifecycleState;
|
||||||
import org.apache.catalina.startup.Tomcat;
|
import org.apache.catalina.startup.Tomcat;
|
||||||
|
|
@ -78,9 +77,13 @@ public class HttpServletSseServerCustomContextPathTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testCustomContextPath() {
|
void testCustomContextPath() {
|
||||||
McpServer.async(mcpServerTransportProvider).serverInfo("test-server", "1.0.0").build();
|
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();
|
try (//@formatter:off
|
||||||
|
var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample " + "client", "0.0.0")) .build()) { //@formatter:on
|
||||||
|
|
||||||
assertThat(client.initialize()).isNotNull();
|
assertThat(client.initialize()).isNotNull();
|
||||||
}
|
}
|
||||||
|
server.close();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
package io.modelcontextprotocol.server.transport;
|
package io.modelcontextprotocol.server.transport;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
@ -44,7 +45,7 @@ import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
public class HttpServletSseServerTransportProviderIntegrationTests {
|
public class HttpServletSseServerTransportProviderIntegrationTests {
|
||||||
|
|
||||||
private static final int PORT = 8185;
|
private static final int PORT = 8189;
|
||||||
|
|
||||||
private static final String CUSTOM_SSE_ENDPOINT = "/somePath/sse";
|
private static final String CUSTOM_SSE_ENDPOINT = "/somePath/sse";
|
||||||
|
|
||||||
|
|
@ -110,10 +111,12 @@ public class HttpServletSseServerTransportProviderIntegrationTests {
|
||||||
return Mono.just(mock(CallToolResult.class));
|
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();
|
||||||
|
|
||||||
|
try (
|
||||||
// Create client without sampling capabilities
|
// 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();
|
||||||
|
|
||||||
|
|
@ -125,12 +128,12 @@ public class HttpServletSseServerTransportProviderIntegrationTests {
|
||||||
.hasMessage("Client must be configured with sampling capabilities");
|
.hasMessage("Client must be configured with sampling capabilities");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
server.close();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testCreateMessageSuccess() throws InterruptedException {
|
void testCreateMessageSuccess() throws InterruptedException {
|
||||||
|
|
||||||
// Client
|
|
||||||
|
|
||||||
Function<CreateMessageRequest, CreateMessageResult> samplingHandler = request -> {
|
Function<CreateMessageRequest, CreateMessageResult> samplingHandler = request -> {
|
||||||
assertThat(request.messages()).hasSize(1);
|
assertThat(request.messages()).hasSize(1);
|
||||||
assertThat(request.messages().get(0).content()).isInstanceOf(McpSchema.TextContent.class);
|
assertThat(request.messages().get(0).content()).isInstanceOf(McpSchema.TextContent.class);
|
||||||
|
|
@ -139,13 +142,6 @@ public class HttpServletSseServerTransportProviderIntegrationTests {
|
||||||
CreateMessageResult.StopReason.STOP_SEQUENCE);
|
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")),
|
CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")),
|
||||||
null);
|
null);
|
||||||
|
|
||||||
|
|
@ -180,6 +176,11 @@ public class HttpServletSseServerTransportProviderIntegrationTests {
|
||||||
.tools(tool)
|
.tools(tool)
|
||||||
.build();
|
.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();
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
assertThat(initResult).isNotNull();
|
assertThat(initResult).isNotNull();
|
||||||
|
|
||||||
|
|
@ -187,8 +188,7 @@ public class HttpServletSseServerTransportProviderIntegrationTests {
|
||||||
|
|
||||||
assertThat(response).isNotNull();
|
assertThat(response).isNotNull();
|
||||||
assertThat(response).isEqualTo(callResponse);
|
assertThat(response).isEqualTo(callResponse);
|
||||||
|
}
|
||||||
mcpClient.close();
|
|
||||||
mcpServer.close();
|
mcpServer.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -200,13 +200,14 @@ public class HttpServletSseServerTransportProviderIntegrationTests {
|
||||||
List<Root> roots = List.of(new Root("uri1://", "root1"), new Root("uri2://", "root2"));
|
List<Root> roots = List.of(new Root("uri1://", "root1"), new Root("uri2://", "root2"));
|
||||||
|
|
||||||
AtomicReference<List<Root>> rootsRef = new AtomicReference<>();
|
AtomicReference<List<Root>> rootsRef = new AtomicReference<>();
|
||||||
|
|
||||||
var mcpServer = McpServer.sync(mcpServerTransportProvider)
|
var mcpServer = McpServer.sync(mcpServerTransportProvider)
|
||||||
.rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate))
|
.rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
|
try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
|
||||||
.roots(roots)
|
.roots(roots)
|
||||||
.build();
|
.build()) {
|
||||||
|
|
||||||
InitializeResult initResult = mcpClient.initialize();
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
assertThat(initResult).isNotNull();
|
assertThat(initResult).isNotNull();
|
||||||
|
|
@ -234,9 +235,9 @@ public class HttpServletSseServerTransportProviderIntegrationTests {
|
||||||
assertThat(rootsRef.get()).containsAll(List.of(roots.get(1), root3));
|
assertThat(rootsRef.get()).containsAll(List.of(roots.get(1), root3));
|
||||||
});
|
});
|
||||||
|
|
||||||
mcpClient.close();
|
|
||||||
mcpServer.close();
|
mcpServer.close();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testRootsWithoutCapability() {
|
void testRootsWithoutCapability() {
|
||||||
|
|
@ -252,9 +253,7 @@ public class HttpServletSseServerTransportProviderIntegrationTests {
|
||||||
var mcpServer = McpServer.sync(mcpServerTransportProvider).rootsChangeHandler((exchange, rootsUpdate) -> {
|
var mcpServer = McpServer.sync(mcpServerTransportProvider).rootsChangeHandler((exchange, rootsUpdate) -> {
|
||||||
}).tools(tool).build();
|
}).tools(tool).build();
|
||||||
|
|
||||||
// Create client without roots capability
|
try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().build()).build()) {
|
||||||
// No roots capability
|
|
||||||
var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().build()).build();
|
|
||||||
|
|
||||||
assertThat(mcpClient.initialize()).isNotNull();
|
assertThat(mcpClient.initialize()).isNotNull();
|
||||||
|
|
||||||
|
|
@ -265,8 +264,8 @@ public class HttpServletSseServerTransportProviderIntegrationTests {
|
||||||
catch (McpError e) {
|
catch (McpError e) {
|
||||||
assertThat(e).isInstanceOf(McpError.class).hasMessage("Roots not supported");
|
assertThat(e).isInstanceOf(McpError.class).hasMessage("Roots not supported");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mcpClient.close();
|
|
||||||
mcpServer.close();
|
mcpServer.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -278,9 +277,9 @@ public class HttpServletSseServerTransportProviderIntegrationTests {
|
||||||
.rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate))
|
.rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate))
|
||||||
.build();
|
.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
|
.roots(List.of()) // Empty roots list
|
||||||
.build();
|
.build()) {
|
||||||
|
|
||||||
InitializeResult initResult = mcpClient.initialize();
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
assertThat(initResult).isNotNull();
|
assertThat(initResult).isNotNull();
|
||||||
|
|
@ -290,8 +289,8 @@ public class HttpServletSseServerTransportProviderIntegrationTests {
|
||||||
await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
|
await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
|
||||||
assertThat(rootsRef.get()).isEmpty();
|
assertThat(rootsRef.get()).isEmpty();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
mcpClient.close();
|
|
||||||
mcpServer.close();
|
mcpServer.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -307,9 +306,9 @@ public class HttpServletSseServerTransportProviderIntegrationTests {
|
||||||
.rootsChangeHandler((exchange, rootsUpdate) -> rootsRef2.set(rootsUpdate))
|
.rootsChangeHandler((exchange, rootsUpdate) -> rootsRef2.set(rootsUpdate))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
|
try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
|
||||||
.roots(roots)
|
.roots(roots)
|
||||||
.build();
|
.build()) {
|
||||||
|
|
||||||
assertThat(mcpClient.initialize()).isNotNull();
|
assertThat(mcpClient.initialize()).isNotNull();
|
||||||
|
|
||||||
|
|
@ -319,8 +318,8 @@ public class HttpServletSseServerTransportProviderIntegrationTests {
|
||||||
assertThat(rootsRef1.get()).containsAll(roots);
|
assertThat(rootsRef1.get()).containsAll(roots);
|
||||||
assertThat(rootsRef2.get()).containsAll(roots);
|
assertThat(rootsRef2.get()).containsAll(roots);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
mcpClient.close();
|
|
||||||
mcpServer.close();
|
mcpServer.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -329,13 +328,14 @@ public class HttpServletSseServerTransportProviderIntegrationTests {
|
||||||
List<Root> roots = List.of(new Root("uri1://", "root1"));
|
List<Root> roots = List.of(new Root("uri1://", "root1"));
|
||||||
|
|
||||||
AtomicReference<List<Root>> rootsRef = new AtomicReference<>();
|
AtomicReference<List<Root>> rootsRef = new AtomicReference<>();
|
||||||
|
|
||||||
var mcpServer = McpServer.sync(mcpServerTransportProvider)
|
var mcpServer = McpServer.sync(mcpServerTransportProvider)
|
||||||
.rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate))
|
.rootsChangeHandler((exchange, rootsUpdate) -> rootsRef.set(rootsUpdate))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
|
try (var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
|
||||||
.roots(roots)
|
.roots(roots)
|
||||||
.build();
|
.build()) {
|
||||||
|
|
||||||
InitializeResult initResult = mcpClient.initialize();
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
assertThat(initResult).isNotNull();
|
assertThat(initResult).isNotNull();
|
||||||
|
|
@ -345,12 +345,9 @@ public class HttpServletSseServerTransportProviderIntegrationTests {
|
||||||
await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
|
await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
|
||||||
assertThat(rootsRef.get()).containsAll(roots);
|
assertThat(rootsRef.get()).containsAll(roots);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Close server while subscription is active
|
|
||||||
mcpServer.close();
|
mcpServer.close();
|
||||||
|
|
||||||
// Verify client can handle server closure gracefully
|
|
||||||
mcpClient.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------
|
// ---------------------------------------
|
||||||
|
|
@ -386,8 +383,7 @@ public class HttpServletSseServerTransportProviderIntegrationTests {
|
||||||
.tools(tool1)
|
.tools(tool1)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
var mcpClient = clientBuilder.build();
|
try (var mcpClient = clientBuilder.build()) {
|
||||||
|
|
||||||
InitializeResult initResult = mcpClient.initialize();
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
assertThat(initResult).isNotNull();
|
assertThat(initResult).isNotNull();
|
||||||
|
|
||||||
|
|
@ -397,8 +393,8 @@ public class HttpServletSseServerTransportProviderIntegrationTests {
|
||||||
|
|
||||||
assertThat(response).isNotNull();
|
assertThat(response).isNotNull();
|
||||||
assertThat(response).isEqualTo(callResponse);
|
assertThat(response).isEqualTo(callResponse);
|
||||||
|
}
|
||||||
|
|
||||||
mcpClient.close();
|
|
||||||
mcpServer.close();
|
mcpServer.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -418,13 +414,14 @@ public class HttpServletSseServerTransportProviderIntegrationTests {
|
||||||
return callResponse;
|
return callResponse;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
AtomicReference<List<Tool>> rootsRef = new AtomicReference<>();
|
||||||
|
|
||||||
var mcpServer = McpServer.sync(mcpServerTransportProvider)
|
var mcpServer = McpServer.sync(mcpServerTransportProvider)
|
||||||
.capabilities(ServerCapabilities.builder().tools(true).build())
|
.capabilities(ServerCapabilities.builder().tools(true).build())
|
||||||
.tools(tool1)
|
.tools(tool1)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
AtomicReference<List<Tool>> rootsRef = new AtomicReference<>();
|
try (var mcpClient = clientBuilder.toolsChangeConsumer(toolsUpdate -> {
|
||||||
var mcpClient = clientBuilder.toolsChangeConsumer(toolsUpdate -> {
|
|
||||||
// perform a blocking call to a remote service
|
// perform a blocking call to a remote service
|
||||||
String response = RestClient.create()
|
String response = RestClient.create()
|
||||||
.get()
|
.get()
|
||||||
|
|
@ -433,7 +430,7 @@ public class HttpServletSseServerTransportProviderIntegrationTests {
|
||||||
.body(String.class);
|
.body(String.class);
|
||||||
assertThat(response).isNotBlank();
|
assertThat(response).isNotBlank();
|
||||||
rootsRef.set(toolsUpdate);
|
rootsRef.set(toolsUpdate);
|
||||||
}).build();
|
}).build()) {
|
||||||
|
|
||||||
InitializeResult initResult = mcpClient.initialize();
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
assertThat(initResult).isNotNull();
|
assertThat(initResult).isNotNull();
|
||||||
|
|
@ -457,29 +454,143 @@ public class HttpServletSseServerTransportProviderIntegrationTests {
|
||||||
|
|
||||||
// Add a new tool
|
// Add a new tool
|
||||||
McpServerFeatures.SyncToolSpecification tool2 = new McpServerFeatures.SyncToolSpecification(
|
McpServerFeatures.SyncToolSpecification tool2 = new McpServerFeatures.SyncToolSpecification(
|
||||||
new McpSchema.Tool("tool2", "tool2 description", emptyJsonSchema), (exchange, request) -> callResponse);
|
new McpSchema.Tool("tool2", "tool2 description", emptyJsonSchema),
|
||||||
|
(exchange, request) -> callResponse);
|
||||||
|
|
||||||
mcpServer.addTool(tool2);
|
mcpServer.addTool(tool2);
|
||||||
|
|
||||||
await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
|
await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
|
||||||
assertThat(rootsRef.get()).containsAll(List.of(tool2.tool()));
|
assertThat(rootsRef.get()).containsAll(List.of(tool2.tool()));
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
mcpClient.close();
|
|
||||||
mcpServer.close();
|
mcpServer.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testInitialize() {
|
void testInitialize() {
|
||||||
|
|
||||||
var mcpServer = McpServer.sync(mcpServerTransportProvider).build();
|
var mcpServer = McpServer.sync(mcpServerTransportProvider).build();
|
||||||
|
|
||||||
var mcpClient = clientBuilder.build();
|
try (var mcpClient = clientBuilder.build()) {
|
||||||
|
|
||||||
InitializeResult initResult = mcpClient.initialize();
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
assertThat(initResult).isNotNull();
|
assertThat(initResult).isNotNull();
|
||||||
|
}
|
||||||
|
|
||||||
mcpClient.close();
|
mcpServer.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------
|
||||||
|
// Logging Tests
|
||||||
|
// ---------------------------------------
|
||||||
|
@Test
|
||||||
|
void testLoggingNotification() {
|
||||||
|
// Create a list to store received logging notifications
|
||||||
|
List<McpSchema.LoggingMessageNotification> 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();
|
||||||
|
try (
|
||||||
|
// 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");
|
||||||
|
});
|
||||||
|
}
|
||||||
mcpServer.close();
|
mcpServer.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue