Skip to content

McpAsyncServerExchange#listRoots() keeps paginating when nextCursor is an empty string #1274

Open

Description

@subhashpolisetti

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

  1. Connect a client whose roots/list handler returns its roots with "nextCursor": "".
  2. Call exchange.listRoots() from a server tool, resource or prompt handler.
  3. 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):

@Test
void testListRootsStopsOnEmptyNextCursor() {
	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.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));

	// Fails on main: a second roots/list request is sent with an empty cursor
	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));
}

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions