DEV Community

Mads Hansen
Mads Hansen

Posted on

Your MCP pagination cursor is a security boundary

An AI agent rarely consumes a large PostgreSQL result in one call.

It reads a page, reasons, calls another tool, and comes back.

Meanwhile, rows change.

LIMIT/OFFSET describes a position in a changing list. Inserts can duplicate rows across pages. Deletes can skip them.

For MCP tools, use a deterministic keyset such as:

ORDER BY created_at DESC, id DESC
Enter fullscreen mode Exit fullscreen mode

Then return an opaque cursor bound to:

  • the last ordering values
  • tenant and environment
  • normalized filter hash
  • sort and query version
  • page-size ceiling
  • snapshot rule
  • expiry

The server should reject a cursor when any of that scope changes.

Also document consistency. Does the next page reflect live committed data, a captured high-water mark, or a materialized short-lived result?

Keyset pagination prevents position drift. It does not magically create a snapshot across separate tool calls.

Every page should carry has_more, returned count, filters, freshness or snapshot time, truncation state, and a trace ID. Otherwise the model may present page one as the complete answer.

Finally, retry the same cursor after a timeout. It should produce the same logical page under the contract—not silently advance or restart.

Full implementation guide: MCP server for Postgres: make pagination a stable data contract

Top comments (0)