Back to Blog
September 28, 2026
Marc Frances Torres
5 min read

We built pagination for our MCP tool results. Then we deleted it.

Every superglue tool can be exposed over MCP. Most of them return a handful of rows. Some return 24,000 invoices, because somebody asked for “all of them” and meant it.

Our MCP server used to cap large results. Last month we replaced the cap with a proper pagination system. Three days later we deleted both. Here is why, and what happens when you send 12 MB of JSON to an AI client.

The safety net that wasn’t

The cap was a sensible default from a time when clients choked on big payloads. Results were sampled down to ten items per array, then cut at 20,000 characters if they still didn’t fit.

It was honest about it. A model that asked for 24,000 invoices got this:

{
  "success": true,
  "data": [
    { "id": "INV-00001", "amount": 1250, "status": "open" },
    /* … 9 more invoices … */
    "sampled from 24000 items"
  ]
}

The model knew it was seeing 10 of 24,000. It just had no way to get the rest: no result ID, no cursor, no next page. Think of an invoice report that lists ten rows, says “23,990 more”, and has no page two.

Ask for the total of all open invoices and a careful model says it only has a sample. A less careful one multiplies.

So we did the responsible thing

We did it by the book. The MCP specification has no pagination for tool results, but it recommends opaque handles for state that spans several calls. So every oversized result got one.

Oversized results became a valid JSON envelope with a handle. The full result went to storage, and a new read_tool_result tool paged through it, with an access check on every read.

{
  "success": true,
  "complete": false,
  "originalBytes": 12418093,
  "totalItems": 24000,
  "retrieval": {
    "tool": "read_tool_result",
    "runId": "4f1c…",
    "hint": "Call read_tool_result with this runId to page through the complete result."
  },
  "previewStrategy": "sampledItems",
  "preview": [ /* first 10 items */ ]
}

606 lines, tests included. It handled every edge case we could think of, down to a test with 20,000 squid emoji. We were a little proud of it.

Then someone asked the obvious question

Who is this for?

An MCP server doesn’t talk to a model. It talks to a harness: Claude, ChatGPT, an IDE, an agent framework. The harness knows the context window and often has a sandbox. The server knows neither. Our cap was a second guess by the party with the least information, a bit like a kitchen deciding you can’t finish your steak.

So before merging, we tested the simplest alternative: no limit at all.

The test

Most large-data tests have a flaw: models are excellent at sounding like they read something. A test that a good guess can pass measures the guess. So every question we asked had one property: a model that skimmed would get it wrong.

  • Needles. High-entropy tokens planted in the middle of the payload. No schema, pattern or prior predicts them, so you either retrieve one or you don’t. The middle defeats previews that keep the first few items, which is exactly what our old cap did.
  • Checksum traps. Questions whose answer depends on every record, such as an exact aggregate. Every record moves the answer, so any sample, however representative, returns a number that is plausible and wrong.

Each dataset went out as a single tool result, at two sizes chosen to break different things. At about 1 MB, a result is roughly a quarter of a million tokens: it fits in the largest context windows, barely. At 12.4 MB, it is several million tokens, more than any mainstream model can hold. At that size, the only way to pass is to not read the data into context at all.

How clients handled unbounded results

Client~1 MB12.4 MB
ClaudePassPass
ChatGPTPassPass
CursorPassPass
LangdockPassDegraded
Raw MCP SDK client, no harnessPassLoud failure

Pass means every planted needle found and every checksum correct. Tested August 24, 2026.

Claude, ChatGPT and Cursor found every needle and got every checksum right, including exact aggregates over all 24,000 records. Langdock was solid at 1 MB and struggled at 12.4 MB.

Now look at the last row. With no harness at all, the client didn’t guess. It failed loudly with a context-length error. An error says “ask for less”. A sample says “go ahead, estimate”.

Why the winner didn’t read it

Passing at 12.4 MB didn’t mean reading 12.4 MB. Claude’s harness, for example, saved the result as a file in its sandbox, and the model queried it in chunks with code: search for the needle, sum the column, check the checksum. Most of the data never entered the context window. The answers did.

That is what our pagination got wrong. read_tool_result let a model read the data one page at a time, through its context. With a 20 KB budget per page, 12.4 MB is at least 620 tool calls. Paging by the default ten items, 24,000 invoices is 2,400. No agent makes hundreds of calls to add up invoices. It reads page one, maybe page two, and then it estimates. We had rebuilt the sample with extra steps.

Pagination is an interface for humans with scroll wheels. An agent with a sandbox wants the file. Anthropic makes the same case in Code execution with MCP: large intermediate data belongs in the execution environment, not in the model’s context.

Where the limit belongs

Removing our cap didn’t remove limits. It moved each one to the layer that has the information:

  • The tool knows the data. Filter, project and aggregate at the source. If the question needs a total, return a total, not 24,000 rows.
  • The server knows correctness. Return the complete result, exactly as the tool produced it.
  • The harness knows the model. Budget the context, spool to disk, or fail loudly.

Our old cap did the harness’s job without the harness’s information. That is how you end up with ten invoices and a confident guess.

The deletion

The result handling now fits on one screen:

content: [
  {
    type: "text",
    text: safeStringify({
      success: true,
      data: result.data ?? null,
      ...(result.warnings?.length ? { warnings: result.warnings } : {}),
    }),
  },
],

The second commit on the branch added 23 lines and removed 654: the old cap, the envelope, the storage spill and read_tool_result. The server does less, and capable clients get better answers. The envelope still lives on its own branch, in case production proves us wrong.

What not to trust

  • A plausible answer over a large result. Plant a needle. If the model can’t find a random token, it didn’t read the data.
  • A label that says “partial”. The model reads the label and estimates anyway. Errors get fixed. Estimates get believed.
  • One client as a proxy for all. Claude, ChatGPT and Cursor handled 12.4 MB. Not every harness does.

What not to do

  • Don’t cap results on the server. It is the layer with the least information about the model.
  • Don’t paginate through the context window. Big results belong in a file the agent can query, not in the prompt.
  • Don’t return 24,000 rows when the question needs a total. Filter and aggregate at the source, and aim for less than 1 MB. No limit doesn’t mean no manners.

The best pagination system is the one your client already wrote. We just had to write ours first to find out.