Cursor-Based Pagination Specification
This document defines the preferred cursor-based pagination semantics for MCP-AQL operations that return collections. Cursor pagination enables efficient iteration through large datasets while minimizing token usage in L
Source: spec/docs/features/pagination.md
On this page
Jump to a section
Use the outline to move through longer pages without losing your place.
Version: 1.0.0-draft Status: Draft Last Updated: 2026-04-15
Abstract
This document defines the preferred cursor-based pagination semantics for MCP-AQL operations that return collections. Cursor pagination enables efficient iteration through large datasets while minimizing token usage in LLM context windows.
1. Overview
1.1 Purpose
Operations returning complete result sets create several problems:
- Token bloat - Large collections consume LLM context windows
- Memory pressure - Servers and clients may struggle with large datasets
- Inefficient re-fetching - No way to resume interrupted queries
- Poor user experience - Long wait times for initial results
Cursor-based pagination addresses these by:
- Bounded responses - Control result set size per request
- Stable iteration - Cursors maintain position across requests
- Efficient forward/backward - Navigate in either direction
- Resumable queries - Continue from any cursor position
1.2 Design Principles
- Opaque cursors - Cursor format is implementation-defined
- Stable ordering - Results maintain consistent order across pages
- Bidirectional - Support both forward and backward navigation
- Optional totals - Total count is optional (expensive for some backends)
1.3 Alignment with Standards
This specification aligns with:
- GraphQL Relay Cursor Connections - Primary inspiration for the connection model
- OData pagination - Parameter naming conventions
- JSON:API pagination - Link-based navigation patterns
2. Pagination Parameters
2.1 Parameter Schema
Operations supporting pagination accept these parameters:
interface PaginationParams {
/**
* Number of items to return from the start
* Mutually exclusive with 'last'
*/
first?: number;
/**
* Cursor to start after (forward pagination)
* Used with 'first'
*/
after?: string;
/**
* Number of items to return from the end
* Mutually exclusive with 'first'
*/
last?: number;
/**
* Cursor to start before (backward pagination)
* Used with 'last'
*/
before?: string;
}2.2 Parameter Combinations
| Parameters | Behavior |
|---|---|
first |
First N items from start |
first + after |
First N items after cursor |
last |
Last N items from end |
last + before |
Last N items before cursor |
| None | Default page size from start |
2.3 Invalid Combinations
The following combinations MUST return a validation error:
first+last(ambiguous direction)afterwithoutfirst(missing page size)beforewithoutlast(missing page size)first+before(mismatched direction)last+after(mismatched direction)
Error response:
{
"success": false,
"error": {
"code": "VALIDATION_INVALID_TYPE",
"message": "Cannot use 'first' and 'last' together",
"details": {
"param_name": "pagination",
"expected_type": "valid pagination combination",
"actual_type": "conflicting parameters",
"provided": ["first", "last"],
"hint": "Use 'first' for forward pagination or 'last' for backward pagination"
}
}
}Note: This uses the existing
VALIDATION_INVALID_TYPEerror code with pagination-specific context in thedetailsobject, following the error-codes.md specification pattern.
2.4 Example Requests
First page:
{
operation: "list_elements",
element_type: "persona",
params: {
first: 10
}
}Next page:
{
operation: "list_elements",
element_type: "persona",
params: {
first: 10,
after: "cursor_xyz789"
}
}Previous page:
{
operation: "list_elements",
element_type: "persona",
params: {
last: 10,
before: "cursor_abc123"
}
}3. PageInfo Response
3.1 PageInfo Schema
All cursor-paginated responses MUST include a pageInfo object:
interface PageInfo {
/**
* Whether more items exist after the last item
* Required
*/
hasNextPage: boolean;
/**
* Whether more items exist before the first item
* Required
*/
hasPreviousPage: boolean;
/**
* Cursor of the first item in the result set
* Required when items are returned
*/
startCursor?: string;
/**
* Cursor of the last item in the result set
* Required when items are returned
*/
endCursor?: string;
/**
* Total count of items matching the query
* Optional - may be omitted if expensive to compute
*/
totalCount?: number;
}3.2 PageInfo Requirements
| Field | Required | Notes |
|---|---|---|
hasNextPage |
Yes | Always include, even if false |
hasPreviousPage |
Yes | Always include, even if false |
startCursor |
Conditional | Required when results are returned (items.length > 0 or edges.length > 0) |
endCursor |
Conditional | Required when results are returned (items.length > 0 or edges.length > 0) |
totalCount |
No | Include when available without performance impact |
3.3 Example PageInfo
First page of results:
{
"pageInfo": {
"hasNextPage": true,
"hasPreviousPage": false,
"startCursor": "cursor_001",
"endCursor": "cursor_010",
"totalCount": 150
}
}Last page of results:
{
"pageInfo": {
"hasNextPage": false,
"hasPreviousPage": true,
"startCursor": "cursor_141",
"endCursor": "cursor_150"
}
}Empty results:
{
"pageInfo": {
"hasNextPage": false,
"hasPreviousPage": false,
"totalCount": 0
}
}4. Connection-Style Response
4.1 Response Structure
Cursor-paginated operations SHOULD return a connection-style response:
interface Connection<T> {
/**
* The items in this page (mutually exclusive with edges)
*/
items?: T[];
/**
* Pagination metadata
*/
pageInfo: PageInfo;
/**
* Edges with per-item cursors (mutually exclusive with items)
* When present, items should be omitted
*/
edges?: Edge<T>[];
}
interface Edge<T> {
/**
* The item
*/
node: T;
/**
* Cursor for this specific item
*/
cursor: string;
}4.2 Simple Response Format
For most use cases, the simple format is sufficient:
{
"success": true,
"data": {
"items": [
{ "name": "alice", "status": "active" },
{ "name": "bob", "status": "active" },
{ "name": "charlie", "status": "inactive" }
],
"pageInfo": {
"hasNextPage": true,
"hasPreviousPage": false,
"startCursor": "cursor_001",
"endCursor": "cursor_003",
"totalCount": 25
}
}
}4.3 Edge Response Format
When per-item cursors are needed (e.g., for deletion during iteration):
{
"success": true,
"data": {
"edges": [
{
"node": { "name": "alice", "status": "active" },
"cursor": "cursor_001"
},
{
"node": { "name": "bob", "status": "active" },
"cursor": "cursor_002"
},
{
"node": { "name": "charlie", "status": "inactive" },
"cursor": "cursor_003"
}
],
"pageInfo": {
"hasNextPage": true,
"hasPreviousPage": false,
"startCursor": "cursor_001",
"endCursor": "cursor_003"
}
}
}4.4 Choosing Response Format
| Use Case | Recommended Format |
|---|---|
| Simple listing | items array |
| Modify during iteration | edges with cursors |
| LLM context (minimize tokens) | items array |
| Resume from specific item | edges with cursors |
4.5 Compatibility Response Metadata
Adapters using page- or offset-based compatibility parameters MAY return pagination metadata in a pagination object instead of pageInfo.
These compatibility metadata examples use snake_case field names to align with MCP-AQL's public naming convention for adapter-facing request and response fields.
Page-based compatibility response:
{
"success": true,
"data": {
"items": [
{ "name": "alice", "status": "active" },
{ "name": "bob", "status": "active" }
],
"pagination": {
"page": 2,
"page_size": 25,
"total_items": 142,
"total_pages": 6,
"has_more": true
}
}
}Offset-based compatibility response:
{
"success": true,
"data": {
"items": [
{ "name": "alice", "status": "active" },
{ "name": "bob", "status": "active" }
],
"pagination": {
"limit": 25,
"offset": 50,
"returned": 25,
"total_items": 142,
"has_more": true
}
}
}For MCP-AQL-native cursor pagination, pageInfo remains the preferred response shape. For general guidance across cursor, page, and offset styles, see Operations.
5. Applicable Operations
5.1 Operations Supporting Pagination
The following operation types SHOULD support pagination:
| Operation | Required | Notes |
|---|---|---|
list_elements |
Yes | Core element listing |
search_elements |
Yes | Search results |
query_elements |
Yes | Complex queries |
list_* |
Yes | Any list operation |
search_* |
Yes | Any search operation |
5.2 Default Behavior
When pagination parameters are omitted:
| Aspect | Default |
|---|---|
| Page size | Implementation-defined (RECOMMENDED: 20) |
| Direction | Forward (from start) |
| Cursor | None (first page) |
5.3 Maximum Page Size
Implementations SHOULD enforce a maximum page size:
- RECOMMENDED maximum: 100 items
- Hard limit: 1000 items
Requests exceeding the maximum SHOULD be clamped to the maximum (not rejected).
5.4 Introspection of Pagination Support
Operations indicate pagination support in introspection:
{
"name": "list_elements",
"supports_pagination": true,
"pagination": {
"default_page_size": 20,
"max_page_size": 100,
"supports_total_count": true
}
}6. Implementation Requirements
6.1 MUST Requirements
Implementations supporting pagination MUST:
- Accept
first,after,last,beforeparameters - Return
pageInfowithhasNextPageandhasPreviousPagefor cursor-paginated responses - Return
startCursorandendCursorwhen items are present - Reject invalid parameter combinations with error code
- Maintain stable ordering within a pagination session
6.2 SHOULD Requirements
Implementations supporting pagination SHOULD:
- Return
totalCountwhen available without performance impact - Enforce maximum page size limits
- Support both forward and backward pagination
- Use opaque, URL-safe cursor format
- Include pagination support in introspection
6.3 MAY Requirements
Implementations supporting pagination MAY:
- Include
edgeswith per-item cursors - Support cursor expiration/TTL
- Implement cursor resumption across sessions
- Support sorting parameters alongside pagination
- Include compatibility metadata such as
returnedfor offset-based responses when that helps clients understand the applied page window
6.4 Cursor Format
Cursors MUST be:
- Opaque - Clients treat as black box
- URL-safe - Base64url encoding recommended
- Stable - Same cursor returns same position
Cursors SHOULD be:
- Tamper-resistant - Include validation/signature
- Compact - Minimize size for LLM context
- Self-contained - No server-side session state required
Example cursor encoding:
// Encoding
const cursor = btoa(JSON.stringify({
id: "item_123",
sort_key: "2026-01-28T12:00:00Z"
}));
// Result: "eyJpZCI6Iml0ZW1fMTIzIiwic29ydF9rZXkiOiIyMDI2LTAxLTI4VDEyOjAwOjAwWiJ9"7. Future Extensions
7.1 Sorting with Pagination
Combine sorting and pagination:
{
operation: "list_elements",
element_type: "persona",
params: {
first: 10,
sort: {
field: "created_at",
order: "desc"
}
}
}7.2 Filtered Pagination
Pagination with filters:
{
operation: "list_elements",
element_type: "persona",
params: {
first: 10,
filter: {
status: "active",
tags: { contains: "admin" }
}
}
}7.3 Offset-Based Pagination
For backends requiring offset:
{
operation: "list_elements",
element_type: "persona",
params: {
offset: 20,
limit: 10
}
}7.4 Streaming Pagination
For real-time data:
{
operation: "list_elements",
element_type: "persona",
params: {
first: 10,
stream: true,
poll_interval_ms: 5000
}
}7.5 Pagination Presets
Named pagination configurations:
# In adapter schema
pagination_presets:
compact:
page_size: 5
include_total: false
detailed:
page_size: 20
include_total: true
include_edges: trueReferences
- MCP-AQL Specification
- GraphQL Relay Cursor Connections
- Field Selection Specification
- Operations Reference
- GitHub Issue: #37
- DollhouseMCP Implementation: #299