Skip to content
This repository was archived by the owner on Feb 14, 2025. It is now read-only.

Commit 2ea5527

Browse files
committed
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.
1 parent 975578b commit 2ea5527

File tree

2 files changed

+96
-11
lines changed

2 files changed

+96
-11
lines changed

spring-ai-mcp-core/src/main/java/org/springframework/ai/mcp/client/McpAsyncClient.java

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import org.springframework.ai.mcp.spec.McpSchema.PaginatedRequest;
4141
import org.springframework.ai.mcp.spec.McpSchema.Root;
4242
import org.springframework.ai.mcp.spec.McpTransport;
43+
import org.springframework.ai.mcp.util.Utils;
4344

4445
/**
4546
* The Model Context Protocol (MCP) client implementation that provides asynchronous
@@ -85,29 +86,44 @@ public McpAsyncClient(McpTransport transport, Duration requestTimeout,
8586
List<Consumer<List<McpSchema.Resource>>> resourcesChangeConsumers,
8687
List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumers) {
8788

89+
// Request Handlers
8890
Map<String, RequestHandler> requestHanlers = new HashMap<>();
8991

92+
// Roots List Request Handler
9093
if (rootsListProviders != null && !rootsListProviders.isEmpty()) {
9194
requestHanlers.put("roots/list", rootsListRequestHandler(rootsListProviders));
9295
this.rootCapabilities = new McpSchema.ClientCapabilities.RootCapabilities(rootsListChangedNotification);
9396
}
9497

98+
// Notification Handlers
9599
Map<String, NotificationHandler> notificationHandlers = new HashMap<>();
96100

97-
if (toolsChangeConsumers != null && !toolsChangeConsumers.isEmpty()) {
98-
notificationHandlers.put("notifications/tools/list_changed",
99-
toolsChangeNotificationHandler(toolsChangeConsumers));
101+
// Tools Change Notification
102+
List<Consumer<List<McpSchema.Tool>>> toolsChangeConsumersFinal = new ArrayList<>();
103+
toolsChangeConsumersFinal.add((notification) -> logger.info("Tools changed: {}", notification));
104+
if (!Utils.isEmpty(toolsChangeConsumers)) {
105+
toolsChangeConsumersFinal.addAll(toolsChangeConsumers);
100106
}
101-
102-
if (resourcesChangeConsumers != null && !resourcesChangeConsumers.isEmpty()) {
103-
notificationHandlers.put("notifications/resources/list_changed",
104-
resourcesChangeNotificationHandler(resourcesChangeConsumers));
107+
notificationHandlers.put("notifications/tools/list_changed",
108+
toolsChangeNotificationHandler(toolsChangeConsumersFinal));
109+
110+
// Resources Change Notification
111+
List<Consumer<List<McpSchema.Resource>>> resourcesChangeConsumersFinal = new ArrayList<>();
112+
resourcesChangeConsumersFinal.add((notification) -> logger.info("Resources changed: {}", notification));
113+
if (!Utils.isEmpty(resourcesChangeConsumers)) {
114+
resourcesChangeConsumersFinal.addAll(resourcesChangeConsumers);
105115
}
106-
107-
if (promptsChangeConsumers != null && !promptsChangeConsumers.isEmpty()) {
108-
notificationHandlers.put("notifications/prompts/list_changed",
109-
promptsChangeNotificationHandler(promptsChangeConsumers));
116+
notificationHandlers.put("notifications/resources/list_changed",
117+
resourcesChangeNotificationHandler(resourcesChangeConsumersFinal));
118+
119+
// Prompts Change Notification
120+
List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumersFinal = new ArrayList<>();
121+
promptsChangeConsumersFinal.add((notification) -> logger.info("Prompts changed: {}", notification));
122+
if (!Utils.isEmpty(promptsChangeConsumers)) {
123+
promptsChangeConsumersFinal.addAll(promptsChangeConsumers);
110124
}
125+
notificationHandlers.put("notifications/prompts/list_changed",
126+
promptsChangeNotificationHandler(promptsChangeConsumersFinal));
111127

112128
this.mcpSession = new DefaultMcpSession(requestTimeout, transport, requestHanlers, notificationHandlers);
113129

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright 2024-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.ai.mcp.util;
18+
19+
import java.util.Collection;
20+
import java.util.Map;
21+
22+
import reactor.util.annotation.Nullable;
23+
24+
import org.springframework.lang.Contract;
25+
26+
/**
27+
* @author Christian Tzolov
28+
*/
29+
30+
public class Utils {
31+
32+
/**
33+
* Check whether the given {@code String} contains actual <em>text</em>.
34+
* <p>
35+
* More specifically, this method returns {@code true} if the {@code String} is not
36+
* {@code null}, its length is greater than 0, and it contains at least one
37+
* non-whitespace character.
38+
* @param str the {@code String} to check (may be {@code null})
39+
* @return {@code true} if the {@code String} is not {@code null}, its length is
40+
* greater than 0, and it does not contain whitespace only
41+
* @see Character#isWhitespace
42+
*/
43+
public static boolean hasText(@Nullable String str) {
44+
return (str != null && !str.isBlank());
45+
}
46+
47+
/**
48+
* Return {@code true} if the supplied Collection is {@code null} or empty. Otherwise,
49+
* return {@code false}.
50+
* @param collection the Collection to check
51+
* @return whether the given Collection is empty
52+
*/
53+
@Contract("null -> true")
54+
public static boolean isEmpty(@Nullable Collection<?> collection) {
55+
return (collection == null || collection.isEmpty());
56+
}
57+
58+
/**
59+
* Return {@code true} if the supplied Map is {@code null} or empty. Otherwise, return
60+
* {@code false}.
61+
* @param map the Map to check
62+
* @return whether the given Map is empty
63+
*/
64+
@Contract("null -> true")
65+
public static boolean isEmpty(@Nullable Map<?, ?> map) {
66+
return (map == null || map.isEmpty());
67+
}
68+
69+
}

0 commit comments

Comments
 (0)