Module: ActionMCP::Server::Pagination

Included in:
TransportHandler
Defined in:
lib/action_mcp/server/pagination.rb

Overview

Shared cursor-based pagination for all list endpoints.

Two strategies:

1. Offset-based — for in-memory arrays (tools, prompts, resource templates).
2. Keyset-based — for ActiveRecord relations (tasks). Stable under concurrent writes.

When pagination_page_size is nil (default), returns all items unless the caller passes force: true or the client sends a cursor.

Constant Summary collapse

DEFAULT_PAGE_SIZE =
10

Instance Method Summary collapse

Instance Method Details

#paginate(collection, cursor: nil, page_size: nil, force: false) ⇒ Array(Array, String|nil)

Offset-based pagination for in-memory collections (tools, prompts, resources). For ActiveRecord relations, use paginate_by_keyset instead.

Parameters:

  • collection (Array)

    Items to paginate

  • cursor (String, nil) (defaults to: nil)

    Opaque cursor from the client

  • page_size (Integer, nil) (defaults to: nil)

    Override page size (nil = use config)

  • force (Boolean) (defaults to: false)

    Force pagination even when globally disabled

Returns:

  • (Array(Array, String|nil))
    page_items, next_cursor_or_nil


24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# File 'lib/action_mcp/server/pagination.rb', line 24

def paginate(collection, cursor: nil, page_size: nil, force: false)
  effective_page_size = page_size || pagination_page_size
  items = Array(collection)

  return [ items, nil ] unless force || effective_page_size || cursor

  page_size = effective_page_size || DEFAULT_PAGE_SIZE
  offset = decode_offset_cursor(cursor)
  page = items.drop(offset).take(page_size + 1)

  has_more = page.size > page_size
  page = page.first(page_size) if has_more
  next_cursor = has_more ? encode_offset_cursor(offset + page_size) : nil

  [ page, next_cursor ]
end

#paginate_by_keyset(relation, cursor: nil, page_size: DEFAULT_PAGE_SIZE, column: :id) ⇒ Array(Array, String|nil)

Keyset-based pagination for ActiveRecord relations. Uses a single column as cursor (must be unique + ordered). The relation MUST already be ordered by that column.

Parameters:

  • relation (ActiveRecord::Relation)

    Ordered AR relation

  • cursor (String, nil) (defaults to: nil)

    Opaque keyset cursor (Base64-encoded column value)

  • page_size (Integer) (defaults to: DEFAULT_PAGE_SIZE)

    Page size

  • column (Symbol) (defaults to: :id)

    Column to use as cursor key (default: :id)

Returns:

  • (Array(Array, String|nil))
    page_items, next_cursor_or_nil


50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/action_mcp/server/pagination.rb', line 50

def paginate_by_keyset(relation, cursor: nil, page_size: DEFAULT_PAGE_SIZE, column: :id)
  if cursor
    value = decode_keyset_cursor(cursor)
    relation = relation.where(column => ...value)
  end

  page = relation.limit(page_size + 1).to_a
  has_more = page.size > page_size
  items = has_more ? page.first(page_size) : page
  next_cursor = has_more ? encode_keyset_cursor(items.last, column) : nil

  [ items, next_cursor ]
end