spreadsheet-qa-kit MCP server
by 0xJRP
README.md
# spreadsheet-qa-kit
**Ask your spreadsheet a question in plain words. Get the answer - and the exact steps used to compute it.**
Point it at a CSV (or an exported Google Sheet) and ask the kind of thing you'd otherwise build a pivot table for:
> "What was total revenue in Q3 for the West region?"
…and the kit answers it, **shows its work** (which rows it filtered, which column it summed, how), and draws a simple chart when the answer breaks down by category. It reads your data - it does not edit it.
The trick: **Claude never computes the number.** Claude only translates your question into a concrete plan (filter `region = West` and `quarter = Q3`, then sum `revenue`). A small, auditable Node engine runs that plan over your real rows. So the math is plain code you can read, not model output you have to trust.
This is a real, runnable, MIT-licensed kit. No paywall, no neutered features. Run it on your own machine.
---
## Quickstart
```bash
git clone https://github.com/0xJRP/spreadsheet-qa-kit.git
cd spreadsheet-qa-kit
npm install
npm run setup # friendly wizard: Claude API key + path to your CSV
npm run dryrun # answers a sample question from the bundled data and SENDS NOTHING
```
`npm run dryrun` loads the bundled sample spreadsheet, calls Claude to plan a sample question, runs that plan over the data, prints the answer with its work shown, renders a printable HTML report into `out/`, and shows exactly what an emailed report *would* contain - then sends nothing. It's the safest way to see the whole pipeline end to end. (It needs `ANTHROPIC_API_KEY` set, since it actually calls Claude.)
When you're ready to ask your own questions:
```bash
npm run ask -- "What was total revenue in Q3 for the West region?"
npm run ask -- "How many units did each rep sell? Show me a chart."
echo "total revenue by quarter" | xargs -0 -I{} npm run ask -- "{}"
```
Open the rendered report in `out/` and print to PDF (Cmd/Ctrl-P → Save as PDF) to get a PDF.
---
## What it does
```
Question Claude brain Your output
┌──────────┐ ───▶ ┌────────────────┐ ───▶ ┌──────────────────┐
│ "Q3 sales│ │ a QUERY PLAN: │ │ the answer + │
│ in the │ │ filter + group │ │ the steps used │
│ West?" │ │ + aggregate │ │ + a simple chart │
└──────────┘ └────────────────┘ └──────────────────┘
Telegram / CLI / MCP (NOT the answer) engine runs the plan
over YOUR real rows locally, shows its work
```
1. **A question.** Type it on the CLI, or text it to a Telegram bot (`npm start`). Plain English, the way you'd ask a coworker.
2. **A brain.** Claude reads the question and the spreadsheet's schema (column names, which are numeric, a few sample values) and returns a **structured query plan** - which rows to filter, whether to group, what to aggregate. Claude is told the exact column names and may only use those.
3. **A deterministic engine.** `src/sheet.js` validates that plan against the real columns and runs it over your rows in plain Node code: filter, group, sum/average/min/max/count. It builds a step-by-step trace as it goes. **This is where the number comes from - not the model.**
4. **The answer, with its work.** You get the headline answer, the steps ("Filtered 30 rows → 4 where region eq West AND quarter eq Q3. Computed sum of revenue over 4 rows = 18753."), an optional ASCII bar chart for grouped answers, and any assumptions Claude made (e.g. how it read "Q3"). All rendered to a clean HTML report you can print to PDF.
### Your data is yours, and read-only
`data/sample-sales.csv` ships so the dry run works out of the box. Point `DATA_PATH` at your own CSV (or set `GOOGLE_SHEET_CSV_URL` to a published Google Sheets CSV link and pass `--remote`). The kit **only reads** that file. There is no write-back path anywhere in the code: it can answer questions about your data, never change it.
### The spreadsheet as an MCP tool
The sheet is also exposed as an [MCP](https://modelcontextprotocol.io) server (`npm run mcp`, built on the official `@modelcontextprotocol/sdk`). It offers three **read-only** tools - `sheet_schema`, `sheet_sample`, `sheet_query` - so any MCP-aware client (Claude Desktop, an agent) can inspect and query your spreadsheet as a tool. `sheet_query` takes the same structured plan and returns the value plus the computation trace, so even an external agent gets the math from the engine, not from a guess.
---
## The safety model - read-only / draft-first
This kit is **read-only by design**, with a draft-first email path. That's the whole point, and it's Rabbithole's trust differentiator.
- **Your spreadsheet is never edited.** There is no write-back code. The kit loads the file, computes from it, and leaves it byte-for-byte unchanged.
- **The answer is auditable.** Claude proposes the plan; the kit runs it and prints every step. You can check the filter and the arithmetic yourself - the engine is a few hundred lines of plain code.
- **The model can't invent a column or a number.** Every column the plan references is validated against your real header row before anything runs. A bad column name is rejected with a clear error, not silently guessed.
- **Two locks guard the one optional send.** Emailing a report happens only when (1) `DRY_RUN=false` in your `.env` **and** (2) you type `yes` to the confirmation. `DRY_RUN` defaults to `true` on a fresh checkout - a hard kill switch that makes every send path a no-op until you deliberately turn it off.
- **Money is never moved.** There is no payment code of any kind.
| Path | What guards it |
|---|---|
| Answer a question (Claude call + engine) | Local; reads your data, writes nothing to it |
| Render an HTML report | Local file write to `out/` |
| Draw a chart | Local, in-memory text |
| **Email a report** | `DRY_RUN=false` **and** typed `yes` confirmation |
| Edit / write back to the spreadsheet | **Not implemented - by design** |
| Charge a card / take payment | **Not implemented - by design** |
CI / demos stay safe automatically: with `DRY_RUN=true` (the default), `npm run dryrun` runs the whole pipeline and provably sends nothing.
---
## Configuration
Run `npm run setup`, or copy `.env.example` to `.env` and fill it in. Every value can also be supplied as a real environment variable, so `npm run dryrun` works non-interactively in CI:
```bash
ANTHROPIC_API_KEY=sk-ant-... npm run dryrun
```
Key settings (see `.env.example` for all of them, with comments):
| Variable | What it's for |
|---|---|
| `ANTHROPIC_API_KEY` | Your Claude API key (from console.anthropic.com) |
| `ANTHROPIC_MODEL` | Defaults to `claude-opus-4-8`. For high-volume use, `claude-haiku-4-5` is cheaper and faster |
| `DATA_PATH` | Path to your spreadsheet (CSV). Read-only |
| `GOOGLE_SHEET_CSV_URL` | Optional public Google Sheets CSV-export URL (use with `--remote`) |
| `DRAW_CHART` | Draw a simple text chart for grouped answers (`true` default) |
| `SMTP_*` / `REPORT_TO` | Email creds - only needed to actually email a finished report |
| `TELEGRAM_BOT_TOKEN` | Optional phone channel - ask questions by text |
| `DRY_RUN` | Global kill switch. `true` (default) = nothing can send |
### Which model?
The default is `claude-opus-4-8` - the most capable Opus-tier model, great while you're dialing in the kinds of questions you ask. Once it's reliable and you're running high volume, set `ANTHROPIC_MODEL=claude-haiku-4-5` for a cheaper, faster option. Turning a question into a filter/aggregate plan is a clean structured task, so Haiku handles it well.
### Want an actual PDF (not just print-to-PDF)?
The kit renders self-contained HTML so there's no headless-browser binary to install. If you want a real `.pdf` generated automatically, add Puppeteer (`npm i puppeteer`) and render the HTML report to PDF - about ten lines. Print-to-PDF from any browser works today with zero extra setup.
---
## How it's built
- **Runtime:** Node.js, ES modules.
- **Brain:** the official Anthropic SDK (`@anthropic-ai/sdk`) with structured outputs (`messages.parse` + `zodOutputFormat`) so the query plan comes back as a typed object. Model defaults to `claude-opus-4-8`.
- **Engine:** plain Node - filter, group, aggregate, with a step-by-step trace. The model never produces the final number.
- **Tools:** the official MCP SDK (`@modelcontextprotocol/sdk`) exposes the spreadsheet as read-only tools.
- **Email:** `nodemailer` (only used when you turn sending on).
- **Inbound:** Telegram long-polling over `fetch` - no extra dependency.
```
src/
config.js # loads .env (and real env vars) into typed config
sheet.js # load a CSV (or fetched Google Sheet) + run a query plan (the engine)
query-engine.js # the Claude brain - turns a question into a structured query plan
render.js # answer → printable HTML report + ASCII chart + a "show your work" text
mailer.js # the only send path; gated behind DRY_RUN + confirmation
pipeline.js # question → plan → computed answer → rendered report (sends nothing)
cli.js # dryrun / ask commands (confirm-before-send on --email)
setup.js # the first-run wizard (writes .env, points at your data)
mcp-server.js # MCP server exposing the spreadsheet as read-only tools
server.js # Telegram inbound - replies to YOU with the answer + its work
data/
sample-sales.csv # sample spreadsheet the dry run uses (point DATA_PATH at yours)
sample-questions.txt # example questions
```
---
## DIY, or have us run it
You just got a complete, working spreadsheet-question-answering tool for free. Run it yourself - that's the whole idea.
When you're ready for the production version, that's where we come in. **Rabbithole wires this to your real tools and runs it for your team:** your live Google Sheets and databases (not just CSVs), richer charts, scheduled reports, multi-sheet joins, role-based access, and guardrails tuned to how your team actually asks questions of its data.
**You can run this yourself - or we'll build the production version for your whole team. → [rabbithole.consulting](https://rabbithole.consulting)**
---
## License
MIT © 2026 Rabbithole Consulting. See [LICENSE](./LICENSE). Use it, fork it, ship it.
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues