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
10 changed files with 403 additions and 94 deletions

View file

@ -1,16 +1,12 @@
package io.modelcontextprotocol.client.transport; package io.modelcontextprotocol.client.transport;
import com.fasterxml.jackson.core.type.TypeReference; import java.io.IOException;
import com.fasterxml.jackson.databind.ObjectMapper; import java.util.List;
import io.modelcontextprotocol.spec.DefaultMcpTransportSession; import java.util.Optional;
import io.modelcontextprotocol.spec.DefaultMcpTransportStream; import java.util.concurrent.atomic.AtomicReference;
import io.modelcontextprotocol.spec.McpClientTransport; import java.util.function.Consumer;
import io.modelcontextprotocol.spec.McpError; import java.util.function.Function;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpTransportSessionNotFoundException;
import io.modelcontextprotocol.spec.McpTransportSession;
import io.modelcontextprotocol.spec.McpTransportStream;
import io.modelcontextprotocol.util.Assert;
import org.reactivestreams.Publisher; import org.reactivestreams.Publisher;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -21,20 +17,25 @@ import org.springframework.http.codec.ServerSentEvent;
import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException; import org.springframework.web.reactive.function.client.WebClientResponseException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.spec.DefaultMcpTransportSession;
import io.modelcontextprotocol.spec.DefaultMcpTransportStream;
import io.modelcontextprotocol.spec.McpClientTransport;
import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpTransportSession;
import io.modelcontextprotocol.spec.McpTransportSessionNotFoundException;
import io.modelcontextprotocol.spec.McpTransportStream;
import io.modelcontextprotocol.util.Assert;
import reactor.core.Disposable; import reactor.core.Disposable;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.util.function.Tuple2; import reactor.util.function.Tuple2;
import reactor.util.function.Tuples; import reactor.util.function.Tuples;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
/** /**
* An implementation of the Streamable HTTP protocol as defined by the * An implementation of the Streamable HTTP protocol as defined by the
* <code>2025-03-26</code> version of the MCP specification. * <code>2025-03-26</code> version of the MCP specification.

View file

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

View file

@ -149,13 +149,28 @@ public abstract class AbstractMcpAsyncClientTests {
@Test @Test
void testListToolsWithoutInitialization() { void testListToolsWithoutInitialization() {
verifyCallSucceedsWithImplicitInitialization(client -> client.listTools(null), "listing tools"); verifyCallSucceedsWithImplicitInitialization(client -> client.listTools(McpSchema.FIRST_PAGE), "listing tools");
} }
@Test @Test
void testListTools() { void testListTools() {
withClient(createMcpTransport(), mcpAsyncClient -> { 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 -> { .consumeNextWith(result -> {
assertThat(result.tools()).isNotNull().isNotEmpty(); assertThat(result.tools()).isNotNull().isNotEmpty();
@ -276,13 +291,33 @@ public abstract class AbstractMcpAsyncClientTests {
@Test @Test
void testListResourcesWithoutInitialization() { void testListResourcesWithoutInitialization() {
verifyCallSucceedsWithImplicitInitialization(client -> client.listResources(null), "listing resources"); verifyCallSucceedsWithImplicitInitialization(client -> client.listResources(McpSchema.FIRST_PAGE),
"listing resources");
} }
@Test @Test
void testListResources() { void testListResources() {
withClient(createMcpTransport(), mcpAsyncClient -> { 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 -> { .consumeNextWith(resources -> {
assertThat(resources).isNotNull().satisfies(result -> { assertThat(resources).isNotNull().satisfies(result -> {
assertThat(result.resources()).isNotNull(); assertThat(result.resources()).isNotNull();
@ -307,13 +342,33 @@ public abstract class AbstractMcpAsyncClientTests {
@Test @Test
void testListPromptsWithoutInitialization() { void testListPromptsWithoutInitialization() {
verifyCallSucceedsWithImplicitInitialization(client -> client.listPrompts(null), "listing " + "prompts"); verifyCallSucceedsWithImplicitInitialization(client -> client.listPrompts(McpSchema.FIRST_PAGE),
"listing " + "prompts");
} }
@Test @Test
void testListPrompts() { void testListPrompts() {
withClient(createMcpTransport(), mcpAsyncClient -> { 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 -> { .consumeNextWith(prompts -> {
assertThat(prompts).isNotNull().satisfies(result -> { assertThat(prompts).isNotNull().satisfies(result -> {
assertThat(result.prompts()).isNotNull(); assertThat(result.prompts()).isNotNull();
@ -469,12 +524,25 @@ public abstract class AbstractMcpAsyncClientTests {
@Test @Test
void testListResourceTemplatesWithoutInitialization() { void testListResourceTemplatesWithoutInitialization() {
verifyCallSucceedsWithImplicitInitialization(client -> client.listResourceTemplates(), verifyCallSucceedsWithImplicitInitialization(client -> client.listResourceTemplates(McpSchema.FIRST_PAGE),
"listing resource templates"); "listing resource templates");
} }
@Test @Test
void testListResourceTemplates() { 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 -> { withClient(createMcpTransport(), mcpAsyncClient -> {
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listResourceTemplates())) StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listResourceTemplates()))
.consumeNextWith(result -> { .consumeNextWith(result -> {

View file

@ -151,14 +151,30 @@ public abstract class AbstractMcpSyncClientTests {
@Test @Test
void testListToolsWithoutInitialization() { void testListToolsWithoutInitialization() {
verifyCallSucceedsWithImplicitInitialization(client -> client.listTools(null), "listing tools"); verifyCallSucceedsWithImplicitInitialization(client -> client.listTools(McpSchema.FIRST_PAGE), "listing tools");
} }
@Test @Test
void testListTools() { void testListTools() {
withClient(createMcpTransport(), mcpSyncClient -> { withClient(createMcpTransport(), mcpSyncClient -> {
mcpSyncClient.initialize(); 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(tools).isNotNull().satisfies(result -> {
assertThat(result.tools()).isNotNull().isNotEmpty(); assertThat(result.tools()).isNotNull().isNotEmpty();
@ -308,14 +324,33 @@ public abstract class AbstractMcpSyncClientTests {
@Test @Test
void testListResourcesWithoutInitialization() { void testListResourcesWithoutInitialization() {
verifyCallSucceedsWithImplicitInitialization(client -> client.listResources(null), "listing resources"); verifyCallSucceedsWithImplicitInitialization(client -> client.listResources(McpSchema.FIRST_PAGE),
"listing resources");
} }
@Test @Test
void testListResources() { void testListResources() {
withClient(createMcpTransport(), mcpSyncClient -> { withClient(createMcpTransport(), mcpSyncClient -> {
mcpSyncClient.initialize(); 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(resources).isNotNull().satisfies(result -> {
assertThat(result.resources()).isNotNull(); assertThat(result.resources()).isNotNull();
@ -466,7 +501,7 @@ public abstract class AbstractMcpSyncClientTests {
@Test @Test
void testListResourceTemplatesWithoutInitialization() { void testListResourceTemplatesWithoutInitialization() {
verifyCallSucceedsWithImplicitInitialization(client -> client.listResourceTemplates(null), verifyCallSucceedsWithImplicitInitialization(client -> client.listResourceTemplates(McpSchema.FIRST_PAGE),
"listing resource templates"); "listing resource templates");
} }
@ -474,7 +509,18 @@ public abstract class AbstractMcpSyncClientTests {
void testListResourceTemplates() { void testListResourceTemplates() {
withClient(createMcpTransport(), mcpSyncClient -> { withClient(createMcpTransport(), mcpSyncClient -> {
mcpSyncClient.initialize(); 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).isNotNull();
assertThat(result.resourceTemplates()).isNotNull(); assertThat(result.resourceTemplates()).isNotNull();

View file

@ -13,7 +13,11 @@ import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Supplier; import java.util.function.Supplier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import io.modelcontextprotocol.spec.McpClientSession; import io.modelcontextprotocol.spec.McpClientSession;
import io.modelcontextprotocol.spec.McpClientSession.NotificationHandler; import io.modelcontextprotocol.spec.McpClientSession.NotificationHandler;
import io.modelcontextprotocol.spec.McpClientSession.RequestHandler; import io.modelcontextprotocol.spec.McpClientSession.RequestHandler;
@ -35,8 +39,6 @@ import io.modelcontextprotocol.spec.McpSchema.Root;
import io.modelcontextprotocol.spec.McpTransportSessionNotFoundException; import io.modelcontextprotocol.spec.McpTransportSessionNotFoundException;
import io.modelcontextprotocol.util.Assert; import io.modelcontextprotocol.util.Assert;
import io.modelcontextprotocol.util.Utils; import io.modelcontextprotocol.util.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.core.publisher.Sinks; import reactor.core.publisher.Sinks;
@ -657,10 +659,18 @@ public class McpAsyncClient {
/** /**
* Retrieves the list of all tools provided by the server. * 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() { 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;
});
} }
/** /**
@ -709,12 +719,20 @@ public class McpAsyncClient {
* Retrieves the list of all resources provided by the server. Resources represent any * 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 * kind of UTF-8 encoded data that an MCP server makes available to clients, such as
* database records, API responses, log files, and more. * 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 McpSchema.ListResourcesResult
* @see #readResource(McpSchema.Resource) * @see #readResource(McpSchema.Resource)
*/ */
public Mono<McpSchema.ListResourcesResult> listResources() { 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;
});
} }
/** /**
@ -772,11 +790,21 @@ public class McpAsyncClient {
* Retrieves the list of all resource templates provided by the server. Resource * Retrieves the list of all resource templates provided by the server. Resource
* templates allow servers to expose parameterized resources using URI templates, * templates allow servers to expose parameterized resources using URI templates,
* enabling dynamic resource access based on variable parameters. * 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 * @see McpSchema.ListResourceTemplatesResult
*/ */
public Mono<McpSchema.ListResourceTemplatesResult> listResourceTemplates() { 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;
});
} }
/** /**
@ -848,12 +876,20 @@ public class McpAsyncClient {
/** /**
* Retrieves the list of all prompts provided by the server. * 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 McpSchema.ListPromptsResult
* @see #getPrompt(GetPromptRequest) * @see #getPrompt(GetPromptRequest)
*/ */
public Mono<ListPromptsResult> listPrompts() { 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;
});
} }
/** /**

View file

@ -5,6 +5,10 @@
package io.modelcontextprotocol.client; package io.modelcontextprotocol.client;
import java.time.Duration; import java.time.Duration;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities; import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
@ -12,8 +16,6 @@ import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
import io.modelcontextprotocol.spec.McpSchema.GetPromptResult; import io.modelcontextprotocol.spec.McpSchema.GetPromptResult;
import io.modelcontextprotocol.spec.McpSchema.ListPromptsResult; import io.modelcontextprotocol.spec.McpSchema.ListPromptsResult;
import io.modelcontextprotocol.util.Assert; import io.modelcontextprotocol.util.Assert;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** /**
* A synchronous client implementation for the Model Context Protocol (MCP) that wraps an * A synchronous client implementation for the Model Context Protocol (MCP) that wraps an
@ -220,8 +222,8 @@ public class McpSyncClient implements AutoCloseable {
/** /**
* Retrieves the list of all tools provided by the server. * Retrieves the list of all tools provided by the server.
* @return The list of tools result containing: - tools: List of available tools, each * @return The list of all tools result containing: - tools: List of available tools,
* with a name, description, and input schema - nextCursor: Optional cursor for * each with a name, description, and input schema - nextCursor: Optional cursor for
* pagination if more tools are available * pagination if more tools are available
*/ */
public McpSchema.ListToolsResult listTools() { public McpSchema.ListToolsResult listTools() {
@ -244,22 +246,22 @@ public class McpSyncClient implements AutoCloseable {
// -------------------------- // --------------------------
/** /**
* Send a resources/list request. * Retrieves the list of all resources provided by the server.
* @param cursor the cursor * @return The list of all resources result
* @return the list of resources result.
*/
public McpSchema.ListResourcesResult listResources(String cursor) {
return this.delegate.listResources(cursor).block();
}
/**
* Send a resources/list request.
* @return the list of resources result.
*/ */
public McpSchema.ListResourcesResult listResources() { public McpSchema.ListResourcesResult listResources() {
return this.delegate.listResources().block(); return this.delegate.listResources().block();
} }
/**
* Retrieves a paginated list of resources provided by the server.
* @param cursor Optional pagination cursor from a previous list request
* @return The list of resources result
*/
public McpSchema.ListResourcesResult listResources(String cursor) {
return this.delegate.listResources(cursor).block();
}
/** /**
* Send a resources/read request. * Send a resources/read request.
* @param resource the resource to read * @param resource the resource to read
@ -279,25 +281,25 @@ public class McpSyncClient implements AutoCloseable {
} }
/** /**
* Resource templates allow servers to expose parameterized resources using URI * Retrieves the list of all resource templates provided by the server.
* templates. Arguments may be auto-completed through the completion API. * @return The list of all resource templates result.
*
* Request a list of resource templates the server has.
* @param cursor the cursor
* @return the list of resource templates result.
*/
public McpSchema.ListResourceTemplatesResult listResourceTemplates(String cursor) {
return this.delegate.listResourceTemplates(cursor).block();
}
/**
* Request a list of resource templates the server has.
* @return the list of resource templates result.
*/ */
public McpSchema.ListResourceTemplatesResult listResourceTemplates() { public McpSchema.ListResourceTemplatesResult listResourceTemplates() {
return this.delegate.listResourceTemplates().block(); return this.delegate.listResourceTemplates().block();
} }
/**
* Resource templates allow servers to expose parameterized resources using URI
* templates. Arguments may be auto-completed through the completion API.
*
* Retrieves a paginated list of resource templates provided by the server.
* @param cursor Optional pagination cursor from a previous list request
* @return The list of resource templates result.
*/
public McpSchema.ListResourceTemplatesResult listResourceTemplates(String cursor) {
return this.delegate.listResourceTemplates(cursor).block();
}
/** /**
* Subscriptions. The protocol supports optional subscriptions to resource changes. * Subscriptions. The protocol supports optional subscriptions to resource changes.
* Clients can subscribe to specific resources and receive notifications when they * Clients can subscribe to specific resources and receive notifications when they
@ -323,14 +325,24 @@ public class McpSyncClient implements AutoCloseable {
// -------------------------- // --------------------------
// Prompts // Prompts
// -------------------------- // --------------------------
public ListPromptsResult listPrompts(String cursor) {
return this.delegate.listPrompts(cursor).block();
}
/**
* Retrieves the list of all prompts provided by the server.
* @return The list of all prompts result.
*/
public ListPromptsResult listPrompts() { public ListPromptsResult listPrompts() {
return this.delegate.listPrompts().block(); return this.delegate.listPrompts().block();
} }
/**
* Retrieves a paginated list of prompts provided by the server.
* @param cursor Optional pagination cursor from a previous list request
* @return The list of prompts result.
*/
public ListPromptsResult listPrompts(String cursor) {
return this.delegate.listPrompts(cursor).block();
}
public GetPromptResult getPrompt(GetPromptRequest getPromptRequest) { public GetPromptResult getPrompt(GetPromptRequest getPromptRequest) {
return this.delegate.getPrompt(getPromptRequest).block(); return this.delegate.getPrompt(getPromptRequest).block();
} }

View file

@ -44,6 +44,8 @@ public final class McpSchema {
public static final String JSONRPC_VERSION = "2.0"; public static final String JSONRPC_VERSION = "2.0";
public static final String FIRST_PAGE = null;
// --------------------------- // ---------------------------
// Method Names // Method Names
// --------------------------- // ---------------------------

View file

@ -150,13 +150,28 @@ public abstract class AbstractMcpAsyncClientTests {
@Test @Test
void testListToolsWithoutInitialization() { void testListToolsWithoutInitialization() {
verifyCallSucceedsWithImplicitInitialization(client -> client.listTools(null), "listing tools"); verifyCallSucceedsWithImplicitInitialization(client -> client.listTools(McpSchema.FIRST_PAGE), "listing tools");
} }
@Test @Test
void testListTools() { void testListTools() {
withClient(createMcpTransport(), mcpAsyncClient -> { 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 -> { .consumeNextWith(result -> {
assertThat(result.tools()).isNotNull().isNotEmpty(); assertThat(result.tools()).isNotNull().isNotEmpty();
@ -277,13 +292,33 @@ public abstract class AbstractMcpAsyncClientTests {
@Test @Test
void testListResourcesWithoutInitialization() { void testListResourcesWithoutInitialization() {
verifyCallSucceedsWithImplicitInitialization(client -> client.listResources(null), "listing resources"); verifyCallSucceedsWithImplicitInitialization(client -> client.listResources(McpSchema.FIRST_PAGE),
"listing resources");
} }
@Test @Test
void testListResources() { void testListResources() {
withClient(createMcpTransport(), mcpAsyncClient -> { 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 -> { .consumeNextWith(resources -> {
assertThat(resources).isNotNull().satisfies(result -> { assertThat(resources).isNotNull().satisfies(result -> {
assertThat(result.resources()).isNotNull(); assertThat(result.resources()).isNotNull();
@ -308,13 +343,33 @@ public abstract class AbstractMcpAsyncClientTests {
@Test @Test
void testListPromptsWithoutInitialization() { void testListPromptsWithoutInitialization() {
verifyCallSucceedsWithImplicitInitialization(client -> client.listPrompts(null), "listing " + "prompts"); verifyCallSucceedsWithImplicitInitialization(client -> client.listPrompts(McpSchema.FIRST_PAGE),
"listing " + "prompts");
} }
@Test @Test
void testListPrompts() { void testListPrompts() {
withClient(createMcpTransport(), mcpAsyncClient -> { 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 -> { .consumeNextWith(prompts -> {
assertThat(prompts).isNotNull().satisfies(result -> { assertThat(prompts).isNotNull().satisfies(result -> {
assertThat(result.prompts()).isNotNull(); assertThat(result.prompts()).isNotNull();
@ -470,12 +525,25 @@ public abstract class AbstractMcpAsyncClientTests {
@Test @Test
void testListResourceTemplatesWithoutInitialization() { void testListResourceTemplatesWithoutInitialization() {
verifyCallSucceedsWithImplicitInitialization(client -> client.listResourceTemplates(), verifyCallSucceedsWithImplicitInitialization(client -> client.listResourceTemplates(McpSchema.FIRST_PAGE),
"listing resource templates"); "listing resource templates");
} }
@Test @Test
void testListResourceTemplates() { 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 -> { withClient(createMcpTransport(), mcpAsyncClient -> {
StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listResourceTemplates())) StepVerifier.create(mcpAsyncClient.initialize().then(mcpAsyncClient.listResourceTemplates()))
.consumeNextWith(result -> { .consumeNextWith(result -> {
@ -579,6 +647,7 @@ public abstract class AbstractMcpAsyncClientTests {
// --------------------------------------- // ---------------------------------------
// Logging Tests // Logging Tests
// --------------------------------------- // ---------------------------------------
@Test @Test
void testLoggingLevelsWithoutInitialization() { void testLoggingLevelsWithoutInitialization() {
verifyNotificationSucceedsWithImplicitInitialization( verifyNotificationSucceedsWithImplicitInitialization(

View file

@ -152,14 +152,30 @@ public abstract class AbstractMcpSyncClientTests {
@Test @Test
void testListToolsWithoutInitialization() { void testListToolsWithoutInitialization() {
verifyCallSucceedsWithImplicitInitialization(client -> client.listTools(null), "listing tools"); verifyCallSucceedsWithImplicitInitialization(client -> client.listTools(McpSchema.FIRST_PAGE), "listing tools");
} }
@Test @Test
void testListTools() { void testListTools() {
withClient(createMcpTransport(), mcpSyncClient -> { withClient(createMcpTransport(), mcpSyncClient -> {
mcpSyncClient.initialize(); 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(tools).isNotNull().satisfies(result -> {
assertThat(result.tools()).isNotNull().isNotEmpty(); assertThat(result.tools()).isNotNull().isNotEmpty();
@ -309,14 +325,33 @@ public abstract class AbstractMcpSyncClientTests {
@Test @Test
void testListResourcesWithoutInitialization() { void testListResourcesWithoutInitialization() {
verifyCallSucceedsWithImplicitInitialization(client -> client.listResources(null), "listing resources"); verifyCallSucceedsWithImplicitInitialization(client -> client.listResources(McpSchema.FIRST_PAGE),
"listing resources");
} }
@Test @Test
void testListResources() { void testListResources() {
withClient(createMcpTransport(), mcpSyncClient -> { withClient(createMcpTransport(), mcpSyncClient -> {
mcpSyncClient.initialize(); 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(resources).isNotNull().satisfies(result -> {
assertThat(result.resources()).isNotNull(); assertThat(result.resources()).isNotNull();
@ -467,7 +502,7 @@ public abstract class AbstractMcpSyncClientTests {
@Test @Test
void testListResourceTemplatesWithoutInitialization() { void testListResourceTemplatesWithoutInitialization() {
verifyCallSucceedsWithImplicitInitialization(client -> client.listResourceTemplates(null), verifyCallSucceedsWithImplicitInitialization(client -> client.listResourceTemplates(McpSchema.FIRST_PAGE),
"listing resource templates"); "listing resource templates");
} }
@ -475,7 +510,18 @@ public abstract class AbstractMcpSyncClientTests {
void testListResourceTemplates() { void testListResourceTemplates() {
withClient(createMcpTransport(), mcpSyncClient -> { withClient(createMcpTransport(), mcpSyncClient -> {
mcpSyncClient.initialize(); 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).isNotNull();
assertThat(result.resourceTemplates()).isNotNull(); assertThat(result.resourceTemplates()).isNotNull();

View file

@ -17,6 +17,7 @@ import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities; import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
import io.modelcontextprotocol.spec.McpSchema.InitializeResult; import io.modelcontextprotocol.spec.McpSchema.InitializeResult;
import io.modelcontextprotocol.spec.McpSchema.PaginatedRequest;
import io.modelcontextprotocol.spec.McpSchema.Root; import io.modelcontextprotocol.spec.McpSchema.Root;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
@ -109,27 +110,52 @@ class McpAsyncClientResponseHandlerTests {
// Create a mock tools list that the server will return // Create a mock tools list that the server will return
Map<String, Object> inputSchema = Map.of("type", "object", "properties", Map.of(), "required", List.of()); Map<String, Object> inputSchema = Map.of("type", "object", "properties", Map.of(), "required", List.of());
McpSchema.Tool mockTool = new McpSchema.Tool("test-tool", "Test Tool Description", McpSchema.Tool mockTool = new McpSchema.Tool("test-tool-1", "Test Tool 1 Description",
new ObjectMapper().writeValueAsString(inputSchema)); new ObjectMapper().writeValueAsString(inputSchema));
McpSchema.ListToolsResult mockToolsResult = new McpSchema.ListToolsResult(List.of(mockTool), null);
// Create page 1 response with nextPageToken
String nextPageToken = "page2Token";
McpSchema.ListToolsResult mockToolsResult1 = new McpSchema.ListToolsResult(List.of(mockTool), nextPageToken);
// Simulate server sending tools/list_changed notification // Simulate server sending tools/list_changed notification
McpSchema.JSONRPCNotification notification = new McpSchema.JSONRPCNotification(McpSchema.JSONRPC_VERSION, McpSchema.JSONRPCNotification notification = new McpSchema.JSONRPCNotification(McpSchema.JSONRPC_VERSION,
McpSchema.METHOD_NOTIFICATION_TOOLS_LIST_CHANGED, null); McpSchema.METHOD_NOTIFICATION_TOOLS_LIST_CHANGED, null);
transport.simulateIncomingMessage(notification); transport.simulateIncomingMessage(notification);
// Simulate server response to tools/list request // Simulate server response to first tools/list request
McpSchema.JSONRPCRequest toolsListRequest = transport.getLastSentMessageAsRequest(); McpSchema.JSONRPCRequest toolsListRequest1 = transport.getLastSentMessageAsRequest();
assertThat(toolsListRequest.method()).isEqualTo(McpSchema.METHOD_TOOLS_LIST); assertThat(toolsListRequest1.method()).isEqualTo(McpSchema.METHOD_TOOLS_LIST);
McpSchema.JSONRPCResponse toolsListResponse = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, McpSchema.JSONRPCResponse toolsListResponse1 = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION,
toolsListRequest.id(), mockToolsResult, null); toolsListRequest1.id(), mockToolsResult1, null);
transport.simulateIncomingMessage(toolsListResponse); transport.simulateIncomingMessage(toolsListResponse1);
// Verify the consumer received the expected tools // Create mock tools for page 2
assertThat(receivedTools).hasSize(1); McpSchema.Tool mockTool2 = new McpSchema.Tool("test-tool-2", "Test Tool 2 Description",
assertThat(receivedTools.get(0).name()).isEqualTo("test-tool"); new ObjectMapper().writeValueAsString(inputSchema));
assertThat(receivedTools.get(0).description()).isEqualTo("Test Tool Description");
// Create page 2 response with no nextPageToken (last page)
McpSchema.ListToolsResult mockToolsResult2 = new McpSchema.ListToolsResult(List.of(mockTool2), null);
// Simulate server response to second tools/list request with page token
McpSchema.JSONRPCRequest toolsListRequest2 = transport.getLastSentMessageAsRequest();
assertThat(toolsListRequest2.method()).isEqualTo(McpSchema.METHOD_TOOLS_LIST);
// Verify the page token was included in the request
PaginatedRequest params = (PaginatedRequest) toolsListRequest2.params();
assertThat(params).isNotNull();
assertThat(params.cursor()).isEqualTo(nextPageToken);
McpSchema.JSONRPCResponse toolsListResponse2 = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION,
toolsListRequest2.id(), mockToolsResult2, null);
transport.simulateIncomingMessage(toolsListResponse2);
// Verify the consumer received all expected tools from both pages
assertThat(receivedTools).hasSize(2);
assertThat(receivedTools.get(0).name()).isEqualTo("test-tool-1");
assertThat(receivedTools.get(0).description()).isEqualTo("Test Tool 1 Description");
assertThat(receivedTools.get(1).name()).isEqualTo("test-tool-2");
assertThat(receivedTools.get(1).description()).isEqualTo("Test Tool 2 Description");
asyncMcpClient.closeGracefully(); asyncMcpClient.closeGracefully();
} }