Skip to content

feat: Add client side handlers to handle pagination + fix: Support pagination in tools change notification handler #306

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -149,13 +149,28 @@ void testConstructorWithInvalidArguments() {

@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();

Expand Down Expand Up @@ -276,13 +291,33 @@ void testCallToolWithMessageAnnotations(String messageType) {

@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();
Expand All @@ -307,13 +342,33 @@ void testMcpAsyncClientState() {

@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();
Expand Down Expand Up @@ -469,12 +524,25 @@ else if (content instanceof BlobResourceContents blobContent) {

@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 -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,14 +151,30 @@ void testConstructorWithInvalidArguments() {

@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();
Expand Down Expand Up @@ -308,14 +324,33 @@ void testRootsListChanged() {

@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();
Expand Down Expand Up @@ -466,15 +501,26 @@ else if (content instanceof BlobResourceContents blobContent) {

@Test
void testListResourceTemplatesWithoutInitialization() {
verifyCallSucceedsWithImplicitInitialization(client -> client.listResourceTemplates(null),
verifyCallSucceedsWithImplicitInitialization(client -> client.listResourceTemplates(McpSchema.FIRST_PAGE),
"listing resource templates");
}

@Test
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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@
import java.util.function.Function;
import java.util.function.Supplier;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.core.type.TypeReference;

import io.modelcontextprotocol.spec.McpClientSession;
import io.modelcontextprotocol.spec.McpClientSession.NotificationHandler;
import io.modelcontextprotocol.spec.McpClientSession.RequestHandler;
Expand All @@ -35,8 +39,6 @@
import io.modelcontextprotocol.spec.McpTransportSessionNotFoundException;
import io.modelcontextprotocol.util.Assert;
import io.modelcontextprotocol.util.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Sinks;
Expand Down Expand Up @@ -657,10 +659,18 @@ public Mono<McpSchema.CallToolResult> callTool(McpSchema.CallToolRequest callToo

/**
* Retrieves the list of all tools provided by the server.
* @return A Mono that emits the list of tools result.
* @return A Mono that emits the list of all tools result
*/
public Mono<McpSchema.ListToolsResult> listTools() {
return this.listTools(null);
return this.listTools(McpSchema.FIRST_PAGE).expand(result -> {
if (result.nextCursor() != null) {
return this.listTools(result.nextCursor());
}
return Mono.empty();
}).reduce(new McpSchema.ListToolsResult(new ArrayList<>(), null), (allToolsResult, result) -> {
allToolsResult.tools().addAll(result.tools());
return allToolsResult;
});
}

/**
Expand Down Expand Up @@ -709,12 +719,20 @@ private NotificationHandler asyncToolsChangeNotificationHandler(
* Retrieves the list of all resources provided by the server. Resources represent any
* kind of UTF-8 encoded data that an MCP server makes available to clients, such as
* database records, API responses, log files, and more.
* @return A Mono that completes with the list of resources result.
* @return A Mono that completes with the list of all resources result
* @see McpSchema.ListResourcesResult
* @see #readResource(McpSchema.Resource)
*/
public Mono<McpSchema.ListResourcesResult> listResources() {
return this.listResources(null);
return this.listResources(McpSchema.FIRST_PAGE).expand(result -> {
if (result.nextCursor() != null) {
return this.listResources(result.nextCursor());
}
return Mono.empty();
}).reduce(new McpSchema.ListResourcesResult(new ArrayList<>(), null), (allResourcesResult, result) -> {
allResourcesResult.resources().addAll(result.resources());
return allResourcesResult;
});
}

/**
Expand Down Expand Up @@ -772,11 +790,21 @@ public Mono<McpSchema.ReadResourceResult> readResource(McpSchema.ReadResourceReq
* Retrieves the list of all resource templates provided by the server. Resource
* templates allow servers to expose parameterized resources using URI templates,
* enabling dynamic resource access based on variable parameters.
* @return A Mono that completes with the list of resource templates result.
* @return A Mono that completes with the list of all resource templates result
* @see McpSchema.ListResourceTemplatesResult
*/
public Mono<McpSchema.ListResourceTemplatesResult> listResourceTemplates() {
return this.listResourceTemplates(null);
return this.listResourceTemplates(McpSchema.FIRST_PAGE).expand(result -> {
if (result.nextCursor() != null) {
return this.listResourceTemplates(result.nextCursor());
}
return Mono.empty();
})
.reduce(new McpSchema.ListResourceTemplatesResult(new ArrayList<>(), null),
(allResourceTemplatesResult, result) -> {
allResourceTemplatesResult.resourceTemplates().addAll(result.resourceTemplates());
return allResourceTemplatesResult;
});
}

/**
Expand Down Expand Up @@ -848,12 +876,20 @@ private NotificationHandler asyncResourcesChangeNotificationHandler(

/**
* Retrieves the list of all prompts provided by the server.
* @return A Mono that completes with the list of prompts result.
* @return A Mono that completes with the list of all prompts result.
* @see McpSchema.ListPromptsResult
* @see #getPrompt(GetPromptRequest)
*/
public Mono<ListPromptsResult> listPrompts() {
return this.listPrompts(null);
return this.listPrompts(McpSchema.FIRST_PAGE).expand(result -> {
if (result.nextCursor() != null) {
return this.listPrompts(result.nextCursor());
}
return Mono.empty();
}).reduce(new ListPromptsResult(new ArrayList<>(), null), (allPromptsResult, result) -> {
allPromptsResult.prompts().addAll(result.prompts());
return allPromptsResult;
});
}

/**
Expand Down
Loading