Support pagination per MCP specification#320
Merged
koic merged 1 commit intomodelcontextprotocol:mainfrom Apr 18, 2026
Merged
Conversation
1369f84 to
f9d7afe
Compare
## Motivation and Context The MCP specification defines cursor-based pagination for list operations that may return large result sets: https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/pagination Pagination allows servers to yield results in smaller chunks rather than all at once, which is especially important when connecting to external services over the internet. The Ruby SDK previously returned complete arrays for all list endpoints (`tools/list`, `resources/list`, `prompts/list`, `resources/templates/list`) without pagination support. This adds cursor-based pagination. ### Server-side A new `MCP::Server::Pagination` module is introduced (mixed into `MCP::Server`) that provides the `paginate` helper for slicing a collection by cursor and a `cursor_from` helper that extracts the cursor from the request params while rejecting non-Hash inputs with `-32602 Invalid params` per the specification. `MCP::Server.new` accepts a new `page_size:` keyword argument. When `nil` (the default), all items are returned in a single response, preserving the existing behavior. When set to a positive integer, list responses include a `nextCursor` field if more pages are available. `Server#page_size=` is a validating setter that rejects anything other than `nil` or a positive `Integer`, raising `ArgumentError` on invalid inputs. The constructor routes through the setter, so both `Server.new(page_size: 0)` and `server.page_size = -1` raise. Cursors are string tokens carrying a zero-based offset, treated as opaque by clients. Cursor inputs are validated to be strings per the MCP spec; invalid cursors (non-string, non-numeric, negative, or out-of-range) raise `RequestHandlerError` with error code `-32602 (Invalid params)` per the spec. ### Client-side: two complementary APIs The SDK exposes two API shapes for listing, each serving a different use case: 1. **Single-page with cursor** (`client.list_tools(cursor:)`, `list_resources`, `list_resource_templates`, `list_prompts`): each call issues one JSON-RPC request and returns a result object (`MCP::Client::ListToolsResult` etc.) carrying the page items and an optional `next_cursor`. These methods match the single-page-with-cursor convention used by the Python SDK (`session.list_tools(params=PaginatedRequestParams(cursor=...))`) and the TypeScript SDK (`client.listTools({ cursor })`). The method name and the `cursor` parameter name are identical across the three SDKs, so pagination code translates directly between languages. 2. **Whole-collection** (`client.tools`, `client.resources`, `client.resource_templates`, `client.prompts`): auto-iterates through all pages and returns a plain array of items, guaranteeing the full collection regardless of the server's `page_size` setting. The auto-pagination loop tracks previously seen cursors in a set and exits if the server revisits any of them, guarding against both immediate repeats and multi-cursor cycles (e.g. `A -> B -> A`). This mirrors Rust SDK's `list_all_*` convenience methods, though the Ruby names are preserved from the pre-pagination 0.13.0 API for backward compatibility. Future rename to `list_all_*` is left as a TODO in the source. Result classes are `Struct`s named to mirror Python SDK's `ListToolsResult` / `ListPromptsResult` / etc. to align naming with other SDKs. Each struct exposes its page items (e.g. `result.tools`), an optional `next_cursor` for continuation, and an optional `meta` field mirroring the MCP `Result` type's `_meta` response field so servers that decorate list responses do not lose metadata through the client. ### Ruby-specific ergonomics `Server.new(page_size:)` and the `Pagination` module are Ruby-specific. The Python and TypeScript SDKs do not expose a built-in `page_size` helper; developers there re-implement cursor slicing inside each handler. In Ruby, setting `page_size:` on the server is enough for the SDK to handle the slicing across the four built-in list endpoints. ### README A Pagination section documents both the server-side `page_size:` option and the client-side iteration patterns, including an explicit note that each `list_*` call is a single JSON-RPC round trip whose response size depends on the server's `page_size`, and that `client.tools` / `.resources` / `.resource_templates` / `.prompts` return the complete collection when needed. ## How Has This Been Tested? - Added tests for the `Pagination` module (`test/mcp/server/pagination_test.rb`), server-side pagination (`test/mcp/server_test.rb`), and client-side pagination (`test/mcp/client_test.rb`). - Added and existing tests pass. `rake rubocop` is clean and `rake conformance` passes. ## Breaking Changes None. This is a new feature addition. Existing code continues to work unchanged: `page_size` defaults to `nil` on the server (returns all items, no `nextCursor`), and the client's existing `tools` / `resources` / `resource_templates` / `prompts` methods continue to return complete arrays by transparently iterating through pages.
f9d7afe to
b97b922
Compare
Contributor
|
And in the future we could add a per-operation page size override. Some operations may be harder on the server than others, so developers may want to e.g. only add pagination to resources while other operations can always return the full list. |
atesgoral
approved these changes
Apr 18, 2026
Member
Author
|
Thanks for the pointer. The Python and TypeScript SDKs don't seem to expose per-operation page size overrides either, so this could be revisited as future work once actual usage patterns surface. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation and Context
The MCP specification defines cursor-based pagination for list operations that may return large result sets:
https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/pagination
Pagination allows servers to yield results in smaller chunks rather than all at once, which is especially important when connecting to external services over the internet.
The Ruby SDK previously returned complete arrays for all list endpoints (
tools/list,resources/list,prompts/list,resources/templates/list) without pagination support. This adds cursor-based pagination.Server-side
A new
MCP::Server::Paginationmodule is introduced (mixed intoMCP::Server) that provides thepaginatehelper for slicing a collection by cursor and acursor_fromhelper that extracts the cursor from the request params while rejecting non-Hash inputs with-32602 Invalid paramsper the specification.MCP::Server.newaccepts a newpage_size:keyword argument. Whennil(the default), all items are returned in a single response, preserving the existing behavior. When set to a positive integer, list responses include anextCursorfield if more pages are available.Server#page_size=is a validating setter that rejects anything other thannilor a positiveInteger, raisingArgumentErroron invalid inputs. The constructor routes through the setter, so bothServer.new(page_size: 0)andserver.page_size = -1raise.Cursors are string tokens carrying a zero-based offset, treated as opaque by clients. Cursor inputs are validated to be strings per the MCP spec; invalid cursors (non-string, non-numeric, negative, or out-of-range) raise
RequestHandlerErrorwith error code-32602 (Invalid params)per the spec.Client-side: two complementary APIs
The SDK exposes two API shapes for listing, each serving a different use case:
Single-page with cursor (
client.list_tools(cursor:),list_resources,list_resource_templates,list_prompts): each call issues one JSON-RPC request and returns a result object (MCP::Client::ListToolsResultetc.) carrying the page items and an optionalnext_cursor. These methods match the single-page-with-cursor convention used by the Python SDK (session.list_tools(params=PaginatedRequestParams(cursor=...))) and the TypeScript SDK (client.listTools({ cursor })). The method name and thecursorparameter name are identical across the three SDKs, so pagination code translates directly between languages.Whole-collection (
client.tools,client.resources,client.resource_templates,client.prompts): auto-iterates through all pages and returns a plain array of items, guaranteeing the full collection regardless of the server'spage_sizesetting. The auto-pagination loop tracks previously seen cursors in a set and exits if the server revisits any of them, guarding against both immediate repeats and multi-cursor cycles (e.g.A -> B -> A). This mirrors Rust SDK'slist_all_*convenience methods, though the Ruby names are preserved from the pre-pagination 0.13.0 API for backward compatibility. Future rename tolist_all_*is left as a TODO in the source.Result classes are
Structs named to mirror Python SDK'sListToolsResult/ListPromptsResult/ etc. to align naming with other SDKs. Each struct exposes its page items (e.g.result.tools), an optionalnext_cursorfor continuation, and an optionalmetafield mirroring the MCPResulttype's_metaresponse field so servers that decorate list responses do not lose metadata through the client.Ruby-specific ergonomics
Server.new(page_size:)and thePaginationmodule are Ruby-specific. The Python and TypeScript SDKs do not expose a built-inpage_sizehelper; developers there re-implement cursor slicing inside each handler. In Ruby, settingpage_size:on the server is enough for the SDK to handle the slicing across the four built-in list endpoints.README
A Pagination section documents both the server-side
page_size:option and the client-side iteration patterns, including an explicit note that eachlist_*call is a single JSON-RPC round trip whose response size depends on the server'spage_size, and thatclient.tools/.resources/.resource_templates/.promptsreturn the complete collection when needed.How Has This Been Tested?
Paginationmodule (test/mcp/server/pagination_test.rb), server-side pagination (test/mcp/server_test.rb), and client-side pagination (test/mcp/client_test.rb).rake rubocopis clean andrake conformancepasses.Breaking Changes
None. This is a new feature addition. Existing code continues to work unchanged:
page_sizedefaults tonilon the server (returns all items, nonextCursor), and the client's existingtools/resources/resource_templates/promptsmethods continue to return complete arrays by transparently iterating through pages.Types of changes
Checklist