Skip to content

SEP-2631: File Objects and Transfer - #2631

Draft
caseychow-oai wants to merge 14 commits into
modelcontextprotocol:mainfrom
caseychow-oai:caseychow/codex/mcp-file-objects-sep
Draft

SEP-2631: File Objects and Transfer#2631
caseychow-oai wants to merge 14 commits into
modelcontextprotocol:mainfrom
caseychow-oai:caseychow/codex/mcp-file-objects-sep

Conversation

@caseychow-oai

@caseychow-oai caseychow-oai commented Apr 22, 2026

Copy link
Copy Markdown

Summary

This PR introduces SEP-2631 for first-class MCP file transfer. It builds on SEP-2356's x-mcp-file input declaration model, keeps file-valued inputs as URI strings, and adds out-of-band upload/download negotiation plus generated file outputs.

Motivation

MCP has partial primitives for file-like workflows, but no single interoperable story for "the user gives a file to a tool" or "a tool returns a file to the user." Implementations currently rely on ad hoc argument conventions, hosted-storage handles, inline base64, or harness-specific rewriting. That makes file upload behavior drift across clients, gateways, and servers, and it makes it hard for server authors to declare portable file-taking tools.

What This SEP Proposes

This SEP adds a focused file transfer contract:

  • SEP-2356 input declarations — file-valued tool and elicitation fields are declared with x-mcp-file on URI string schema properties.
  • URI-string file inputs — clients can satisfy file inputs with SEP-2356 data: URIs or with file URIs obtained through files/authorizeUpload.
  • Allowed transfer modesx-mcp-file.transferModes lets servers allow inline transfer, require upload transfer, or leave clients free to choose.
  • FileValue outputs — generated or returned files use a URI plus optional display metadata such as name, MIME type, and size.
  • Upload/download control planefiles/authorizeUpload and files/authorizeDownload authorize HTTPS transfer descriptors while keeping bytes out of JSON-RPC payloads.
  • URI namespace guidance — file URIs and resource URIs have distinct resolution contracts; file URIs resolve through file transfer methods, not resources/read.
  • Tool, elicitation, content, and resource integration — file URI values can appear in tool arguments and elicitation responses, while FileValue can appear in structured tool output, file content blocks, and optional file-backed resource contents.

Relationship to Other SEPs

This SEP is designed to complement nearby work:

  • SEP-2356: Declarative file inputs — this SEP adopts x-mcp-file declarations and extends the URI-string value contract with negotiated file URI transfer.
  • SEP-2575: Make MCP Stateless — this SEP keeps file identity in the JSON-RPC control plane and leaves lifecycle/storage policy server-defined so it can fit stateless-by-default directions.
  • SEP-2322: Multi Round-Trip Requests — file upload and download negotiation is intentionally expressed as explicit control-plane round trips that can compose with broader multi-round-trip request work.

Validation

  • npm run generate
  • env npm_config_cache=/private/tmp/mcp-npm-cache mise x node@22.20.0 -- npm run check

AI Assistance Disclosure

This SEP was drafted with Codex assistance under human direction and review.

@caseychow-oai caseychow-oai changed the title Add SEP for file objects and transfer SEP-2631: File Objects and Transfer Apr 22, 2026
@caseychow-oai
caseychow-oai force-pushed the caseychow/codex/mcp-file-objects-sep branch from 9f7caad to d1e6de6 Compare April 23, 2026 19:01
@caseychow-oai
caseychow-oai changed the base branch from main to claude/file-upload-sep-sdks-8Rub5 April 23, 2026 19:01
localden added a commit to modelcontextprotocol/access that referenced this pull request Apr 24, 2026
Casey is the author of SEP-2631 (File Objects and Transfer), see
modelcontextprotocol/modelcontextprotocol#2631.

