Repository navigation
Conversation
Working draft of the io.modelcontextprotocol/files extension in SEP form, a README describing the repository's status and how to participate, and CODEOWNERS for the File Uploads WG.
Return the upload target as an extension result type (upload_required) instead of a new input request type, use a placeholder value so required file arguments pass schema validation, bind the tool name, arguments and file URIs into requestState, scope credential forwarding to the server's canonical URI, and trim the first revision to a single PUT. Restate the SEP-2356 rules the draft depends on and align the background with the October 2 working group notes.
|
Nice to see this moving @localden! We run an MCP host (NimbleBrain) and ship Wanted to share a few thoughts from running that:
Also, what error do you get for a used or unknown file URI? We return the same one for "doesn't exist" and "can't see it" so servers can't probe. Happy to be a design partner here. We can wire up the client side in our host and the server side in our Python SDK. 🤙 |
|
Thanks @localden. I built a small prototype of the 2 October flow (metadata first, target back, PUT, finalize on the MRTR retry) on the Python SDK. This draft resolves most of what it surfaced: the extension result type, gating on the declared capability, and excluding the file pointers from the request digest. Four things it hit that the draft doesn't settle yet:
If it's useful, I can port the prototype into this repo as a Python reference implementation alongside the planned TypeScript one, with tests for each of these cases. AI disclosure: I used Claude Code to build the prototype and draft this review; I reviewed the spec and each point. |
meastes
left a comment
There was a problem hiding this comment.
Engineer from Dropbox here.
This looks useful, particularly having the server validate the destination and file metadata before transferring bytes. A few integration considerations:
-
PUT versus POST. Some storage APIs accept raw file bytes through POST, while this proposal requires PUT. Could
UploadTargetspecify the HTTP method, supporting at least PUT and POST? This would accommodate existing storage APIs without requiring a proxy or changes to their endpoints. This is a raw-body POST requirement, separate from multipart support. -
Large files, chunking, and resumability. Some storage APIs impose per-request upload limits and require upload sessions for larger files. Deferring chunked transfers makes direct integration with those backends harder. Separately, without resumability, an interrupted large upload can require retransmitting the entire file, increasing latency and bandwidth usage. Could we clarify the expected approach for backends with per-request limits and the path toward resumable transfers?
-
Make inline transfer optional. Inline uploads avoid additional round trips, but require servers to implement and test another way of receiving file bytes. For some servers, that complexity may outweigh the latency benefit.
Could servers explicitly advertise which transfer mechanisms they support—including out-of-band-only—with clients required to respect that choice? The existing
maxInlineBytesandx-mcp-file.maxSizefields cover much of the size negotiation, but disabling inline transfer should have explicit semantics, including whethermaxInlineBytes: 0does that.For servers supporting both, an example showing a 1 MiB aggregate inline limit and a 150 MiB per-file limit would help clarify how the limits interact and how the client selects the transfer mechanism.
|
So excited to see this!! @localden Some potential concerns:
|
I see a couple of concerns with using an MCP method for the file transfer. How would that method carry the bytes? Adding a method alone doesn’t change MCP’s JSON-RPC encoding:
The metadata-first approach also seems valuable. It lets the server validate the destination folder, permissions, quota, and other constraints before transferring bytes. An upload-first approach would need to preserve that context somehow; otherwise, we could transfer a large file only to reject the subsequent tool call. |
|
Looks great - few comments:
|
|
Thanks @localden! One thing I want to make sure the draft accounts for is that MCP clients today aren’t only calling tools directly. A number of gateways and servers put one executor tool in front of the rest, so the model searches for the tool it wants and then calls the executor with that tool’s name and arguments. For example, Arcade.dev’s Tool Recommendation does this with SelectTools and UseTool, Cloudflare’s Code Mode MCP server exposes search() and execute(), Docker’s Dynamic MCP gateway has mcp-find and mcp-exec. In all of those the client never sees the inner tool’s schema, so it never sees |
| 4. Clients **MUST** honor `credentials: "include"` only when `url` has the same origin as the MCP | ||
| server's | ||
| [canonical URI](https://modelcontextprotocol.io/specification/draft/basic/authorization/index#canonical-server-uri) | ||
| and a path equal to the canonical path or starting with it followed by `/`. In a gateway |
There was a problem hiding this comment.
I don't know if the path prefix requirement is reasonable. A pretty common implementation is going to be "everything prefixed by /mcp is routed to the SDK, which speaks JRPC". For us at least, it's most natural for the OOB endpoint to reside on a different path (but same origin) from the mcp server.
There was a problem hiding this comment.
+1 to this, I think same origin is the right level of restriction here, because, to Kevin's point, it will often be the case that the server endpoints for receiving the bytes are not on the /mcp path.
There was a problem hiding this comment.
Confirming from a host we have run since March across three agents, file params declared with an x-file keyword and resolved to bytes before dispatch.
@EricGustin's case has a non-gateway instance: a file referenced inside a free-text argument,  in a markdown parameter rendered into a document. No declared file argument exists, so the placeholder, _meta and inline-vs-out-of-band paths never engage. We fall back to resolving registered filenames inside text values. However ideally the model fills an attachment list and places the attachments also in the free text argument.
@mgoldsborough's host handle is what we also came up with, as he mentioned: the model names a file, the host swaps in the bytes before dispatch, and the result path strips URLs back out.
On open question 5, how the tool's typed result reaches the model: a symbolic name plus size, nothing else. The name has to carry the relative subfolder for multi-file output, and the model must not echo the size back or the next lookup breaks.
The initial draft separates file metadata from a placeholder URI and replaces that argument on retry. This proposal keeps the metadata in the marked file argument: required `id`, `mimeType`, `size`, and SHA-256 `digest`, with optional base64 `data` for inline bytes. When `data` is absent, the server sees the intended tool call before choosing an upload target. The client resumes with unchanged arguments and reports `completed` or `failed` per file under `inputResponses.uploads`. The echoed `requestState` and `fileId` bind each report to the target. Retryable transfers stop after three total attempts per file, or sooner for permanent or unsafe failures. The original empty extension declaration remains; this proposal adds no capability profile flags. Standard request `_meta` is omitted from the examples for brevity, while file metadata is in the argument. This changes the specification and its README overview. The proposed upload input request requires a core MRTR extension point; it is not currently interoperable with unmodified MCP. Validation: all five embedded JSON examples parse; the upload sequence agrees on arguments, state, file ID and digest; `git diff --check` passes. No runtime or upstream conformance test was run.
The draft covered passing omit targets through and relaying include targets. Let a gateway relay either kind from its own origin, and require it to check the original target before forwarding the bytes.
Return a file as a resource_link whose _meta carries a download target with the same url, headers and credentials as an upload target. Mark the section as not yet reviewed by the working group, explain the choice in Rationale, and extend the redaction rule to cover both directions.
|
I maintain fmsg-mcp, an MCP server for fmsg, a messaging protocol where Reference implementation: markmnl/fmsg-mcp#25 implements the
Why this matters to us: an agent couldn't send a 76 KB PDF through our server, because base64 meant ~100K Still missing: a way for hosts to upload files the agent created itself (not just user-picked ones), and |
There was a problem hiding this comment.
Just noting that this is another advantage to eventually support chunking since the client (browser) will only have to calculate the digest for the passed chunk.
| Clients **MUST NOT** use this extension with a server that did not advertise it. Servers report an | ||
| oversized inline `data` value as a tool error, following | ||
| [SEP-1303](https://modelcontextprotocol.io/seps/1303-input-validation-errors-as-tool-execution-errors), |
There was a problem hiding this comment.
I'm not sure that a tool error is the correct mechanism. This would send the error to the model, but the model has no control over whether the client sends files as inline bytes or out-of-band. I think this should instead be protocol error handled directly by the client. Per-file maxSize violations can stay tool errors
|
Wanted to +1 @meastes's point on allowing servers to declare that they don't support inline uploads.
Something like:
Very excited for this, will unlock a lot of great functionality for users of Harvey's MCP! |
|
As someone working on fynk's MCP server, super-excited to see this! File uploads have been such a pain point for us until now. Just wanted to +1 @meastes point on including the HTTP method in the upload target metadata. It would be very helpful for a server to be able to specify POST as opposed to PUT |
|
We run a Streamable HTTP server whose backend takes files and data in one request using the The out-of-band flow brings that problem back since the bytes land before the retry, and if the retry never comes, cleanup depends on storage expiry. That's the gap @markmnl raised in point 3. Would the group consider an optional mode for Streamable HTTP where the tools/call JSON-RPC message goes as the first part of a multipart POST, and each x-mcp-file argument references a following part (for example cid: or a part name, like GraphQL's map)? That keeps raw bytes out of JSON with no base64 or size inflation and needs no second endpoint or credential. The tool also gets the file and its arguments atomically. @meastes is right that this touches the transport contract, which is why I'd propose it as opt-in, with out-of-band staying the default. Metadata-first rejection still works if the server checks the JSON part before reading the file parts. If that's out of scope, I'd ask the spec to at least define a staged upload's lifetime and ownership, and to require that a server never makes a staged file visible until the retry commits it. |
|
Want to +1 @meastes comment about allowing servers to declare that they don't support inline uploads. Ideally we would like to avoid inlining all files if possible. As someone who works on Descript's MCP server this could be incredibly helpful for us. Upload reliability has been a bit of a thorn in our side for a while now and this seems like it has the components to get us where we need to go. |
Co-authored-by: den <den@anthropic.com>
Co-authored-by: den <den@anthropic.com>
Co-authored-by: den <den@anthropic.com>
Co-authored-by: den <den@anthropic.com>
Co-authored-by: den <den@anthropic.com>
…ten credentials and errors Co-authored-by: den <den@anthropic.com>
…transfer rules Co-authored-by: den <den@anthropic.com>
… and digest Co-authored-by: den <den@anthropic.com>
Adds the working draft of the
io.modelcontextprotocol/filesextension and CODEOWNERS forfile-uploads-wgandcore-maintainers, and updates the README. Motivation and rationale live here. They move into the document when it becomes a SEP.How it works
x-mcp-fileininputSchema.data, within the server'smaxInlineBytes.data.url, or apathunder the MCP address.PUT,POST, or aPOSTmultipart form, then retries with the same arguments. If a transfer fails, the client does not retry._meta.The flow keeps no protocol session state by default. Each retry carries its context in the request and in
requestState, so any instance that can reach the storage backend can process it. Only a tool that is unsafe to run twice needs shared storage, to make eachrequestStatework once.Motivation
MCP has no interoperable way to get a file from a user to a tool. Today's options put the bytes in a JSON-RPC message: base64 arguments going in,
EmbeddedResourceandBlobResourceContentscoming back. When the model emits arguments as tokens, the bytes pass through its context window, and large uploads to remote servers fail.The working group collected these cases:
It also recorded these host-side constraints:
SEP-2356 defined a file argument and an inline encoding, and closed without merging. SEP-2631 added out-of-band transfer, and also closed without merging. Its bytes moved before the server saw the rest of the call. Uploads that permissions, quota, or data residency rules would reject still used bandwidth and could land in the wrong region. The group agreed on metadata first, then the target, then bytes, then finalize, built on multi round-trip requests (MRTR) and existing primitives.
Why existing building blocks fall short
tools/callmoves bytes before the server can check the destination, or any quota that depends on it.inputSchemahas both. Elicitation'srequestedSchemaallows only flat primitive fields, so a structured file argument does not fit it today, and that is Future Work. Applying this flow to resources is listed under Open Questions.Target shapes
Targets cover single-request upload URLs that services hand out today, within each service's per-request limit. A client can use each of these without the server's credentials, apart from the headers the target supplies. Some, such as Linear, do not accept browser uploads.
Raw
PUT:Content-Rangein the target's headers)Raw
POST:POSTmultipart form:Rationale
Upload input request
SEP-2631 issued the upload URL before the server saw the arguments, so the server could not check the destination, or any quota that depends on it. Returning the target from
tools/callfixes the order, andrequestStatecarries the binding. MRTR allows only elicitation, sampling, and roots requests today, so this needs a core change before clients and servers can interoperate.One upload round
A server issues one upload input request per logical call. No backend we checked needs a second one: S3, GCS, Azure, R2, and Oracle are strongly consistent on the normal path, and re-sending bytes fixes neither replication lag nor post-upload processing. Tools can still use other MRTR rounds, including elicitation after the upload.
Stateless by default
Staged bytes are application data that
requestStaterefers to, so any instance that can reach the storage can serve the retry. A tool that is safe to run twice needs nothing more. For a tool that is unsafe to run twice, eachrequestStatefrom the upload onward works once, in storage every instance shares. Clients and gateways do not repeat a retry automatically, so a lost response leaves the outcome unknown. A safe tool may then need a new call and a new upload. Tools that outlast a timeout should use Tasks.No per-file status
A retry states that every transfer received a
2xx. A failed transfer means no retry. Any failure already ended the whole call, the server confirms the bytes anyway, and a stateless server could not enforce a reported failure. The cost is that a server waits forrequestStateexpiry to clean up. A failure signal is listed under Open Questions.Schema keyword
x-mcp-filesits on the argument it describes, asx-mcp-headerdoes. A separate list inTool._metawould need a pointer into the schema, and pointers handle$refandanyOfpoorly. A strict validator has to allow this keyword as it doesx-mcp-header.Structured file argument
SDKs and gateways can validate
argumentsbefore a handler runs, so the initial call cannot omit a required file argument. A structured value satisfies the schema without a placeholder URI or a parallel_metamap.sizealso marks empty files, so a typed SDK that drops an empty string cannot turn an empty file into an upload request.No content digest
This draft removes the digest for now, and we want to hear whether that is right. A required digest makes clients read each file twice, browsers cannot hash a large file natively, and most backends need a full server read because they do not expose SHA-256. No comparable upload API requires one. The cost is real: a server cannot detect same-size bytes written through a leaked target, or tampered downloads. The draft leans on short lifetimes and one-write targets, and asks the question under Open Questions. This changes an earlier revision that required a digest.
POSTandformImplementers asked for raw
POST, and some upload URLs document onlyPOST. Several document onlymultipart/form-data. Withform, those services work without a proxy. Every client that declaresuploadsupportsform, so a server can issue one without a fallback.fieldsis an array because policy-based form uploads depend on order: Cloud Storage POST requires the file to be the last field.Errors
The specification reserves
-32020to-32099, so an extension cannot allocate a code there.inline_too_largeuses-32602with a reason namespaced under the extension identifier. A consumedrequestStatereturns plain-32602, and a client treats that as an unknown outcome.Oversized inline data
SEP-1303 returns validation errors to the model so it can correct them. The model does not choose the transfer path, but the client does. A file over
maxSizestays a tool execution error, because the model or user can choose another file.omitandincludeSending the user's credential has to be optional, so a token never reaches third-party storage. Some enterprise deployments reject pre-signed URLs and want the same token on the control and data planes. A per-target flag covers both.
includeis honored onpathtargets and onurltargets with the same origin as the server's canonical URI. Withomit, clients attach no credential.includelets a server choose the path, method, and body of a request sent with the client's credential, so a server cannot issue one for a URL another application serves.Unbound
omittargetsPre-signed URLs are common, and requiring a matching host means each server runs its own upload endpoint. This draft leaves
omittargets unbound, because an approved server receives the file through its own tool anyway. User approval and host policy mitigate the risk but do not remove it, andformadds request forgery to that risk. The group has not decided this, including whether to allow wildcard hosts, so it is an Open Question.pathtargetsOne absolute URL assumes every client reaches a gateway the same way. A client in a sandbox or behind a proxy may not reach the public host at all. With
path, the bytes take the same route as the client's MCP calls.Downloads in result
_metaAn earlier revision used a
resource_link. Resources are mostly static, a link invitesresources/readon a file meant to travel out of band, and a client without the extension got a link it could not fetch. A typed list mirrors uploads, and a gateway finds returned files at one known key. The group discussed downloads when the draft usedresource_link, and has not reviewed this shape. It is listed under Open Questions.Resumable uploads and downloads
Clients can already split and resume downloads with HTTP range requests. Resumable upload is Future Work, once a standard is established.
Prior art
fileField, and applyingmaxSizeto every file. SEP-2356 applied it to inline values only.What we need from reviewers
Check whether the flow fits the storage APIs you work with, and tell us if anything in the security model would stop you from deploying it. Prototypes should also confirm that SDKs and gateways pass result
_metathrough intact.