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

Commit 0fd93a9

Browse files
committed
feat: Add roots change notification support to MCP server
- Implement roots change notification handler in McpAsyncServer - Add rootsChangeConsumer builder methods to support single and multiple consumers - Rename info() builder method to serverInfo() for clarity - Add comprehensive tests for roots change notification functionality - Add default server info implementation in Builder Resolves #39
1 parent 0b0d508 commit 0fd93a9

File tree

6 files changed

+266
-58
lines changed

6 files changed

+266
-58
lines changed

mcp/src/main/java/org/springframework/ai/mcp/server/McpAsyncServer.java

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.util.Optional;
2424
import java.util.concurrent.ConcurrentHashMap;
2525
import java.util.concurrent.CopyOnWriteArrayList;
26+
import java.util.function.Consumer;
2627

2728
import com.fasterxml.jackson.core.type.TypeReference;
2829
import org.slf4j.Logger;
@@ -39,6 +40,7 @@
3940
import org.springframework.ai.mcp.spec.McpSchema;
4041
import org.springframework.ai.mcp.spec.McpSchema.CallToolResult;
4142
import org.springframework.ai.mcp.spec.McpSchema.ClientCapabilities;
43+
import org.springframework.ai.mcp.spec.McpSchema.ListRootsResult;
4244
import org.springframework.ai.mcp.spec.McpSchema.LoggingLevel;
4345
import org.springframework.ai.mcp.spec.McpSchema.LoggingMessageNotification;
4446
import org.springframework.ai.mcp.spec.McpSchema.Tool;
@@ -84,7 +86,6 @@ public class McpAsyncServer {
8486

8587
private LoggingLevel minLoggingLevel = LoggingLevel.DEBUG;
8688

87-
// TODO: Add support for roots list changed notification
8889
/**
8990
* Create a new McpAsyncServer with the given transport and capabilities.
9091
* @param mcpTransport The transport layer implementation for MCP communication
@@ -94,11 +95,13 @@ public class McpAsyncServer {
9495
* @param resources The map of resource registrations
9596
* @param resourceTemplates The list of resource templates
9697
* @param prompts The map of prompt registrations
98+
* @param rootsChangeConsumers The list of consumers that will be notified when the
99+
* roots list changes
97100
*/
98101
public McpAsyncServer(McpTransport mcpTransport, McpSchema.Implementation serverInfo,
99102
McpSchema.ServerCapabilities serverCapabilities, List<ToolRegistration> tools,
100103
Map<String, ResourceRegistration> resources, List<McpSchema.ResourceTemplate> resourceTemplates,
101-
Map<String, PromptRegistration> prompts) {
104+
Map<String, PromptRegistration> prompts, List<Consumer<List<McpSchema.Root>>> rootsChangeConsumers) {
102105

103106
this.serverInfo = serverInfo;
104107
this.tools = new CopyOnWriteArrayList<>(tools != null ? tools : List.of());
@@ -156,6 +159,13 @@ public McpAsyncServer(McpTransport mcpTransport, McpSchema.Implementation server
156159

157160
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_INITIALIZED, (params) -> Mono.empty());
158161

162+
if (Utils.isEmpty(rootsChangeConsumers)) {
163+
rootsChangeConsumers = List.of((roots) -> logger
164+
.warn("Roots list changed notification, but no consumers provided. Roots list changed: {}", roots));
165+
}
166+
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_ROOTS_LIST_CHANGED,
167+
rootsListChnagedNotificationHandler(rootsChangeConsumers));
168+
159169
this.transport = mcpTransport;
160170
this.mcpSession = new DefaultMcpSession(Duration.ofSeconds(10), mcpTransport, requestHandlers,
161171
notificationHandlers);
@@ -238,6 +248,33 @@ public void close() {
238248
this.mcpSession.close();
239249
}
240250

251+
private static TypeReference<McpSchema.ListRootsResult> LIST_ROOTS_RESULT_TYPE_REF = new TypeReference<>() {
252+
};
253+
254+
private NotificationHandler rootsListChnagedNotificationHandler(
255+
List<Consumer<List<McpSchema.Root>>> rootsChangeConsumers) {
256+
257+
if (this.clientCapabilities != null && this.clientCapabilities.roots() != null) {
258+
259+
Mono<ListRootsResult> updatedRootsList = this.mcpSession.sendRequest(McpSchema.METHOD_ROOTS_LIST, null,
260+
LIST_ROOTS_RESULT_TYPE_REF);
261+
262+
return params -> updatedRootsList // @formatter:off
263+
.flatMap(listRootsResult ->
264+
Mono.fromRunnable(() ->
265+
rootsChangeConsumers.stream().forEach(consumer -> consumer.accept(listRootsResult.roots())))
266+
.subscribeOn(Schedulers.boundedElastic())) // TODO: Check if this is needed
267+
.onErrorResume(error -> {
268+
logger.error("Error handling roots list change notification", error);
269+
return Mono.empty();
270+
})
271+
.then(); // Convert to Mono<Void>
272+
// @formatter:on
273+
}
274+
275+
return params -> Mono.empty();
276+
};
277+
241278
// ---------------------------------------
242279
// Tool Management
243280
// ---------------------------------------

mcp/src/main/java/org/springframework/ai/mcp/server/McpServer.java

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.util.HashMap;
2121
import java.util.List;
2222
import java.util.Map;
23+
import java.util.function.Consumer;
2324
import java.util.function.Function;
2425

2526
import org.springframework.ai.mcp.spec.McpSchema;
@@ -206,9 +207,12 @@ public static Builder using(McpTransport transport) {
206207
*/
207208
public static class Builder {
208209

210+
private final static McpSchema.Implementation DEFAULT_SERVER_INFO = new McpSchema.Implementation("mcp-server",
211+
"1.0.0");
212+
209213
private final McpTransport transport;
210214

211-
private McpSchema.Implementation serverInfo;
215+
private McpSchema.Implementation serverInfo = DEFAULT_SERVER_INFO;
212216

213217
private McpSchema.ServerCapabilities serverCapabilities;
214218

@@ -241,6 +245,8 @@ public static class Builder {
241245
*/
242246
private Map<String, PromptRegistration> prompts = new HashMap<>();
243247

248+
private List<Consumer<List<McpSchema.Root>>> rootsChangeConsumers = new ArrayList<>();
249+
244250
private Builder(McpTransport transport) {
245251
Assert.notNull(transport, "Transport must not be null");
246252
this.transport = transport;
@@ -255,22 +261,23 @@ private Builder(McpTransport transport) {
255261
* @return This builder instance for method chaining
256262
* @throws IllegalArgumentException if serverInfo is null
257263
*/
258-
public Builder info(McpSchema.Implementation serverInfo) {
264+
public Builder serverInfo(McpSchema.Implementation serverInfo) {
259265
Assert.notNull(serverInfo, "Server info must not be null");
260266
this.serverInfo = serverInfo;
261267
return this;
262268
}
263269

264270
/**
265271
* Sets the server implementation information using name and version strings. This
266-
* is a convenience method alternative to {@link #info(McpSchema.Implementation)}.
272+
* is a convenience method alternative to
273+
* {@link #serverInfo(McpSchema.Implementation)}.
267274
* @param name The server name. Must not be null or empty.
268275
* @param version The server version. Must not be null or empty.
269276
* @return This builder instance for method chaining
270277
* @throws IllegalArgumentException if name or version is null or empty
271-
* @see #info(McpSchema.Implementation)
278+
* @see #serverInfo(McpSchema.Implementation)
272279
*/
273-
public Builder info(String name, String version) {
280+
public Builder serverInfo(String name, String version) {
274281
Assert.hasText(name, "Name must not be null or empty");
275282
Assert.hasText(version, "Version must not be null or empty");
276283
this.serverInfo = new McpSchema.Implementation(name, version);
@@ -518,6 +525,49 @@ public Builder prompts(PromptRegistration... prompts) {
518525
return this;
519526
}
520527

528+
/**
529+
* Registers a consumer that will be notified when the list of roots changes. This
530+
* is useful for updating resource availability dynamically, such as when new
531+
* files are added or removed.
532+
* @param consumer The consumer to register. Must not be null.
533+
* @return This builder instance for method chaining
534+
* @throws IllegalArgumentException if consumer is null
535+
*/
536+
public Builder rootsChangeConsumer(Consumer<List<McpSchema.Root>> consumer) {
537+
Assert.notNull(consumer, "Consumer must not be null");
538+
this.rootsChangeConsumers.add(consumer);
539+
return this;
540+
}
541+
542+
/**
543+
* Registers multiple consumers that will be notified when the list of roots
544+
* changes. This method is useful when multiple consumers need to be registered at
545+
* once.
546+
* @param consumers The list of consumers to register. Must not be null.
547+
* @return This builder instance for method chaining
548+
* @throws IllegalArgumentException if consumers is null
549+
*/
550+
public Builder rootsChangeConsumers(List<Consumer<List<McpSchema.Root>>> consumers) {
551+
Assert.notNull(consumers, "Consumers list must not be null");
552+
this.rootsChangeConsumers.addAll(consumers);
553+
return this;
554+
}
555+
556+
/**
557+
* Registers multiple consumers that will be notified when the list of roots
558+
* changes using varargs. This method provides a convenient way to register
559+
* multiple consumers inline.
560+
* @param consumers The consumers to register. Must not be null.
561+
* @return This builder instance for method chaining
562+
* @throws IllegalArgumentException if consumers is null
563+
*/
564+
public Builder rootsChangeConsumers(Consumer<List<McpSchema.Root>>... consumers) {
565+
for (Consumer<List<McpSchema.Root>> consumer : consumers) {
566+
this.rootsChangeConsumers.add(consumer);
567+
}
568+
return this;
569+
}
570+
521571
/**
522572
* Builds a synchronous MCP server that provides blocking operations. Synchronous
523573
* servers process each request to completion before handling the next one, making
@@ -539,12 +589,9 @@ public McpSyncServer sync() {
539589
* settings
540590
*/
541591
public McpAsyncServer async() {
542-
if (serverInfo == null) {
543-
serverInfo = new McpSchema.Implementation("mcp-server", "1.0.0");
544-
}
545592

546593
return new McpAsyncServer(transport, serverInfo, serverCapabilities, tools, resources, resourceTemplates,
547-
prompts);
594+
prompts, rootsChangeConsumers);
548595
}
549596

550597
}

mcp/src/test/java/org/springframework/ai/mcp/attic/DemoServer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public static void main(String[] args) {
4343
SseServerTransport transport = new SseServerTransport(new ObjectMapper(), "/mcp/message");
4444

4545
var mcpServer = McpServer.using(transport)
46-
.info("Weather Forecast", "1.0.0")
46+
.serverInfo("Weather Forecast", "1.0.0")
4747
.tool(new McpSchema.Tool("weather", "Weather forecast tool by location", Map.of("city", "String")),
4848
(arguments) -> {
4949
String city = (String) arguments.get("city");

0 commit comments

Comments
 (0)