From 2ee67c953e00458983fdbae374304cccd62da54d Mon Sep 17 00:00:00 2001 From: subhash polisetti Date: Thu, 22 Oct 2026 16:31:26 -0700 Subject: [PATCH] Fix listRoots() pagination on an empty nextCursor McpAsyncServerExchange#listRoots() kept following nextCursor when a client returned "" instead of omitting it, sending roots/list again with an empty cursor. Apply the same guard the client list methods already use. --- .../server/McpAsyncServerExchange.java | 6 ++-- .../server/McpAsyncServerExchangeTests.java | 28 +++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java index 280a3c87a..4f70fa3ad 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java @@ -207,8 +207,10 @@ public Mono listRoots() { // @formatter:off return this.listRoots(McpSchema.FIRST_PAGE) - .expand(result -> (result.nextCursor() != null) ? - this.listRoots(result.nextCursor()) : Mono.empty()) + .expand(result -> { + String next = result.nextCursor(); + return (next != null && !next.isEmpty()) ? this.listRoots(next) : Mono.empty(); + }) .reduceWith(() -> McpSchema.ListRootsResult.builder(new ArrayList<>()).build(), (allRootsResult, result) -> { allRootsResult.roots().addAll(result.roots()); diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/server/McpAsyncServerExchangeTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/server/McpAsyncServerExchangeTests.java index d56829a74..80a69c12e 100644 --- a/mcp-core/src/test/java/io/modelcontextprotocol/server/McpAsyncServerExchangeTests.java +++ b/mcp-core/src/test/java/io/modelcontextprotocol/server/McpAsyncServerExchangeTests.java @@ -160,6 +160,34 @@ void testListRootsWithEmptyResult() { }).verifyComplete(); } + @Test + void testListRootsStopsOnEmptyNextCursor() { + + List page1Roots = Arrays + .asList(McpSchema.Root.builder("file:///home/user/project1").name("Project 1").build()); + List page2Roots = Arrays + .asList(McpSchema.Root.builder("file:///home/user/project2").name("Project 2").build()); + + McpSchema.ListRootsResult page1Result = McpSchema.ListRootsResult.builder(page1Roots).nextCursor("").build(); + McpSchema.ListRootsResult page2Result = McpSchema.ListRootsResult.builder(page2Roots).build(); + + when(mockSession.sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), eq(new McpSchema.PaginatedRequest()), + any(TypeRef.class))) + .thenReturn(Mono.just(page1Result)); + + when(mockSession.sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), eq(new McpSchema.PaginatedRequest("")), + any(TypeRef.class))) + .thenReturn(Mono.just(page2Result)); + + StepVerifier.create(exchange.listRoots()).assertNext(result -> { + assertThat(result.roots()).extracting(McpSchema.Root::uri).containsExactly("file:///home/user/project1"); + assertThat(result.nextCursor()).isNull(); + }).verifyComplete(); + + verify(mockSession, times(1)).sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), + any(McpSchema.PaginatedRequest.class), any(TypeRef.class)); + } + @Test void testListRootsWithSpecificCursor() {