From 2ea5527ade64dc124ea8c69fc2464aa7ea2f3432 Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Fri, 20 Dec 2024 10:39:22 +0100 Subject: [PATCH] feat: Add default logging for MCP notification handlers - Adds default logging for tools, resources and prompts change notifications. - Introduces Utils class with collection and string helper methods. - Ensures notification handlers are always registered with at least a default logging consumer. --- .../ai/mcp/client/McpAsyncClient.java | 34 ++++++--- .../springframework/ai/mcp/util/Utils.java | 69 +++++++++++++++++++ 2 files changed, 94 insertions(+), 9 deletions(-) create mode 100644 spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/util/Utils.java diff --git a/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/client/McpAsyncClient.java b/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/client/McpAsyncClient.java index ba3346e..4af152e 100644 --- a/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/client/McpAsyncClient.java +++ b/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/client/McpAsyncClient.java @@ -40,6 +40,7 @@ import org.springframework.ai.mcp.spec.McpSchema.ListPromptsResult; import org.springframework.ai.mcp.spec.McpSchema.PaginatedRequest; import org.springframework.ai.mcp.spec.McpSchema.Root; import org.springframework.ai.mcp.spec.McpTransport; +import org.springframework.ai.mcp.util.Utils; /** * The Model Context Protocol (MCP) client implementation that provides asynchronous @@ -85,29 +86,44 @@ public class McpAsyncClient { List>> resourcesChangeConsumers, List>> promptsChangeConsumers) { + // Request Handlers Map requestHanlers = new HashMap<>(); + // Roots List Request Handler if (rootsListProviders != null && !rootsListProviders.isEmpty()) { requestHanlers.put("roots/list", rootsListRequestHandler(rootsListProviders)); this.rootCapabilities = new McpSchema.ClientCapabilities.RootCapabilities(rootsListChangedNotification); } + // Notification Handlers Map notificationHandlers = new HashMap<>(); - if (toolsChangeConsumers != null && !toolsChangeConsumers.isEmpty()) { - notificationHandlers.put("notifications/tools/list_changed", - toolsChangeNotificationHandler(toolsChangeConsumers)); + // Tools Change Notification + List>> toolsChangeConsumersFinal = new ArrayList<>(); + toolsChangeConsumersFinal.add((notification) -> logger.info("Tools changed: {}", notification)); + if (!Utils.isEmpty(toolsChangeConsumers)) { + toolsChangeConsumersFinal.addAll(toolsChangeConsumers); } + notificationHandlers.put("notifications/tools/list_changed", + toolsChangeNotificationHandler(toolsChangeConsumersFinal)); - if (resourcesChangeConsumers != null && !resourcesChangeConsumers.isEmpty()) { - notificationHandlers.put("notifications/resources/list_changed", - resourcesChangeNotificationHandler(resourcesChangeConsumers)); + // Resources Change Notification + List>> resourcesChangeConsumersFinal = new ArrayList<>(); + resourcesChangeConsumersFinal.add((notification) -> logger.info("Resources changed: {}", notification)); + if (!Utils.isEmpty(resourcesChangeConsumers)) { + resourcesChangeConsumersFinal.addAll(resourcesChangeConsumers); } + notificationHandlers.put("notifications/resources/list_changed", + resourcesChangeNotificationHandler(resourcesChangeConsumersFinal)); - if (promptsChangeConsumers != null && !promptsChangeConsumers.isEmpty()) { - notificationHandlers.put("notifications/prompts/list_changed", - promptsChangeNotificationHandler(promptsChangeConsumers)); + // Prompts Change Notification + List>> promptsChangeConsumersFinal = new ArrayList<>(); + promptsChangeConsumersFinal.add((notification) -> logger.info("Prompts changed: {}", notification)); + if (!Utils.isEmpty(promptsChangeConsumers)) { + promptsChangeConsumersFinal.addAll(promptsChangeConsumers); } + notificationHandlers.put("notifications/prompts/list_changed", + promptsChangeNotificationHandler(promptsChangeConsumersFinal)); this.mcpSession = new DefaultMcpSession(requestTimeout, transport, requestHanlers, notificationHandlers); diff --git a/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/util/Utils.java b/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/util/Utils.java new file mode 100644 index 0000000..7f6850a --- /dev/null +++ b/spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/util/Utils.java @@ -0,0 +1,69 @@ +/* + * Copyright 2024-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.ai.mcp.util; + +import java.util.Collection; +import java.util.Map; + +import reactor.util.annotation.Nullable; + +import org.springframework.lang.Contract; + +/** + * @author Christian Tzolov + */ + +public class Utils { + + /** + * Check whether the given {@code String} contains actual text. + *

+ * More specifically, this method returns {@code true} if the {@code String} is not + * {@code null}, its length is greater than 0, and it contains at least one + * non-whitespace character. + * @param str the {@code String} to check (may be {@code null}) + * @return {@code true} if the {@code String} is not {@code null}, its length is + * greater than 0, and it does not contain whitespace only + * @see Character#isWhitespace + */ + public static boolean hasText(@Nullable String str) { + return (str != null && !str.isBlank()); + } + + /** + * Return {@code true} if the supplied Collection is {@code null} or empty. Otherwise, + * return {@code false}. + * @param collection the Collection to check + * @return whether the given Collection is empty + */ + @Contract("null -> true") + public static boolean isEmpty(@Nullable Collection collection) { + return (collection == null || collection.isEmpty()); + } + + /** + * Return {@code true} if the supplied Map is {@code null} or empty. Otherwise, return + * {@code false}. + * @param map the Map to check + * @return whether the given Map is empty + */ + @Contract("null -> true") + public static boolean isEmpty(@Nullable Map map) { + return (map == null || map.isEmpty()); + } + +}