Skip to content

Commit 90c4b44

Browse files
committed
fix(client): enforce pagination timeout across all pages
1 parent dcc1dd2 commit 90c4b44

2 files changed

Lines changed: 42 additions & 10 deletions

File tree

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

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1055,14 +1055,20 @@ private <R, A> Mono<R> paginate(Function<String, Mono<R>> pageFetcher, Function<
10551055
Supplier<A> initialAccumulator, BiFunction<A, R, A> accumulate, Function<A, R> finalize) {
10561056
return Mono.defer(() -> {
10571057
PaginationGuard guard = new PaginationGuard(this.paginationConfig);
1058-
return pageFetcher.apply(McpSchema.FIRST_PAGE).expand(page -> {
1058+
Mono<R> result = pageFetcher.apply(McpSchema.FIRST_PAGE).expand(page -> {
10591059
String next = nextCursorOf.apply(page);
10601060
if (next == null || next.isEmpty()) {
10611061
return Mono.empty();
10621062
}
10631063
guard.beforeNextPage(next);
10641064
return pageFetcher.apply(next);
10651065
}).reduce(initialAccumulator.get(), accumulate).map(finalize);
1066+
if (this.paginationConfig.timeout() != null) {
1067+
return result.timeout(this.paginationConfig.timeout(), Mono.error(new McpPaginationException(
1068+
"Pagination timed out after " + this.paginationConfig.timeout()
1069+
+ ". Increase paginationTimeout if this is expected for the server.")));
1070+
}
1071+
return result;
10661072
});
10671073
}
10681074

@@ -1077,8 +1083,6 @@ private static final class PaginationGuard {
10771083

10781084
private final PaginationConfig config;
10791085

1080-
private final long startNanos = System.nanoTime();
1081-
10821086
private int pagesFetched = 1;
10831087

10841088
PaginationGuard(PaginationConfig config) {
@@ -1100,11 +1104,6 @@ void beforeNextPage(String cursor) {
11001104
"Pagination limit exceeded: the server returned more than " + this.config.maxPages()
11011105
+ " pages. Increase maxPaginationPages if this is expected for the server.");
11021106
}
1103-
if (this.config.timeout() != null
1104-
&& Duration.ofNanos(System.nanoTime() - this.startNanos).compareTo(this.config.timeout()) > 0) {
1105-
throw new McpPaginationException("Pagination timed out after " + this.config.timeout()
1106-
+ ". Increase paginationTimeout if this is expected for the server.");
1107-
}
11081107
this.pagesFetched++;
11091108
}
11101109

‎mcp-test/src/test/java/io/modelcontextprotocol/client/McpAsyncClientPaginationTests.java‎

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ default Duration pageDelay() {
6565
return Duration.ZERO;
6666
}
6767

68+
default Duration pageDelay(String cursor) {
69+
return pageDelay();
70+
}
71+
6872
}
6973

7074
private McpClientTransport createPaginatedTransport(PaginatedServer server, AtomicInteger toolsRequests,
@@ -119,8 +123,9 @@ else if (McpSchema.METHOD_RESOURCES_LIST.equals(request.method())) {
119123
}
120124

121125
Mono<McpSchema.JSONRPCMessage> responseMono = Mono.just(response);
122-
if (!server.pageDelay().isZero()) {
123-
responseMono = responseMono.delayElement(server.pageDelay());
126+
Duration delay = server.pageDelay(cursorOf(request));
127+
if (!delay.isZero()) {
128+
responseMono = responseMono.delayElement(delay);
124129
}
125130
return responseMono.flatMap(r -> handler.apply(Mono.just(r))).then();
126131
}
@@ -235,6 +240,34 @@ public Duration pageDelay() {
235240
}).verify();
236241
}
237242

243+
@Test
244+
void listToolsTimesOutWhileWaitingForFinalPage() {
245+
AtomicInteger toolsRequests = new AtomicInteger();
246+
AtomicInteger resourcesRequests = new AtomicInteger();
247+
PaginatedServer server = new PaginatedServer() {
248+
@Override
249+
public String nextCursorFor(String cursor) {
250+
return cursor == null ? "last-page" : null;
251+
}
252+
253+
@Override
254+
public Duration pageDelay(String cursor) {
255+
return cursor == null ? Duration.ofMillis(20) : Duration.ofMillis(800);
256+
}
257+
};
258+
McpAsyncClient client = McpClient.async(createPaginatedTransport(server, toolsRequests, resourcesRequests))
259+
.paginationTimeout(Duration.ofMillis(300))
260+
.build();
261+
262+
client.initialize().block();
263+
264+
StepVerifier.create(client.listTools()).expectErrorSatisfies(error -> {
265+
assertThat(error).isInstanceOf(McpPaginationException.class);
266+
assertThat(error.getMessage()).contains("timed out");
267+
}).verify();
268+
assertThat(toolsRequests.get()).isEqualTo(2);
269+
}
270+
238271
@Test
239272
void listResourcesAggregatesAllPagesUntilNullCursor() {
240273
AtomicInteger toolsRequests = new AtomicInteger();

0 commit comments

Comments
 (0)