MCP Pagination and Cursor Safety: Let Agents Read Large Collections Without Loops or Omissions
Pagination is a state machine, not a while loop. Bind opaque cursors to query identity, cap pages and records, detect repetition, and preserve evidence across MCP tool calls.
An agent asks an MCP server for invoices, receives 100 items and a cursor, then keeps calling until the cursor disappears. That innocent loop can skip records when data changes, duplicate actions when pages overlap, or consume thousands of calls when a server repeats the same cursor.
Pagination is an execution protocol. The host must track query identity, page state, limits, and termination independently of the model. A language model can decide that more evidence is useful; it should not control an unbounded cursor loop.
Make the cursor opaque and query-bound
Clients must treat a cursor as an opaque token. They should not parse offsets from it, increment it, or transfer it to another query. The server should bind the token to normalized filters, sort order, tenant, authorization scope, page size, and snapshot information.
{
"items": [{ "id": "inv_123", "version": 7 }],
"nextCursor": "opaque-signed-token",
"hasMore": true,
"snapshot": "2026-09-08T11:00:00Z"
}
If a bound property changes, reject the cursor with a structured non-retryable error. Quiet acceptance under new filters can create omissions nobody notices.
Protect cursor contents with authenticated encryption or a server-side lookup. A base64-encoded offset is not a security boundary. Include expiry and key rotation while preserving enough overlap for legitimate long-running jobs to finish or restart from a checkpoint.
Prefer stable keyset pagination
Offset pagination is simple but unstable under concurrent inserts and deletions. If a new record appears before offset 100, the second page may repeat an item from the first. If an earlier record disappears, another item may be skipped.
Keyset pagination advances from the last stable sort key:
select * from invoices
where (created_at, id) < (:last_created_at, :last_id)
order by created_at desc, id desc
limit :page_size;
Always add a unique tie-breaker such as the record ID. Sorting only by timestamp allows records sharing the same time to move unpredictably between pages.
Some workflows need a consistent snapshot. The server can bind a database snapshot, high-water mark, or updated_at <= started_at filter into the cursor. Declare the semantics because “all invoices” could mean all items at start time or a moving stream including later inserts.
Put budgets in the host
Define maximum pages, records, bytes, elapsed time, and tool cost before the first call. The lower applicable limit wins: user scope, application policy, tool metadata, or provider constraint.
const budget = {
maxPages: 20,
maxItems: 2_000,
maxBytes: 8_000_000,
deadlineMs: Date.now() + 30_000,
};
The loop belongs in deterministic orchestration code. After each page it updates counters and checks termination. If the budget ends first, return a partial-result envelope explaining why collection stopped and preserve the next cursor as protected state.
Do not paste thousands of raw records into model context. Reduce, filter, or aggregate in trusted code while retaining references for selective follow-up. Context capacity is not a pagination budget.
Detect repetition and no progress
Servers fail in surprising ways. A response may return hasMore: true with no cursor. It may repeat a previous cursor, return the same items under a new token, or cycle through several tokens.
Track a digest of every cursor and page identity. Stop when a cursor repeats, an empty page claims more data, or several pages add no unique record IDs.
if (seenCursors.has(hash(nextCursor))) throw new PaginationCycle();
if (hasMore && !nextCursor) throw new InvalidPageContract();
if (newIds === 0 && hasMore) noProgressPages++;
Keep the raw response digest and correlation ID for diagnosis. Never ask the model to improvise a new cursor after an invalid response.
Duplicate records across pages are not always a server bug when data moves. Deduplicate by stable ID and version, then apply a declared conflict rule. The newest version may suit reporting, while an export may require the snapshot version.
Bind pages to authorization
A cursor is a capability-like object and can leak collection position or query state. Bind it to the authenticated subject, tenant, resource, and scope. A cursor issued to one user must not be replayable by another.
Re-evaluate authorization on every page. Permission can be revoked during a long run, and a cursor must not bypass current policy. Frozen authorization needs an explicit, short-lived export policy rather than accidental token behavior.
Never include bearer tokens or raw credentials inside a model-visible cursor. Store protected cursor state in the host and expose a short reference if the model needs to request continuation.
Separate reading from acting
An agent that performs an action for every listed record creates dangerous coupling. If page three retries, records may receive duplicate messages or updates. First collect a bounded immutable worklist, then execute effects with per-record idempotency keys.
For collections too large to materialize, keep a durable checkpoint containing query digest, cursor reference, processed record IDs or high-water mark, and action receipts. A restart resumes from verified state rather than conversation history.
If records change between listing and action, perform a version check at the effect boundary. A record that no longer matches the original filter may require re-planning instead of automatic execution.
Design resumability and useful errors
Return errors such as CURSOR_EXPIRED, CURSOR_QUERY_MISMATCH, PAGE_LIMIT_EXCEEDED, and SNAPSHOT_UNAVAILABLE. Include retry safety and a recovery instruction: restart the listing, narrow the filter, or request a new snapshot.
When a cursor expires, do not silently start at page one inside the same acting workflow. That can duplicate effects. Restart collection into a new worklist and reconcile it with completed receipts.
Expose progress without exposing cursor contents: pages read, unique records admitted, bytes consumed, snapshot time, and stop reason. This gives the model and user enough information to decide whether a narrower follow-up is worthwhile.
Test mutation during traversal
Populate records with identical sort timestamps, then insert and delete items between pages. Change permissions mid-run. Rotate signing keys. Expire cursors. Return overlaps, repeated cursors, empty pages with hasMore, and oversized items.
Verify three properties: every admitted record satisfies the original query and authorization; no logical record is acted on twice; and incomplete traversal is reported as incomplete.
Pagination succeeds when the agent can request more evidence without owning the loop. Opaque query-bound cursors, stable ordering, strict budgets, cycle detection, and durable checkpoints turn a potentially unbounded conversation into a finite, auditable protocol.
Sources
> Want more like this?
Get the best AI insights delivered weekly.
By subscribing, you agree to our Privacy Policy. You can unsubscribe at any time.
> Related Articles
Causal Event Ordering for AI Agents: Stop Late Tool Results From Rewriting the Past
Concurrent agent tools return out of order. Use durable sequence numbers, causal parents, version checks, and deterministic reducers so stale results cannot corrupt current state.
LLM Output Provenance Attestations: Prove Which Model, Prompt, and Sources Produced an Answer
A trustworthy AI pipeline needs more than a generated-text label. Build signed provenance records that bind inputs, model identity, retrieval evidence, policy, and output hashes.
Context Window Truncation Safeguards: Stop AI Agents From Forgetting Critical Instructions
Long-running agents eventually exceed a model's context window. Use explicit budgets, pinned invariants, summaries, and regression tests to prevent silent instruction loss.
Tags
> Stay in the loop
Weekly AI tools & insights.