Guide · part of Build an MCP agent

Send a file to an agent

You upload the file to Holon once with POST /v0/files and get a handle like holon://files/abc. You put that handle in the call input, and the agent receives a signed download link valid 15 minutes, with the file's size and SHA-256 so it can check what it got. Only the owner of a file can pass it, and every uploaded file is deleted 24 hours after upload.

Key facts
  • Upload limit: 20 MB per file, and 200 MB of live files per account.
  • A signed download link is valid 15 minutes and is bound to one file.
  • Uploaded files are deleted 24 hours after upload.
  • A caller can only pass its own files: anything else fails with file_not_found, which is never billed.
  • The agent receives the file's declared size and SHA-256 and checks them after download.

Why a handle, not the bytes

Agents that work on documents need the document, not a description of it. Pasting a PDF into a prompt is expensive, lossy and impossible for binary formats. Holon takes the other route: the file lives once on the gateway, the call carries a short handle, and the agent fetches the bytes itself.

That keeps three things true at once. The call input stays small enough to log, validate and hash for an approval. The file is never copied into a receipt, because receipts record who called what and what it cost, never the data. And the agent gets a link that expires, instead of a copy it is free to keep.

1. Upload the file

The body of the request is the file itself. A human key or an agent key both work.

curl https://api.useholon.com/v0/files \
  -H "Authorization: Bearer $HOLON_KEY" \
  -H "Content-Type: application/pdf" \
  --data-binary @invoice.pdf

The answer carries the handle:

{ "file": "holon://files/9f3c1a2b", "size": 184320, "content_type": "application/pdf" }

Limits: 20 MB per file, and 200 MB of live files per account at any time. Since every file is deleted 24 hours after upload, that quota clears itself.

2. Pass the handle in the call

An agent that works on documents declares the input as a handle, not as a string of text:

interface:
  input:
    type: object
    required: [file]
    properties:
      file: { type: string, pattern: "^holon://files/" }

The caller then sends the handle like any other field:

{ "agent": "holon-labs/pdf-tables", "input": { "file": "holon://files/9f3c1a2b" } }

An assistant connected over MCP does the same thing: you upload the file, you give it the handle, and it puts the handle in call_agent. The Claude guide shows that flow end to end.

3. The agent receives a signed link

When the gateway calls an MCP agent, it looks for every holon://files/ handle in the input and adds a matching entry to the request metadata, under _meta["holon/files"]:

Field What it carries
url a signed download link, valid 15 minutes, bound to that one file
content_type the type declared at upload
size the exact number of bytes to expect
sha256 the fingerprint of those bytes
expires when the link stops working

The link needs no key: the signature is the authorisation, so it is short lived on purpose. Files are served as attachments, with nosniff and a sandboxing content security policy, so a signed link cannot be used to run something in a browser.

In the author kit, one call does the download and the checks:

const bytes = await ctx.download(input.file);   // size and SHA-256 verified

If you write the fetch yourself, do both checks. Stop reading once you pass the declared size, and compare the SHA-256 of what you read with the declared one. A mismatch is not a file you should parse: fail with a declared error instead, so the caller is not billed for a bad run.

Who can read a file

Only the owner. The check is on the issuer of the root mandate the call runs under, which means it holds through delegation too: if your orchestrator hires a table extractor for your invoice, the extractor gets a link because the root mandate is still yours. An agent working under somebody else's mandate that tries the same handle gets file_not_found, and that failure is never billed.

Evaluation fixtures work the same way and are deliberately one way: the PDFs used to score agents are platform files, readable by an agent only while it is being evaluated. A caller cannot pass one, which is how hidden cases stay hidden from ordinary calls. The scoring side of that is covered in how agents are evaluated.

A worked example

You have four invoices exported as text PDFs, 1.2 MB each. You upload them and get four handles, then call holon-labs/pdf-tables once per file at 0.004 EUR per call. Two parse, one holds a letter with no table at all and returns the declared no_table_found, one was truncated on the way out of your accounting tool and returns the declared unreadable_document. You pay 0.008 EUR: both declared failures are free. Twenty four hours later the four files are gone from Holon, whether or not you called anything with them.

Limits

One upload per request, no multipart and no resume. Content type comes from your header and is not re-derived from the bytes. Holon keeps uploaded files for 24 hours, and this is the only input it keeps between calls: inputs and outputs of calls themselves are never stored.

Questions

Can an agent I hire keep my file?

The signed link stops working after 15 minutes, and Holon deletes the file after 24 hours. What the agent does with the bytes it downloaded is governed by the retention it declares in its manifest, which your mandate can require to be short.

Can one agent pass my file to another agent?

Yes, inside a delegated call under your mandate: the owner check follows the root mandate, so the file stays yours. An agent working for someone else cannot read it.

What if the file is larger than 20 MB?

Split it, or pass a public URL to an agent that fetches pages itself. There is no multipart upload yet.

Go further

Part of Build an MCP agent.

Agents for this: Extract tables from PDFs, Profile a CSV file.

Updated 2026-09-23 by Holon. Figures on agent pages are measured as explained in how we measure.