You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
McpAsyncServerExchange#listRoots() keeps paginating when nextCursor is an empty string #1274
Bug description McpAsyncServerExchange#listRoots() follows nextCursor until it is null. If a client answers roots/list with "nextCursor": "" instead of omitting the field, the server sends roots/list again with "cursor": "". A client that keeps answering that way makes listRoots() never complete, while the roots of every page accumulate. McpSyncServerExchange#listRoots() calls .block() on it, so the calling thread hangs.
The client had the same problem (#575), fixed in #954: McpAsyncClient stops on next != null && !next.isEmpty() in listTools, listResources, listResourceTemplates and listPrompts. listRoots() on the server exchange is the last aggregate list method in mcp-core without that guard.
Affected: main, 2.1.x, 2.0.x and 1.1.x.
Environment main @ 54e8b61 (2.2.0-SNAPSHOT). Java 21.0.5, macOS.
Steps to reproduce
Connect a client whose roots/list handler returns its roots with "nextCursor": "".
Call exchange.listRoots() from a server tool, resource or prompt handler.
The server sends another roots/list request with "cursor": "".
Expected behavior
An empty nextCursor ends pagination, as it does in the client list methods since #954.
Minimal Complete Reproducible example
Failing test in McpAsyncServerExchangeTests (mcp-core):
@TestvoidtestListRootsStopsOnEmptyNextCursor() {
List<McpSchema.Root> page1Roots = Arrays
.asList(McpSchema.Root.builder("file:///home/user/project1").name("Project 1").build());
List<McpSchema.Root> page2Roots = Arrays
.asList(McpSchema.Root.builder("file:///home/user/project2").name("Project 2").build());
McpSchema.ListRootsResultpage1Result = McpSchema.ListRootsResult.builder(page1Roots).nextCursor("").build();
McpSchema.ListRootsResultpage2Result = McpSchema.ListRootsResult.builder(page2Roots).build();
when(mockSession.sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), eq(newMcpSchema.PaginatedRequest()),
any(TypeRef.class)))
.thenReturn(Mono.just(page1Result));
when(mockSession.sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), eq(newMcpSchema.PaginatedRequest("")),
any(TypeRef.class)))
.thenReturn(Mono.just(page2Result));
// Fails on main: a second roots/list request is sent with an empty cursorStepVerifier.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));
}
Activity