AI agents are good with code because code is plain text on a local disk. The agent looks at what's there, searches, and opens only the part that matters.
A company's knowledge doesn't look like that. It's annual reports, spreadsheets and contracts in an S3 or R2 bucket. An agent with a shell downloads them and writes Python to parse them, again on every run. An agent without a shell (Claude Desktop, a chat app, a support bot, any MCP-only client) often can't use them at all.
That's the first half of the video. I gave Claude Code (Sonnet) only its Read, Grep and Glob tools, put the World Bank's 82 MB World Development Indicators workbook in the folder, and asked for one number:
> Bhutan: access to electricity in 2005?
● Glob **/WDIEXCEL.xlsx
✓ WDIEXCEL.xlsx
● Read WDIEXCEL.xlsx
✗ This tool cannot read binary files. The file ap…
ANSWER: Unable to determine. I couldn't read the xlsx…
11.8 s · $0.010
So I built bucketlens, an open source MCP server that lets any agent read the PDFs, spreadsheets and CSVs in your own bucket. Same question, with no files on disk and no shell, only bucketlens:
● outline real/wdi/WDIEXCEL.xlsx
✓ real/wdi/WDIEXCEL.xlsx: 6 table(s)
● query SELECT "Country Name","Indicator Name","2…
✓ | Bhutan | Access to electricity (% of populati…
ANSWER: 59.8% of Bhutan's population had access to el…
13.9 s · $0.023
(Lines are cut at the terminal width, as in the video. The value in the file is 59.8.)
Seven tools, built around how a person reads a long document: get the shape first, then go to the part that matters.
| Tool | What it returns |
|---|---|
list_files(prefix) |
names, sizes, dates; nothing is downloaded |
outline(path, depth) |
PDF: page count, table of contents with page numbers, what reading it whole would cost in tokens. Spreadsheet: sheets, rows, columns and types |
search(path, text) |
the pages of one PDF that contain the text, with snippets |
search_all(text, folder) |
every PDF in a folder at once, as hits or just file names and counts |
read_pages(path, pages) |
the text of specific pages, like "36-38" |
query(path, sql) |
read-only SQL over a spreadsheet or CSV; each sheet is a table |
read_rows(path, sheet, first, last) |
rows by the spreadsheet's own row numbers |
My first idea was to skip parsing and read slices of the original file with HTTP range requests. I measured it. Opening a PDF or reading the first rows of a workbook fetched 5 to 7% of the file, which is fine. Deep pages and deep rows fetched 50 to 75%, because of how PDF and XLSX are laid out inside. That's barely better than down.
So bucketlens reads each file once, the first time it's touched, and stores the result in your bucket next to it:
.bucketlens/index/<file>/<etag>/
manifest.json outline, sheets, and where each page starts
pages.txt PDF: every page's text, back to back
sheets/*.parquet XLSX/CSV: one Parquet file per sheet, with a _row column
pages.txt, so reading pages 600 to 602 of a 733-page PDF is one range request of about 150 KB.manifest.json is written last, so a half-built index is never used.
Because the index lives in the bucket and not on one machine, every agent, sandbox and laptop pointed at the bucket shares it.
You can run bucketlens index once, but files change. On Cloudflare R2, bucket event notifications go to a Queue, and bucketlens watch pulls from it over HTTP. In my tests a new 492-page PDF was indexed 8.8 s after its upload started, an edited workbook 4.4 s after it was saved, and a deleted file's index was gone 3 s later. For any other S3-compatible storage, bucketlens watch --poll 300 lists the bucket every 5 minutes instead.
I asked 10 questions about real public files: Berkshire Hathaway's 2025 annual report and 2026 quarterly reports, the World Bank workbook, and 100 commercial contracts from the CUAD dataset. Each question ran three ways with Claude Code (Sonnet), and answers were checked automatically against values taken from the files. The 2026 quarterly figures are newer than the model's training data, so they had to be read, not remembered.
| Setup | Right answers | Tokens | Cost | Time |
|---|---|---|---|---|
| Downloads the files, has a shell and Python | 10/10 | 226,231 | $0.21 | 405 s |
| Downloads the files, no shell | 6/10 | 433,144 | $0.59 | 370 s |
| bucketlens, no files, no shell | 10/10 | 159,280 | $0.23 | 141 s |
What I take from it:
.xlsx, and 1 of 2 on the 100 contracts, using three times the tokens bucketlens did.
query takes SQL from the model, so it's locked down twice. The server accepts one statement that starts with SELECT, WITH, DESCRIBE, SUMMARIZE, SHOW, PIVOT or FROM. Then the DuckDB connection itself is locked before the agent's SQL ever runs:
con.execute("SET disabled_filesystems = 'LocalFileSystem'; SET lock_configuration = true;")
So a query can't read or write files on your machine, and it can't change those settings back. Your keys stay in a file only you can read, and bucketlens never prints them. The agent can read anything the key can read, so give it a key scoped to one bucket.
With Claude Code (Python 3.10 or newer):
pip install bucketlens
bucketlens setup # bucket and keys, typed hidden
bucketlens index # optional: parse every PDF, XLSX and CSV now
claude mcp add bucketlens -- bucketlens serve
For Claude Desktop, Cursor or any client with a JSON config, there's nothing to install with uvx:
{
"mcpServers": {
"bucketlens": {
"command": "uvx",
"args": ["bucketlens", "serve"],
"env": {
"BUCKETLENS_R2_ACCOUNT_ID": "your Cloudflare account ID (or BUCKETLENS_ENDPOINT for S3)",
"BUCKETLENS_BUCKET": "your-bucket",
"BUCKETLENS_ACCESS_KEY_ID": "...",
"BUCKETLENS_SECRET_ACCESS_KEY": "..."
}
}
}
}
It works with Cloudflare R2, AWS S3, MinIO and other S3-compatible storage. On R2, the free tier covered every benchmark run in this post.
watch or index so agents never wait on it.
If your agents work with documents in a bucket, which files do they choke on? That's the list I'm building the next version from.
pip install bucketlens · MCP Registry: io.github.Pritom14/bucketlens
Music in the video: Sascha Ende, ende.app (CC BY 4.0).