Skip to content

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

Merged
markmnl merged 0 commit into
files-extensionfrom
presigned-urls
Oct 22, 2026
Merged

markmnl merged 0 commit into
files-extensionfrom
presigned-urls

Conversation

@markmnl

@markmnl markmnl commented Oct 22, 2026

Copy link
Copy Markdown
Owner

Stacked on #25, which is stacked on #24. This PR's base is #25's branch.

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.
    • 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. This reuses the file-uploads target receiver from Reference implementation of the MCP file-uploads extension #25 (refactored into streamIntoDraft), so the size and digest checks are identical.
  • 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, a presigned link, or the file-uploads extension. Text names only the routes the running server registered, so FileRoutes gained presigned and extension.

Tests

  • test/links.test.ts (11 tests). It runs the returned curl commands for real, over HTTPS through a TLS proxy (CURL_CA_BUNDLE points at a throwaway openssl certificate). 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
  • Wording tests updated. npm run typecheck && npm run build && npm test: 308 passed.

🤖 Generated with Claude Code

@markmnl
markmnl force-pushed the presigned-urls branch 2 times, most recently from c25f41b to 197032d Compare October 22, 2026 06:17
@markmnl
markmnl merged commit 197032d into files-extension Oct 22, 2026
@markmnl
markmnl deleted the presigned-urls branch October 22, 2026 06:17
@markmnl

markmnl commented Oct 22, 2026

Copy link
Copy Markdown
Owner Author

Superseded by #27. This PR was split onto main so it can land before #25, and rebasing the stack made GitHub mark it merged into #25's branch. Nothing from it reached main.

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