MCP's file transfer draft keeps bytes out of your model's context: 5 mechanics and 3 open security questions
Yesterday the MCP maintainers published the first draft of an extension for moving files between clients and tools. If you build MCP servers, this is the piece you have been hand-rolling: base64 blobs in tool arguments, bespoke presigned-URL conventions, CSVs pasted into prompts. The extension, identified as io.modelcontextprotocol/files, wants to standardize all of it. It works with protocol revision 2026-07-28 and later, and the maintainers expect it stable before the end of the year.
I read the full announcement and the draft's mechanics. Here is what actually changes for a server author, and where the design still has open holes.
The core design decision: bytes never enter JSON-RPC
Every scenario in the draft (save a PDF to cloud storage, pull a big CSV into a sandbox, upload a ZIP deck to a third-party service) shares one property. The model needs to know things about the file, but it never needs to read every byte.
So the extension moves file contents out of band. Tool arguments carry metadata only: an id, a name, a mime type, a size, optionally a SHA-256 digest. The actual bytes travel over plain HTTPS PUT or POST, from the client directly to an upload target the server specifies. On the way back, downloads ride an HTTPS GET against a URL in the result's _meta.
The consequence for your context window is the interesting part. The model makes the tool call and gets the result. Neither the file contents nor the upload URL ever enter its context. We measured 41k tokens of MCP overhead before the first prompt on a typical server set. Any pattern that keeps bulk data out of the model's view is worth adopting early.
Mechanic 1: marking a file argument with x-mcp-file
A server advertises a file argument by adding x-mcp-file to the argument's input schema, the same pattern as x-mcp-header:
"file": { "type": "object", "x-mcp-file": { "accept": ["application/pdf"], "maxSize": 157286400 }, "properties": { "id": { "type": "string", "minLength": 1, "maxLength": 128 }, "name": { "type": "string" }, "mimeType": { "type": "string" }, "size": { "type": "integer", "minimum": 0 }, "data": { "type": "string" } }, "required": ["id", "mimeType", "size"] } The client, not the model, fills this argument in. Small files can still go inline as base64 in data, capped by a maxInlineBytes value the server advertises in its server/discover response. Anything bigger goes through the out-of-band flow below.
There is a graceful degradation path: a client that does not support the extension ignores x-mcp-file and treats the argument as an ordinary object. If the client never declared extension support in its tools/list request, the server should omit the tool or offer an alternative that takes a URL.
Mechanic 2: metadata first, upload second
Before anything is sent, the host gets the user's approval, or agent policy decides when no human is present. Then the client calls the tool with metadata only:
{ "method": "tools/call", "params": { "name": "save_to_folder", "arguments": { "folderId": "fld_8c1a", "file": { "id": "file-1", "name": "Q3 report.pdf", "mimeType": "application/pdf", "size": 41204133 } } } } This ordering is deliberate. The server can check permissions and storage quota before any bytes move. You do not upload 40 MB only to learn the destination folder is read-only. If checks pass, the server returns the extension's new upload_required result type, containing one upload target per file: a URL, headers, an expiry, a credentials mode, and a requestState value.
Mechanic 3: requestState, built for the stateless core
After the client PUTs the file and gets a 2xx, it calls the same tool again with the same arguments plus requestState returned unchanged.
That value represents the unfinished job. The server packages the details it needs to continue, protects the value against tampering, and hands it back. The client does not need to understand it. This fits the stateless direction MCP took in the July 28 revision, where persistent sessions went away. Any server instance that can verify the value and reach the uploaded file can finish the job. For tools where a second run would create a duplicate, the server marks the requestState as used before running, and all instances share that record.
This is the Multi Round-Trip Requests pattern, which shipped recently, applied to file transfer. If you write servers against the 2026-07-28 revision, the shape will look familiar.
Mechanic 4: downloads ride in _meta
Downloads are simpler because they do not need a retry. The tool completes normally, and the download target travels in the result's _meta under the extension's id:
"result": { "resultType": "complete", "content": [{ "type": "text", "text": "Exported q3-orders.csv." }], "_meta": { "io.modelcontextprotocol/files": { "downloads": [{ "name": "q3-orders.csv", "mimeType": "text/csv", "size": 209715200, "url": "https://download.example.com/d/01JD3V9P4Q?sig=...", "credentials": "omit", "expiresAt": "2026-11-04T18:30:00Z" }] } } } The client only sees download targets if it declared download support in the tool call. After fetching, it checks byte count against size, and against digest if one was supplied. A mismatch means discard the file.
One line in the announcement deserves emphasis: a matching digest confirms the bytes match the hash, not that the file is safe. A malicious server can hand you a harmful file and its correct digest. Downloaded files stay untrusted input.
Mechanic 5: the credentials field
A transfer target may be a third-party storage service, not the MCP server itself. So every target carries a credentials mode. omit means the client attaches no MCP token, cookies, or environment credentials; the target authorizes itself, typically via a presigned URL. include means the client attaches its MCP credential, but only to a path under the client's own MCP address or the server's origin, which helps gateways and sandboxed clients.
The draft also requires clients to refuse redirects on transfers and reject targets that supply an Authorization header. Both close trivial exfiltration paths.
The three open questions
The maintainers are explicit that this is a draft that may change shape, with no reference implementation yet. Three things are genuinely undecided:
- Where download details belong.
_metais the current choice, and it is an open design question. If you have burned your fingers on result-shape drift, comment on the draft now rather than after it stabilizes. - The allowlist question. With
credentials: "omit", a malicious server can still direct an upload to any public endpoint it controls. The host approves the transfer before it knows the target, so approval never covers the specific destination. The draft recommends hosts restrict omit targets to an allowlist of permitted origins. Whether that restriction should be mandatory is an open question. - Whether your stack needs it at all. Servers that only ever take URLs to already-uploaded files do not need the extension, and the degradation path means those servers can keep their existing tool. The extension is optional by design.
What I would do this week
Nothing breaks today. The extension is experimental, optional, and unimplemented in the reference SDKs. But the direction matters: MCP is standardizing the last big chunk of bespoke server glue, and it is doing so in a way that keeps both the bytes and the URLs out of model context. If you run MCP servers for agents, read the draft, and if your tools shuffle files, your feedback window is now, before the shape hardens near year end. The maintainers take feedback in the extension repository's issues and the working group's Discord discussion.
For broader context on how the 2026-07-28 stateless revision changed server authoring, see MCP C# SDK 2.0 is stable: HTTP is stateless by default.