qbo-mcp
# qbo-mcp
Apply CSV-driven changes to QuickBooks Online transactions, plus an MCP server
(`qbo-mcp`) that exposes the same operations to Claude / any MCP client.
Company-agnostic: one install can drive multiple QBO companies via **profiles**
(see [Multiple companies](#multiple-companies-profiles)).
Workflow: export a Transaction Detail report from QBO, edit a copy of the CSV
(or build one from scratch) with the changes you want, then run the applier.
Or skip the CSV and drive edits conversationally via the MCP server.
## Scope
Edit existing posted transactions:
- Account reclassification (line item)
- Class / Location
- Customer / Vendor (entity ref)
- Memo / Description
Out of scope: mapping pending bank-feed "For Review" items.
## One-time setup
### 1. Host the OAuth redirect bouncer (one-time per GitHub account / fork)
Intuit's Production OAuth keys require an HTTPS redirect URI. They reject
`http://localhost...` for production. To avoid making every user run a tunnel
(ngrok etc.), this repo ships a static bouncer page at [`docs/index.html`](docs/index.html)
that you publish on GitHub Pages.
1. Push this repo to your own GitHub account (e.g. `<you>/qbo-mcp`).
2. In the repo on GitHub: **Settings → Pages**.
3. Under **Source**, choose **Deploy from a branch**, select `main` and `/docs`.
4. Save. After ~30 seconds, your bouncer URL is `https://<you>.github.io/qbo-mcp/`.
The page is a stateless redirect — it just reads `?code=...&state=...&realmId=...`
from the URL and forwards them to `http://localhost:<port>/callback` on your
machine. The port is encoded in the OAuth `state` param so multiple machines —
and multiple companies — can share the same bouncer URL.
### 2. Create an Intuit Developer app
You need **one Intuit Developer app per QBO company** (Intuit ties client
credentials to the app, and you authorize each company separately).
1. Sign in at https://developer.intuit.com with your Intuit ID.
2. Dashboard → **Create an app** → choose **QuickBooks Online and Payments**.
3. Name it (e.g. `qbo-mcp-natu`). Scope: `com.intuit.quickbooks.accounting`.
4. Under **Keys & credentials**, switch to the **Production** tab.
5. Add redirect URI: `https://<you>.github.io/qbo-mcp/` (from step 1, with trailing slash).
6. Copy the **Client ID** and **Client Secret** into your `.env` (or `.env.<profile>`).
7. Set `QBO_REDIRECT_URI=https://<you>.github.io/qbo-mcp/`.
> **Sandbox shortcut:** To try the flow against QBO's sandbox first, switch to
> the **Development** tab in step 4, register `http://localhost:8765/callback`
> as the redirect URI, set `QBO_ENV=sandbox`, and skip step 1 entirely.
Note: Production keys for a self-distributed app you only connect to your own
QBO company do not require Intuit's app review.
### 3. Install
```bash
python3 -m venv ~/.venvs/qbo-mcp
source ~/.venvs/qbo-mcp/bin/activate
pip install git+https://github.com/<you>/qbo-mcp.git # or: pip install -e . from a clone
```
This installs four console scripts: `qbo-auth`, `qbo-apply`, `qbo-find`, `qbo-mcp`.
### 4. Configure credentials
Either export them in your shell, or drop a `.env` file in the directory you run
the commands from (`python-dotenv` reads from cwd):
```bash
export QBO_CLIENT_ID=...
export QBO_CLIENT_SECRET=...
export QBO_REDIRECT_URI=https://<you>.github.io/qbo-mcp/
```
### 5. Authorize against your QBO company
```bash
qbo-auth
```
This opens a browser, you log into QBO and pick the company, and tokens are
written to `~/.config/qbo-mcp/<profile>/tokens.json` (override with
`QBO_TOKENS_PATH`). Refresh tokens last 100 days; access tokens auto-refresh.
## Multiple companies (profiles)
One install can serve several QBO companies. Set **`QBO_PROFILE`** to a short
company slug; it namespaces tokens (`~/.config/qbo-mcp/<profile>/tokens.json`)
and selects a per-company env file.
Keep base settings in `.env`, and company-specific credentials in `.env.<profile>`
(these override the base `.env` whenever `QBO_PROFILE` is set):
```
.env # shared, e.g. QBO_REDIRECT_URI, QBO_ENV
.env.natu # QBO_CLIENT_ID / QBO_CLIENT_SECRET for the Natu Intuit app
.env.span # QBO_CLIENT_ID / QBO_CLIENT_SECRET for the Span Intuit app
```
Authorize each company once:
```bash
QBO_PROFILE=natu qbo-auth
QBO_PROFILE=span qbo-auth
```
Then every command targets a company via the same env var:
```bash
QBO_PROFILE=span qbo-find Bill --vendor "AWS" --date 2026-04-15
QBO_PROFILE=span qbo-apply changes.csv --dry-run
```
With `QBO_PROFILE` unset, everything uses the `default` profile — fine for a
single-company setup.
### Read-only access
Set **`QBO_READONLY=1`** (typically in a profile's `.env.<profile>`) to block all
writes — `update_transaction`, `apply_csv`, and `qbo-apply` refuse at the client
layer, while reads/queries keep working. Use it for companies you only want to
inspect. There is no read-only QBO OAuth scope, so this guard (not Intuit) is what
enforces it; unset the var to re-enable writes.
## Applying changes
```bash
qbo-apply path/to/changes.csv --dry-run
qbo-apply path/to/changes.csv
```
CSV columns: `txn_type, txn_id, line_id, field, new_value`
- `txn_type`: QBO entity name — `JournalEntry`, `Bill`, `Invoice`, `Purchase` (= cash/check/CC expense), `Deposit`
- `txn_id`: QBO internal Id (not the displayed Doc Number — see below)
- `line_id`: required for line-level edits (account, class on a line, line memo). Empty for header-level edits (vendor on a Bill, location on most txns, txn-level memo)
- `field`: `account` | `class` | `location` | `customer` | `vendor` | `memo`
- `new_value`: name of the target Account/Class/Department/Customer/Vendor, or memo text
See `changes_example.csv`.
### Finding the QBO txn Id
The displayed reference (e.g. `JE-1042`) is not the API Id. Use the helper:
```bash
qbo-find JournalEntry --doc-number JE-1042
qbo-find Bill --vendor "Amazon Web Services" --date 2026-04-15
qbo-find Invoice --customer "Acme Corp" --date-range 2026-04-01 2026-04-30
qbo-find Purchase --amount 1234.56
```
Or open the transaction in QBO — the URL contains `txnId=<id>`.
### Notes / known limits
- Item-based lines (Invoice line items, item-based Bill lines) get their account from the Item — change the Item, not the line account.
- All edits to one transaction are batched into a single sparse update (all-or-nothing per txn).
- `tokens.json` lives at `~/.config/qbo-mcp/<profile>/tokens.json` (override via `QBO_TOKENS_PATH`); refresh tokens last 100 days.
## MCP server (`qbo-mcp`)
The same operations are exposed as an MCP server so an MCP client (e.g. Claude
Desktop, Claude Code) can drive QBO edits in chat without writing a CSV.
```bash
qbo-mcp # stdio transport
```
For multiple companies, register **one server entry per profile** — each with
its own `QBO_PROFILE` and credentials. The server name reports the profile
(`qbo-mcp (span)`), so Claude can tell them apart.
Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json`):
```json
{
"mcpServers": {
"qbo-natu": {
"command": "/Users/<you>/.venvs/qbo-mcp/bin/qbo-mcp",
"env": {
"QBO_PROFILE": "natu",
"QBO_CLIENT_ID": "...",
"QBO_CLIENT_SECRET": "...",
"QBO_REDIRECT_URI": "https://<you>.github.io/qbo-mcp/"
}
},
"qbo-span": {
"command": "/Users/<you>/.venvs/qbo-mcp/bin/qbo-mcp",
"env": {
"QBO_PROFILE": "span",
"QBO_CLIENT_ID": "...",
"QBO_CLIENT_SECRET": "...",
"QBO_REDIRECT_URI": "https://<you>.github.io/qbo-mcp/"
}
}
}
}
```
Claude Code: `claude mcp add qbo-span --scope user --env QBO_PROFILE=span --env QBO_CLIENT_ID=... --env QBO_CLIENT_SECRET=... -- /Users/<you>/.venvs/qbo-mcp/bin/qbo-mcp`
Tools exposed:
- `find_transactions` — search by doc number, date, customer/vendor, amount
- `get_transaction` — fetch a full entity (use to read line Ids before editing)
- `lookup_ref` — resolve an Account/Class/Department/Customer/Vendor name to its Id
- `query` — read-only QBO SQL passthrough
- `update_transaction` — apply granular field changes to one transaction (defaults to `dry_run=True`)
- `apply_csv` — batch path, same CSV format as `qbo-apply` (defaults to `dry_run=True`)
Both write tools default to `dry_run=True`. To commit, pass `dry_run=False`
explicitly — Claude will surface the change plan first either way.
OAuth still happens via `qbo-auth` (one-time per company); the MCP server reads
`tokens.json` and refreshes access tokens automatically.
TDQS
Scored across 6 tools
Each tool has a clearly distinct purpose: batch updates vs single updates, searching vs full fetch vs SQL query, and name resolution. No two tools overlap in functionality.
Most tools follow a verb_noun pattern (apply_csv, find_transactions, get_transaction, update_transaction), but 'lookup_ref' uses a verb+noun and 'query' is a single noun, breaking the pattern slightly. Overall consistent snake_case.
6 tools is well-scoped for a QuickBooks Online MCP, covering search, retrieval, updates (single and batch), reference resolution, and raw queries without being excessive.
The tool set lacks a create transaction tool, which is a notable gap for typical CRUD operations. While read and update are covered, agents cannot create new transactions, limiting the server's usefulness.