Send a file
upload and download print a one-time URL. A human opens the browser link, or another machine streams with curl -T after upload PATH --curl. You never need their local disk path from MCP, and you should not paste binaries into chat.
There is no upload --to. The path is the first argument. It is relative to the current working directory and must stay under $HOME.
MCP write_file / read_file are UTF-8 text only. Use transfer for CSVs, zips, images, and anything awkward to paste.
Done looks like
- In (browser):
upload pollprints success and the file exists at the path you named. - In (curl):
curl -Treturns200withok: true, thenupload pollsucceeds. - Out: the person opens the URL once and the browser saves the file (or a
.tar.gzfor a directory). No poll step.
Worked example: get a spreadsheet into the repo
Job: a human has q2-leads.csv on their laptop. You need it at inbox/q2-leads.csv.
upload inbox/q2-leads.csv
Expected output (shape, not exact host):
Open this URL in your browser to upload (repository members):
https://…/sails/transfer/…
Target: ~/inbox/q2-leads.csv
Transfer session: 3f2a9c1e-…
- Send that URL to the person.
- They sign in as a repository member and drop the file.
- You wait:
upload poll 3f2a9c1e-…
When poll returns success, the file is in the live workspace and autosave has it. Then you can read it, publish it, or hand it to another command.
Do not pass --wait from bash_exec. That call buffers until the upload finishes, so you never see the URL. Print the URL, then upload poll ID.
Worked example: hand someone a report
download sites/sails/pages/blog/launch.page.md
That prints a one-time URL. First successful GET downloads the file and consumes the link. There is no download poll.
A directory is packed as gzip tar:
download ./site
# browser saves site.tar.gz
Worked example: reverse-curl from another machine
Job: a laptop, Grok Bot, or Cursor box already has hero.png. The browser path is wrong. Mint a capability URL, then curl -T from that machine. Bytes never ride in MCP tool JSON.
upload ~/looks/hero.png --curl
Expected shape (JSON):
{
"ok": true,
"id": "3f2a9c1e-…",
"url": "https://…/sails/transfer/…",
"method": "PUT",
"target": "/org/repo/looks/hero.png",
"max_bytes": 104857600,
"expires_at": "…"
}
On the machine that has the file:
curl -T ./hero.png \
-H "Content-Type: application/octet-stream" \
-H "Content-Length: <n>" \
"$UPLOAD_URL"
Optional checksum — reject on mismatch before commit:
curl -T ./hero.png \
-H "Content-Type: application/octet-stream" \
-H "X-Content-SHA256: $(sha256sum ./hero.png | awk '{print $1}')" \
"$UPLOAD_URL"
Success is 200 plus { "ok": true, "path": "…", "bytes": N, "sha256": "…" }. Then in the workspace:
upload poll 3f2a9c1e-…
--curl is file only. --dir stays browser / tar. --wait is not valid with --curl.
Upload a folder
upload assets --dir
upload poll TRANSFER_ID
The browser can pick a folder, or they can upload a .tar / .tar.gz / .tgz and it extracts into assets.
Flags
| Command | What it does |
|---|---|
upload PATH | One-time browser URL for a single file at PATH |
upload PATH --curl | JSON capability URL for curl -T (file only; no browser cookie) |
upload PATH --dir | One-time browser URL for a directory (folder picker or archive) |
upload poll ID | Block until that upload finishes, fails, or expires |
upload PATH --wait | Block in one command — avoid inside bash_exec. Not valid with --curl |
download PATH | One-time URL; directories become .tar.gz |
Common flag: --timeout SECS (default 900). After that the session expires; run the command again.
upload help
download help
Failure modes
| What you see | What it means | What to do |
|---|---|---|
| Browser asks to sign in / 403 | Browser transfer URLs need a repository member cookie | They must be signed in as a member of this repo |
| Link dead on second open | Single-use (download completes on first GET; curl URL returns 410) | Run upload / download again |
upload poll times out | Nobody finished the browser / curl step, or --timeout elapsed | New upload PATH (or --curl), new URL |
| Path rejected | Outside $HOME / the active repo mount | Use a path under the workspace |
| File missing after “I uploaded” | You never polled, or they hit a different session | Confirm the transfer id from the upload output, then upload poll |
| 100 MiB exceeded | Hard limit | Split the archive, or fetch a public URL with curl -o instead |
You used upload --to DIR | That flag does not exist | upload DIR/filename or upload DIR --dir |
--curl with --dir or --wait | Reverse-curl is a single file; --wait is browser-only | Drop --dir / --wait; mint with --curl, then upload poll |
Curl ok: false / checksum reject | Body never committed | Fix the file or X-Content-SHA256, mint a new URL |
When not to use this
| Job | Use instead |
|---|---|
| Small text edit already in chat | MCP write_file / edit_file |
| File is already on the public internet | curl -o path https://… |
| API key, token, DSN | secret open — see Store a secret |
| Public URL for a folder | Publish a site |
| Another repository should copy a folder | Share a directory with another repo |
See File transfer.