feat: Improve pagination support on client side (#306)

This change enhances the client API by providing both paginated access for fine-grained
control and convenience methods for retrieving complete result sets automatically.

- Implement "list all" methods that automatically handle pagination using expand/reduce operations
- Replace null parameters with McpSchema.FIRST_PAGE constant for better readability
- Add tests for both paginated and "list all" functionality
- Update method javadocs to clarify pagination behavior
- Maintain backward compatibility

Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
This commit is contained in:
Anurag Pant 2025-06-11 17:09:28 +00:00 • committed by Christian Tzolov
commit d4a1bfa58f
3 changed files with 403 additions and 94 deletions

View file

@ -1,3 +1,6 @@
/*
* Copyright 2024-2024 the original author or authors.
*/
package io.modelcontextprotocol.client;
import eu.rekawek.toxiproxy.Proxy;

View file

@ -149,13 +149,28 @@ public abstract class AbstractMcpAsyncClientTests {
@Test
void testListToolsWithoutInitialization() {
verifyCallSucceedsWithImplicitInitialization(client -> client.listTools(null), "listing tools");
verifyCallSucceedsWithImplicitInitialization(client -> client.listTools(McpSchema.FIRST_PAGE), "listing tools");
}
@Test
void testListTools() {
withClient(createMcpTransport(), mcpAsyncClient -> {
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listTools(null)))
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listTools(McpSchema.FIRST_PAGE)))
.consumeNextWith(result -> {
assertThat(result.tools()).isNotNull().isNotEmpty();
Tool firstTool = result.tools().get(0);
assertThat(firstTool.name()).isNotNull();
assertThat(firstTool.description()).isNotNull();
})
.verifyComplete();
});
}
@Test
void testListAllTools() {
withClient(createMcpTransport(), mcpAsyncClient -> {
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listTools()))
.consumeNextWith(result -> {
assertThat(result.tools()).isNotNull().isNotEmpty();
@ -276,13 +291,33 @@ public abstract class AbstractMcpAsyncClientTests {
@Test
void testListResourcesWithoutInitialization() {
verifyCallSucceedsWithImplicitInitialization(client -> client.listResources(null), "listing resources");
verifyCallSucceedsWithImplicitInitialization(client -> client.listResources(McpSchema.FIRST_PAGE),
"listing resources");
}
@Test
void testListResources() {
withClient(createMcpTransport(), mcpAsyncClient -> {
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listResources(null)))
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listResources(McpSchema.FIRST_PAGE)))
.consumeNextWith(resources -> {
assertThat(resources).isNotNull().satisfies(result -> {
assertThat(result.resources()).isNotNull();
if (!result.resources().isEmpty()) {
Resource firstResource = result.resources().get(0);
assertThat(firstResource.uri()).isNotNull();
assertThat(firstResource.name()).isNotNull();
}
});
})
.verifyComplete();
});
}
@Test
void testListAllResources() {
withClient(createMcpTransport(), mcpAsyncClient -> {
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listResources()))
.consumeNextWith(resources -> {
assertThat(resources).isNotNull().satisfies(result -> {
assertThat(result.resources()).isNotNull();
@ -307,13 +342,33 @@ public abstract class AbstractMcpAsyncClientTests {
@Test
void testListPromptsWithoutInitialization() {
verifyCallSucceedsWithImplicitInitialization(client -> client.listPrompts(null), "listing " + "prompts");
verifyCallSucceedsWithImplicitInitialization(client -> client.listPrompts(McpSchema.FIRST_PAGE),
"listing " + "prompts");
}
@Test
void testListPrompts() {
withClient(createMcpTransport(), mcpAsyncClient -> {
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listPrompts(null)))
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listPrompts(McpSchema.FIRST_PAGE)))
.consumeNextWith(prompts -> {
assertThat(prompts).isNotNull().satisfies(result -> {
assertThat(result.prompts()).isNotNull();
if (!result.prompts().isEmpty()) {
Prompt firstPrompt = result.prompts().get(0);
assertThat(firstPrompt.name()).isNotNull();
assertThat(firstPrompt.description()).isNotNull();
}
});
})
.verifyComplete();
});
}
@Test
void testListAllPrompts() {
withClient(createMcpTransport(), mcpAsyncClient -> {
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listPrompts()))
.consumeNextWith(prompts -> {
assertThat(prompts).isNotNull().satisfies(result -> {
assertThat(result.prompts()).isNotNull();
@ -469,12 +524,25 @@ public abstract class AbstractMcpAsyncClientTests {
@Test
void testListResourceTemplatesWithoutInitialization() {
verifyCallSucceedsWithImplicitInitialization(client -> client.listResourceTemplates(),
verifyCallSucceedsWithImplicitInitialization(client -> client.listResourceTemplates(McpSchema.FIRST_PAGE),
"listing resource templates");
}
@Test
void testListResourceTemplates() {
withClient(createMcpTransport(), mcpAsyncClient -> {
StepVerifier
.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listResourceTemplates(McpSchema.FIRST_PAGE)))
.consumeNextWith(result -> {
assertThat(result).isNotNull();
assertThat(result.resourceTemplates()).isNotNull();
})
.verifyComplete();
});
}
@Test
void testListAllResourceTemplates() {
withClient(createMcpTransport(), mcpAsyncClient -> {
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listResourceTemplates()))
.consumeNextWith(result -> {

View file

@ -151,14 +151,30 @@ public abstract class AbstractMcpSyncClientTests {
@Test
void testListToolsWithoutInitialization() {
verifyCallSucceedsWithImplicitInitialization(client -> client.listTools(null), "listing tools");
verifyCallSucceedsWithImplicitInitialization(client -> client.listTools(McpSchema.FIRST_PAGE), "listing tools");
}
@Test
void testListTools() {
withClient(createMcpTransport(), mcpSyncClient -> {
mcpSyncClient.initialize();
ListToolsResult tools = mcpSyncClient.listTools(null);
ListToolsResult tools = mcpSyncClient.listTools(McpSchema.FIRST_PAGE);
assertThat(tools).isNotNull().satisfies(result -> {
assertThat(result.tools()).isNotNull().isNotEmpty();
Tool firstTool = result.tools().get(0);
assertThat(firstTool.name()).isNotNull();
assertThat(firstTool.description()).isNotNull();
});
});
}
@Test
void testListAllTools() {
withClient(createMcpTransport(), mcpSyncClient -> {
mcpSyncClient.initialize();
ListToolsResult tools = mcpSyncClient.listTools();
assertThat(tools).isNotNull().satisfies(result -> {
assertThat(result.tools()).isNotNull().isNotEmpty();
@ -308,14 +324,33 @@ public abstract class AbstractMcpSyncClientTests {
@Test
void testListResourcesWithoutInitialization() {
verifyCallSucceedsWithImplicitInitialization(client -> client.listResources(null), "listing resources");
verifyCallSucceedsWithImplicitInitialization(client -> client.listResources(McpSchema.FIRST_PAGE),
"listing resources");
}
@Test
void testListResources() {
withClient(createMcpTransport(), mcpSyncClient -> {
mcpSyncClient.initialize();
ListResourcesResult resources = mcpSyncClient.listResources(null);
ListResourcesResult resources = mcpSyncClient.listResources(McpSchema.FIRST_PAGE);
assertThat(resources).isNotNull().satisfies(result -> {
assertThat(result.resources()).isNotNull();
if (!result.resources().isEmpty()) {
Resource firstResource = result.resources().get(0);
assertThat(firstResource.uri()).isNotNull();
assertThat(firstResource.name()).isNotNull();
}
});
});
}
@Test
void testListAllResources() {
withClient(createMcpTransport(), mcpSyncClient -> {
mcpSyncClient.initialize();
ListResourcesResult resources = mcpSyncClient.listResources();
assertThat(resources).isNotNull().satisfies(result -> {
assertThat(result.resources()).isNotNull();
@ -466,7 +501,7 @@ public abstract class AbstractMcpSyncClientTests {
@Test
void testListResourceTemplatesWithoutInitialization() {
verifyCallSucceedsWithImplicitInitialization(client -> client.listResourceTemplates(null),
verifyCallSucceedsWithImplicitInitialization(client -> client.listResourceTemplates(McpSchema.FIRST_PAGE),
"listing resource templates");
}
@ -474,7 +509,18 @@ public abstract class AbstractMcpSyncClientTests {
void testListResourceTemplates() {
withClient(createMcpTransport(), mcpSyncClient -> {
mcpSyncClient.initialize();
ListResourceTemplatesResult result = mcpSyncClient.listResourceTemplates(null);
ListResourceTemplatesResult result = mcpSyncClient.listResourceTemplates(McpSchema.FIRST_PAGE);
assertThat(result).isNotNull();
assertThat(result.resourceTemplates()).isNotNull();
});
}
@Test
void testListAllResourceTemplates() {
withClient(createMcpTransport(), mcpSyncClient -> {
mcpSyncClient.initialize();
ListResourceTemplatesResult result = mcpSyncClient.listResourceTemplates();
assertThat(result).isNotNull();
assertThat(result.resourceTemplates()).isNotNull();