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.)

What the agent gets

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

Parse once, keep it next to the file

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 downloading.

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 
  • PDF pages are byte ranges in pages.txt, so reading pages 600 to 602 of a 733-page PDF is one range request of about 150 KB.
  • Sheets are queried with DuckDB straight from the bucket. It reads only the columns and row groups a query needs: rows 150,000 to 150,049 of a 20 MB workbook came to about 190 KB.
  • The ETag is in the path. When a file changes, its ETag changes, so an agent is never answered from an old version. 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.

Keeping the index fresh

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.

Does it actually help?

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:

  • Without a shell, bucketlens changes the outcome. That agent got 0 of 3 spreadsheet questions because it can't open .xlsx, and 1 of 2 on the 100 contracts, using three times the tokens bucketlens did.
  • With a shell, the answers are the same. bucketlens is about 2.5x faster (the 82 MB workbook alone took 20 to 65 s to download) and uses fewer tokens. It is not cheaper: tool results count as fresh input, while a shell agent's long prompt is mostly cached. A re-run with the released package came to $0.30.

Letting an agent run SQL on your files

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.

Try it

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.

What isn't done yet

  • Word and PowerPoint aren't read yet. They're next.
  • Scanned PDFs without a text layer aren't searchable, because there's no OCR yet.
  • The first touch of a file reads it whole. Run watch or index so agents never wait on it.
  • One agent, one model. The benchmark used Claude Code with Sonnet only.

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.

Music in the video: Sascha Ende, ende.app (CC BY 4.0).