Paginating an MCP tool result an assistant has to read
The consumer of an MCP tool result is a model with a fixed budget for reading. Paging for that reader is a different design from paging for a web page.

An MCP tool that can return more rows than fit in one reply should return a page of them, say how many it returned and how many remain, and hand back the exact argument value needed to fetch the next page. That last part is what separates pagination designed for an assistant from pagination designed for a web page. A person reading a list can click a numbered link. A model reading a tool result has to construct the next call from what the result told it, so anything the result leaves implicit becomes a guess, and a guessed cursor is a wrong call the model cannot detect.
An unpaged tool result fails differently for an assistant than for a browser, because the cost is not time, it is space. A large response either exceeds what the host will accept, in which case the call produced nothing at all, or it is accepted and displaces the rest of the conversation, in which case the assistant has bought a list of every record at the price of forgetting why it wanted one. Both outcomes look like the tool worked. Neither leaves the assistant able to continue.
The second failure is the more damaging because it is silent. A tool that quietly returns the first fifty of four hundred records, with no field saying so, has told the assistant that four hundred records are fifty. Every conclusion drawn afterwards is confidently wrong, and nothing in the transcript marks the point where it went wrong. Truncation without a truncation flag is the single design decision most likely to produce a fluent, incorrect answer.

When an assistant works through dozens of your tools in a single turn, there is no transaction around them. Everything before the failure has already happened, and the turn can still end by reporting success.

Market4 turns one release note into a changelog page, a blog post, a mail-out and a week of social posts — and then tells you which of them brought anyone back.
Choose a cursor when the underlying set changes while it is being read, and an offset when it does not. An offset is simpler and easier for an assistant to reason about, and it is correct as long as nothing is inserted or deleted between calls. On a set that moves, an offset silently skips and repeats rows around the boundary, which produces a page that looks fine and is missing records. A cursor encodes the position rather than the count, so an insertion earlier in the set does not corrupt the next page.
| Property | Offset paging | Cursor paging | No paging |
|---|---|---|---|
| Next call is obvious to a model | Yes, add the page size | Yes, if the cursor is returned verbatim | Not applicable |
| Correct while the set is changing | No, rows shift across the boundary | Yes | Yes, one snapshot |
| Can jump to an arbitrary position | Yes | Usually not | Not applicable |
| Behaviour when the set is large | Works, at some cost per page | Works | The call fails or floods the context |
| Risk of silent incompleteness | Low, if a remaining count is returned | Low, if a has-more flag is returned | High, when the result is truncated silently |
The default page size of an MCP tool should be the number of rows that answers the ordinary question in one call. Most questions an assistant asks a listing tool are of the form "what is the most recent" or "is there one matching this", and both are answered by a small page. A default set to the maximum the transport will carry optimises for the rare exhaustive read and makes every ordinary call expensive. Set the default small, allow a larger page explicitly, and cap it at something the reply can actually hold.
Filtering is usually a better answer than paging, and the tool description is where that gets decided. If a listing tool can filter by status, by tag or by date, saying so in the description changes the assistant's first call from "give me everything" to "give me the drafts". A parameter the model does not know about is a parameter that does not exist, so the pagination story and the filtering story have to be told in the same place.
A listing tool that returns each item's full body cannot be made small by paging, because the size problem is per row rather than per page. The fix is to split the operation: a list that returns identifiers, titles, dates and status, and a separate read that returns one item in full. That shape lets an assistant scan cheaply and fetch expensively, and it is the difference between a tool that can be called at the start of a task and a tool that can only be called when nothing else will be needed afterwards.
The same reasoning applies to fields nobody reads. Internal timestamps, revision counters and denormalised copies of data available elsewhere all cost context on every row of every page. A listing result is a summary; anything in it that does not help the caller decide which item to fetch is paying rent it does not earn.
If fetching a page spends money, quota or an external rate-limit allowance, the result has to say so, because an assistant asked to "get all of them" will otherwise loop until something breaks. A page that costs nothing and a page that costs an API call are the same shape and completely different decisions. Declare the cost in the tool description, return what the last call consumed, and where the total is unbounded make the tool refuse a request for everything rather than obediently spending its way through it.
With an explicit field that is present on every response, not with the length of the page. A page shorter than the requested size usually means the end, but not always, and a model that infers termination from length will stop early on the day it is wrong. Return a boolean saying whether more exist, and alongside it the exact value to pass to get them. Where nothing remains, return the boolean as false rather than omitting the field.
Return it when it is cheap to compute and say it is unknown when it is not. A total lets an assistant decide whether to page at all, which often saves several calls. What it must never do is guess: an estimated total presented as a number will be quoted back as a fact. If counting the set costs as much as reading it, the honest response is a field explicitly marked unknown.
Small enough that an ordinary call is cheap, and large enough to answer the common question in one shot. For most listing tools that is a page in the tens rather than the hundreds. The maximum should be capped by what a reply can carry rather than by what the database can produce, and the description should state both numbers so the assistant can plan instead of discovering the limit by hitting it.
Yes, when the underlying set is stable between calls and the result reports how many rows remain. Offsets are easier for a model to reason about than opaque cursors, and jumping to a position is sometimes genuinely useful. The case where offsets fail is a set that is being written to while it is read, because rows shift across the page boundary and the caller silently misses some. If that can happen, use a cursor.
Return the first page and say what remains, rather than attempting the whole set. If the caller genuinely needs everything and the set is bounded, an explicit parameter can allow it. If the set is unbounded, or each page spends quota, the tool should refuse and say why, because a refusal that names the limit is more useful than a partial answer that does not admit to being partial.
Both can hand a model a block of JSON, so the choice looks cosmetic. It is not. A tool call is a decision the model makes with arguments it invents; a resource read is a fetch the client performs against an address it already holds.

A tool error that says "invalid input" ends the run. One that quotes the rejected value, lists the accepted set and names the next move lets the assistant repair the call itself. Four real messages from one codebase, read closely.