docs/advanced/pagination.md
Most servers never need this.
MCPServer answers every list_* request with everything it has, in one page, next_cursor=None. For a few dozen tools, resources or prompts that is the right answer and there is nothing to configure.
Pagination is for the server whose resource list is really a database: thousands of rows it refuses to serialize in one response. The protocol's answer is a cursor: the server returns a page plus an opaque token, and the client sends that token back to get the next page.
@mcp.resource() has no hook for any of that. To page, you write the list handler yourself, on the low-level Server.
--8<-- "docs_src/pagination/tutorial001.py"
Server, handlers are constructor arguments, not decorators. on_list_resources answers every resources/list request; that's the whole hookup.params: PaginatedRequestParams | None, and the example accepts both. Over a connection, though, the SDK never hands you None (a request with no params member reaches the handler as the model with its defaults), so the signal that matters is params.cursor is None: start from the top.next_cursor=None is how you say "that was the last page". There is no count, no total, no has_more. None is the entire signal.!!! tip
A PAGE_SIZE of 10 makes the example readable. Pick yours per endpoint: a list of
one-line resources can afford a page of 500; a list of fat prompt templates cannot.
The client has no say in it, and that is by design.
Client(server) connects to a low-level Server in memory exactly as it connects to an MCPServer.
Call list_resources() with no arguments. You get ten resources, book-1 through book-10, and next_cursor is the string "10".
Hand it back with list_resources(cursor="10") and the first resource is book-11, the new next_cursor is "20".
The tenth page comes back with next_cursor set to None. Done.
Every list_* method on Client (list_tools, list_resources, list_resource_templates, list_prompts) takes a cursor= keyword. Draining a paged list is one while True:
--8<-- "docs_src/pagination/tutorial002.py"
cursor starts as None, so the first request carries no cursor.next_cursor: the last page has resources too.next_cursor is None is the exit. Anything else goes straight back into cursor=, untouched.Run its main() and it prints 100 resources: ten pages of ten, stitched together by a loop that never knew there were ten pages.
This is the same loop The Client chapter showed you, and it costs nothing against a server that doesn't page: next_cursor is None on the first response and the loop runs once.
Cursors are opaque. A client must never parse, build, or guess one. The only legal source of a cursor is the previous page's next_cursor, verbatim.
The server picks the page size. There is no limit= in the protocol. If you need a different page size, you change the server.
A client that ignores paging still works. It calls list_resources() once, gets the first ten, and never notices the next_cursor it threw away. Nothing breaks; it sees less.
!!! check
Opaque means opaque. Invent a cursor (list_resources(cursor="page-2")) and there is
nothing the protocol can do for you. This server tries int("page-2"), the handler raises,
and what comes back to the client is:
```text
MCPError(-32603, 'Internal server error', None)
```
A cursor you didn't get from the server is a bug, not a feature request.
MCPServer returns everything in one page. Pagination is opt-in, and you opt in on the low-level Server.on_list_resources (and on_list_tools, on_list_prompts, on_list_resource_templates) receives PaginatedRequestParams | None; params.cursor is None for the first page.next_cursor: any string you'll recognise later, or None when there is nothing left.cursor=, accumulate, repeat until next_cursor is None.The rest of the hand-written Server API (on_call_tool, input_schema dicts, _meta) is The low-level Server.