SOLFIND
Web Lens
Portal home

Pagination - MCP Python SDK

https://py.sdk.modelcontextprotocol.io/advanced/pagination/ • 62 KB fetched
Open original page


Pagination - MCP Python SDK Skip to content MCP Python SDK Pagination * en - English * de - Deutsch * es - español * fr - français * hi - हिन्दी * ja - 日本語 * ko - 한국어 * pt - português (Brasil) * ru - русский язык * tr - Türkçe * uk - українська мова * zh - 简体中文 * zh-hant - 繁體中文 Search modelcontextprotocol/python-sdk MCP Python SDK modelcontextprotocol/python-sdk * MCP Python SDK * What's new in v2 * Get started Get started * Installation * First steps * Connect to a real host * Testing * Servers Servers * Tools * Structured Output * Resources * URI templates * Prompts * Completions * Images, audio & icons * Handling errors * Inside your handler Inside your handler * The Context * Dependencies * Lifespan * Elicitation * Multi-round-trip requests * Sampling and roots * Progress * Logging * Subscriptions * Running your server Running your server * Add to an existing app * Deploy & scale * Authorization * OpenTelemetry * Serving legacy clients * Clients Clients * Callbacks * Transports * OAuth * Identity assertion * Multiple servers * Subscriptions * Caching * Protocol versions * Deprecated features * Advanced Advanced * The low-level Server * Pagination Pagination On this page * A server that pages * Try it * The client loop * The three rules * Recap * Middleware * Extensions * MCP Apps * Troubleshooting * Translations * Migration Guide * API Reference API Reference * mcp * mcp_types On this page * A server that pages * Try it * The client loop * The three rules * Recap * MCP Python SDK * Advanced Pagination 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 . A server that pages server.py from typing import Any from mcp.server import Server , ServerRequestContext from mcp.types import ListResourcesResult , PaginatedRequestParams , Resource BOOKS = [ f "book- { n } " for n in range ( 1 , 101 )] PAGE_SIZE = 10 async def list_books ( ctx : ServerRequestContext [ Any ], params : PaginatedRequestParams | None ) -> ListResourcesResult : start = 0 if params is None or params . cursor is None else int ( params . cursor ) end = start + PAGE_SIZE page = [ Resource ( uri = f "books://catalog/ { name } " , name = name ) for name in BOOKS [ start : end ]] next_cursor = str ( end ) if end < len ( BOOKS ) else None return ListResourcesResult ( resources = page , next_cursor = next_cursor ) server = Server ( "Bookshop" , on_list_resources = list_books ) app = server . streamable_http_app () * On a low-level Server , handlers are constructor arguments, not decorators. on_list_resources answers every resources/list request; that's the whole hookup. * Every paged handler is typed 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 . * You decide what a cursor is . Here it's an offset rendered as a string. A timestamp, a primary key, a base64 blob: anything you can mint on the way out and recognise on the way back in. * 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. Try it mcp run only accepts an MCPServer , so you serve this one yourself. The last line of server.py builds an ordinary ASGI app from the Server , and uvicorn runs that: uvicorn server:app --port 8000 Point any client ( The Client , or the Inspector) at http://localhost:8000/mcp and 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. The client loop 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 : client.py import anyio from mcp import Client from mcp.types import Resource async def list_all_resources ( client : Client ) -> list [ Resource ]: resources : list [ Resource ] = [] cursor : str | None = None while True : page = await client . list_resources ( cursor = cursor ) resources . extend ( page . resources ) if page . next_cursor is None : break cursor = page . next_cursor return resources async def main () -> None : async with Client ( "http://localhost:8000/mcp" ) as client : resources = await list_all_resources ( client ) print ( f " { len ( resources ) } resources" ) if __name__ == "__main__" : anyio . run ( main ) * cursor starts as None , so the first request carries no cursor. * Extend before you look at next_cursor : the last page has resources too. * next_cursor is None is the exit. Anything else goes straight back into cursor= , untouched. With uvicorn still serving server.py , run python client.py in a second terminal. 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 shows for every list_* verb, and it costs nothing against a server that doesn't page: next_cursor is None on the first response and the loop runs once. The three rules 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: MCPError(-32603, 'Internal server error', None) A cursor you didn't get from the server is a bug, not a feature request. Recap * 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. * You return a page plus next_cursor : any string you'll recognise later, or None when there is nothing left. * The client loop: pass cursor= , accumulate, repeat until next_cursor is None . * Cursors are opaque, the server owns the page size, and a non-paging client still gets page one. The rest of the hand-written Server API ( on_call_tool , input_schema dicts, _meta ) is The low-level Server . Back to top Previous The low-level Server Next Middleware Made with Zensical

Links found on this page

  1. Skip to content [direct]
  2. en - English [direct]
  3. de - Deutsch [direct]
  4. es - español [direct]
  5. fr - français [direct]
  6. hi - हिन्दी [direct]
  7. ja - 日本語 [direct]
  8. ko - 한국어 [direct]
  9. pt - português (Brasil) [direct]
  10. ru - русский язык [direct]
  11. tr - Türkçe [direct]
  12. uk - українська мова [direct]
  13. zh - 简体中文 [direct]
  14. zh-hant - 繁體中文 [direct]
  15. modelcontextprotocol/python-sdk [direct]
  16. What's new in v2 [direct]
  17. Get started [direct]
  18. Installation [direct]
  19. First steps [direct]
  20. Connect to a real host [direct]
  21. Testing [direct]
  22. Servers [direct]
  23. Tools [direct]
  24. Structured Output [direct]
  25. Resources [direct]
  26. URI templates [direct]
  27. Prompts [direct]
  28. Completions [direct]
  29. Images, audio & icons [direct]
  30. Handling errors [direct]
  31. Inside your handler [direct]
  32. The Context [direct]
  33. Dependencies [direct]
  34. Lifespan [direct]
  35. Elicitation [direct]
  36. Multi-round-trip requests [direct]
  37. Sampling and roots [direct]
  38. Progress [direct]
  39. Logging [direct]
  40. Subscriptions [direct]
  41. Running your server [direct]
  42. Add to an existing app [direct]
  43. Deploy & scale [direct]
  44. Authorization [direct]
  45. OpenTelemetry [direct]
  46. Serving legacy clients [direct]
  47. Clients [direct]
  48. Callbacks [direct]
  49. Transports [direct]
  50. OAuth [direct]
  51. Identity assertion [direct]
  52. Multiple servers [direct]
  53. Subscriptions [direct]
  54. Caching [direct]
  55. Protocol versions [direct]
  56. Deprecated features [direct]
  57. Advanced [direct]
  58. The low-level Server [direct]
  59. Middleware [direct]
  60. Extensions [direct]
  61. MCP Apps [direct]
  62. Troubleshooting [direct]
  63. Translations [direct]
  64. Migration Guide [direct]
  65. mcp [direct]
  66. mcp_types [direct]
  67. Zensical [direct]