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..68875ade5 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java @@ -207,7 +207,7 @@ public Mono listRoots() { // @formatter:off return this.listRoots(McpSchema.FIRST_PAGE) - .expand(result -> (result.nextCursor() != null) ? + .expand(result -> (result.nextCursor() != null && !result.nextCursor().isEmpty()) ? this.listRoots(result.nextCursor()) : Mono.empty()) .reduceWith(() -> McpSchema.ListRootsResult.builder(new ArrayList<>()).build(), (allRootsResult, result) -> { 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..514fcdb9a 100644 --- a/mcp-core/src/test/java/io/modelcontextprotocol/server/McpAsyncServerExchangeTests.java +++ b/mcp-core/src/test/java/io/modelcontextprotocol/server/McpAsyncServerExchangeTests.java @@ -141,6 +141,37 @@ void testListRootsWithMultiplePages() { }).verifyComplete(); } + @Test + void testListRootsStopsPagingOnEmptyNextCursor() { + + List page1Roots = List + .of(McpSchema.Root.builder("file:///home/user/project1").name("Project 1").build()); + List page2Roots = List + .of(McpSchema.Root.builder("file:///home/user/project2").name("Project 2").build()); + + McpSchema.ListRootsResult page1Result = McpSchema.ListRootsResult.builder(page1Roots) + .nextCursor("cursor1") + .build(); + // A client that reports an empty next cursor ends pagination. + McpSchema.ListRootsResult page2Result = McpSchema.ListRootsResult.builder(page2Roots).nextCursor("").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("cursor1")), + any(TypeRef.class))) + .thenReturn(Mono.just(page2Result)); + + StepVerifier.create(exchange.listRoots()).assertNext(result -> { + assertThat(result.roots()).hasSize(2); + assertThat(result.roots().get(0).uri()).isEqualTo("file:///home/user/project1"); + assertThat(result.roots().get(1).uri()).isEqualTo("file:///home/user/project2"); + }).verifyComplete(); + + verify(mockSession, times(2)).sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), + any(McpSchema.PaginatedRequest.class), any(TypeRef.class)); + } + @Test void testListRootsWithEmptyResult() {