Upload files
Some scripts can upload a file, such as sending an email with attachment, uploading a CV for a job form, or an invoice for a portal.
Uploading files with scripts has different modus operandi, whether we run the script from MCP, the CLI or the REST API.
We will see how it works with an example with the reduck/mail.google.com/send_email, from the official library.
From your agent (MCP)
An agent cannot upload a file cheaply: it would have to write every byte as text. So it names a file that is already on your machine, and your Chrome reads it from your disk.
- Turn on file access for the extension. You do this once per browser.
Open
chrome://extensions, click Details on Reduck, and turn on Allow access to file URLs. - Put the file in the
reduckfolder on your Desktop. Create the folder if it does not exist.mkdir -p ~/Desktop/reduck cp ~/Downloads/one-integration-every-website.png ~/Desktop/reduck/
- Ask your agent:
Using Reduck MCP, send one-integration-every-website.png from my reduck folder to me@example.com by Gmail, with the subject "Our new banner".
The agent makes one
run_scriptcall. Thehandleidentifies the script’s owner:@user(with@) for a user, or a project handle (without@) likereduckfor the official catalogue (see handles). The file is named relative to thereduckfolder:{ "script": { "host": "mail.google.com", "slug": "send_email", "handle": "reduck", "args": { "to": "me@example.com", "subject": "Our new banner" }, "files": { "attachment": { "relativePath": "one-integration-every-website.png" } } } }
The file keeps its own name, so the email arrives with
one-integration-every-website.pngattached. A file in a sub-folder is named with its path:2026/invoice.pdf.
From the CLI
The CLI runs on your machine, so it reads the file itself and sends the bytes with the run. There is nothing to set up, and the file can be anywhere.
npx @reduck-ai/cli@latest run \ --script reduck/mail.google.com/send_email \ to=me@example.com \ subject="Our new banner" \ attachmentName=one-integration-every-website.png \ file:attachment=~/Downloads/one-integration-every-website.png
file:attachment=<path> binds the file input named attachment to a file on your disk. Bytes carry
no file name, so attachmentName gives the name the recipient sees. Without it, the file arrives
named attachment. The result comes back as JSON:
{ "sent": true, "to": ["me@example.com"], "cc": [], "bcc": [], "subject": "Our new banner", "attachment": "one-integration-every-website.png" }
Sending the bytes, from the CLI or the REST API, is the only way that works on a managed browser, because a managed browser cannot see your disk.
From the REST API
POST /run takes a file in two ways. Both call the same script as above.
Send the bytes. Send the request as multipart/form-data: the usual JSON body goes in a spec
field, and the file goes in a part named <step>:<file input>. For a single script, the step is
0. A file can be up to 50 MB.
curl -X POST https://mcp.reduck.ai/run \ -H "X-API-Key: $REDUCK_API_KEY" \ -F 'spec={"browser":"extension","script":{"host":"mail.google.com","slug":"send_email","handle":"reduck","args":{"to":"me@example.com","subject":"Our new banner","attachmentName":"one-integration-every-website.png"}}}' \ -F '0:attachment=@one-integration-every-website.png'
As with the CLI, bytes carry no file name, so pass attachmentName.
Name a file on your machine. Send JSON, with the file in files, as the agent does. This works
only on your own Chrome, with file access turned on (see From your agent).
curl -X POST https://mcp.reduck.ai/run \ -H "X-API-Key: $REDUCK_API_KEY" \ -H "content-type: application/json" \ -d '{"browser":"extension","script":{"host":"mail.google.com","slug":"send_email","handle":"reduck","args":{"to":"me@example.com","subject":"Our new banner"},"files":{"attachment":{"relativePath":"one-integration-every-website.png"}}}}'
Both answer with the script’s result and a runId:
{ "result": { "sent": true, "to": ["me@example.com"], "cc": [], "bcc": [], "subject": "Our new banner", "attachment": "one-integration-every-website.png" }, "runId": "86b0479f-a68f-4fcd-92d1-b68f485ee3d9" }
browser: "extension" runs on your own Chrome, where you are signed in to Gmail. Without it, the
run goes to a managed browser, which needs a connector for Gmail.
Write a script that takes a file
A file input is a string property marked format: "file" in the script’s input schema. Its key is
the name the script uses for the file.
{ "type": "object", "required": ["to", "subject"], "properties": { "to": { "type": "string" }, "subject": { "type": "string" }, "attachment": { "type": "string", "format": "file" } } }
In the script, one line puts the file into a file field of the page. It is the same line for both ways of giving the file:
await builtins.uploadFile("input[type=file]", "attachment");
Two rules to know:
- A file input is optional unless
requiredlists it. Listed, a run with no file stops withmissing file input "attachment". Not listed, the script checksfiles.attachmentbefore it callsuploadFile, so one script serves the caller with a file and the caller without one. - Bytes arrive under the input’s name. A file named by
relativePathkeeps its own name, but bytes from the CLI reach the page as a file namedattachment. If the site shows the file name, as Gmail does, take the real name as an ordinary argument and rename the file in the page. That is whatattachmentNamedoes in the example.
When it does not work
Each failure stops the run before anything is sent, and says what to fix.
This paired browser can't read local files: Allow access to file URLs is off. Turn it on inchrome://extensions, then run again.must be a plain name inside the Reduck folder: the name is an absolute path, or contains... Name the file relative to~/Desktop/reduck.bound 0 bytes — the browser could not read: the file is not in the folder, or Chrome cannot read your Desktop. Check the file is in~/Desktop/reduck. On macOS, allow Chrome to read the Desktop folder.a managed browser can't read a file from your machine: a file was named on a managed run. Use the CLI, which sends the bytes.unknown file inputormissing file input: the names you gave do not match the script’s file inputs. Read the script’s inputs and use the same names.Payload Too Large(HTTP 413) from the REST API: a base64 file made the JSON body bigger than 100 KB. Send the file with multipart instead.