Skip to content

Commit 8873da1

Browse files
nanookclawKehrlann
authored andcommitted
fix: stop pagination on empty cursors
Signed-off-by: Nanook <nanookclaw@users.noreply.github.com>
1 parent fd00498 commit 8873da1

2 files changed

Lines changed: 144 additions & 22 deletions

File tree

mcp-core/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -818,13 +818,13 @@ private NotificationHandler asyncToolsChangeNotificationHandler(
818818
* @see #readResource(McpSchema.Resource)
819819
*/
820820
public Mono<McpSchema.ListResourcesResult> listResources() {
821-
return this.listResources(McpSchema.FIRST_PAGE)
822-
.expand(result -> (result.nextCursor() != null) ? this.listResources(result.nextCursor()) : Mono.empty())
823-
.reduce(new ArrayList<McpSchema.Resource>(), (accumulated, result) -> {
824-
accumulated.addAll(result.resources());
825-
return accumulated;
826-
})
827-
.map(all -> McpSchema.ListResourcesResult.builder(Collections.unmodifiableList(all)).build());
821+
return this.listResources(McpSchema.FIRST_PAGE).expand(result -> {
822+
String next = result.nextCursor();
823+
return (next != null && !next.isEmpty()) ? this.listResources(next) : Mono.empty();
824+
}).reduce(new ArrayList<McpSchema.Resource>(), (accumulated, result) -> {
825+
accumulated.addAll(result.resources());
826+
return accumulated;
827+
}).map(all -> McpSchema.ListResourcesResult.builder(Collections.unmodifiableList(all)).build());
828828
}
829829

830830
/**
@@ -904,14 +904,13 @@ public Mono<McpSchema.ReadResourceResult> readResource(McpSchema.ReadResourceReq
904904
* @see McpSchema.ListResourceTemplatesResult
905905
*/
906906
public Mono<McpSchema.ListResourceTemplatesResult> listResourceTemplates() {
907-
return this.listResourceTemplates(McpSchema.FIRST_PAGE)
908-
.expand(result -> (result.nextCursor() != null) ? this.listResourceTemplates(result.nextCursor())
909-
: Mono.empty())
910-
.reduce(new ArrayList<McpSchema.ResourceTemplate>(), (accumulated, result) -> {
911-
accumulated.addAll(result.resourceTemplates());
912-
return accumulated;
913-
})
914-
.map(all -> McpSchema.ListResourceTemplatesResult.builder(Collections.unmodifiableList(all)).build());
907+
return this.listResourceTemplates(McpSchema.FIRST_PAGE).expand(result -> {
908+
String next = result.nextCursor();
909+
return (next != null && !next.isEmpty()) ? this.listResourceTemplates(next) : Mono.empty();
910+
}).reduce(new ArrayList<McpSchema.ResourceTemplate>(), (accumulated, result) -> {
911+
accumulated.addAll(result.resourceTemplates());
912+
return accumulated;
913+
}).map(all -> McpSchema.ListResourceTemplatesResult.builder(Collections.unmodifiableList(all)).build());
915914
}
916915

917916
/**
@@ -1024,13 +1023,13 @@ private NotificationHandler asyncResourcesUpdatedNotificationHandler(
10241023
* @see #getPrompt(GetPromptRequest)
10251024
*/
10261025
public Mono<ListPromptsResult> listPrompts() {
1027-
return this.listPrompts(McpSchema.FIRST_PAGE)
1028-
.expand(result -> (result.nextCursor() != null) ? this.listPrompts(result.nextCursor()) : Mono.empty())
1029-
.reduce(new ArrayList<McpSchema.Prompt>(), (accumulated, result) -> {
1030-
accumulated.addAll(result.prompts());
1031-
return accumulated;
1032-
})
1033-
.map(all -> McpSchema.ListPromptsResult.builder(Collections.unmodifiableList(all)).build());
1026+
return this.listPrompts(McpSchema.FIRST_PAGE).expand(result -> {
1027+
String next = result.nextCursor();
1028+
return (next != null && !next.isEmpty()) ? this.listPrompts(next) : Mono.empty();
1029+
}).reduce(new ArrayList<McpSchema.Prompt>(), (accumulated, result) -> {
1030+
accumulated.addAll(result.prompts());
1031+
return accumulated;
1032+
}).map(all -> McpSchema.ListPromptsResult.builder(Collections.unmodifiableList(all)).build());
10341033
}
10351034

10361035
/**

mcp-test/src/test/java/io/modelcontextprotocol/client/McpAsyncClientTests.java

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import java.util.Map;
99
import java.util.Objects;
1010
import java.util.Set;
11+
import java.util.concurrent.atomic.AtomicInteger;
1112
import java.util.concurrent.atomic.AtomicReference;
1213
import java.util.function.Function;
1314
import java.util.stream.Collectors;
@@ -298,6 +299,42 @@ void testListPromptsWithCursorAndMeta() {
298299

299300
}
300301

302+
@Test
303+
void listResourcesStopsOnEmptyNextCursor() {
304+
var transport = new EmptyCursorTestMcpClientTransport(McpSchema.METHOD_RESOURCES_LIST);
305+
McpAsyncClient client = McpClient.async(transport).build();
306+
307+
McpSchema.ListResourcesResult result = client.listResources().block();
308+
309+
assertThat(result).isNotNull();
310+
assertThat(result.resources()).extracting(McpSchema.Resource::name).containsExactly("test.txt");
311+
assertThat(transport.getRequestCount()).isEqualTo(1);
312+
}
313+
314+
@Test
315+
void listResourceTemplatesStopsOnEmptyNextCursor() {
316+
var transport = new EmptyCursorTestMcpClientTransport(McpSchema.METHOD_RESOURCES_TEMPLATES_LIST);
317+
McpAsyncClient client = McpClient.async(transport).build();
318+
319+
McpSchema.ListResourceTemplatesResult result = client.listResourceTemplates().block();
320+
321+
assertThat(result).isNotNull();
322+
assertThat(result.resourceTemplates()).extracting(McpSchema.ResourceTemplate::name).containsExactly("template");
323+
assertThat(transport.getRequestCount()).isEqualTo(1);
324+
}
325+
326+
@Test
327+
void listPromptsStopsOnEmptyNextCursor() {
328+
var transport = new EmptyCursorTestMcpClientTransport(McpSchema.METHOD_PROMPT_LIST);
329+
McpAsyncClient client = McpClient.async(transport).build();
330+
331+
McpSchema.ListPromptsResult result = client.listPrompts().block();
332+
333+
assertThat(result).isNotNull();
334+
assertThat(result.prompts()).extracting(McpSchema.Prompt::name).containsExactly("test-prompt");
335+
assertThat(transport.getRequestCount()).isEqualTo(1);
336+
}
337+
301338
static class TestMcpClientTransport implements McpClientTransport {
302339

303340
private Function<Mono<McpSchema.JSONRPCMessage>, Mono<McpSchema.JSONRPCMessage>> handler;
@@ -397,4 +434,90 @@ public McpSchema.PaginatedRequest getCapturedRequest() {
397434

398435
}
399436

437+
static class EmptyCursorTestMcpClientTransport implements McpClientTransport {
438+
439+
private final String listMethod;
440+
441+
private final AtomicInteger requestCount = new AtomicInteger();
442+
443+
private Function<Mono<McpSchema.JSONRPCMessage>, Mono<McpSchema.JSONRPCMessage>> handler;
444+
445+
EmptyCursorTestMcpClientTransport(String listMethod) {
446+
this.listMethod = listMethod;
447+
}
448+
449+
@Override
450+
public Mono<Void> connect(Function<Mono<McpSchema.JSONRPCMessage>, Mono<McpSchema.JSONRPCMessage>> handler) {
451+
this.handler = handler;
452+
return Mono.empty();
453+
}
454+
455+
@Override
456+
public Mono<Void> closeGracefully() {
457+
return Mono.empty();
458+
}
459+
460+
@Override
461+
public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message) {
462+
if (!(message instanceof McpSchema.JSONRPCRequest request)) {
463+
return Mono.empty();
464+
}
465+
466+
McpSchema.JSONRPCResponse response;
467+
if (McpSchema.METHOD_INITIALIZE.equals(request.method())) {
468+
McpSchema.ServerCapabilities caps = McpSchema.ServerCapabilities.builder()
469+
.prompts(false)
470+
.resources(false, false)
471+
.tools(false)
472+
.build();
473+
474+
McpSchema.InitializeResult initResult = McpSchema.InitializeResult
475+
.builder(ProtocolVersions.MCP_2024_11_05, caps, MOCK_SERVER_INFO)
476+
.build();
477+
response = McpSchema.JSONRPCResponse.result(request.id(), initResult);
478+
}
479+
else if (this.listMethod.equals(request.method())) {
480+
this.requestCount.incrementAndGet();
481+
response = McpSchema.JSONRPCResponse.result(request.id(), resultForMethod(request.method()));
482+
}
483+
else {
484+
return Mono.empty();
485+
}
486+
487+
return this.handler.apply(Mono.just(response)).then();
488+
}
489+
490+
private Object resultForMethod(String method) {
491+
if (McpSchema.METHOD_RESOURCES_LIST.equals(method)) {
492+
McpSchema.Resource resource = McpSchema.Resource.builder("file:///test.txt", "test.txt").build();
493+
return McpSchema.ListResourcesResult.builder(List.of(resource)).nextCursor("").build();
494+
}
495+
if (McpSchema.METHOD_RESOURCES_TEMPLATES_LIST.equals(method)) {
496+
McpSchema.ResourceTemplate template = McpSchema.ResourceTemplate.builder("file:///{name}", "template")
497+
.build();
498+
return McpSchema.ListResourceTemplatesResult.builder(List.of(template)).nextCursor("").build();
499+
}
500+
if (McpSchema.METHOD_PROMPT_LIST.equals(method)) {
501+
McpSchema.Prompt prompt = McpSchema.Prompt.builder("test-prompt").build();
502+
return McpSchema.ListPromptsResult.builder(List.of(prompt)).nextCursor("").build();
503+
}
504+
throw new IllegalArgumentException("Unsupported method: " + method);
505+
}
506+
507+
@Override
508+
public <T> T unmarshalFrom(Object data, TypeRef<T> typeRef) {
509+
return JSON_MAPPER.convertValue(data, new TypeRef<>() {
510+
@Override
511+
public java.lang.reflect.Type getType() {
512+
return typeRef.getType();
513+
}
514+
});
515+
}
516+
517+
int getRequestCount() {
518+
return this.requestCount.get();
519+
}
520+
521+
}
522+
400523
}

0 commit comments

Comments
 (0)