Repository navigation
Conversation
markmnl
force-pushed
the
files-extension
branch
from
October 22, 2026 05:46
298874c to
397dcb7
Compare
markmnl
force-pushed
the
files-extension
branch
from
October 22, 2026 06:17
397dcb7 to
09b708c
Compare
A reference implementation of the experimental file-uploads draft at modelcontextprotocol/experimental-ext-file-uploads, branch localden/initial-draft at 6101d34eb52b21d4b87028543ecd1fd59ed3d5df (the head after PR #2's merge). Over HTTP with an https public MCP URL, server/discover advertises the extension with maxInlineBytes, and the attachments of send_message, reply and create_draft also take FileInputs ({id, name, mimeType, size, digest, data?}, marked x-mcp-file), several per call. - Inline files are decoded and checked against their size and digest. - Files without data are checked on their metadata (ids, names, media type, digest form, the operator's per-file limit, the inline budget) after the tool's own checks. The call then creates its fmsg draft as the staging location and answers input_required with one upload target per file. - Targets are PUT /mcp/uploads/{token} with credentials "include": an HMAC token bound to the principal, draft, file, declared size and SHA-256, call and expiry. Each takes one successful upload and streams it into the draft through the same receiver as presigned upload links (streamIntoDraft), so no file bytes are kept on this server. - The retry is verified by the SDK codec (MAC, expiry, principal; -32602 on failure), then against the tool, the canonical argument hash and the per-file outcomes. Each file is confirmed in the draft by name, size and digest before the draft is sent (or returned by create_draft). Failed outcomes, early retries, repeated or concurrent retries, expired targets and malformed outcomes get specific errors. The SDK accepts only the base protocol's input request kinds, so a tool returns input_required with requestState alone, and the HTTP layer adds inputRequests.uploads to that response (JSON or SSE). Classic data_base64, data_text and stdio path attachments are unchanged. Tests cover the rules, plus an end-to-end run of a minimal client harness over HTTPS through a TLS proxy and the OAuth path. docs/files-extension-notes.md lists every ambiguity or gap in the draft, the choice made, and a suggested fix, for the working group. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
markmnl
force-pushed
the
files-extension
branch
from
October 22, 2026 06:27
09b708c to
0a201ff
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Based on
main(presigned links landed in #27). On hold until the upstream draft (modelcontextprotocol/experimental-ext-file-uploads#1) merges.Uploads to targets go through #27's
streamIntoDraft(src/upload.ts).A reference implementation of the experimental MCP file-uploads extension (
io.modelcontextprotocol/files). It is built againstspecification/draft/file-uploads.mdonlocalden/initial-draftat6101d34eb52b21d4b87028543ecd1fd59ed3d5df, the head of the draft branch after PR #2 merged as348c794.What it does
The extension is on over Streamable HTTP with an https public MCP URL.
FMSG_MCP_FILES_EXTENSION=offturns it off.server/discoveradvertises{"io.modelcontextprotocol/files": {"maxInlineBytes": N}}. Upload requests go only to requests that declared the extension in their per-requestclientCapabilities.attachmentsitems ofsend_message,replyandcreate_draftareanyOf: [classic attachment, FileInput]. The FileInput branch is markedx-mcp-file, withmaxSizewhen the operator sets a per-file limit. A call can carry several files.maxInlineBytes.input_requiredwith one target per file underinputRequests.uploads.PUT {public URL}/uploads/{token}withcredentials: "include"(same origin, under the canonical path). The token is an HMAC bound to the principal, draft, file, declared size and SHA-256, media type, call and expiry (10 minutes by default). A target takes one successful upload and refuses a wrong size (413/400) or digest (422). Bytes stream into the draft, so none are stored on this server.requestState(MAC, expiry, principal binding) and answers-32602on failure. The flow then checks the tool, a canonical hash of the arguments, and the per-file outcomes, and confirms each file in the draft by name, size and SHA-256. Only then is the draft sent, or returned forcreate_draft.data_base64,data_textand stdiopathwork as before, and stdio does not advertise the extension.SDK workaround
SDK v2.0.0 accepts only elicitation, sampling and roots as
inputRequestsentries; this is the draft's Open Question 1. So a tool returnsinput_requiredwithrequestStateonly.FilesRuntime.servethen addsinputRequests.uploadsto that one response, in JSON or SSE, via a per-requestAsyncLocalStoragescope. The inboundinputResponses.uploadspasses through the SDK unchanged.Code
src/files/extension.ts: identifiers, the FileInput schema, metadata checks, digest conversion, argument hashing.src/files/runtime.ts: HMAC key, signed targets, call state, the abandoned-draft reaper, response injection.src/files/flow.ts: initial call and retry.src/files/upload-route.ts: the PUT handler.src/files/tools.ts: what the send-type tools share.send.tsanddraft.ts: the tools' result building is pulled into functions so the upload path returns the same results.FMSG_MCP_FILES_EXTENSION,FMSG_MCP_FILES_MAX_INLINE_BYTES,FMSG_MCP_FILES_MAX_FILE_BYTES,FMSG_MCP_FILES_TARGET_TTL_SECONDS,FMSG_MCP_STATE_SECRET(needed when several instances serve one endpoint).Feedback for the working group
docs/files-extension-notes.mdlists 24 places where the draft is ambiguous or silent, the choice made and a suggested spec fix. Examples: the MRTR extension point, the undefined upload-answer interface,x-mcp-fileinsideanyOf, how retries are rejected, what "single-use" means with three attempts, which statuses are permanent, staging when the backend has no storage-side expiry, and inline-only servers.Tests
test/files-extension.test.ts(27 tests). An end-to-end run of a minimal client harness (test/files-client.ts) over HTTPS through a TLS proxy, with a throwaway openssl certificate made at test time so no key is committed. The harness does the client side: declare, check targets (https, address allowlist, headers,includeonly for the canonical URI), PUT with at most three attempts, retry. Tests cover send/reply/create_draft with out-of-band, inline and classic files in one call, plus every rule above.test/files-runtime.test.ts: JSON and SSE injection, the reaper (single key versus shared), target signing and expiry.test/oauth.test.ts: targets needfmsg:writeand stream with the exchanged Web API token.npm run typecheck && npm run build && npm test: 308 passed. The suite needsopensslon PATH for the HTTPS tests; they are skipped without it.Not done
🤖 Generated with Claude Code