MCP’s write_file Escaping Problem Is Really a Tool-Call Serialization Tax

MCP’s write_file Escaping Problem Is Really a Tool-Call Serialization Tax

Base64 is ugly. Retry loops caused by broken JSON tool calls are uglier.

A fresh issue in the Model Context Protocol servers repository proposes adding an optional content_base64 parameter to write_file, plus a matching newText_base64 path for edit operations. The request is narrow: let agents send file contents as base64 so quotes, backticks, template literals, code fences, and other syntax-heavy text do not explode inside a JSON tool-call envelope. The bigger lesson is much less narrow: LLM agents are mediocre clients for large, escape-heavy structured payloads, and MCP server authors should design tool APIs accordingly.

This is one of those infrastructure problems that looks too small until it costs real money. A model tries to write a Markdown file containing inline code, double quotes, ${template} expressions, and fenced snippets. It emits a giant JSON argument string. Somewhere in the nesting, an escape is missed. The host sees JSON Parse error: Unterminated string. The agent retries, maybe with slightly different content, maybe after consuming another round of planning tokens, and maybe eventually tells the user the file was written even though the durable artifact is partial, changed, or missing.

The serialization boundary is part of the agent UX

The issue, modelcontextprotocol/servers #4394, was opened June 20 at 13:06:23 UTC. It proposes keeping today’s plain content parameter while adding content_base64?: string, with server-side decoding through a normal Node path such as Buffer.from(args.content_base64, 'base64'). The schema would require either content or content_base64, not force every simple write through base64.

The argument is practical. The base64 alphabet — A-Z, a-z, 0-9, +, /, and = — is safe to carry inside JSON strings. It avoids the common escape failures caused by raw quotes, backticks, template expressions, and long multiline payloads. The reporter also points to prior art in server-commands-rtk, where a write_file tool already implements this pattern, and notes that MCP already uses base64 for media reads through readFileAsBase64Stream.

The reproduction details are specific enough to be useful: OpenCode 1.17.8, Node.js v24.15.0, MCP SDK server-filesystem ^1.25.2, MCP SDK reference implementation ^1.9.0, Linux x86_64, and server-filesystem 2026.1.14. The report had only one human comment and no reactions at research time, so this is not yet a movement. It does not need to be. The failure mode is obvious to anyone who has watched an agent attempt to serialize a big TypeScript, Markdown, SQL, shell, or JSON example into a JSON parameter.

The token math is the uncomfortable part

Base64 costs more on the happy path. The issue’s own math says a 10KB document costs about 5,738 tokens via base64 versus 3,770 tokens for a direct write with no retries — roughly a 52% premium. A linked analysis generalizes the overhead: base64 inflates bytes by 33.3%, and tokenizer behavior can make observed content-token overhead roughly 50–56% in a Markdown sample.

That sounds like a loss until the first retry. One failed direct attempt plus one retry costs about 7,590 tokens, making the base64 path a 24% saving. Two retries cost 11,410 tokens, a roughly 50% saving for base64. Four retries cost 19,050 tokens, where base64 is about 70% cheaper, or roughly 3.3× more efficient. In other words, a “wasteful” encoding becomes economical the moment the direct path becomes unreliable.

That is the right way to evaluate agent tool design. Token waste is not only model choice, prompt length, or whether someone picked the expensive reasoning model. It is also interface design. A fragile schema can turn one file write into five full-payload attempts. A robust schema can spend more once and still be cheaper, faster, and more trustworthy than a retry loop.

The deeper design principle is simple: minimize the amount of syntax the model must get exactly right. Deterministic software clients can escape a 10KB string correctly every time. Language models produce text probabilistically. When a tool parameter routinely carries large opaque payloads, forcing that payload through one raw JSON string is asking the model to be a perfect serializer instead of a useful planner. Safer shapes include base64 fields, staged artifacts, file handles, multipart uploads, patch formats with checksums, host-mediated editing, or references to content the host already stores.

For MCP server authors, the action item is not to make everything base64 forever. Keep plain content for small, readable text. Add content_base64 for arbitrary text, generated files, binary-ish material, and syntax-heavy payloads. Validate “exactly one of these fields” or “one required” semantics clearly. Return decoded byte counts, content hashes, and path metadata so agents can verify postconditions. For edit tools, consider old/new checks, patch application, or checksummed replacements instead of making the model stuff an entire replacement file into a brittle JSON string.

For agent hosts, log malformed tool-call parse errors separately from server-side tool failures. They are not the same bug. If the server rejected valid arguments, that is one problem. If the model failed to produce a parseable tool call, that is a serialization-boundary problem, and users should not have to infer it from a transcript artifact.

MCP is becoming the tool layer for local agents, database agents, coding agents, and internal control planes. That makes these small schema decisions matter. The protocol does not merely connect tools. It defines what kinds of mistakes agents are likely to make, how expensive recovery is, and whether users can trust the artifact at the end. Base64 will not win any beauty contest. But in agent infrastructure, pretty transcripts are less important than files that actually get written.

Sources: Model Context Protocol servers issue #4394, server-commands-rtk user-experience analysis, MCP TypeScript SDK, MCP servers repository