feat: Enhance MCP roots implementation and test coverage

- Add proper ListRootsResult wrapping in async client
- Implement paginated listRoots methods in server components
- Fix roots notification handling in async server
- Add roots integration tests
- Update architecture diagram and documentation layout

The changes improve the roots feature implementation with proper result
types,  and robust notification handling. Adds thorough
test coverage for roots functionality including async notifications and
root addition/removal scenarios.

Resolves #46
This commit is contained in:
Christian Tzolov 2025-01-04 01:11:34 +01:00
commit 2748260cda
3 changed files with 200 additions and 98 deletions

View file

@ -409,7 +409,7 @@ public class McpAsyncClient {
List<Root> roots = this.roots.values().stream().toList();
return Mono.just(roots);
return Mono.just(new McpSchema.ListRootsResult(roots));
};
}

View file

@ -40,7 +40,6 @@ import org.springframework.ai.mcp.spec.McpError;
import org.springframework.ai.mcp.spec.McpSchema;
import org.springframework.ai.mcp.spec.McpSchema.CallToolResult;
import org.springframework.ai.mcp.spec.McpSchema.ClientCapabilities;
import org.springframework.ai.mcp.spec.McpSchema.ListRootsResult;
import org.springframework.ai.mcp.spec.McpSchema.LoggingLevel;
import org.springframework.ai.mcp.spec.McpSchema.LoggingMessageNotification;
import org.springframework.ai.mcp.spec.McpSchema.Tool;
@ -251,28 +250,35 @@ public class McpAsyncServer {
private static TypeReference<McpSchema.ListRootsResult> LIST_ROOTS_RESULT_TYPE_REF = new TypeReference<>() {
};
/**
* Retrieves the list of all roots provided by the client.
* @return A Mono that emits the list of roots result.
*/
public Mono<McpSchema.ListRootsResult> listRoots() {
return this.listRoots(null);
}
/**
* Retrieves a paginated list of roots provided by the server.
* @param cursor Optional pagination cursor from a previous list request
* @return A Mono that emits the list of roots result containing
*/
public Mono<McpSchema.ListRootsResult> listRoots(String cursor) {
return this.mcpSession.sendRequest(McpSchema.METHOD_ROOTS_LIST, new McpSchema.PaginatedRequest(cursor),
LIST_ROOTS_RESULT_TYPE_REF);
}
private NotificationHandler rootsListChnagedNotificationHandler(
List<Consumer<List<McpSchema.Root>>> rootsChangeConsumers) {
if (this.clientCapabilities != null && this.clientCapabilities.roots() != null) {
Mono<ListRootsResult> updatedRootsList = this.mcpSession.sendRequest(McpSchema.METHOD_ROOTS_LIST, null,
LIST_ROOTS_RESULT_TYPE_REF);
return params -> updatedRootsList // @formatter:off
.flatMap(listRootsResult ->
Mono.fromRunnable(() ->
rootsChangeConsumers.stream().forEach(consumer -> consumer.accept(listRootsResult.roots())))
.subscribeOn(Schedulers.boundedElastic())) // TODO: Check if this is needed
.onErrorResume(error -> {
logger.error("Error handling roots list change notification", error);
return Mono.empty();
})
.then(); // Convert to Mono<Void>
// @formatter:on
}
return params -> Mono.empty();
return params -> {
return listRoots().flatMap(listRootsResult -> Mono.fromRunnable(() -> {
rootsChangeConsumers.stream().forEach(consumer -> consumer.accept(listRootsResult.roots()));
}).subscribeOn(Schedulers.boundedElastic())).onErrorResume(error -> {
logger.error("Error handling roots list change notification", error);
return Mono.empty();
}).then();
};
};
// ---------------------------------------

View file

@ -19,6 +19,7 @@ package org.springframework.ai.mcp.server;
import org.springframework.ai.mcp.server.McpServer.PromptRegistration;
import org.springframework.ai.mcp.server.McpServer.ResourceRegistration;
import org.springframework.ai.mcp.server.McpServer.ToolRegistration;
import org.springframework.ai.mcp.spec.McpError;
import org.springframework.ai.mcp.spec.McpSchema;
import org.springframework.ai.mcp.spec.McpSchema.ClientCapabilities;
import org.springframework.ai.mcp.spec.McpSchema.LoggingMessageNotification;
@ -47,6 +48,23 @@ public class McpSyncServer {
this.asyncServer = asyncServer;
}
/**
* Retrieves the list of all roots provided by the client.
* @return The list of roots
*/
public McpSchema.ListRootsResult listRoots() {
return this.listRoots(null);
}
/**
* Retrieves a paginated list of roots provided by the server.
* @param cursor Optional pagination cursor from a previous list request
* @return The list of roots
*/
public McpSchema.ListRootsResult listRoots(String cursor) {
return this.asyncServer.listRoots(cursor).block();
}
/**
* Add a new tool handler.
* @param toolHandler The tool handler to add