From 31ce8a17addb54909918c558a1e6965ae5a51538 Mon Sep 17 00:00:00 2001 From: Den Delimarsky Date: Wed, 14 Oct 2026 06:13:14 +0000 Subject: [PATCH 01/19] Add initial file uploads extension draft 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. --- .github/CODEOWNERS | 1 + README.md | 51 ++- specification/draft/file-uploads.md | 674 ++++++++++++++++++++++++++++ 3 files changed, 725 insertions(+), 1 deletion(-) create mode 100644 .github/CODEOWNERS create mode 100644 specification/draft/file-uploads.md diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..b59a410 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @modelcontextprotocol/file-uploads-wg @modelcontextprotocol/core-maintainers diff --git a/README.md b/README.md index 7ca1512..9952093 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,51 @@ # experimental-ext-file-uploads -Experimental MCP extension for uploading files to tools over MCP. Maintained by the File Uploads Working Group. + +Experimental MCP extension for handing files from a client to a tool without routing the +bytes through the model or through JSON-RPC. Maintained by the +[File Uploads Working Group](https://modelcontextprotocol.io/community/working-groups/file-uploads). + +> [!IMPORTANT] +> This is an [experimental extension](https://modelcontextprotocol.io/extensions/overview#experimental-extensions). +> It is not part of the MCP specification, has not been proposed as a SEP, and will change +> without notice while the working group iterates on it. Do not ship it in production clients +> or servers. + +## What this is + +A server marks file-valued tool arguments in its input schema. When a client calls such a +tool with a file that is too large to inline, it sends the file's metadata with the other +arguments; the server either rejects the call on that basis or answers with an upload target +inside a multi-round-trip `InputRequiredResult`; the client sends the bytes out of band and +retries the call with the server-issued file URI. Small files travel inline as `data:` URIs. +No new top-level methods, no session state. + +The working draft is in [`specification/draft/file-uploads.md`](specification/draft/file-uploads.md). +A reference implementation will live in this repository alongside it. + +## Background + +- [Files Working Group meeting notes, October 2, 2026](https://github.com/modelcontextprotocol/modelcontextprotocol/discussions/3412), + where the group settled the problem statement, the metadata-first ordering, and the + decision to build on multi-round-trip requests and ship as an extension first. +- [SEP-2356: Declarative File Inputs](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2356) + (closed), which this draft reuses for the `x-mcp-file` keyword and inline encoding. +- [SEP-2631: File Objects and Transfer](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2631), + prior art for file URIs and transfer descriptors, which this work is expected to supersede + for the upload direction. +- [SEP-2532: Resource Streaming](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2532), + adjacent work on the download direction. + +## Participating + +Discussion happens in the Files WG thread on the +[MCP Discord](https://modelcontextprotocol.io/community/communication#discord) and in issues +and pull requests here. The group meets weekly; notes are posted as GitHub Discussions in the +main specification repository. + +Feedback that is most useful right now: whether the flow in the draft maps onto your storage +backend's upload API, which of the [open questions](specification/draft/file-uploads.md#open-questions) +matter to you, and anything in the security model that would stop you from deploying it. + +## License + +Apache 2.0, see [LICENSE](LICENSE). diff --git a/specification/draft/file-uploads.md b/specification/draft/file-uploads.md new file mode 100644 index 0000000..0657901 --- /dev/null +++ b/specification/draft/file-uploads.md @@ -0,0 +1,674 @@ +# File Uploads Extension + +- **Status**: Experimental draft, not yet proposed as a SEP +- **Extension Identifier**: `io.modelcontextprotocol/files` +- **Working Group**: [File Uploads](https://modelcontextprotocol.io/community/working-groups/file-uploads) +- **Editors**: Den Delimarsky (@localden) +- **Discussion**: [Files WG Discord thread](https://discord.com/channels/1358869848138059966/1551968557292593172/1552399008440520865) + and issues in this repository + +This document is the working draft of the extension. It is written in SEP form so that it can +be submitted as an Extensions Track SEP once a reference implementation exists and the +working group is satisfied with it. Until then everything here is subject to change. + +## Abstract + +This extension lets a client hand a file to a tool without routing the +bytes through the model or through JSON-RPC. A server marks file-valued tool arguments with +the `x-mcp-file` JSON Schema keyword from +[SEP-2356](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2356). When the +client calls such a tool it sends the file's metadata first, alongside the other arguments. +The server either rejects the call on that metadata alone, or answers with an +[`InputRequiredResult`](https://modelcontextprotocol.io/specification/draft/basic/patterns/mrtr) +carrying a `files/upload` input request that names an HTTPS upload target. The client sends +the bytes to that target and retries the same `tools/call` with the server-issued file URI in +the argument. Small files skip the exchange and travel inline as RFC 2397 `data:` URIs. + +The whole exchange lives inside one `tools/call` and its retry. There are no new top-level +methods, no session state, and nothing a gateway has to special-case beyond result types it +already forwards. The extension is negotiated per request through `clientCapabilities` and +advertised by servers in `server/discover`. + +This revision covers client-to-server transfer only. Server-to-client transfer (downloads), +resumable uploads, and file inputs on the elicitation surface are deferred and listed under +[Future Work](#future-work). + +## Motivation + +MCP has no interoperable way to get a file from the user to a tool. The pieces that exist +(`EmbeddedResource`, `BlobResourceContents`, base64 strings in arguments) all put the bytes +inside a JSON-RPC message, which means they also pass through whatever assembled that +message. For a host where the model emits tool arguments as tokens, that is the context +window. The practical result is that remote MCP servers cap file arguments at a few megabytes +and anything larger fails. + +The Files Working Group collected the cases this blocks at its +[October 2, 2026 meeting](https://github.com/modelcontextprotocol/modelcontextprotocol/discussions/3412): + +- A user asks an assistant to save a 40 MB PDF from the conversation into a cloud-storage + folder. +- An agent pulls a 200 MB CSV into a code sandbox exposed as an MCP server. +- An agent uploads a 50 MB deck as a new revision of an existing document and keeps the link. +- An enterprise agent uploads on a user's behalf, with or without the user present, to one of + many sites or drives, so the call that hands back an upload target has to carry the + destination and other parameters. + +The same meeting recorded the constraints a solution has to meet from the host side: one +model turn per upload; file contents and credentials never enter the context window; small +files stay in band to keep latency down; large files go out of band to avoid base64; it has +to work when a gateway, rather than the host, holds the OAuth credentials; and file pointers +on the wire have to be marked explicitly so a deterministic proxy can find them. + +Two earlier proposals addressed parts of this. SEP-2356 defined how a server declares "this +argument is a file" and an inline `data:` encoding, and deliberately stopped there. +[SEP-2631](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2631) added +out-of-band transfer through new `files/authorizeUpload` and `files/authorizeDownload` +methods. The working group found two problems with 2631 as written: bytes move before the +server has seen the rest of the call, so uploads that permissions, quota, or data residency +would reject still consume bandwidth and may land in the wrong region; and new top-level +methods are invisible to gateways and SDKs that only pass through request types they know. +This draft keeps 2356's declaration model and 2631's descriptor shapes, and moves the +negotiation into the multi-round-trip request pattern that the `2026-07-28` revision already +requires for server-to-client interaction. + +## Specification + +### Extension Identifier + +`io.modelcontextprotocol/files` + +### Negotiation + +A client that can perform out-of-band uploads declares the extension in the +`io.modelcontextprotocol/clientCapabilities` request metadata: + +```json +{ + "extensions": { + "io.modelcontextprotocol/files": { + "maxInlineBytes": 1048576 + } + } +} +``` + +| Field | Type | Description | +| ---------------- | ------ | ------------------------------------------------------------------------------------------------ | +| `maxInlineBytes` | number | Largest decoded file the client is willing to send inline as a `data:` URI. Optional; default 0. | + +A server that accepts out-of-band uploads advertises the extension in its `server/discover` +capabilities: + +```json +{ + "capabilities": { + "tools": {}, + "extensions": { + "io.modelcontextprotocol/files": { + "maxInlineBytes": 262144, + "maxUploadBytes": 262144000, + "uploadOrigins": ["https://upload.example.com"] + } + } + } +} +``` + +| Field | Type | Description | +| ---------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------- | +| `maxInlineBytes` | number | Largest decoded file the server accepts inline. Optional; if omitted the per-field `maxSize` governs. | +| `maxUploadBytes` | number | Largest file the server accepts through an upload target. Optional. | +| `uploadOrigins` | string[] | HTTPS origins the server's upload targets will use. Optional. See [Upload target restrictions](#upload-target-restrictions). | + +A client that has not declared the extension on a request **MUST NOT** receive a +`files/upload` input request in response to it. A server that needs a file larger than the +inline limits from such a client **SHOULD** fail the call with an explanatory tool error, or +fall back to URL-mode elicitation as described in SEP-2356. + +### Declaring file arguments + +Servers mark file-valued tool arguments with the `x-mcp-file` keyword exactly as SEP-2356 +specifies it: on a property of the form `{"type": "string", "format": "uri"}` inside +`Tool.inputSchema`, with an optional descriptor. + +```json +{ + "name": "save_to_folder", + "description": "Save a file into a folder in the user's drive.", + "inputSchema": { + "type": "object", + "properties": { + "folderId": { "type": "string" }, + "fileName": { "type": "string" }, + "file": { + "type": "string", + "format": "uri", + "x-mcp-file": { + "accept": ["application/pdf", "image/*"], + "maxSize": 262144000 + } + } + }, + "required": ["folderId", "fileName", "file"] + } +} +``` + +```typescript +interface FileInputDescriptor { + /** Media types or dot-prefixed extensions the host should filter to. Picker hint only. */ + accept?: string[]; + /** Largest decoded file, in bytes, the server will accept for this argument by any path. */ + maxSize?: number; +} +``` + +This draft changes one thing about the descriptor relative to SEP-2356: `maxSize` is the +server's per-argument limit regardless of transfer path, as SEP-2631 proposed, rather than an +inline-only limit. The inline ceiling is the smaller of `maxSize` and the negotiated +`maxInlineBytes` values. + +The keyword applies unchanged to array items (`{"type": "array", "items": {"type": "string", +"format": "uri", "x-mcp-file": {...}}}`) for tools that take several files. + +All host-side rules from SEP-2356 apply: hosts **SHOULD NOT** show `x-mcp-file` properties +to a model that emits arguments as tokens, **MUST** bind a file to the slot only on an +affirmative user selection or under their existing approval policy for model-supplied +values, and **MUST NOT** place file bytes in model context. + +### File argument values + +An `x-mcp-file` argument carries a URI string in one of two forms: + +- An RFC 2397 `data:` URI, encoded as SEP-2356 specifies. Used when the decoded size is at or + below the inline ceiling. +- A **file URI** issued by the server in a `files/upload` input request. Used after an + out-of-band upload. + +File URIs are opaque to the client. Clients **MUST NOT** parse, dereference, resolve, or +hand a file URI to any URI handler, including when the value happens to use a scheme the +platform recognizes. Servers **SHOULD** mint file URIs under the `mcp-file:` scheme so that +hosts and proxies can recognize them syntactically. A file URI identifies one immutable byte +sequence; servers **MUST** issue a new URI for different bytes. + +Servers **MUST** reject any other scheme in an `x-mcp-file` argument and **MUST NOT** +dereference it. + +### Upload flow + +#### 1. Initial call with file metadata + +When a bound file exceeds the inline ceiling, the client sends `tools/call` with every +argument populated except the file argument, and describes the pending file in request +metadata under the extension identifier: + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call", + "params": { + "name": "save_to_folder", + "arguments": { + "folderId": "fld_8c1a", + "fileName": "Q3 report.pdf" + }, + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "io.modelcontextprotocol/clientCapabilities": { + "extensions": { + "io.modelcontextprotocol/files": { "maxInlineBytes": 1048576 } + } + }, + "io.modelcontextprotocol/files": { + "pending": { + "/file": [ + { + "name": "Q3 report.pdf", + "mimeType": "application/pdf", + "size": 41204133, + "digest": { + "algorithm": "sha-256", + "value": "uU0nuZNNPgilLlLX2n2r-sSE7-N6U4D6ZVe-_rYh2sU" + } + } + ] + } + } + } + } +} +``` + +`pending` is keyed by a JSON Pointer ([RFC 6901](https://www.rfc-editor.org/rfc/rfc6901)) +into `arguments` identifying the `x-mcp-file` property. The value is a list of +`FileMetadata` objects, length one for a scalar property and one entry per file for an array +property, in argument order. + +```typescript +interface FileMetadata { + name?: string; + mimeType: string; // application/octet-stream if unknown + size: number; // decoded bytes + digest?: FileDigest; // SHOULD be sent when the client can compute it +} + +interface FileDigest { + algorithm: "sha-256" | string; + value: string; // base64url, no padding +} +``` + +The server now has everything the tool call will carry except the bytes. It **SHOULD** +perform all validation it can at this point (authorization for the destination, quota, +`accept` and `maxSize`, name collisions) and fail fast with a `CallToolResult` where +`isError` is `true`, following +[SEP-1303](https://modelcontextprotocol.io/seps/1303-input-validation-errors-as-tool-execution-errors), if the upload would +be refused. + +#### 2. Upload target + +If the server will accept the file, it responds with an `InputRequiredResult` containing one +`files/upload` input request per pending file: + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "result": { + "resultType": "input_required", + "inputRequests": { + "u1": { + "method": "files/upload", + "params": { + "argument": "/file", + "index": 0, + "file": { + "uri": "mcp-file:01JD3V7K2M", + "name": "Q3 report.pdf", + "mimeType": "application/pdf", + "size": 41204133 + }, + "upload": { + "url": "https://upload.example.com/u/01JD3V7K2M?sig=...", + "method": "PUT", + "headers": { "Content-Type": "application/pdf" }, + "credentials": "omit", + "expiresAt": "2026-10-14T18:30:00Z" + } + } + } + }, + "requestState": "..." + } +} +``` + +```typescript +interface FilesUploadRequest { + method: "files/upload"; + params: { + /** JSON Pointer into arguments, matching a key the client sent in `pending`. */ + argument: string; + /** Position within that pending list. */ + index: number; + /** The file URI the client will place in the argument, plus echoed metadata. */ + file: { uri: string; name?: string; mimeType?: string; size?: number }; + upload: UploadTarget; + }; +} + +interface UploadTarget { + /** Absolute https URL. */ + url: string; + method: "PUT" | "POST"; + /** Headers the client MUST send verbatim. MUST NOT include Authorization. */ + headers?: { [name: string]: string }; + /** + * If present, the client sends multipart/form-data: `fields` as form fields in order, + * then the file bytes under `fileField`. If absent, the request body is the raw bytes. + */ + form?: { fileField: string; fields?: { [name: string]: string } }; + /** + * "omit": the URL is self-authorizing; the client MUST NOT attach MCP credentials. + * "include": the client attaches the same Authorization it presents to the MCP + * endpoint. Permitted only for same-origin targets; see Upload target restrictions. + */ + credentials: "omit" | "include"; + /** RFC 3339 timestamp after which the target SHOULD be treated as dead. */ + expiresAt?: string; +} +``` + +Servers **SHOULD** carry whatever they need to correlate the retry (the issued file URIs, +the validated arguments, an expiry) in `requestState`, integrity-protected as the MRTR +pattern requires, so that the retry can be served by any instance. + +#### 3. Transfer + +For each `files/upload` request the client: + +1. Applies the [upload target restrictions](#upload-target-restrictions). A target that fails + them **MUST NOT** be contacted; the client treats the input request as unsatisfiable. +2. Sends one HTTPS request to `upload.url` with `upload.method`, the listed `headers`, and + either the raw bytes or the multipart body described by `form`. When `credentials` is + `"omit"` the client **MUST NOT** send an `Authorization` header or cookies. When it is + `"include"` the client sends the credential it would present to the MCP endpoint for this + server and nothing else. +3. Treats any `2xx` response as success. Any other outcome is a failed upload. + +The party that performs step 2 is whichever component holds the bytes and, for +`credentials: "include"`, the credential. In a gateway deployment that is typically the +gateway; the host forwards the `InputRequiredResult` unchanged and the gateway satisfies the +`files/upload` requests before the retry reaches the server. Nothing in the exchange requires +the model or the host UI to see the target URL. + +#### 4. Retry with file URIs + +The client retries `tools/call` with the original arguments, the file argument now set to +the issued file URI (or an array of them), an `inputResponses` entry for each `files/upload` +key, and `requestState` echoed verbatim: + +```json +{ + "jsonrpc": "2.0", + "id": 2, + "method": "tools/call", + "params": { + "name": "save_to_folder", + "arguments": { + "folderId": "fld_8c1a", + "fileName": "Q3 report.pdf", + "file": "mcp-file:01JD3V7K2M" + }, + "inputResponses": { + "u1": { "uri": "mcp-file:01JD3V7K2M", "status": "uploaded" } + }, + "requestState": "...", + "_meta": { + "io.modelcontextprotocol/protocolVersion": "2026-07-28", + "...": "..." + } + } +} +``` + +```typescript +interface FilesUploadResult { + uri: string; + status: "uploaded" | "failed"; + /** Present when status is "failed"; free text for logs, not shown to the model. */ + error?: string; +} +``` + +On receipt the server verifies `requestState`, confirms that each file URI in the arguments +was issued under that state and that the bytes arrived (and match `digest` when one was +given), performs any storage-side finalize step, runs the tool, and returns an ordinary +`CallToolResult`. The model sees that result and nothing of the exchange that produced it. + +If an upload is reported `failed`, did not arrive, or the target expired, the server +**SHOULD** return a fresh `InputRequiredResult` with new targets when a retry is plausible, +and otherwise a tool error. Servers **MUST NOT** run the tool with a file argument whose +bytes they have not confirmed. + +#### Sequence + +```mermaid +sequenceDiagram + participant H as Host or gateway + participant S as MCP server + participant T as Upload target + + H->>S: tools/call {args without file, _meta.files.pending} + alt rejected on metadata + S-->>H: CallToolResult {isError: true} + else accepted + S-->>H: InputRequiredResult {files/upload: target, file URI; requestState} + H->>T: PUT bytes (credentials per target) + T-->>H: 2xx + H->>S: tools/call {args with file URI, inputResponses, requestState} + S-->>H: CallToolResult + end +``` + +For a file at or below the inline ceiling the client skips all of this and sends a single +`tools/call` with a `data:` URI in the argument. + +### Upload target restrictions + +The upload target is chosen by the server, and in the `credentials: "include"` case the +client attaches a user credential to a request the server directed. Clients and gateways +therefore enforce the following independently of anything the server returns: + +1. `upload.url` **MUST** be an absolute `https` URL. Clients **MUST** reject any other + scheme. +2. Clients **MUST** reject targets whose host resolves to a loopback, link-local, private + ([RFC 1918](https://www.rfc-editor.org/rfc/rfc1918), [RFC 4193](https://www.rfc-editor.org/rfc/rfc4193)), + or otherwise non-globally-routable address, unless the MCP server itself was reached at + such an address. +3. Clients **MUST** restrict targets to an origin set established out of band for that + server: an operator or registry allowlist where one exists, and otherwise the MCP + endpoint's own origin. The server's advertised `uploadOrigins` **MAY** narrow that set and + **MUST NOT** widen it; a client with no out-of-band allowlist **MAY** treat + `uploadOrigins` as advisory input to a user- or operator-facing consent decision but + **MUST NOT** treat it as sufficient on its own. +4. `credentials: "include"` is honored only when the target's origin equals the MCP + endpoint's origin. For any other origin the client **MUST** behave as if `credentials` + were `"omit"`, and **MAY** refuse the target. +5. Clients **MUST NOT** follow HTTP redirects from an upload target. + +These rules exist so that a compromised or malicious server cannot use a host or gateway as +a request proxy into networks it cannot otherwise reach, and cannot harvest a user's MCP +credential by pointing an upload at itself under another name. + +### Error handling + +- Validation failures the model can act on (wrong type, too large, destination not + writable, quota) are tool errors: `CallToolResult` with `isError: true` and text naming the + argument and the constraint, per SEP-1303. +- A retry whose `requestState` fails verification, or whose `inputResponses` is structurally + malformed, is a JSON-RPC `-32602`. +- A retry that references a file URI the server did not issue under the presented + `requestState` **MUST** be rejected; servers **SHOULD** use a tool error so the host can + restart the flow. +- Digest mismatch after upload **SHOULD** be reported distinctly from other failures. + +Servers, clients, gateways, and SDKs **MUST** treat `data:` values in `x-mcp-file` arguments +and upload target URLs as sensitive in logs and telemetry, redacting bytes and query strings. + +### Changes to the core specification + +The MRTR pattern currently enumerates the request types permitted in `inputRequests` +(`elicitation/create`, `sampling/createMessage`, `roots/list`). This extension requires one +sentence added to that list: a server **MAY** also include a request type defined by an +extension the client declared on the originating request. No other core text changes. + +## Rationale + +### Why multi-round-trip requests instead of `files/*` methods + +SEP-2631 put upload negotiation in a separate `files/authorizeUpload` call made before +`tools/call`. That ordering means the server authorizes bytes without seeing the arguments +they belong to, so it cannot check the destination, apply quota, or pick a storage region +until after the transfer. It also means two requests that the protocol does not tie together; +a server has to invent its own correlation, and an intermediary has to learn a new method. + +Folding the negotiation into `InputRequiredResult` fixes all three. The server sees the full +call before issuing a target. The issued file URI, the validated arguments, and the expiry +ride in `requestState`, so the retry is self-contained and any instance can serve it. And +the only wire artifacts are a `tools/call`, a result, and another `tools/call`, which every +gateway and SDK built for `2026-07-28` already forwards. + +The working group considered and set aside a dedicated primitive for files. Tools plus MRTR +cover the collected use cases; a new primitive can come later if they stop doing so. + +### Why metadata rides in `_meta` on the first call + +The file argument's schema is a URI string, so it cannot hold a metadata object, and +inventing a placeholder URI that encodes name and size would be a second wire format for the +same information. Request `_meta` under the extension's own key is already where +per-request extension data lives, it survives SDK argument validation, and it keeps +`arguments` schema-valid on both the initial call and the retry. This is listed under +[Open Questions](#open-questions) in case reviewers prefer a different placement. + +### Why keep `x-mcp-file` and `data:` + +SEP-2356's reasoning for an inline schema keyword over a sidecar map, and for `data:` as the +inline carrier, still holds and is not repeated here. Reusing both means a server written +against 2356 gains out-of-band transfer by declaring the extension and handling one new +input request type; its tool schemas do not change. + +### Why `credentials: omit | include` and the same-origin rule + +The working group heard two firm and opposite requirements: storage providers that issue +pre-signed URLs and do not want a bearer token anywhere near them, and enterprise +deployments where pre-authorized URLs are unacceptable and the data plane has to be +authorized with the same token as the control plane. A per-target flag covers both without +the client guessing. Restricting `include` to the MCP endpoint's origin is the narrowest rule +that serves the enterprise case (the server fronts its own upload path) while guaranteeing +the token never reaches a third party. Widening it, for example to any origin listed in the +server's Protected Resource Metadata, is an open question. + +A third mode, where the upload target needs a credential distinct from the MCP one (a +storage provider with its own OAuth), is real but deferred; it likely composes with URL-mode +elicitation or a second authorization flow and does not change the shapes here. + +### Why a single request and no resumability in this revision + +No resumable-upload protocol has broad enough adoption to mandate. tus and the +[httpbis resumable upload draft](https://datatracker.ietf.org/doc/draft-ietf-httpbis-resumable-upload/) +are both candidates; pinning an extension to an Internet-Draft is premature. A single PUT or +POST handles files into the low hundreds of megabytes on ordinary connections, which covers +the collected use cases. Resumability can be added as a negotiated `UploadTarget` variant +without touching anything specified here. + +### Why host-enforced target restrictions rather than server-declared ones + +A server-declared allowlist is chosen by the same party that chooses the URL, so on its own +it constrains nothing. The restrictions section makes the client or gateway the enforcing +party and lets `uploadOrigins` only narrow. This matches how hosts already treat other +server-supplied URLs (OAuth metadata, URL-mode elicitation) and is the control that makes +`credentials: "include"` safe to offer at all. + +### Why uploads only + +Downloads touch different host code paths (how a result reaches the user and the model), do +not have the metadata-first problem, and already have partial answers in `resource_link`, +`EmbeddedResource`, and +[SEP-2532](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2532). The +working group is treating them as the same track, and the `UploadTarget` shape is written so +a `DownloadTarget` can mirror it, but specifying both at once would double the review surface +for the first revision. + +### Prior art + +- SEP-2356 (declaration keyword, inline encoding, host-side rules), closed in favor of 2631 + and revived here for the parts this draft depends on. +- SEP-2631 (file URIs, digest, upload and download descriptors, `maxSize` semantics), which + this draft expects to supersede for the upload direction. +- SEP-2532 (resource streaming) and SEP-2571 (resource submission), adjacent work in the same + group. +- OpenAI Apps SDK `openai/fileParams`, a vendor `_meta` hint naming file-valued parameters. +- Storage-provider APIs that already separate "create upload session" from "send bytes" + (Google Drive resumable upload, Box upload sessions, Dropbox temporary upload links, + S3 and GCS signed URLs). The `files/upload` input request is shaped so a server can wrap + any of these. + +## Backward Compatibility + +The extension is additive and off by default. A client that does not declare it never sees a +`files/upload` input request. A server that does not advertise it receives `data:` URIs up +to whatever `maxSize` it declares, exactly as under SEP-2356, or no file support at all. +Tools that today take ad hoc base64 or URL string arguments keep working and gain nothing +until their authors adopt `x-mcp-file`. + +The one core edit (permitting extension-defined request types in `inputRequests` when the +client declared the extension) does not change behavior for any existing client or server. + +## Security Implications + +The threat model has three parties that do not fully trust each other: the user (represented +by the host), the MCP server, and whatever storage sits behind the upload target. + +**Server-directed requests from the client.** The upload target is an instruction from the +server to make an HTTP request. [Upload target restrictions](#upload-target-restrictions) +bound where that request can go and what credential it can carry. Implementations that relax +those rules reintroduce server-side request forgery through the client or gateway and +credential disclosure to third-party origins. + +**User consent and file provenance.** Everything SEP-2356 says about host-side file access +applies: the host binds a file to an `x-mcp-file` slot only on explicit user selection for +that server and tool, or forwards a model-supplied `data:` value under its existing approval +policy without dereferencing it. Hosts **SHOULD** show the user the file identity, size, and +destination server before the initial call, since after that point the transfer proceeds +without further interaction. The server receives no provenance signal and **MUST NOT** assume +a file was user-selected rather than model-authored. + +**Replay and substitution.** `requestState` binds the issued file URIs to the validated +arguments, the principal, and an expiry; servers **MUST** integrity-protect it as the MRTR +pattern requires and **MUST** refuse file URIs not issued under the presented state. Without +this a client could upload under one call's authorization and attach the file to another. +Digests, when the client supplies them, let the server detect byte substitution between +metadata and upload. + +**Untrusted content.** Servers are receiving arbitrary bytes and **MUST** apply the file +handling controls appropriate to the formats they accept; +[OWASP ASVS 5.0 V5](https://github.com/OWASP/ASVS/blob/v5.0.0/5.0/en/0x14-V5-File-Handling.md) +is the reference. Tool results derived from uploaded content are untrusted with respect to +the model and hosts **MUST** present them as data. + +**Logging.** Upload URLs frequently embed signatures; `data:` values embed user bytes. Both +**MUST** be redacted in logs, traces, and error reports at every hop. + +## Reference Implementation + +To be built in this repository before the draft is submitted as a SEP. The plan is a TypeScript SDK branch adding +the `files/upload` input request type and client-side transfer helper, a reference server +that fronts an S3-compatible store, and a conformance scenario covering inline, out-of-band, +early rejection, expired target, and digest mismatch. + +## Open Questions + +1. **Placement of pending-file metadata.** Request `_meta` under the extension key (as + specified), a reserved placeholder URI in the argument slot, or a sibling field on + `CallToolRequestParams`. +2. **Multipart in the first revision.** Raw-body PUT/POST covers signed-URL stores; several + provider APIs want `multipart/form-data` with extra fields. The `form` member is included + above; reviewers may prefer to defer it. +3. **Scope of `credentials: "include"`.** Same origin as the MCP endpoint (as specified), or + any origin the server's Protected Resource Metadata names as the same protected resource. +4. **Mandating the `mcp-file:` scheme** for issued file URIs versus leaving the scheme to the + server with opacity rules only. +5. **Per-argument transfer mode.** SEP-2631's `transferModes` let a server force out-of-band + for a specific field. This draft relies on `maxInlineBytes` and `maxSize` instead; a server + that never wants inline sets `maxInlineBytes: 0`. +6. **Explicit finalize.** Some backends need a commit call after the bytes land. As written + the server performs it while handling the retry; reviewers from those backends should + confirm that is sufficient. +7. **Elicitation-surface uploads.** An `x-mcp-file` field inside an `elicitation/create` + form is itself delivered through `inputRequests`; supporting out-of-band upload there + means a nested negotiation. This draft leaves elicitation file fields inline-only. +8. **Client-reported upload evidence.** Whether `FilesUploadResult` should carry the + target's response status, `ETag`, or body for stores that need it to finalize. + +## Future Work + +- **Downloads.** A `DownloadTarget` mirroring `UploadTarget`, attached to `resource_link` + content or returned through a `files/download` input request on `resources/read`, with the + same origin and credential rules. +- **Resumable and chunked transfer** as an additional negotiated target type once a standard + settles. +- **Third-party storage credentials**, where the upload target needs an authorization the + MCP credential does not cover. +- **Promotion to the core specification** after the extension has shipped in SDKs and at + least two independent hosts, per the working group's October 2 decision to validate in use + first. + +## Acknowledgments + +This draft consolidates work by Olivier Chafik (SEP-2356) and Casey Chow (SEP-2631), and the +discussion at the Files Working Group's October 2, 2026 meeting with Wil St. Charles, Kurtis +Van Gent, Thomas Sarlandie, and Kryspin Ziemski. Thomas Sarlandie proposed modeling the +upload as a multi-round-trip request. Patrick Rodgers' SEP-2532 informed the deferred +download direction. From d6760df54fcaeefe2aaca82a68567324610f902f Mon Sep 17 00:00:00 2001 From: Den Delimarsky Date: Wed, 14 Oct 2026 06:27:57 +0000 Subject: [PATCH 02/19] Revise file uploads draft after review 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. --- README.md | 52 +- specification/draft/file-uploads.md | 819 +++++++++++----------------- 2 files changed, 343 insertions(+), 528 deletions(-) diff --git a/README.md b/README.md index 9952093..68c68f2 100644 --- a/README.md +++ b/README.md @@ -1,51 +1,45 @@ # experimental-ext-file-uploads -Experimental MCP extension for handing files from a client to a tool without routing the -bytes through the model or through JSON-RPC. Maintained by the +Experimental MCP extension for sending files from a client to a tool out of band, so the bytes +stay out of model context and out of JSON-RPC messages. Maintained by the [File Uploads Working Group](https://modelcontextprotocol.io/community/working-groups/file-uploads). > [!IMPORTANT] > This is an [experimental extension](https://modelcontextprotocol.io/extensions/overview#experimental-extensions). -> It is not part of the MCP specification, has not been proposed as a SEP, and will change -> without notice while the working group iterates on it. Do not ship it in production clients -> or servers. +> It is not part of the MCP specification and has not been proposed as a SEP. Expect breaking +> changes while the working group iterates. -## What this is +## Overview -A server marks file-valued tool arguments in its input schema. When a client calls such a -tool with a file that is too large to inline, it sends the file's metadata with the other -arguments; the server either rejects the call on that basis or answers with an upload target -inside a multi-round-trip `InputRequiredResult`; the client sends the bytes out of band and -retries the call with the server-issued file URI. Small files travel inline as `data:` URIs. -No new top-level methods, no session state. +A server marks file arguments in a tool's input schema. To send a file too large to inline, the +client calls the tool with a placeholder in the file argument and the file's metadata in request +`_meta`. The server rejects the call or returns an `upload_required` result with an upload +target. The client uploads the bytes to that target and retries the call with the file URI the +server issued. Small files go inline as `data:` URIs. -The working draft is in [`specification/draft/file-uploads.md`](specification/draft/file-uploads.md). -A reference implementation will live in this repository alongside it. +The draft is in [`specification/draft/file-uploads.md`](specification/draft/file-uploads.md). +The reference implementation will live in this repository. ## Background -- [Files Working Group meeting notes, October 2, 2026](https://github.com/modelcontextprotocol/modelcontextprotocol/discussions/3412), - where the group settled the problem statement, the metadata-first ordering, and the - decision to build on multi-round-trip requests and ship as an extension first. +- [Working group meeting notes, October 2, 2026](https://github.com/modelcontextprotocol/modelcontextprotocol/discussions/3412) - [SEP-2356: Declarative File Inputs](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2356) - (closed), which this draft reuses for the `x-mcp-file` keyword and inline encoding. + (closed), source of the `x-mcp-file` keyword and inline encoding - [SEP-2631: File Objects and Transfer](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2631), - prior art for file URIs and transfer descriptors, which this work is expected to supersede - for the upload direction. + prior art for file URIs and transfer descriptors - [SEP-2532: Resource Streaming](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2532), - adjacent work on the download direction. + related work on getting file bytes from a server ## Participating -Discussion happens in the Files WG thread on the -[MCP Discord](https://modelcontextprotocol.io/community/communication#discord) and in issues -and pull requests here. The group meets weekly; notes are posted as GitHub Discussions in the -main specification repository. +The working group meets weekly and discusses this work in its thread on the +[MCP Discord](https://modelcontextprotocol.io/community/communication#discord). Open an issue or +pull request here for anything specific to the draft. -Feedback that is most useful right now: whether the flow in the draft maps onto your storage -backend's upload API, which of the [open questions](specification/draft/file-uploads.md#open-questions) -matter to you, and anything in the security model that would stop you from deploying it. +We want to hear whether the flow fits your storage backend's upload API, which +[open questions](specification/draft/file-uploads.md#open-questions) block you, and whether +anything in the security model would stop you from deploying it. ## License -Apache 2.0, see [LICENSE](LICENSE). +Apache 2.0. See [LICENSE](LICENSE). diff --git a/specification/draft/file-uploads.md b/specification/draft/file-uploads.md index 0657901..49a9782 100644 --- a/specification/draft/file-uploads.md +++ b/specification/draft/file-uploads.md @@ -1,206 +1,170 @@ # File Uploads Extension - **Status**: Experimental draft, not yet proposed as a SEP -- **Extension Identifier**: `io.modelcontextprotocol/files` +- **Extension Identifier**: `io.modelcontextprotocol/files` (provisional) - **Working Group**: [File Uploads](https://modelcontextprotocol.io/community/working-groups/file-uploads) - **Editors**: Den Delimarsky (@localden) -- **Discussion**: [Files WG Discord thread](https://discord.com/channels/1358869848138059966/1551968557292593172/1552399008440520865) +- **Discussion**: [File Uploads Working Group Discord thread](https://discord.com/channels/1358869848138059966/1551968557292593172/1552399008440520865) and issues in this repository -This document is the working draft of the extension. It is written in SEP form so that it can -be submitted as an Extensions Track SEP once a reference implementation exists and the -working group is satisfied with it. Until then everything here is subject to change. +This is the working draft of the extension, written in SEP form so the working group can submit +it as an Extensions Track SEP once a reference implementation exists. Anything here can change +until then, including the extension identifier. ## Abstract -This extension lets a client hand a file to a tool without routing the -bytes through the model or through JSON-RPC. A server marks file-valued tool arguments with -the `x-mcp-file` JSON Schema keyword from -[SEP-2356](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2356). When the -client calls such a tool it sends the file's metadata first, alongside the other arguments. -The server either rejects the call on that metadata alone, or answers with an -[`InputRequiredResult`](https://modelcontextprotocol.io/specification/draft/basic/patterns/mrtr) -carrying a `files/upload` input request that names an HTTPS upload target. The client sends -the bytes to that target and retries the same `tools/call` with the server-issued file URI in -the argument. Small files skip the exchange and travel inline as RFC 2397 `data:` URIs. - -The whole exchange lives inside one `tools/call` and its retry. There are no new top-level -methods, no session state, and nothing a gateway has to special-case beyond result types it -already forwards. The extension is negotiated per request through `clientCapabilities` and -advertised by servers in `server/discover`. - -This revision covers client-to-server transfer only. Server-to-client transfer (downloads), -resumable uploads, and file inputs on the elicitation surface are deferred and listed under -[Future Work](#future-work). +This extension lets a client send a file to a tool without passing a large file's bytes through +the model or a JSON-RPC message. A server marks file arguments with the `x-mcp-file` keyword +first proposed in [SEP-2356](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2356). +To send a file too large to inline, the client calls the tool with a placeholder in the file +argument and the file's metadata in request `_meta`. The server can reject the call on that +metadata. If it accepts, it returns an `upload_required` result with an HTTPS upload target and +a server-issued file URI. The client uploads the bytes and retries the call with the file URI in +place of the placeholder. Small files go inline as RFC 2397 `data:` URIs. + +This revision covers uploads only. Downloads, chunked and resumable transfer, and file fields in +elicitation forms are listed under [Future Work](#future-work). ## Motivation -MCP has no interoperable way to get a file from the user to a tool. The pieces that exist -(`EmbeddedResource`, `BlobResourceContents`, base64 strings in arguments) all put the bytes -inside a JSON-RPC message, which means they also pass through whatever assembled that -message. For a host where the model emits tool arguments as tokens, that is the context -window. The practical result is that remote MCP servers cap file arguments at a few megabytes -and anything larger fails. +MCP has no interoperable way to get a file from a user to a tool. The existing options +(`EmbeddedResource`, `BlobResourceContents`, base64 strings in arguments) put the bytes inside a +JSON-RPC message. When the model emits tool arguments as tokens, the bytes pass through its +context window, and large uploads to remote MCP servers fail. -The Files Working Group collected the cases this blocks at its +The File Uploads Working Group collected the cases this blocks at its [October 2, 2026 meeting](https://github.com/modelcontextprotocol/modelcontextprotocol/discussions/3412): -- A user asks an assistant to save a 40 MB PDF from the conversation into a cloud-storage - folder. -- An agent pulls a 200 MB CSV into a code sandbox exposed as an MCP server. +- A user asks an assistant to save a 40 MB PDF from the conversation into a cloud-storage folder. +- An agent pulls a 200 MB CSV into its code sandbox. - An agent uploads a 50 MB deck as a new revision of an existing document and keeps the link. -- An enterprise agent uploads on a user's behalf, with or without the user present, to one of - many sites or drives, so the call that hands back an upload target has to carry the - destination and other parameters. - -The same meeting recorded the constraints a solution has to meet from the host side: one -model turn per upload; file contents and credentials never enter the context window; small -files stay in band to keep latency down; large files go out of band to avoid base64; it has -to work when a gateway, rather than the host, holds the OAuth credentials; and file pointers -on the wire have to be marked explicitly so a deterministic proxy can find them. - -Two earlier proposals addressed parts of this. SEP-2356 defined how a server declares "this -argument is a file" and an inline `data:` encoding, and deliberately stopped there. -[SEP-2631](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2631) added -out-of-band transfer through new `files/authorizeUpload` and `files/authorizeDownload` -methods. The working group found two problems with 2631 as written: bytes move before the -server has seen the rest of the call, so uploads that permissions, quota, or data residency -would reject still consume bandwidth and may land in the wrong region; and new top-level -methods are invisible to gateways and SDKs that only pass through request types they know. -This draft keeps 2356's declaration model and 2631's descriptor shapes, and moves the -negotiation into the multi-round-trip request pattern that the `2026-07-28` revision already -requires for server-to-client interaction. +- An enterprise agent uploads or downloads on a user's behalf, with or without the user present, + to one of many sites or drives, so the call that returns an upload target has to carry the + destination. + +The same meeting recorded the host-side constraints: + +- One model turn per upload. +- File contents and credentials never enter the context window. +- Small files stay in band to keep latency down. +- Large files go out of band to avoid base64. +- Uploads work when a gateway holds the OAuth credentials instead of the host. +- File pointers on the wire are marked so a deterministic proxy can find them. + +SEP-2356 defined how a server declares a file argument and an inline `data:` encoding, and sent +larger files to URL-mode elicitation instead of defining an upload protocol. It closed without +merging. [SEP-2631](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2631) added +out-of-band transfer through new `files/authorizeUpload` and `files/authorizeDownload` methods. +Thomas Sarlandie called ordering SEP-2631's biggest flaw: bytes move before the server sees the +rest of the call, so uploads that permissions, quota, or data residency rules would reject still +consume bandwidth and can land in the wrong region. The group agreed on metadata first, then the +upload target, then bytes, then finalize, and agreed to build on multi-round-trip requests (MRTR) +and existing primitives before adding new ones. ## Specification -### Extension Identifier +In this document, _client_ is the component that speaks MCP to the server: the host, or a +gateway acting for it. _Host_ is the application that presents the model and the user interface. + +### Extension identifier `io.modelcontextprotocol/files` ### Negotiation -A client that can perform out-of-band uploads declares the extension in the -`io.modelcontextprotocol/clientCapabilities` request metadata: +A client that can perform out-of-band uploads declares the extension, with no settings, in the +`io.modelcontextprotocol/clientCapabilities` request metadata of every request that may need it: ```json -{ - "extensions": { - "io.modelcontextprotocol/files": { - "maxInlineBytes": 1048576 - } - } -} +{ "extensions": { "io.modelcontextprotocol/files": {} } } ``` -| Field | Type | Description | -| ---------------- | ------ | ------------------------------------------------------------------------------------------------ | -| `maxInlineBytes` | number | Largest decoded file the client is willing to send inline as a `data:` URI. Optional; default 0. | - -A server that accepts out-of-band uploads advertises the extension in its `server/discover` -capabilities: +A server that accepts out-of-band uploads advertises the extension in `server/discover`: ```json { "capabilities": { "tools": {}, "extensions": { - "io.modelcontextprotocol/files": { - "maxInlineBytes": 262144, - "maxUploadBytes": 262144000, - "uploadOrigins": ["https://upload.example.com"] - } + "io.modelcontextprotocol/files": { "maxInlineBytes": 1048576 } } } } ``` -| Field | Type | Description | -| ---------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------- | -| `maxInlineBytes` | number | Largest decoded file the server accepts inline. Optional; if omitted the per-field `maxSize` governs. | -| `maxUploadBytes` | number | Largest file the server accepts through an upload target. Optional. | -| `uploadOrigins` | string[] | HTTPS origins the server's upload targets will use. Optional. See [Upload target restrictions](#upload-target-restrictions). | +`maxInlineBytes` is the largest total decoded size of `data:` values the server accepts in one +request. If the server omits it, clients **SHOULD** use out-of-band transfer for any file larger +than a small, implementation-defined size. -A client that has not declared the extension on a request **MUST NOT** receive a -`files/upload` input request in response to it. A server that needs a file larger than the -inline limits from such a client **SHOULD** fail the call with an explanatory tool error, or -fall back to URL-mode elicitation as described in SEP-2356. +Clients **MUST NOT** send a placeholder or pending-file metadata to a server that did not +advertise the extension. Servers report an oversized `data:` value as a tool error, following +[SEP-1303](https://modelcontextprotocol.io/seps/1303-input-validation-errors-as-tool-execution-errors), +and **MAY** return `MissingRequiredClientCapabilityError` (`-32021`) instead when the request +did not declare the extension. -### Declaring file arguments +### File arguments -Servers mark file-valued tool arguments with the `x-mcp-file` keyword exactly as SEP-2356 -specifies it: on a property of the form `{"type": "string", "format": "uri"}` inside -`Tool.inputSchema`, with an optional descriptor. +SEP-2356 closed without merging, so this section restates the parts of it this extension uses. -```json -{ - "name": "save_to_folder", - "description": "Save a file into a folder in the user's drive.", - "inputSchema": { - "type": "object", - "properties": { - "folderId": { "type": "string" }, - "fileName": { "type": "string" }, - "file": { - "type": "string", - "format": "uri", - "x-mcp-file": { - "accept": ["application/pdf", "image/*"], - "maxSize": 262144000 - } - } - }, - "required": ["folderId", "fileName", "file"] - } -} -``` +A server marks a file argument by adding `x-mcp-file` to a property of the form +`{"type": "string", "format": "uri"}` in `Tool.inputSchema`, including array items: ```typescript interface FileInputDescriptor { - /** Media types or dot-prefixed extensions the host should filter to. Picker hint only. */ + /** Media types or dot-prefixed extensions to filter a file picker to. A hint only. */ accept?: string[]; - /** Largest decoded file, in bytes, the server will accept for this argument by any path. */ + /** Largest decoded size of each file, in bytes, on any transfer path. */ maxSize?: number; } ``` -This draft changes one thing about the descriptor relative to SEP-2356: `maxSize` is the -server's per-argument limit regardless of transfer path, as SEP-2631 proposed, rather than an -inline-only limit. The inline ceiling is the smaller of `maxSize` and the negotiated -`maxInlineBytes` values. +SEP-2356 applied `maxSize` to inline values only. This extension applies it to every file, as +SEP-2631 proposed. -The keyword applies unchanged to array items (`{"type": "array", "items": {"type": "string", -"format": "uri", "x-mcp-file": {...}}}`) for tools that take several files. +An `x-mcp-file` value is one of: -All host-side rules from SEP-2356 apply: hosts **SHOULD NOT** show `x-mcp-file` properties -to a model that emits arguments as tokens, **MUST** bind a file to the slot only on an -affirmative user selection or under their existing approval policy for model-supplied -values, and **MUST NOT** place file bytes in model context. +- An RFC 2397 `data:` URI. Clients use `application/octet-stream` when the media type is + unknown. +- A placeholder beginning with `mcp-file:pending`, on the initial call of an out-of-band upload. + Clients **MAY** append a suffix (`mcp-file:pending:2`) to keep array items distinct. +- A file URI the server issued in an `upload_required` result, on the retry. -### File argument values +Servers **MUST** reject any other value and **MUST NOT** dereference it. Clients **MUST** treat +file URIs as opaque and **MUST NOT** parse, resolve, or pass a file URI to any URI handler. +Servers **SHOULD** mint file URIs under the `mcp-file:` scheme so proxies can recognize them. -An `x-mcp-file` argument carries a URI string in one of two forms: +Hosts: -- An RFC 2397 `data:` URI, encoded as SEP-2356 specifies. Used when the decoded size is at or - below the inline ceiling. -- A **file URI** issued by the server in a `files/upload` input request. Used after an - out-of-band upload. +- **SHOULD NOT** show `x-mcp-file` properties to a model that emits arguments as tokens. +- **MUST** bind a file to an argument only on the user's selection or under the host's approval + policy for model-supplied values. +- **MUST NOT** place file bytes, upload URLs, or issued file URIs in model context. -File URIs are opaque to the client. Clients **MUST NOT** parse, dereference, resolve, or -hand a file URI to any URI handler, including when the value happens to use a scheme the -platform recognizes. Servers **SHOULD** mint file URIs under the `mcp-file:` scheme so that -hosts and proxies can recognize them syntactically. A file URI identifies one immutable byte -sequence; servers **MUST** issue a new URI for different bytes. +### Upload flow -Servers **MUST** reject any other scheme in an `x-mcp-file` argument and **MUST NOT** -dereference it. +```mermaid +sequenceDiagram + participant C as Client + participant S as MCP server + participant T as Upload target -### Upload flow + C->>S: tools/call {file: "mcp-file:pending"}, _meta pending + alt rejected on metadata + S-->>C: CallToolResult {isError: true} + else accepted + S-->>C: upload_required {uploads, requestState} + C->>T: PUT bytes + T-->>C: 2xx + C->>S: tools/call {file: "mcp-file:01JD..."}, requestState + S-->>C: CallToolResult + end +``` -#### 1. Initial call with file metadata +#### Initial call -When a bound file exceeds the inline ceiling, the client sends `tools/call` with every -argument populated except the file argument, and describes the pending file in request -metadata under the extension identifier: +The client sends `tools/call` with every argument populated, a placeholder in each out-of-band +file argument or array item, and the pending files' metadata in request `_meta` under the +extension identifier: ```json { @@ -211,28 +175,24 @@ metadata under the extension identifier: "name": "save_to_folder", "arguments": { "folderId": "fld_8c1a", - "fileName": "Q3 report.pdf" + "file": "mcp-file:pending" }, "_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientCapabilities": { - "extensions": { - "io.modelcontextprotocol/files": { "maxInlineBytes": 1048576 } - } + "extensions": { "io.modelcontextprotocol/files": {} } }, "io.modelcontextprotocol/files": { "pending": { - "/file": [ - { - "name": "Q3 report.pdf", - "mimeType": "application/pdf", - "size": 41204133, - "digest": { - "algorithm": "sha-256", - "value": "uU0nuZNNPgilLlLX2n2r-sSE7-N6U4D6ZVe-_rYh2sU" - } + "/file": { + "name": "Q3 report.pdf", + "mimeType": "application/pdf", + "size": 41204133, + "digest": { + "algorithm": "sha-256", + "value": "uU0nuZNNPgilLlLX2n2r-sSE7-N6U4D6ZVe-_rYh2sU" } - ] + } } } } @@ -240,63 +200,52 @@ metadata under the extension identifier: } ``` -`pending` is keyed by a JSON Pointer ([RFC 6901](https://www.rfc-editor.org/rfc/rfc6901)) -into `arguments` identifying the `x-mcp-file` property. The value is a list of -`FileMetadata` objects, length one for a scalar property and one entry per file for an array -property, in argument order. +Each key in `pending` is a [JSON Pointer](https://www.rfc-editor.org/rfc/rfc6901) to one +placeholder in `arguments` (`/file`, or `/files/2` for an array item). An array argument can mix +inline `data:` items and placeholders. ```typescript interface FileMetadata { name?: string; - mimeType: string; // application/octet-stream if unknown - size: number; // decoded bytes - digest?: FileDigest; // SHOULD be sent when the client can compute it -} - -interface FileDigest { - algorithm: "sha-256" | string; - value: string; // base64url, no padding + /** "application/octet-stream" if unknown. */ + mimeType: string; + /** Decoded size in bytes. */ + size: number; + digest?: { algorithm: "sha-256"; value: string /* base64url, no padding */ }; } ``` -The server now has everything the tool call will carry except the bytes. It **SHOULD** -perform all validation it can at this point (authorization for the destination, quota, -`accept` and `maxSize`, name collisions) and fail fast with a `CallToolResult` where -`isError` is `true`, following -[SEP-1303](https://modelcontextprotocol.io/seps/1303-input-validation-errors-as-tool-execution-errors), if the upload would -be refused. +Clients **SHOULD** include `digest`. Before sending the initial call, the host **MUST** obtain +the user's approval, or apply operator policy when no user is present, for sending the file to +this server and to storage the server designates. -#### 2. Upload target +Servers **SHOULD** run every check that does not need the bytes (destination authorization, +quota, `accept` and `maxSize`, name collisions) and return a `CallToolResult` with +`isError: true` if one fails. Servers **MUST** treat the metadata as client-asserted and re-check +size and type once the bytes arrive. -If the server will accept the file, it responds with an `InputRequiredResult` containing one -`files/upload` input request per pending file: +#### Upload required + +If the server accepts the files, it returns a result with `resultType: "upload_required"`, an +extension-defined result type as the +[base protocol](https://modelcontextprotocol.io/specification/draft/basic/index#resulttype) +permits. This extension reserves the value `"upload_required"`. ```json { "jsonrpc": "2.0", "id": 1, "result": { - "resultType": "input_required", - "inputRequests": { - "u1": { - "method": "files/upload", - "params": { - "argument": "/file", - "index": 0, - "file": { - "uri": "mcp-file:01JD3V7K2M", - "name": "Q3 report.pdf", - "mimeType": "application/pdf", - "size": 41204133 - }, - "upload": { - "url": "https://upload.example.com/u/01JD3V7K2M?sig=...", - "method": "PUT", - "headers": { "Content-Type": "application/pdf" }, - "credentials": "omit", - "expiresAt": "2026-10-14T18:30:00Z" - } - } + "resultType": "upload_required", + "uploads": { + "/file": { + "uri": "mcp-file:01JD3V7K2M", + "url": "https://upload.example.com/u/01JD3V7K2M?sig=...", + "headers": { + "Content-Type": "application/pdf", + "x-amz-checksum-sha256": "uU0nuZNNPgilLlLX2n2r+sSE7+N6U4D6ZVe+/rYh2sU=" + }, + "credentials": "omit" } }, "requestState": "..." @@ -305,69 +254,67 @@ If the server will accept the file, it responds with an `InputRequiredResult` co ``` ```typescript -interface FilesUploadRequest { - method: "files/upload"; - params: { - /** JSON Pointer into arguments, matching a key the client sent in `pending`. */ - argument: string; - /** Position within that pending list. */ - index: number; - /** The file URI the client will place in the argument, plus echoed metadata. */ - file: { uri: string; name?: string; mimeType?: string; size?: number }; - upload: UploadTarget; - }; +interface UploadRequiredResult extends Result { + resultType: "upload_required"; + /** One entry per pending pointer. */ + uploads: { [pointer: string]: UploadTarget }; + requestState: string; } interface UploadTarget { - /** Absolute https URL. */ + /** The file URI the client places in the argument on the retry. */ + uri: string; + /** Absolute https URL. The client sends the bytes with PUT. */ url: string; - method: "PUT" | "POST"; - /** Headers the client MUST send verbatim. MUST NOT include Authorization. */ + /** Headers the client sends with the PUT. */ headers?: { [name: string]: string }; /** - * If present, the client sends multipart/form-data: `fields` as form fields in order, - * then the file bytes under `fileField`. If absent, the request body is the raw bytes. - */ - form?: { fileField: string; fields?: { [name: string]: string } }; - /** - * "omit": the URL is self-authorizing; the client MUST NOT attach MCP credentials. - * "include": the client attaches the same Authorization it presents to the MCP - * endpoint. Permitted only for same-origin targets; see Upload target restrictions. + * "omit": the URL authorizes itself. "include": the client attaches the credential it + * presents to the MCP server. See Upload targets. */ credentials: "omit" | "include"; - /** RFC 3339 timestamp after which the target SHOULD be treated as dead. */ - expiresAt?: string; } ``` -Servers **SHOULD** carry whatever they need to correlate the retry (the issued file URIs, -the validated arguments, an expiry) in `requestState`, integrity-protected as the MRTR -pattern requires, so that the retry can be served by any instance. +Servers **MUST** return `upload_required` only on `tools/call` and only to a request that +declared the extension. A server using the +[Tasks extension](https://modelcontextprotocol.io/extensions/tasks/overview) **MUST** return +`upload_required` before creating a task. + +Servers **MUST** bind the issued file URIs, the tool name, and the other arguments into +`requestState`, which follows the +[MRTR server rules](https://modelcontextprotocol.io/specification/draft/basic/patterns/mrtr#server-requirements-basic-workflow). +When computing the request digest that MRTR recommends, servers **MUST** exclude the values at +the pointers bound in `requestState`. Clients handle `requestState` as the +[MRTR client rules](https://modelcontextprotocol.io/specification/draft/basic/patterns/mrtr#client-requirements-basic-workflow) +require for `InputRequiredResult`. -#### 3. Transfer +Servers **SHOULD** issue targets that write to a staging location and enforce the declared size. +A target that writes to the final destination **MUST** enforce the declared size, and the +digest where the backend supports it. Servers **SHOULD** rely on storage-side expiry to reclaim +uploads with no retry. -For each `files/upload` request the client: +#### Transfer -1. Applies the [upload target restrictions](#upload-target-restrictions). A target that fails - them **MUST NOT** be contacted; the client treats the input request as unsatisfiable. -2. Sends one HTTPS request to `upload.url` with `upload.method`, the listed `headers`, and - either the raw bytes or the multipart body described by `form`. When `credentials` is - `"omit"` the client **MUST NOT** send an `Authorization` header or cookies. When it is - `"include"` the client sends the credential it would present to the MCP endpoint for this - server and nothing else. -3. Treats any `2xx` response as success. Any other outcome is a failed upload. +For each `UploadTarget` the client checks the target against [Upload targets](#upload-targets), +sends one PUT to `url` with `headers` and the file bytes as the body, and treats a `2xx` +response as success. Clients **MUST NOT** send more bytes than the declared `size`, and +**SHOULD** bound the number of times they repeat an upload for one call. -The party that performs step 2 is whichever component holds the bytes and, for -`credentials: "include"`, the credential. In a gateway deployment that is typically the -gateway; the host forwards the `InputRequiredResult` unchanged and the gateway satisfies the -`files/upload` requests before the retry reaches the server. Nothing in the exchange requires -the model or the host UI to see the target URL. +The component that holds the bytes performs the transfer. A gateway that holds the credential +but not the bytes can pass `omit` targets to the host, or rewrite an `include` target's `url` to +a relay at its own origin and proxy it. -#### 4. Retry with file URIs +Clients **MUST** complete the upload and retry without returning control to the model. Servers +**SHOULD NOT** echo upload URLs or issued file URIs in the `CallToolResult`. -The client retries `tools/call` with the original arguments, the file argument now set to -the issued file URI (or an array of them), an `inputResponses` entry for each `files/upload` -key, and `requestState` echoed verbatim: +Servers that issue `omit` targets to browser clients **SHOULD** configure the storage to answer +the CORS preflight for the listed headers. + +#### Retry + +The client retries `tools/call` with the same tool name and arguments, each placeholder replaced +by its file URI, and `requestState` echoed: ```json { @@ -376,299 +323,173 @@ key, and `requestState` echoed verbatim: "method": "tools/call", "params": { "name": "save_to_folder", - "arguments": { - "folderId": "fld_8c1a", - "fileName": "Q3 report.pdf", - "file": "mcp-file:01JD3V7K2M" - }, - "inputResponses": { - "u1": { "uri": "mcp-file:01JD3V7K2M", "status": "uploaded" } - }, + "arguments": { "folderId": "fld_8c1a", "file": "mcp-file:01JD3V7K2M" }, "requestState": "...", "_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28", - "...": "..." + "io.modelcontextprotocol/clientCapabilities": { + "extensions": { "io.modelcontextprotocol/files": {} } + } } } } ``` -```typescript -interface FilesUploadResult { - uri: string; - status: "uploaded" | "failed"; - /** Present when status is "failed"; free text for logs, not shown to the model. */ - error?: string; -} -``` +If an upload failed, the client leaves the placeholder in that argument and sends that file's +entry in `pending` again. + +On the retry the server: + +1. Verifies `requestState` and returns JSON-RPC error `-32602` if verification fails. +2. Rejects the call if the tool name differs from the one bound in `requestState`. If the + non-file arguments differ, the server either rejects the call or re-runs every metadata check + on the new arguments. +3. If any placeholder remains, returns a new `upload_required` covering only the pending + pointers, or a `CallToolResult` with `isError: true`. Servers **MUST NOT** run the tool while + a placeholder remains. +4. Confirms with the storage backend that each file's bytes arrived (for example with a HEAD + request or a session status query), verifies `digest` where the backend can do so without + reading the bytes back, and re-checks size and type. Servers **MUST NOT** run the tool with a + file whose bytes they have not confirmed. +5. Performs any finalize step the backend needs, runs the tool, and returns its result. + +A file URI is valid only in a retry carrying a `requestState` that binds it. A server that +answers a retry with `upload_required` or `input_required` **MUST** bind every file URI it has +not consumed into the new `requestState`. A file URI is consumed when the server starts running +the tool, and servers **MUST** reject a consumed file URI. Failures before that point do not +consume it. + +### Upload targets + +The server chooses the upload target, and with `credentials: "include"` the client attaches a +user credential to a request the server directed. These rules apply to every target: + +1. Clients **MUST** reject a `url` that is not an absolute `https` URL. +2. Clients **MUST NOT** connect to an address that the + [IANA special-purpose registries](https://www.rfc-editor.org/rfc/rfc6890) mark as not + globally reachable, checked on the address they connect to, unless an operator allowlist + permits it. Clients **MUST NOT** follow redirects. +3. Clients **MUST** reject a target whose `headers` include `Authorization`, `Cookie`, a + [forbidden request header](https://fetch.spec.whatwg.org/#forbidden-request-header), or a + name or value that is not valid under [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110#section-5). +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 + deployment this is the canonical URI of the server whose credential is attached. Otherwise + the client **MUST** treat the target as `"omit"` or refuse it. + +On stdio there is no canonical URI, so only `omit` targets apply. -On receipt the server verifies `requestState`, confirms that each file URI in the arguments -was issued under that state and that the bytes arrived (and match `digest` when one was -given), performs any storage-side finalize step, runs the tool, and returns an ordinary -`CallToolResult`. The model sees that result and nothing of the exchange that produced it. +## Rationale -If an upload is reported `failed`, did not arrive, or the target expired, the server -**SHOULD** return a fresh `InputRequiredResult` with new targets when a retry is plausible, -and otherwise a tool error. Servers **MUST NOT** run the tool with a file argument whose -bytes they have not confirmed. +### Why an extension result type -#### Sequence +SEP-2631 negotiated uploads in a separate `files/authorizeUpload` call before `tools/call`. The +server had to issue a regional upload URL before it saw the arguments, so it could not check the +destination or quota first, and the two calls had no protocol-level link. Returning the upload +target from `tools/call` itself lets the server see the full call first, and `requestState` +carries the binding so any instance can serve the retry. -```mermaid -sequenceDiagram - participant H as Host or gateway - participant S as MCP server - participant T as Upload target +An earlier version of this draft carried the target as a new `files/upload` entry in an +`InputRequiredResult`. The MRTR rules and the schema allow only elicitation, sampling, and roots +requests there, so that design needed a change to the specification. The base protocol lets +extensions add result types, and the Tasks extension does so. The cost is that a server cannot +ask for an upload and an elicitation in the same round trip. - H->>S: tools/call {args without file, _meta.files.pending} - alt rejected on metadata - S-->>H: CallToolResult {isError: true} - else accepted - S-->>H: InputRequiredResult {files/upload: target, file URI; requestState} - H->>T: PUT bytes (credentials per target) - T-->>H: 2xx - H->>S: tools/call {args with file URI, inputResponses, requestState} - S-->>H: CallToolResult - end -``` +### Why a placeholder and `_meta` -For a file at or below the inline ceiling the client skips all of this and sends a single -`tools/call` with a `data:` URI in the argument. - -### Upload target restrictions - -The upload target is chosen by the server, and in the `credentials: "include"` case the -client attaches a user credential to a request the server directed. Clients and gateways -therefore enforce the following independently of anything the server returns: - -1. `upload.url` **MUST** be an absolute `https` URL. Clients **MUST** reject any other - scheme. -2. Clients **MUST** reject targets whose host resolves to a loopback, link-local, private - ([RFC 1918](https://www.rfc-editor.org/rfc/rfc1918), [RFC 4193](https://www.rfc-editor.org/rfc/rfc4193)), - or otherwise non-globally-routable address, unless the MCP server itself was reached at - such an address. -3. Clients **MUST** restrict targets to an origin set established out of band for that - server: an operator or registry allowlist where one exists, and otherwise the MCP - endpoint's own origin. The server's advertised `uploadOrigins` **MAY** narrow that set and - **MUST NOT** widen it; a client with no out-of-band allowlist **MAY** treat - `uploadOrigins` as advisory input to a user- or operator-facing consent decision but - **MUST NOT** treat it as sufficient on its own. -4. `credentials: "include"` is honored only when the target's origin equals the MCP - endpoint's origin. For any other origin the client **MUST** behave as if `credentials` - were `"omit"`, and **MAY** refuse the target. -5. Clients **MUST NOT** follow HTTP redirects from an upload target. - -These rules exist so that a compromised or malicious server cannot use a host or gateway as -a request proxy into networks it cannot otherwise reach, and cannot harvest a user's MCP -credential by pointing an upload at itself under another name. - -### Error handling - -- Validation failures the model can act on (wrong type, too large, destination not - writable, quota) are tool errors: `CallToolResult` with `isError: true` and text naming the - argument and the constraint, per SEP-1303. -- A retry whose `requestState` fails verification, or whose `inputResponses` is structurally - malformed, is a JSON-RPC `-32602`. -- A retry that references a file URI the server did not issue under the presented - `requestState` **MUST** be rejected; servers **SHOULD** use a tool error so the host can - restart the flow. -- Digest mismatch after upload **SHOULD** be reported distinctly from other failures. - -Servers, clients, gateways, and SDKs **MUST** treat `data:` values in `x-mcp-file` arguments -and upload target URLs as sensitive in logs and telemetry, redacting bytes and query strings. - -### Changes to the core specification - -The MRTR pattern currently enumerates the request types permitted in `inputRequests` -(`elicitation/create`, `sampling/createMessage`, `roots/list`). This extension requires one -sentence added to that list: a server **MAY** also include a request type defined by an -extension the client declared on the originating request. No other core text changes. +SDKs and gateways can validate `arguments` against `inputSchema` before a handler runs, so the +initial call cannot omit a required file argument. A placeholder passes that validation and +marks the argument. The metadata goes in request `_meta` under the extension's key because the +argument's schema is a URI string and cannot hold an object. -## Rationale +### Why `omit` and `include` -### Why multi-round-trip requests instead of `files/*` methods - -SEP-2631 put upload negotiation in a separate `files/authorizeUpload` call made before -`tools/call`. That ordering means the server authorizes bytes without seeing the arguments -they belong to, so it cannot check the destination, apply quota, or pick a storage region -until after the transfer. It also means two requests that the protocol does not tie together; -a server has to invent its own correlation, and an intermediary has to learn a new method. - -Folding the negotiation into `InputRequiredResult` fixes all three. The server sees the full -call before issuing a target. The issued file URI, the validated arguments, and the expiry -ride in `requestState`, so the retry is self-contained and any instance can serve it. And -the only wire artifacts are a `tools/call`, a result, and another `tools/call`, which every -gateway and SDK built for `2026-07-28` already forwards. - -The working group considered and set aside a dedicated primitive for files. Tools plus MRTR -cover the collected use cases; a new primitive can come later if they stop doing so. - -### Why metadata rides in `_meta` on the first call - -The file argument's schema is a URI string, so it cannot hold a metadata object, and -inventing a placeholder URI that encodes name and size would be a second wire format for the -same information. Request `_meta` under the extension's own key is already where -per-request extension data lives, it survives SDK argument validation, and it keeps -`arguments` schema-valid on both the initial call and the retry. This is listed under -[Open Questions](#open-questions) in case reviewers prefer a different placement. - -### Why keep `x-mcp-file` and `data:` - -SEP-2356's reasoning for an inline schema keyword over a sidecar map, and for `data:` as the -inline carrier, still holds and is not repeated here. Reusing both means a server written -against 2356 gains out-of-band transfer by declaring the extension and handling one new -input request type; its tool schemas do not change. - -### Why `credentials: omit | include` and the same-origin rule - -The working group heard two firm and opposite requirements: storage providers that issue -pre-signed URLs and do not want a bearer token anywhere near them, and enterprise -deployments where pre-authorized URLs are unacceptable and the data plane has to be -authorized with the same token as the control plane. A per-target flag covers both without -the client guessing. Restricting `include` to the MCP endpoint's origin is the narrowest rule -that serves the enterprise case (the server fronts its own upload path) while guaranteeing -the token never reaches a third party. Widening it, for example to any origin listed in the -server's Protected Resource Metadata, is an open question. - -A third mode, where the upload target needs a credential distinct from the MCP one (a -storage provider with its own OAuth), is real but deferred; it likely composes with URL-mode -elicitation or a second authorization flow and does not change the shapes here. - -### Why a single request and no resumability in this revision - -No resumable-upload protocol has broad enough adoption to mandate. tus and the -[httpbis resumable upload draft](https://datatracker.ietf.org/doc/draft-ietf-httpbis-resumable-upload/) -are both candidates; pinning an extension to an Internet-Draft is premature. A single PUT or -POST handles files into the low hundreds of megabytes on ordinary connections, which covers -the collected use cases. Resumability can be added as a negotiated `UploadTarget` variant -without touching anything specified here. - -### Why host-enforced target restrictions rather than server-declared ones - -A server-declared allowlist is chosen by the same party that chooses the URL, so on its own -it constrains nothing. The restrictions section makes the client or gateway the enforcing -party and lets `uploadOrigins` only narrow. This matches how hosts already treat other -server-supplied URLs (OAuth metadata, URL-mode elicitation) and is the control that makes -`credentials: "include"` safe to offer at all. - -### Why uploads only - -Downloads touch different host code paths (how a result reaches the user and the model), do -not have the metadata-first problem, and already have partial answers in `resource_link`, -`EmbeddedResource`, and -[SEP-2532](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2532). The -working group is treating them as the same track, and the `UploadTarget` shape is written so -a `DownloadTarget` can mirror it, but specifying both at once would double the review surface -for the first revision. +The working group heard that sending the user's credential has to be optional, so a token never +reaches third-party storage, and that some enterprise deployments reject pre-signed URLs and +want the data plane authorized with the same token as the control plane. A per-target flag +covers both. Tying `include` to the server's canonical URI keeps the token within the resource +it was issued for. The group has not finished listing the authorization methods it needs to +support, and this pair is a starting point. ### Prior art -- SEP-2356 (declaration keyword, inline encoding, host-side rules), closed in favor of 2631 - and revived here for the parts this draft depends on. -- SEP-2631 (file URIs, digest, upload and download descriptors, `maxSize` semantics), which - this draft expects to supersede for the upload direction. -- SEP-2532 (resource streaming) and SEP-2571 (resource submission), adjacent work in the same - group. -- OpenAI Apps SDK `openai/fileParams`, a vendor `_meta` hint naming file-valued parameters. -- Storage-provider APIs that already separate "create upload session" from "send bytes" - (Google Drive resumable upload, Box upload sessions, Dropbox temporary upload links, - S3 and GCS signed URLs). The `files/upload` input request is shaped so a server can wrap - any of these. +- SEP-2356, for the declaration keyword, inline encoding, and host rules. +- SEP-2631, for file URIs, digests, transfer descriptors, and the `maxSize` change. +- [SEP-2532](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2532) (resource + streaming), which Patrick Rodgers offered as a way to get file bytes directly from a server. +- Storage APIs that separate creating an upload from sending the bytes (pre-signed URLs and + upload sessions). ## Backward Compatibility -The extension is additive and off by default. A client that does not declare it never sees a -`files/upload` input request. A server that does not advertise it receives `data:` URIs up -to whatever `maxSize` it declares, exactly as under SEP-2356, or no file support at all. -Tools that today take ad hoc base64 or URL string arguments keep working and gain nothing -until their authors adopt `x-mcp-file`. +The extension is additive and off by default. A client that does not declare it never receives +`upload_required`. A server that does not advertise it receives `data:` URIs up to its declared +`maxSize`. Tools that take base64 or URL strings today keep working. -The one core edit (permitting extension-defined request types in `inputRequests` when the -client declared the extension) does not change behavior for any existing client or server. +SDKs that validate `tools/call` results against `CallToolResult | InputRequiredResult` need a +hook for extension result types before they can carry `upload_required`. ## Security Implications -The threat model has three parties that do not fully trust each other: the user (represented -by the host), the MCP server, and whatever storage sits behind the upload target. - -**Server-directed requests from the client.** The upload target is an instruction from the -server to make an HTTP request. [Upload target restrictions](#upload-target-restrictions) -bound where that request can go and what credential it can carry. Implementations that relax -those rules reintroduce server-side request forgery through the client or gateway and -credential disclosure to third-party origins. - -**User consent and file provenance.** Everything SEP-2356 says about host-side file access -applies: the host binds a file to an `x-mcp-file` slot only on explicit user selection for -that server and tool, or forwards a model-supplied `data:` value under its existing approval -policy without dereferencing it. Hosts **SHOULD** show the user the file identity, size, and -destination server before the initial call, since after that point the transfer proceeds -without further interaction. The server receives no provenance signal and **MUST NOT** assume -a file was user-selected rather than model-authored. - -**Replay and substitution.** `requestState` binds the issued file URIs to the validated -arguments, the principal, and an expiry; servers **MUST** integrity-protect it as the MRTR -pattern requires and **MUST** refuse file URIs not issued under the presented state. Without -this a client could upload under one call's authorization and attach the file to another. -Digests, when the client supplies them, let the server detect byte substitution between -metadata and upload. - -**Untrusted content.** Servers are receiving arbitrary bytes and **MUST** apply the file -handling controls appropriate to the formats they accept; +[Upload targets](#upload-targets) covers requests the server directs the client to make. +Relaxing those rules allows server-side request forgery through the client and disclosure of +the user's credential to other resources. + +Binding the tool name, arguments, and file URIs in `requestState`, and consuming each file URI +once, prevents a client from uploading under one call's authorization and attaching the file to +another call or a different destination. A target that enforces the declared size and digest +prevents anyone who sees an `omit` URL from replacing the bytes before the retry. + +Servers **SHOULD** follow [OWASP ASVS 5.0 V5](https://github.com/OWASP/ASVS/blob/v5.0.0/5.0/en/0x14-V5-File-Handling.md) -is the reference. Tool results derived from uploaded content are untrusted with respect to -the model and hosts **MUST** present them as data. +for the formats they accept. -**Logging.** Upload URLs frequently embed signatures; `data:` values embed user bytes. Both -**MUST** be redacted in logs, traces, and error reports at every hop. +Clients and servers **MUST** redact upload URLs, upload headers, and `data:` values in logs, +traces, and error reports. ## Reference Implementation -To be built in this repository before the draft is submitted as a SEP. The plan is a TypeScript SDK branch adding -the `files/upload` input request type and client-side transfer helper, a reference server -that fronts an S3-compatible store, and a conformance scenario covering inline, out-of-band, -early rejection, expired target, and digest mismatch. +The reference implementation will include a TypeScript SDK branch with the `upload_required` +result type and a client transfer helper, a reference server in front of an object store, and +conformance scenarios for inline transfer, out-of-band transfer, rejection on metadata, a failed +upload, and a digest mismatch. ## Open Questions -1. **Placement of pending-file metadata.** Request `_meta` under the extension key (as - specified), a reserved placeholder URI in the argument slot, or a sibling field on - `CallToolRequestParams`. -2. **Multipart in the first revision.** Raw-body PUT/POST covers signed-URL stores; several - provider APIs want `multipart/form-data` with extra fields. The `form` member is included - above; reviewers may prefer to defer it. -3. **Scope of `credentials: "include"`.** Same origin as the MCP endpoint (as specified), or - any origin the server's Protected Resource Metadata names as the same protected resource. -4. **Mandating the `mcp-file:` scheme** for issued file URIs versus leaving the scheme to the - server with opacity rules only. -5. **Per-argument transfer mode.** SEP-2631's `transferModes` let a server force out-of-band - for a specific field. This draft relies on `maxInlineBytes` and `maxSize` instead; a server - that never wants inline sets `maxInlineBytes: 0`. -6. **Explicit finalize.** Some backends need a commit call after the bytes land. As written - the server performs it while handling the retry; reviewers from those backends should - confirm that is sufficient. -7. **Elicitation-surface uploads.** An `x-mcp-file` field inside an `elicitation/create` - form is itself delivered through `inputRequests`; supporting out-of-band upload there - means a nested negotiation. This draft leaves elicitation file fields inline-only. -8. **Client-reported upload evidence.** Whether `FilesUploadResult` should carry the - target's response status, `ETag`, or body for stores that need it to finalize. +1. **`upload_required` or an input request.** An input request inside `InputRequiredResult` + would let a server combine an upload with an elicitation in one round trip, at the cost of a + change to the specification. Is that worth it? +2. **Mandatory digest.** Requiring `digest` closes the byte-replacement gap for `omit` targets, + but browsers cannot hash a large file incrementally with Web Crypto. +3. **Authorization methods.** The working group agreed to list every method it has to support, + including storage that needs a credential separate from the MCP one and HTTP Message + Signatures. +4. **Finalize.** As written the server finalizes while handling the retry. Owners of backends + with an explicit commit step should confirm that works for them. +5. **How the tool's typed result reaches the model**, raised at the October 2 meeting. +6. **stdio.** Out-of-band `omit` targets work for stdio servers. Should anything more be defined + for them? +7. **Resources.** Do resources fit this flow? Thomas Sarlandie took this as an action item on + October 2. +8. **The `mcp-file:` scheme.** Require it for issued file URIs, or leave the scheme to the + server under the opacity rules. +9. **Multipart and POST.** Some storage APIs take only `multipart/form-data`. They can sit + behind a server proxy with `include`, or a later revision can add a `form` target. ## Future Work -- **Downloads.** A `DownloadTarget` mirroring `UploadTarget`, attached to `resource_link` - content or returned through a `files/download` input request on `resources/read`, with the - same origin and credential rules. -- **Resumable and chunked transfer** as an additional negotiated target type once a standard - settles. -- **Third-party storage credentials**, where the upload target needs an authorization the - MCP credential does not cover. -- **Promotion to the core specification** after the extension has shipped in SDKs and at - least two independent hosts, per the working group's October 2 decision to validate in use - first. +- **Downloads.** A download target that mirrors `UploadTarget`, with the same rules. +- **Chunked and resumable transfer**, for backends with per-request size limits and for + interrupted transfers, once a standard settles. +- **Elicitation forms.** Out-of-band upload for `x-mcp-file` fields in `elicitation/create`. +- **Promotion to the specification**, after the working group has validated the extension in + use, per its October 2 decision. ## Acknowledgments -This draft consolidates work by Olivier Chafik (SEP-2356) and Casey Chow (SEP-2631), and the -discussion at the Files Working Group's October 2, 2026 meeting with Wil St. Charles, Kurtis -Van Gent, Thomas Sarlandie, and Kryspin Ziemski. Thomas Sarlandie proposed modeling the -upload as a multi-round-trip request. Patrick Rodgers' SEP-2532 informed the deferred -download direction. +This draft builds on SEP-2356 by Olivier Chafik and SEP-2631 by Casey Chow, and on the File +Uploads Working Group's October 2, 2026 meeting with Wil St. Charles, Kurtis Van Gent, Thomas +Sarlandie, and Kryspin Ziemski. Thomas Sarlandie proposed modeling the upload as a +multi-round-trip request. From de0661f513e4ad412ae70129d66a341c5b8d9d9a Mon Sep 17 00:00:00 2001 From: Den Delimarsky Date: Wed, 14 Oct 2026 06:29:51 +0000 Subject: [PATCH 03/19] Tighten draft wording --- specification/draft/file-uploads.md | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/specification/draft/file-uploads.md b/specification/draft/file-uploads.md index 49a9782..d1e9bfb 100644 --- a/specification/draft/file-uploads.md +++ b/specification/draft/file-uploads.md @@ -7,9 +7,7 @@ - **Discussion**: [File Uploads Working Group Discord thread](https://discord.com/channels/1358869848138059966/1551968557292593172/1552399008440520865) and issues in this repository -This is the working draft of the extension, written in SEP form so the working group can submit -it as an Extensions Track SEP once a reference implementation exists. Anything here can change -until then, including the extension identifier. +The working group writes this draft in SEP form so it can submit it as an Extensions Track SEP once a reference implementation exists. Anything here can change before then, including the extension identifier. ## Abstract @@ -22,8 +20,7 @@ metadata. If it accepts, it returns an `upload_required` result with an HTTPS up a server-issued file URI. The client uploads the bytes and retries the call with the file URI in place of the placeholder. Small files go inline as RFC 2397 `data:` URIs. -This revision covers uploads only. Downloads, chunked and resumable transfer, and file fields in -elicitation forms are listed under [Future Work](#future-work). +This revision covers uploads. [Future Work](#future-work) lists downloads, chunked and resumable transfer, and file fields in elicitation forms. ## Motivation @@ -49,13 +46,13 @@ The same meeting recorded the host-side constraints: - Small files stay in band to keep latency down. - Large files go out of band to avoid base64. - Uploads work when a gateway holds the OAuth credentials instead of the host. -- File pointers on the wire are marked so a deterministic proxy can find them. +- File pointers on the wire carry a marker a deterministic proxy can find. SEP-2356 defined how a server declares a file argument and an inline `data:` encoding, and sent larger files to URL-mode elicitation instead of defining an upload protocol. It closed without merging. [SEP-2631](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2631) added out-of-band transfer through new `files/authorizeUpload` and `files/authorizeDownload` methods. -Thomas Sarlandie called ordering SEP-2631's biggest flaw: bytes move before the server sees the +Thomas Sarlandie called ordering SEP-2631's biggest flaw. Bytes move before the server sees the rest of the call, so uploads that permissions, quota, or data residency rules would reject still consume bandwidth and can land in the wrong region. The group agreed on metadata first, then the upload target, then bytes, then finalize, and agreed to build on multi-round-trip requests (MRTR) @@ -104,7 +101,7 @@ did not declare the extension. ### File arguments -SEP-2356 closed without merging, so this section restates the parts of it this extension uses. +This section restates the parts of SEP-2356 this extension uses. A server marks a file argument by adding `x-mcp-file` to a property of the form `{"type": "string", "format": "uri"}` in `Tool.inputSchema`, including array items: @@ -387,15 +384,13 @@ On stdio there is no canonical URI, so only `omit` targets apply. SEP-2631 negotiated uploads in a separate `files/authorizeUpload` call before `tools/call`. The server had to issue a regional upload URL before it saw the arguments, so it could not check the -destination or quota first, and the two calls had no protocol-level link. Returning the upload -target from `tools/call` itself lets the server see the full call first, and `requestState` +destination or quota first, and the two calls had no protocol-level link. Returning the upload target from `tools/call` lets the server see the full call first, and `requestState` carries the binding so any instance can serve the retry. An earlier version of this draft carried the target as a new `files/upload` entry in an `InputRequiredResult`. The MRTR rules and the schema allow only elicitation, sampling, and roots requests there, so that design needed a change to the specification. The base protocol lets -extensions add result types, and the Tasks extension does so. The cost is that a server cannot -ask for an upload and an elicitation in the same round trip. +extensions add result types, and the Tasks extension does so. A server then cannot ask for an upload and an elicitation in the same round trip. ### Why a placeholder and `_meta` @@ -418,7 +413,7 @@ support, and this pair is a starting point. - SEP-2356, for the declaration keyword, inline encoding, and host rules. - SEP-2631, for file URIs, digests, transfer descriptors, and the `maxSize` change. - [SEP-2532](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2532) (resource - streaming), which Patrick Rodgers offered as a way to get file bytes directly from a server. + streaming), which Patrick Rodgers offered as a way to get file bytes from a server. - Storage APIs that separate creating an upload from sending the bytes (pre-signed URLs and upload sessions). @@ -434,8 +429,7 @@ hook for extension result types before they can carry `upload_required`. ## Security Implications [Upload targets](#upload-targets) covers requests the server directs the client to make. -Relaxing those rules allows server-side request forgery through the client and disclosure of -the user's credential to other resources. +A client that relaxes those rules lets a server use it for server-side request forgery and can leak the user's credential to other resources. Binding the tool name, arguments, and file URIs in `requestState`, and consuming each file URI once, prevents a client from uploading under one call's authorization and attaching the file to @@ -462,7 +456,7 @@ upload, and a digest mismatch. would let a server combine an upload with an elicitation in one round trip, at the cost of a change to the specification. Is that worth it? 2. **Mandatory digest.** Requiring `digest` closes the byte-replacement gap for `omit` targets, - but browsers cannot hash a large file incrementally with Web Crypto. + but Web Crypto cannot hash a large file in chunks, so browsers have to read the whole file first. 3. **Authorization methods.** The working group agreed to list every method it has to support, including storage that needs a credential separate from the MCP one and HTTP Message Signatures. From 348c794ff07be13ec874cd1f74b586f402081fed Mon Sep 17 00:00:00 2001 From: thomas Date: Tue, 20 Oct 2026 08:35:38 -0700 Subject: [PATCH 04/19] Keep file metadata in tool arguments across upload retries (#2) 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. --- README.md | 8 +- specification/draft/file-uploads.md | 277 +++++++++++++--------------- 2 files changed, 134 insertions(+), 151 deletions(-) diff --git a/README.md b/README.md index 68c68f2..f1f4a3c 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,9 @@ stay out of model context and out of JSON-RPC messages. Maintained by the ## Overview -A server marks file arguments in a tool's input schema. To send a file too large to inline, the -client calls the tool with a placeholder in the file argument and the file's metadata in request -`_meta`. The server rejects the call or returns an `upload_required` result with an upload -target. The client uploads the bytes to that target and retries the call with the file URI the -server issued. Small files go inline as `data:` URIs. +A server marks file arguments in a tool's input schema. Each file argument contains its metadata +and optional base64 data. When data is absent, the server rejects the call or requests an upload. +The client uploads the bytes and retries with unchanged arguments and per-file outcomes. The draft is in [`specification/draft/file-uploads.md`](specification/draft/file-uploads.md). The reference implementation will live in this repository. diff --git a/specification/draft/file-uploads.md b/specification/draft/file-uploads.md index d1e9bfb..83c06c6 100644 --- a/specification/draft/file-uploads.md +++ b/specification/draft/file-uploads.md @@ -14,11 +14,9 @@ The working group writes this draft in SEP form so it can submit it as an Extens This extension lets a client send a file to a tool without passing a large file's bytes through the model or a JSON-RPC message. A server marks file arguments with the `x-mcp-file` keyword first proposed in [SEP-2356](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2356). -To send a file too large to inline, the client calls the tool with a placeholder in the file -argument and the file's metadata in request `_meta`. The server can reject the call on that -metadata. If it accepts, it returns an `upload_required` result with an HTTPS upload target and -a server-issued file URI. The client uploads the bytes and retries the call with the file URI in -place of the placeholder. Small files go inline as RFC 2397 `data:` URIs. +The client calls the tool with the file's metadata in the file argument and optional base64 data. +If data is absent, the server can reject the call on the metadata or request an upload to an HTTPS +target. The client uploads the bytes and retries with the same arguments and a per-file outcome. This revision covers uploads. [Future Work](#future-work) lists downloads, chunked and resumable transfer, and file fields in elicitation forms. @@ -89,22 +87,21 @@ A server that accepts out-of-band uploads advertises the extension in `server/di } ``` -`maxInlineBytes` is the largest total decoded size of `data:` values the server accepts in one -request. If the server omits it, clients **SHOULD** use out-of-band transfer for any file larger +`maxInlineBytes` is the largest total decoded size of inline `data` values the server accepts in +one request. If the server omits it, clients **SHOULD** use out-of-band transfer for any file larger than a small, implementation-defined size. -Clients **MUST NOT** send a placeholder or pending-file metadata to a server that did not -advertise the extension. Servers report an oversized `data:` value as a tool error, following +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), and **MAY** return `MissingRequiredClientCapabilityError` (`-32021`) instead when the request did not declare the extension. ### File arguments -This section restates the parts of SEP-2356 this extension uses. - -A server marks a file argument by adding `x-mcp-file` to a property of the form -`{"type": "string", "format": "uri"}` in `Tool.inputSchema`, including array items: +A server marks a file argument by adding `x-mcp-file` to an object schema in `Tool.inputSchema`, +including array items. The keyword's value has the `FileInputDescriptor` shape, and the marked +schema **MUST** require the `FileInput` shape below: ```typescript interface FileInputDescriptor { @@ -113,29 +110,35 @@ interface FileInputDescriptor { /** Largest decoded size of each file, in bytes, on any transfer path. */ maxSize?: number; } + +interface FileInput { + /** Unique per file argument in this tool call. */ + id: string; + name?: string; + /** "application/octet-stream" if unknown. */ + mimeType: string; + /** Non-negative integer; decoded size in bytes. */ + size: number; + digest: { algorithm: "sha-256"; value: string /* base64url, no padding */ }; + /** RFC 4648 base64-encoded file bytes; absent for out-of-band uploads. */ + data?: string; +} ``` SEP-2356 applied `maxSize` to inline values only. This extension applies it to every file, as SEP-2631 proposed. -An `x-mcp-file` value is one of: - -- An RFC 2397 `data:` URI. Clients use `application/octet-stream` when the media type is - unknown. -- A placeholder beginning with `mcp-file:pending`, on the initial call of an out-of-band upload. - Clients **MAY** append a suffix (`mcp-file:pending:2`) to keep array items distinct. -- A file URI the server issued in an `upload_required` result, on the retry. - -Servers **MUST** reject any other value and **MUST NOT** dereference it. Clients **MUST** treat -file URIs as opaque and **MUST NOT** parse, resolve, or pass a file URI to any URI handler. -Servers **SHOULD** mint file URIs under the `mcp-file:` scheme so proxies can recognize them. +The `id` identifies a file argument within the call and **MUST** be unique in that call. The +digest is SHA-256 over the original bytes. Present `data` carries the file inline (`""` carries an +empty file); absent `data` requests out-of-band upload. Servers **MUST** validate the decoded +size and digest before using the bytes. Hosts: - **SHOULD NOT** show `x-mcp-file` properties to a model that emits arguments as tokens. - **MUST** bind a file to an argument only on the user's selection or under the host's approval policy for model-supplied values. -- **MUST NOT** place file bytes, upload URLs, or issued file URIs in model context. +- **MUST NOT** place file bytes or upload URLs in model context. ### Upload flow @@ -145,23 +148,23 @@ sequenceDiagram participant S as MCP server participant T as Upload target - C->>S: tools/call {file: "mcp-file:pending"}, _meta pending + C->>S: tools/call with file metadata in arguments alt rejected on metadata S-->>C: CallToolResult {isError: true} else accepted - S-->>C: upload_required {uploads, requestState} + S-->>C: input_required {inputRequests, requestState} C->>T: PUT bytes T-->>C: 2xx - C->>S: tools/call {file: "mcp-file:01JD..."}, requestState + C->>S: same arguments + requestState + inputResponses S-->>C: CallToolResult end ``` #### Initial call -The client sends `tools/call` with every argument populated, a placeholder in each out-of-band -file argument or array item, and the pending files' metadata in request `_meta` under the -extension identifier: +The client sends `tools/call` with every argument populated and omits `data` for each file it +will upload out of band. The examples omit standard request `_meta` fields for readability; +the base protocol and [Negotiation](#negotiation) requirements still apply. ```json { @@ -172,24 +175,14 @@ extension identifier: "name": "save_to_folder", "arguments": { "folderId": "fld_8c1a", - "file": "mcp-file:pending" - }, - "_meta": { - "io.modelcontextprotocol/protocolVersion": "2026-07-28", - "io.modelcontextprotocol/clientCapabilities": { - "extensions": { "io.modelcontextprotocol/files": {} } - }, - "io.modelcontextprotocol/files": { - "pending": { - "/file": { - "name": "Q3 report.pdf", - "mimeType": "application/pdf", - "size": 41204133, - "digest": { - "algorithm": "sha-256", - "value": "uU0nuZNNPgilLlLX2n2r-sSE7-N6U4D6ZVe-_rYh2sU" - } - } + "file": { + "id": "file-1", + "name": "Q3 report.pdf", + "mimeType": "application/pdf", + "size": 41204133, + "digest": { + "algorithm": "sha-256", + "value": "uU0nuZNNPgilLlLX2n2r-sSE7-N6U4D6ZVe-_rYh2sU" } } } @@ -197,52 +190,41 @@ extension identifier: } ``` -Each key in `pending` is a [JSON Pointer](https://www.rfc-editor.org/rfc/rfc6901) to one -placeholder in `arguments` (`/file`, or `/files/2` for an array item). An array argument can mix -inline `data:` items and placeholders. - -```typescript -interface FileMetadata { - name?: string; - /** "application/octet-stream" if unknown. */ - mimeType: string; - /** Decoded size in bytes. */ - size: number; - digest?: { algorithm: "sha-256"; value: string /* base64url, no padding */ }; -} -``` - -Clients **SHOULD** include `digest`. Before sending the initial call, the host **MUST** obtain -the user's approval, or apply operator policy when no user is present, for sending the file to -this server and to storage the server designates. +Before sending the initial call, the host **MUST** obtain the user's approval, or apply operator +policy when no user is present, for sending the file to this server and to storage the server +designates. Servers **SHOULD** run every check that does not need the bytes (destination authorization, quota, `accept` and `maxSize`, name collisions) and return a `CallToolResult` with `isError: true` if one fails. Servers **MUST** treat the metadata as client-asserted and re-check -size and type once the bytes arrive. +size, digest, and type once the bytes arrive. #### Upload required -If the server accepts the files, it returns a result with `resultType: "upload_required"`, an -extension-defined result type as the -[base protocol](https://modelcontextprotocol.io/specification/draft/basic/index#resulttype) -permits. This extension reserves the value `"upload_required"`. +If the server accepts the files, it returns an `InputRequiredResult` with one upload input +request under `inputRequests.uploads`. The method below is proposed by this extension; using it +requires a core MRTR extension point because the current input-request union is closed. ```json { "jsonrpc": "2.0", "id": 1, "result": { - "resultType": "upload_required", - "uploads": { - "/file": { - "uri": "mcp-file:01JD3V7K2M", - "url": "https://upload.example.com/u/01JD3V7K2M?sig=...", - "headers": { - "Content-Type": "application/pdf", - "x-amz-checksum-sha256": "uU0nuZNNPgilLlLX2n2r+sSE7+N6U4D6ZVe+/rYh2sU=" - }, - "credentials": "omit" + "resultType": "input_required", + "inputRequests": { + "uploads": { + "method": "io.modelcontextprotocol/files/upload", + "params": { + "files": [{ + "fileId": "file-1", + "url": "https://upload.example.com/u/01JD3V7K2M?sig=...", + "headers": { + "Content-Type": "application/pdf", + "x-amz-checksum-sha256": "uU0nuZNNPgilLlLX2n2r+sSE7+N6U4D6ZVe+/rYh2sU=" + }, + "credentials": "omit" + }] + } } }, "requestState": "..." @@ -251,16 +233,14 @@ permits. This extension reserves the value `"upload_required"`. ``` ```typescript -interface UploadRequiredResult extends Result { - resultType: "upload_required"; - /** One entry per pending pointer. */ - uploads: { [pointer: string]: UploadTarget }; - requestState: string; +interface UploadInputRequest { + method: "io.modelcontextprotocol/files/upload"; + params: { files: UploadTarget[] }; } interface UploadTarget { - /** The file URI the client places in the argument on the retry. */ - uri: string; + /** The corresponding FileInput.id in the original arguments. */ + fileId: string; /** Absolute https URL. The client sends the bytes with PUT. */ url: string; /** Headers the client sends with the PUT. */ @@ -273,16 +253,16 @@ interface UploadTarget { } ``` -Servers **MUST** return `upload_required` only on `tools/call` and only to a request that -declared the extension. A server using the +Servers **MUST** return an upload input request only on `tools/call` and only to a request that +declared the extension. Each `fileId` **MUST** match one file argument without `data`, and each +file **MUST** have at most one outstanding target in the current round. A server using the [Tasks extension](https://modelcontextprotocol.io/extensions/tasks/overview) **MUST** return -`upload_required` before creating a task. +the upload input request before creating a task. -Servers **MUST** bind the issued file URIs, the tool name, and the other arguments into -`requestState`, which follows the +Servers **MUST** bind the authenticated principal, tool name, all arguments, and the current +target for each file to `requestState`, which follows the [MRTR server rules](https://modelcontextprotocol.io/specification/draft/basic/patterns/mrtr#server-requirements-basic-workflow). -When computing the request digest that MRTR recommends, servers **MUST** exclude the values at -the pointers bound in `requestState`. Clients handle `requestState` as the +Clients handle `requestState` as the [MRTR client rules](https://modelcontextprotocol.io/specification/draft/basic/patterns/mrtr#client-requirements-basic-workflow) require for `InputRequiredResult`. @@ -294,24 +274,26 @@ uploads with no retry. #### Transfer For each `UploadTarget` the client checks the target against [Upload targets](#upload-targets), -sends one PUT to `url` with `headers` and the file bytes as the body, and treats a `2xx` +sends a PUT to `url` with `headers` and the file bytes as the body, and treats a `2xx` response as success. Clients **MUST NOT** send more bytes than the declared `size`, and -**SHOULD** bound the number of times they repeat an upload for one call. +**MUST** limit retryable transfer failures to three total HTTP attempts per file per logical call. +Clients **MUST** stop sooner if a failure is permanent or retrying is unsafe. The component that holds the bytes performs the transfer. A gateway that holds the credential but not the bytes can pass `omit` targets to the host, or rewrite an `include` target's `url` to a relay at its own origin and proxy it. Clients **MUST** complete the upload and retry without returning control to the model. Servers -**SHOULD NOT** echo upload URLs or issued file URIs in the `CallToolResult`. +**SHOULD NOT** echo upload URLs in the `CallToolResult`. Servers that issue `omit` targets to browser clients **SHOULD** configure the storage to answer the CORS preflight for the listed headers. #### Retry -The client retries `tools/call` with the same tool name and arguments, each placeholder replaced -by its file URI, and `requestState` echoed: +The client retries `tools/call` with a new JSON-RPC ID, the same tool name and exact arguments, +the echoed `requestState`, and one outcome for each requested file under +`inputResponses.uploads`: ```json { @@ -320,41 +302,50 @@ by its file URI, and `requestState` echoed: "method": "tools/call", "params": { "name": "save_to_folder", - "arguments": { "folderId": "fld_8c1a", "file": "mcp-file:01JD3V7K2M" }, + "arguments": { + "folderId": "fld_8c1a", + "file": { + "id": "file-1", + "name": "Q3 report.pdf", + "mimeType": "application/pdf", + "size": 41204133, + "digest": { + "algorithm": "sha-256", + "value": "uU0nuZNNPgilLlLX2n2r-sSE7-N6U4D6ZVe-_rYh2sU" + } + } + }, "requestState": "...", - "_meta": { - "io.modelcontextprotocol/protocolVersion": "2026-07-28", - "io.modelcontextprotocol/clientCapabilities": { - "extensions": { "io.modelcontextprotocol/files": {} } + "inputResponses": { + "uploads": { + "resultType": "complete", + "files": [{ "fileId": "file-1", "status": "completed" }] } } } } ``` -If an upload failed, the client leaves the placeholder in that argument and sends that file's -entry in `pending` again. +`resultType: "complete"` means the upload input request was answered; each file's `status` is +`completed` if the client observed a successful transfer, or `failed` if it did not. A failed +file ends this logical call. Clients **MUST NOT** include upload URLs or credentials in the response. On the retry the server: 1. Verifies `requestState` and returns JSON-RPC error `-32602` if verification fails. -2. Rejects the call if the tool name differs from the one bound in `requestState`. If the - non-file arguments differ, the server either rejects the call or re-runs every metadata check - on the new arguments. -3. If any placeholder remains, returns a new `upload_required` covering only the pending - pointers, or a `CallToolResult` with `isError: true`. Servers **MUST NOT** run the tool while - a placeholder remains. +2. Rejects the call if the tool name or any argument differs from the values bound in `requestState`. +3. Rejects duplicate or unknown file IDs and unknown statuses. If any file has status `failed`, + returns a `CallToolResult` with `isError: true` without running the tool. A missing outcome is + unresolved; the server follows MRTR error handling and **MUST NOT** run the tool. 4. Confirms with the storage backend that each file's bytes arrived (for example with a HEAD - request or a session status query), verifies `digest` where the backend can do so without - reading the bytes back, and re-checks size and type. Servers **MUST NOT** run the tool with a - file whose bytes they have not confirmed. + request or a session status query), verifies `digest`, and re-checks size and type. Servers + **MUST NOT** run the tool with a file whose bytes they have not confirmed. 5. Performs any finalize step the backend needs, runs the tool, and returns its result. -A file URI is valid only in a retry carrying a `requestState` that binds it. A server that -answers a retry with `upload_required` or `input_required` **MUST** bind every file URI it has -not consumed into the new `requestState`. A file URI is consumed when the server starts running -the tool, and servers **MUST** reject a consumed file URI. Failures before that point do not -consume it. +The echoed state and `fileId` identify the current target. A replacement target requires a new +state, and the server **MUST** prevent the old target from satisfying the new round. Servers +**MUST NOT** accept stale state, use the same target for a different call, or execute a call +twice on a repeated continuation. ### Upload targets @@ -380,24 +371,23 @@ On stdio there is no canonical URI, so only `omit` targets apply. ## Rationale -### Why an extension result type +### Why an upload input request SEP-2631 negotiated uploads in a separate `files/authorizeUpload` call before `tools/call`. The server had to issue a regional upload URL before it saw the arguments, so it could not check the destination or quota first, and the two calls had no protocol-level link. Returning the upload target from `tools/call` lets the server see the full call first, and `requestState` carries the binding so any instance can serve the retry. -An earlier version of this draft carried the target as a new `files/upload` entry in an -`InputRequiredResult`. The MRTR rules and the schema allow only elicitation, sampling, and roots -requests there, so that design needed a change to the specification. The base protocol lets -extensions add result types, and the Tasks extension does so. A server then cannot ask for an upload and an elicitation in the same round trip. +An upload input request can share the MRTR envelope with other inputs, but the current MRTR +rules and schema allow only elicitation, sampling, and roots requests. This proposal therefore +requires a change to the specification before clients and servers can interoperate. -### Why a placeholder and `_meta` +### Why a structured file argument SDKs and gateways can validate `arguments` against `inputSchema` before a handler runs, so the -initial call cannot omit a required file argument. A placeholder passes that validation and -marks the argument. The metadata goes in request `_meta` under the extension's key because the -argument's schema is a URI string and cannot hold an object. +initial call cannot omit a required file argument. A structured value satisfies that schema and +keeps the file's metadata together, without a parallel request `_meta` map or a placeholder URI. +Omitting `data` distinguishes out-of-band upload from inline transfer. ### Why `omit` and `include` @@ -420,19 +410,17 @@ support, and this pair is a starting point. ## Backward Compatibility The extension is additive and off by default. A client that does not declare it never receives -`upload_required`. A server that does not advertise it receives `data:` URIs up to its declared -`maxSize`. Tools that take base64 or URL strings today keep working. +an upload input request. Tools that take base64 or URL strings today keep working. -SDKs that validate `tools/call` results against `CallToolResult | InputRequiredResult` need a -hook for extension result types before they can carry `upload_required`. +SDKs need an MRTR extension point before they can carry the proposed upload input request. ## Security Implications [Upload targets](#upload-targets) covers requests the server directs the client to make. A client that relaxes those rules lets a server use it for server-side request forgery and can leak the user's credential to other resources. -Binding the tool name, arguments, and file URIs in `requestState`, and consuming each file URI -once, prevents a client from uploading under one call's authorization and attaching the file to +Binding the principal, tool name, arguments, and upload targets to `requestState` prevents a +client from uploading under one call's authorization and attaching the file to another call or a different destination. A target that enforces the declared size and digest prevents anyone who sees an `omit` URL from replacing the bytes before the retry. @@ -440,23 +428,22 @@ Servers **SHOULD** follow [OWASP ASVS 5.0 V5](https://github.com/OWASP/ASVS/blob/v5.0.0/5.0/en/0x14-V5-File-Handling.md) for the formats they accept. -Clients and servers **MUST** redact upload URLs, upload headers, and `data:` values in logs, +Clients and servers **MUST** redact upload URLs, upload headers, and inline `data` values in logs, traces, and error reports. ## Reference Implementation -The reference implementation will include a TypeScript SDK branch with the `upload_required` -result type and a client transfer helper, a reference server in front of an object store, and +The reference implementation will include a TypeScript SDK branch with the upload input request +and a client transfer helper, a reference server in front of an object store, and conformance scenarios for inline transfer, out-of-band transfer, rejection on metadata, a failed upload, and a digest mismatch. ## Open Questions -1. **`upload_required` or an input request.** An input request inside `InputRequiredResult` - would let a server combine an upload with an elicitation in one round trip, at the cost of a - change to the specification. Is that worth it? +1. **MRTR extensibility.** How should the specification enable extension-defined input requests? 2. **Mandatory digest.** Requiring `digest` closes the byte-replacement gap for `omit` targets, but Web Crypto cannot hash a large file in chunks, so browsers have to read the whole file first. + Is that tradeoff acceptable? 3. **Authorization methods.** The working group agreed to list every method it has to support, including storage that needs a credential separate from the MCP one and HTTP Message Signatures. @@ -467,9 +454,7 @@ upload, and a digest mismatch. for them? 7. **Resources.** Do resources fit this flow? Thomas Sarlandie took this as an action item on October 2. -8. **The `mcp-file:` scheme.** Require it for issued file URIs, or leave the scheme to the - server under the opacity rules. -9. **Multipart and POST.** Some storage APIs take only `multipart/form-data`. They can sit +8. **Multipart and POST.** Some storage APIs take only `multipart/form-data`. They can sit behind a server proxy with `include`, or a later revision can add a `form` target. ## Future Work From 1b9fc108a7b0fc027e19d48fb346f991d02b78f9 Mon Sep 17 00:00:00 2001 From: Den Delimarsky Date: Wed, 21 Oct 2026 07:09:23 +0000 Subject: [PATCH 05/19] Allow gateways to relay any upload target 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. --- specification/draft/file-uploads.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/specification/draft/file-uploads.md b/specification/draft/file-uploads.md index 83c06c6..4c65b23 100644 --- a/specification/draft/file-uploads.md +++ b/specification/draft/file-uploads.md @@ -279,9 +279,9 @@ response as success. Clients **MUST NOT** send more bytes than the declared `siz **MUST** limit retryable transfer failures to three total HTTP attempts per file per logical call. Clients **MUST** stop sooner if a failure is permanent or retrying is unsafe. -The component that holds the bytes performs the transfer. A gateway that holds the credential -but not the bytes can pass `omit` targets to the host, or rewrite an `include` target's `url` to -a relay at its own origin and proxy it. +The component that holds the bytes performs the transfer. A gateway **MAY** replace a target's +`url`, `headers`, and `credentials` with a relay at its own origin. The gateway then checks the +original target against [Upload targets](#upload-targets) and forwards the bytes to it. Clients **MUST** complete the upload and retry without returning control to the model. Servers **SHOULD NOT** echo upload URLs in the `CallToolResult`. From 6101d34eb52b21d4b87028543ecd1fd59ed3d5df Mon Sep 17 00:00:00 2001 From: Den Delimarsky Date: Wed, 21 Oct 2026 14:50:42 +0000 Subject: [PATCH 06/19] Propose a download shape 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. --- specification/draft/file-uploads.md | 74 +++++++++++++++++++++++++++-- 1 file changed, 71 insertions(+), 3 deletions(-) diff --git a/specification/draft/file-uploads.md b/specification/draft/file-uploads.md index 4c65b23..f25ce5b 100644 --- a/specification/draft/file-uploads.md +++ b/specification/draft/file-uploads.md @@ -18,7 +18,7 @@ The client calls the tool with the file's metadata in the file argument and opti If data is absent, the server can reject the call on the metadata or request an upload to an HTTPS target. The client uploads the bytes and retries with the same arguments and a per-file outcome. -This revision covers uploads. [Future Work](#future-work) lists downloads, chunked and resumable transfer, and file fields in elicitation forms. +This revision covers uploads and proposes a shape for [downloads](#downloads). [Future Work](#future-work) lists chunked and resumable transfer and file fields in elicitation forms. ## Motivation @@ -369,6 +369,63 @@ user credential to a request the server directed. These rules apply to every tar On stdio there is no canonical URI, so only `omit` targets apply. +### Downloads + +> **Note:** The working group has not reviewed this section yet. + +A tool returns a file as a +[`resource_link`](https://modelcontextprotocol.io/specification/draft/server/tools#resource-links) +in `content`. The link describes the file with the base protocol's `name`, `mimeType`, and `size`. +The extension adds a download target in the link's `_meta`, under the extension identifier: + +```json +{ + "type": "resource_link", + "uri": "export://exports/exp_01JD3V7K2M/q3-orders.csv", + "name": "q3-orders.csv", + "mimeType": "text/csv", + "size": 209715200, + "_meta": { + "io.modelcontextprotocol/files": { + "url": "https://download.example.com/d/01JD3V9P4Q?sig=...", + "credentials": "omit" + } + } +} +``` + +```typescript +interface DownloadTarget { + /** Absolute https URL. The client fetches the bytes with GET. */ + url: string; + /** Headers the client sends with the GET. */ + headers?: { [name: string]: string }; + /** Same meaning as in UploadTarget. */ + credentials: "omit" | "include"; + /** SHA-256 over the whole file. */ + digest?: { algorithm: "sha-256"; value: string /* base64url, no padding */ }; +} +``` + +[Upload targets](#upload-targets) applies to download targets unchanged. + +Servers **MUST** add a download target only to the result of a request that declared the +extension. The `uri` identifies the file and **MUST NOT** carry a credential. It can equal `url` +when the URL holds no secret. Clients do not follow redirects, so servers resolve any backend +redirect and return the final URL. + +The client sends a GET to `url` with `headers` and treats a `200` response as success. `size` and +`digest` describe the bytes after any `Content-Encoding` is removed, and clients **MUST** check +each one that is present. A target can expire and cannot be renewed, so a client that wants the file +**SHOULD** fetch it before returning control to the model. Calling the tool again runs it again, +so clients **MUST NOT** do that automatically. + +Hosts **MUST** remove the download target before the result reaches model context. A gateway +**MAY** replace a target with a relay at its own origin. The gateway then checks the original +target, fetches the bytes from it, and serves them only to the caller. + +Small files can still travel as an embedded resource, as they do in the base protocol. + ## Rationale ### Why an upload input request @@ -398,6 +455,15 @@ covers both. Tying `include` to the server's canonical URI keeps the token withi it was issued for. The group has not finished listing the authorization methods it needs to support, and this pair is a starting point. +### Why a `resource_link` for downloads + +Upload and download share what the server hands the client to move bytes: the same `url`, `headers`, +and `credentials`, under the same target rules. They differ in where the file's description travels, +because MCP types tool arguments with `inputSchema` and returns results as content blocks, and +`resource_link` is already the block for a file. A client or gateway can find a returned file by +scanning `content`, without the tool's schema. A download needs no second call, because the server +has run the tool by the time it names the file. + ### Prior art - SEP-2356, for the declaration keyword, inline encoding, and host rules. @@ -428,7 +494,10 @@ Servers **SHOULD** follow [OWASP ASVS 5.0 V5](https://github.com/OWASP/ASVS/blob/v5.0.0/5.0/en/0x14-V5-File-Handling.md) for the formats they accept. -Clients and servers **MUST** redact upload URLs, upload headers, and inline `data` values in logs, +A URL that authorizes itself is a secret for as long as it works, so servers choose its lifetime +with that in mind. + +Clients and servers **MUST** redact target URLs, target headers, and inline `data` values in logs, traces, and error reports. ## Reference Implementation @@ -459,7 +528,6 @@ upload, and a digest mismatch. ## Future Work -- **Downloads.** A download target that mirrors `UploadTarget`, with the same rules. - **Chunked and resumable transfer**, for backends with per-request size limits and for interrupted transfers, once a standard settles. - **Elicitation forms.** Out-of-band upload for `x-mcp-file` fields in `elicitation/create`. From b22d3409250cbab0c21c0407291a277eb0001396 Mon Sep 17 00:00:00 2001 From: Den Delimarsky Date: Wed, 4 Nov 2026 07:18:35 +0000 Subject: [PATCH 07/19] Allow upload and download targets to use a path under the MCP address Co-authored-by: den --- specification/draft/file-uploads.md | 54 +++++++++++++++++++++-------- 1 file changed, 40 insertions(+), 14 deletions(-) diff --git a/specification/draft/file-uploads.md b/specification/draft/file-uploads.md index f25ce5b..685b7fe 100644 --- a/specification/draft/file-uploads.md +++ b/specification/draft/file-uploads.md @@ -241,12 +241,14 @@ interface UploadInputRequest { interface UploadTarget { /** The corresponding FileInput.id in the original arguments. */ fileId: string; - /** Absolute https URL. The client sends the bytes with PUT. */ - url: string; + /** Absolute https URL. The client sends the bytes with PUT. Exactly one of url and path is present. */ + url?: string; + /** Path under the address the client uses for MCP. The client sends the bytes with PUT. */ + path?: string; /** Headers the client sends with the PUT. */ headers?: { [name: string]: string }; /** - * "omit": the URL authorizes itself. "include": the client attaches the credential it + * "omit": the target authorizes itself. "include": the client attaches the credential it * presents to the MCP server. See Upload targets. */ credentials: "omit" | "include"; @@ -274,14 +276,16 @@ uploads with no retry. #### Transfer For each `UploadTarget` the client checks the target against [Upload targets](#upload-targets), -sends a PUT to `url` with `headers` and the file bytes as the body, and treats a `2xx` +sends a PUT to the target's `url` or resolved `path` with `headers` and the file bytes as the body, and treats a `2xx` response as success. Clients **MUST NOT** send more bytes than the declared `size`, and **MUST** limit retryable transfer failures to three total HTTP attempts per file per logical call. Clients **MUST** stop sooner if a failure is permanent or retrying is unsafe. The component that holds the bytes performs the transfer. A gateway **MAY** replace a target's -`url`, `headers`, and `credentials` with a relay at its own origin. The gateway then checks the -original target against [Upload targets](#upload-targets) and forwards the bytes to it. +`url` or `path`, `headers`, and `credentials` with a relay at its own origin. The gateway then checks the +original target against [Upload targets](#upload-targets) and forwards the bytes to it, resolving +a `path` against the address it uses for the server. A gateway that passes a `path` target through +unchanged **MUST** route that path to the server. Clients **MUST** complete the upload and retry without returning control to the model. Servers **SHOULD NOT** echo upload URLs in the `CallToolResult`. @@ -352,22 +356,31 @@ twice on a repeated continuation. The server chooses the upload target, and with `credentials: "include"` the client attaches a user credential to a request the server directed. These rules apply to every target: -1. Clients **MUST** reject a `url` that is not an absolute `https` URL. +1. Clients **MUST** reject a target that does not have exactly one of `url` and `path`, and a + `url` that is not an absolute `https` URL. 2. Clients **MUST NOT** connect to an address that the [IANA special-purpose registries](https://www.rfc-editor.org/rfc/rfc6890) mark as not globally reachable, checked on the address they connect to, unless an operator allowlist - permits it. Clients **MUST NOT** follow redirects. + permits it or the target is a `path`, which uses the address the client already connects to. + Clients **MUST NOT** follow redirects. 3. Clients **MUST** reject a target whose `headers` include `Authorization`, `Cookie`, a [forbidden request header](https://fetch.spec.whatwg.org/#forbidden-request-header), or a name or value that is not valid under [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110#section-5). -4. Clients **MUST** honor `credentials: "include"` only when `url` has the same origin as the MCP +4. Clients **MUST** honor `credentials: "include"` only on a `path` target, or 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 deployment this is the canonical URI of the server whose credential is attached. Otherwise the client **MUST** treat the target as `"omit"` or refuse it. +5. Clients **MUST** reject a `path` that does not start with exactly one `/`, or that contains a + `.` or `..` segment, a backslash, `?`, or `#`, including in percent-encoded form. -On stdio there is no canonical URI, so only `omit` targets apply. +A client resolves a `path` by adding it to the end of the path of the address it sends MCP +requests to, without a doubled `/` and before any query string on that address. With the MCP +address `https://gateway.example.com/mcp/abc`, the path `/uploads` resolves to +`https://gateway.example.com/mcp/abc/uploads`. + +On stdio there is no canonical URI or MCP address, so only `url` targets with `omit` apply. ### Downloads @@ -396,8 +409,10 @@ The extension adds a download target in the link's `_meta`, under the extension ```typescript interface DownloadTarget { - /** Absolute https URL. The client fetches the bytes with GET. */ - url: string; + /** Absolute https URL. The client fetches the bytes with GET. Exactly one of url and path is present. */ + url?: string; + /** Path under the address the client uses for MCP. The client fetches the bytes with GET. */ + path?: string; /** Headers the client sends with the GET. */ headers?: { [name: string]: string }; /** Same meaning as in UploadTarget. */ @@ -414,7 +429,7 @@ extension. The `uri` identifies the file and **MUST NOT** carry a credential. It when the URL holds no secret. Clients do not follow redirects, so servers resolve any backend redirect and return the final URL. -The client sends a GET to `url` with `headers` and treats a `200` response as success. `size` and +The client sends a GET to the target's `url` or resolved `path` with `headers` and treats a `200` response as success. `size` and `digest` describe the bytes after any `Content-Encoding` is removed, and clients **MUST** check each one that is present. A target can expire and cannot be renewed, so a client that wants the file **SHOULD** fetch it before returning control to the model. Calling the tool again runs it again, @@ -455,9 +470,19 @@ covers both. Tying `include` to the server's canonical URI keeps the token withi it was issued for. The group has not finished listing the authorization methods it needs to support, and this pair is a starting point. +### Why a `path` target + +A gateway can replace a target with a relay at its own origin, but one absolute URL assumes every +client reaches the gateway the same way. A client in a sandbox or behind a proxy can reach it +through another address and may not be able to reach the public host. Because that host's origin +differs from the one the client called, rule 4 of [Upload targets](#upload-targets) also makes the +client drop its credential or refuse the target. With `path`, each client transfers through the +address it already uses, so the bytes take the same route as its MCP calls. `url` stays for +anywhere else, such as third-party storage. + ### Why a `resource_link` for downloads -Upload and download share what the server hands the client to move bytes: the same `url`, `headers`, +Upload and download share what the server hands the client to move bytes: the same `url` or `path`, `headers`, and `credentials`, under the same target rules. They differ in where the file's description travels, because MCP types tool arguments with `inputSchema` and returns results as content blocks, and `resource_link` is already the block for a file. A client or gateway can find a returned file by @@ -484,6 +509,7 @@ SDKs need an MRTR extension point before they can carry the proposed upload inpu [Upload targets](#upload-targets) covers requests the server directs the client to make. A client that relaxes those rules lets a server use it for server-side request forgery and can leak the user's credential to other resources. +The `path` rules keep a target under the address the client already uses for MCP. Binding the principal, tool name, arguments, and upload targets to `requestState` prevents a client from uploading under one call's authorization and attaching the file to From a3b695ccd56058bc277382dc31ce900616607978 Mon Sep 17 00:00:00 2001 From: Den Delimarsky Date: Wed, 4 Nov 2026 08:58:27 +0000 Subject: [PATCH 08/19] Incorporate working group and review feedback into the draft Co-authored-by: den --- README.md | 5 +- specification/draft/file-uploads.md | 764 +++++++++++++++++++--------- 2 files changed, 532 insertions(+), 237 deletions(-) diff --git a/README.md b/README.md index f1f4a3c..042c9d7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # experimental-ext-file-uploads -Experimental MCP extension for sending files from a client to a tool out of band, so the bytes +Experimental MCP extension for moving files between a client and a tool out of band, so the bytes stay out of model context and out of JSON-RPC messages. Maintained by the [File Uploads Working Group](https://modelcontextprotocol.io/community/working-groups/file-uploads). @@ -13,7 +13,8 @@ stay out of model context and out of JSON-RPC messages. Maintained by the A server marks file arguments in a tool's input schema. Each file argument contains its metadata and optional base64 data. When data is absent, the server rejects the call or requests an upload. -The client uploads the bytes and retries with unchanged arguments and per-file outcomes. +The client uploads the bytes and retries with unchanged arguments and per-file outcomes. A tool +returns a file by listing a download target in the result's `_meta`. The draft is in [`specification/draft/file-uploads.md`](specification/draft/file-uploads.md). The reference implementation will live in this repository. diff --git a/specification/draft/file-uploads.md b/specification/draft/file-uploads.md index 685b7fe..2af95db 100644 --- a/specification/draft/file-uploads.md +++ b/specification/draft/file-uploads.md @@ -7,18 +7,23 @@ - **Discussion**: [File Uploads Working Group Discord thread](https://discord.com/channels/1358869848138059966/1551968557292593172/1552399008440520865) and issues in this repository -The working group writes this draft in SEP form so it can submit it as an Extensions Track SEP once a reference implementation exists. Anything here can change before then, including the extension identifier. +This draft uses SEP form so the working group can submit it as an Extensions Track SEP once a +reference implementation exists. Anything here, including the extension identifier, can change +before then. ## Abstract -This extension lets a client send a file to a tool without passing a large file's bytes through -the model or a JSON-RPC message. A server marks file arguments with the `x-mcp-file` keyword -first proposed in [SEP-2356](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2356). -The client calls the tool with the file's metadata in the file argument and optional base64 data. -If data is absent, the server can reject the call on the metadata or request an upload to an HTTPS -target. The client uploads the bytes and retries with the same arguments and a per-file outcome. +This extension lets a client send a file to a tool, and receive a file from one, without passing a +large file's bytes through the model or a JSON-RPC message. A server marks file arguments with the +`x-mcp-file` keyword first proposed in +[SEP-2356](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2356). The client calls +the tool with the file's metadata in the file argument, with optional base64 `data`. If `data` is +absent, the server can reject the call on the metadata or request an upload to an HTTPS target. The +client uploads the bytes and retries with the same arguments and a per-file outcome. A tool returns +a file by listing a download target in the result's `_meta`. -This revision covers uploads and proposes a shape for [downloads](#downloads). [Future Work](#future-work) lists chunked and resumable transfer and file fields in elicitation forms. +This revision covers uploads and downloads. [Future Work](#future-work) lists chunked and resumable +uploads and file fields in elicitation forms. ## Motivation @@ -46,73 +51,157 @@ The same meeting recorded the host-side constraints: - Uploads work when a gateway holds the OAuth credentials instead of the host. - File pointers on the wire carry a marker a deterministic proxy can find. -SEP-2356 defined how a server declares a file argument and an inline `data:` encoding, and sent -larger files to URL-mode elicitation instead of defining an upload protocol. It closed without -merging. [SEP-2631](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2631) added -out-of-band transfer through new `files/authorizeUpload` and `files/authorizeDownload` methods. -Thomas Sarlandie called ordering SEP-2631's biggest flaw. Bytes move before the server sees the -rest of the call, so uploads that permissions, quota, or data residency rules would reject still -consume bandwidth and can land in the wrong region. The group agreed on metadata first, then the -upload target, then bytes, then finalize, and agreed to build on multi-round-trip requests (MRTR) -and existing primitives before adding new ones. +[SEP-2356](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2356) defined how a +server declares a file argument and an inline `data:` encoding, and sent larger files to URL-mode +elicitation instead of defining an upload protocol. It closed without merging. +[SEP-2631](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2631) added out-of-band +transfer through new `files/authorizeUpload` and `files/authorizeDownload` methods. Thomas Sarlandie +identified ordering as the main problem in SEP-2631. Bytes move before the server sees the rest of +the call, so uploads that permissions, quota, or data residency rules would reject still consume +bandwidth and can land in the wrong region. The group agreed on metadata first, then the upload +target, then bytes, then finalize, and agreed to build on multi round-trip requests (MRTR) and +existing primitives before adding new ones. ## Specification -In this document, _client_ is the component that speaks MCP to the server: the host, or a -gateway acting for it. _Host_ is the application that presents the model and the user interface. +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", +"RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as +described in [BCP 14](https://datatracker.ietf.org/doc/html/bcp14) +[[RFC2119](https://datatracker.ietf.org/doc/html/rfc2119)] +[[RFC8174](https://datatracker.ietf.org/doc/html/rfc8174)] when, and only when, they appear in all +capitals, as shown here. -### Extension identifier +In this document, _client_ is the component that speaks MCP to the next hop, which can be a server +or a gateway. A host behind a gateway is a client of that gateway, and the gateway is a client of +the server. _Host_ is the application that presents the model and the user interface. _MCP address_ +is the URL a client sends MCP requests to, so it can differ per hop and can differ from the server's +canonical URI. Requirements on clients in [Transfer](#transfer), [Transfer +Targets](#transfer-targets), and [Downloads](#downloads) bind the component that performs the +transfer. -`io.modelcontextprotocol/files` +This revision applies to tools. Resources keep their base protocol behavior. + +### Extension Identifier + +This extension uses the identifier `io.modelcontextprotocol/files`. ### Negotiation -A client that can perform out-of-band uploads declares the extension, with no settings, in the -`io.modelcontextprotocol/clientCapabilities` request metadata of every request that may need it: +Clients **MUST** declare the extension in the `io.modelcontextprotocol/clientCapabilities` request +metadata of every request that may need it: ```json -{ "extensions": { "io.modelcontextprotocol/files": {} } } +{ "extensions": { "io.modelcontextprotocol/files": { "upload": {}, "download": {} } } } ``` -A server that accepts out-of-band uploads advertises the extension in `server/discover`: +Servers advertise extension support in the `server/discover` response: ```json { "capabilities": { "tools": {}, "extensions": { - "io.modelcontextprotocol/files": { "maxInlineBytes": 1048576 } + "io.modelcontextprotocol/files": { + "upload": {}, + "download": {}, + "maxInlineBytes": 1048576 + } } } } ``` -`maxInlineBytes` is the largest total decoded size of inline `data` values the server accepts in -one request. If the server omits it, clients **SHOULD** use out-of-band transfer for any file larger -than a small, implementation-defined size. +The settings object has the following fields: + +- `upload` _(optional)_: The sender supports out-of-band uploads. +- `download` _(optional)_: The sender supports out-of-band downloads. +- `maxInlineBytes` _(optional, servers only)_: The largest total decoded size of inline `data` + values the server accepts in one request. + +A missing `upload` or `download` field means that direction is not supported. Receivers **MUST** +ignore fields they do not recognize. + +Clients **MUST NOT** use this extension with a server that did not advertise it. + +A server that accepts inline `data` **MUST** advertise `maxInlineBytes`. If `maxInlineBytes` is `0` +or missing, the server does not accept inline transfer. Clients **MUST NOT** send a request whose +inline `data` values exceed `maxInlineBytes` in total, and **MUST** omit `data` from every file +argument when the server does not accept inline transfer. If such a server advertises `upload`, it +takes every file through an upload round trip, however small the file is. A client **MUST NOT** send +a file out of band to a server that did not advertise `upload`. + +For example, a server that advertises a `maxInlineBytes` of 1 MiB and marks a file argument with +a `maxSize` of 150 MiB accepts a 200 KiB file inline, and two 400 KiB files inline in one +request. It accepts a 40 MiB file out of band, and rejects a 200 MiB file on its metadata. + +If a request's inline `data` values exceed `maxInlineBytes` in total, or a request carries inline +`data` and the server does not accept inline transfer, the server **MUST** return a JSON-RPC error +with code `-32602` and `data.reason` set to `"inline_too_large"`. This is an exception to +[SEP-1303](https://modelcontextprotocol.io/seps/1303-input-validation-errors-as-tool-execution-errors); +see [Why oversized inline data is a protocol error](#why-oversized-inline-data-is-a-protocol-error). +A client that receives this error **MAY** repeat the call with `data` omitted if it declared +`upload` and the server advertised it. -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), -and **MAY** return `MissingRequiredClientCapabilityError` (`-32021`) instead when the request -did not declare the extension. +If a file argument omits `data`, the server advertises `upload`, and the request did not declare +`upload`, the server **MUST** return a [`MissingRequiredClientCapabilityError`](https://modelcontextprotocol.io/specification/draft/schema#missingrequiredclientcapabilityerror) +(`-32021`) whose `data.requiredCapabilities` lists the missing capability. A server **MUST** do the +same, before running the tool, if it knows the tool can deliver its output only as a download target +and the request did not declare `download`. -### File arguments +A server that cannot take a file inline or out of band **MUST** return a `CallToolResult` with +`isError: true`. -A server marks a file argument by adding `x-mcp-file` to an object schema in `Tool.inputSchema`, -including array items. The keyword's value has the `FileInputDescriptor` shape, and the marked -schema **MUST** require the `FileInput` shape below: +### File Arguments + +A server marks a file argument by adding `x-mcp-file` directly to the argument's object schema in +`Tool.inputSchema`, the placement core uses for `x-mcp-header`. Unlike `x-mcp-header`, `x-mcp-file` +applies to objects and can appear in array items. The keyword's value has the `FileInputDescriptor` +shape. Servers **MUST** make the marked schema require the `FileInput` shape. + +```json +{ + "name": "save_to_folder", + "description": "Save a file to a folder", + "inputSchema": { + "type": "object", + "properties": { + "folderId": { "type": "string" }, + "file": { + "type": "object", + "x-mcp-file": { "accept": ["application/pdf"], "maxSize": 157286400 }, + "properties": { + "id": { "type": "string" }, + "name": { "type": "string" }, + "mimeType": { "type": "string" }, + "size": { "type": "integer", "minimum": 0 }, + "digest": { + "type": "object", + "properties": { + "algorithm": { "const": "sha-256" }, + "value": { "type": "string" } + }, + "required": ["algorithm", "value"] + }, + "data": { "type": "string" } + }, + "required": ["id", "mimeType", "size", "digest"] + } + }, + "required": ["folderId", "file"] + } +} +``` ```typescript interface FileInputDescriptor { - /** Media types or dot-prefixed extensions to filter a file picker to. A hint only. */ + /** Media types or dot-prefixed extensions for a file picker. Servers can still reject on type. */ accept?: string[]; - /** Largest decoded size of each file, in bytes, on any transfer path. */ + /** Largest decoded size of each file, in bytes, whether inline or out of band. */ maxSize?: number; } interface FileInput { - /** Unique per file argument in this tool call. */ + /** Unique within this tool call. */ id: string; name?: string; /** "application/octet-stream" if unknown. */ @@ -125,22 +214,32 @@ interface FileInput { } ``` -SEP-2356 applied `maxSize` to inline values only. This extension applies it to every file, as -SEP-2631 proposed. +Clients that do not support this extension ignore the keyword. Validators that reject unknown +keywords by default, such as Ajv in strict mode, need to allow `x-mcp-file`. + +Clients **MUST** make each `id` unique within the call. The digest is SHA-256 over the original +bytes. When `data` is present, it carries the file inline, and `""` carries an empty file. When +`data` is absent, the client is requesting out-of-band upload. Servers **MUST** validate the decoded +size and digest before using the bytes. A server **MUST** return a `CallToolResult` with `isError: +true` for inline data that cannot be decoded or fails these checks, and JSON-RPC error `-32602` for +duplicate `id` values. -The `id` identifies a file argument within the call and **MUST** be unique in that call. The -digest is SHA-256 over the original bytes. Present `data` carries the file inline (`""` carries an -empty file); absent `data` requests out-of-band upload. Servers **MUST** validate the decoded -size and digest before using the bytes. +The host rules below apply to any file the host can read, including a file an agent created. Hosts: - **SHOULD NOT** show `x-mcp-file` properties to a model that emits arguments as tokens. +- **MAY** instead show the model a substitute parameter that takes a host-defined handle, and + replace the handle with a `FileInput` before sending the call. - **MUST** bind a file to an argument only on the user's selection or under the host's approval policy for model-supplied values. - **MUST NOT** place file bytes or upload URLs in model context. -### Upload flow +A tool whose text argument can refer to files, such as Markdown with image links, **SHOULD** take a +separate array of file arguments and relate the two through its description. How a tool's result +is presented to the model is host behavior and outside this extension. + +### Upload Flow ```mermaid sequenceDiagram @@ -153,18 +252,20 @@ sequenceDiagram S-->>C: CallToolResult {isError: true} else accepted S-->>C: input_required {inputRequests, requestState} - C->>T: PUT bytes + C->>T: PUT or POST bytes T-->>C: 2xx C->>S: same arguments + requestState + inputResponses S-->>C: CallToolResult end ``` -#### Initial call +#### Initial Call The client sends `tools/call` with every argument populated and omits `data` for each file it -will upload out of band. The examples omit standard request `_meta` fields for readability; -the base protocol and [Negotiation](#negotiation) requirements still apply. +will upload out of band. For brevity, the request examples in this document omit the `_meta` +request metadata. Every request **MUST** include the required `_meta` fields; see +[`_meta`](https://modelcontextprotocol.io/specification/draft/basic/index#meta) and +[Negotiation](#negotiation). ```json { @@ -190,20 +291,21 @@ the base protocol and [Negotiation](#negotiation) requirements still apply. } ``` -Before sending the initial call, the host **MUST** obtain the user's approval, or apply operator -policy when no user is present, for sending the file to this server and to storage the server -designates. +Before sending the initial call, the host **MUST** obtain the user's approval to send the file to +this server and to storage the server designates, or apply operator policy when no user is present. Servers **SHOULD** run every check that does not need the bytes (destination authorization, -quota, `accept` and `maxSize`, name collisions) and return a `CallToolResult` with -`isError: true` if one fails. Servers **MUST** treat the metadata as client-asserted and re-check -size, digest, and type once the bytes arrive. +quota, `accept` and `maxSize`) and return a `CallToolResult` with `isError: true` if one fails. +Whether an existing file with the same name is a conflict is up to the tool. Tools that support +conditional writes take the revision or condition as a tool argument. Servers **MUST** treat the +metadata as client-asserted and re-check size, digest, and type once the bytes arrive. -#### Upload required +#### Upload Required -If the server accepts the files, it returns an `InputRequiredResult` with one upload input -request under `inputRequests.uploads`. The method below is proposed by this extension; using it -requires a core MRTR extension point because the current input-request union is closed. +If the server accepts the files, it returns an `InputRequiredResult` with one upload input request +under `inputRequests.uploads`. This extension proposes the method below. Using it requires a core +MRTR extension point, because the current input-request and input-response unions are closed. The +fixed `uploads` key also differs from core, where `inputRequests` keys are server-assigned. ```json { @@ -215,15 +317,18 @@ requires a core MRTR extension point because the current input-request union is "uploads": { "method": "io.modelcontextprotocol/files/upload", "params": { - "files": [{ - "fileId": "file-1", - "url": "https://upload.example.com/u/01JD3V7K2M?sig=...", - "headers": { - "Content-Type": "application/pdf", - "x-amz-checksum-sha256": "uU0nuZNNPgilLlLX2n2r+sSE7+N6U4D6ZVe+/rYh2sU=" - }, - "credentials": "omit" - }] + "files": [ + { + "fileId": "file-1", + "url": "https://upload.example.com/u/01JD3V7K2M?sig=...", + "headers": { + "Content-Type": "application/pdf", + "x-amz-checksum-sha256": "uU0nuZNNPgilLlLX2n2r+sSE7+N6U4D6ZVe+/rYh2sU=" + }, + "credentials": "omit", + "expiresAt": "2026-11-04T18:30:00Z" + } + ] } } }, @@ -241,62 +346,109 @@ interface UploadInputRequest { interface UploadTarget { /** The corresponding FileInput.id in the original arguments. */ fileId: string; - /** Absolute https URL. The client sends the bytes with PUT. Exactly one of url and path is present. */ + /** Absolute https URL. Exactly one of url and path is present. */ url?: string; - /** Path under the address the client uses for MCP. The client sends the bytes with PUT. */ + /** Path under the MCP address. Exactly one of url and path is present. */ path?: string; - /** Headers the client sends with the PUT. */ + /** HTTP method for the transfer. Defaults to "PUT". */ + method?: "PUT" | "POST"; + /** Headers the client sends with the transfer. */ headers?: { [name: string]: string }; /** * "omit": the target authorizes itself. "include": the client attaches the credential it - * presents to the MCP server. See Upload targets. + * presents to the MCP server. See Transfer Targets. */ credentials: "omit" | "include"; + /** ISO 8601 time. The latest time to start the transfer. A hint; see Transfer. */ + expiresAt?: string; } ``` Servers **MUST** return an upload input request only on `tools/call` and only to a request that -declared the extension. Each `fileId` **MUST** match one file argument without `data`, and each -file **MUST** have at most one outstanding target in the current round. A server using the +declared `upload`. Servers **MUST** make each `fileId` match one file argument without `data`, and +**MUST NOT** give a file more than one outstanding target in the current round. A server using the [Tasks extension](https://modelcontextprotocol.io/extensions/tasks/overview) **MUST** return the upload input request before creating a task. -Servers **MUST** bind the authenticated principal, tool name, all arguments, and the current -target for each file to `requestState`, which follows the -[MRTR server rules](https://modelcontextprotocol.io/specification/draft/basic/patterns/mrtr#server-requirements-basic-workflow). -Clients handle `requestState` as the +Servers **MUST** bind the tool name, all arguments, the current target for each file, and the +authenticated principal, if any, to `requestState`, and **MUST** handle `requestState` as the +[MRTR server rules](https://modelcontextprotocol.io/specification/draft/basic/patterns/mrtr#server-requirements-basic-workflow) +require. Clients **MUST** handle `requestState` as the [MRTR client rules](https://modelcontextprotocol.io/specification/draft/basic/patterns/mrtr#client-requirements-basic-workflow) require for `InputRequiredResult`. -Servers **SHOULD** issue targets that write to a staging location and enforce the declared size. -A target that writes to the final destination **MUST** enforce the declared size, and the -digest where the backend supports it. Servers **SHOULD** rely on storage-side expiry to reclaim -uploads with no retry. +Servers **MUST NOT** let a target outlive the latest `requestState` it was issued under. MRTR +recommends a short expiry for `requestState`. A server issuing targets for large files **SHOULD** +choose a `requestState` lifetime that covers a transfer started at `expiresAt`, plus the retry. -#### Transfer +#### Staged Uploads + +Servers **SHOULD** issue targets that write to a staging location and that enforce the declared +size. + +The server owns the lifetime and cleanup of bytes it staged: + +- Servers **MUST** keep staged bytes available until the latest `requestState` bound to them + expires, the call ends, or their target is replaced, whichever comes first. +- Servers **MUST NOT** let a staged file appear as a finished file until they confirm it on the + retry. +- Servers **SHOULD** delete staged bytes when a call fails or a target is replaced, and **SHOULD** + use storage-side expiry to reclaim staged bytes that receive no retry. +- A server with several instances **MUST** let any instance that receives the retry confirm the + staged bytes. +- Servers **SHOULD** stop further writes through a target before verifying its bytes, and + **SHOULD** finalize the bytes they verified. -For each `UploadTarget` the client checks the target against [Upload targets](#upload-targets), -sends a PUT to the target's `url` or resolved `path` with `headers` and the file bytes as the body, and treats a `2xx` -response as success. Clients **MUST NOT** send more bytes than the declared `size`, and -**MUST** limit retryable transfer failures to three total HTTP attempts per file per logical call. -Clients **MUST** stop sooner if a failure is permanent or retrying is unsafe. +A target that writes to the final destination creates the file before the server has confirmed it. +Such a target **MUST** enforce the declared size, and the digest where the backend supports it. +Servers **SHOULD NOT** issue such a target unless the backend enforces the declared size and digest +on the write and the write cannot replace existing content. If a later check fails, the client +reports `failed`, or no retry arrives before the `requestState` expires, the server **MUST** remove +that file. -The component that holds the bytes performs the transfer. A gateway **MAY** replace a target's -`url` or `path`, `headers`, and `credentials` with a relay at its own origin. The gateway then checks the -original target against [Upload targets](#upload-targets) and forwards the bytes to it, resolving -a `path` against the address it uses for the server. A gateway that passes a `path` target through -unchanged **MUST** route that path to the server. +#### Transfer + +For each `UploadTarget` the client checks the target against [Transfer Targets](#transfer-targets), +sends the request to the target's `url` or resolved `path` with `method`, `headers`, and the file +bytes as the body, and treats a `2xx` response as success. Clients **MUST NOT** send more bytes +than the declared `size`. + +`expiresAt` is a hint; the target decides whether to accept a transfer. Clients **SHOULD NOT** start +a transfer after `expiresAt`, and **MUST** report that file as `failed` if they do not start it. + +Clients **MUST** limit transfer attempts to three per file per logical call (the initial call and +its retries). Each request sent to a target counts as an attempt, including one that answers a +repeated upload input request. A client at this limit **MUST** report that file as `failed` on the +retry. Clients **MUST** stop sooner if a failure is permanent or retrying is unsafe. Clients **MUST +NOT** resend a `POST` on their own unless they know the request did not reach the target. A transfer +that answers a repeated upload input request is not a resend. + +The component that holds the bytes performs the transfer. A gateway **MAY** replace a target's `url` +or `path`, `method`, `headers`, and `credentials` with a relay at its own origin, and **MUST** do so +for a `url` target with `credentials: "include"` when it holds the credential for that server. A +gateway that does so **MUST** check the original target against [Transfer +Targets](#transfer-targets) and forward the bytes to it with the original target's `method`, +`headers`, and `credentials`, resolving a `path` against the address it uses for the server. The +relay **MUST** accept bytes only from the caller, and **MUST NOT** answer `2xx` until the original +target has. + +A gateway that presents more than one server at one MCP address **MUST** replace every `path` +target. Any other gateway that passes a `path` target through unchanged **MUST** route everything +under that MCP address to that one server, **MUST NOT** forward the caller's credential upstream, +and **MUST** attach its own credential for the server when the target has `credentials: "include"`. Clients **MUST** complete the upload and retry without returning control to the model. Servers **SHOULD NOT** echo upload URLs in the `CallToolResult`. +If the user cancels, the client **SHOULD** retry with status `failed` so the server can clean up. + Servers that issue `omit` targets to browser clients **SHOULD** configure the storage to answer the CORS preflight for the listed headers. #### Retry -The client retries `tools/call` with a new JSON-RPC ID, the same tool name and exact arguments, -the echoed `requestState`, and one outcome for each requested file under +When retrying `tools/call`, the client **MUST** send a new JSON-RPC `id`, the same tool name and +exact arguments, the exact `requestState` value, and one outcome for each requested file under `inputResponses.uploads`: ```json @@ -319,180 +471,305 @@ the echoed `requestState`, and one outcome for each requested file under } } }, - "requestState": "...", "inputResponses": { "uploads": { - "resultType": "complete", "files": [{ "fileId": "file-1", "status": "completed" }] } - } + }, + "requestState": "..." } } ``` -`resultType: "complete"` means the upload input request was answered; each file's `status` is -`completed` if the client observed a successful transfer, or `failed` if it did not. A failed -file ends this logical call. Clients **MUST NOT** include upload URLs or credentials in the response. - -On the retry the server: - -1. Verifies `requestState` and returns JSON-RPC error `-32602` if verification fails. -2. Rejects the call if the tool name or any argument differs from the values bound in `requestState`. -3. Rejects duplicate or unknown file IDs and unknown statuses. If any file has status `failed`, - returns a `CallToolResult` with `isError: true` without running the tool. A missing outcome is - unresolved; the server follows MRTR error handling and **MUST NOT** run the tool. -4. Confirms with the storage backend that each file's bytes arrived (for example with a HEAD - request or a session status query), verifies `digest`, and re-checks size and type. Servers - **MUST NOT** run the tool with a file whose bytes they have not confirmed. -5. Performs any finalize step the backend needs, runs the tool, and returns its result. - -The echoed state and `fileId` identify the current target. A replacement target requires a new -state, and the server **MUST** prevent the old target from satisfying the new round. Servers -**MUST NOT** accept stale state, use the same target for a different call, or execute a call -twice on a repeated continuation. - -### Upload targets +```typescript +interface UploadInputResponse { + files: { fileId: string; status: "completed" | "failed" }[]; +} +``` -The server chooses the upload target, and with `credentials: "include"` the client attaches a -user credential to a request the server directed. These rules apply to every target: +Each file's `status` is `completed` if the client observed a successful transfer, or `failed` if it +did not. A failed file ends this logical call. Clients **MUST NOT** include upload URLs or +credentials in the response. + +The steps below apply to a retry that answers an upload input request. A server that issues a later +round **MUST** bind the confirmed files to the new `requestState` and **MUST NOT** ask for their +outcome again. + +On the retry the server **MUST** perform these steps in order: + +1. Verify `requestState`, and return JSON-RPC error `-32602` if verification fails. +2. Return JSON-RPC error `-32602` if the tool name or any argument differs from the values bound in + `requestState`. +3. Claim `requestState` where [Repeated Retries](#repeated-retries) calls for it. If it is already + claimed, follow that section. +4. Return JSON-RPC error `-32602` for duplicate or unknown `fileId` values and unknown statuses. If + any file has status `failed`, return a `CallToolResult` with `isError: true` without running the + tool. If an outcome is missing, follow + [MRTR error handling](https://modelcontextprotocol.io/specification/draft/basic/patterns/mrtr#error-handling) + and do not run the tool. +5. Confirm with the storage backend that each file's bytes arrived (for example with a `HEAD` + request or a session status query), verify `digest`, and re-check size and type. The server + **MUST NOT** run the tool with a file whose bytes it has not confirmed. + - If the bytes have not arrived, the server **MAY** return another `InputRequiredResult` with a + new `requestState` and a new or reissued target for the file. It is not required to wait for + the bytes to become visible first. Otherwise it returns a `CallToolResult` with + `isError: true` without running the tool. + - If the bytes arrived but fail a check, the server returns a `CallToolResult` with + `isError: true` without running the tool. +6. Perform any finalize step the backend needs, run the tool, and return its result. + +A repeated upload input request can cost a full re-upload, including when the bytes arrived but +the backend had not yet made them visible. + +The echoed `requestState` and `fileId` identify the current target. A new round requires a new +`requestState`. A server **MAY** reissue the same target under it. If the server issues a different +target, it **MUST** prevent the old target from satisfying the new round. Servers **MUST NOT** use +the same target for a different call. + +#### Repeated Retries + +A client can repeat a retry if it does not receive the response. Servers **SHOULD** claim a +`requestState` atomically before running the tool, using storage every instance shares, and **MUST** +do so if running the tool twice is unsafe. A claimed `requestState` counts as processed even while +the tool is running. + +A server that receives a claimed `requestState` **MUST NOT** run the tool again. It **MUST** either +return a JSON-RPC error with code `-32602` and `data.reason` set to `"already_processed"`, or return +the original result to the same principal. If the earlier retry produced an `InputRequiredResult`, +the server **SHOULD** return that result. + +A client that receives `"already_processed"` **MUST NOT** treat it as proof that the call failed, +and **MUST NOT** repeat the call automatically. The same applies to a repeated retry that fails +`requestState` verification. In both cases the outcome of the earlier attempt is unknown. + +### Transfer Targets + +The server chooses each target, and with `credentials: "include"` the client attaches a user +credential to a request the server directed. These rules apply to every upload and download target: 1. Clients **MUST** reject a target that does not have exactly one of `url` and `path`, and a `url` that is not an absolute `https` URL. -2. Clients **MUST NOT** connect to an address that the - [IANA special-purpose registries](https://www.rfc-editor.org/rfc/rfc6890) mark as not - globally reachable, checked on the address they connect to, unless an operator allowlist - permits it or the target is a `path`, which uses the address the client already connects to. - Clients **MUST NOT** follow redirects. +2. Clients **MUST** check the address they connect to, and **MUST NOT** connect to one that the + [IANA special-purpose registries](https://www.rfc-editor.org/rfc/rfc6890) mark as not globally + reachable, unless an operator allowlist permits it or the target is a `path`, which uses the + address the client already connects to. A client that cannot observe the address, such as a + browser or a client behind a forward proxy, **MUST** rely on a platform or proxy control that + enforces the same restriction, or **MUST** reject `url` targets not on an operator allowlist. 3. Clients **MUST** reject a target whose `headers` include `Authorization`, `Cookie`, a [forbidden request header](https://fetch.spec.whatwg.org/#forbidden-request-header), or a name or value that is not valid under [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110#section-5). -4. Clients **MUST** honor `credentials: "include"` only on a `path` target, or 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 - deployment this is the canonical URI of the server whose credential is attached. Otherwise - the client **MUST** treat the target as `"omit"` or refuse it. -5. Clients **MUST** reject a `path` that does not start with exactly one `/`, or that contains a - `.` or `..` segment, a backslash, `?`, or `#`, including in percent-encoded form. - -A client resolves a `path` by adding it to the end of the path of the address it sends MCP -requests to, without a doubled `/` and before any query string on that address. With the MCP -address `https://gateway.example.com/mcp/abc`, the path `/uploads` resolves to -`https://gateway.example.com/mcp/abc/uploads`. - -On stdio there is no canonical URI or MCP address, so only `url` targets with `omit` apply. + On a `path` target or a target with `credentials: "include"`, clients **MUST** also reject + `Forwarded`, `X-Original-URL`, `X-Rewrite-URL`, `X-HTTP-Method-Override`, and any name that + starts with `X-Forwarded-`. +4. Clients **MUST** honor `credentials: "include"` only on a `path` target, or when `url` has the + same origin as the MCP server's + [canonical URI](https://modelcontextprotocol.io/specification/draft/basic/authorization#canonical-server-uri), + compared as an [RFC 6454](https://www.rfc-editor.org/rfc/rfc6454) origin. A parent domain or + wildcard does not match. In a gateway deployment, the comparison uses the canonical URI of the + server whose credential is attached. Otherwise the client **MUST** treat the target as `omit` or + reject it. +5. Clients **MUST** reject a `path` that does not match `1*( "/" segment )`, where `segment` is one + or more ASCII letters, digits, `-`, `_`, `~`, or `.`, and is neither `.` nor `..`. +6. Clients **MUST** reject a target with a missing or unrecognized `credentials` value, or an + unrecognized `method` value. + +A client resolves a `path` by adding it to the end of the path of the MCP address, without a +doubled `/` and before any query string on that address. With the MCP address +`https://gateway.example.com/mcp/abc`, the path `/uploads` resolves to +`https://gateway.example.com/mcp/abc/uploads`. A `path` target inherits the scheme of the MCP +address. Clients **MUST** reject a `path` target over `http`, except on loopback. + +Hosts **MAY** restrict `omit` targets to an allowlist of origins. + +Servers **SHOULD** issue targets that answer without a redirect. Clients **MUST NOT** follow +redirects, and **MUST** treat a `3xx` response as a failed transfer. + +On stdio there is no canonical URI or MCP address, so servers **MUST** issue only `url` targets +with `credentials: "omit"`. ### Downloads -> **Note:** The working group has not reviewed this section yet. +> **Note**: The working group discussed an earlier form of this section on October 30, 2026, and +> has not reviewed this form yet. -A tool returns a file as a -[`resource_link`](https://modelcontextprotocol.io/specification/draft/server/tools#resource-links) -in `content`. The link describes the file with the base protocol's `name`, `mimeType`, and `size`. -The extension adds a download target in the link's `_meta`, under the extension identifier: +A tool returns files by listing them in the `_meta` of its `CallToolResult`, under the extension +identifier. The server **MUST** also describe the result in `content`, so that a host can tell the +model what it returned: ```json { - "type": "resource_link", - "uri": "export://exports/exp_01JD3V7K2M/q3-orders.csv", - "name": "q3-orders.csv", - "mimeType": "text/csv", - "size": 209715200, - "_meta": { - "io.modelcontextprotocol/files": { - "url": "https://download.example.com/d/01JD3V9P4Q?sig=...", - "credentials": "omit" + "jsonrpc": "2.0", + "id": 3, + "result": { + "resultType": "complete", + "content": [{ "type": "text", "text": "Exported q3-orders.csv." }], + "_meta": { + "io.modelcontextprotocol/files": { + "downloads": [ + { + "id": "export-1", + "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" + } + ] + } } } } ``` ```typescript +interface ResultFilesMeta { + downloads: DownloadTarget[]; +} + interface DownloadTarget { - /** Absolute https URL. The client fetches the bytes with GET. Exactly one of url and path is present. */ + /** Unique within this result. */ + id: string; + name?: string; + /** "application/octet-stream" if unknown. */ + mimeType: string; + /** Non-negative integer; size in bytes. */ + size?: number; + /** SHA-256 over the whole file. */ + digest?: { algorithm: "sha-256"; value: string /* base64url, no padding */ }; + /** Absolute https URL. Exactly one of url and path is present. */ url?: string; - /** Path under the address the client uses for MCP. The client fetches the bytes with GET. */ + /** Path under the MCP address. Exactly one of url and path is present. */ path?: string; - /** Headers the client sends with the GET. */ + /** Headers the client sends with the `GET`. */ headers?: { [name: string]: string }; /** Same meaning as in UploadTarget. */ credentials: "omit" | "include"; - /** SHA-256 over the whole file. */ - digest?: { algorithm: "sha-256"; value: string /* base64url, no padding */ }; + /** Same meaning as in UploadTarget. */ + expiresAt?: string; } ``` -[Upload targets](#upload-targets) applies to download targets unchanged. +A tool returns several files as several entries. -Servers **MUST** add a download target only to the result of a request that declared the -extension. The `uri` identifies the file and **MUST NOT** carry a credential. It can equal `url` -when the URL holds no secret. Clients do not follow redirects, so servers resolve any backend -redirect and return the final URL. +Servers **MUST** add a download target only to the result of a tool call whose `tools/call` request +declared `download`, however that result is delivered. -The client sends a GET to the target's `url` or resolved `path` with `headers` and treats a `200` response as success. `size` and -`digest` describe the bytes after any `Content-Encoding` is removed, and clients **MUST** check -each one that is present. A target can expire and cannot be renewed, so a client that wants the file -**SHOULD** fetch it before returning control to the model. Calling the tool again runs it again, -so clients **MUST NOT** do that automatically. +The client sends a `GET` to the target's `url` or resolved `path` with `headers`. A `200` response +is a success, and so is a `206` response to a range request. Clients **MAY** use range requests to +split or resume a download. -Hosts **MUST** remove the download target before the result reaches model context. A gateway -**MAY** replace a target with a relay at its own origin. The gateway then checks the original -target, fetches the bytes from it, and serves them only to the caller. +Servers **SHOULD** include `size` when they know it. `size` and `digest` describe the whole file +after any `Content-Encoding` is removed. Once they have the whole file, clients **MUST** check each +one that is present, and **MUST** discard a file that fails a check. + +Clients **MUST** treat `name`, `mimeType`, `size`, and the bytes as untrusted. Clients **MUST NOT** +use `name` as a filesystem path without reducing it to a safe base name. Clients **MUST** abort a +transfer once the decoded bytes exceed `size`, and **SHOULD** apply their own limit when `size` is +absent. + +Clients **SHOULD NOT** start a download after `expiresAt`. A target can expire and cannot be +renewed, so a client that wants the file, and whose user or operator policy allows fetching it from +this server, **SHOULD** fetch it before returning control to the model. A second call runs the tool +again, so clients **MUST NOT** repeat the call automatically. + +Hosts **MUST NOT** place download targets in model context. The gateway rules in +[Transfer](#transfer) apply to download targets. A relay for a download **MUST** check the original +target, fetch the bytes from it, and serve them only to the caller. + +A server **MUST NOT** list a download target until the file can be fetched. How a tool waits for a +file to exist is outside this extension. Small files can still travel as an embedded resource, as they do in the base protocol. ## Rationale -### Why an upload input request +### Why an Upload Input Request SEP-2631 negotiated uploads in a separate `files/authorizeUpload` call before `tools/call`. The server had to issue a regional upload URL before it saw the arguments, so it could not check the -destination or quota first, and the two calls had no protocol-level link. Returning the upload target from `tools/call` lets the server see the full call first, and `requestState` -carries the binding so any instance can serve the retry. +destination or quota first, and the two calls had no protocol-level link. Returning the upload +target from `tools/call` lets the server see the full call first, and `requestState` carries the +binding, so any instance that can reach the staged bytes, and the shared claim record if the server +keeps one, can serve the retry. + +An upload input request can share the MRTR envelope with other inputs, but the current MRTR rules +and schema allow only elicitation, sampling, and roots requests. This extension therefore requires a +change to the specification before clients and servers can interoperate. -An upload input request can share the MRTR envelope with other inputs, but the current MRTR -rules and schema allow only elicitation, sampling, and roots requests. This proposal therefore -requires a change to the specification before clients and servers can interoperate. +### Why a Schema Keyword -### Why a structured file argument +`x-mcp-file` sits on the argument it describes, as `x-mcp-header` does. Olivier Chafik raised that +a keyword can trip strict validators and suggested listing file arguments in `Tool._meta` instead. +A separate list needs a pointer into the schema, and pointers handle `$ref` and `anyOf` poorly in +generated schemas. A strict validator has to allow this keyword as it does `x-mcp-header`, by +registering it or by relaxing strict mode. + +### Why a Structured File Argument SDKs and gateways can validate `arguments` against `inputSchema` before a handler runs, so the initial call cannot omit a required file argument. A structured value satisfies that schema and keeps the file's metadata together, without a parallel request `_meta` map or a placeholder URI. Omitting `data` distinguishes out-of-band upload from inline transfer. +### Why `digest` Is Required + +A required digest lets the server confirm it received what the client declared, and detects bytes +replaced through an `omit` URL before the server uses them. The cost falls on browsers: Web Crypto +cannot hash a large file in chunks, so a browser client reads the whole file first or uses a +non-native hash. Chunked transfer can ease this later. + +### Why Oversized Inline Data Is a Protocol Error + +SEP-1303 returns input validation errors to the model so that it can correct them. The model does +not choose between inline and out-of-band transfer, so it cannot correct this one, but the client +can. A file over its `maxSize` remains a tool execution error, because the model or the user can +choose another file. + ### Why `omit` and `include` The working group heard that sending the user's credential has to be optional, so a token never -reaches third-party storage, and that some enterprise deployments reject pre-signed URLs and -want the data plane authorized with the same token as the control plane. A per-target flag -covers both. Tying `include` to the server's canonical URI keeps the token within the resource -it was issued for. The group has not finished listing the authorization methods it needs to -support, and this pair is a starting point. +reaches third-party storage, and that some enterprise deployments reject pre-signed URLs and want +the data plane authorized with the same token as the control plane. A per-target flag covers both. +Tying `include` to the server's origin keeps the token on the origin of the resource it was issued +for, without requiring upload endpoints to sit under the MCP path. Wildcards would send the token to +hosts outside that resource. A provider with a separate upload host can use `omit`. `omit` and +`include` are the complete set for this revision; other methods are deferred to [Future +Work](#future-work). + +### Why `omit` Targets Are Not Bound to the Server's Origin -### Why a `path` target +Pre-signed URLs are a common way to accept uploads, and requiring a matching host means each server +stands up its own upload endpoint. Binding them would also add little protection: a server the user +approved receives the file through its own tool regardless. The user's approval and host policy +cover this risk, and hosts can narrow it with an allowlist. + +### Why a `path` Target A gateway can replace a target with a relay at its own origin, but one absolute URL assumes every client reaches the gateway the same way. A client in a sandbox or behind a proxy can reach it -through another address and may not be able to reach the public host. Because that host's origin -differs from the one the client called, rule 4 of [Upload targets](#upload-targets) also makes the -client drop its credential or refuse the target. With `path`, each client transfers through the -address it already uses, so the bytes take the same route as its MCP calls. `url` stays for -anywhere else, such as third-party storage. +through another address and may not be able to reach the public host at all. With `path`, each +client transfers through the address it already uses, so the bytes take the same route as its MCP +calls. `url` stays for anywhere else, such as third-party storage. + +### Why Downloads Are Listed in Result `_meta` -### Why a `resource_link` for downloads +An earlier revision placed the download target in the `_meta` of a `resource_link`. Resources are +mostly static, and a `resource_link` invites a client to call `resources/read` on a file meant to +travel out of band. A client without the extension also received a link it could not fetch. A +typed list mirrors uploads and gives each target its own fields. A gateway finds returned files at +one known key. -Upload and download share what the server hands the client to move bytes: the same `url` or `path`, `headers`, -and `credentials`, under the same target rules. They differ in where the file's description travels, -because MCP types tool arguments with `inputSchema` and returns results as content blocks, and -`resource_link` is already the block for a file. A client or gateway can find a returned file by -scanning `content`, without the tool's schema. A download needs no second call, because the server -has run the tool by the time it names the file. +### Why Resumable Downloads Are Out of Scope -### Prior art +Clients can already split and resume a download with standard HTTP range requests, so the protocol +does not need to define resumption. + +### Prior Art - SEP-2356, for the declaration keyword, inline encoding, and host rules. -- SEP-2631, for file URIs, digests, transfer descriptors, and the `maxSize` change. +- SEP-2631, for file URIs, digests, transfer descriptors, and applying `maxSize` to every file. + SEP-2356 applied it to inline values only. - [SEP-2532](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2532) (resource streaming), which Patrick Rodgers offered as a way to get file bytes from a server. - Storage APIs that separate creating an upload from sending the bytes (pre-signed URLs and @@ -501,27 +778,42 @@ has run the tool by the time it names the file. ## Backward Compatibility The extension is additive and off by default. A client that does not declare it never receives -an upload input request. Tools that take base64 or URL strings today keep working. +an upload input request or a download target. Tools that take base64 or URL strings today keep +working. A tool that adopts `x-mcp-file` changes its `inputSchema`, so a client without this +extension ignores the keyword and sees an object argument it cannot fill correctly. SDKs need an MRTR extension point before they can carry the proposed upload input request. ## Security Implications -[Upload targets](#upload-targets) covers requests the server directs the client to make. -A client that relaxes those rules lets a server use it for server-side request forgery and can leak the user's credential to other resources. -The `path` rules keep a target under the address the client already uses for MCP. +[Transfer Targets](#transfer-targets) covers requests the server directs the client to make. +A client that relaxes those rules lets a server use it for server-side request forgery and can +leak the user's credential to other resources. The `path` rules keep a target under the address the +client already uses for MCP. A gateway that does not implement this extension cannot meet those +rules, so operators **SHOULD NOT** enable this extension behind such a gateway. + +`omit` targets are not bound to the server's origin, so a malicious server can name any public +URL and receive the file there. It can use `POST` as well as `PUT`, which reaches more existing +endpoints. The host's approval before the initial call is the control for this, and it applies to +agent-created files too. A compromised agent can write sensitive data to a file and try to send it. + +Binding the principal, tool name, arguments, and upload targets to `requestState` prevents a client +from uploading under one call's authorization and attaching the file to another call or a different +destination. A required digest lets the server detect bytes replaced through an `omit` URL before it +uses them. + +User files stay in staging until the server removes them or storage-side expiry reclaims them. -Binding the principal, tool name, arguments, and upload targets to `requestState` prevents a -client from uploading under one call's authorization and attaching the file to -another call or a different destination. A target that enforces the declared size and digest -prevents anyone who sees an `omit` URL from replacing the bytes before the retry. +A target that remains writable after the server verifies its bytes lets anyone holding it replace +the file without another check, until the target expires. Pre-signed URLs often cannot be revoked, +so servers that use them rely on short lifetimes or finalize a copy of the verified bytes. Servers **SHOULD** follow [OWASP ASVS 5.0 V5](https://github.com/OWASP/ASVS/blob/v5.0.0/5.0/en/0x14-V5-File-Handling.md) for the formats they accept. -A URL that authorizes itself is a secret for as long as it works, so servers choose its lifetime -with that in mind. +A URL that authorizes itself is a secret for as long as it works. Servers **SHOULD** issue such +URLs with the shortest lifetime that covers the expected transfer time. Clients and servers **MUST** redact target URLs, target headers, and inline `data` values in logs, traces, and error reports. @@ -531,38 +823,40 @@ traces, and error reports. The reference implementation will include a TypeScript SDK branch with the upload input request and a client transfer helper, a reference server in front of an object store, and conformance scenarios for inline transfer, out-of-band transfer, rejection on metadata, a failed -upload, and a digest mismatch. +upload, a digest mismatch, a repeated retry, and a download. ## Open Questions 1. **MRTR extensibility.** How should the specification enable extension-defined input requests? -2. **Mandatory digest.** Requiring `digest` closes the byte-replacement gap for `omit` targets, - but Web Crypto cannot hash a large file in chunks, so browsers have to read the whole file first. - Is that tradeoff acceptable? -3. **Authorization methods.** The working group agreed to list every method it has to support, - including storage that needs a credential separate from the MCP one and HTTP Message - Signatures. -4. **Finalize.** As written the server finalizes while handling the retry. Owners of backends - with an explicit commit step should confirm that works for them. -5. **How the tool's typed result reaches the model**, raised at the October 2 meeting. -6. **stdio.** Out-of-band `omit` targets work for stdio servers. Should anything more be defined - for them? -7. **Resources.** Do resources fit this flow? Thomas Sarlandie took this as an action item on - October 2. -8. **Multipart and POST.** Some storage APIs take only `multipart/form-data`. They can sit - behind a server proxy with `include`, or a later revision can add a `form` target. + Repeated retries are a general MRTR concern and may belong in the same change, along with a + dedicated error code in place of `-32602` with `data.reason`. +2. **Finalize.** As written the server finalizes while handling the retry. Some backends return a + commit handle in the HTTP response to the transfer, such as an ETag or block ID, and the retry + has no place to carry it. Owners of backends with an explicit commit step are asked to confirm + whether that works for them. An optional per-file field in `UploadInputResponse` can be added + if needed. +3. **Tools behind an executor.** Gateways that expose one generic tool hide the inner tool's + schema, so the client never sees `x-mcp-file`. One approach is for the + executor to declare its own file argument and map it to the inner call. This has to be answered + before the extension leaves the experimental phase. +4. **Resources.** Should resources join this flow in a later revision? Thomas Sarlandie took this + as an action item on October 2. +5. **Multipart.** Some storage APIs take only `multipart/form-data`. They can sit behind a server + proxy with `include`, or a later revision can add a `form` target. ## Future Work -- **Chunked and resumable transfer**, for backends with per-request size limits and for +- **Chunked and resumable uploads**, for backends with per-request size limits and for interrupted transfers, once a standard settles. - **Elicitation forms.** Out-of-band upload for `x-mcp-file` fields in `elicitation/create`. +- **More authorization methods**, including storage that needs a credential separate from the MCP + one and HTTP Message Signatures. - **Promotion to the specification**, after the working group has validated the extension in use, per its October 2 decision. ## Acknowledgments -This draft builds on SEP-2356 by Olivier Chafik and SEP-2631 by Casey Chow, and on the File -Uploads Working Group's October 2, 2026 meeting with Wil St. Charles, Kurtis Van Gent, Thomas -Sarlandie, and Kryspin Ziemski. Thomas Sarlandie proposed modeling the upload as a -multi-round-trip request. +This draft builds on SEP-2356 by Olivier Chafik and SEP-2631 by Casey Chow, on the File Uploads +Working Group's October 2, 2026 meeting with Wil St. Charles, Kurtis Van Gent, Thomas Sarlandie, and +Kryspin Ziemski, on the group's October 30, 2026 meeting, and on review from the group and +implementers. Thomas Sarlandie proposed modeling the upload as a multi round-trip request. From d8f53a9831300b0070f05ed2a5723962f6d8e92a Mon Sep 17 00:00:00 2001 From: Den Delimarsky Date: Wed, 4 Nov 2026 16:00:45 +0000 Subject: [PATCH 09/19] Simplify the draft and move motivation and rationale to the PR Co-authored-by: den --- specification/draft/file-uploads.md | 911 +++++++++++++--------------- 1 file changed, 413 insertions(+), 498 deletions(-) diff --git a/specification/draft/file-uploads.md b/specification/draft/file-uploads.md index 2af95db..4028fad 100644 --- a/specification/draft/file-uploads.md +++ b/specification/draft/file-uploads.md @@ -7,94 +7,49 @@ - **Discussion**: [File Uploads Working Group Discord thread](https://discord.com/channels/1358869848138059966/1551968557292593172/1552399008440520865) and issues in this repository -This draft uses SEP form so the working group can submit it as an Extensions Track SEP once a -reference implementation exists. Anything here, including the extension identifier, can change -before then. +The working group plans to submit this document as an Extensions Track SEP once a reference +implementation exists. All content, including the extension identifier, is subject to change before +submission. ## Abstract -This extension lets a client send a file to a tool, and receive a file from one, without passing a -large file's bytes through the model or a JSON-RPC message. A server marks file arguments with the -`x-mcp-file` keyword first proposed in -[SEP-2356](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2356). The client calls -the tool with the file's metadata in the file argument, with optional base64 `data`. If `data` is -absent, the server can reject the call on the metadata or request an upload to an HTTPS target. The -client uploads the bytes and retries with the same arguments and a per-file outcome. A tool returns -a file by listing a download target in the result's `_meta`. - -This revision covers uploads and downloads. [Future Work](#future-work) lists chunked and resumable -uploads and file fields in elicitation forms. - -## Motivation - -MCP has no interoperable way to get a file from a user to a tool. The existing options -(`EmbeddedResource`, `BlobResourceContents`, base64 strings in arguments) put the bytes inside a -JSON-RPC message. When the model emits tool arguments as tokens, the bytes pass through its -context window, and large uploads to remote MCP servers fail. - -The File Uploads Working Group collected the cases this blocks at its -[October 2, 2026 meeting](https://github.com/modelcontextprotocol/modelcontextprotocol/discussions/3412): - -- A user asks an assistant to save a 40 MB PDF from the conversation into a cloud-storage folder. -- An agent pulls a 200 MB CSV into its code sandbox. -- An agent uploads a 50 MB deck as a new revision of an existing document and keeps the link. -- An enterprise agent uploads or downloads on a user's behalf, with or without the user present, - to one of many sites or drives, so the call that returns an upload target has to carry the - destination. - -The same meeting recorded the host-side constraints: - -- One model turn per upload. -- File contents and credentials never enter the context window. -- Small files stay in band to keep latency down. -- Large files go out of band to avoid base64. -- Uploads work when a gateway holds the OAuth credentials instead of the host. -- File pointers on the wire carry a marker a deterministic proxy can find. - -[SEP-2356](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2356) defined how a -server declares a file argument and an inline `data:` encoding, and sent larger files to URL-mode -elicitation instead of defining an upload protocol. It closed without merging. -[SEP-2631](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2631) added out-of-band -transfer through new `files/authorizeUpload` and `files/authorizeDownload` methods. Thomas Sarlandie -identified ordering as the main problem in SEP-2631. Bytes move before the server sees the rest of -the call, so uploads that permissions, quota, or data residency rules would reject still consume -bandwidth and can land in the wrong region. The group agreed on metadata first, then the upload -target, then bytes, then finalize, and agreed to build on multi round-trip requests (MRTR) and -existing primitives before adding new ones. +This extension defines how MCP clients transfer files to and from tools over HTTPS. Files +transferred out of band do not enter model context or JSON-RPC messages. + +To send a file, the client calls the tool with the file's metadata. The server rejects the call +based on the metadata or returns an upload target. The client then uploads the bytes and retries the +call. Clients can instead send small files inline. A tool returns a file by listing a download +target in its result. ## Specification -The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", -"RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as -described in [BCP 14](https://datatracker.ietf.org/doc/html/bcp14) -[[RFC2119](https://datatracker.ietf.org/doc/html/rfc2119)] -[[RFC8174](https://datatracker.ietf.org/doc/html/rfc8174)] when, and only when, they appear in all -capitals, as shown here. +This revision applies to tools. Resource behavior is unchanged from the base protocol. -In this document, _client_ is the component that speaks MCP to the next hop, which can be a server -or a gateway. A host behind a gateway is a client of that gateway, and the gateway is a client of -the server. _Host_ is the application that presents the model and the user interface. _MCP address_ -is the URL a client sends MCP requests to, so it can differ per hop and can differ from the server's -canonical URI. Requirements on clients in [Transfer](#transfer), [Transfer -Targets](#transfer-targets), and [Downloads](#downloads) bind the component that performs the -transfer. +### Terms -This revision applies to tools. Resources keep their base protocol behavior. +- **Client**: The component that sends MCP requests to the next hop. A host behind a gateway is a + client of the gateway, and the gateway is a client of the server. +- **Host**: The application that presents the model and the user interface. +- **MCP address**: The URL a client sends MCP requests to. This URL can vary per hop and can differ + from the server's canonical URI. +- **Target**: The URL or path to which a client sends file bytes, or from which it fetches them. +- **Logical call**: An initial `tools/call` request and its retries. -### Extension Identifier +Requirements on clients in [Transfer](#transfer), [Downloads](#downloads), and [Targets](#targets) +bind the component that performs the transfer. -This extension uses the identifier `io.modelcontextprotocol/files`. +### Capabilities -### Negotiation +The extension identifier is `io.modelcontextprotocol/files`. -Clients **MUST** declare the extension in the `io.modelcontextprotocol/clientCapabilities` request -metadata of every request that may need it: +Clients **MUST** declare the extension in the `io.modelcontextprotocol/clientCapabilities` metadata +of every request that may need it: ```json { "extensions": { "io.modelcontextprotocol/files": { "upload": {}, "download": {} } } } ``` -Servers advertise extension support in the `server/discover` response: +Servers advertise the extension in the `server/discover` response: ```json { @@ -116,52 +71,100 @@ The settings object has the following fields: - `upload` _(optional)_: The sender supports out-of-band uploads. - `download` _(optional)_: The sender supports out-of-band downloads. - `maxInlineBytes` _(optional, servers only)_: The largest total decoded size of inline `data` - values the server accepts in one request. + values the server accepts in one request. If `0` or absent, the server does not accept inline + transfer. -A missing `upload` or `download` field means that direction is not supported. Receivers **MUST** +If `upload` or `download` is absent, the sender does not support that direction. Receivers **MUST** ignore fields they do not recognize. -Clients **MUST NOT** use this extension with a server that did not advertise it. +Clients: + +- **MUST NOT** use this extension with a server that did not advertise it. +- **MUST NOT** send a request whose inline `data` exceeds `maxInlineBytes` in total. +- **MUST** omit `data` from every file argument if the server does not accept inline transfer. +- **MUST NOT** send a file out of band to a server that did not advertise `upload`. -A server that accepts inline `data` **MUST** advertise `maxInlineBytes`. If `maxInlineBytes` is `0` -or missing, the server does not accept inline transfer. Clients **MUST NOT** send a request whose -inline `data` values exceed `maxInlineBytes` in total, and **MUST** omit `data` from every file -argument when the server does not accept inline transfer. If such a server advertises `upload`, it -takes every file through an upload round trip, however small the file is. A client **MUST NOT** send -a file out of band to a server that did not advertise `upload`. +A server that accepts inline `data` **MUST** advertise `maxInlineBytes`. A server that does not +accept inline transfer and advertises `upload` requires an upload round trip for every file, +regardless of size. -For example, a server that advertises a `maxInlineBytes` of 1 MiB and marks a file argument with -a `maxSize` of 150 MiB accepts a 200 KiB file inline, and two 400 KiB files inline in one -request. It accepts a 40 MiB file out of band, and rejects a 200 MiB file on its metadata. +For example, a server with a `maxInlineBytes` of 1 MiB and a file argument with a `maxSize` of 150 +MiB accepts a 200 KiB file or two 400 KiB files inline in one request, accepts a 40 MiB file out of +band, and rejects a 200 MiB file based on its metadata. -If a request's inline `data` values exceed `maxInlineBytes` in total, or a request carries inline -`data` and the server does not accept inline transfer, the server **MUST** return a JSON-RPC error -with code `-32602` and `data.reason` set to `"inline_too_large"`. This is an exception to -[SEP-1303](https://modelcontextprotocol.io/seps/1303-input-validation-errors-as-tool-execution-errors); -see [Why oversized inline data is a protocol error](#why-oversized-inline-data-is-a-protocol-error). -A client that receives this error **MAY** repeat the call with `data` omitted if it declared -`upload` and the server advertised it. +### Flow -If a file argument omits `data`, the server advertises `upload`, and the request did not declare -`upload`, the server **MUST** return a [`MissingRequiredClientCapabilityError`](https://modelcontextprotocol.io/specification/draft/schema#missingrequiredclientcapabilityerror) -(`-32021`) whose `data.requiredCapabilities` lists the missing capability. A server **MUST** do the -same, before running the tool, if it knows the tool can deliver its output only as a download target -and the request did not declare `download`. +A client sends a file inline or out of band. A tool returns a file through a download target. + +```mermaid +sequenceDiagram + participant C as Client + participant S as MCP server + participant T as Target -A server that cannot take a file inline or out of band **MUST** return a `CallToolResult` with -`isError: true`. + Note over C,S: Inline upload + C->>S: tools/call with file metadata and data + S-->>C: CallToolResult + + Note over C,T: Out-of-band upload + C->>S: tools/call with file metadata + alt rejected on metadata + S-->>C: CallToolResult {isError: true} + else accepted + S-->>C: input_required {inputRequests, requestState} + C->>T: PUT or POST bytes + T-->>C: 2xx + C->>S: same arguments + requestState + inputResponses + S-->>C: CallToolResult + end + + Note over C,T: Download + C->>S: tools/call + S-->>C: CallToolResult with download target in _meta + C->>T: GET + T-->>C: 200 or 206 with bytes +``` ### File Arguments A server marks a file argument by adding `x-mcp-file` directly to the argument's object schema in -`Tool.inputSchema`, the placement core uses for `x-mcp-header`. Unlike `x-mcp-header`, `x-mcp-file` -applies to objects and can appear in array items. The keyword's value has the `FileInputDescriptor` -shape. Servers **MUST** make the marked schema require the `FileInput` shape. +`Tool.inputSchema`. This behavior is consistent with +[`x-mcp-header`](https://modelcontextprotocol.io/specification/draft/server/tools#x-mcp-header). +Unlike `x-mcp-header`, `x-mcp-file` applies to objects and can appear in array items. Servers +**MUST** make the marked schema require the `FileInput` shape. + +```typescript +/** The value of x-mcp-file. */ +interface FileInputDescriptor { + /** Media types or dot-prefixed extensions for a file picker. Servers can reject a file by type. */ + accept?: string[]; + /** Maximum decoded size of each file in bytes, for inline and out-of-band transfer. */ + maxSize?: number; +} + +interface FileInput { + /** Unique within this tool call. */ + id: string; + name?: string; + /** "application/octet-stream" if unknown. */ + mimeType: string; + /** Non-negative integer; decoded size in bytes. */ + size: number; + /** SHA-256 over the original bytes. */ + digest: { algorithm: "sha-256"; value: string /* base64url, no padding */ }; + /** + * RFC 4648 base64-encoded file bytes. "" denotes an empty file. If absent, the client requests + * an out-of-band upload. + */ + data?: string; +} +``` + +This can be represented in a server's schema as follows: ```json { "name": "save_to_folder", - "description": "Save a file to a folder", "inputSchema": { "type": "object", "properties": { @@ -192,80 +195,36 @@ shape. Servers **MUST** make the marked schema require the `FileInput` shape. } ``` -```typescript -interface FileInputDescriptor { - /** Media types or dot-prefixed extensions for a file picker. Servers can still reject on type. */ - accept?: string[]; - /** Largest decoded size of each file, in bytes, whether inline or out of band. */ - maxSize?: number; -} - -interface FileInput { - /** Unique within this tool call. */ - id: string; - name?: string; - /** "application/octet-stream" if unknown. */ - mimeType: string; - /** Non-negative integer; decoded size in bytes. */ - size: number; - digest: { algorithm: "sha-256"; value: string /* base64url, no padding */ }; - /** RFC 4648 base64-encoded file bytes; absent for out-of-band uploads. */ - data?: string; -} -``` +Clients **MUST** make each `id` unique within the call. Servers **MUST** validate the decoded size +and digest before using the bytes. Clients that do not support this extension ignore the keyword. Validators that reject unknown -keywords by default, such as Ajv in strict mode, need to allow `x-mcp-file`. - -Clients **MUST** make each `id` unique within the call. The digest is SHA-256 over the original -bytes. When `data` is present, it carries the file inline, and `""` carries an empty file. When -`data` is absent, the client is requesting out-of-band upload. Servers **MUST** validate the decoded -size and digest before using the bytes. A server **MUST** return a `CallToolResult` with `isError: -true` for inline data that cannot be decoded or fails these checks, and JSON-RPC error `-32602` for -duplicate `id` values. - -The host rules below apply to any file the host can read, including a file an agent created. +keywords by default, such as Ajv in strict mode, require configuration to allow `x-mcp-file`. Hosts: - **SHOULD NOT** show `x-mcp-file` properties to a model that emits arguments as tokens. -- **MAY** instead show the model a substitute parameter that takes a host-defined handle, and +- **MAY** instead show the model a substitute parameter whose value is a host-defined handle, and replace the handle with a `FileInput` before sending the call. - **MUST** bind a file to an argument only on the user's selection or under the host's approval policy for model-supplied values. - **MUST NOT** place file bytes or upload URLs in model context. -A tool whose text argument can refer to files, such as Markdown with image links, **SHOULD** take a -separate array of file arguments and relate the two through its description. How a tool's result -is presented to the model is host behavior and outside this extension. +The host rules above apply to any file the host can read, including a file an agent created. -### Upload Flow +A tool whose text argument can refer to files, such as Markdown with image links, **SHOULD** accept +a separate array of file arguments and document the relationship in its description. How a result is +presented to the model is host behavior and outside the scope of this extension. -```mermaid -sequenceDiagram - participant C as Client - participant S as MCP server - participant T as Upload target - - C->>S: tools/call with file metadata in arguments - alt rejected on metadata - S-->>C: CallToolResult {isError: true} - else accepted - S-->>C: input_required {inputRequests, requestState} - C->>T: PUT or POST bytes - T-->>C: 2xx - C->>S: same arguments + requestState + inputResponses - S-->>C: CallToolResult - end -``` +### Uploads #### Initial Call -The client sends `tools/call` with every argument populated and omits `data` for each file it -will upload out of band. For brevity, the request examples in this document omit the `_meta` -request metadata. Every request **MUST** include the required `_meta` fields; see -[`_meta`](https://modelcontextprotocol.io/specification/draft/basic/index#meta) and -[Negotiation](#negotiation). +The client sends `tools/call` with every argument populated, and omits `data` for each file it will +upload out of band. For brevity, the request examples omit the `_meta` request metadata. Every +request **MUST** include the required +[`_meta`](https://modelcontextprotocol.io/specification/draft/basic/index#meta) fields; see also +[Capabilities](#capabilities). ```json { @@ -291,21 +250,23 @@ request metadata. Every request **MUST** include the required `_meta` fields; se } ``` -Before sending the initial call, the host **MUST** obtain the user's approval to send the file to -this server and to storage the server designates, or apply operator policy when no user is present. +Before this call, the host **MUST** obtain the user's approval to send the file to this server and +to storage the server designates, or apply operator policy when no user is present. + +Servers **SHOULD** perform every check that does not require the bytes, such as destination +authorization, quota, `accept`, and `maxSize`, and return a `CallToolResult` with `isError: true` if +one fails. Servers **MUST** treat the metadata as client-asserted and re-check size, digest, and +type once the bytes arrive. -Servers **SHOULD** run every check that does not need the bytes (destination authorization, -quota, `accept` and `maxSize`) and return a `CallToolResult` with `isError: true` if one fails. -Whether an existing file with the same name is a conflict is up to the tool. Tools that support -conditional writes take the revision or condition as a tool argument. Servers **MUST** treat the -metadata as client-asserted and re-check size, digest, and type once the bytes arrive. +The tool determines whether an existing file with the same name is a conflict. A tool that supports +conditional writes accepts the revision or condition as a tool argument. -#### Upload Required +#### Upload Request -If the server accepts the files, it returns an `InputRequiredResult` with one upload input request -under `inputRequests.uploads`. This extension proposes the method below. Using it requires a core -MRTR extension point, because the current input-request and input-response unions are closed. The -fixed `uploads` key also differs from core, where `inputRequests` keys are server-assigned. +If the server accepts the files, it returns an `InputRequiredResult` with an upload input request +under `inputRequests.uploads`. The core MRTR input-request and input-response unions are closed. +Using this method requires a core MRTR extension point. The fixed `uploads` key differs from core +MRTR, in which `inputRequests` keys are server-assigned. ```json { @@ -350,106 +311,69 @@ interface UploadTarget { url?: string; /** Path under the MCP address. Exactly one of url and path is present. */ path?: string; - /** HTTP method for the transfer. Defaults to "PUT". */ + /** Defaults to "PUT". */ method?: "PUT" | "POST"; /** Headers the client sends with the transfer. */ headers?: { [name: string]: string }; /** - * "omit": the target authorizes itself. "include": the client attaches the credential it - * presents to the MCP server. See Transfer Targets. + * "omit": the target carries its own authorization. "include": the client attaches its MCP + * credential. */ credentials: "omit" | "include"; - /** ISO 8601 time. The latest time to start the transfer. A hint; see Transfer. */ + /** ISO 8601 timestamp. Advisory latest start time for the transfer. */ expiresAt?: string; } ``` -Servers **MUST** return an upload input request only on `tools/call` and only to a request that -declared `upload`. Servers **MUST** make each `fileId` match one file argument without `data`, and -**MUST NOT** give a file more than one outstanding target in the current round. A server using the -[Tasks extension](https://modelcontextprotocol.io/extensions/tasks/overview) **MUST** return -the upload input request before creating a task. - -Servers **MUST** bind the tool name, all arguments, the current target for each file, and the -authenticated principal, if any, to `requestState`, and **MUST** handle `requestState` as the -[MRTR server rules](https://modelcontextprotocol.io/specification/draft/basic/patterns/mrtr#server-requirements-basic-workflow) -require. Clients **MUST** handle `requestState` as the -[MRTR client rules](https://modelcontextprotocol.io/specification/draft/basic/patterns/mrtr#client-requirements-basic-workflow) -require for `InputRequiredResult`. - -Servers **MUST NOT** let a target outlive the latest `requestState` it was issued under. MRTR -recommends a short expiry for `requestState`. A server issuing targets for large files **SHOULD** -choose a `requestState` lifetime that covers a transfer started at `expiresAt`, plus the retry. - -#### Staged Uploads - -Servers **SHOULD** issue targets that write to a staging location and that enforce the declared -size. - -The server owns the lifetime and cleanup of bytes it staged: - -- Servers **MUST** keep staged bytes available until the latest `requestState` bound to them - expires, the call ends, or their target is replaced, whichever comes first. -- Servers **MUST NOT** let a staged file appear as a finished file until they confirm it on the - retry. -- Servers **SHOULD** delete staged bytes when a call fails or a target is replaced, and **SHOULD** - use storage-side expiry to reclaim staged bytes that receive no retry. -- A server with several instances **MUST** let any instance that receives the retry confirm the - staged bytes. -- Servers **SHOULD** stop further writes through a target before verifying its bytes, and - **SHOULD** finalize the bytes they verified. - -A target that writes to the final destination creates the file before the server has confirmed it. -Such a target **MUST** enforce the declared size, and the digest where the backend supports it. -Servers **SHOULD NOT** issue such a target unless the backend enforces the declared size and digest -on the write and the write cannot replace existing content. If a later check fails, the client -reports `failed`, or no retry arrives before the `requestState` expires, the server **MUST** remove -that file. +Servers: + +- **MUST** return an upload input request only on `tools/call`, and only to a request that declared + `upload`. +- **MUST** make each `fileId` match one file argument without `data`. +- **MUST NOT** give a file more than one outstanding target in a round. +- **MUST** return the upload input request before creating a task, if they use the [Tasks + extension](https://modelcontextprotocol.io/extensions/tasks/overview). +- **MUST** bind the tool name, all arguments, each file's current target, and the authenticated + principal, if any, to `requestState`. +- **MUST NOT** allow a target to remain valid after the latest `requestState` it was issued under + expires. + +Servers **MUST** handle `requestState` as the [MRTR server +rules](https://modelcontextprotocol.io/specification/draft/basic/patterns/mrtr#server-requirements-basic-workflow) +require. Clients **MUST** handle `requestState` as the [MRTR client +rules](https://modelcontextprotocol.io/specification/draft/basic/patterns/mrtr#client-requirements-basic-workflow) +require for `InputRequiredResult`. MRTR recommends a short `requestState` expiry. A server issuing +targets for large files **SHOULD** choose a `requestState` lifetime that covers a transfer started +at `expiresAt`, plus the retry. #### Transfer -For each `UploadTarget` the client checks the target against [Transfer Targets](#transfer-targets), -sends the request to the target's `url` or resolved `path` with `method`, `headers`, and the file -bytes as the body, and treats a `2xx` response as success. Clients **MUST NOT** send more bytes -than the declared `size`. - -`expiresAt` is a hint; the target decides whether to accept a transfer. Clients **SHOULD NOT** start -a transfer after `expiresAt`, and **MUST** report that file as `failed` if they do not start it. - -Clients **MUST** limit transfer attempts to three per file per logical call (the initial call and -its retries). Each request sent to a target counts as an attempt, including one that answers a -repeated upload input request. A client at this limit **MUST** report that file as `failed` on the -retry. Clients **MUST** stop sooner if a failure is permanent or retrying is unsafe. Clients **MUST -NOT** resend a `POST` on their own unless they know the request did not reach the target. A transfer -that answers a repeated upload input request is not a resend. - -The component that holds the bytes performs the transfer. A gateway **MAY** replace a target's `url` -or `path`, `method`, `headers`, and `credentials` with a relay at its own origin, and **MUST** do so -for a `url` target with `credentials: "include"` when it holds the credential for that server. A -gateway that does so **MUST** check the original target against [Transfer -Targets](#transfer-targets) and forward the bytes to it with the original target's `method`, -`headers`, and `credentials`, resolving a `path` against the address it uses for the server. The -relay **MUST** accept bytes only from the caller, and **MUST NOT** answer `2xx` until the original -target has. - -A gateway that presents more than one server at one MCP address **MUST** replace every `path` -target. Any other gateway that passes a `path` target through unchanged **MUST** route everything -under that MCP address to that one server, **MUST NOT** forward the caller's credential upstream, -and **MUST** attach its own credential for the server when the target has `credentials: "include"`. +The client checks the target against [Targets](#targets), then sends the file bytes as the request +body to the target's `url` or resolved `path`, with `method` and `headers`. A `2xx` response +indicates success. -Clients **MUST** complete the upload and retry without returning control to the model. Servers -**SHOULD NOT** echo upload URLs in the `CallToolResult`. +Clients: -If the user cancels, the client **SHOULD** retry with status `failed` so the server can clean up. +- **MUST NOT** send more bytes than the declared `size`. +- **SHOULD NOT** start a transfer after `expiresAt`, and **MUST** report that file as `failed` if + they do not start it. The target determines whether to accept a transfer. +- **MUST** limit transfer attempts to three per file per logical call. Each request sent to a target + is an attempt, including a request that answers a repeated upload input request. +- **MUST** report the file as `failed` on the retry after reaching that limit, and **MUST** stop + before that limit if a failure is permanent or a further attempt is unsafe. +- **MUST NOT** automatically resend a `POST` unless they know the request did not reach the target. + A transfer that answers a repeated upload input request is not a resend. +- **MUST** complete the upload and retry without returning control to the model. +- **SHOULD** send the retry with status `failed` if the user cancels. -Servers that issue `omit` targets to browser clients **SHOULD** configure the storage to answer -the CORS preflight for the listed headers. +Servers **SHOULD NOT** echo upload URLs in the `CallToolResult`. Servers that issue `omit` targets +to browser clients **SHOULD** configure the storage to respond to the CORS preflight request for the +listed headers. #### Retry -When retrying `tools/call`, the client **MUST** send a new JSON-RPC `id`, the same tool name and -exact arguments, the exact `requestState` value, and one outcome for each requested file under -`inputResponses.uploads`: +When retrying, the client **MUST** send a new JSON-RPC `id`, the same tool name and exact arguments, +the exact `requestState`, and one outcome per requested file under `inputResponses.uploads`: ```json { @@ -487,105 +411,74 @@ interface UploadInputResponse { } ``` -Each file's `status` is `completed` if the client observed a successful transfer, or `failed` if it -did not. A failed file ends this logical call. Clients **MUST NOT** include upload URLs or -credentials in the response. - -The steps below apply to a retry that answers an upload input request. A server that issues a later -round **MUST** bind the confirmed files to the new `requestState` and **MUST NOT** ask for their -outcome again. - -On the retry the server **MUST** perform these steps in order: - -1. Verify `requestState`, and return JSON-RPC error `-32602` if verification fails. -2. Return JSON-RPC error `-32602` if the tool name or any argument differs from the values bound in - `requestState`. -3. Claim `requestState` where [Repeated Retries](#repeated-retries) calls for it. If it is already - claimed, follow that section. -4. Return JSON-RPC error `-32602` for duplicate or unknown `fileId` values and unknown statuses. If - any file has status `failed`, return a `CallToolResult` with `isError: true` without running the - tool. If an outcome is missing, follow - [MRTR error handling](https://modelcontextprotocol.io/specification/draft/basic/patterns/mrtr#error-handling) +`status` is `completed` if the client observed a successful transfer, and `failed` otherwise. A file +with status `failed` ends the logical call. Clients **MUST NOT** include upload URLs or credentials +in the response. + +On a retry that answers an upload input request, the server **MUST** perform these steps in order, +and **MUST** respond as [Errors](#errors) specifies when a step fails: + +1. Verify `requestState`. +2. Check the tool name and arguments against the values bound in `requestState`. +3. Claim `requestState` as specified in [Repeated Retries](#repeated-retries). If it is already + claimed, proceed as that section specifies. +4. Check each `fileId` and status. If any file has status `failed`, stop without running the tool. + If an outcome is missing, follow [MRTR error + handling](https://modelcontextprotocol.io/specification/draft/basic/patterns/mrtr#error-handling) and do not run the tool. -5. Confirm with the storage backend that each file's bytes arrived (for example with a `HEAD` - request or a session status query), verify `digest`, and re-check size and type. The server - **MUST NOT** run the tool with a file whose bytes it has not confirmed. - - If the bytes have not arrived, the server **MAY** return another `InputRequiredResult` with a - new `requestState` and a new or reissued target for the file. It is not required to wait for - the bytes to become visible first. Otherwise it returns a `CallToolResult` with - `isError: true` without running the tool. - - If the bytes arrived but fail a check, the server returns a `CallToolResult` with - `isError: true` without running the tool. +5. Confirm with the storage backend that each file's bytes arrived, for example with a `HEAD` + request or a session status query. Verify `digest`, and re-check size and type. The server + **MUST NOT** run the tool with bytes it has not confirmed. 6. Perform any finalize step the backend needs, run the tool, and return its result. -A repeated upload input request can cost a full re-upload, including when the bytes arrived but -the backend had not yet made them visible. +If the bytes have not arrived at step 5, the server **MAY** return another `InputRequiredResult` +with a new `requestState` and a new or reissued target. It is not required to wait for the bytes to +become visible first. A repeated upload input request can require a full re-upload, including when +the bytes arrived but the backend had not yet made them visible. The echoed `requestState` and `fileId` identify the current target. A new round requires a new -`requestState`. A server **MAY** reissue the same target under it. If the server issues a different -target, it **MUST** prevent the old target from satisfying the new round. Servers **MUST NOT** use -the same target for a different call. +`requestState`, and a server **MAY** reissue the same target under it. If the server issues a +different target, it **MUST** prevent the old target from satisfying the new round. A server that +issues a later round **MUST** bind confirmed files to the new `requestState`, and **MUST NOT** ask +for their outcome again. Servers **MUST NOT** use a target for a different call. #### Repeated Retries -A client can repeat a retry if it does not receive the response. Servers **SHOULD** claim a -`requestState` atomically before running the tool, using storage every instance shares, and **MUST** -do so if running the tool twice is unsafe. A claimed `requestState` counts as processed even while -the tool is running. - -A server that receives a claimed `requestState` **MUST NOT** run the tool again. It **MUST** either -return a JSON-RPC error with code `-32602` and `data.reason` set to `"already_processed"`, or return -the original result to the same principal. If the earlier retry produced an `InputRequiredResult`, -the server **SHOULD** return that result. - -A client that receives `"already_processed"` **MUST NOT** treat it as proof that the call failed, -and **MUST NOT** repeat the call automatically. The same applies to a repeated retry that fails -`requestState` verification. In both cases the outcome of the earlier attempt is unknown. - -### Transfer Targets - -The server chooses each target, and with `credentials: "include"` the client attaches a user -credential to a request the server directed. These rules apply to every upload and download target: - -1. Clients **MUST** reject a target that does not have exactly one of `url` and `path`, and a - `url` that is not an absolute `https` URL. -2. Clients **MUST** check the address they connect to, and **MUST NOT** connect to one that the - [IANA special-purpose registries](https://www.rfc-editor.org/rfc/rfc6890) mark as not globally - reachable, unless an operator allowlist permits it or the target is a `path`, which uses the - address the client already connects to. A client that cannot observe the address, such as a - browser or a client behind a forward proxy, **MUST** rely on a platform or proxy control that - enforces the same restriction, or **MUST** reject `url` targets not on an operator allowlist. -3. Clients **MUST** reject a target whose `headers` include `Authorization`, `Cookie`, a - [forbidden request header](https://fetch.spec.whatwg.org/#forbidden-request-header), or a - name or value that is not valid under [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110#section-5). - On a `path` target or a target with `credentials: "include"`, clients **MUST** also reject - `Forwarded`, `X-Original-URL`, `X-Rewrite-URL`, `X-HTTP-Method-Override`, and any name that - starts with `X-Forwarded-`. -4. Clients **MUST** honor `credentials: "include"` only on a `path` target, or when `url` has the - same origin as the MCP server's - [canonical URI](https://modelcontextprotocol.io/specification/draft/basic/authorization#canonical-server-uri), - compared as an [RFC 6454](https://www.rfc-editor.org/rfc/rfc6454) origin. A parent domain or - wildcard does not match. In a gateway deployment, the comparison uses the canonical URI of the - server whose credential is attached. Otherwise the client **MUST** treat the target as `omit` or - reject it. -5. Clients **MUST** reject a `path` that does not match `1*( "/" segment )`, where `segment` is one - or more ASCII letters, digits, `-`, `_`, `~`, or `.`, and is neither `.` nor `..`. -6. Clients **MUST** reject a target with a missing or unrecognized `credentials` value, or an - unrecognized `method` value. - -A client resolves a `path` by adding it to the end of the path of the MCP address, without a -doubled `/` and before any query string on that address. With the MCP address -`https://gateway.example.com/mcp/abc`, the path `/uploads` resolves to -`https://gateway.example.com/mcp/abc/uploads`. A `path` target inherits the scheme of the MCP -address. Clients **MUST** reject a `path` target over `http`, except on loopback. - -Hosts **MAY** restrict `omit` targets to an allowlist of origins. - -Servers **SHOULD** issue targets that answer without a redirect. Clients **MUST NOT** follow -redirects, and **MUST** treat a `3xx` response as a failed transfer. +A client can repeat a retry if it does not receive the response. + +Servers **SHOULD** claim a `requestState` atomically before running the tool, using storage every +instance shares, and **MUST** do so if running the tool twice is unsafe. A claimed `requestState` is +considered processed, including while the tool is running. + +A server that receives a claimed `requestState` **MUST NOT** run the tool again. It **MUST** return +the original result to the same principal, or the `already_processed` error. If the earlier retry +produced an `InputRequiredResult`, it **SHOULD** return that result. + +A client that receives `already_processed`, or whose repeated retry fails `requestState` +verification, **MUST NOT** treat the response as proof that the call failed, and **MUST NOT** repeat +the call automatically. The outcome of the earlier attempt is unknown. -On stdio there is no canonical URI or MCP address, so servers **MUST** issue only `url` targets -with `credentials: "omit"`. +#### Staging + +Servers **SHOULD** issue targets that write to a staging location and that enforce the declared +size. + +Servers are responsible for the lifetime and cleanup of staged bytes. Servers: + +- **MUST** keep staged bytes until the latest `requestState` bound to them expires, the call ends, + or their target is replaced, whichever comes first. +- **MUST NOT** expose a staged file as finished until they confirm it on the retry. +- **SHOULD** delete staged bytes when a call fails or a target is replaced, and **SHOULD** use + storage-side expiry for staged bytes for which no retry arrives. +- **MUST** allow any instance that receives the retry to confirm the staged bytes. +- **SHOULD** prevent further writes through a target before verifying its bytes, and **SHOULD** + finalize the bytes they verified. + +A target that writes to the final destination creates the file before the server confirms it. Such a +target **MUST** enforce the declared size, and the digest where the backend supports it. Servers +**SHOULD NOT** issue such a target unless the backend enforces both on the write and the write +cannot replace existing content. If a later check fails, the client reports `failed`, or no retry +arrives before the `requestState` expires, the server **MUST** remove the file. ### Downloads @@ -593,8 +486,7 @@ with `credentials: "omit"`. > has not reviewed this form yet. A tool returns files by listing them in the `_meta` of its `CallToolResult`, under the extension -identifier. The server **MUST** also describe the result in `content`, so that a host can tell the -model what it returned: +identifier, with one entry per file. The server **MUST** also describe the result in `content`: ```json { @@ -633,230 +525,253 @@ interface DownloadTarget { name?: string; /** "application/octet-stream" if unknown. */ mimeType: string; - /** Non-negative integer; size in bytes. */ + /** Non-negative integer; size in bytes, after any Content-Encoding is removed. */ size?: number; - /** SHA-256 over the whole file. */ + /** SHA-256 over the whole file, after any Content-Encoding is removed. */ digest?: { algorithm: "sha-256"; value: string /* base64url, no padding */ }; - /** Absolute https URL. Exactly one of url and path is present. */ + /** url, path, credentials, and expiresAt have the same meaning as in UploadTarget. */ url?: string; - /** Path under the MCP address. Exactly one of url and path is present. */ path?: string; /** Headers the client sends with the `GET`. */ headers?: { [name: string]: string }; - /** Same meaning as in UploadTarget. */ credentials: "omit" | "include"; - /** Same meaning as in UploadTarget. */ expiresAt?: string; } ``` -A tool returns several files as several entries. +Servers: -Servers **MUST** add a download target only to the result of a tool call whose `tools/call` request -declared `download`, however that result is delivered. +- **MUST** add a download target only to the result of a call whose `tools/call` request declared + `download`, however that result is delivered. +- **MUST NOT** list a target until the file can be fetched. How a tool waits for a file to exist is + outside the scope of this extension. +- **SHOULD** include `size` when they know it. -The client sends a `GET` to the target's `url` or resolved `path` with `headers`. A `200` response -is a success, and so is a `206` response to a range request. Clients **MAY** use range requests to +The client sends a `GET` to the target's `url` or resolved `path` with `headers`. A `200` response, +or a `206` response to a range request, indicates success. Clients **MAY** use range requests to split or resume a download. -Servers **SHOULD** include `size` when they know it. `size` and `digest` describe the whole file -after any `Content-Encoding` is removed. Once they have the whole file, clients **MUST** check each -one that is present, and **MUST** discard a file that fails a check. +Clients: + +- **MUST** check `size` and `digest`, if present, after receiving the whole file, and **MUST** + discard a file that fails a check. +- **MUST** treat `name`, `mimeType`, `size`, and the bytes as untrusted. +- **MUST NOT** use `name` as a filesystem path without reducing it to a safe base name. +- **MUST** abort once the decoded bytes exceed `size`, and **SHOULD** apply their own limit when + `size` is absent. +- **SHOULD NOT** start a download after `expiresAt`. +- **SHOULD** fetch any file they intend to retrieve before returning control to the model, if user + or operator policy permits. A target cannot be renewed. +- **MUST NOT** repeat the call automatically. A repeated call runs the tool again. -Clients **MUST** treat `name`, `mimeType`, `size`, and the bytes as untrusted. Clients **MUST NOT** -use `name` as a filesystem path without reducing it to a safe base name. Clients **MUST** abort a -transfer once the decoded bytes exceed `size`, and **SHOULD** apply their own limit when `size` is -absent. +Hosts **MUST NOT** place download targets in model context. -Clients **SHOULD NOT** start a download after `expiresAt`. A target can expire and cannot be -renewed, so a client that wants the file, and whose user or operator policy allows fetching it from -this server, **SHOULD** fetch it before returning control to the model. A second call runs the tool -again, so clients **MUST NOT** repeat the call automatically. +Servers can return small files as embedded resources, as defined in the base protocol. -Hosts **MUST NOT** place download targets in model context. The gateway rules in -[Transfer](#transfer) apply to download targets. A relay for a download **MUST** check the original -target, fetch the bytes from it, and serve them only to the caller. +### Targets -A server **MUST NOT** list a download target until the file can be fetched. How a tool waits for a -file to exist is outside this extension. +The rules in this section apply to every upload and download target. With `credentials: "include"`, +the client attaches a user credential to a server-directed request. -Small files can still travel as an embedded resource, as they do in the base protocol. +Clients **MUST** reject a target if: -## Rationale +1. It does not have exactly one of `url` and `path`. +2. Its `url` is not an absolute `https` URL. +3. Its `path` does not match `1*( "/" segment )`, where `segment` is one or more ASCII letters, + digits, `-`, `_`, `~`, or `.`, and is neither `.` nor `..`. +4. Its `credentials` is missing or unrecognized, or its `method` is unrecognized. +5. Its `headers` include `Authorization`, `Cookie`, a [forbidden request + header](https://fetch.spec.whatwg.org/#forbidden-request-header), or a name or value that is not + valid under [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110#section-5). +6. It is a `path` target or has `credentials: "include"`, and its `headers` include `Forwarded`, + `X-Original-URL`, `X-Rewrite-URL`, `X-HTTP-Method-Override`, or a name that starts with + `X-Forwarded-`. -### Why an Upload Input Request +#### Credentials -SEP-2631 negotiated uploads in a separate `files/authorizeUpload` call before `tools/call`. The -server had to issue a regional upload URL before it saw the arguments, so it could not check the -destination or quota first, and the two calls had no protocol-level link. Returning the upload -target from `tools/call` lets the server see the full call first, and `requestState` carries the -binding, so any instance that can reach the staged bytes, and the shared claim record if the server -keeps one, can serve the retry. +Clients **MUST** honor `include` only on a `path` target, or when `url` has the same [RFC +6454](https://www.rfc-editor.org/rfc/rfc6454) origin as the server's [canonical +URI](https://modelcontextprotocol.io/specification/draft/basic/authorization#canonical-server-uri). +A parent domain or wildcard does not match. Behind a gateway, the comparison uses the canonical URI +of the server whose credential is attached. Otherwise the client **MUST** treat the target as `omit` +or reject it. -An upload input request can share the MRTR envelope with other inputs, but the current MRTR rules -and schema allow only elicitation, sampling, and roots requests. This extension therefore requires a -change to the specification before clients and servers can interoperate. +#### Addresses -### Why a Schema Keyword +Clients **MUST** check the address they connect to, and **MUST NOT** connect to one the [IANA +special-purpose registries](https://www.rfc-editor.org/rfc/rfc6890) mark as not globally reachable, +unless an operator allowlist permits it or the target is a `path`. A client that cannot observe the +address, such as a browser or a client behind a forward proxy, **MUST** rely on a platform or proxy +mechanism that enforces the same restriction, or **MUST** reject `url` targets not on an operator +allowlist. Hosts **MAY** restrict `omit` targets to an allowlist of origins. -`x-mcp-file` sits on the argument it describes, as `x-mcp-header` does. Olivier Chafik raised that -a keyword can trip strict validators and suggested listing file arguments in `Tool._meta` instead. -A separate list needs a pointer into the schema, and pointers handle `$ref` and `anyOf` poorly in -generated schemas. A strict validator has to allow this keyword as it does `x-mcp-header`, by -registering it or by relaxing strict mode. +#### Redirects -### Why a Structured File Argument +Servers **SHOULD** issue targets that respond without a redirect. Clients **MUST NOT** follow +redirects, and **MUST** treat a `3xx` response as a failed transfer. -SDKs and gateways can validate `arguments` against `inputSchema` before a handler runs, so the -initial call cannot omit a required file argument. A structured value satisfies that schema and -keeps the file's metadata together, without a parallel request `_meta` map or a placeholder URI. -Omitting `data` distinguishes out-of-band upload from inline transfer. +#### Resolving `path` -### Why `digest` Is Required +A client resolves a `path` by appending it to the path of the MCP address, without a doubled `/` and +before any query string. With the MCP address `https://gateway.example.com/mcp/abc`, `/uploads` +resolves to `https://gateway.example.com/mcp/abc/uploads`. A `path` target inherits the scheme of +the MCP address. Clients **MUST** reject a `path` target over `http`, except on loopback. -A required digest lets the server confirm it received what the client declared, and detects bytes -replaced through an `omit` URL before the server uses them. The cost falls on browsers: Web Crypto -cannot hash a large file in chunks, so a browser client reads the whole file first or uses a -non-native hash. Chunked transfer can ease this later. +#### stdio -### Why Oversized Inline Data Is a Protocol Error +Servers on stdio **MUST** issue only `url` targets with `credentials: "omit"`. The stdio transport +has no canonical URI or MCP address. -SEP-1303 returns input validation errors to the model so that it can correct them. The model does -not choose between inline and out-of-band transfer, so it cannot correct this one, but the client -can. A file over its `maxSize` remains a tool execution error, because the model or the user can -choose another file. +### Gateways -### Why `omit` and `include` +The component that holds the bytes performs the transfer. -The working group heard that sending the user's credential has to be optional, so a token never -reaches third-party storage, and that some enterprise deployments reject pre-signed URLs and want -the data plane authorized with the same token as the control plane. A per-target flag covers both. -Tying `include` to the server's origin keeps the token on the origin of the resource it was issued -for, without requiring upload endpoints to sit under the MCP path. Wildcards would send the token to -hosts outside that resource. A provider with a separate upload host can use `omit`. `omit` and -`include` are the complete set for this revision; other methods are deferred to [Future -Work](#future-work). +A gateway **MAY** replace a target's `url` or `path`, `method`, `headers`, and `credentials` with a +relay at its own origin. It **MUST** do so for a `url` target with `credentials: "include"` when it +holds the credential for that server. -### Why `omit` Targets Are Not Bound to the Server's Origin +A gateway that relays an upload **MUST**: -Pre-signed URLs are a common way to accept uploads, and requiring a matching host means each server -stands up its own upload endpoint. Binding them would also add little protection: a server the user -approved receives the file through its own tool regardless. The user's approval and host policy -cover this risk, and hosts can narrow it with an allowlist. +- Check the original target against [Targets](#targets). +- Forward the bytes with the original target's `method`, `headers`, and `credentials`, resolving a + `path` against the address it uses for the server. +- Accept bytes only from the caller. -### Why a `path` Target +The relay **MUST NOT** answer `2xx` until the original target has. -A gateway can replace a target with a relay at its own origin, but one absolute URL assumes every -client reaches the gateway the same way. A client in a sandbox or behind a proxy can reach it -through another address and may not be able to reach the public host at all. With `path`, each -client transfers through the address it already uses, so the bytes take the same route as its MCP -calls. `url` stays for anywhere else, such as third-party storage. +A gateway that relays a download **MUST** check the original target against [Targets](#targets), +fetch the bytes from it with the original target's `headers` and `credentials`, resolving a `path` +against the address it uses for the server, and serve them only to the caller. The relay +**MUST NOT** answer `2xx` until the original target has. -### Why Downloads Are Listed in Result `_meta` +A gateway that presents more than one server at one MCP address **MUST** replace every `path` +target. Any other gateway that passes a `path` target through unchanged **MUST** route everything +under that MCP address to that one server, **MUST NOT** forward the caller's credential upstream, +and **MUST** attach its own credential for the server when the target has `credentials: "include"`. -An earlier revision placed the download target in the `_meta` of a `resource_link`. Resources are -mostly static, and a `resource_link` invites a client to call `resources/read` on a file meant to -travel out of band. A client without the extension also received a link it could not fetch. A -typed list mirrors uploads and gives each target its own fields. A gateway finds returned files at -one known key. +### Errors + +Servers **MUST** respond to the following conditions as shown. The `requestState`, `fileId`, status, +and bytes conditions apply to a retry that answers an upload input request. `isError: true` denotes +a `CallToolResult` with `isError` set to `true`. `-32021` is +[`MissingRequiredClientCapabilityError`](https://modelcontextprotocol.io/specification/draft/schema#missingrequiredclientcapabilityerror). + +| Condition | Response | +| ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- | +| Inline `data` exceeds `maxInlineBytes` in total, or the server does not accept it | JSON-RPC `-32602`, `data.reason` `"inline_too_large"` | +| File omits `data`, server advertises `upload`, request did not declare it | `-32021` with `data.requiredCapabilities` listing the missing capability | +| Server knows the tool can only return a download, request did not declare `download` | `-32021` as above, before running the tool | +| Server cannot accept a file inline or out of band | `isError: true` | +| Inline data cannot be decoded, or fails the size or digest check | `isError: true` | +| Duplicate `id` values in file arguments | JSON-RPC `-32602` | +| `requestState` fails verification | JSON-RPC `-32602` | +| Tool name or arguments differ from `requestState` | JSON-RPC `-32602` | +| Duplicate or unknown `fileId`, or unknown status | JSON-RPC `-32602` | +| A file has status `failed` | `isError: true`, without running the tool | +| Bytes have not arrived, and the server does not start a new round | `isError: true`, without running the tool | +| Bytes arrived but fail a check | `isError: true`, without running the tool | +| `requestState` already claimed | JSON-RPC `-32602`, `data.reason` `"already_processed"`, or the original result to the same principal | + +`inline_too_large` is an exception to +[SEP-1303](https://modelcontextprotocol.io/seps/1303-input-validation-errors-as-tool-execution-errors). +A client that receives this error **MAY** repeat the call without `data` if it declared `upload` and +the server advertised it. -### Why Resumable Downloads Are Out of Scope +## Backward Compatibility -Clients can already split and resume a download with standard HTTP range requests, so the protocol -does not need to define resumption. +This extension is optional. A client that does not declare it never receives an upload input request +or a download target. Tools that accept file content inline today, such as base64 strings, continue +to work, as they do not depend on the upload or download flows. A tool that adopts `x-mcp-file` +changes its `inputSchema`, so a client without this extension sees an object argument it cannot +populate correctly. -### Prior Art +SDKs require an MRTR extension point to support the upload input request. -- SEP-2356, for the declaration keyword, inline encoding, and host rules. -- SEP-2631, for file URIs, digests, transfer descriptors, and applying `maxSize` to every file. - SEP-2356 applied it to inline values only. -- [SEP-2532](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2532) (resource - streaming), which Patrick Rodgers offered as a way to get file bytes from a server. -- Storage APIs that separate creating an upload from sending the bytes (pre-signed URLs and - upload sessions). +## Security Implications -## Backward Compatibility +Clients and servers **MUST** redact target URLs, target headers, and inline `data` in logs, traces, +and error reports. -The extension is additive and off by default. A client that does not declare it never receives -an upload input request or a download target. Tools that take base64 or URL strings today keep -working. A tool that adopts `x-mcp-file` changes its `inputSchema`, so a client without this -extension ignores the keyword and sees an object argument it cannot fill correctly. +### Server-Directed Requests -SDKs need an MRTR extension point before they can carry the proposed upload input request. +A client that does not enforce [Targets](#targets) is exposed to server-side request forgery and can +leak the user's credential. Operators **SHOULD NOT** enable this extension behind a gateway that +does not implement it. Such a gateway cannot meet the rules in [Targets](#targets). -## Security Implications +### Exfiltration -[Transfer Targets](#transfer-targets) covers requests the server directs the client to make. -A client that relaxes those rules lets a server use it for server-side request forgery and can -leak the user's credential to other resources. The `path` rules keep a target under the address the -client already uses for MCP. A gateway that does not implement this extension cannot meet those -rules, so operators **SHOULD NOT** enable this extension behind such a gateway. +A malicious server can specify any public URL as an `omit` target and receive the file at that URL. +`POST` reaches more existing endpoints than `PUT`. The host approval required before the initial +call mitigates this risk and applies to agent-created files. A compromised agent can write sensitive +data to a file and attempt to send it. -`omit` targets are not bound to the server's origin, so a malicious server can name any public -URL and receive the file there. It can use `POST` as well as `PUT`, which reaches more existing -endpoints. The host's approval before the initial call is the control for this, and it applies to -agent-created files too. A compromised agent can write sensitive data to a file and try to send it. +### Binding Binding the principal, tool name, arguments, and upload targets to `requestState` prevents a client -from uploading under one call's authorization and attaching the file to another call or a different -destination. A required digest lets the server detect bytes replaced through an `omit` URL before it -uses them. +from uploading under one call's authorization and attaching the file to another call or destination. +The required digest enables the server to detect bytes replaced through an `omit` URL before it uses +them. + +### Writable Targets + +A target that remains writable after verification allows any holder of the target to replace the +file. Pre-signed URLs often cannot be revoked. Servers that use them rely on short lifetimes or +finalize a copy of the verified bytes. + +### Secret URLs -User files stay in staging until the server removes them or storage-side expiry reclaims them. +A URL that carries its own authorization is a secret for as long as it is valid. Servers **SHOULD** +issue such URLs with the shortest lifetime that covers the expected transfer. -A target that remains writable after the server verifies its bytes lets anyone holding it replace -the file without another check, until the target expires. Pre-signed URLs often cannot be revoked, -so servers that use them rely on short lifetimes or finalize a copy of the verified bytes. +### Staged Files -Servers **SHOULD** follow -[OWASP ASVS 5.0 V5](https://github.com/OWASP/ASVS/blob/v5.0.0/5.0/en/0x14-V5-File-Handling.md) -for the formats they accept. +User files remain in staging until the server or storage-side expiry removes them. -A URL that authorizes itself is a secret for as long as it works. Servers **SHOULD** issue such -URLs with the shortest lifetime that covers the expected transfer time. +### File Contents -Clients and servers **MUST** redact target URLs, target headers, and inline `data` values in logs, -traces, and error reports. +Servers **SHOULD** follow [OWASP ASVS 5.0 +V5](https://github.com/OWASP/ASVS/blob/v5.0.0/5.0/en/0x14-V5-File-Handling.md) for the formats they +accept. ## Reference Implementation -The reference implementation will include a TypeScript SDK branch with the upload input request -and a client transfer helper, a reference server in front of an object store, and -conformance scenarios for inline transfer, out-of-band transfer, rejection on metadata, a failed -upload, a digest mismatch, a repeated retry, and a download. +The reference implementation will include a TypeScript SDK branch with the upload input request and +a client transfer helper, a reference server in front of an object store, and conformance scenarios +for inline transfer, out-of-band transfer, rejection on metadata, a failed upload, a digest +mismatch, a repeated retry, and a download. ## Open Questions 1. **MRTR extensibility.** How should the specification enable extension-defined input requests? - Repeated retries are a general MRTR concern and may belong in the same change, along with a - dedicated error code in place of `-32602` with `data.reason`. -2. **Finalize.** As written the server finalizes while handling the retry. Some backends return a - commit handle in the HTTP response to the transfer, such as an ETag or block ID, and the retry - has no place to carry it. Owners of backends with an explicit commit step are asked to confirm - whether that works for them. An optional per-file field in `UploadInputResponse` can be added - if needed. -3. **Tools behind an executor.** Gateways that expose one generic tool hide the inner tool's - schema, so the client never sees `x-mcp-file`. One approach is for the - executor to declare its own file argument and map it to the inner call. This has to be answered - before the extension leaves the experimental phase. -4. **Resources.** Should resources join this flow in a later revision? Thomas Sarlandie took this + Repeated retries and a dedicated error code are candidates for the same change. +2. **Finalize.** Some backends return a commit handle in the transfer's HTTP response, such as an + ETag or block ID, and the retry has no field for it. Owners of backends with an explicit commit + step are asked to confirm whether finalizing on the retry is sufficient. An optional per-file + field in `UploadInputResponse` can be added if required. +3. **Tools behind an executor.** Gateways that expose a single generic tool hide the inner tool's + schema, including `x-mcp-file`. The executor could declare its own file argument and map it to + the inner call. This question requires resolution before the extension leaves the experimental + phase. +4. **Resources.** Should a later revision apply this flow to resources? Thomas Sarlandie took this as an action item on October 2. -5. **Multipart.** Some storage APIs take only `multipart/form-data`. They can sit behind a server - proxy with `include`, or a later revision can add a `form` target. +5. **Multipart.** Some storage APIs accept only `multipart/form-data`. A server can proxy such an + API with an `include` target, or a later revision can add a `form` target. ## Future Work -- **Chunked and resumable uploads**, for backends with per-request size limits and for - interrupted transfers, once a standard settles. +- **Chunked and resumable uploads**, for backends with per-request size limits and for interrupted + transfers, once a standard is established. - **Elicitation forms.** Out-of-band upload for `x-mcp-file` fields in `elicitation/create`. - **More authorization methods**, including storage that needs a credential separate from the MCP - one and HTTP Message Signatures. -- **Promotion to the specification**, after the working group has validated the extension in - use, per its October 2 decision. + one, and HTTP Message Signatures. +- **Promotion to the specification**, after the group has validated the extension in use, per its + October 2 decision. ## Acknowledgments -This draft builds on SEP-2356 by Olivier Chafik and SEP-2631 by Casey Chow, on the File Uploads -Working Group's October 2, 2026 meeting with Wil St. Charles, Kurtis Van Gent, Thomas Sarlandie, and -Kryspin Ziemski, on the group's October 30, 2026 meeting, and on review from the group and -implementers. Thomas Sarlandie proposed modeling the upload as a multi round-trip request. +This draft builds on +[SEP-2356](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2356) by Olivier Chafik +and [SEP-2631](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2631) by Casey +Chow, on the working group's October 2, 2026 meeting with Wil St. Charles, Kurtis Van Gent, Thomas +Sarlandie, and Kryspin Ziemski, on its October 30, 2026 meeting, and on review from the group and +implementers. Thomas Sarlandie proposed modeling the upload as a multi round-trip request. \ No newline at end of file From ac9a03df0d67cb1e62a71d7a33da6a87d3be1466 Mon Sep 17 00:00:00 2001 From: Den Delimarsky Date: Thu, 5 Nov 2026 07:11:42 +0000 Subject: [PATCH 10/19] Use plainer wording and remove meeting and individual references Co-authored-by: den --- specification/draft/file-uploads.md | 80 +++++++++++++---------------- 1 file changed, 37 insertions(+), 43 deletions(-) diff --git a/specification/draft/file-uploads.md b/specification/draft/file-uploads.md index 4028fad..09f33ac 100644 --- a/specification/draft/file-uploads.md +++ b/specification/draft/file-uploads.md @@ -8,8 +8,7 @@ and issues in this repository The working group plans to submit this document as an Extensions Track SEP once a reference -implementation exists. All content, including the extension identifier, is subject to change before -submission. +implementation exists. Anything here can change before then, including the extension identifier. ## Abstract @@ -35,8 +34,8 @@ This revision applies to tools. Resource behavior is unchanged from the base pro - **Target**: The URL or path to which a client sends file bytes, or from which it fetches them. - **Logical call**: An initial `tools/call` request and its retries. -Requirements on clients in [Transfer](#transfer), [Downloads](#downloads), and [Targets](#targets) -bind the component that performs the transfer. +In [Transfer](#transfer), [Downloads](#downloads), and [Targets](#targets), client means the +component that performs the transfer. ### Capabilities @@ -213,8 +212,8 @@ Hosts: The host rules above apply to any file the host can read, including a file an agent created. A tool whose text argument can refer to files, such as Markdown with image links, **SHOULD** accept -a separate array of file arguments and document the relationship in its description. How a result is -presented to the model is host behavior and outside the scope of this extension. +a separate array of file arguments and document the relationship in its description. This extension +does not define how a host presents a result to the model. ### Uploads @@ -264,9 +263,9 @@ conditional writes accepts the revision or condition as a tool argument. #### Upload Request If the server accepts the files, it returns an `InputRequiredResult` with an upload input request -under `inputRequests.uploads`. The core MRTR input-request and input-response unions are closed. -Using this method requires a core MRTR extension point. The fixed `uploads` key differs from core -MRTR, in which `inputRequests` keys are server-assigned. +under `inputRequests.uploads`. This method needs a core MRTR extension point, because the +input-request and input-response unions are closed. The fixed `uploads` key also differs from core +MRTR, where keys are server-assigned. ```json { @@ -356,7 +355,7 @@ Clients: - **MUST NOT** send more bytes than the declared `size`. - **SHOULD NOT** start a transfer after `expiresAt`, and **MUST** report that file as `failed` if - they do not start it. The target determines whether to accept a transfer. + they do not start it. The target decides whether to accept a transfer. - **MUST** limit transfer attempts to three per file per logical call. Each request sent to a target is an attempt, including a request that answers a repeated upload input request. - **MUST** report the file as `failed` on the retry after reaching that limit, and **MUST** stop @@ -421,7 +420,7 @@ and **MUST** respond as [Errors](#errors) specifies when a step fails: 1. Verify `requestState`. 2. Check the tool name and arguments against the values bound in `requestState`. 3. Claim `requestState` as specified in [Repeated Retries](#repeated-retries). If it is already - claimed, proceed as that section specifies. + claimed, follow that section. 4. Check each `fileId` and status. If any file has status `failed`, stop without running the tool. If an outcome is missing, follow [MRTR error handling](https://modelcontextprotocol.io/specification/draft/basic/patterns/mrtr#error-handling) @@ -447,8 +446,8 @@ for their outcome again. Servers **MUST NOT** use a target for a different call. A client can repeat a retry if it does not receive the response. Servers **SHOULD** claim a `requestState` atomically before running the tool, using storage every -instance shares, and **MUST** do so if running the tool twice is unsafe. A claimed `requestState` is -considered processed, including while the tool is running. +instance shares, and **MUST** do so if running the tool twice is unsafe. A claimed `requestState` +counts as processed, even while the tool is running. A server that receives a claimed `requestState` **MUST NOT** run the tool again. It **MUST** return the original result to the same principal, or the `already_processed` error. If the earlier retry @@ -469,7 +468,7 @@ Servers are responsible for the lifetime and cleanup of staged bytes. Servers: or their target is replaced, whichever comes first. - **MUST NOT** expose a staged file as finished until they confirm it on the retry. - **SHOULD** delete staged bytes when a call fails or a target is replaced, and **SHOULD** use - storage-side expiry for staged bytes for which no retry arrives. + storage-side expiry for staged bytes with no retry. - **MUST** allow any instance that receives the retry to confirm the staged bytes. - **SHOULD** prevent further writes through a target before verifying its bytes, and **SHOULD** finalize the bytes they verified. @@ -482,8 +481,7 @@ arrives before the `requestState` expires, the server **MUST** remove the file. ### Downloads -> **Note**: The working group discussed an earlier form of this section on October 30, 2026, and -> has not reviewed this form yet. +> **Note**: The working group has not reviewed this section yet. A tool returns files by listing them in the `_meta` of its `CallToolResult`, under the extension identifier, with one entry per file. The server **MUST** also describe the result in `content`: @@ -543,8 +541,8 @@ Servers: - **MUST** add a download target only to the result of a call whose `tools/call` request declared `download`, however that result is delivered. -- **MUST NOT** list a target until the file can be fetched. How a tool waits for a file to exist is - outside the scope of this extension. +- **MUST NOT** list a target until the file can be fetched. This extension does not define how a + tool waits for a file to exist. - **SHOULD** include `size` when they know it. The client sends a `GET` to the target's `url` or resolved `path` with `headers`. A `200` response, @@ -560,8 +558,8 @@ Clients: - **MUST** abort once the decoded bytes exceed `size`, and **SHOULD** apply their own limit when `size` is absent. - **SHOULD NOT** start a download after `expiresAt`. -- **SHOULD** fetch any file they intend to retrieve before returning control to the model, if user - or operator policy permits. A target cannot be renewed. +- **SHOULD** fetch a file they want before returning control to the model, if user or operator + policy permits. A target cannot be renewed. - **MUST NOT** repeat the call automatically. A repeated call runs the tool again. Hosts **MUST NOT** place download targets in model context. @@ -570,8 +568,7 @@ Servers can return small files as embedded resources, as defined in the base pro ### Targets -The rules in this section apply to every upload and download target. With `credentials: "include"`, -the client attaches a user credential to a server-directed request. +These rules apply to every upload and download target. Clients **MUST** reject a target if: @@ -652,8 +649,8 @@ and **MUST** attach its own credential for the server when the target has `crede ### Errors Servers **MUST** respond to the following conditions as shown. The `requestState`, `fileId`, status, -and bytes conditions apply to a retry that answers an upload input request. `isError: true` denotes -a `CallToolResult` with `isError` set to `true`. `-32021` is +and bytes conditions apply to a retry that answers an upload input request. `isError: true` means a +`CallToolResult` with `isError` set to `true`. `-32021` is [`MissingRequiredClientCapabilityError`](https://modelcontextprotocol.io/specification/draft/schema#missingrequiredclientcapabilityerror). | Condition | Response | @@ -703,20 +700,19 @@ does not implement it. Such a gateway cannot meet the rules in [Targets](#target A malicious server can specify any public URL as an `omit` target and receive the file at that URL. `POST` reaches more existing endpoints than `PUT`. The host approval required before the initial call mitigates this risk and applies to agent-created files. A compromised agent can write sensitive -data to a file and attempt to send it. +data to a file and try to send it. ### Binding Binding the principal, tool name, arguments, and upload targets to `requestState` prevents a client from uploading under one call's authorization and attaching the file to another call or destination. -The required digest enables the server to detect bytes replaced through an `omit` URL before it uses -them. +The required digest lets the server detect bytes replaced through an `omit` URL before it uses them. ### Writable Targets -A target that remains writable after verification allows any holder of the target to replace the -file. Pre-signed URLs often cannot be revoked. Servers that use them rely on short lifetimes or -finalize a copy of the verified bytes. +A target that remains writable after verification lets anyone holding it replace the file. +Pre-signed URLs often cannot be revoked. Servers that use them rely on short lifetimes or finalize a +copy of the verified bytes. ### Secret URLs @@ -743,17 +739,16 @@ mismatch, a repeated retry, and a download. ## Open Questions 1. **MRTR extensibility.** How should the specification enable extension-defined input requests? - Repeated retries and a dedicated error code are candidates for the same change. + Repeated retries and a dedicated error code may belong in the same change. 2. **Finalize.** Some backends return a commit handle in the transfer's HTTP response, such as an ETag or block ID, and the retry has no field for it. Owners of backends with an explicit commit - step are asked to confirm whether finalizing on the retry is sufficient. An optional per-file - field in `UploadInputResponse` can be added if required. + step are asked to confirm whether finalizing on the retry works. An optional per-file field in + `UploadInputResponse` can be added if needed. 3. **Tools behind an executor.** Gateways that expose a single generic tool hide the inner tool's schema, including `x-mcp-file`. The executor could declare its own file argument and map it to - the inner call. This question requires resolution before the extension leaves the experimental + the inner call. The group needs to answer this before the extension leaves the experimental phase. -4. **Resources.** Should a later revision apply this flow to resources? Thomas Sarlandie took this - as an action item on October 2. +4. **Resources.** Should a later revision apply this flow to resources? 5. **Multipart.** Some storage APIs accept only `multipart/form-data`. A server can proxy such an API with an `include` target, or a later revision can add a `form` target. @@ -764,14 +759,13 @@ mismatch, a repeated retry, and a download. - **Elicitation forms.** Out-of-band upload for `x-mcp-file` fields in `elicitation/create`. - **More authorization methods**, including storage that needs a credential separate from the MCP one, and HTTP Message Signatures. -- **Promotion to the specification**, after the group has validated the extension in use, per its - October 2 decision. +- **Promotion to the specification**, after the group has validated the extension in use. ## Acknowledgments This draft builds on -[SEP-2356](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2356) by Olivier Chafik -and [SEP-2631](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2631) by Casey -Chow, on the working group's October 2, 2026 meeting with Wil St. Charles, Kurtis Van Gent, Thomas -Sarlandie, and Kryspin Ziemski, on its October 30, 2026 meeting, and on review from the group and -implementers. Thomas Sarlandie proposed modeling the upload as a multi round-trip request. \ No newline at end of file +[SEP-2356](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2356) and +[SEP-2631](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2631), and on +collaboration in the +[File Uploads Working Group](https://modelcontextprotocol.io/community/working-groups/file-uploads) +and review from implementers. From 52450243dfbcf9be789491f3b1f5bdc00dce8d25 Mon Sep 17 00:00:00 2001 From: Den Delimarsky Date: Thu, 5 Nov 2026 07:25:58 +0000 Subject: [PATCH 11/19] Clarify the abstract and simplify the status line Co-authored-by: den --- specification/draft/file-uploads.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/specification/draft/file-uploads.md b/specification/draft/file-uploads.md index 09f33ac..f8dce99 100644 --- a/specification/draft/file-uploads.md +++ b/specification/draft/file-uploads.md @@ -1,6 +1,6 @@ # File Uploads Extension -- **Status**: Experimental draft, not yet proposed as a SEP +- **Status**: Draft - **Extension Identifier**: `io.modelcontextprotocol/files` (provisional) - **Working Group**: [File Uploads](https://modelcontextprotocol.io/community/working-groups/file-uploads) - **Editors**: Den Delimarsky (@localden) @@ -15,10 +15,14 @@ implementation exists. Anything here can change before then, including the exten This extension defines how MCP clients transfer files to and from tools over HTTPS. Files transferred out of band do not enter model context or JSON-RPC messages. -To send a file, the client calls the tool with the file's metadata. The server rejects the call -based on the metadata or returns an upload target. The client then uploads the bytes and retries the -call. Clients can instead send small files inline. A tool returns a file by listing a download -target in its result. +A client has two options for uploading a file: + +- **Inline**: The client sends a small file in the tool call. +- **Out of band**: The client calls the tool with the file's metadata. The server either returns an + upload target or rejects the call based on the metadata. If the server returns a target, the client + uploads the bytes and retries the call. + +To download a file, the client fetches it from a download target that the tool lists in its result. ## Specification From 01fa8f46ca3ced141ea5d1bc2fe5d9f4f26ee0ee Mon Sep 17 00:00:00 2001 From: Den Delimarsky Date: Thu, 5 Nov 2026 07:45:10 +0000 Subject: [PATCH 12/19] Add form targets, make the upload flow stateless by default, and tighten credentials and errors Co-authored-by: den --- specification/draft/file-uploads.md | 178 +++++++++++++++++++--------- 1 file changed, 119 insertions(+), 59 deletions(-) diff --git a/specification/draft/file-uploads.md b/specification/draft/file-uploads.md index f8dce99..29458c4 100644 --- a/specification/draft/file-uploads.md +++ b/specification/draft/file-uploads.md @@ -156,7 +156,7 @@ interface FileInput { /** SHA-256 over the original bytes. */ digest: { algorithm: "sha-256"; value: string /* base64url, no padding */ }; /** - * RFC 4648 base64-encoded file bytes. "" denotes an empty file. If absent, the client requests + * RFC 4648 base64-encoded file bytes. If absent and size is greater than 0, the client requests * an out-of-band upload. */ data?: string; @@ -201,6 +201,9 @@ This can be represented in a server's schema as follows: Clients **MUST** make each `id` unique within the call. Servers **MUST** validate the decoded size and digest before using the bytes. +A file with `size` 0 has no bytes to transfer. Clients **MUST** omit `data` for it. Servers **MUST** +treat it as an empty file, **MUST** check its `digest`, and **MUST NOT** issue a target for it. + Clients that do not support this extension ignore the keyword. Validators that reject unknown keywords by default, such as Ajv in strict mode, require configuration to allow `x-mcp-file`. @@ -224,8 +227,8 @@ does not define how a host presents a result to the model. #### Initial Call The client sends `tools/call` with every argument populated, and omits `data` for each file it will -upload out of band. For brevity, the request examples omit the `_meta` request metadata. Every -request **MUST** include the required +upload out of band. The request examples omit the `_meta` request metadata. Every request **MUST** +include the required [`_meta`](https://modelcontextprotocol.io/specification/draft/basic/index#meta) fields; see also [Capabilities](#capabilities). @@ -316,23 +319,52 @@ interface UploadTarget { path?: string; /** Defaults to "PUT". */ method?: "PUT" | "POST"; + /** Sends the file as multipart/form-data. Requires method "POST". */ + form?: UploadForm; /** Headers the client sends with the transfer. */ headers?: { [name: string]: string }; /** * "omit": the target carries its own authorization. "include": the client attaches its MCP - * credential. + * credential. Valid only on a path target. */ credentials: "omit" | "include"; /** ISO 8601 timestamp. Advisory latest start time for the transfer. */ expiresAt?: string; } + +interface UploadForm { + /** Fields the client sends before the file, in array order. */ + fields: { name: string; value: string }[]; + /** Name of the form field that carries the file. */ + fileField: string; +} +``` + +A storage API that accepts only `multipart/form-data` uses a `form` target: + +```json +{ + "fileId": "file-1", + "url": "https://upload.example.com/", + "method": "POST", + "form": { + "fields": [ + { "name": "key", "value": "staging/01JD3V7K2M" }, + { "name": "policy", "value": "..." }, + { "name": "signature", "value": "..." } + ], + "fileField": "file" + }, + "credentials": "omit", + "expiresAt": "2026-11-04T18:30:00Z" +} ``` Servers: - **MUST** return an upload input request only on `tools/call`, and only to a request that declared `upload`. -- **MUST** make each `fileId` match one file argument without `data`. +- **MUST** make each `fileId` match one non-empty file argument without `data`. - **MUST NOT** give a file more than one outstanding target in a round. - **MUST** return the upload input request before creating a task, if they use the [Tasks extension](https://modelcontextprotocol.io/extensions/tasks/overview). @@ -351,19 +383,24 @@ at `expiresAt`, plus the retry. #### Transfer -The client checks the target against [Targets](#targets), then sends the file bytes as the request -body to the target's `url` or resolved `path`, with `method` and `headers`. A `2xx` response -indicates success. +The client checks the target against [Targets](#targets), then sends the file to the target's `url` +or resolved `path`, with `method` and `headers`. Without `form`, the file bytes are the request +body. With `form`, the body is `multipart/form-data` as defined in [RFC +7578](https://www.rfc-editor.org/rfc/rfc7578). A `2xx` response indicates success. Clients: -- **MUST NOT** send more bytes than the declared `size`. +- **MUST** support `form` targets if they declare `upload`. +- **MUST** send each entry in a `form` target's `fields` as one part, in array order, then send the + file as the last part, named `fileField`. The file part's `filename` is the base name of + `FileInput.name`, or `file` if `name` is absent. Its `Content-Type` is `FileInput.mimeType`. +- **MUST NOT** send more file bytes than the declared `size`. - **SHOULD NOT** start a transfer after `expiresAt`, and **MUST** report that file as `failed` if they do not start it. The target decides whether to accept a transfer. -- **MUST** limit transfer attempts to three per file per logical call. Each request sent to a target - is an attempt, including a request that answers a repeated upload input request. -- **MUST** report the file as `failed` on the retry after reaching that limit, and **MUST** stop - before that limit if a failure is permanent or a further attempt is unsafe. +- **MUST** bound transfer attempts per file per logical call. Each request sent to a target is an + attempt, including a request that answers a repeated upload input request. +- **MUST** report the file as `failed` on the retry after reaching that bound, and **MUST** stop + before it if a failure is permanent or a further attempt is unsafe. - **MUST NOT** automatically resend a `POST` unless they know the request did not reach the target. A transfer that answers a repeated upload input request is not a resend. - **MUST** complete the upload and retry without returning control to the model. @@ -423,18 +460,18 @@ and **MUST** respond as [Errors](#errors) specifies when a step fails: 1. Verify `requestState`. 2. Check the tool name and arguments against the values bound in `requestState`. -3. Claim `requestState` as specified in [Repeated Retries](#repeated-retries). If it is already - claimed, follow that section. -4. Check each `fileId` and status. If any file has status `failed`, stop without running the tool. +3. Check each `fileId` and status. If any file has status `failed`, stop without running the tool. If an outcome is missing, follow [MRTR error handling](https://modelcontextprotocol.io/specification/draft/basic/patterns/mrtr#error-handling) and do not run the tool. -5. Confirm with the storage backend that each file's bytes arrived, for example with a `HEAD` +4. Confirm with the storage backend that each file's bytes arrived, for example with a `HEAD` request or a session status query. Verify `digest`, and re-check size and type. The server **MUST NOT** run the tool with bytes it has not confirmed. +5. If running the tool twice is unsafe, claim `requestState` as specified in [Repeated + Retries](#repeated-retries). If it is already claimed, follow that section. 6. Perform any finalize step the backend needs, run the tool, and return its result. -If the bytes have not arrived at step 5, the server **MAY** return another `InputRequiredResult` +If the bytes have not arrived at step 4, the server **MAY** return another `InputRequiredResult` with a new `requestState` and a new or reissued target. It is not required to wait for the bytes to become visible first. A repeated upload input request can require a full re-upload, including when the bytes arrived but the backend had not yet made them visible. @@ -449,13 +486,28 @@ for their outcome again. Servers **MUST NOT** use a target for a different call. A client can repeat a retry if it does not receive the response. -Servers **SHOULD** claim a `requestState` atomically before running the tool, using storage every -instance shares, and **MUST** do so if running the tool twice is unsafe. A claimed `requestState` -counts as processed, even while the tool is running. +The upload flow is stateless by default. Each retry carries what the server needs in the request +and in `requestState`, and any instance can process it. A tool that is safe to run twice, such as +one that is idempotent on `digest` and its arguments, needs no claim. The server runs it again on a +repeated retry, and treats a file it already finalized as confirmed at step 4. -A server that receives a claimed `requestState` **MUST NOT** run the tool again. It **MUST** return -the original result to the same principal, or the `already_processed` error. If the earlier retry -produced an `InputRequiredResult`, it **SHOULD** return that result. +A tool that is unsafe to run twice needs a `requestState` that works only once. [MRTR leaves that to +the +server](https://modelcontextprotocol.io/specification/draft/basic/patterns/mrtr#server-requirements-basic-workflow). +Before running such a tool, the server **MUST** claim the `requestState` atomically, using storage +every instance shares. A claimed `requestState` counts as processed, even while the tool is running. + +A server that receives a claimed `requestState` **MUST NOT** run the tool again. While the tool is +running, the server **MUST** return an `InputRequiredResult` with the same `requestState` and no +`inputRequests`. After the tool finishes, the server **MUST** return the original result to the same +principal, or the `already_processed` error. A tool that can outlast a client or gateway timeout +**SHOULD** use the [Tasks extension](https://modelcontextprotocol.io/extensions/tasks/overview). + +A client that receives such an `InputRequiredResult` repeats the same retry. Clients **MUST** wait +between repeats and **MUST** bound their number. A client that reaches that bound treats the outcome +as unknown. + +A gateway can repeat a retry, and **MUST** pass `already_processed` through unchanged. A client that receives `already_processed`, or whose repeated retry fails `requestState` verification, **MUST NOT** treat the response as proof that the call failed, and **MUST NOT** repeat @@ -466,22 +518,21 @@ the call automatically. The outcome of the earlier attempt is unknown. Servers **SHOULD** issue targets that write to a staging location and that enforce the declared size. -Servers are responsible for the lifetime and cleanup of staged bytes. Servers: +Staged bytes are application data that `requestState` refers to. They are not protocol session +state. Servers manage their lifetime and cleanup, for example with storage-side expiry. Servers: - **MUST** keep staged bytes until the latest `requestState` bound to them expires, the call ends, or their target is replaced, whichever comes first. - **MUST NOT** expose a staged file as finished until they confirm it on the retry. -- **SHOULD** delete staged bytes when a call fails or a target is replaced, and **SHOULD** use - storage-side expiry for staged bytes with no retry. - **MUST** allow any instance that receives the retry to confirm the staged bytes. - **SHOULD** prevent further writes through a target before verifying its bytes, and **SHOULD** finalize the bytes they verified. -A target that writes to the final destination creates the file before the server confirms it. Such a -target **MUST** enforce the declared size, and the digest where the backend supports it. Servers -**SHOULD NOT** issue such a target unless the backend enforces both on the write and the write -cannot replace existing content. If a later check fails, the client reports `failed`, or no retry -arrives before the `requestState` expires, the server **MUST** remove the file. +A target that writes to the final destination creates the file before the server confirms it. +Servers **SHOULD NOT** issue such a target unless the backend enforces the declared size and digest +on the write, and the write cannot replace existing content. If a later check fails, the client +reports `failed`, or no retry arrives before the `requestState` expires, the server **MUST** remove +the file. Storage-side expiry can do this without tracking the call. ### Downloads @@ -584,18 +635,18 @@ Clients **MUST** reject a target if: 5. Its `headers` include `Authorization`, `Cookie`, a [forbidden request header](https://fetch.spec.whatwg.org/#forbidden-request-header), or a name or value that is not valid under [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110#section-5). -6. It is a `path` target or has `credentials: "include"`, and its `headers` include `Forwarded`, - `X-Original-URL`, `X-Rewrite-URL`, `X-HTTP-Method-Override`, or a name that starts with - `X-Forwarded-`. +6. It is a `path` target and its `headers` include `Forwarded`, `X-Original-URL`, `X-Rewrite-URL`, + `X-HTTP-Method-Override`, or a name that starts with `X-Forwarded-`. +7. It has `form`, and its `method` is not `POST` or its `headers` include `Content-Type`. +8. Its `form` has an empty `fileField` or field `name`. +9. Its `form` has a `fileField`, field `name`, or field `value` that contains a CR, LF, or NUL + character. #### Credentials -Clients **MUST** honor `include` only on a `path` target, or when `url` has the same [RFC -6454](https://www.rfc-editor.org/rfc/rfc6454) origin as the server's [canonical -URI](https://modelcontextprotocol.io/specification/draft/basic/authorization#canonical-server-uri). -A parent domain or wildcard does not match. Behind a gateway, the comparison uses the canonical URI -of the server whose credential is attached. Otherwise the client **MUST** treat the target as `omit` -or reject it. +`credentials: "include"` is valid only on a `path` target. A `path` resolves under the MCP address, +so the credential stays within the server it was issued for. Clients **MUST** reject a `url` target +with `credentials: "include"`. #### Addresses @@ -627,15 +678,14 @@ has no canonical URI or MCP address. The component that holds the bytes performs the transfer. -A gateway **MAY** replace a target's `url` or `path`, `method`, `headers`, and `credentials` with a -relay at its own origin. It **MUST** do so for a `url` target with `credentials: "include"` when it -holds the credential for that server. +A gateway **MAY** replace a target's `url` or `path`, `method`, `form`, `headers`, and `credentials` +with a relay at its own origin. A gateway that relays an upload **MUST**: - Check the original target against [Targets](#targets). -- Forward the bytes with the original target's `method`, `headers`, and `credentials`, resolving a - `path` against the address it uses for the server. +- Forward the bytes with the original target's `method`, `form`, `headers`, and `credentials`, + resolving a `path` against the address it uses for the server. - Accept bytes only from the caller. The relay **MUST NOT** answer `2xx` until the original target has. @@ -659,8 +709,8 @@ and bytes conditions apply to a retry that answers an upload input request. `isE | Condition | Response | | ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- | -| Inline `data` exceeds `maxInlineBytes` in total, or the server does not accept it | JSON-RPC `-32602`, `data.reason` `"inline_too_large"` | -| File omits `data`, server advertises `upload`, request did not declare it | `-32021` with `data.requiredCapabilities` listing the missing capability | +| Inline `data` exceeds `maxInlineBytes` in total, or the server does not accept it | JSON-RPC `-32602`, reason `inline_too_large` | +| Non-empty file omits `data`, server has `upload`, request did not declare it | `-32021` with `data.requiredCapabilities` listing the missing capability | | Server knows the tool can only return a download, request did not declare `download` | `-32021` as above, before running the tool | | Server cannot accept a file inline or out of band | `isError: true` | | Inline data cannot be decoded, or fails the size or digest check | `isError: true` | @@ -671,7 +721,17 @@ and bytes conditions apply to a retry that answers an upload input request. `isE | A file has status `failed` | `isError: true`, without running the tool | | Bytes have not arrived, and the server does not start a new round | `isError: true`, without running the tool | | Bytes arrived but fail a check | `isError: true`, without running the tool | -| `requestState` already claimed | JSON-RPC `-32602`, `data.reason` `"already_processed"`, or the original result to the same principal | +| `requestState` already claimed, and the tool has finished | Original result to the same principal, or JSON-RPC `-32602`, reason `already_processed` | + +The error's `data` carries the reason under the extension identifier: + +```json +{ + "code": -32602, + "message": "Invalid params", + "data": { "io.modelcontextprotocol/files": { "reason": "already_processed" } } +} +``` `inline_too_large` is an exception to [SEP-1303](https://modelcontextprotocol.io/seps/1303-input-validation-errors-as-tool-execution-errors). @@ -690,8 +750,8 @@ SDKs require an MRTR extension point to support the upload input request. ## Security Implications -Clients and servers **MUST** redact target URLs, target headers, and inline `data` in logs, traces, -and error reports. +Clients and servers **MUST** redact target URLs, target headers, `form` fields, and inline +`data` in logs, traces, and error reports. ### Server-Directed Requests @@ -702,9 +762,9 @@ does not implement it. Such a gateway cannot meet the rules in [Targets](#target ### Exfiltration A malicious server can specify any public URL as an `omit` target and receive the file at that URL. -`POST` reaches more existing endpoints than `PUT`. The host approval required before the initial -call mitigates this risk and applies to agent-created files. A compromised agent can write sensitive -data to a file and try to send it. +`POST` reaches more existing endpoints than `PUT`, and `form` also reaches HTML form handlers. The +host approval required before the initial call mitigates this risk and applies to agent-created +files. A compromised agent can write sensitive data to a file and try to send it. ### Binding @@ -737,13 +797,15 @@ accept. The reference implementation will include a TypeScript SDK branch with the upload input request and a client transfer helper, a reference server in front of an object store, and conformance scenarios -for inline transfer, out-of-band transfer, rejection on metadata, a failed upload, a digest -mismatch, a repeated retry, and a download. +for inline transfer, out-of-band transfer, a `form` target, rejection on metadata, a failed +upload, a digest mismatch, a repeated retry, and a download. ## Open Questions 1. **MRTR extensibility.** How should the specification enable extension-defined input requests? - Repeated retries and a dedicated error code may belong in the same change. + Repeated retries may belong in the same change. That change should also allocate a core error + code for an already-processed `requestState`, because extensions cannot use the reserved range. + This extension would then replace `already_processed` with that code. 2. **Finalize.** Some backends return a commit handle in the transfer's HTTP response, such as an ETag or block ID, and the retry has no field for it. Owners of backends with an explicit commit step are asked to confirm whether finalizing on the retry works. An optional per-file field in @@ -753,8 +815,6 @@ mismatch, a repeated retry, and a download. the inner call. The group needs to answer this before the extension leaves the experimental phase. 4. **Resources.** Should a later revision apply this flow to resources? -5. **Multipart.** Some storage APIs accept only `multipart/form-data`. A server can proxy such an - API with an `include` target, or a later revision can add a `form` target. ## Future Work From 5ed80b46a6922b215bf07b5cd4fe2be83a4f72d4 Mon Sep 17 00:00:00 2001 From: Den Delimarsky Date: Thu, 5 Nov 2026 08:23:07 +0000 Subject: [PATCH 13/19] Fix claim scope, restore host-bound credentials, and harden form and transfer rules Co-authored-by: den --- specification/draft/file-uploads.md | 214 +++++++++++++++++++--------- 1 file changed, 148 insertions(+), 66 deletions(-) diff --git a/specification/draft/file-uploads.md b/specification/draft/file-uploads.md index 29458c4..4376ee1 100644 --- a/specification/draft/file-uploads.md +++ b/specification/draft/file-uploads.md @@ -12,15 +12,15 @@ implementation exists. Anything here can change before then, including the exten ## Abstract -This extension defines how MCP clients transfer files to and from tools over HTTPS. Files -transferred out of band do not enter model context or JSON-RPC messages. +This extension defines how MCP clients transfer files to and from tools. Files transferred out of +band do not enter model context or JSON-RPC messages. A client has two options for uploading a file: - **Inline**: The client sends a small file in the tool call. - **Out of band**: The client calls the tool with the file's metadata. The server either returns an - upload target or rejects the call based on the metadata. If the server returns a target, the client - uploads the bytes and retries the call. + upload target or rejects the call based on the metadata. If the server returns a target, the + client uploads the bytes and retries the call. To download a file, the client fetches it from a download target that the tool lists in its result. @@ -78,7 +78,7 @@ The settings object has the following fields: transfer. If `upload` or `download` is absent, the sender does not support that direction. Receivers **MUST** -ignore fields they do not recognize. +ignore fields they do not recognize in these settings. Clients: @@ -88,8 +88,8 @@ Clients: - **MUST NOT** send a file out of band to a server that did not advertise `upload`. A server that accepts inline `data` **MUST** advertise `maxInlineBytes`. A server that does not -accept inline transfer and advertises `upload` requires an upload round trip for every file, -regardless of size. +accept inline transfer and advertises `upload` requires an upload round trip for every non-empty +file, regardless of size. For example, a server with a `maxInlineBytes` of 1 MiB and a file argument with a `maxSize` of 150 MiB accepts a 200 KiB file or two 400 KiB files inline in one request, accepts a 40 MiB file out of @@ -203,6 +203,7 @@ and digest before using the bytes. A file with `size` 0 has no bytes to transfer. Clients **MUST** omit `data` for it. Servers **MUST** treat it as an empty file, **MUST** check its `digest`, and **MUST NOT** issue a target for it. +Servers **SHOULD** treat an empty `data` string the same way. A tool can still reject an empty file. Clients that do not support this extension ignore the keyword. Validators that reject unknown keywords by default, such as Ajv in strict mode, require configuration to allow `x-mcp-file`. @@ -325,11 +326,13 @@ interface UploadTarget { headers?: { [name: string]: string }; /** * "omit": the target carries its own authorization. "include": the client attaches its MCP - * credential. Valid only on a path target. + * credential. */ credentials: "omit" | "include"; /** ISO 8601 timestamp. Advisory latest start time for the transfer. */ expiresAt?: string; + /** Carries no transfer semantics. */ + _meta?: { [key: string]: unknown }; } interface UploadForm { @@ -392,23 +395,36 @@ Clients: - **MUST** support `form` targets if they declare `upload`. - **MUST** send each entry in a `form` target's `fields` as one part, in array order, then send the - file as the last part, named `fileField`. The file part's `filename` is the base name of - `FileInput.name`, or `file` if `name` is absent. Its `Content-Type` is `FileInput.mimeType`. + file as the last part, named `fileField`. +- **MUST** quote each part's `name` and the file's `filename` in UTF-8, writing `"`, CR, and LF as + `%22`, `%0D`, and `%0A`, as the [WHATWG multipart/form-data + encoding](https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#multipart/form-data-encoding-algorithm) + does, and **MUST NOT** send `filename*`. +- **MUST** set `filename` to the base name of `FileInput.name`, taken after both `/` and `\`. If + `name` is absent, or the base name is empty, `.`, or `..`, the `filename` is `file`. +- **MUST** set the file part's `Content-Type` to `FileInput.mimeType` if it is a valid media type, + and otherwise to `application/octet-stream`. +- **MUST** use a boundary with at least 128 bits of randomness. +- **MUST** send `Content-Length`. With `form`, it includes the multipart framing, so it exceeds + `size`. - **MUST NOT** send more file bytes than the declared `size`. - **SHOULD NOT** start a transfer after `expiresAt`, and **MUST** report that file as `failed` if they do not start it. The target decides whether to accept a transfer. -- **MUST** bound transfer attempts per file per logical call. Each request sent to a target is an - attempt, including a request that answers a repeated upload input request. +- **MUST** bound transfer attempts per file per logical call, and **MUST** answer at least one + repeated upload input request. Each request sent to a target is an attempt, including a request + that answers a repeated upload input request. - **MUST** report the file as `failed` on the retry after reaching that bound, and **MUST** stop before it if a failure is permanent or a further attempt is unsafe. -- **MUST NOT** automatically resend a `POST` unless they know the request did not reach the target. +- **MUST NOT** automatically resend a `POST` unless they know the target did not apply the request. A transfer that answers a repeated upload input request is not a resend. +- **MUST** treat a `401` or `403` from a target as a failed transfer, and **MUST NOT** start + authorization discovery from it. - **MUST** complete the upload and retry without returning control to the model. - **SHOULD** send the retry with status `failed` if the user cancels. Servers **SHOULD NOT** echo upload URLs in the `CallToolResult`. Servers that issue `omit` targets -to browser clients **SHOULD** configure the storage to respond to the CORS preflight request for the -listed headers. +to browser clients **SHOULD** configure the storage to allow the client's origin on the transfer +response, and on the preflight for any listed headers, because the client must read the status. #### Retry @@ -460,18 +476,20 @@ and **MUST** respond as [Errors](#errors) specifies when a step fails: 1. Verify `requestState`. 2. Check the tool name and arguments against the values bound in `requestState`. -3. Check each `fileId` and status. If any file has status `failed`, stop without running the tool. +3. If the logical call is claimed, follow [Repeated Retries](#repeated-retries) and skip the + remaining steps. +4. Check each `fileId` and status. If any file has status `failed`, stop without running the tool. If an outcome is missing, follow [MRTR error handling](https://modelcontextprotocol.io/specification/draft/basic/patterns/mrtr#error-handling) and do not run the tool. -4. Confirm with the storage backend that each file's bytes arrived, for example with a `HEAD` - request or a session status query. Verify `digest`, and re-check size and type. The server +5. Confirm with the storage backend that each requested file's bytes arrived, for example with a + `HEAD` request or a session status query. Verify `digest`, and re-check size and type. The server **MUST NOT** run the tool with bytes it has not confirmed. -5. If running the tool twice is unsafe, claim `requestState` as specified in [Repeated - Retries](#repeated-retries). If it is already claimed, follow that section. -6. Perform any finalize step the backend needs, run the tool, and return its result. +6. If running the tool twice is unsafe, claim the logical call as specified in [Repeated + Retries](#repeated-retries). If another retry holds the claim, follow that section. +7. Perform any finalize step the backend needs, run the tool, and return its result. -If the bytes have not arrived at step 4, the server **MAY** return another `InputRequiredResult` +If the bytes have not arrived at step 5, the server **MAY** return another `InputRequiredResult` with a new `requestState` and a new or reissued target. It is not required to wait for the bytes to become visible first. A repeated upload input request can require a full re-upload, including when the bytes arrived but the backend had not yet made them visible. @@ -486,28 +504,27 @@ for their outcome again. Servers **MUST NOT** use a target for a different call. A client can repeat a retry if it does not receive the response. -The upload flow is stateless by default. Each retry carries what the server needs in the request -and in `requestState`, and any instance can process it. A tool that is safe to run twice, such as -one that is idempotent on `digest` and its arguments, needs no claim. The server runs it again on a -repeated retry, and treats a file it already finalized as confirmed at step 4. +The upload 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. A +tool that is safe to run twice, such as one that is idempotent on `digest` and its arguments, needs +no claim. The server runs it again on a repeated retry, and treats a file it already finalized as +confirmed at step 5. -A tool that is unsafe to run twice needs a `requestState` that works only once. [MRTR leaves that to -the +A tool that is unsafe to run twice must run only once per logical call, which [MRTR leaves to the server](https://modelcontextprotocol.io/specification/draft/basic/patterns/mrtr#server-requirements-basic-workflow). -Before running such a tool, the server **MUST** claim the `requestState` atomically, using storage -every instance shares. A claimed `requestState` counts as processed, even while the tool is running. +Servers **MUST** bind one call identifier into every `requestState` of a logical call. Before +running such a tool, the server **MUST** claim that identifier atomically, using storage every +instance shares, and **MUST** keep the claim until every `requestState` of the call has expired. A +claimed call counts as processed, even while the tool is running. -A server that receives a claimed `requestState` **MUST NOT** run the tool again. While the tool is -running, the server **MUST** return an `InputRequiredResult` with the same `requestState` and no -`inputRequests`. After the tool finishes, the server **MUST** return the original result to the same -principal, or the `already_processed` error. A tool that can outlast a client or gateway timeout -**SHOULD** use the [Tasks extension](https://modelcontextprotocol.io/extensions/tasks/overview). +A server that receives a retry for a claimed call **MUST NOT** run the tool again and **MUST NOT** +start a new round. It **MUST** return the original result to the same principal, or the +`already_processed` error. A tool that can outlast a client or gateway timeout **SHOULD** use the +[Tasks extension](https://modelcontextprotocol.io/extensions/tasks/overview). -A client that receives such an `InputRequiredResult` repeats the same retry. Clients **MUST** wait -between repeats and **MUST** bound their number. A client that reaches that bound treats the outcome -as unknown. - -A gateway can repeat a retry, and **MUST** pass `already_processed` through unchanged. +A gateway can repeat a retry, and **MUST** pass `already_processed` through unchanged. A gateway +that repeats a retry and receives a `requestState` verification failure **MUST** return +`already_processed` to its caller. A client that receives `already_processed`, or whose repeated retry fails `requestState` verification, **MUST NOT** treat the response as proof that the call failed, and **MUST NOT** repeat @@ -521,8 +538,8 @@ size. Staged bytes are application data that `requestState` refers to. They are not protocol session state. Servers manage their lifetime and cleanup, for example with storage-side expiry. Servers: -- **MUST** keep staged bytes until the latest `requestState` bound to them expires, the call ends, - or their target is replaced, whichever comes first. +- **MUST** keep staged bytes until the latest `requestState` bound to them expires or their target + is replaced, whichever comes first. - **MUST NOT** expose a staged file as finished until they confirm it on the retry. - **MUST** allow any instance that receives the retry to confirm the staged bytes. - **SHOULD** prevent further writes through a target before verifying its bytes, and **SHOULD** @@ -589,6 +606,8 @@ interface DownloadTarget { headers?: { [name: string]: string }; credentials: "omit" | "include"; expiresAt?: string; + /** Carries no transfer semantics. */ + _meta?: { [key: string]: unknown }; } ``` @@ -598,12 +617,22 @@ Servers: `download`, however that result is delivered. - **MUST NOT** list a target until the file can be fetched. This extension does not define how a tool waits for a file to exist. -- **SHOULD** include `size` when they know it. +- **SHOULD** include `size` and `digest` when they know them. The client sends a `GET` to the target's `url` or resolved `path` with `headers`. A `200` response, or a `206` response to a range request, indicates success. Clients **MAY** use range requests to split or resume a download. +`Content-Length` describes the message, not the file. It counts encoded bytes when +`Content-Encoding` applies, counts one part on a `206`, and can be absent. Clients **MUST NOT** use +it in place of the `size` and `digest` checks, and **MUST** treat a response that HTTP reports as +incomplete as a failed transfer ([RFC 9112](https://www.rfc-editor.org/rfc/rfc9112#section-8)). + +Clients that use range requests **MUST** combine parts only if they share the same strong validator, +and otherwise **MUST** restart. Offsets apply to the bytes as sent, so clients **MUST NOT** derive +ranges from `size` for a representation with a `Content-Encoding`. Clients **MUST NOT** send a range +request to a target with `size` 0. + Clients: - **MUST** check `size` and `digest`, if present, after receiving the whole file, and **MUST** @@ -628,31 +657,55 @@ These rules apply to every upload and download target. Clients **MUST** reject a target if: 1. It does not have exactly one of `url` and `path`. -2. Its `url` is not an absolute `https` URL. +2. Its `url` is not an absolute `https` URL, or it contains userinfo. 3. Its `path` does not match `1*( "/" segment )`, where `segment` is one or more ASCII letters, digits, `-`, `_`, `~`, or `.`, and is neither `.` nor `..`. 4. Its `credentials` is missing or unrecognized, or its `method` is unrecognized. 5. Its `headers` include `Authorization`, `Cookie`, a [forbidden request header](https://fetch.spec.whatwg.org/#forbidden-request-header), or a name or value that is not valid under [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110#section-5). -6. It is a `path` target and its `headers` include `Forwarded`, `X-Original-URL`, `X-Rewrite-URL`, - `X-HTTP-Method-Override`, or a name that starts with `X-Forwarded-`. -7. It has `form`, and its `method` is not `POST` or its `headers` include `Content-Type`. -8. Its `form` has an empty `fileField` or field `name`. -9. Its `form` has a `fileField`, field `name`, or field `value` that contains a CR, LF, or NUL - character. +6. It is a `path` target or has `credentials: "include"`, and its `headers` include `Forwarded`, + `X-Original-URL`, `X-Rewrite-URL`, `X-HTTP-Method-Override`, `X-HTTP-Method`, + `X-Method-Override`, or a name that starts with `X-Forwarded-`. +7. It has `form`, and its `method` is not `POST` or its `headers` include `Content-Type`, + `Content-Encoding`, `Content-Range`, `Content-Digest`, or `Repr-Digest`. +8. Its `form` has a missing or empty `fileField`, a missing `fields`, or an empty field `name`. +9. Its `form` has a `fileField` or field `name` that contains a control character, `"`, or `\`, or a + field `value` that contains a CR, LF, or NUL character. +10. A field `name` in its `form` equals `fileField`. +11. It does not conform to its type. A required member is missing, a member has the wrong JSON type, + or, at any depth, a member other than `_meta` is not defined by this extension or by a + capability the client declared. +12. It has a `digest` whose `algorithm` is not `sha-256`. + +Member names are case-sensitive, and header names are compared case-insensitively. Clients **MUST** +treat a member whose value is `null` as absent, and **MUST** ignore `_meta`. Servers **MUST NOT** +send a target member other than `_meta` that this revision does not define, unless the request +declared the capability that defines it. #### Credentials -`credentials: "include"` is valid only on a `path` target. A `path` resolves under the MCP address, -so the credential stays within the server it was issued for. Clients **MUST** reject a `url` target -with `credentials: "include"`. +With `include`, clients **MUST** attach the `Authorization` credential they send on MCP requests to +that MCP address, and no other. With `omit`, clients **MUST NOT** attach that credential, cookies, +client certificates, or platform-supplied authentication. + +Clients **MUST** honor `include` only on a `path` target, or when `url` has the same [RFC +6454](https://www.rfc-editor.org/rfc/rfc6454) origin as the server's [canonical +URI](https://modelcontextprotocol.io/specification/draft/basic/authorization#canonical-server-uri). +A parent domain or wildcard does not match. Behind a gateway, the comparison uses the canonical URI +of the server whose credential is attached. Otherwise the client **MUST** reject the target. + +`include` lets a server choose the path, method, and body of a request sent with the client's +credential. Servers **MUST NOT** issue an `include` target for a URL that another application +serves, including one behind the same reverse proxy. Servers **MUST** verify that the credential on +an `include` target identifies the principal the target was issued to. #### Addresses Clients **MUST** check the address they connect to, and **MUST NOT** connect to one the [IANA special-purpose registries](https://www.rfc-editor.org/rfc/rfc6890) mark as not globally reachable, -unless an operator allowlist permits it or the target is a `path`. A client that cannot observe the +unless an operator allowlist permits it or the target is a `path` under an `https` or loopback MCP +address. A client that cannot observe the address, such as a browser or a client behind a forward proxy, **MUST** rely on a platform or proxy mechanism that enforces the same restriction, or **MUST** reject `url` targets not on an operator allowlist. Hosts **MAY** restrict `omit` targets to an allowlist of origins. @@ -660,14 +713,18 @@ allowlist. Hosts **MAY** restrict `omit` targets to an allowlist of origins. #### Redirects Servers **SHOULD** issue targets that respond without a redirect. Clients **MUST NOT** follow -redirects, and **MUST** treat a `3xx` response as a failed transfer. +redirects, and **MUST** treat a `3xx` response as a failed transfer. A `form` target **MUST NOT** +ask the storage to answer success with a redirect. #### Resolving `path` A client resolves a `path` by appending it to the path of the MCP address, without a doubled `/` and before any query string. With the MCP address `https://gateway.example.com/mcp/abc`, `/uploads` -resolves to `https://gateway.example.com/mcp/abc/uploads`. A `path` target inherits the scheme of -the MCP address. Clients **MUST** reject a `path` target over `http`, except on loopback. +resolves to `https://gateway.example.com/mcp/abc/uploads`. The query string of the MCP address is kept. +A `path` target inherits the scheme of the MCP address. If the client followed a redirect to reach +the MCP address, it resolves against the redirect's result. Clients **MUST** reject a `path` target +over `http` unless the MCP address is on loopback or an operator allowlist permits it. A `digest` +received over `http` does not protect against an active attacker on that network. #### stdio @@ -676,10 +733,14 @@ has no canonical URI or MCP address. ### Gateways -The component that holds the bytes performs the transfer. +The component that holds the bytes to upload, or that will keep a downloaded file, performs the +transfer unless a gateway relays it. A gateway **MAY** replace a target's `url` or `path`, `method`, `form`, `headers`, and `credentials` -with a relay at its own origin. +with a relay at its own origin. It **MUST** do so for a `url` target with `credentials: "include"` +when it holds the credential for that server. A gateway that replaces targets **MUST** forward only +the `upload` and `download` settings it implements, and **MUST NOT** add a target member other than +under `_meta`. A gateway that relays an upload **MUST**: @@ -690,11 +751,21 @@ A gateway that relays an upload **MUST**: The relay **MUST NOT** answer `2xx` until the original target has. +A relay for a `form` target **SHOULD** present a target without `form` and build the multipart body +itself, taking `filename`, the part's `Content-Type`, and `size` from the `FileInput` that `fileId` +names. A relay that keeps `form` **MUST** send the original `fields` upstream. + A gateway that relays a download **MUST** check the original target against [Targets](#targets), fetch the bytes from it with the original target's `headers` and `credentials`, resolving a `path` against the address it uses for the server, and serve them only to the caller. The relay **MUST NOT** answer `2xx` until the original target has. +A relay **MUST** pass a download target's `size` and `digest` through unchanged unless it changes +the bytes, and then **MUST** replace them with values for the bytes it serves, or remove them. A +relay that checks a file while streaming it **MUST** abort the response without completing it if a +check fails. A relay **MUST NOT** delimit a response by closing the connection, and **MUST** apply +its own limit when `size` is absent. + A gateway that presents more than one server at one MCP address **MUST** replace every `path` target. Any other gateway that passes a `path` target through unchanged **MUST** route everything under that MCP address to that one server, **MUST NOT** forward the caller's credential upstream, @@ -713,15 +784,15 @@ and bytes conditions apply to a retry that answers an upload input request. `isE | Non-empty file omits `data`, server has `upload`, request did not declare it | `-32021` with `data.requiredCapabilities` listing the missing capability | | Server knows the tool can only return a download, request did not declare `download` | `-32021` as above, before running the tool | | Server cannot accept a file inline or out of band | `isError: true` | -| Inline data cannot be decoded, or fails the size or digest check | `isError: true` | -| Duplicate `id` values in file arguments | JSON-RPC `-32602` | +| Inline data cannot be decoded, or an inline or empty file fails a size or digest check | `isError: true` | +| Duplicate `id` values in file arguments | `isError: true` | | `requestState` fails verification | JSON-RPC `-32602` | | Tool name or arguments differ from `requestState` | JSON-RPC `-32602` | | Duplicate or unknown `fileId`, or unknown status | JSON-RPC `-32602` | | A file has status `failed` | `isError: true`, without running the tool | | Bytes have not arrived, and the server does not start a new round | `isError: true`, without running the tool | | Bytes arrived but fail a check | `isError: true`, without running the tool | -| `requestState` already claimed, and the tool has finished | Original result to the same principal, or JSON-RPC `-32602`, reason `already_processed` | +| Logical call already claimed | Original result to the same principal, or JSON-RPC `-32602`, reason `already_processed` | The error's `data` carries the reason under the extension identifier: @@ -759,6 +830,14 @@ A client that does not enforce [Targets](#targets) is exposed to server-side req leak the user's credential. Operators **SHOULD NOT** enable this extension behind a gateway that does not implement it. Such a gateway cannot meet the rules in [Targets](#targets). +Enforcing [Targets](#targets) does not stop a malicious server from sending a request of its choice +to a public address, from the client's network position. With `form`, the server controls most of +the request body. Targets do not protect services that trust a client's address, or form handlers +without their own request-forgery defense. The `status` a client reports also tells the server +whether the target accepted the request. Host approval happens before the target is known, so it +does not cover this risk. Hosts **SHOULD** restrict `omit` targets to an allowlist of origins where +an operator can provide one, and **SHOULD** do so for `form` targets in unattended use. + ### Exfiltration A malicious server can specify any public URL as an `omit` target and receive the file at that URL. @@ -797,15 +876,16 @@ accept. The reference implementation will include a TypeScript SDK branch with the upload input request and a client transfer helper, a reference server in front of an object store, and conformance scenarios -for inline transfer, out-of-band transfer, a `form` target, rejection on metadata, a failed -upload, a digest mismatch, a repeated retry, and a download. +for inline transfer, out-of-band transfer, a `form` target, target rejection, rejection on metadata, +a failed upload, a digest mismatch, a repeated retry, and a download. ## Open Questions 1. **MRTR extensibility.** How should the specification enable extension-defined input requests? Repeated retries may belong in the same change. That change should also allocate a core error - code for an already-processed `requestState`, because extensions cannot use the reserved range. - This extension would then replace `already_processed` with that code. + code for an already-processed call. Extensions cannot use the reserved range. Core allows a code + outside it, but such a code has no registry and can collide, so this draft reuses `-32602` with a + namespaced reason. This extension would then replace `already_processed` with the core code. 2. **Finalize.** Some backends return a commit handle in the transfer's HTTP response, such as an ETag or block ID, and the retry has no field for it. Owners of backends with an explicit commit step are asked to confirm whether finalizing on the retry works. An optional per-file field in @@ -815,6 +895,8 @@ upload, a digest mismatch, a repeated retry, and a download. the inner call. The group needs to answer this before the extension leaves the experimental phase. 4. **Resources.** Should a later revision apply this flow to resources? +5. **`omit` binding.** Should `omit` targets be bound to an origin or allowlist, and should wildcard + hosts be allowed? ## Future Work From bb2edc7898b13ea1b6421a16b51720184bc38560 Mon Sep 17 00:00:00 2001 From: Den Delimarsky Date: Fri, 6 Nov 2026 16:53:16 +0000 Subject: [PATCH 14/19] Simplify uploads to one round and single-use state, and remove status and digest Co-authored-by: den --- specification/draft/file-uploads.md | 309 +++++++++++++--------------- 1 file changed, 142 insertions(+), 167 deletions(-) diff --git a/specification/draft/file-uploads.md b/specification/draft/file-uploads.md index 4376ee1..29a36f6 100644 --- a/specification/draft/file-uploads.md +++ b/specification/draft/file-uploads.md @@ -36,7 +36,8 @@ This revision applies to tools. Resource behavior is unchanged from the base pro - **MCP address**: The URL a client sends MCP requests to. This URL can vary per hop and can differ from the server's canonical URI. - **Target**: The URL or path to which a client sends file bytes, or from which it fetches them. -- **Logical call**: An initial `tools/call` request and its retries. +- **Logical call**: An initial `tools/call` request and its retries, on one hop. A gateway's call to + a server is a separate logical call from its caller's call to the gateway. In [Transfer](#transfer), [Downloads](#downloads), and [Targets](#targets), client means the component that performs the transfer. @@ -61,7 +62,6 @@ Servers advertise the extension in the `server/discover` response: "extensions": { "io.modelcontextprotocol/files": { "upload": {}, - "download": {}, "maxInlineBytes": 1048576 } } @@ -72,13 +72,14 @@ Servers advertise the extension in the `server/discover` response: The settings object has the following fields: - `upload` _(optional)_: The sender supports out-of-band uploads. -- `download` _(optional)_: The sender supports out-of-band downloads. +- `download` _(optional, clients only)_: The client accepts download targets. - `maxInlineBytes` _(optional, servers only)_: The largest total decoded size of inline `data` values the server accepts in one request. If `0` or absent, the server does not accept inline transfer. -If `upload` or `download` is absent, the sender does not support that direction. Receivers **MUST** -ignore fields they do not recognize in these settings. +If `upload` is absent, the sender does not support out-of-band uploads. If a client omits +`download`, the server does not send it download targets. Receivers **MUST** ignore fields they do +not recognize in these settings. Clients: @@ -153,8 +154,6 @@ interface FileInput { mimeType: string; /** Non-negative integer; decoded size in bytes. */ size: number; - /** SHA-256 over the original bytes. */ - digest: { algorithm: "sha-256"; value: string /* base64url, no padding */ }; /** * RFC 4648 base64-encoded file bytes. If absent and size is greater than 0, the client requests * an out-of-band upload. @@ -180,17 +179,9 @@ This can be represented in a server's schema as follows: "name": { "type": "string" }, "mimeType": { "type": "string" }, "size": { "type": "integer", "minimum": 0 }, - "digest": { - "type": "object", - "properties": { - "algorithm": { "const": "sha-256" }, - "value": { "type": "string" } - }, - "required": ["algorithm", "value"] - }, "data": { "type": "string" } }, - "required": ["id", "mimeType", "size", "digest"] + "required": ["id", "mimeType", "size"] } }, "required": ["folderId", "file"] @@ -199,10 +190,10 @@ This can be represented in a server's schema as follows: ``` Clients **MUST** make each `id` unique within the call. Servers **MUST** validate the decoded size -and digest before using the bytes. +before using the bytes. A file with `size` 0 has no bytes to transfer. Clients **MUST** omit `data` for it. Servers **MUST** -treat it as an empty file, **MUST** check its `digest`, and **MUST NOT** issue a target for it. +treat it as an empty file, and **MUST NOT** issue a target for it. Servers **SHOULD** treat an empty `data` string the same way. A tool can still reject an empty file. Clients that do not support this extension ignore the keyword. Validators that reject unknown @@ -246,11 +237,7 @@ include the required "id": "file-1", "name": "Q3 report.pdf", "mimeType": "application/pdf", - "size": 41204133, - "digest": { - "algorithm": "sha-256", - "value": "uU0nuZNNPgilLlLX2n2r-sSE7-N6U4D6ZVe-_rYh2sU" - } + "size": 41204133 } } } @@ -262,8 +249,8 @@ to storage the server designates, or apply operator policy when no user is prese Servers **SHOULD** perform every check that does not require the bytes, such as destination authorization, quota, `accept`, and `maxSize`, and return a `CallToolResult` with `isError: true` if -one fails. Servers **MUST** treat the metadata as client-asserted and re-check size, digest, and -type once the bytes arrive. +one fails. Servers **MUST** treat the metadata as client-asserted and re-check size and type once +the bytes arrive. The tool determines whether an existing file with the same name is a conflict. A tool that supports conditional writes accepts the revision or condition as a tool argument. @@ -289,10 +276,7 @@ MRTR, where keys are server-assigned. { "fileId": "file-1", "url": "https://upload.example.com/u/01JD3V7K2M?sig=...", - "headers": { - "Content-Type": "application/pdf", - "x-amz-checksum-sha256": "uU0nuZNNPgilLlLX2n2r+sSE7+N6U4D6ZVe+/rYh2sU=" - }, + "headers": { "Content-Type": "application/pdf" }, "credentials": "omit", "expiresAt": "2026-11-04T18:30:00Z" } @@ -368,13 +352,17 @@ Servers: - **MUST** return an upload input request only on `tools/call`, and only to a request that declared `upload`. - **MUST** make each `fileId` match one non-empty file argument without `data`. -- **MUST NOT** give a file more than one outstanding target in a round. +- **MUST** list every non-empty file argument without `data` in the upload input request, each + `fileId` once. +- **MUST** record in `requestState` that an upload input request was issued, and carry that record + into every later `requestState` of the logical call. +- **MUST NOT** return an upload input request on a retry whose `requestState` holds that record. +- **MUST NOT** use a target for a different call. - **MUST** return the upload input request before creating a task, if they use the [Tasks extension](https://modelcontextprotocol.io/extensions/tasks/overview). -- **MUST** bind the tool name, all arguments, each file's current target, and the authenticated +- **MUST** bind the tool name, all arguments, each file's target, and the authenticated principal, if any, to `requestState`. -- **MUST NOT** allow a target to remain valid after the latest `requestState` it was issued under - expires. +- **MUST NOT** allow a target to remain valid after the `requestState` it was issued under expires. Servers **MUST** handle `requestState` as the [MRTR server rules](https://modelcontextprotocol.io/specification/draft/basic/patterns/mrtr#server-requirements-basic-workflow) @@ -408,19 +396,24 @@ Clients: - **MUST** send `Content-Length`. With `form`, it includes the multipart framing, so it exceeds `size`. - **MUST NOT** send more file bytes than the declared `size`. -- **SHOULD NOT** start a transfer after `expiresAt`, and **MUST** report that file as `failed` if - they do not start it. The target decides whether to accept a transfer. -- **MUST** bound transfer attempts per file per logical call, and **MUST** answer at least one - repeated upload input request. Each request sent to a target is an attempt, including a request - that answers a repeated upload input request. -- **MUST** report the file as `failed` on the retry after reaching that bound, and **MUST** stop - before it if a failure is permanent or a further attempt is unsafe. +- **SHOULD NOT** start a transfer after `expiresAt`. The target decides whether to accept a + transfer. +- **MAY** repeat a `PUT` to the same target after a failed attempt, and **MUST** bound attempts per + file. Each request sent to a target is an attempt. +- **MUST** stop after reaching that bound, and **MUST** stop before it if a failure is permanent or + a further attempt is unsafe. +- **MUST** treat the call as failed if they receive a second upload input request in the logical + call, and **MUST NOT** transfer to its targets or retry the call. - **MUST NOT** automatically resend a `POST` unless they know the target did not apply the request. - A transfer that answers a repeated upload input request is not a resend. - **MUST** treat a `401` or `403` from a target as a failed transfer, and **MUST NOT** start authorization discovery from it. -- **MUST** complete the upload and retry without returning control to the model. -- **SHOULD** send the retry with status `failed` if the user cancels. +- **MUST** finish the upload flow without returning control to the model. + +Clients **MUST** send the retry only if every requested file received a `2xx` response. If a target +is rejected, a transfer fails, or the user cancels, the client **MUST NOT** send the retry, and +**SHOULD** stop any other transfer in the logical call. The tool has not run. A gateway that +performs the transfer itself **MUST** return `isError: true` to its caller. A host **MUST** tell the +user or model that the tool did not run, and **MUST NOT** claim that no bytes were stored. Servers **SHOULD NOT** echo upload URLs in the `CallToolResult`. Servers that issue `omit` targets to browser clients **SHOULD** configure the storage to allow the client's origin on the transfer @@ -429,7 +422,7 @@ response, and on the preflight for any listed headers, because the client must r #### Retry When retrying, the client **MUST** send a new JSON-RPC `id`, the same tool name and exact arguments, -the exact `requestState`, and one outcome per requested file under `inputResponses.uploads`: +the exact `requestState`, and an empty object under `inputResponses.uploads`: ```json { @@ -444,91 +437,77 @@ the exact `requestState`, and one outcome per requested file under `inputRespons "id": "file-1", "name": "Q3 report.pdf", "mimeType": "application/pdf", - "size": 41204133, - "digest": { - "algorithm": "sha-256", - "value": "uU0nuZNNPgilLlLX2n2r-sSE7-N6U4D6ZVe-_rYh2sU" - } - } - }, - "inputResponses": { - "uploads": { - "files": [{ "fileId": "file-1", "status": "completed" }] + "size": 41204133 } }, + "inputResponses": { "uploads": {} }, "requestState": "..." } } ``` ```typescript -interface UploadInputResponse { - files: { fileId: string; status: "completed" | "failed" }[]; -} +/** Empty. Its presence states that every requested file received a `2xx` response. */ +interface UploadInputResponse {} ``` -`status` is `completed` if the client observed a successful transfer, and `failed` otherwise. A file -with status `failed` ends the logical call. Clients **MUST NOT** include upload URLs or credentials -in the response. +The retry does not show that the bytes arrived. Clients **MUST NOT** include upload URLs or +credentials in the response. On a retry that answers an upload input request, the server **MUST** perform these steps in order, and **MUST** respond as [Errors](#errors) specifies when a step fails: -1. Verify `requestState`. +1. Verify `requestState`. A consumed `requestState` fails verification. 2. Check the tool name and arguments against the values bound in `requestState`. -3. If the logical call is claimed, follow [Repeated Retries](#repeated-retries) and skip the - remaining steps. -4. Check each `fileId` and status. If any file has status `failed`, stop without running the tool. - If an outcome is missing, follow [MRTR error - handling](https://modelcontextprotocol.io/specification/draft/basic/patterns/mrtr#error-handling) - and do not run the tool. -5. Confirm with the storage backend that each requested file's bytes arrived, for example with a - `HEAD` request or a session status query. Verify `digest`, and re-check size and type. The server - **MUST NOT** run the tool with bytes it has not confirmed. -6. If running the tool twice is unsafe, claim the logical call as specified in [Repeated - Retries](#repeated-retries). If another retry holds the claim, follow that section. -7. Perform any finalize step the backend needs, run the tool, and return its result. - -If the bytes have not arrived at step 5, the server **MAY** return another `InputRequiredResult` -with a new `requestState` and a new or reissued target. It is not required to wait for the bytes to -become visible first. A repeated upload input request can require a full re-upload, including when -the bytes arrived but the backend had not yet made them visible. - -The echoed `requestState` and `fileId` identify the current target. A new round requires a new -`requestState`, and a server **MAY** reissue the same target under it. If the server issues a -different target, it **MUST** prevent the old target from satisfying the new round. A server that -issues a later round **MUST** bind confirmed files to the new `requestState`, and **MUST NOT** ask -for their outcome again. Servers **MUST NOT** use a target for a different call. +3. Check that `inputResponses.uploads` is present. +4. Confirm with the storage backend that the bytes of each file bound in `requestState` arrived, for + example with a `HEAD` request or a session status query. Re-check size and + type. The server **MUST NOT** run the tool with bytes it has not confirmed. +5. If running the tool twice is unsafe, consume `requestState` as specified in [Repeated + Retries](#repeated-retries). If another retry consumed it first, respond as for step 1. +6. Perform any finalize step the backend needs, run the tool, and return its result. + +If a file's bytes have not arrived at step 4, the server **MAY** wait for them. If they do not +arrive, it **MUST NOT** run the tool. + +This extension narrows two [MRTR +rules](https://modelcontextprotocol.io/specification/draft/basic/patterns/mrtr) for upload input +requests only. A server does not prompt again, and does not re-request a missing `uploads` response. +Other input requests are unaffected. + +A server that returns a later `InputRequiredResult` in the logical call **MUST** bind each file's +confirmation to its `requestState`. On the retry that answers it, the server performs the steps +above that it has not completed. #### Repeated Retries -A client can repeat a retry if it does not receive the response. +If a client or gateway sends the retry that answers an upload input request, or a later retry in the +logical call, and does not receive the response, the outcome is unknown. The tool may have run, in +whole or in part. Clients and gateways **MUST NOT** send the retry again or start the call again +automatically, unless they know the server did not process it. + +A `-32602` response to such a retry does not show that the tool did not run. Clients and gateways +**MUST NOT** treat it as proof that the call failed, and **MUST NOT** repeat the call automatically. + +A gateway that does not receive the response **MUST** return `isError: true` to its caller, stating +that the outcome is unknown. A host **MUST** tell the user or model that the outcome is unknown. Any +new call needs the approval described in [Initial Call](#initial-call). The upload 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. A -tool that is safe to run twice, such as one that is idempotent on `digest` and its arguments, needs -no claim. The server runs it again on a repeated retry, and treats a file it already finalized as -confirmed at step 5. +tool that is safe to run twice, such as one that is idempotent on its arguments, needs +nothing more. If a retry repeats, the server runs the tool again, and treats a file it already +finalized as confirmed at step 4. -A tool that is unsafe to run twice must run only once per logical call, which [MRTR leaves to the +For a tool that is unsafe to run twice, the server **MUST** accept each `requestState` at most once, +from the one that answers the upload input request onward, as [MRTR leaves to the server](https://modelcontextprotocol.io/specification/draft/basic/patterns/mrtr#server-requirements-basic-workflow). -Servers **MUST** bind one call identifier into every `requestState` of a logical call. Before -running such a tool, the server **MUST** claim that identifier atomically, using storage every -instance shares, and **MUST** keep the claim until every `requestState` of the call has expired. A -claimed call counts as processed, even while the tool is running. - -A server that receives a retry for a claimed call **MUST NOT** run the tool again and **MUST NOT** -start a new round. It **MUST** return the original result to the same principal, or the -`already_processed` error. A tool that can outlast a client or gateway timeout **SHOULD** use the -[Tasks extension](https://modelcontextprotocol.io/extensions/tasks/overview). - -A gateway can repeat a retry, and **MUST** pass `already_processed` through unchanged. A gateway -that repeats a retry and receives a `requestState` verification failure **MUST** return -`already_processed` to its caller. +It **MUST** consume each one atomically, in storage every instance shares, before it runs the tool +or returns a later `InputRequiredResult`. It **MUST** issue a distinct `requestState` in each +result, give each an expiry, and keep the record until that expiry. -A client that receives `already_processed`, or whose repeated retry fails `requestState` -verification, **MUST NOT** treat the response as proof that the call failed, and **MUST NOT** repeat -the call automatically. The outcome of the earlier attempt is unknown. +A tool that can outlast a client or gateway timeout **SHOULD** use the [Tasks +extension](https://modelcontextprotocol.io/extensions/tasks/overview). #### Staging @@ -538,22 +517,23 @@ size. Staged bytes are application data that `requestState` refers to. They are not protocol session state. Servers manage their lifetime and cleanup, for example with storage-side expiry. Servers: -- **MUST** keep staged bytes until the latest `requestState` bound to them expires or their target - is replaced, whichever comes first. +- **MUST** keep staged bytes until the latest `requestState` bound to them expires. - **MUST NOT** expose a staged file as finished until they confirm it on the retry. - **MUST** allow any instance that receives the retry to confirm the staged bytes. -- **SHOULD** prevent further writes through a target before verifying its bytes, and **SHOULD** - finalize the bytes they verified. +- **SHOULD** issue targets that accept one write, and **SHOULD** finalize a copy of the bytes they + confirmed. A target that writes to the final destination creates the file before the server confirms it. -Servers **SHOULD NOT** issue such a target unless the backend enforces the declared size and digest -on the write, and the write cannot replace existing content. If a later check fails, the client -reports `failed`, or no retry arrives before the `requestState` expires, the server **MUST** remove -the file. Storage-side expiry can do this without tracking the call. +Servers **SHOULD NOT** issue such a target unless the backend enforces the declared size +on the write, and the write cannot replace existing content. If a later check fails, or no retry +arrives before the `requestState` expires, the server **MUST** remove the file. Storage-side expiry +can do this only if it can tell an unconfirmed file from a confirmed one. A server with no way to +remove the file **MUST NOT** issue such a target. ### Downloads -> **Note**: The working group has not reviewed this section yet. +> **Note**: The working group has discussed downloads but has not reviewed this shape. See [Open +> Questions](#open-questions). A tool returns files by listing them in the `_meta` of its `CallToolResult`, under the extension identifier, with one entry per file. The server **MUST** also describe the result in `content`: @@ -569,7 +549,6 @@ identifier, with one entry per file. The server **MUST** also describe the resul "io.modelcontextprotocol/files": { "downloads": [ { - "id": "export-1", "name": "q3-orders.csv", "mimeType": "text/csv", "size": 209715200, @@ -590,15 +569,11 @@ interface ResultFilesMeta { } interface DownloadTarget { - /** Unique within this result. */ - id: string; name?: string; /** "application/octet-stream" if unknown. */ mimeType: string; /** Non-negative integer; size in bytes, after any Content-Encoding is removed. */ - size?: number; - /** SHA-256 over the whole file, after any Content-Encoding is removed. */ - digest?: { algorithm: "sha-256"; value: string /* base64url, no padding */ }; + size: number; /** url, path, credentials, and expiresAt have the same meaning as in UploadTarget. */ url?: string; path?: string; @@ -617,30 +592,17 @@ Servers: `download`, however that result is delivered. - **MUST NOT** list a target until the file can be fetched. This extension does not define how a tool waits for a file to exist. -- **SHOULD** include `size` and `digest` when they know them. The client sends a `GET` to the target's `url` or resolved `path` with `headers`. A `200` response, -or a `206` response to a range request, indicates success. Clients **MAY** use range requests to -split or resume a download. - -`Content-Length` describes the message, not the file. It counts encoded bytes when -`Content-Encoding` applies, counts one part on a `206`, and can be absent. Clients **MUST NOT** use -it in place of the `size` and `digest` checks, and **MUST** treat a response that HTTP reports as -incomplete as a failed transfer ([RFC 9112](https://www.rfc-editor.org/rfc/rfc9112#section-8)). - -Clients that use range requests **MUST** combine parts only if they share the same strong validator, -and otherwise **MUST** restart. Offsets apply to the bytes as sent, so clients **MUST NOT** derive -ranges from `size` for a representation with a `Content-Encoding`. Clients **MUST NOT** send a range -request to a target with `size` 0. +or a `206` response to a range request, indicates success. Clients **MAY** use range requests as [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110#section-14) defines them to split or resume a download. Ranges apply to the bytes as sent, while `size` describes the file after any `Content-Encoding` is removed. Clients: -- **MUST** check `size` and `digest`, if present, after receiving the whole file, and **MUST** - discard a file that fails a check. +- **MUST** check `size` after receiving the whole file, and **MUST** discard a file that fails the + check. - **MUST** treat `name`, `mimeType`, `size`, and the bytes as untrusted. - **MUST NOT** use `name` as a filesystem path without reducing it to a safe base name. -- **MUST** abort once the decoded bytes exceed `size`, and **SHOULD** apply their own limit when - `size` is absent. +- **MUST** abort once the decoded bytes exceed `size`. - **SHOULD NOT** start a download after `expiresAt`. - **SHOULD** fetch a file they want before returning control to the model, if user or operator policy permits. A target cannot be renewed. @@ -676,7 +638,6 @@ Clients **MUST** reject a target if: 11. It does not conform to its type. A required member is missing, a member has the wrong JSON type, or, at any depth, a member other than `_meta` is not defined by this extension or by a capability the client declared. -12. It has a `digest` whose `algorithm` is not `sha-256`. Member names are case-sensitive, and header names are compared case-insensitively. Clients **MUST** treat a member whose value is `null` as absent, and **MUST** ignore `_meta`. Servers **MUST NOT** @@ -723,8 +684,7 @@ before any query string. With the MCP address `https://gateway.example.com/mcp/a resolves to `https://gateway.example.com/mcp/abc/uploads`. The query string of the MCP address is kept. A `path` target inherits the scheme of the MCP address. If the client followed a redirect to reach the MCP address, it resolves against the redirect's result. Clients **MUST** reject a `path` target -over `http` unless the MCP address is on loopback or an operator allowlist permits it. A `digest` -received over `http` does not protect against an active attacker on that network. +over `http` unless the MCP address is on loopback or an operator allowlist permits it. #### stdio @@ -740,7 +700,8 @@ A gateway **MAY** replace a target's `url` or `path`, `method`, `form`, `headers with a relay at its own origin. It **MUST** do so for a `url` target with `credentials: "include"` when it holds the credential for that server. A gateway that replaces targets **MUST** forward only the `upload` and `download` settings it implements, and **MUST NOT** add a target member other than -under `_meta`. +under `_meta`. A gateway **MUST NOT** pass a second upload input request to its caller in one +logical call, and **MUST** return `isError: true` instead. A gateway that relays an upload **MUST**: @@ -760,11 +721,9 @@ fetch the bytes from it with the original target's `headers` and `credentials`, against the address it uses for the server, and serve them only to the caller. The relay **MUST NOT** answer `2xx` until the original target has. -A relay **MUST** pass a download target's `size` and `digest` through unchanged unless it changes -the bytes, and then **MUST** replace them with values for the bytes it serves, or remove them. A -relay that checks a file while streaming it **MUST** abort the response without completing it if a -check fails. A relay **MUST NOT** delimit a response by closing the connection, and **MUST** apply -its own limit when `size` is absent. +A relay **MUST** pass a download target's `size` through unchanged unless it changes the bytes, and +then **MUST** replace it for the bytes it serves. A relay that checks a file while streaming it +**MUST** abort the response without completing it if a check fails. A gateway that presents more than one server at one MCP address **MUST** replace every `path` target. Any other gateway that passes a `path` target through unchanged **MUST** route everything @@ -773,9 +732,9 @@ and **MUST** attach its own credential for the server when the target has `crede ### Errors -Servers **MUST** respond to the following conditions as shown. The `requestState`, `fileId`, status, -and bytes conditions apply to a retry that answers an upload input request. `isError: true` means a -`CallToolResult` with `isError` set to `true`. `-32021` is +Servers **MUST** respond to the following conditions as shown. The `requestState`, +`inputResponses.uploads`, and bytes conditions apply to a retry that answers an upload input +request. `isError: true` means a `CallToolResult` with `isError` set to `true`. `-32021` is [`MissingRequiredClientCapabilityError`](https://modelcontextprotocol.io/specification/draft/schema#missingrequiredclientcapabilityerror). | Condition | Response | @@ -784,15 +743,13 @@ and bytes conditions apply to a retry that answers an upload input request. `isE | Non-empty file omits `data`, server has `upload`, request did not declare it | `-32021` with `data.requiredCapabilities` listing the missing capability | | Server knows the tool can only return a download, request did not declare `download` | `-32021` as above, before running the tool | | Server cannot accept a file inline or out of band | `isError: true` | -| Inline data cannot be decoded, or an inline or empty file fails a size or digest check | `isError: true` | +| Inline data cannot be decoded, or fails the size check | `isError: true` | | Duplicate `id` values in file arguments | `isError: true` | -| `requestState` fails verification | JSON-RPC `-32602` | +| `requestState` fails verification or was already consumed | JSON-RPC `-32602` | | Tool name or arguments differ from `requestState` | JSON-RPC `-32602` | -| Duplicate or unknown `fileId`, or unknown status | JSON-RPC `-32602` | -| A file has status `failed` | `isError: true`, without running the tool | -| Bytes have not arrived, and the server does not start a new round | `isError: true`, without running the tool | +| `inputResponses.uploads` is missing or is not an object | JSON-RPC `-32602` | +| Bytes have not arrived | `isError: true`, without running the tool | | Bytes arrived but fail a check | `isError: true`, without running the tool | -| Logical call already claimed | Original result to the same principal, or JSON-RPC `-32602`, reason `already_processed` | The error's `data` carries the reason under the extension identifier: @@ -800,7 +757,7 @@ The error's `data` carries the reason under the extension identifier: { "code": -32602, "message": "Invalid params", - "data": { "io.modelcontextprotocol/files": { "reason": "already_processed" } } + "data": { "io.modelcontextprotocol/files": { "reason": "inline_too_large" } } } ``` @@ -833,8 +790,8 @@ does not implement it. Such a gateway cannot meet the rules in [Targets](#target Enforcing [Targets](#targets) does not stop a malicious server from sending a request of its choice to a public address, from the client's network position. With `form`, the server controls most of the request body. Targets do not protect services that trust a client's address, or form handlers -without their own request-forgery defense. The `status` a client reports also tells the server -whether the target accepted the request. Host approval happens before the target is known, so it +without their own request-forgery defense. Whether a client sends the retry also tells the server +whether every target accepted the request. Host approval happens before the target is known, so it does not cover this risk. Hosts **SHOULD** restrict `omit` targets to an allowlist of origins where an operator can provide one, and **SHOULD** do so for `form` targets in unattended use. @@ -849,13 +806,13 @@ files. A compromised agent can write sensitive data to a file and try to send it Binding the principal, tool name, arguments, and upload targets to `requestState` prevents a client from uploading under one call's authorization and attaching the file to another call or destination. -The required digest lets the server detect bytes replaced through an `omit` URL before it uses them. ### Writable Targets -A target that remains writable after verification lets anyone holding it replace the file. -Pre-signed URLs often cannot be revoked. Servers that use them rely on short lifetimes or finalize a -copy of the verified bytes. +This extension has no content digest, so a server cannot detect same-size bytes written through a +leaked target. A target that stays writable lets anyone holding it replace the file, and pre-signed +URLs often cannot be revoked. Servers rely on short lifetimes, targets that accept one write, or a +copy of the confirmed bytes. ### Secret URLs @@ -866,6 +823,14 @@ issue such URLs with the shortest lifetime that covers the expected transfer. User files remain in staging until the server or storage-side expiry removes them. +### Downloaded Files + +A download target needs no user approval, so a result can make a client send a `GET` of the server's +choosing, with the server's headers, and the request tells the server the client acted on the +result. A downloaded file can contain instructions aimed at the model, or content that harms an +application that opens it. Hosts **SHOULD** treat downloaded files as untrusted tool output, and +**SHOULD** apply a storage limit per result as well as per file. + ### File Contents Servers **SHOULD** follow [OWASP ASVS 5.0 @@ -877,15 +842,14 @@ accept. The reference implementation will include a TypeScript SDK branch with the upload input request and a client transfer helper, a reference server in front of an object store, and conformance scenarios for inline transfer, out-of-band transfer, a `form` target, target rejection, rejection on metadata, -a failed upload, a digest mismatch, a repeated retry, and a download. +a failed transfer with no retry, a size mismatch, a consumed `requestState`, a second upload input +request, and a download. ## Open Questions 1. **MRTR extensibility.** How should the specification enable extension-defined input requests? - Repeated retries may belong in the same change. That change should also allocate a core error - code for an already-processed call. Extensions cannot use the reserved range. Core allows a code - outside it, but such a code has no registry and can collide, so this draft reuses `-32602` with a - namespaced reason. This extension would then replace `already_processed` with the core code. + Repeated retries may belong in the same change, and it should let an extension limit repeats of + its own input requests. 2. **Finalize.** Some backends return a commit handle in the transfer's HTTP response, such as an ETag or block ID, and the retry has no field for it. Owners of backends with an explicit commit step are asked to confirm whether finalizing on the retry works. An optional per-file field in @@ -897,6 +861,17 @@ a failed upload, a digest mismatch, a repeated retry, and a download. 4. **Resources.** Should a later revision apply this flow to resources? 5. **`omit` binding.** Should `omit` targets be bound to an origin or allowlist, and should wildcard hosts be allowed? +6. **Failure signal.** A server learns of a failed transfer only when no retry arrives. Should a + client be able to tell the server it gave up, so the server can release quota and clean up early? + A later revision can add this behind a capability. +7. **Download shape.** This draft lists download targets in result `_meta`. Should it use + `resource_link` instead? How a tool returns a file that takes a long time to generate is also + open, and so is whether [Targets](#targets) should treat the two directions alike, including for + redirects. +8. **Integrity.** This draft has no content digest. A required digest would detect same-size + replacement and tampered downloads, but it makes clients read each file twice, browsers cannot + hash a large file natively, and most backends need a full server read. Should a later revision + add one, and should it be required or optional? ## Future Work From 2213b61d2ee19b0182fe934d5d801862eb5ebad6 Mon Sep 17 00:00:00 2001 From: Den Delimarsky Date: Fri, 6 Nov 2026 17:50:03 +0000 Subject: [PATCH 15/19] Make digest optional, fix transfer rules, and shorten the draft Co-authored-by: den --- specification/draft/file-uploads.md | 401 +++++++++++----------------- 1 file changed, 160 insertions(+), 241 deletions(-) diff --git a/specification/draft/file-uploads.md b/specification/draft/file-uploads.md index 29a36f6..11ddccf 100644 --- a/specification/draft/file-uploads.md +++ b/specification/draft/file-uploads.md @@ -77,25 +77,14 @@ The settings object has the following fields: values the server accepts in one request. If `0` or absent, the server does not accept inline transfer. -If `upload` is absent, the sender does not support out-of-band uploads. If a client omits -`download`, the server does not send it download targets. Receivers **MUST** ignore fields they do -not recognize in these settings. +Receivers **MUST** ignore fields they do not recognize in these settings. Clients: - **MUST NOT** use this extension with a server that did not advertise it. -- **MUST NOT** send a request whose inline `data` exceeds `maxInlineBytes` in total. -- **MUST** omit `data` from every file argument if the server does not accept inline transfer. +- **MUST NOT** send inline `data` above `maxInlineBytes` in total, treating an absent value as `0`. - **MUST NOT** send a file out of band to a server that did not advertise `upload`. -A server that accepts inline `data` **MUST** advertise `maxInlineBytes`. A server that does not -accept inline transfer and advertises `upload` requires an upload round trip for every non-empty -file, regardless of size. - -For example, a server with a `maxInlineBytes` of 1 MiB and a file argument with a `maxSize` of 150 -MiB accepts a 200 KiB file or two 400 KiB files inline in one request, accepts a 40 MiB file out of -band, and rejects a 200 MiB file based on its metadata. - ### Flow A client sends a file inline or out of band. A tool returns a file through a download target. @@ -134,8 +123,8 @@ sequenceDiagram A server marks a file argument by adding `x-mcp-file` directly to the argument's object schema in `Tool.inputSchema`. This behavior is consistent with [`x-mcp-header`](https://modelcontextprotocol.io/specification/draft/server/tools#x-mcp-header). -Unlike `x-mcp-header`, `x-mcp-file` applies to objects and can appear in array items. Servers -**MUST** make the marked schema require the `FileInput` shape. +`x-mcp-file` applies to objects and can appear in array items. Servers **MUST** make the marked +schema require the `FileInput` shape. ```typescript /** The value of x-mcp-file. */ @@ -147,13 +136,14 @@ interface FileInputDescriptor { } interface FileInput { - /** Unique within this tool call. */ id: string; name?: string; /** "application/octet-stream" if unknown. */ mimeType: string; /** Non-negative integer; decoded size in bytes. */ size: number; + /** SHA-256 over the original bytes. A tool that needs it requires it in its schema. */ + digest?: { algorithm: "sha-256"; value: string /* base64url, no padding */ }; /** * RFC 4648 base64-encoded file bytes. If absent and size is greater than 0, the client requests * an out-of-band upload. @@ -179,6 +169,14 @@ This can be represented in a server's schema as follows: "name": { "type": "string" }, "mimeType": { "type": "string" }, "size": { "type": "integer", "minimum": 0 }, + "digest": { + "type": "object", + "properties": { + "algorithm": { "const": "sha-256" }, + "value": { "type": "string" } + }, + "required": ["algorithm", "value"] + }, "data": { "type": "string" } }, "required": ["id", "mimeType", "size"] @@ -190,14 +188,14 @@ This can be represented in a server's schema as follows: ``` Clients **MUST** make each `id` unique within the call. Servers **MUST** validate the decoded size -before using the bytes. +before using the bytes. If the tool's schema requires `digest`, servers **MUST** check it against +inline `data` and empty files. A file with `size` 0 has no bytes to transfer. Clients **MUST** omit `data` for it. Servers **MUST** -treat it as an empty file, and **MUST NOT** issue a target for it. -Servers **SHOULD** treat an empty `data` string the same way. A tool can still reject an empty file. +treat it as an empty file and **MUST NOT** issue a target for it. Servers **SHOULD** treat an empty +`data` string the same way. A tool can still reject an empty file. -Clients that do not support this extension ignore the keyword. Validators that reject unknown -keywords by default, such as Ajv in strict mode, require configuration to allow `x-mcp-file`. +Clients that do not support this extension ignore the keyword. Hosts: @@ -206,12 +204,9 @@ Hosts: replace the handle with a `FileInput` before sending the call. - **MUST** bind a file to an argument only on the user's selection or under the host's approval policy for model-supplied values. -- **MUST NOT** place file bytes or upload URLs in model context. +- **MUST NOT** place file bytes or any target in model context. -The host rules above apply to any file the host can read, including a file an agent created. - -A tool whose text argument can refer to files, such as Markdown with image links, **SHOULD** accept -a separate array of file arguments and document the relationship in its description. This extension +These rules apply to any file the host can read, including a file an agent created. This extension does not define how a host presents a result to the model. ### Uploads @@ -219,10 +214,7 @@ does not define how a host presents a result to the model. #### Initial Call The client sends `tools/call` with every argument populated, and omits `data` for each file it will -upload out of band. The request examples omit the `_meta` request metadata. Every request **MUST** -include the required -[`_meta`](https://modelcontextprotocol.io/specification/draft/basic/index#meta) fields; see also -[Capabilities](#capabilities). +upload out of band. The request examples omit `_meta`. ```json { @@ -249,10 +241,10 @@ to storage the server designates, or apply operator policy when no user is prese Servers **SHOULD** perform every check that does not require the bytes, such as destination authorization, quota, `accept`, and `maxSize`, and return a `CallToolResult` with `isError: true` if -one fails. Servers **MUST** treat the metadata as client-asserted and re-check size and type once -the bytes arrive. +one fails. Servers **MUST** treat the metadata as client-asserted, re-check size and type once the +bytes arrive, and **MUST NOT** treat a `digest` they did not establish as verified. -The tool determines whether an existing file with the same name is a conflict. A tool that supports +The tool decides whether an existing file with the same name is a conflict. A tool that supports conditional writes accepts the revision or condition as a tool argument. #### Upload Request @@ -360,17 +352,12 @@ Servers: - **MUST NOT** use a target for a different call. - **MUST** return the upload input request before creating a task, if they use the [Tasks extension](https://modelcontextprotocol.io/extensions/tasks/overview). -- **MUST** bind the tool name, all arguments, each file's target, and the authenticated - principal, if any, to `requestState`. +- **MUST** bind the tool name, all arguments, each file's target, and the authenticated principal, + if any, to `requestState`. - **MUST NOT** allow a target to remain valid after the `requestState` it was issued under expires. -Servers **MUST** handle `requestState` as the [MRTR server -rules](https://modelcontextprotocol.io/specification/draft/basic/patterns/mrtr#server-requirements-basic-workflow) -require. Clients **MUST** handle `requestState` as the [MRTR client -rules](https://modelcontextprotocol.io/specification/draft/basic/patterns/mrtr#client-requirements-basic-workflow) -require for `InputRequiredResult`. MRTR recommends a short `requestState` expiry. A server issuing -targets for large files **SHOULD** choose a `requestState` lifetime that covers a transfer started -at `expiresAt`, plus the retry. +A server issuing targets for large files **SHOULD** choose a `requestState` lifetime that covers a +transfer started at `expiresAt`, plus the retry. #### Transfer @@ -393,18 +380,13 @@ Clients: - **MUST** set the file part's `Content-Type` to `FileInput.mimeType` if it is a valid media type, and otherwise to `application/octet-stream`. - **MUST** use a boundary with at least 128 bits of randomness. -- **MUST** send `Content-Length`. With `form`, it includes the multipart framing, so it exceeds - `size`. +- **MUST** send `Content-Length`. - **MUST NOT** send more file bytes than the declared `size`. -- **SHOULD NOT** start a transfer after `expiresAt`. The target decides whether to accept a - transfer. -- **MAY** repeat a `PUT` to the same target after a failed attempt, and **MUST** bound attempts per - file. Each request sent to a target is an attempt. -- **MUST** stop after reaching that bound, and **MUST** stop before it if a failure is permanent or - a further attempt is unsafe. +- **SHOULD NOT** start a transfer after `expiresAt`. +- **MAY** repeat a failed `PUT` to the same target, up to a bound per file, and **MUST NOT** + automatically resend a `POST` unless they know the target did not apply it. - **MUST** treat the call as failed if they receive a second upload input request in the logical call, and **MUST NOT** transfer to its targets or retry the call. -- **MUST NOT** automatically resend a `POST` unless they know the target did not apply the request. - **MUST** treat a `401` or `403` from a target as a failed transfer, and **MUST NOT** start authorization discovery from it. - **MUST** finish the upload flow without returning control to the model. @@ -415,14 +397,12 @@ is rejected, a transfer fails, or the user cancels, the client **MUST NOT** send performs the transfer itself **MUST** return `isError: true` to its caller. A host **MUST** tell the user or model that the tool did not run, and **MUST NOT** claim that no bytes were stored. -Servers **SHOULD NOT** echo upload URLs in the `CallToolResult`. Servers that issue `omit` targets -to browser clients **SHOULD** configure the storage to allow the client's origin on the transfer -response, and on the preflight for any listed headers, because the client must read the status. +Servers **MUST NOT** include a target in a result's `content`. #### Retry -When retrying, the client **MUST** send a new JSON-RPC `id`, the same tool name and exact arguments, -the exact `requestState`, and an empty object under `inputResponses.uploads`: +When retrying, the client sends the same tool name and arguments, and an empty object under +`inputResponses.uploads`: ```json { @@ -451,18 +431,24 @@ the exact `requestState`, and an empty object under `inputResponses.uploads`: interface UploadInputResponse {} ``` -The retry does not show that the bytes arrived. Clients **MUST NOT** include upload URLs or -credentials in the response. +The retry does not show that the bytes arrived. On a retry that answers an upload input request, the server **MUST** perform these steps in order, and **MUST** respond as [Errors](#errors) specifies when a step fails: 1. Verify `requestState`. A consumed `requestState` fails verification. -2. Check the tool name and arguments against the values bound in `requestState`. +2. Check the tool name and arguments against the values bound in `requestState`. Compare arguments + with [JSON Schema instance + equality](https://json-schema.org/draft/2020-12/json-schema-core#section-4.2.2), so member order + and number spelling do not matter. A server that binds a hash **MUST** hash a form that preserves + this. 3. Check that `inputResponses.uploads` is present. 4. Confirm with the storage backend that the bytes of each file bound in `requestState` arrived, for - example with a `HEAD` request or a session status query. Re-check size and - type. The server **MUST NOT** run the tool with bytes it has not confirmed. + example with a `HEAD` request or a session status query. If the tool's schema requires `digest`, + establish it by hashing the bytes, by comparing a SHA-256 the storage computed from them, or by a + write the storage rejects on mismatch, where the target's authorization fixes the expected + digest. A weaker hash or a client-supplied value does not establish it. Re-check size and type. + The server **MUST NOT** run the tool with bytes it has not confirmed. 5. If running the tool twice is unsafe, consume `requestState` as specified in [Repeated Retries](#repeated-retries). If another retry consumed it first, respond as for step 1. 6. Perform any finalize step the backend needs, run the tool, and return its result. @@ -481,54 +467,48 @@ above that it has not completed. #### Repeated Retries -If a client or gateway sends the retry that answers an upload input request, or a later retry in the -logical call, and does not receive the response, the outcome is unknown. The tool may have run, in -whole or in part. Clients and gateways **MUST NOT** send the retry again or start the call again -automatically, unless they know the server did not process it. - -A `-32602` response to such a retry does not show that the tool did not run. Clients and gateways -**MUST NOT** treat it as proof that the call failed, and **MUST NOT** repeat the call automatically. +If a client sends the retry that answers an upload input request, or a later retry in the logical +call, and does not receive the response, the outcome is unknown. The tool may have run, in whole or +in part. Clients and gateways **MUST NOT** resend the retry or restart the call automatically unless +they know the server did not process it. A `-32602` response does not show that. A gateway that does not receive the response **MUST** return `isError: true` to its caller, stating -that the outcome is unknown. A host **MUST** tell the user or model that the outcome is unknown. Any -new call needs the approval described in [Initial Call](#initial-call). +that the outcome is unknown. A host **MUST** tell the user or model so. Any new call needs the +approval described in [Initial Call](#initial-call). The upload 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. A -tool that is safe to run twice, such as one that is idempotent on its arguments, needs -nothing more. If a retry repeats, the server runs the tool again, and treats a file it already -finalized as confirmed at step 4. +tool that is safe to run twice, such as one that is idempotent on its arguments, needs nothing more. +If a retry repeats, the server runs the tool again and treats a file it already finalized as +confirmed at step 4. For a tool that is unsafe to run twice, the server **MUST** accept each `requestState` at most once, -from the one that answers the upload input request onward, as [MRTR leaves to the -server](https://modelcontextprotocol.io/specification/draft/basic/patterns/mrtr#server-requirements-basic-workflow). -It **MUST** consume each one atomically, in storage every instance shares, before it runs the tool -or returns a later `InputRequiredResult`. It **MUST** issue a distinct `requestState` in each -result, give each an expiry, and keep the record until that expiry. +from the one that answers the upload input request onward. +[MRTR](https://modelcontextprotocol.io/specification/draft/basic/patterns/mrtr#server-requirements-basic-workflow) +leaves this to the server. The server **MUST** consume each one atomically, in storage every +instance shares, before it runs the tool or returns a later `InputRequiredResult`. It **MUST** issue +a distinct `requestState` in each result, give each an expiry, and keep the record until that +expiry. A tool that can outlast a client or gateway timeout **SHOULD** use the [Tasks extension](https://modelcontextprotocol.io/extensions/tasks/overview). #### Staging -Servers **SHOULD** issue targets that write to a staging location and that enforce the declared -size. - Staged bytes are application data that `requestState` refers to. They are not protocol session -state. Servers manage their lifetime and cleanup, for example with storage-side expiry. Servers: +state. -- **MUST** keep staged bytes until the latest `requestState` bound to them expires. -- **MUST NOT** expose a staged file as finished until they confirm it on the retry. -- **MUST** allow any instance that receives the retry to confirm the staged bytes. -- **SHOULD** issue targets that accept one write, and **SHOULD** finalize a copy of the bytes they - confirmed. +Servers **SHOULD** issue targets that write to a staging location, enforce the declared size, and +accept one write, and **SHOULD** finalize a copy of the bytes they confirmed. Servers **MUST** keep +staged bytes until the latest `requestState` bound to them expires, and **MUST NOT** expose a staged +file as finished until they confirm it on the retry. If the tool's schema requires `digest` and the +target stays writable, servers **MUST** establish it on a copy the target cannot write, and run the +tool on that copy. -A target that writes to the final destination creates the file before the server confirms it. -Servers **SHOULD NOT** issue such a target unless the backend enforces the declared size -on the write, and the write cannot replace existing content. If a later check fails, or no retry -arrives before the `requestState` expires, the server **MUST** remove the file. Storage-side expiry -can do this only if it can tell an unconfirmed file from a confirmed one. A server with no way to -remove the file **MUST NOT** issue such a target. +Servers **MUST NOT** issue a target that writes to the final destination unless the backend enforces +the declared size on the write, the write cannot replace existing content, and the server can remove +an unconfirmed file. The server **MUST** remove the file if a later check fails or no retry arrives +before the `requestState` expires. ### Downloads @@ -574,6 +554,8 @@ interface DownloadTarget { mimeType: string; /** Non-negative integer; size in bytes, after any Content-Encoding is removed. */ size: number; + /** SHA-256 over the whole file, after any Content-Encoding is removed. */ + digest?: { algorithm: "sha-256"; value: string /* base64url, no padding */ }; /** url, path, credentials, and expiresAt have the same meaning as in UploadTarget. */ url?: string; path?: string; @@ -594,12 +576,15 @@ Servers: tool waits for a file to exist. The client sends a `GET` to the target's `url` or resolved `path` with `headers`. A `200` response, -or a `206` response to a range request, indicates success. Clients **MAY** use range requests as [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110#section-14) defines them to split or resume a download. Ranges apply to the bytes as sent, while `size` describes the file after any `Content-Encoding` is removed. +or a `206` response to a range request, indicates success. Clients **MAY** use range requests as +[RFC 9110](https://www.rfc-editor.org/rfc/rfc9110#section-14) defines them to split or resume a +download. Ranges apply to the bytes as sent, while `size` describes the file after any +`Content-Encoding` is removed. Clients: -- **MUST** check `size` after receiving the whole file, and **MUST** discard a file that fails the - check. +- **MUST** check `size`, and `digest` if present, after receiving the whole file, and **MUST** + discard a file that fails a check. - **MUST** treat `name`, `mimeType`, `size`, and the bytes as untrusted. - **MUST NOT** use `name` as a filesystem path without reducing it to a safe base name. - **MUST** abort once the decoded bytes exceed `size`. @@ -608,41 +593,31 @@ Clients: policy permits. A target cannot be renewed. - **MUST NOT** repeat the call automatically. A repeated call runs the tool again. -Hosts **MUST NOT** place download targets in model context. - Servers can return small files as embedded resources, as defined in the base protocol. ### Targets -These rules apply to every upload and download target. - -Clients **MUST** reject a target if: - -1. It does not have exactly one of `url` and `path`. -2. Its `url` is not an absolute `https` URL, or it contains userinfo. -3. Its `path` does not match `1*( "/" segment )`, where `segment` is one or more ASCII letters, - digits, `-`, `_`, `~`, or `.`, and is neither `.` nor `..`. -4. Its `credentials` is missing or unrecognized, or its `method` is unrecognized. -5. Its `headers` include `Authorization`, `Cookie`, a [forbidden request - header](https://fetch.spec.whatwg.org/#forbidden-request-header), or a name or value that is not - valid under [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110#section-5). -6. It is a `path` target or has `credentials: "include"`, and its `headers` include `Forwarded`, +These rules apply to every upload and download target. A member whose value is `null` is absent, and +clients **MUST** ignore `_meta`. Header names are compared case-insensitively. + +Clients **MUST** reject a target that: + +1. Does not conform to its type: it lacks exactly one of `url` and `path`, lacks a required member, + has a value its type does not allow, or has, at any depth, a member other than `_meta` that + neither this extension nor a declared capability defines. +2. Has a `url` that is not an absolute `https` URL or that contains userinfo, or a `path` that does + not match `1*( "/" segment )`, where `segment` is one or more ASCII letters, digits, `-`, `_`, + `~`, or `.`, and is neither `.` nor `..`. +3. Has `headers` that include `Authorization`, `Cookie`, a [forbidden request + header](https://fetch.spec.whatwg.org/#forbidden-request-header), or a name or value not valid + under [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110#section-5). +4. Is a `path` target or has `credentials: "include"`, and has `headers` that include `Forwarded`, `X-Original-URL`, `X-Rewrite-URL`, `X-HTTP-Method-Override`, `X-HTTP-Method`, - `X-Method-Override`, or a name that starts with `X-Forwarded-`. -7. It has `form`, and its `method` is not `POST` or its `headers` include `Content-Type`, - `Content-Encoding`, `Content-Range`, `Content-Digest`, or `Repr-Digest`. -8. Its `form` has a missing or empty `fileField`, a missing `fields`, or an empty field `name`. -9. Its `form` has a `fileField` or field `name` that contains a control character, `"`, or `\`, or a - field `value` that contains a CR, LF, or NUL character. -10. A field `name` in its `form` equals `fileField`. -11. It does not conform to its type. A required member is missing, a member has the wrong JSON type, - or, at any depth, a member other than `_meta` is not defined by this extension or by a - capability the client declared. - -Member names are case-sensitive, and header names are compared case-insensitively. Clients **MUST** -treat a member whose value is `null` as absent, and **MUST** ignore `_meta`. Servers **MUST NOT** -send a target member other than `_meta` that this revision does not define, unless the request -declared the capability that defines it. + `X-Method-Override`, or a name starting with `X-Forwarded-`. +5. Has `form`, and its `method` is not `POST`; its `headers` include `Content-Type`, + `Content-Encoding`, `Content-Range`, `Content-Digest`, or `Repr-Digest`; its `fileField` or a + field `name` is empty or contains a control character, `"`, or `\`; a field `name` equals + `fileField`; or a field `value` contains a CR, LF, or NUL character. #### Credentials @@ -653,8 +628,8 @@ client certificates, or platform-supplied authentication. Clients **MUST** honor `include` only on a `path` target, or when `url` has the same [RFC 6454](https://www.rfc-editor.org/rfc/rfc6454) origin as the server's [canonical URI](https://modelcontextprotocol.io/specification/draft/basic/authorization#canonical-server-uri). -A parent domain or wildcard does not match. Behind a gateway, the comparison uses the canonical URI -of the server whose credential is attached. Otherwise the client **MUST** reject the target. +Behind a gateway, the comparison uses the canonical URI of the server whose credential is attached. +Otherwise the client **MUST** reject the target. `include` lets a server choose the path, method, and body of a request sent with the client's credential. Servers **MUST NOT** issue an `include` target for a URL that another application @@ -666,25 +641,24 @@ an `include` target identifies the principal the target was issued to. Clients **MUST** check the address they connect to, and **MUST NOT** connect to one the [IANA special-purpose registries](https://www.rfc-editor.org/rfc/rfc6890) mark as not globally reachable, unless an operator allowlist permits it or the target is a `path` under an `https` or loopback MCP -address. A client that cannot observe the -address, such as a browser or a client behind a forward proxy, **MUST** rely on a platform or proxy -mechanism that enforces the same restriction, or **MUST** reject `url` targets not on an operator -allowlist. Hosts **MAY** restrict `omit` targets to an allowlist of origins. +address. A client behind a forward proxy **MUST** use a proxy that refuses these addresses after +resolving the name, or **MUST** reject `url` targets not on an operator allowlist. A browser page +that sends the transfer with `fetch` meets this rule through the browser's own limits, because it +has no more reach than any web page. No other client qualifies through CORS alone. #### Redirects -Servers **SHOULD** issue targets that respond without a redirect. Clients **MUST NOT** follow -redirects, and **MUST** treat a `3xx` response as a failed transfer. A `form` target **MUST NOT** -ask the storage to answer success with a redirect. +Clients **MUST NOT** follow redirects, and **MUST** treat a `3xx` response as a failed transfer. A +`form` target **MUST NOT** ask the storage to answer success with a redirect. #### Resolving `path` A client resolves a `path` by appending it to the path of the MCP address, without a doubled `/` and before any query string. With the MCP address `https://gateway.example.com/mcp/abc`, `/uploads` -resolves to `https://gateway.example.com/mcp/abc/uploads`. The query string of the MCP address is kept. -A `path` target inherits the scheme of the MCP address. If the client followed a redirect to reach -the MCP address, it resolves against the redirect's result. Clients **MUST** reject a `path` target -over `http` unless the MCP address is on loopback or an operator allowlist permits it. +resolves to `https://gateway.example.com/mcp/abc/uploads`. The query string of the MCP address is +kept. A `path` target inherits the scheme of the MCP address. If the client followed a redirect to +reach the MCP address, it resolves against the redirect's result. Clients **MUST** reject a `path` +target over `http` unless the MCP address is on loopback or an operator allowlist permits it. #### stdio @@ -696,96 +670,53 @@ has no canonical URI or MCP address. The component that holds the bytes to upload, or that will keep a downloaded file, performs the transfer unless a gateway relays it. -A gateway **MAY** replace a target's `url` or `path`, `method`, `form`, `headers`, and `credentials` -with a relay at its own origin. It **MUST** do so for a `url` target with `credentials: "include"` -when it holds the credential for that server. A gateway that replaces targets **MUST** forward only -the `upload` and `download` settings it implements, and **MUST NOT** add a target member other than -under `_meta`. A gateway **MUST NOT** pass a second upload input request to its caller in one -logical call, and **MUST** return `isError: true` instead. - -A gateway that relays an upload **MUST**: - -- Check the original target against [Targets](#targets). -- Forward the bytes with the original target's `method`, `form`, `headers`, and `credentials`, - resolving a `path` against the address it uses for the server. -- Accept bytes only from the caller. - -The relay **MUST NOT** answer `2xx` until the original target has. - -A relay for a `form` target **SHOULD** present a target without `form` and build the multipart body -itself, taking `filename`, the part's `Content-Type`, and `size` from the `FileInput` that `fileId` -names. A relay that keeps `form` **MUST** send the original `fields` upstream. - -A gateway that relays a download **MUST** check the original target against [Targets](#targets), -fetch the bytes from it with the original target's `headers` and `credentials`, resolving a `path` -against the address it uses for the server, and serve them only to the caller. The relay -**MUST NOT** answer `2xx` until the original target has. - -A relay **MUST** pass a download target's `size` through unchanged unless it changes the bytes, and -then **MUST** replace it for the bytes it serves. A relay that checks a file while streaming it -**MUST** abort the response without completing it if a check fails. - -A gateway that presents more than one server at one MCP address **MUST** replace every `path` -target. Any other gateway that passes a `path` target through unchanged **MUST** route everything -under that MCP address to that one server, **MUST NOT** forward the caller's credential upstream, -and **MUST** attach its own credential for the server when the target has `credentials: "include"`. +A gateway **MAY** relay a target by replacing its `url` or `path`, `form`, `headers`, and +`credentials` with a target at its own origin, keeping `method`. It **MUST** relay a `url` target +with `credentials: "include"` when it holds that server's credential. A relay is a client of the +original target, so every client rule applies upstream. To its caller, it is a target. A relay: + +- **MUST** forward only the `upload` and `download` settings it implements. +- **MUST** present a `form` target without `form`, and build the multipart body from the `FileInput` + that `fileId` names. +- **MUST NOT** answer `2xx` until the original target has, and **MUST** abort a streamed response + without completing it if the transfer or a check fails. +- **MUST** pass `size` and `digest` through unchanged unless it changes a download's bytes, and then + **MUST** update `size` and update or remove `digest`. + +A gateway **MUST NOT** forward its caller's credential upstream, and **MUST** return `isError: true` +rather than pass a second upload input request to its caller. A gateway presenting more than one +server at one MCP address **MUST** replace every `path` target. Any other gateway that passes one +through unchanged **MUST** route everything under that MCP address to that one server, and **MUST** +attach its own credential when the target has `credentials: "include"`. ### Errors -Servers **MUST** respond to the following conditions as shown. The `requestState`, -`inputResponses.uploads`, and bytes conditions apply to a retry that answers an upload input -request. `isError: true` means a `CallToolResult` with `isError` set to `true`. `-32021` is +Servers **MUST** respond as shown. Conditions about a retry apply to a retry that answers an upload +input request. `-32021` is [`MissingRequiredClientCapabilityError`](https://modelcontextprotocol.io/specification/draft/schema#missingrequiredclientcapabilityerror). -| Condition | Response | -| ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- | -| Inline `data` exceeds `maxInlineBytes` in total, or the server does not accept it | JSON-RPC `-32602`, reason `inline_too_large` | -| Non-empty file omits `data`, server has `upload`, request did not declare it | `-32021` with `data.requiredCapabilities` listing the missing capability | -| Server knows the tool can only return a download, request did not declare `download` | `-32021` as above, before running the tool | -| Server cannot accept a file inline or out of band | `isError: true` | -| Inline data cannot be decoded, or fails the size check | `isError: true` | -| Duplicate `id` values in file arguments | `isError: true` | -| `requestState` fails verification or was already consumed | JSON-RPC `-32602` | -| Tool name or arguments differ from `requestState` | JSON-RPC `-32602` | -| `inputResponses.uploads` is missing or is not an object | JSON-RPC `-32602` | -| Bytes have not arrived | `isError: true`, without running the tool | -| Bytes arrived but fail a check | `isError: true`, without running the tool | - -The error's `data` carries the reason under the extension identifier: - -```json -{ - "code": -32602, - "message": "Invalid params", - "data": { "io.modelcontextprotocol/files": { "reason": "inline_too_large" } } -} -``` - -`inline_too_large` is an exception to -[SEP-1303](https://modelcontextprotocol.io/seps/1303-input-validation-errors-as-tool-execution-errors). -A client that receives this error **MAY** repeat the call without `data` if it declared `upload` and -the server advertised it. +| Condition | Response | +| ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | +| Non-empty file omits `data`, server has `upload`, request did not declare it | `-32021` with `data.requiredCapabilities` listing the missing capability | +| Server knows the tool can only return a download, request did not declare `download` | `-32021` as above, before running the tool | +| Inline `data` is over `maxInlineBytes` in total, not accepted, undecodable, or fails a size or digest check; duplicate `id`; or no way to accept a file | `isError: true`, without running the tool | +| `requestState` fails verification or was consumed; tool name or arguments differ from it; `inputResponses.uploads` is missing or not an object | JSON-RPC `-32602` | +| Bytes have not arrived or fail a check | `isError: true`, without running the tool | ## Backward Compatibility -This extension is optional. A client that does not declare it never receives an upload input request -or a download target. Tools that accept file content inline today, such as base64 strings, continue -to work, as they do not depend on the upload or download flows. A tool that adopts `x-mcp-file` -changes its `inputSchema`, so a client without this extension sees an object argument it cannot -populate correctly. - -SDKs require an MRTR extension point to support the upload input request. +This extension is optional. Tools that accept file content inline today, such as base64 strings, +continue to work. A tool that adopts `x-mcp-file` changes its `inputSchema`, so a client without +this extension sees an object argument it cannot populate correctly. ## Security Implications -Clients and servers **MUST** redact target URLs, target headers, `form` fields, and inline -`data` in logs, traces, and error reports. +Clients and servers **MUST** redact target URLs, target headers, `form` fields, and inline `data` in +logs, traces, and error reports. ### Server-Directed Requests -A client that does not enforce [Targets](#targets) is exposed to server-side request forgery and can -leak the user's credential. Operators **SHOULD NOT** enable this extension behind a gateway that -does not implement it. Such a gateway cannot meet the rules in [Targets](#targets). +Operators **SHOULD NOT** enable this extension behind a gateway that does not implement it. Enforcing [Targets](#targets) does not stop a malicious server from sending a request of its choice to a public address, from the client's network position. With `form`, the server controls most of @@ -797,39 +728,27 @@ an operator can provide one, and **SHOULD** do so for `form` targets in unattend ### Exfiltration -A malicious server can specify any public URL as an `omit` target and receive the file at that URL. -`POST` reaches more existing endpoints than `PUT`, and `form` also reaches HTML form handlers. The -host approval required before the initial call mitigates this risk and applies to agent-created -files. A compromised agent can write sensitive data to a file and try to send it. - -### Binding - -Binding the principal, tool name, arguments, and upload targets to `requestState` prevents a client -from uploading under one call's authorization and attaching the file to another call or destination. +A malicious server can name any public URL as an `omit` target and receive the file there. `POST` +reaches more existing endpoints than `PUT`, and `form` also reaches HTML form handlers. The host +approval required before the initial call mitigates this risk. ### Writable Targets -This extension has no content digest, so a server cannot detect same-size bytes written through a -leaked target. A target that stays writable lets anyone holding it replace the file, and pre-signed -URLs often cannot be revoked. Servers rely on short lifetimes, targets that accept one write, or a -copy of the confirmed bytes. +A target that stays writable lets anyone holding it replace the file with same-size bytes, and +pre-signed URLs often cannot be revoked. Servers rely on short lifetimes, targets that accept one +write, or a copy of the confirmed bytes. A tool can also require `digest`. ### Secret URLs A URL that carries its own authorization is a secret for as long as it is valid. Servers **SHOULD** issue such URLs with the shortest lifetime that covers the expected transfer. -### Staged Files - -User files remain in staging until the server or storage-side expiry removes them. - ### Downloaded Files A download target needs no user approval, so a result can make a client send a `GET` of the server's choosing, with the server's headers, and the request tells the server the client acted on the result. A downloaded file can contain instructions aimed at the model, or content that harms an -application that opens it. Hosts **SHOULD** treat downloaded files as untrusted tool output, and -**SHOULD** apply a storage limit per result as well as per file. +application that opens it. Hosts **SHOULD** apply a storage limit per result as well as per file. ### File Contents @@ -868,10 +787,10 @@ request, and a download. `resource_link` instead? How a tool returns a file that takes a long time to generate is also open, and so is whether [Targets](#targets) should treat the two directions alike, including for redirects. -8. **Integrity.** This draft has no content digest. A required digest would detect same-size - replacement and tampered downloads, but it makes clients read each file twice, browsers cannot - hash a large file natively, and most backends need a full server read. Should a later revision - add one, and should it be required or optional? +8. **Integrity.** `digest` is optional, and a tool that needs it requires it in its schema. A + required digest would detect same-size replacement for every file, but it makes clients read each + file twice, browsers cannot hash a large file natively, and most backends need a full server + read. Should it be required? ## Future Work @@ -887,6 +806,6 @@ request, and a download. This draft builds on [SEP-2356](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2356) and [SEP-2631](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2631), and on -collaboration in the -[File Uploads Working Group](https://modelcontextprotocol.io/community/working-groups/file-uploads) -and review from implementers. +collaboration in the [File Uploads Working +Group](https://modelcontextprotocol.io/community/working-groups/file-uploads) and review from +implementers. From 9cadced9f1a85458330850a0654e92730ba3f40f Mon Sep 17 00:00:00 2001 From: Den Delimarsky Date: Fri, 6 Nov 2026 18:24:12 +0000 Subject: [PATCH 16/19] List contributors in the draft header Co-authored-by: den --- specification/draft/file-uploads.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/specification/draft/file-uploads.md b/specification/draft/file-uploads.md index 11ddccf..c033dba 100644 --- a/specification/draft/file-uploads.md +++ b/specification/draft/file-uploads.md @@ -3,7 +3,9 @@ - **Status**: Draft - **Extension Identifier**: `io.modelcontextprotocol/files` (provisional) - **Working Group**: [File Uploads](https://modelcontextprotocol.io/community/working-groups/file-uploads) -- **Editors**: Den Delimarsky (@localden) +- **Contributors**: Den Delimarsky (@localden), Wil St. Charles (@st-charles), Thomas Sarlandie + (@tsarlandie-oai), Patrick Rodgers (@patrick-rodgers), Kurtis Van Gent (@kurtisvg), Kryspin Ziemski + (@kziemski), Mike Eastes (@meastes) - **Discussion**: [File Uploads Working Group Discord thread](https://discord.com/channels/1358869848138059966/1551968557292593172/1552399008440520865) and issues in this repository From 6db3ef012c7de911e6ee65ffdff78c2c84f902b2 Mon Sep 17 00:00:00 2001 From: Den Delimarsky Date: Fri, 6 Nov 2026 18:43:44 +0000 Subject: [PATCH 17/19] Retitle the draft, tidy terms and rule placement, and update the README Co-authored-by: den --- README.md | 9 ++-- specification/draft/file-uploads.md | 65 ++++++++++++++++------------- 2 files changed, 40 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 042c9d7..234c4f2 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,7 @@ stay out of model context and out of JSON-RPC messages. Maintained by the A server marks file arguments in a tool's input schema. Each file argument contains its metadata and optional base64 data. When data is absent, the server rejects the call or requests an upload. -The client uploads the bytes and retries with unchanged arguments and per-file outcomes. A tool -returns a file by listing a download target in the result's `_meta`. +The client uploads the bytes and retries with unchanged arguments. A tool returns a file by listing a download target in the result's `_meta`. The draft is in [`specification/draft/file-uploads.md`](specification/draft/file-uploads.md). The reference implementation will live in this repository. @@ -23,9 +22,9 @@ The reference implementation will live in this repository. - [Working group meeting notes, October 2, 2026](https://github.com/modelcontextprotocol/modelcontextprotocol/discussions/3412) - [SEP-2356: Declarative File Inputs](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2356) - (closed), source of the `x-mcp-file` keyword and inline encoding -- [SEP-2631: File Objects and Transfer](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2631), - prior art for file URIs and transfer descriptors + (closed), source of the `x-mcp-file` keyword and inline transfer +- [SEP-2631: File Objects and Transfer](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2631) + (closed), prior art for transfer descriptors - [SEP-2532: Resource Streaming](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2532), related work on getting file bytes from a server diff --git a/specification/draft/file-uploads.md b/specification/draft/file-uploads.md index c033dba..3c9f9e0 100644 --- a/specification/draft/file-uploads.md +++ b/specification/draft/file-uploads.md @@ -1,4 +1,4 @@ -# File Uploads Extension +# File Uploads and Downloads Extension - **Status**: Draft - **Extension Identifier**: `io.modelcontextprotocol/files` (provisional) @@ -38,6 +38,9 @@ This revision applies to tools. Resource behavior is unchanged from the base pro - **MCP address**: The URL a client sends MCP requests to. This URL can vary per hop and can differ from the server's canonical URI. - **Target**: The URL or path to which a client sends file bytes, or from which it fetches them. +- **Gateway**: An intermediary that is a server to its caller and a client of the next hop. +- **Relay**: A gateway that replaces a target with its own and carries the bytes between its caller + and the original target. - **Logical call**: An initial `tools/call` request and its retries, on one hop. A gateway's call to a server is a separate logical call from its caller's call to the gateway. @@ -213,6 +216,9 @@ does not define how a host presents a result to the model. ### Uploads +The upload 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 can process it. + #### Initial Call The client sends `tools/call` with every argument populated, and omits `data` for each file it will @@ -244,7 +250,7 @@ to storage the server designates, or apply operator policy when no user is prese Servers **SHOULD** perform every check that does not require the bytes, such as destination authorization, quota, `accept`, and `maxSize`, and return a `CallToolResult` with `isError: true` if one fails. Servers **MUST** treat the metadata as client-asserted, re-check size and type once the -bytes arrive, and **MUST NOT** treat a `digest` they did not establish as verified. +bytes arrive, and **MUST NOT** rely on a `digest` they did not verify. The tool decides whether an existing file with the same name is a conflict. A tool that supports conditional writes accepts the revision or condition as a tool argument. @@ -373,8 +379,8 @@ Clients: - **MUST** support `form` targets if they declare `upload`. - **MUST** send each entry in a `form` target's `fields` as one part, in array order, then send the file as the last part, named `fileField`. -- **MUST** quote each part's `name` and the file's `filename` in UTF-8, writing `"`, CR, and LF as - `%22`, `%0D`, and `%0A`, as the [WHATWG multipart/form-data +- **MUST** quote each part's `name`, and quote the file's `filename` in UTF-8, writing its `"`, CR, + and LF as `%22`, `%0D`, and `%0A`, as the [WHATWG multipart/form-data encoding](https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#multipart/form-data-encoding-algorithm) does, and **MUST NOT** send `filename*`. - **MUST** set `filename` to the base name of `FileInput.name`, taken after both `/` and `\`. If @@ -389,8 +395,7 @@ Clients: automatically resend a `POST` unless they know the target did not apply it. - **MUST** treat the call as failed if they receive a second upload input request in the logical call, and **MUST NOT** transfer to its targets or retry the call. -- **MUST** treat a `401` or `403` from a target as a failed transfer, and **MUST NOT** start - authorization discovery from it. + - **MUST** finish the upload flow without returning control to the model. Clients **MUST** send the retry only if every requested file received a `2xx` response. If a target @@ -399,7 +404,7 @@ is rejected, a transfer fails, or the user cancels, the client **MUST NOT** send performs the transfer itself **MUST** return `isError: true` to its caller. A host **MUST** tell the user or model that the tool did not run, and **MUST NOT** claim that no bytes were stored. -Servers **MUST NOT** include a target in a result's `content`. + #### Retry @@ -445,15 +450,15 @@ and **MUST** respond as [Errors](#errors) specifies when a step fails: and number spelling do not matter. A server that binds a hash **MUST** hash a form that preserves this. 3. Check that `inputResponses.uploads` is present. -4. Confirm with the storage backend that the bytes of each file bound in `requestState` arrived, for - example with a `HEAD` request or a session status query. If the tool's schema requires `digest`, - establish it by hashing the bytes, by comparing a SHA-256 the storage computed from them, or by a - write the storage rejects on mismatch, where the target's authorization fixes the expected - digest. A weaker hash or a client-supplied value does not establish it. Re-check size and type. - The server **MUST NOT** run the tool with bytes it has not confirmed. +4. Confirm with the storage that the bytes of each file bound in `requestState` arrived, for example + with a `HEAD` request or a session status query. If the tool's schema requires `digest`, verify + it by hashing the bytes, by comparing a SHA-256 the storage computed from them, or by a write the + storage rejects on mismatch, where the target's authorization fixes the expected digest. A weaker + hash or a client-supplied value does not verify it. Re-check size and type. The server + **MUST NOT** run the tool with bytes it has not confirmed. 5. If running the tool twice is unsafe, consume `requestState` as specified in [Repeated Retries](#repeated-retries). If another retry consumed it first, respond as for step 1. -6. Perform any finalize step the backend needs, run the tool, and return its result. +6. Perform any finalize step the storage needs, run the tool, and return its result. If a file's bytes have not arrived at step 4, the server **MAY** wait for them. If they do not arrive, it **MUST NOT** run the tool. @@ -472,16 +477,15 @@ above that it has not completed. If a client sends the retry that answers an upload input request, or a later retry in the logical call, and does not receive the response, the outcome is unknown. The tool may have run, in whole or in part. Clients and gateways **MUST NOT** resend the retry or restart the call automatically unless -they know the server did not process it. A `-32602` response does not show that. +they know the server did not process it. A `-32602` response does not show that the tool did not +run. A gateway that does not receive the response **MUST** return `isError: true` to its caller, stating that the outcome is unknown. A host **MUST** tell the user or model so. Any new call needs the approval described in [Initial Call](#initial-call). -The upload 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. A -tool that is safe to run twice, such as one that is idempotent on its arguments, needs nothing more. -If a retry repeats, the server runs the tool again and treats a file it already finalized as +A tool that is safe to run twice, such as one that is idempotent on its arguments, needs nothing +more. If a retry repeats, the server runs the tool again and treats a file it already finalized as confirmed at step 4. For a tool that is unsafe to run twice, the server **MUST** accept each `requestState` at most once, @@ -504,10 +508,10 @@ Servers **SHOULD** issue targets that write to a staging location, enforce the d accept one write, and **SHOULD** finalize a copy of the bytes they confirmed. Servers **MUST** keep staged bytes until the latest `requestState` bound to them expires, and **MUST NOT** expose a staged file as finished until they confirm it on the retry. If the tool's schema requires `digest` and the -target stays writable, servers **MUST** establish it on a copy the target cannot write, and run the +target stays writable, servers **MUST** verify it on a copy the target cannot write, and run the tool on that copy. -Servers **MUST NOT** issue a target that writes to the final destination unless the backend enforces +Servers **MUST NOT** issue a target that writes to the final destination unless the storage enforces the declared size on the write, the write cannot replace existing content, and the server can remove an unconfirmed file. The server **MUST** remove the file if a later check fails or no retry arrives before the `requestState` expires. @@ -621,6 +625,9 @@ Clients **MUST** reject a target that: field `name` is empty or contains a control character, `"`, or `\`; a field `name` equals `fileField`; or a field `value` contains a CR, LF, or NUL character. +Clients **MUST** treat a `401` or `403` from a target as a failed transfer, and **MUST NOT** start +authorization discovery from it. Servers **MUST NOT** include a target in a result's `content`. + #### Credentials With `include`, clients **MUST** attach the `Authorization` credential they send on MCP requests to @@ -650,8 +657,8 @@ has no more reach than any web page. No other client qualifies through CORS alon #### Redirects -Clients **MUST NOT** follow redirects, and **MUST** treat a `3xx` response as a failed transfer. A -`form` target **MUST NOT** ask the storage to answer success with a redirect. +Clients **MUST NOT** follow redirects, and **MUST** treat a `3xx` response as a failed transfer. +Servers **MUST NOT** ask the storage to answer a `form` upload with a redirect. #### Resolving `path` @@ -689,7 +696,8 @@ A gateway **MUST NOT** forward its caller's credential upstream, and **MUST** re rather than pass a second upload input request to its caller. A gateway presenting more than one server at one MCP address **MUST** replace every `path` target. Any other gateway that passes one through unchanged **MUST** route everything under that MCP address to that one server, and **MUST** -attach its own credential when the target has `credentials: "include"`. +attach its own credential when the target has `credentials: "include"`. Operators **SHOULD NOT** +enable this extension behind a gateway that does not implement it. ### Errors @@ -718,7 +726,7 @@ logs, traces, and error reports. ### Server-Directed Requests -Operators **SHOULD NOT** enable this extension behind a gateway that does not implement it. + Enforcing [Targets](#targets) does not stop a malicious server from sending a request of its choice to a public address, from the client's network position. With `form`, the server controls most of @@ -769,11 +777,10 @@ request, and a download. ## Open Questions 1. **MRTR extensibility.** How should the specification enable extension-defined input requests? - Repeated retries may belong in the same change, and it should let an extension limit repeats of - its own input requests. + That change should also let an extension limit repeats of its own input requests. 2. **Finalize.** Some backends return a commit handle in the transfer's HTTP response, such as an - ETag or block ID, and the retry has no field for it. Owners of backends with an explicit commit - step are asked to confirm whether finalizing on the retry works. An optional per-file field in + ETag, and the retry has no field for it. Owners of backends with an explicit commit step are + asked to confirm whether finalizing on the retry works. An optional per-file field in `UploadInputResponse` can be added if needed. 3. **Tools behind an executor.** Gateways that expose a single generic tool hide the inner tool's schema, including `x-mcp-file`. The executor could declare its own file argument and map it to From 1c12b7fd8eccaeb75fa5d8fe72ae9c5d9ae95fe6 Mon Sep 17 00:00:00 2001 From: Den Delimarsky Date: Fri, 6 Nov 2026 23:24:43 +0000 Subject: [PATCH 18/19] Close security and retry gaps found in the spec review Co-authored-by: den --- specification/draft/file-uploads.md | 115 ++++++++++++++++++---------- 1 file changed, 76 insertions(+), 39 deletions(-) diff --git a/specification/draft/file-uploads.md b/specification/draft/file-uploads.md index 3c9f9e0..7c4ce69 100644 --- a/specification/draft/file-uploads.md +++ b/specification/draft/file-uploads.md @@ -52,7 +52,8 @@ component that performs the transfer. The extension identifier is `io.modelcontextprotocol/files`. Clients **MUST** declare the extension in the `io.modelcontextprotocol/clientCapabilities` metadata -of every request that may need it: +of every request that may need it, and **MUST** send the same settings on every request of a +logical call: ```json { "extensions": { "io.modelcontextprotocol/files": { "upload": {}, "download": {} } } } @@ -90,6 +91,13 @@ Clients: - **MUST NOT** send inline `data` above `maxInlineBytes` in total, treating an absent value as `0`. - **MUST NOT** send a file out of band to a server that did not advertise `upload`. +`maxInlineBytes` limits the total of all inline files in one request, and `x-mcp-file.maxSize` +limits each file however it travels. A client can send a file inline if it fits within +`maxInlineBytes`, and sends the rest out of band if the server advertises `upload`. With `upload`, a +`maxInlineBytes` of 1 MiB, and a `maxSize` of 150 MiB, a 200 KiB file can go inline, a 40 MiB file +goes out of band, and a 200 MiB file is rejected. A server with `upload` and no `maxInlineBytes` +accepts files only out of band. + ### Flow A client sends a file inline or out of band. A tool returns a file through a download target. @@ -200,7 +208,9 @@ A file with `size` 0 has no bytes to transfer. Clients **MUST** omit `data` for treat it as an empty file and **MUST NOT** issue a target for it. Servers **SHOULD** treat an empty `data` string the same way. A tool can still reject an empty file. -Clients that do not support this extension ignore the keyword. +Clients that do not support this extension ignore the keyword, which JSON Schema treats as an +annotation. Implementations of this extension **MUST** accept `x-mcp-file` in schema validators, +including in a strict mode. Hosts: @@ -219,6 +229,10 @@ does not define how a host presents a result to the model. The upload 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 can process it. +Bytes travel over HTTP to a target because storage APIs already accept them there. An MCP upload +method would put the bytes back in JSON-RPC messages and on the server's path, and the client would +send them before the server had seen the call and could reject it. + #### Initial Call The client sends `tools/call` with every argument populated, and omits `data` for each file it will @@ -362,6 +376,7 @@ Servers: extension](https://modelcontextprotocol.io/extensions/tasks/overview). - **MUST** bind the tool name, all arguments, each file's target, and the authenticated principal, if any, to `requestState`. +- **MUST** give every `requestState` an expiry. - **MUST NOT** allow a target to remain valid after the `requestState` it was issued under expires. A server issuing targets for large files **SHOULD** choose a `requestState` lifetime that covers a @@ -389,13 +404,14 @@ Clients: and otherwise to `application/octet-stream`. - **MUST** use a boundary with at least 128 bits of randomness. - **MUST** send `Content-Length`. +- **MUST**, without `form`, send `Content-Type` exactly as `headers` lists it, and **MUST NOT** send + one that `headers` does not list. Storage can include this header in a signature. - **MUST NOT** send more file bytes than the declared `size`. - **SHOULD NOT** start a transfer after `expiresAt`. - **MAY** repeat a failed `PUT` to the same target, up to a bound per file, and **MUST NOT** automatically resend a `POST` unless they know the target did not apply it. - **MUST** treat the call as failed if they receive a second upload input request in the logical call, and **MUST NOT** transfer to its targets or retry the call. - - **MUST** finish the upload flow without returning control to the model. Clients **MUST** send the retry only if every requested file received a `2xx` response. If a target @@ -404,7 +420,8 @@ is rejected, a transfer fails, or the user cancels, the client **MUST NOT** send performs the transfer itself **MUST** return `isError: true` to its caller. A host **MUST** tell the user or model that the tool did not run, and **MUST NOT** claim that no bytes were stored. - +A client that tries again after giving up, or that sends a changed file, starts a new logical call. +It **MUST NOT** reuse an earlier target or `requestState`. #### Retry @@ -444,8 +461,8 @@ On a retry that answers an upload input request, the server **MUST** perform the and **MUST** respond as [Errors](#errors) specifies when a step fails: 1. Verify `requestState`. A consumed `requestState` fails verification. -2. Check the tool name and arguments against the values bound in `requestState`. Compare arguments - with [JSON Schema instance +2. Check the tool name, arguments, and authenticated principal, if any, against the values bound in + `requestState`. Compare arguments with [JSON Schema instance equality](https://json-schema.org/draft/2020-12/json-schema-core#section-4.2.2), so member order and number spelling do not matter. A server that binds a hash **MUST** hash a form that preserves this. @@ -468,9 +485,10 @@ rules](https://modelcontextprotocol.io/specification/draft/basic/patterns/mrtr) requests only. A server does not prompt again, and does not re-request a missing `uploads` response. Other input requests are unaffected. -A server that returns a later `InputRequiredResult` in the logical call **MUST** bind each file's -confirmation to its `requestState`. On the retry that answers it, the server performs the steps -above that it has not completed. +A server that returns a later `InputRequiredResult` in the logical call **MUST** bind the tool name, +all arguments, the authenticated principal, if any, and each file's confirmation to its +`requestState`. On the retry that answers it, the server **MUST** repeat steps 1, 2, and 5, and +perform any other step it has not completed. #### Repeated Retries @@ -482,7 +500,9 @@ run. A gateway that does not receive the response **MUST** return `isError: true` to its caller, stating that the outcome is unknown. A host **MUST** tell the user or model so. Any new call needs the -approval described in [Initial Call](#initial-call). +approval described in [Initial Call](#initial-call). A tool that is unsafe to run twice **SHOULD** +accept an idempotency key as a tool argument, so that a new call after an unknown outcome does not +repeat the work. A tool that is safe to run twice, such as one that is idempotent on its arguments, needs nothing more. If a retry repeats, the server runs the tool again and treats a file it already finalized as @@ -493,23 +513,24 @@ from the one that answers the upload input request onward. [MRTR](https://modelcontextprotocol.io/specification/draft/basic/patterns/mrtr#server-requirements-basic-workflow) leaves this to the server. The server **MUST** consume each one atomically, in storage every instance shares, before it runs the tool or returns a later `InputRequiredResult`. It **MUST** issue -a distinct `requestState` in each result, give each an expiry, and keep the record until that -expiry. +a distinct `requestState` in each result, and keep the record until that `requestState` expires. A tool that can outlast a client or gateway timeout **SHOULD** use the [Tasks extension](https://modelcontextprotocol.io/extensions/tasks/overview). #### Staging -Staged bytes are application data that `requestState` refers to. They are not protocol session -state. +A staging location is storage the server controls, such as a temporary prefix in an object store, +where bytes wait between the transfer and the retry. The server owns it and cleans it up. Neither +the client nor a relay has to hold the bytes for the retry. Staged bytes are application data that +`requestState` refers to. They are not protocol session state. Servers **SHOULD** issue targets that write to a staging location, enforce the declared size, and accept one write, and **SHOULD** finalize a copy of the bytes they confirmed. Servers **MUST** keep -staged bytes until the latest `requestState` bound to them expires, and **MUST NOT** expose a staged -file as finished until they confirm it on the retry. If the tool's schema requires `digest` and the -target stays writable, servers **MUST** verify it on a copy the target cannot write, and run the -tool on that copy. +staged bytes until the latest `requestState` bound to them expires, **SHOULD** delete bytes no retry +finalized after that, and **MUST NOT** expose a staged file as finished until they confirm it on the +retry. If the tool's schema requires `digest` and the target stays writable, servers **MUST** verify +it on a copy the target cannot write, and run the tool on that copy. Servers **MUST NOT** issue a target that writes to the final destination unless the storage enforces the declared size on the write, the write cannot replace existing content, and the server can remove @@ -593,6 +614,10 @@ Clients: discard a file that fails a check. - **MUST** treat `name`, `mimeType`, `size`, and the bytes as untrusted. - **MUST NOT** use `name` as a filesystem path without reducing it to a safe base name. +- **MUST** save a file only in a directory they control, which nothing runs or loads configuration + from, unless the user chooses the location. +- **MUST NOT** overwrite an existing file without the user's approval, and **MUST NOT** set execute + permission. - **MUST** abort once the decoded bytes exceed `size`. - **SHOULD NOT** start a download after `expiresAt`. - **SHOULD** fetch a file they want before returning control to the model, if user or operator @@ -626,7 +651,8 @@ Clients **MUST** reject a target that: `fileField`; or a field `value` contains a CR, LF, or NUL character. Clients **MUST** treat a `401` or `403` from a target as a failed transfer, and **MUST NOT** start -authorization discovery from it. Servers **MUST NOT** include a target in a result's `content`. +authorization discovery from it. Servers **MUST NOT** include a target in a result's `content` or +`structuredContent`. #### Credentials @@ -672,7 +698,7 @@ target over `http` unless the MCP address is on loopback or an operator allowlis #### stdio Servers on stdio **MUST** issue only `url` targets with `credentials: "omit"`. The stdio transport -has no canonical URI or MCP address. +has no canonical URI or MCP address. Clients on stdio **MUST** reject any other target. ### Gateways @@ -680,9 +706,10 @@ The component that holds the bytes to upload, or that will keep a downloaded fil transfer unless a gateway relays it. A gateway **MAY** relay a target by replacing its `url` or `path`, `form`, `headers`, and -`credentials` with a target at its own origin, keeping `method`. It **MUST** relay a `url` target -with `credentials: "include"` when it holds that server's credential. A relay is a client of the -original target, so every client rule applies upstream. To its caller, it is a target. A relay: +`credentials` with a target at its own origin, keeping `method`. It **MUST NOT** pass a `url` target +with `credentials: "include"` to its caller, and instead relays it, performs the transfer itself, or +rejects it. A relay is a client of the original target, so every client rule applies upstream. To +its caller, it is a target. A relay: - **MUST** forward only the `upload` and `download` settings it implements. - **MUST** present a `form` target without `form`, and build the multipart body from the `FileInput` @@ -701,17 +728,18 @@ enable this extension behind a gateway that does not implement it. ### Errors -Servers **MUST** respond as shown. Conditions about a retry apply to a retry that answers an upload -input request. `-32021` is +Servers **MUST** respond as shown. Conditions on `requestState`, the tool name, arguments, and +principal apply to every retry from the one that answers an upload input request onward. The +`inputResponses.uploads` condition applies only to the retry that answers it. `-32021` is [`MissingRequiredClientCapabilityError`](https://modelcontextprotocol.io/specification/draft/schema#missingrequiredclientcapabilityerror). -| Condition | Response | -| ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | -| Non-empty file omits `data`, server has `upload`, request did not declare it | `-32021` with `data.requiredCapabilities` listing the missing capability | -| Server knows the tool can only return a download, request did not declare `download` | `-32021` as above, before running the tool | -| Inline `data` is over `maxInlineBytes` in total, not accepted, undecodable, or fails a size or digest check; duplicate `id`; or no way to accept a file | `isError: true`, without running the tool | -| `requestState` fails verification or was consumed; tool name or arguments differ from it; `inputResponses.uploads` is missing or not an object | JSON-RPC `-32602` | -| Bytes have not arrived or fail a check | `isError: true`, without running the tool | +| Condition | Response | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | +| Non-empty file omits `data`, server has `upload`, request did not declare it | `-32021` with `data.requiredCapabilities` listing the missing capability | +| Server knows the tool can only return a download, request did not declare `download` | `-32021` as above, before running the tool | +| Inline `data` is over `maxInlineBytes` in total, not accepted, undecodable, or fails a size or digest check; an empty file fails a digest check; duplicate `id`; or no way to accept a file | `isError: true`, without running the tool | +| `requestState` fails verification or was consumed; tool name, arguments, or principal differ from it; `inputResponses.uploads` is missing or not an object | JSON-RPC `-32602` | +| Bytes have not arrived or fail a check | `isError: true`, without running the tool | ## Backward Compatibility @@ -721,13 +749,11 @@ this extension sees an object argument it cannot populate correctly. ## Security Implications -Clients and servers **MUST** redact target URLs, target headers, `form` fields, and inline `data` in -logs, traces, and error reports. +Clients and servers **MUST** redact target URLs, target headers, `form` fields, `requestState`, and +inline `data` in logs, traces, and error reports. ### Server-Directed Requests - - Enforcing [Targets](#targets) does not stop a malicious server from sending a request of its choice to a public address, from the client's network position. With `form`, the server controls most of the request body. Targets do not protect services that trust a client's address, or form handlers @@ -736,6 +762,13 @@ whether every target accepted the request. Host approval happens before the targ does not cover this risk. Hosts **SHOULD** restrict `omit` targets to an allowlist of origins where an operator can provide one, and **SHOULD** do so for `form` targets in unattended use. +### Shared Origins + +The client's origin check on an `include` target cannot tell applications on one origin apart. A +malicious server can direct the client's credential to another application on its origin. Operators +**SHOULD NOT** host an MCP server on an origin shared with applications it does not control, and +those applications **SHOULD** reject a token not issued for them. + ### Exfiltration A malicious server can name any public URL as an `omit` target and receive the file there. `POST` @@ -758,7 +791,8 @@ issue such URLs with the shortest lifetime that covers the expected transfer. A download target needs no user approval, so a result can make a client send a `GET` of the server's choosing, with the server's headers, and the request tells the server the client acted on the result. A downloaded file can contain instructions aimed at the model, or content that harms an -application that opens it. Hosts **SHOULD** apply a storage limit per result as well as per file. +application that opens it. Hosts **SHOULD** apply a storage limit per result as well as per file, +and **SHOULD** limit how many targets they fetch per result and how long a transfer can run. ### File Contents @@ -784,8 +818,9 @@ request, and a download. `UploadInputResponse` can be added if needed. 3. **Tools behind an executor.** Gateways that expose a single generic tool hide the inner tool's schema, including `x-mcp-file`. The executor could declare its own file argument and map it to - the inner call. The group needs to answer this before the extension leaves the experimental - phase. + the inner call. A file referenced in free text, such as an image link in a markdown argument, has + the same gap, because no file argument exists. The group needs to answer this before the + extension leaves the experimental phase. 4. **Resources.** Should a later revision apply this flow to resources? 5. **`omit` binding.** Should `omit` targets be bound to an origin or allowlist, and should wildcard hosts be allowed? @@ -805,6 +840,8 @@ request, and a download. - **Chunked and resumable uploads**, for backends with per-request size limits and for interrupted transfers, once a standard is established. +- **Multipart `tools/call`.** An opt-in Streamable HTTP mode that sends files in the same request as + the call, so a file cannot exist without its call. - **Elicitation forms.** Out-of-band upload for `x-mcp-file` fields in `elicitation/create`. - **More authorization methods**, including storage that needs a credential separate from the MCP one, and HTTP Message Signatures. From 4d3e2d939af3b8f0d4df3416d265992f8bc119ad Mon Sep 17 00:00:00 2001 From: Den Delimarsky Date: Sat, 7 Nov 2026 00:37:44 +0000 Subject: [PATCH 19/19] Use an extension result type for uploads and apply readiness fixes Co-authored-by: den --- specification/draft/file-uploads.md | 208 ++++++++++++++-------------- 1 file changed, 105 insertions(+), 103 deletions(-) diff --git a/specification/draft/file-uploads.md b/specification/draft/file-uploads.md index 7c4ce69..87c3851 100644 --- a/specification/draft/file-uploads.md +++ b/specification/draft/file-uploads.md @@ -1,7 +1,8 @@ # File Uploads and Downloads Extension - **Status**: Draft -- **Extension Identifier**: `io.modelcontextprotocol/files` (provisional) +- **Extension Identifier**: `io.modelcontextprotocol/files` +- **Protocol Revisions**: 2026-07-28 and later - **Working Group**: [File Uploads](https://modelcontextprotocol.io/community/working-groups/file-uploads) - **Contributors**: Den Delimarsky (@localden), Wil St. Charles (@st-charles), Thomas Sarlandie (@tsarlandie-oai), Patrick Rodgers (@patrick-rodgers), Kurtis Van Gent (@kurtisvg), Kryspin Ziemski @@ -10,7 +11,7 @@ and issues in this repository The working group plans to submit this document as an Extensions Track SEP once a reference -implementation exists. Anything here can change before then, including the extension identifier. +implementation exists. Anything here can change before then. ## Abstract @@ -52,8 +53,8 @@ component that performs the transfer. The extension identifier is `io.modelcontextprotocol/files`. Clients **MUST** declare the extension in the `io.modelcontextprotocol/clientCapabilities` metadata -of every request that may need it, and **MUST** send the same settings on every request of a -logical call: +of every request that may need it, including `tools/list`, and **MUST** send the same settings on +every request of a logical call: ```json { "extensions": { "io.modelcontextprotocol/files": { "upload": {}, "download": {} } } } @@ -77,11 +78,15 @@ Servers advertise the extension in the `server/discover` response: The settings object has the following fields: -- `upload` _(optional)_: The sender supports out-of-band uploads. +- `upload` _(optional)_: The sender supports out-of-band uploads and the `upload_required` result + type. - `download` _(optional, clients only)_: The client accepts download targets. - `maxInlineBytes` _(optional, servers only)_: The largest total decoded size of inline `data` values the server accepts in one request. If `0` or absent, the server does not accept inline transfer. +- `revision` _(optional)_: A string that changes when a revision of this extension makes a breaking + change. Absent means the initial revision. A peer that receives a `revision` it does not know + **SHOULD NOT** enable the extension. Receivers **MUST** ignore fields they do not recognize in these settings. @@ -96,7 +101,8 @@ limits each file however it travels. A client can send a file inline if it fits `maxInlineBytes`, and sends the rest out of band if the server advertises `upload`. With `upload`, a `maxInlineBytes` of 1 MiB, and a `maxSize` of 150 MiB, a 200 KiB file can go inline, a 40 MiB file goes out of band, and a 200 MiB file is rejected. A server with `upload` and no `maxInlineBytes` -accepts files only out of band. +accepts files only out of band. Inline transfer needs no client declaration. Servers **MUST NOT** +reject inline `data` because a request did not declare the extension. ### Flow @@ -117,10 +123,10 @@ sequenceDiagram alt rejected on metadata S-->>C: CallToolResult {isError: true} else accepted - S-->>C: input_required {inputRequests, requestState} + S-->>C: upload_required {uploads, requestState} C->>T: PUT or POST bytes T-->>C: 2xx - C->>S: same arguments + requestState + inputResponses + C->>S: same arguments + requestState S-->>C: CallToolResult end @@ -136,7 +142,7 @@ sequenceDiagram A server marks a file argument by adding `x-mcp-file` directly to the argument's object schema in `Tool.inputSchema`. This behavior is consistent with [`x-mcp-header`](https://modelcontextprotocol.io/specification/draft/server/tools#x-mcp-header). -`x-mcp-file` applies to objects and can appear in array items. Servers **MUST** make the marked +`x-mcp-file` applies to objects and can appear in array items and in `anyOf` or `oneOf` branches. Servers **MUST** make the marked schema require the `FileInput` shape. ```typescript @@ -149,6 +155,7 @@ interface FileInputDescriptor { } interface FileInput { + /** 1 to 128 characters. */ id: string; name?: string; /** "application/octet-stream" if unknown. */ @@ -178,7 +185,7 @@ This can be represented in a server's schema as follows: "type": "object", "x-mcp-file": { "accept": ["application/pdf"], "maxSize": 157286400 }, "properties": { - "id": { "type": "string" }, + "id": { "type": "string", "minLength": 1, "maxLength": 128 }, "name": { "type": "string" }, "mimeType": { "type": "string" }, "size": { "type": "integer", "minimum": 0 }, @@ -214,7 +221,8 @@ including in a strict mode. Hosts: -- **SHOULD NOT** show `x-mcp-file` properties to a model that emits arguments as tokens. +- **SHOULD NOT** show `x-mcp-file` schemas to a model that emits arguments as tokens. If the schema + is an `anyOf` or `oneOf` branch, this covers that branch only. - **MAY** instead show the model a substitute parameter whose value is a host-defined handle, and replace the handle with a `FileInput` before sending the call. - **MUST** bind a file to an argument only on the user's selection or under the host's approval @@ -229,9 +237,12 @@ does not define how a host presents a result to the model. The upload 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 can process it. -Bytes travel over HTTP to a target because storage APIs already accept them there. An MCP upload -method would put the bytes back in JSON-RPC messages and on the server's path, and the client would -send them before the server had seen the call and could reject it. +The flow uses its own result type, `upload_required`, and the retry pattern of [multi round-trip +requests](https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr). Core allows +extensions to add result types, and a client that does not recognize one treats the result as +invalid. Bytes travel over HTTP to a target because storage APIs already accept them there. An MCP +upload method would put the bytes back in JSON-RPC messages and on the server's path, and the client +would send them before the server had seen the call and could reject it. #### Initial Call @@ -263,50 +274,43 @@ to storage the server designates, or apply operator policy when no user is prese Servers **SHOULD** perform every check that does not require the bytes, such as destination authorization, quota, `accept`, and `maxSize`, and return a `CallToolResult` with `isError: true` if -one fails. Servers **MUST** treat the metadata as client-asserted, re-check size and type once the -bytes arrive, and **MUST NOT** rely on a `digest` they did not verify. +one fails. Servers **MUST** treat the metadata as client-asserted, re-check size, and type if the +tool restricts it, once the bytes arrive, and **MUST NOT** rely on a `digest` they did not verify. The tool decides whether an existing file with the same name is a conflict. A tool that supports conditional writes accepts the revision or condition as a tool argument. #### Upload Request -If the server accepts the files, it returns an `InputRequiredResult` with an upload input request -under `inputRequests.uploads`. This method needs a core MRTR extension point, because the -input-request and input-response unions are closed. The fixed `uploads` key also differs from core -MRTR, where keys are server-assigned. +If the server accepts the files, it returns an `UploadRequiredResult`, called an upload request. ```json { "jsonrpc": "2.0", "id": 1, "result": { - "resultType": "input_required", - "inputRequests": { - "uploads": { - "method": "io.modelcontextprotocol/files/upload", - "params": { - "files": [ - { - "fileId": "file-1", - "url": "https://upload.example.com/u/01JD3V7K2M?sig=...", - "headers": { "Content-Type": "application/pdf" }, - "credentials": "omit", - "expiresAt": "2026-11-04T18:30:00Z" - } - ] - } + "resultType": "upload_required", + "uploads": [ + { + "fileId": "file-1", + "url": "https://upload.example.com/u/01JD3V7K2M?sig=...", + "headers": { "Content-Type": "application/pdf" }, + "credentials": "omit", + "expiresAt": "2026-11-04T18:30:00Z" } - }, + ], "requestState": "..." } } ``` ```typescript -interface UploadInputRequest { - method: "io.modelcontextprotocol/files/upload"; - params: { files: UploadTarget[] }; +interface UploadRequiredResult extends Result { + resultType: "upload_required"; + /** One target for each file the client sends out of band. */ + uploads: UploadTarget[]; + /** Opaque to the client, which echoes it on the retry. */ + requestState: string; } interface UploadTarget { @@ -363,20 +367,24 @@ A storage API that accepts only `multipart/form-data` uses a `form` target: Servers: -- **MUST** return an upload input request only on `tools/call`, and only to a request that declared +- **MUST** return an upload request only on `tools/call`, and only to a request that declared `upload`. - **MUST** make each `fileId` match one non-empty file argument without `data`. -- **MUST** list every non-empty file argument without `data` in the upload input request, each +- **MUST** list every non-empty file argument without `data` in the upload request, each `fileId` once. -- **MUST** record in `requestState` that an upload input request was issued, and carry that record +- **MUST** record in `requestState` that an upload request was issued, and carry that record into every later `requestState` of the logical call. -- **MUST NOT** return an upload input request on a retry whose `requestState` holds that record. +- **MUST NOT** return an upload request on a retry whose `requestState` holds that record. - **MUST NOT** use a target for a different call. -- **MUST** return the upload input request before creating a task, if they use the [Tasks +- **MUST** return the upload request before creating a task, if they use the [Tasks extension](https://modelcontextprotocol.io/extensions/tasks/overview). - **MUST** bind the tool name, all arguments, each file's target, and the authenticated principal, if any, to `requestState`. -- **MUST** give every `requestState` an expiry. +- **MUST** carry earlier `inputResponses` they still need in `requestState`. The retry does not + repeat them. +- **MUST** give every `requestState` an expiry, and protect it as + [MRTR](https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr) requires of an + `InputRequiredResult`. - **MUST NOT** allow a target to remain valid after the `requestState` it was issued under expires. A server issuing targets for large files **SHOULD** choose a `requestState` lifetime that covers a @@ -391,6 +399,7 @@ body. With `form`, the body is `multipart/form-data` as defined in [RFC Clients: +- **MUST** fail the call unless the `fileId`s in the upload request are exactly their non-empty files without `data`, each once. - **MUST** support `form` targets if they declare `upload`. - **MUST** send each entry in a `form` target's `fields` as one part, in array order, then send the file as the last part, named `fileField`. @@ -410,7 +419,7 @@ Clients: - **SHOULD NOT** start a transfer after `expiresAt`. - **MAY** repeat a failed `PUT` to the same target, up to a bound per file, and **MUST NOT** automatically resend a `POST` unless they know the target did not apply it. -- **MUST** treat the call as failed if they receive a second upload input request in the logical +- **MUST** treat the call as failed if they receive a second upload request in the logical call, and **MUST NOT** transfer to its targets or retry the call. - **MUST** finish the upload flow without returning control to the model. @@ -425,8 +434,9 @@ It **MUST NOT** reuse an earlier target or `requestState`. #### Retry -When retrying, the client sends the same tool name and arguments, and an empty object under -`inputResponses.uploads`: +When retrying, the client sends a new JSON-RPC `id`, the same tool name and arguments, and echoes +`requestState` exactly. Clients **MUST NOT** inspect or modify it. Sending it states that every +requested file received a `2xx` response: ```json { @@ -444,55 +454,44 @@ When retrying, the client sends the same tool name and arguments, and an empty o "size": 41204133 } }, - "inputResponses": { "uploads": {} }, "requestState": "..." } } ``` -```typescript -/** Empty. Its presence states that every requested file received a `2xx` response. */ -interface UploadInputResponse {} -``` - The retry does not show that the bytes arrived. -On a retry that answers an upload input request, the server **MUST** perform these steps in order, +On a retry that answers an upload request, the server **MUST** perform these steps in order, and **MUST** respond as [Errors](#errors) specifies when a step fails: 1. Verify `requestState`. A consumed `requestState` fails verification. 2. Check the tool name, arguments, and authenticated principal, if any, against the values bound in `requestState`. Compare arguments with [JSON Schema instance - equality](https://json-schema.org/draft/2020-12/json-schema-core#section-4.2.2), so member order + equality](https://json-schema.org/draft/2020-12/json-schema-core.html#section-4.2.2), so member order and number spelling do not matter. A server that binds a hash **MUST** hash a form that preserves this. -3. Check that `inputResponses.uploads` is present. -4. Confirm with the storage that the bytes of each file bound in `requestState` arrived, for example +3. Confirm with the storage that the bytes of each file bound in `requestState` arrived, for example with a `HEAD` request or a session status query. If the tool's schema requires `digest`, verify it by hashing the bytes, by comparing a SHA-256 the storage computed from them, or by a write the storage rejects on mismatch, where the target's authorization fixes the expected digest. A weaker - hash or a client-supplied value does not verify it. Re-check size and type. The server - **MUST NOT** run the tool with bytes it has not confirmed. -5. If running the tool twice is unsafe, consume `requestState` as specified in [Repeated + hash or a client-supplied value does not verify it. Re-check size, and inspect the bytes if the + tool restricts type. The server **MUST NOT** run the tool with bytes it has not confirmed. +4. If running the tool twice is unsafe, consume `requestState` as specified in [Repeated Retries](#repeated-retries). If another retry consumed it first, respond as for step 1. -6. Perform any finalize step the storage needs, run the tool, and return its result. +5. Perform any finalize step the storage needs, run the tool or create its task, and return the + result. A server finalizes only in that round. -If a file's bytes have not arrived at step 4, the server **MAY** wait for them. If they do not +If a file's bytes have not arrived at step 3, the server **MAY** wait for them. If they do not arrive, it **MUST NOT** run the tool. -This extension narrows two [MRTR -rules](https://modelcontextprotocol.io/specification/draft/basic/patterns/mrtr) for upload input -requests only. A server does not prompt again, and does not re-request a missing `uploads` response. -Other input requests are unaffected. - A server that returns a later `InputRequiredResult` in the logical call **MUST** bind the tool name, all arguments, the authenticated principal, if any, and each file's confirmation to its -`requestState`. On the retry that answers it, the server **MUST** repeat steps 1, 2, and 5, and +`requestState`. On the retry that answers it, the server **MUST** repeat steps 1, 2, and 4, and perform any other step it has not completed. #### Repeated Retries -If a client sends the retry that answers an upload input request, or a later retry in the logical +If a client sends the retry that answers an upload request, or a later retry in the logical call, and does not receive the response, the outcome is unknown. The tool may have run, in whole or in part. Clients and gateways **MUST NOT** resend the retry or restart the call automatically unless they know the server did not process it. A `-32602` response does not show that the tool did not @@ -506,10 +505,10 @@ repeat the work. A tool that is safe to run twice, such as one that is idempotent on its arguments, needs nothing more. If a retry repeats, the server runs the tool again and treats a file it already finalized as -confirmed at step 4. +confirmed at step 3. For a tool that is unsafe to run twice, the server **MUST** accept each `requestState` at most once, -from the one that answers the upload input request onward. +from the one that answers the upload request onward. [MRTR](https://modelcontextprotocol.io/specification/draft/basic/patterns/mrtr#server-requirements-basic-workflow) leaves this to the server. The server **MUST** consume each one atomically, in storage every instance shares, before it runs the tool or returns a later `InputRequiredResult`. It **MUST** issue @@ -634,8 +633,8 @@ clients **MUST** ignore `_meta`. Header names are compared case-insensitively. Clients **MUST** reject a target that: 1. Does not conform to its type: it lacks exactly one of `url` and `path`, lacks a required member, - has a value its type does not allow, or has, at any depth, a member other than `_meta` that - neither this extension nor a declared capability defines. + has a value its type does not allow, or has, at any depth, a member that neither this extension + nor a declared capability defines, other than `_meta` and the entries of `headers`. 2. Has a `url` that is not an absolute `https` URL or that contains userinfo, or a `path` that does not match `1*( "/" segment )`, where `segment` is one or more ASCII letters, digits, `-`, `_`, `~`, or `.`, and is neither `.` nor `..`. @@ -650,9 +649,9 @@ Clients **MUST** reject a target that: field `name` is empty or contains a control character, `"`, or `\`; a field `name` equals `fileField`; or a field `value` contains a CR, LF, or NUL character. -Clients **MUST** treat a `401` or `403` from a target as a failed transfer, and **MUST NOT** start -authorization discovery from it. Servers **MUST NOT** include a target in a result's `content` or -`structuredContent`. +An operator allowlist **MAY** permit `http` on loopback. Clients **MUST** treat a `401` or `403` +from a target as a failed transfer, and **MUST NOT** start authorization discovery from it. Servers +**MUST NOT** include a target in a result's `content` or `structuredContent`. #### Credentials @@ -663,8 +662,9 @@ client certificates, or platform-supplied authentication. Clients **MUST** honor `include` only on a `path` target, or when `url` has the same [RFC 6454](https://www.rfc-editor.org/rfc/rfc6454) origin as the server's [canonical URI](https://modelcontextprotocol.io/specification/draft/basic/authorization#canonical-server-uri). -Behind a gateway, the comparison uses the canonical URI of the server whose credential is attached. -Otherwise the client **MUST** reject the target. +A client that learned no canonical URI through authorization uses the MCP address. Behind a gateway, +the comparison uses the canonical URI of the server whose credential is attached. Otherwise the +client **MUST** reject the target. `include` lets a server choose the path, method, and body of a request sent with the client's credential. Servers **MUST NOT** issue an `include` target for a URL that another application @@ -716,21 +716,23 @@ its caller, it is a target. A relay: that `fileId` names. - **MUST NOT** answer `2xx` until the original target has, and **MUST** abort a streamed response without completing it if the transfer or a check fails. +- **MAY** check `size` and `digest` only on whole-file responses, not on range responses. - **MUST** pass `size` and `digest` through unchanged unless it changes a download's bytes, and then **MUST** update `size` and update or remove `digest`. -A gateway **MUST NOT** forward its caller's credential upstream, and **MUST** return `isError: true` -rather than pass a second upload input request to its caller. A gateway presenting more than one -server at one MCP address **MUST** replace every `path` target. Any other gateway that passes one -through unchanged **MUST** route everything under that MCP address to that one server, and **MUST** -attach its own credential when the target has `credentials: "include"`. Operators **SHOULD NOT** -enable this extension behind a gateway that does not implement it. +A gateway with an authenticated caller **MUST** bind that principal to each `requestState` and +`path` target it returns, by wrapping the value or keeping a record, and check it on use. A gateway +**MUST NOT** forward its caller's credential upstream, and **MUST** return `isError: true` rather +than pass a second upload request to its caller. A gateway presenting more than one server at one +MCP address **MUST** replace every `path` target. Any other gateway that passes one through +unchanged **MUST** route everything under that MCP address to that one server, and **MUST** attach +its own credential when the target has `credentials: "include"`. Operators **SHOULD NOT** enable +this extension behind a gateway that does not implement it. ### Errors Servers **MUST** respond as shown. Conditions on `requestState`, the tool name, arguments, and -principal apply to every retry from the one that answers an upload input request onward. The -`inputResponses.uploads` condition applies only to the retry that answers it. `-32021` is +principal apply to every retry from the one that answers an upload request onward. `-32021` is [`MissingRequiredClientCapabilityError`](https://modelcontextprotocol.io/specification/draft/schema#missingrequiredclientcapabilityerror). | Condition | Response | @@ -738,14 +740,16 @@ principal apply to every retry from the one that answers an upload input request | Non-empty file omits `data`, server has `upload`, request did not declare it | `-32021` with `data.requiredCapabilities` listing the missing capability | | Server knows the tool can only return a download, request did not declare `download` | `-32021` as above, before running the tool | | Inline `data` is over `maxInlineBytes` in total, not accepted, undecodable, or fails a size or digest check; an empty file fails a digest check; duplicate `id`; or no way to accept a file | `isError: true`, without running the tool | -| `requestState` fails verification or was consumed; tool name, arguments, or principal differ from it; `inputResponses.uploads` is missing or not an object | JSON-RPC `-32602` | +| `requestState` fails verification or was consumed; tool name, arguments, or principal differ from it | JSON-RPC `-32602` | | Bytes have not arrived or fail a check | `isError: true`, without running the tool | ## Backward Compatibility This extension is optional. Tools that accept file content inline today, such as base64 strings, continue to work. A tool that adopts `x-mcp-file` changes its `inputSchema`, so a client without -this extension sees an object argument it cannot populate correctly. +this extension sees an object argument it cannot populate correctly. Servers **SHOULD** omit tools +with `x-mcp-file`, or list a variant that takes the file another way, for requests that did not +declare this extension. ## Security Implications @@ -802,36 +806,34 @@ accept. ## Reference Implementation -The reference implementation will include a TypeScript SDK branch with the upload input request and -a client transfer helper, a reference server in front of an object store, and conformance scenarios +The reference implementation will include a TypeScript SDK branch with the upload request and a +client transfer helper, a reference server in front of an object store, and conformance scenarios for inline transfer, out-of-band transfer, a `form` target, target rejection, rejection on metadata, -a failed transfer with no retry, a size mismatch, a consumed `requestState`, a second upload input +a failed transfer with no retry, a size mismatch, a consumed `requestState`, a second upload request, and a download. ## Open Questions -1. **MRTR extensibility.** How should the specification enable extension-defined input requests? - That change should also let an extension limit repeats of its own input requests. -2. **Finalize.** Some backends return a commit handle in the transfer's HTTP response, such as an +1. **Finalize.** Some backends return a commit handle in the transfer's HTTP response, such as an ETag, and the retry has no field for it. Owners of backends with an explicit commit step are - asked to confirm whether finalizing on the retry works. An optional per-file field in - `UploadInputResponse` can be added if needed. -3. **Tools behind an executor.** Gateways that expose a single generic tool hide the inner tool's + asked to confirm whether finalizing on the retry works. An optional per-file field on the retry + can be added if needed. +2. **Tools behind an executor.** Gateways that expose a single generic tool hide the inner tool's schema, including `x-mcp-file`. The executor could declare its own file argument and map it to the inner call. A file referenced in free text, such as an image link in a markdown argument, has the same gap, because no file argument exists. The group needs to answer this before the extension leaves the experimental phase. -4. **Resources.** Should a later revision apply this flow to resources? -5. **`omit` binding.** Should `omit` targets be bound to an origin or allowlist, and should wildcard +3. **Resources.** Should a later revision apply this flow to resources? +4. **`omit` binding.** Should `omit` targets be bound to an origin or allowlist, and should wildcard hosts be allowed? -6. **Failure signal.** A server learns of a failed transfer only when no retry arrives. Should a +5. **Failure signal.** A server learns of a failed transfer only when no retry arrives. Should a client be able to tell the server it gave up, so the server can release quota and clean up early? A later revision can add this behind a capability. -7. **Download shape.** This draft lists download targets in result `_meta`. Should it use +6. **Download shape.** This draft lists download targets in result `_meta`. Should it use `resource_link` instead? How a tool returns a file that takes a long time to generate is also open, and so is whether [Targets](#targets) should treat the two directions alike, including for redirects. -8. **Integrity.** `digest` is optional, and a tool that needs it requires it in its schema. A +7. **Integrity.** `digest` is optional, and a tool that needs it requires it in its schema. A required digest would detect same-size replacement for every file, but it makes clients read each file twice, browsers cannot hash a large file natively, and most backends need a full server read. Should it be required?