:house: Remote-Dev: homespace
@caseychow-oai
caseychow-oai force-pushed the caseychow/codex/mcp-file-objects-sep branch from d1e6de6 to 8c45ad6 Compare May 7, 2026 17:00
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label May 7, 2026
@caseychow-oai
caseychow-oai changed the base branch from claude/file-upload-sep-sdks-8Rub5 to main May 7, 2026 17:00
@caseychow-oai
caseychow-oai force-pushed the caseychow/codex/mcp-file-objects-sep branch from fe09240 to 9bb59fa Compare May 7, 2026 21:11
@caseychow-oai
caseychow-oai force-pushed the caseychow/codex/mcp-file-objects-sep branch from 9bb59fa to 27b0a95 Compare June 1, 2026 16:23

@Aaronontheweb Aaronontheweb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some comments

"files": {
"upload": true,
"download": true,
"transports": ["https"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A thing I would worry about and price into the concern here early on is max file size / support for chunked file uploads. Many HTTP server implementations themselves enforce upload size constraints at the transport layer and it'd be helpful to communicate this back to the MCP client.

For generated files and file content blocks, this SEP introduces `FileValue`: file URI
plus optional display and integrity metadata.

```json

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM


interface AuthorizedFile {
file: FileValue;
download?: FileTransferDescriptor;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this pattern is great, very similar to what I have to do with the existing MCP limitations today for one of our services for doing email attachments: https://textforge.net/docs/mcp#tools-attachment-management - return a pre-authorized URL for the upload itself.


interface AuthorizedFile {
file: FileValue;
download?: FileTransferDescriptor;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this vein, one thing I would include here is an expiration date on the file transfer descriptor. In a stateful MCP session this wouldn't be an issue - the file transfer descriptors die with the session. But in the upcoming stateless world of MCP, if these descriptors can theoretically live forever that's a potential security vector if they get unused.

For instance,

  1. Claude Code session prompts to upload a file;
  2. User gets prompted for approval, but never does;
  3. Session closes, file is never uploaded;
  4. If descriptor is still open, could this user re-use it at any arbitrary date in the future?

Scoping the lifetime of these to some bounded window of time would eliminate the need to persist these descriptors on the serve and eliminate this problem too.

(I apologize if you've already addressed this elsewhere - I'm, reading the document from top to bottom)


The server responds with an upload authorization:

```json

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Implementations **SHOULD** use standard JSON-RPC errors with the following guidance:

- `-32601` when `files/authorizeUpload` or `files/authorizeDownload` is not supported.
- `-32602` when a file URI is malformed or violates declared constraints such as

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would include another error code in here that implementors will want to have on the server: rejecting a file that doesn't pass some type of security validation. i.e. if I do magic byte detection on a file that claims it's a .png but it isn't, or ClamAV rejects a .zip file due to its contents, that should be distinguished from a user-error or a server failure.

base64. Other servers may prefer inline `data:` URIs for small, lightweight files where
an upload negotiation round trip would be unnecessary.

`transferModes` keeps that policy with the file input declaration. Omitting it preserves

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably a subject for another SEP, but I would consider introducing a "chunked" mode for larger files. A use case for this I have personally run into is automated video editing via MCPs. This aligns with what I wrote earlier about the server advertising the max acceptable size and maybe having MCP clients chunk in order to meet the requirements.

A distinction I'd make here:

  1. Max total file size == that part of the spec is totally clear right now, no issues.
  2. Max per-upload size == this is a mechanical issue with the server itself, not a business constraint the server is enforcing on clients. That's what I was suggesting be advertised in my earlier comment, albeit less clearly than I am trying to do now. That'd be a necessary ingredient for supporting chunking in the future.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Chunked mode would also require marking it as complete and how garbage collection is accomplished.


### Why File Values for Outputs

Generated files often need display metadata in addition to a URI. A `FileValue` gives the

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a good abstraction and a good design choice.

Implementations **SHOULD** use standard JSON-RPC errors with the following guidance:

- `-32601` when `files/authorizeUpload` or `files/authorizeDownload` is not supported.
- `-32602` when a file URI is malformed or violates declared constraints such as

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beyond maxSize, probably need errors for expired handles and uploads which would violate quotas (i.e. file counts, total file size).

Comment on lines +823 to +824
- Servers **MUST** validate MIME type, size, and file content according to their own
policy rather than trusting client metadata.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Servers should also apply limits to the total counts and sizes of files.
  • Servers should also apply limits to the number of pending authorized uploads/downloads

Comment on lines +548 to +549
Servers **MUST** treat file arguments as URI strings whose file URI values are resolved
through the upload contract, not as local filesystem paths or storage-specific handles.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the upload is out-of-band, when the server compares digests is left open and thus how the error is surfaced should be covered in this section.

Comment on lines +840 to +848
The main performance impact is positive:

- large files no longer need to be encoded into inline `data:` URIs inside JSON-RPC
messages;
- intermediaries and servers avoid parsing oversized request bodies for file content;
- clients can use storage-native upload and download paths.

This SEP does add one control-plane round trip for upload or download negotiation, but
that tradeoff is preferable to making every file transfer inline.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One negative would be abandoned uploads from clients, especially if they're added as part of stateless sessions. I realize this is probably diving a bit into the black box of "implementation details" for each server, but either (a) standard error codes, or (b) standards for expressing retention would help with client-side reasoning without enforcing an implementation.

Comment on lines +448 to +454
"name": "report.pdf",
"mimeType": "application/pdf",
"size": 248123,
"digest": {
"algorithm": "sha-256",
"value": "uU0nuZNNPgilLlLX2n2r-sSE7-N6U4D6ZVe-_rYh2sU"
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good, however there is a gap in these params. In many signed upload system I know, the signature covers the upload policy, not just the file. Take S3 presigned POST as the most familiar example. The server signs a policy document like this:

{
  "expiration": "2026-08-17T12:00:00Z",
  "conditions": [
    {"bucket": "media-assets"},
    ["starts-with", "$key", "uploads/customer-42/"],
    ["starts-with", "$Content-Type", "image/"],
    ["content-length-range", 0, 26214400]
  ]
}

The destination path, allowed content type and size limits are baked into the signature, and the upload endpoint rejects anything that does not match. The client cannot change them because that is the whole point of signing.

ImageKit/Cloudinary works the same way, production flow is literally this SEP's shape already: the client gets a signed JWT from API and multipart POSTs the file with that token, and the JWT signs the destination folder, tags and overwrite behavior.

To produce that signature the server must know those values at signing time. In this SEP, signing time is files/authorizeUpload, but the destination folder and similar values are ordinary tool arguments that only arrive later in tools/call, after the bytes have already been uploaded. At authorize time the server only sees name, mimeType, size and digest.

Nothing in the schema forbids extra params on this request, but that does not help in practice. The client builds this request and will only send what the spec defines. There is no way for a server to declare that authorizing an upload for a given file input needs certain values from the pending tool call, so no client will ever send them. Note that in the tool call flow the client already holds the full arguments object at this point, with only the file field waiting for substitution, so the information exists on the client side and is simply not forwarded.

Without this, providers like us (ImageKit) have to sign for a staging location, accept the upload there, and move the file once tools/call arrives with the real destination. That means an extra copy, a window where the file is in the wrong place, and cleanup for staged files whose tool call never arrives.

Suggestion: let the file input declaration name the tool arguments that the client must include in the files/authorizeUpload params, similar in spirit to how x-mcp-file already marks the field itself. Happy to share details of our production flow if useful.

Comment on lines +519 to +522
Client->>Upload: POST multipart/form-data { fileField: "file", file: report.pdf, token }
Upload-->>Client: 200 OK
Client->>Server: tools/call analyze_document { document: "mcp-file://server/file_01HXYZ" }
Server-->>Client: CallToolResult content or structuredContent

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at this sequence, the upload endpoint's response terminates at the client on line 520 and nothing from it ever reaches the server. The next message the server sees is tools/call carrying only the file URI it minted earlier.

That is a problem because upload endpoints usually return a meaningful body: the stored object's final identifier, its location, size, sometimes a content hash. The server issued the file URI before any bytes existed, and the final identity of the stored object is often assigned by the upload service at upload time, so the server cannot predict it at authorize time. After the upload, the only party holding the ground truth about what was actually stored is the client, and the protocol gives it no way to hand that back.

The server is left to discover the outcome of an upload it authorized itself, through side channels: deterministic staging keys plus a move step, callbacks from the storage service, or proxying the bytes through the server, which defeats the point of out of band transfer. All of that is overhead to recover information the client already has in hand.

Suggestion: add a completion step between the upload and tools/call, for example a files/completeUpload message from the client with the file URI, the HTTP status, and optionally the upload endpoint's response body. The server should treat a relayed body as a claim to verify rather than trust, but verifying a claim is much cheaper than discovering the outcome from scratch. An explicit completion message would also resolve two things already discussed in this review: it gives the server a definitive signal for the race where tools/call arrives before the upload finished, and a natural expiry point for authorizations that were never used.

I would keep this out of the tools/call arguments themselves, since those are model produced and schema validated; a control plane message between client and server is the right layer.

Comment on lines +122 to +124
Clients that support out-of-band file transfer declare a new `files` capability during
initialization:

@chrisbennight chrisbennight Aug 18, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor update needed since stateless landed recently, moving this to _meta. Maybe something like:

{
  "jsonrpc": "2.0",
  "id": 10,
  "method": "files/authorizeUpload",
  "params": {
    "name": "report.pdf",
    "mimeType": "application/pdf",
    "size": 248123,
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientCapabilities": {
        "extensions": {
          "io.modelcontextprotocol/files": {
            "upload": true,
            "download": true,
            "transports": ["https"]
          }
        }
      }
    }
  }
}

Comment on lines +386 to +389
This SEP supports elicitation by allowing the eventual value for an `x-mcp-file` field to
be either a SEP-2356 `data:` URI or, when out-of-band transfer is used, a file URI
prepared through `files/authorizeUpload`. It does not add a new elicitation mode or change
the surrounding elicitation control flow.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also minor update needed for stateless since elicitation was deprecated and move to the resultType: "input_required" pattern. It's a pretty clean map in the new stateless. model though server asks for a file with the "input_required" call, client calls files/authorizeUpload, uploads out of band, retries with the file URI in inputResponses.

Comment on lines +31 to +34
This SEP extends [SEP-2356](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2356)
with out-of-band file transfer and generated file outputs. SEP-2356 defines
`x-mcp-file`, an inline JSON Schema extension keyword for declaring file-valued fields,
and uses URI strings, including RFC 2397 `data:` URIs, as the baseline input value while

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parts of 2356 this spec needs probably need to be rolled into this SEP, since 2356 is closed in favor of 2631 ( #2356 (comment) )

Looks like the definitions of. x-mcp-file, accept, maxSize, and transferModes would cover most of it.

Comment on lines +423 to +438
```json
{
"jsonrpc": "2.0",
"id": 10,
"method": "files/authorizeUpload",
"params": {
"name": "report.pdf",
"mimeType": "application/pdf",
"size": 248123,
"digest": {
"algorithm": "sha-256",
"value": "uU0nuZNNPgilLlLX2n2r-sSE7-N6U4D6ZVe-_rYh2sU"
}
}
}
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be valuable to support binding of the file upload to an intended purpose - something like:

{
  "target": {
    "method": "tools/call",
    "name": "analyze_document",
    "argumentPath": "/document"
  },
  "file": {
    "name": "report.pdf",
    "mimeType": "application/pdf",
    "size": 248123
  }
}

for example, when uploading the file I'd like to say "hey, this file is authorized for "analyze_document" and not "publish_to_x". With codemode and other capabilities, as well as the inherent nondeterminism of agents I think this kind of defense would be great. I could likely be made optional, but I think there's real value in putting it out up front.

@serg0x

serg0x commented Aug 19, 2026

Copy link
Copy Markdown

Hey, so I was pushing a lot for MCP file transfer in #2356 but it got closed and referred to here. When can I expect this to work? I need it urgently for my business.

@ochafik @localden @nickcoai are you guys still on it? https://modelcontextprotocol.io/community/working-groups/file-uploads this page says the target was May and June, what is going on here?
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants