Skip to content

Support pagination per MCP specification#320

Merged
koic merged 1 commit intomodelcontextprotocol:mainfrom
koic:feature_pagination
Apr 18, 2026
Merged

Support pagination per MCP specification#320
koic merged 1 commit intomodelcontextprotocol:mainfrom
koic:feature_pagination

Conversation

@koic
Copy link
Copy Markdown
Member

@koic koic commented Apr 18, 2026

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 Structs 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.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

@koic koic force-pushed the feature_pagination branch from 1369f84 to f9d7afe Compare April 18, 2026 09:22
## 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.
@koic koic force-pushed the feature_pagination branch from f9d7afe to b97b922 Compare April 18, 2026 09:25
@atesgoral
Copy link
Copy Markdown
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.

@koic
Copy link
Copy Markdown
Member Author

koic commented Apr 18, 2026

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.

@koic koic merged commit e73c444 into modelcontextprotocol:main Apr 18, 2026
11 checks passed
@koic koic deleted the feature_pagination branch April 18, 2026 18:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants