Skip to content

Add initial file uploads extension draft - #1

Open
localden wants to merge 14 commits into
mainfrom
localden/initial-draft
Open

localden wants to merge 14 commits into
mainfrom
localden/initial-draft

Conversation

@localden

@localden localden commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Adds the working draft of the io.modelcontextprotocol/files extension and CODEOWNERS for file-uploads-wg and core-maintainers, and updates the README. Motivation and rationale live here. They move into the document when it becomes a SEP.

How it works

  • Servers mark file arguments with x-mcp-file in inputSchema.
  • Small files can travel inline as base64 data, within the server's maxInlineBytes.
  • For a larger file, the client calls the tool with the file's metadata and no data.
  • The server rejects the call based on the metadata, or returns an upload input request with a target. A target is an HTTPS url, or a path under the MCP address.
  • The client sends the bytes with PUT, POST, or a POST multipart form, then retries with the same arguments. If a transfer fails, the client does not retry.
  • Tools return files as a typed list of download targets in the result's _meta.

The flow keeps no protocol session state by default. Each retry carries its context in the request and in requestState, so any instance that can reach the storage backend can process it. Only a tool that is unsafe to run twice needs shared storage, to make each requestState work once.

Motivation

MCP has no interoperable way to get a file from a user to a tool. Today's options put the bytes in a JSON-RPC message: base64 arguments going in, EmbeddedResource and BlobResourceContents coming back. When the model emits arguments as tokens, the bytes pass through its context window, and large uploads to remote servers fail.

The working group collected these cases:

  • A user saves a 40 MB PDF from a 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 a document and keeps the link.
  • An enterprise agent moves files 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.

It also recorded these host-side constraints:

  • One model turn per upload.
  • File contents and credentials never enter the context window.
  • Small files stay in band for latency, and 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 defined a file argument and an inline encoding, and closed without merging. SEP-2631 added out-of-band transfer, and also closed without merging. Its bytes moved before the server saw the rest of the call. Uploads that permissions, quota, or data residency rules would reject still used bandwidth and could land in the wrong region. The group agreed on metadata first, then the target, then bytes, then finalize, built on multi round-trip requests (MRTR) and existing primitives.

Why existing building blocks fall short

  • URL-mode elicitation needs a person in a browser, so it fails for agent-created files and unattended runs, and it adds a step outside the model turn.
  • A tool that returns a presigned URL puts the URL in model context unless client and server share a private convention, and the client has no signal that it should act on it.
  • A separate upload call before tools/call moves bytes before the server can check the destination, or any quota that depends on it.
  • Resources have no request body for a file argument and no schema to mark. A tool's inputSchema has both. Elicitation's requestedSchema allows only flat primitive fields, so a structured file argument does not fit it today, and that is Future Work. Applying this flow to resources is listed under Open Questions.

Target shapes

Targets cover single-request upload URLs that services hand out today, within each service's per-request limit. A client can use each of these without the server's credentials, apart from the headers the target supplies. Some, such as Linear, do not accept browser uploads.

Raw PUT:

Raw POST:

POST multipart form:

Rationale

Upload input request

SEP-2631 issued the upload URL before the server saw the arguments, so the server could not check the destination, or any quota that depends on it. Returning the target from tools/call fixes the order, and requestState carries the binding. MRTR allows only elicitation, sampling, and roots requests today, so this needs a core change before clients and servers can interoperate.

One upload round

A server issues one upload input request per logical call. No backend we checked needs a second one: S3, GCS, Azure, R2, and Oracle are strongly consistent on the normal path, and re-sending bytes fixes neither replication lag nor post-upload processing. Tools can still use other MRTR rounds, including elicitation after the upload.

Stateless by default

Staged bytes are application data that requestState refers to, so any instance that can reach the storage can serve the retry. A tool that is safe to run twice needs nothing more. For a tool that is unsafe to run twice, each requestState from the upload onward works once, in storage every instance shares. Clients and gateways do not repeat a retry automatically, so a lost response leaves the outcome unknown. A safe tool may then need a new call and a new upload. Tools that outlast a timeout should use Tasks.

No per-file status

A retry states that every transfer received a 2xx. A failed transfer means no retry. Any failure already ended the whole call, the server confirms the bytes anyway, and a stateless server could not enforce a reported failure. The cost is that a server waits for requestState expiry to clean up. A failure signal is listed under Open Questions.

Schema keyword

x-mcp-file sits on the argument it describes, as x-mcp-header does. A separate list in Tool._meta would need a pointer into the schema, and pointers handle $ref and anyOf poorly. A strict validator has to allow this keyword as it does x-mcp-header.

Structured file argument

SDKs and gateways can validate arguments before a handler runs, so the initial call cannot omit a required file argument. A structured value satisfies the schema without a placeholder URI or a parallel _meta map. size also marks empty files, so a typed SDK that drops an empty string cannot turn an empty file into an upload request.

No content digest

This draft removes the digest for now, and we want to hear whether that is right. A required digest makes clients read each file twice, browsers cannot hash a large file natively, and most backends need a full server read because they do not expose SHA-256. No comparable upload API requires one. The cost is real: a server cannot detect same-size bytes written through a leaked target, or tampered downloads. The draft leans on short lifetimes and one-write targets, and asks the question under Open Questions. This changes an earlier revision that required a digest.

POST and form

Implementers asked for raw POST, and some upload URLs document only POST. Several document only multipart/form-data. With form, those services work without a proxy. Every client that declares upload supports form, so a server can issue one without a fallback. fields is an array because policy-based form uploads depend on order: Cloud Storage POST requires the file to be the last field.

Errors

The specification reserves -32020 to -32099, so an extension cannot allocate a code there. inline_too_large uses -32602 with a reason namespaced under the extension identifier. A consumed requestState returns plain -32602, and a client treats that as an unknown outcome.

Oversized inline data

SEP-1303 returns validation errors to the model so it can correct them. The model does not choose the transfer path, but the client does. A file over maxSize stays a tool execution error, because the model or user can choose another file.

omit and include

Sending the user's credential has to be optional, so a token never reaches third-party storage. Some enterprise deployments reject pre-signed URLs and want the same token on the control and data planes. A per-target flag covers both. include is honored on path targets and on url targets with the same origin as the server's canonical URI. With omit, clients attach no credential. include lets a server choose the path, method, and body of a request sent with the client's credential, so a server cannot issue one for a URL another application serves.

Unbound omit targets

Pre-signed URLs are common, and requiring a matching host means each server runs its own upload endpoint. This draft leaves omit targets unbound, because an approved server receives the file through its own tool anyway. User approval and host policy mitigate the risk but do not remove it, and form adds request forgery to that risk. The group has not decided this, including whether to allow wildcard hosts, so it is an Open Question.

path targets

One absolute URL assumes every client reaches a gateway the same way. A client in a sandbox or behind a proxy may not reach the public host at all. With path, the bytes take the same route as the client's MCP calls.

Downloads in result _meta

An earlier revision used a resource_link. Resources are mostly static, a link invites resources/read on a file meant to travel out of band, and a client without the extension got a link it could not fetch. A typed list mirrors uploads, and a gateway finds returned files at one known key. The group discussed downloads when the draft used resource_link, and has not reviewed this shape. It is listed under Open Questions.

Resumable uploads and downloads

Clients can already split and resume downloads with HTTP range requests. Resumable upload is Future Work, once a standard is established.

Prior art

  • SEP-2356, for the keyword, inline transfer, and host rules.
  • SEP-2631, for transfer descriptors including multipart fileField, and applying maxSize to every file. SEP-2356 applied it to inline values only.
  • SEP-2532 (resource streaming), as a way to get file bytes from a server.
  • Storage APIs that separate creating an upload from sending the bytes.

