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.
This commit is contained in:
Christian Tzolov 2024-12-20 10:39:22 +01:00
commit 2ea5527ade
2 changed files with 96 additions and 11 deletions

View file

@ -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<Consumer<List<McpSchema.Resource>>> resourcesChangeConsumers,
List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumers) {
// Request Handlers
Map<String, RequestHandler> 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<String, NotificationHandler> notificationHandlers = new HashMap<>();
if (toolsChangeConsumers != null && !toolsChangeConsumers.isEmpty()) {
notificationHandlers.put("notifications/tools/list_changed",
toolsChangeNotificationHandler(toolsChangeConsumers));
// Tools Change Notification
List<Consumer<List<McpSchema.Tool>>> 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<Consumer<List<McpSchema.Resource>>> 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<Consumer<List<McpSchema.Prompt>>> 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);

View file

@ -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 <em>text</em>.
* <p>
* 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());
}
}