Tool result size: when to persist to a file instead of returning it
•6 min read
A result too large to return has three cheaper fixes before persistence: send the delta instead of the state, a count instead of a list, and a budgeted answer that says it was budgeted.
Persist a tool result to a file when the caller genuinely needs all of it and it genuinely will not fit in the reply. Before that, three cheaper answers usually apply: return what changed rather than the whole state, return a count where the question only asks whether the number is zero, and budget the expensive half of the answer while saying plainly that it was budgeted. A result that has to be persisted is a design decision; a result that could have been smaller is an accident.
2.4 MBOne read of every published post on a 212-post blog, which the client wrote to a file rather than passing to the model
That figure is what a full posts read costs on a blog of a couple of hundred articles: 39,686 lines of JSON, bodies and all. No context window takes it, so the MCP client intercepted the result, saved it to a path and handed back the path with instructions to grep it. This is the correct end state for that particular call — the tool's job is to return everything, and the caller wanted one field from each row — but it is the last resort, not the first move.
Return the delta, not the state
The largest reduction available to most tools is to answer with what changed. Market4's index sweep does this: the return value is the difference since the last run rather than the current state of every URL, because the current state is what the caller already had. "Four pages became indexed and one dropped out" is a thing to act on. "37 of 42 indexed" is a thing to look at, and looking at it costs the whole table every time.
A delta also changes what a second call means. When a tool returns the full state, calling it twice produces two near-identical payloads and no information. When it returns a delta, the second call is honestly empty, which is a much clearer signal to an agent deciding whether anything happened.
Return a count where only the count is ever asked
Some lists are stored as numbers on purpose. When Market4 records what Search Console said about a URL, it keeps how many referring URLs Google listed rather than the list itself, because the diagnosis built on top of it only ever asks whether that number is zero. Storing a stranger's link graph per URL would be a collection of its own, with its own retention question and its own size problem, in service of a boolean.
The test is whether any caller ever reads an individual item. If every consumer of a field asks "how many" or "is it empty", the field is a number that has been written down as a list. Shrinking it is not a compromise; it is removing data nobody reads.
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.
No card to start. Cancel from the settings screen, not from an email.
Budget the expensive half, and say that you did
Some answers are cheap to list and expensive to enrich. Market4's internal-link audit lists every orphan page it found, because an incomplete list of defects is worse than none, but it only computes suggested linking sources for the worst of them. The response says how many got suggestions, and a flag marks the rest, so an empty suggestion list means "we did not look" rather than "nothing can link here".
That distinction is the whole point of the technique. Truncating silently produces a result that is wrong in a way the caller cannot detect. Truncating with a stated budget produces a result that is smaller and still honest, and it gives the caller something to do next: fix the ones above the line and run it again.
Four ways to keep a tool result inside a context window, roughly in order of what to try first.
Technique
What comes back
Use it when
Return the delta
What changed since last time
The caller already holds the current state
Return a count
How many, not which ones
Every consumer only asks whether the number is zero
Budget the expensive part
The full list, plus a flag naming what was skipped
Listing is cheap and per-item work is not
Persist and hand back a handle
An id or a path the caller reads from
All of it is genuinely needed and none of it fits
When persistence is genuinely the right answer
Persistence is right when the result is an artefact rather than an answer: an export, a full archive read, a report somebody will open later. The signal is that the caller wants to keep it, not just read it. In that case the tool should return a handle plus enough metadata to decide whether to open it at all — how many rows, what window, what was included — so the agent can often finish without fetching the body.
Return a handle, not a promise. An id or a path the caller can act on immediately beats "it is being generated" with no way to check.
Put the shape in the envelope. Row counts, the window covered and what was excluded belong beside the handle, not inside the file.
Say what the file is. A caller that knows it holds one JSON object per post can grep it; a caller that has to open it to find out has paid the cost you were avoiding.
Keep the small answer available. A tool that can only return everything forces persistence on callers who wanted one number.
Do not persist to hide a design problem. A payload nobody can read is not fixed by moving it to disk.
The underlying rule is that a tool result is read by something with a fixed budget, and the tool is the only party that knows which parts of the payload carry the information. A caller can filter what it was sent, but it cannot un-spend the window that arrived. Deciding what not to send is work that belongs on the server side of the call.
When should an MCP tool write its result to a file instead of returning it?
When the caller genuinely needs the whole artefact and it will not fit in a reply — an export, a full archive read, a report to open later. Before that, try returning the delta since the last call, a count where only the count is read, or a budgeted answer that states what it skipped. Persistence is a last resort because a handle costs the caller a second round trip.
Why return a delta instead of the current state?
Because the current state is usually what the caller already has, so sending it again spends the context window on information the caller cannot use. A delta is smaller, it names the thing to act on, and it makes a repeated call meaningful: a second call that returns nothing is a clear statement that nothing changed, which a full-state response can never make.
Is truncating a large tool result acceptable?
Only when the response says it truncated and how much. A silently shortened list is a result that is wrong in a way the caller cannot detect, and an agent will treat the missing items as absent. A stated budget — how many items were fully processed, and a flag on the rest — keeps the response small and leaves the caller with a next step.
How large is too large for a tool result?
There is no single number, because the budget belongs to the caller's context window and is shared with everything else in the conversation. The practical test is whether the caller reads every field it receives. A read of every post on a two-hundred-post blog came to 2.4 MB of JSON in one measurement here, and the useful part of it was one field per row.
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.
A connection is a transport detail a client can reset by reconnecting. The unit worth counting is the tool, scoped to the account whose resources the call spends.