Pagination
Pagination allows servers to return large lists of items (tools, resources, or prompts) in smaller, manageable chunks called pages. This is critical for performance and stability when dealing with hundreds or thousands of items.
The mcp-cpp-sdk implements cursor-based pagination as defined in the Model Context Protocol.
Server-side Configuration
Servers can control the maximum number of items returned in a single list response by calling set_page_size().
// Return at most 100 entries from each tools/list, resources/list,
// resources/templates/list, or prompts/list request.
server.set_page_size(100);
When set_page_size is set to a non-zero value, any list request (e.g., tools/list, resources/list) that exceeds this limit will automatically be paginated. The server will return a nextCursor in the result, which the client can use to fetch the next page.
The following example demonstrates setting a page size of 3:
// Set page size to 3 to demonstrate pagination
server.set_page_size(3);
std::cout << "[Server] Page size set to 3\n";
Cursor-based Pagination
Unlike offset-based pagination, cursor-based pagination uses an opaque string (the “cursor”) to mark the position in the list. This pattern is more robust against changes to the underlying data during traversal.
First Request: The client sends a list request without a cursor.
Paginated Response: If the server has more items, it returns a page of results along with a
nextCursor.Subsequent Request: The client sends the same list request, including the
nextCursorreceived from the previous response.Termination: The process continues until the server returns a response without a
nextCursor.
Client-side Handling
Clients should check for the presence of nextCursor in list results and loop until all items are retrieved.
The following example shows how a client can paginate through a list of tools:
while (true) {
std::cout << "\n[Client] Fetching page " << page_num;
if (cursor) {
std::cout << " (cursor: " << cursor.value() << ")";
}
std::cout << "\n";
// Call list_tools with cursor if available
auto list_result = co_await client.list_tools(cursor);
std::cout << "[Client] Received " << list_result.tools.size() << " tools\n";
// Display tools on this page
for (const auto& tool : list_result.tools) {
total_tools++;
std::cout << " [" << total_tools << "] " << tool.name << ": ";
if (tool.description) {
std::cout << tool.description.value();
} else {
std::cout << "(no description)";
}
std::cout << "\n";
}
// Check if there are more pages
if (list_result.nextCursor) {
std::cout << "[Client] More pages available (nextCursor: "
<< list_result.nextCursor.value() << ")\n";
cursor = list_result.nextCursor;
page_num++;
// Small delay between pages
asio::steady_timer page_timer(io_ctx.get_executor());
page_timer.expires_after(std::chrono::milliseconds(50));
co_await page_timer.async_wait(asio::use_awaitable);
} else {
std::cout << "[Client] No more pages (nextCursor is null)\n";
break;
}
Key components of the client-side loop:
Cursor Storage: Keep track of the current cursor (initially empty).
Request with Cursor: Pass the
cursorto thelist_tools(),list_resources(), orlist_prompts()method.Result Processing: Process the items returned in the current page.
Cursor Update: If
list_result.nextCursoris present, update the cursor and continue the loop. If not, the traversal is complete.
Automatic Pagination
The SDK handles the complexity of slicing the data and generating cursors automatically. When a list request arrives:
The SDK checks if a page size is set.
It identifies the subset of registered items corresponding to the provided cursor (or the first page if no cursor is provided).
It returns the items and, if more items remain, calculates the cursor for the next page.
Implementation Details
Internal pagination logic validates the cursor, calculates the current slice, and returns an opaque next cursor only when another page remains.
The process of pagination is fully transparent to the server developer once set_page_size() is called. The SDK internal dispatch logic detects when a list request arrives, calculates the appropriate slice of items based on the provided cursor (or lack thereof), and constructs the response with the nextCursor if more items remain. This ensures that even as the number of tools or resources grows, the memory footprint and network payload per request remain stable.
Client-side Example
The following example demonstrates a complete client-side loop that retrieves all tools from a server, regardless of the page size:
std::cout << "\n[Client] Starting pagination through tools\n";
int page_num = 1;
std::optional<std::string> cursor;
int total_tools = 0;
while (true) {
std::cout << "\n[Client] Fetching page " << page_num;
if (cursor) {
std::cout << " (cursor: " << cursor.value() << ")";
}
std::cout << "\n";
// Call list_tools with cursor if available
auto list_result = co_await client.list_tools(cursor);
std::cout << "[Client] Received " << list_result.tools.size() << " tools\n";
// Display tools on this page
for (const auto& tool : list_result.tools) {
total_tools++;
std::cout << " [" << total_tools << "] " << tool.name << ": ";
if (tool.description) {
std::cout << tool.description.value();
} else {
std::cout << "(no description)";
}
std::cout << "\n";
}
// Check if there are more pages
if (list_result.nextCursor) {
std::cout << "[Client] More pages available (nextCursor: "
<< list_result.nextCursor.value() << ")\n";
cursor = list_result.nextCursor;
page_num++;
// Small delay between pages
asio::steady_timer page_timer(io_ctx.get_executor());
page_timer.expires_after(std::chrono::milliseconds(50));
co_await page_timer.async_wait(asio::use_awaitable);
} else {
std::cout << "[Client] No more pages (nextCursor is null)\n";
break;
}
}
std::cout << "\n[Client] Pagination complete! Total tools fetched: " << total_tools
<< "\n";
Pagination Best Practices
When implementing or consuming paginated APIs, consider the following best practices:
Consistent Page Size: While the server determines the page size, clients should be prepared to handle any number of items up to the requested limit.
Opaque Cursors: Clients must treat cursors as opaque strings. Never attempt to parse or construct cursors manually, as their format is internal to the server implementation.
Graceful Termination: Always check for the absence of
nextCursoras the definitive signal that the list is exhausted.Resource Efficiency: Use pagination to avoid blocking the server’s event loop with massive JSON serialization tasks for very large lists.