What we need from reviewers

Check whether the flow fits the storage APIs you work with, and tell us if anything in the security model would stop you from deploying it. Prototypes should also confirm that SDKs and gateways pass result _meta through intact.

Working draft of the io.modelcontextprotocol/files extension in SEP form,
a README describing the repository's status and how to participate, and
CODEOWNERS for the File Uploads WG.
Return the upload target as an extension result type (upload_required)
instead of a new input request type, use a placeholder value so required
file arguments pass schema validation, bind the tool name, arguments and
file URIs into requestState, scope credential forwarding to the server's
canonical URI, and trim the first revision to a single PUT. Restate the
SEP-2356 rules the draft depends on and align the background with the
October 2 working group notes.
@mgoldsborough

Copy link
Copy Markdown

Nice to see this moving @localden! We run an MCP host (NimbleBrain) and ship ai.nimblebrain/host-resources, which lets servers read workspace files by URI.

Wanted to share a few thoughts from running that:

  1. re: open question 1, I'd lean toward the input request. Our extension hits the same wall. On 2026-07-28 servers can't reach it at all because inputRequests only takes the three built-in methods (MRTR: inputRequests is a closed enum of three methods — add an extensible / reverse-DNS-namespaced member type modelcontextprotocol#2919). If every extension mints its own result type you can't mix an upload with an elicitation in one round trip, and SDKs have to special-case each one. Opening up inputRequests/inputResponses to namespaced members seems like the cleaner fix. upload_required is fine for now, just want to make sure switching later isn't a breaking change.

  2. re: How does the model say which file it means? If hosts hide x-mcp-file props from the model, "save that PDF to my Drive folder" gets tricky. We let the model pass a short host handle (files://fl_abc123) and the host resolves it, so the model never sees bytes. Might be worth letting hosts do something like that and swap in the data: / placeholder before dispatch. (@simontaurus mentioned the same thing on SEP-2631: File Objects and Transfer modelcontextprotocol#2631).

  3. Client capability is just {} right now. Might be worth giving it some shape ({ upload: { include: true } } or similar) so downloads, resumable, etc. can slot in later without breaking anything. Also lets a stdio/browser client say it can't do include. We did per-op objects for read/list/write for this reason.

Also, what error do you get for a used or unknown file URI? We return the same one for "doesn't exist" and "can't see it" so servers can't probe.

Happy to be a design partner here. We can wire up the client side in our host and the server side in our Python SDK. 🤙

@jayzuccarelli

Copy link
Copy Markdown

Thanks @localden. I built a small prototype of the 2 October flow (metadata first, target back, PUT, finalize on the MRTR retry) on the Python SDK. This draft resolves most of what it surfaced: the extension result type, gating on the declared capability, and excluding the file pointers from the request digest. Four things it hit that the draft doesn't settle yet:

  1. Two expiries, no stated relationship. UploadTarget has no expiresAt, and requestState has its own lifetime (600 s by default in the Python SDK). A 250 MB upload on a slow link can outlast requestState even when the URL is still valid. Suggest adding expiresAt to UploadTarget, plus a rule that servers MUST NOT issue a target that outlives the requestState it rides with.
  2. Retry with the file URI before the bytes are confirmed. Retry step 4 says the server MUST NOT run the tool with unconfirmed bytes, but not what it returns. Step 3 only covers placeholders. I'd return upload_required again with the same target, as for a remaining placeholder. That way a client that retries early, or races storage consistency, loses nothing.
  3. Duplicate retries. The retry is a new request, so a client can resend it after a lost response. Today the second one hits "consumed file URI" and gets an error, so the client can't tell a completed upload from a failed one. Suggest that servers SHOULD return the original result for a retry whose requestState was already consumed, or define a distinct error the client can treat as "already done". Related: a PUT to a target after its file URI is consumed should be refused.
  4. No slot for what storage returned. Step 4 has the server confirm with the backend, which works when the server can query it. Some backends put the commit handle in the PUT response (an ETag or version id, a block id, an upload-session token). There the server has nothing to check unless the client passes the handle back, and the retry has no place for it. A per-pointer entry in retry _meta under the extension key would cover it. This also feeds open question 4.

If it's useful, I can port the prototype into this repo as a Python reference implementation alongside the planned TypeScript one, with tests for each of these cases.

AI disclosure: I used Claude Code to build the prototype and draft this review; I reviewed the spec and each point.

@meastes meastes 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.

Engineer from Dropbox here.

This looks useful, particularly having the server validate the destination and file metadata before transferring bytes. A few integration considerations:

  1. PUT versus POST. Some storage APIs accept raw file bytes through POST, while this proposal requires PUT. Could UploadTarget specify the HTTP method, supporting at least PUT and POST? This would accommodate existing storage APIs without requiring a proxy or changes to their endpoints. This is a raw-body POST requirement, separate from multipart support.

  2. Large files, chunking, and resumability. Some storage APIs impose per-request upload limits and require upload sessions for larger files. Deferring chunked transfers makes direct integration with those backends harder. Separately, without resumability, an interrupted large upload can require retransmitting the entire file, increasing latency and bandwidth usage. Could we clarify the expected approach for backends with per-request limits and the path toward resumable transfers?

  3. Make inline transfer optional. Inline uploads avoid additional round trips, but require servers to implement and test another way of receiving file bytes. For some servers, that complexity may outweigh the latency benefit.

    Could servers explicitly advertise which transfer mechanisms they support—including out-of-band-only—with clients required to respect that choice? The existing maxInlineBytes and x-mcp-file.maxSize fields cover much of the size negotiation, but disabling inline transfer should have explicit semantics, including whether maxInlineBytes: 0 does that.

    For servers supporting both, an example showing a 1 MiB aggregate inline limit and a 150 MiB per-file limit would help clarify how the limits interact and how the client selects the transfer mechanism.

@ochafik

ochafik commented Oct 15, 2026

Copy link
Copy Markdown

So excited to see this!!

@localden Some potential concerns:

  • Where to put that x-mcp-file: if it's a key in the inputSchema's values, could it trip validators in some SDKs? (e.g. if $schema is defined, they might validate strictly) An alternative could be e.g. to list the file input names in Tool._meta['x-mcp-files'] (less pretty though, closer to SEP-2356: File input support for tools and elicitation modelcontextprotocol#2356 )
  • I'd suggest adding a new x-mcp-file/upload MCP method instead of relying on HTTP PUT (even though resumability seems about to be solved!)
    • HTTP PUT may have a higher client & server implementation burden (new external surface w/ new auth, is sharing the MCP auth w/ a potentially different domain ok? people w/ mcp logs access can replay / overwrite uploads? <insert security vuln here>)
    • a new MCP method would be orthogonal to transports & auth concerns
    • a new method would remove the need for a first tool failure & avoid the weird placeholder mechanism: the client can immediately upload its N files in parallel (maybe skip some TLS handshakes?), then do a single tool call w/ the server-returned resource URIs for all of them (or put some inline data uris). One less round trip for lower latency (And the initial tool call "upload_require" error needs to allocate upload uris so it's not even an instant call)

@meastes

meastes commented Oct 15, 2026 •

Copy link
Copy Markdown

So excited to see this!!

@localden Some potential concerns:

  • Where to put that x-mcp-file: if it's a key in the inputSchema's values, could it trip validators in some SDKs? (e.g. if $schema is defined, they might validate strictly) An alternative could be e.g. to list the file input names in Tool._meta['x-mcp-files'] (less pretty though, closer to SEP-2356: File input support for tools and elicitation modelcontextprotocol#2356 )

  • I'd suggest adding a new x-mcp-file/upload MCP method instead of relying on HTTP PUT (even though resumability seems about to be solved!)

    • HTTP PUT may have a higher client & server implementation burden (new external surface w/ new auth, is sharing the MCP auth w/ a potentially different domain ok? people w/ mcp logs access can replay / overwrite uploads? <insert security vuln here>)
    • a new MCP method would be orthogonal to transports & auth concerns
    • a new method would remove the need for a first tool failure & avoid the weird placeholder mechanism: the client can immediately upload its N files in parallel (maybe skip some TLS handshakes?), then do a single tool call w/ the server-returned resource URIs for all of them (or put some inline data uris). One less round trip for lower latency (And the initial tool call "upload_require" error needs to allocate upload uris so it's not even an instant call)

I see a couple of concerns with using an MCP method for the file transfer. How would that method carry the bytes? Adding a method alone doesn’t change MCP’s JSON-RPC encoding:

  • Encoding the file as base64 adds roughly 33% overhead before compression and subjects large files to MCP request-size limits and JSON processing.
  • Carrying raw binary within MCP would require changes to the transport contract. A separate HTTP endpoint accepting raw bytes is possible, but that would still be an out-of-band upload and introduce the additional HTTP surface mentioned above.

The metadata-first approach also seems valuable. It lets the server validate the destination folder, permissions, quota, and other constraints before transferring bytes. An upload-first approach would need to preserve that context somehow; otherwise, we could transfer a large file only to reject the subsequent tool call.

@tsarlandie-oai

Copy link
Copy Markdown

Looks great - few comments:

  • How about requiring uri, size and digest on any input argument with the x-mcp-file attribute? This way we would not have the separate metadata on the request.

    Something like:

        type: object
        x-mcp-file: {}
        required: [uri, size, digest]
        properties:
          uri:
            type: string
            format: uri
          size:
            type: integer
            minimum: 0
            description: Size of the decoded file in bytes.
          digest:
            type: object
            required: [algorithm, value]
            properties:
              algorithm:
                const: sha-256
              value:
                type: string
                pattern: '^[A-Za-z0-9_-]{43}$'
    
  • I think we can remove the need for a custom URI scheme. We can use uri to uniquely identify each file with a client generated unique id (could be uri: myresume.pdf). The server re-uses the uri to provide upload information to the client. Once a file has been uploaded, the client can notify the server via a inputResults argument with:

    inputResults: {
      "uploads": {
          "<file uri>": { /* maybe repeat the url here? - or just an empty object? */ }
        }
    }
    
    • Server can identify "pending" files because they are in the request but not in inputResults.uploads. No need to specifically mark them.
  • "Clients MUST honor credentials: "include" only when url has the same origin as the MCP server's"

    • I would love to hear from Google and Microsoft on this. Is this enough? Or do you actually need cross-origin credentials?
  • In "Rationale": 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. I am not sure I am following this. Why does it have to be a string?

@EricGustin

Copy link
Copy Markdown

Thanks @localden!

One thing I want to make sure the draft accounts for is that MCP clients today aren’t only calling tools directly. A number of gateways and servers put one executor tool in front of the rest, so the model searches for the tool it wants and then calls the executor with that tool’s name and arguments.

For example, Arcade.dev’s Tool Recommendation does this with SelectTools and UseTool, Cloudflare’s Code Mode MCP server exposes search() and execute(), Docker’s Dynamic MCP gateway has mcp-find and mcp-exec.

In all of those the client never sees the inner tool’s schema, so it never sees x-mcp-file, and it can’t do any of the client-side steps in the draft (placeholder, metadata, inline vs out of band). The file reference reaches the server as-is and gets rejected. I don’t think the spec can be this rigid about how the client learns that a parameter takes a file. It needs to be flexible enough to cover this case too.

Comment thread specification/draft/file-uploads.md Outdated
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

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 don't know if the path prefix requirement is reasonable. A pretty common implementation is going to be "everything prefixed by /mcp is routed to the SDK, which speaks JRPC". For us at least, it's most natural for the OOB endpoint to reside on a different path (but same origin) from the mcp server.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

+1 to this, I think same origin is the right level of restriction here, because, to Kevin's point, it will often be the case that the server endpoints for receiving the bytes are not on the /mcp path.

@simontaurus simontaurus 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.

Confirming from a host we have run since March across three agents, file params declared with an x-file keyword and resolved to bytes before dispatch.

@EricGustin's case has a non-gateway instance: a file referenced inside a free-text argument, ![alt](diagram.png) in a markdown parameter rendered into a document. No declared file argument exists, so the placeholder, _meta and inline-vs-out-of-band paths never engage. We fall back to resolving registered filenames inside text values. However ideally the model fills an attachment list and places the attachments also in the free text argument.

@mgoldsborough's host handle is what we also came up with, as he mentioned: the model names a file, the host swaps in the bytes before dispatch, and the result path strips URLs back out.

On open question 5, how the tool's typed result reaches the model: a symbolic name plus size, nothing else. The name has to carry the relative subfolder for multi-file output, and the model must not echo the size back or the next lookup breaks.

tsarlandie-oai and others added 3 commits October 20, 2026 08:35
The initial draft separates file metadata from a placeholder URI and
replaces that argument on retry. This proposal keeps the metadata in the
marked file argument: required `id`, `mimeType`, `size`, and SHA-256
`digest`, with optional base64 `data` for inline bytes. When `data` is
absent, the server sees the intended tool call before choosing an upload
target.

The client resumes with unchanged arguments and reports `completed` or
`failed` per file under `inputResponses.uploads`. The echoed
`requestState` and `fileId` bind each report to the target. Retryable
transfers stop after three total attempts per file, or sooner for
permanent or unsafe failures. The original empty extension declaration
remains; this proposal adds no capability profile flags. Standard
request `_meta` is omitted from the examples for brevity, while file
metadata is in the argument.

This changes the specification and its README overview. The proposed
upload input request requires a core MRTR extension point; it is not
currently interoperable with unmodified MCP.

Validation: all five embedded JSON examples parse; the upload sequence
agrees on arguments, state, file ID and digest; `git diff --check`
passes. No runtime or upstream conformance test was run.
The draft covered passing omit targets through and relaying include targets. Let a gateway relay either kind from its own origin, and require it to check the original target before forwarding the bytes.
Return a file as a resource_link whose _meta carries a download target with the same url, headers and credentials as an upload target. Mark the section as not yet reviewed by the working group, explain the choice in Rationale, and extend the redaction rule to cover both directions.
@markmnl

markmnl commented Oct 22, 2026

Copy link
Copy Markdown

I maintain fmsg-mcp, an MCP server for fmsg, a messaging protocol where
attachments are first-class.

Reference implementation: markmnl/fmsg-mcp#25 implements the
server side of this draft (built against 6101d34), including multiple files per call and an end-to-end test.
docs/files-extension-notes.md lists 24 gaps we hit, each with a suggested fix. The main ones:

  1. The TypeScript SDK rejects custom inputRequests entries, so we add inputRequests.uploads in the HTTP layer
    as a workaround. The SDK needs a proper way to do this.
  2. Upload targets should have an explicit expiresAt. Ours are single-use, expire after 10 minutes, and are
    bound to the file's size and digest.
  3. With several server instances, a retry must reach the instance that issued the target, and an abandoned call
    leaves staged data nobody can safely delete. The spec should say how long staged uploads live and who cleans up.
  4. Not every backend can check quota before upload, so the spec shouldn't assume the server can always refuse early.

Why this matters to us: an agent couldn't send a 76 KB PDF through our server, because base64 meant ~100K
characters through the model. PNGs sent that way arrived corrupted, and JPEGs arrived silently altered. So +1 to
the host doing the transfer, and to a required digest on every file.

Still missing: a way for hosts to upload files the agent created itself (not just user-picked ones), and
downloads, since hosts often reject common types like PDFs.

Comment thread specification/draft/file-uploads.md Outdated

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Just noting that this is another advantage to eventually support chunking since the client (browser) will only have to calculate the digest for the passed chunk.

Comment thread specification/draft/file-uploads.md Outdated
Comment on lines +94 to +96
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),

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'm not sure that a tool error is the correct mechanism. This would send the error to the model, but the model has no control over whether the client sends files as inline bytes or out-of-band. I think this should instead be protocol error handled directly by the client. Per-file maxSize violations can stay tool errors

@scott-harveyai

Copy link
Copy Markdown

Wanted to +1 @meastes's point on allowing servers to declare that they don't support inline uploads. maxInlineBytes: 0 seems like a reasonable mechanism to me if we close a couple of gaps:

  1. The draft never says what 0 means. Read literally, it rejects any non-empty inline file but still allows data: "", and nothing tells clients to treat it as "no inline."
  2. Clients have no requirement to follow an advertised limit. The only client rule, the SHOULD, covers the case where maxInlineBytes is omitted. When the server does set it, enforcement is left entirely to the server rejecting the call.

Something like:

maxInlineBytes is the largest total decoded size of inline data values the server accepts in one request. When the server advertises it, clients MUST NOT send a request whose inline data values exceed it in total. A value of 0 means the server does not accept inline transfer, and clients MUST omit data for every file argument.

Very excited for this, will unlock a lot of great functionality for users of Harvey's MCP!

@petert82

Copy link
Copy Markdown

As someone working on fynk's MCP server, super-excited to see this! File uploads have been such a pain point for us until now.

Just wanted to +1 @meastes point on including the HTTP method in the upload target metadata. It would be very helpful for a server to be able to specify POST as opposed to PUT

@freb

freb commented Nov 2, 2026

Copy link
Copy Markdown

We run a Streamable HTTP server whose backend takes files and data in one request using the
[GraphQL multipart request spec](https://github.com/jaydenseric/graphql-multipart-request-spec): a single multipart/form-data POST with the operation JSON, a map, and the raw file parts. We moved to it specifically so a file can never exist without the record it belongs to. A failed or abandoned call doesn't leave any orphaned files.

The out-of-band flow brings that problem back since the bytes land before the retry, and if the retry never comes, cleanup depends on storage expiry. That's the gap @markmnl raised in point 3.

Would the group consider an optional mode for Streamable HTTP where the tools/call JSON-RPC message goes as the first part of a multipart POST, and each x-mcp-file argument references a following part (for example cid: or a part name, like GraphQL's map)? That keeps raw bytes out of JSON with no base64 or size inflation and needs no second endpoint or credential. The tool also gets the file and its arguments atomically. @meastes is right that this touches the transport contract, which is why I'd propose it as opt-in, with out-of-band staying the default. Metadata-first rejection still works if the server checks the JSON part before reading the file parts.

If that's out of scope, I'd ask the spec to at least define a staged upload's lifetime and ownership, and to require that a server never makes a staged file visible until the retry commits it.

@bgouldsb

bgouldsb commented Nov 2, 2026

Copy link
Copy Markdown

Want to +1 @meastes comment about allowing servers to declare that they don't support inline uploads. Ideally we would like to avoid inlining all files if possible. As someone who works on Descript's MCP server this could be incredibly helpful for us. Upload reliability has been a bit of a thorn in our side for a while now and this seems like it has the components to get us where we need to go.

localden and others added 3 commits November 4, 2026 07:18
localden and others added 5 commits November 5, 2026 07:11
Co-authored-by: den <den@anthropic.com>
…ten credentials and errors

Co-authored-by: den <den@anthropic.com>
…transfer rules

Co-authored-by: den <den@anthropic.com>
… and digest

Co-authored-by: den <den@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.