Skip to content

Presigned single-use links for downloads and draft uploads; base64 is best effort - #27

Merged
markmnl merged 1 commit into
mainfrom
presigned-links
Oct 22, 2026
Merged

markmnl merged 1 commit into
mainfrom
presigned-links

Conversation

@markmnl

@markmnl markmnl commented Oct 22, 2026

Copy link
Copy Markdown
Owner

Replaces #26. That PR had to be split from the file-uploads extension (#25) so it can land first. Rebasing the stack made GitHub mark #26 as merged into #25's branch and delete its head branch, so it can't be reopened. Nothing from it reached main. Same commit, now on presigned-links.

Presigned links

get_attachment_download_url and the draft upload URL both needed the connection's bearer token. Hosts that cannot attach it to their own HTTP requests had to fall back to base64 through the model.

  • Downloads: get_attachment_download_url takes presigned: true and adds presigned: {url, method, expires_at, size, sha256, curl}. The server streams the attachment once to compute its sha256.
  • Uploads: new tool get_attachment_upload_url with {id, filename, size, sha256, content_type?}. It returns {draft_id, filename, url, method: "PUT", expires_at, size, sha256, curl}. It refuses sent messages, other people's drafts, and names already taken. The OAuth scope is read+write.
  • Links are /mcp/links/{token}: an HMAC-signed capability for one transfer, valid for 5 minutes, bound to the message or draft, file name, size and sha256. FMSG_MCP_STATE_SECRET (at least 32 bytes) sets the key.
    • GET links work once. They are spent only after the attachment is found, so concurrent requests cannot both use one.
    • PUT links take one successful upload; failed attempts leave them usable. Uploads go through the new streamIntoDraft in src/upload.ts, which enforces exact size and sha256 while streaming. The file-uploads extension (Reference implementation of the MCP file-uploads extension #25) reuses it.
  • If the attachment changes after its download link was issued, the client never receives all the declared bytes. The last chunk is held back until size and sha256 are verified. A small file gets 409 before any byte is sent; a large one is cut off.
  • Links never contain an API key or access token. The caller a link acts for stays in the issuing process, so a link works only on that instance. Elsewhere it gets 404 with a message saying so. AGENTS.md's "keep credentials out of links" rule now names this one exception.

data_base64 wording

The data_base64 and sha256 descriptions, the instructions, the tool guidance and the send_file prompt now say three things. Base64 passes through the model, so it is best effort and suited to small files (verified exact up to 16 KB). Always pass sha256. For anything larger, prefer path or a presigned link. Text names only the routes the running server registered, so FileRoutes gained presigned. #25 adds the extension to that wording.

Tests

  • test/links.test.ts (11 tests). It runs the returned curl commands for real, over HTTPS through a TLS proxy (test/tls.ts, with a throwaway openssl certificate made at test time). Covered:
    • download once and refuse the second
    • upload, then send_draft
    • size and digest mismatches, reuse, wrong method, tampered and expired links
    • a changed attachment, small and large
    • tool refusals
    • instance-bound links with a shared key
    • shell quoting
  • npm run typecheck && npm run build && npm test: 273 passed.

🤖 Generated with Claude Code

Hosts that cannot send this connection's Authorization header with their own
HTTP requests had no way to move a file without writing it through the model.

- get_attachment_download_url takes presigned: true and also returns a
  single-use GET link valid for 5 minutes, with its size, sha256 (computed
  from the attachment), expires_at and a ready-to-run curl command. If the
  attachment changes after the link is issued, the download is refused, or
  cut off before its last bytes because the last chunk is held back until
  the whole body is verified.
- The new get_attachment_upload_url returns a single-use PUT link valid for 5
  minutes into one draft, bound to the declared size and sha256, with a curl
  command. Exact size and digest are enforced while streaming, one upload
  succeeds, and failed attempts leave the link usable (streamIntoDraft in
  src/upload.ts).

Links live at /mcp/links/{token}: an HMAC-signed capability for one
transfer that never contains an API key or access token. The caller they
act for stays in the issuing process, so a link works only on the instance
that issued it. FMSG_MCP_STATE_SECRET sets the key.

data_base64 is now described as best effort, because the base64 passes
through the model: fine for small files (verified exact up to 16 KB), always
with sha256, and path or a presigned link for anything larger. Tool text
names only the routes the server registered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@markmnl
markmnl marked this pull request as ready for review October 22, 2026 06:25
@markmnl
markmnl merged commit 2e7b431 into main Oct 22, 2026
8 checks passed
@markmnl
markmnl deleted the presigned-links branch October 22, 2026 06:26
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.

1 participant