Large Results & Pagination in MCP
MCP paginates the catalog, not the call. A tool that can return a lot of data has to carry its own pagination contract, written for the smallest host.
The protocol's pagination is well specified: opaque cursors, a page size the server chooses, -32602 for a bad cursor. It also covers exactly four methods, and tools/call is not one of them. A tool result has no cursor, no size limit and no chunking in either 2025-11-25 or 2026-07-28. The only spec primitive for "the data is somewhere else" is resource_link plus resources/read, which has no range reads and which many hosts give the model no way to follow. So what happens to a large result is decided by whichever host the user runs, and the hosts disagree. A server author who wants predictable behaviour has to define a result contract in the tool's own arguments and size it for the least forgiving client.
Step 1What the spec paginates, and the cursor rules worth copying.
The pagination utility opens with "The Model Context Protocol (MCP) supports paginating list operations that may return large result sets", and the key words are list operations. It names four: tools/list, resources/list, resources/templates/list and prompts/list. The list is the same in 2025-11-25 and 2026-07-28. Nothing was added or removed when the 2026-07-28 revision rewrote most of the rest of the protocol.
Within those four the rules are short, and they are a good template for your own tools:
- The cursor is opaque. It is "an opaque string token, representing a position in the result set". Clients "MUST treat cursors as opaque tokens": they must not parse or modify one, or infer anything from it except whether one was provided. 2026-07-28 added that "an empty string is a valid cursor" and must not be read as end-of-results.
- The server chooses the page size. "Page size is determined by the server, and clients MUST NOT assume a fixed page size." The spec has no page-size parameter and no maximum.
- The end is signalled by absence.
nextCursoris present "if more results exist", and clients SHOULD treat a missing one as the end. - Bad cursors fail loudly. Servers SHOULD provide stable cursors and handle invalid ones gracefully. An invalid cursor SHOULD produce error
-32602(Invalid params).
Caching makes the pages less consistent than they look. Under 2026-07-28 each page is "an independently cacheable response" with its own ttlMs, and "there is no cross-page consistency guarantee": a client walking a long tools/list "may observe duplicates or gaps". When a cursor goes invalid the client SHOULD discard every cached page and start again. ttlMs and cacheScope exist only on server/discover, the four list methods and resources/read, so caching helps a large catalog but not a large tool result.
Tool-call pagination was asked for early and never picked up. Issue #229, "Pagination for tool/call", has been open since 2025-03-26. Issue #798 proposed a paginationHint annotation and a pagination: {hasMore, nextCursor} block in tool results, and was closed the day it was filed.
Step 2What tools/call gives you instead: content blocks, hints and a discriminator, none of which bounds size.
A 2026-07-28 tool result has content, structuredContent, isError and the new required resultType. The content block types are text, image, audio, resource_link and embedded resource. No field states how big a result may be or what to do when it is too big. The four pieces that look relevant each fail differently:
structuredContentcan double your bill. Since SEP-2106 it can be any JSON value. The spec also says "a tool that returns structured content SHOULD also return the serialized JSON in a TextContent block" for backwards compatibility. If a host passes both to the model, you pay for the payload twice. (The OpenAI Agents SDK, withuse_structured_content=True, uses only the structured copy.) Send a short text summary next to the structured data, not the same JSON twice.- Annotations are hints, and most hosts ignore them. Content blocks can carry
audience("user","assistant") and apriorityfrom 0.0 to 1.0. No MUST tells a host to hideaudience: ["user"]content from the model. The roadmap admits that "most implementers haven't adopted these annotations" and floats deprecating them, and SEP-2200, which tried to clarify content visibility, is closed. Do not count onpriority: 0keeping a block out of the context window. resultTypeis not chunking. Its values are"complete","input_required"for the Multi Round-Trip Request pattern, and"task"from the Tasks extension.input_requiredexists for elicitation and sampling round trips, not for "here is part one".- Tasks are asynchronous, not chunked. A task handle lets a slow call outlive one HTTP response. The final result still arrives whole through
tasks/get, and the extension's abstract sets no size or pagination rule for it. A slow large result becomes a late large result.
Step 3Size for the strictest host, not the one you test on.
The spec says nothing about oversized results, so every host decides for itself, and the hosts do not even agree on the unit: some limit in tokens, others in characters, and the conversion between them depends on the content.
Run a 60,000-token result through today's hosts and, depending on which one the user runs, it is spilled to a file, cut at a character threshold, trimmed to a per-tool token budget, kept whole, or truncated behind a hint the agent cannot follow. One server, one call, six different results. Each host's ceiling and what it does past it are kept current in Step 2 of when MCP results overload the context. For a contract, two numbers set the size: Claude Code's 25,000-token default cap and Gemini CLI's 40,000-character truncation. Only the server can make the results consistent, by never returning more than the smallest host can take.
Claude Code's own failure message tells the model to "use pagination, filtering, or limit parameters", which only helps if your tool has them.
Step 4A tool-level pagination contract: a cursor in the arguments, a budget in tokens, and the truncation flag in the result.
Since the protocol leaves this to you, define it once and use it in every tool that can return many records. The general result envelope (status, summary, projected data, omitted counts, next cursor) is covered in shaping tool results. These are the MCP-specific parts:
- Copy the spec's cursor rules into your arguments. Use an opaque
cursorstring the model passes back unchanged, a server-chosen default page size, and anisErrorresult that names the fix when a cursor is stale. GitHub's server usesafter+perPageon its cursor-based tools, and one of its bugs was a schema missing the cursor its description promised, so check the schema against the description. - Size the page in tokens, not just records. Datadog found that "a Datadog log message might be only 100 characters or it might be 1 MB", so a page of N records has no size bound. Their server "cuts off its response after a certain number of tokens and returns a cursor for more." Keep a record limit as well, but let the token or character budget end the page.
- Fetch one extra record to detect more. MotherDuck's server fetches
max_rows+1(default 1,024) to find out whether the result was cut, setstruncated: truewith a warning, and then drops about 10% of rows at a time until the JSON is undermax_chars(default 50,000). One extra row is cheaper than a count query. - Put the flag in the result, next to the fix. Grafana's Loki tool returns
resultsTruncatedandmaxLinesAllowedin the response itself. A truncation the model cannot see looks like a complete answer. - Keep ordering stable. The spec offers no cross-page consistency for its own lists, and your tool does not get it for free either. Sort on a unique key and encode the position in the cursor, so a record inserted mid-walk does not shift every later page by one.
- Return fewer fields before you add pages. GitHub cut its per-item project output by about 93% this way, and the case study of GitHub's server traces that history. No page size fixes a record that is 17KB on its own. Keep the identifiers, though: Anthropic's Slack example cut a response from 206 tokens to 72 with a concise format, and the concise form dropped the IDs that later calls need.
// tools/call arguments: the contract lives here, not in the protocol {"name": "search_logs", "arguments": {"query": "service:checkout status:error", "fields": ["ts", "host", "message"], "max_records": 200, // whichever budget is hit first ends the page "max_tokens": 8000, "cursor": "c2VhcmNoOjE3Mjk0MDAwMDA6NDQx"}} // result: the flag and the remedy are in-band {"resultType": "complete", "content": [{"type": "text", "text": "141 of 3,912 matching lines (stopped at the 8,000-token budget). Pass next_cursor for more, or add host:… to narrow."}], "structuredContent": { "records": [/* 141 projected records */], "truncated": true, "total_estimate": 3912, "stopped_by": "token_budget", "next_cursor": "c2VhcmNoOjE3Mjk0MDAwMDA6NTgy"}}
The spec says a tool returning structured content SHOULD also put the serialized JSON in a text block, and that choice decides who sees what: some clients forward only the text, so a summary-only text block hides the records from them, while a client that forwards both makes the model read the data twice. The trade-off, and the code that measures what you actually send, are on the code-level page. The default max_tokens is your choice, and the smallest host sets its ceiling: stay well under Gemini CLI's 40,000 characters and Claude Code's 10,000-token warning. Measure before you return, as Block's MCP playbook recommends (bytes, characters, estimated tokens).
Step 5Handles and spill files: resource_link done right, and output a host can grep.
When the data is too big to page through the conversation, the spec offers only a reference. "A tool MAY return links to Resources … the tool will return a URI that can be subscribed to or fetched by the client." A resource_link carries uri, name, description and mimeType. Done properly, that means:
- a stable URI that still resolves when the model gets round to it, since links returned by tools "are not guaranteed to appear in the results of a
resources/listrequest"; - the Resource's optional
sizein bytes and an accuratemimeType, so the client can decide whether to fetch at all; - and a description that says what is in it: "3,912 log lines, 14:00–15:00 UTC, newest first".
Then plan for the link going nowhere. resources/read has no range or pagination (range reads exist only as a File Uploads working-group item), so fetching a 2MB resource returns 2MB. Many hosts give the model no way to read a URI. Open PR #2527 would add a SHOULD that clients expose resource reads to the model, and until something like it lands, a resource_link is a dead end in many clients. Use it as an addition to a paged tool, not a replacement.
The more common case is that the host spills your result to a file whether you asked or not. Claude Code and Cursor both do. That only helps if the agent can read part of the file:
- Make it line-structured. A spill file packed onto a few enormous lines cannot be read in part by a reader that pages by line offset, and reading it whole runs into the host's cap. JSON Lines, Markdown or one record per line keeps the spill file usable with
grep,headand offsets. - Don't make round-trip edits depend on the model repeating the body. If the read was spilled, the model never saw the body and cannot rebuild it for the write. Edit tools should take a patch, a section id or a server-side handle, not the whole document back.
The public reports behind both rules, from Notion's and Atlassian's servers, are collected as host-side test cases in Step 3 of when MCP results overload the context.
_meta["anthropic/maxResultSizeChars"] is a workaround, not a fix. Declared on a tool's tools/list entry, it raises Claude Code's threshold for that tool's text results alone. Use it for a tool whose large output really is the point, such as a file reader whose user asked for the file. It has no effect on any other host, so a tool that depends on it still overflows the stricter ones.
Step 6What is and is not coming. Don't wait for it.
The fix at the protocol level is on the roadmap but not in the spec. The roadmap, last updated 2026-08-22, lists three relevant items under a Core Primitives working group that is still forming. None has shipped and none has a date:
- a redesign of the
tools/callinterface "to resolve fidelity disparities among return types and streamline the handling of structured and unstructured output"; - progressive discovery, where clients learn tools and resources "as they need them instead of ingesting the full catalog up front", which addresses the catalog side, not the result side;
- ETags for caching, "in particular tool calls", so a client could revalidate a result instead of fetching it again.
The streaming proposals were closed. SEP-2664 (streaming tool results through progress notifications with chunks) closed on 2026-07-02. SEP-2998 (partial tool results) closed on 2026-09-22 with the maintainer note that "every new SEP should be developed with the help of a Working Group", so it was closed on process grounds and the idea could come back through a working group. Issue #117 on streaming tool results has been open since December 2024. No accepted or in-flight SEP defines a result-size limit or a chunked tools/call result.
Build for the protocol as it is. Every tool that can return an unbounded number of records needs an opaque cursor, a token budget that ends the page, a truncated flag next to a remedy sentence, and stable ordering. Return fewer fields before adding pages, and make anything a host might spill to disk line-structured. If the tools/call redesign ships, a tool built this way needs a new envelope; one that relied on the host to cope needs redesigning. How GitHub's server learned this the hard way is in the page was counted in records. Which tools to build at all is covered in designing MCP tools. Controlling what overflows on the host side, for servers you don't own, is in when MCP results overload the context.