Skip to main content
Glama
CharlesPiccioneBTP

QuickBooks MCP Server

README.md
# QuickBooks MCP server — local, read-only

Ask Claude Desktop questions about a QuickBooks Online company in plain English
and get answers from the live books.

**RESPONSES WILL BE DIFFERENT THAN THOSE WRITTEN HERE: EXAMPLE FIGURES ARE DERIVED FROM YOUR OWN SANDBOX**

> **You:** Who owes us money and how overdue is it?
>
> **Claude:** Sandbox Company US 4768 has **$5,281.52** outstanding across 20 open
> invoices. Most of it isn't late yet — $3,756.02 is still current. Of the $1,525.50
> that *is* overdue, the worst is Red Rock Diner at **$156.00, 65 days past due**
> (invoice 1024, due 3 June). Kookies by Kathy ($75.00) and Bill's Windsurf Shop
> ($85.00) are both around 45 days out.

Everything runs on your own machine. Nothing is hosted or deployed, and no
company data leaves your computer except to Claude, in the answer.

> **The figures above are from one particular sandbox.** You'll get your own
> sandbox company when you sign up, with its own company number, its own
> customers and its own dates — so your totals and overdue days will not match
> these. That's expected, not a fault. Every example in this README is
> illustrative for the same reason.

---

## What you can ask

| Question | Tool used |
|---|---|
| Who owes us money? How overdue? Who should we chase? | `get_receivables_aging` |
| What bills do we owe? What's due soon? | `get_payables_aging` |
| How profitable were we this year? What did we spend on X? | `get_profit_and_loss` |
| What do we own and owe? How much cash? | `get_balance_sheet` |
| What's Amy's email? Who are our customers? | `find_contacts` |
| Which company am I connected to? | `get_company_info` |
| Anything else in the books | `run_query` + `describe_schema` |

The aging tools compute days overdue and bucket everything (current, 1–30,
31–60, 61–90, 90+) rather than making Claude do date arithmetic on raw invoices.

---

## What "read-only" means here

**The server cannot write to QuickBooks.** Not "writes are switched off" — there
is no code that could write:

- `client.py` is the only module that can reach the QuickBooks API. It exposes a
  single request method, the HTTP verb is a hardcoded literal `"GET"`, and every
  path is checked against an allowlist of exactly two prefixes (`/query` and
  `/reports/`). QuickBooks writes are `POST`s to `/v3/company/{realm}/{entity}`,
  which is neither.
- `tests/test_readonly.py` parses the source and **fails the build** if a non-GET
  verb appears in that module, if any other module grows its own HTTP client, or
  if the path allowlist changes.

**The honest caveat:** Intuit publishes no read-only scope for accounting data.
The OAuth token this server holds *is* capable of writing — the restriction lives
in this code, not in the token. If you don't trust the code, don't trust the
claim; read `client.py` (about 200 lines) and the test that guards it.

One `POST` does exist, in `auth.py`: the OAuth token exchange. It is pinned to
Intuit's token endpoint, which is a different host from the accounting API, and
the test asserts that too.

**Want to see for yourself?** Every request is logged. Open the MCP log
(`%APPDATA%\Claude\logs\mcp-server-quickbooks.log`) and you'll see only `GET`
lines.

---

## Install

Takes about 10 minutes, most of it on Intuit's website.

> If someone has handed you a Client ID, Client Secret, Realm ID and refresh
> token from an existing connection, skip steps 3 and 4 and run
> `uv run qbo-mcp-import` instead. Read
> [When someone genuinely cannot make their own account](#when-someone-genuinely-cannot-make-their-own-account)
> first — it works, but you give up the ability to fix your own installation.

### 1. Install `uv`

`uv` runs the server and handles Python and dependencies for you.

```powershell
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
```

Close and reopen your terminal afterwards. Check it worked:

```bash
uv --version
```

*(macOS or Linux: `curl -LsSf https://astral.sh/uv/install.sh | sh`)*

### 2. Get this code

```bash
git clone https://github.com/CharlesPiccioneBTP/Local-MCP-server-for-QuickBooks-Sandbox.git
```

Remember where you put it — you'll need the full path in step 5.

### 3. Create your own Intuit app

Free, no card required. **Do this yourself even if a colleague has already set
theirs up** — sandbox companies belong to the developer account that created
them, so you can't connect to someone else's. You'll get your own, with its own
sample data.

1. Sign up at **[developer.intuit.com](https://developer.intuit.com)**. A **sandbox
   company** with realistic sample data is created for you automatically.
2. **My Hub → Workspaces → +** → *Get Started* → fill in the basic details.
3. **+** to add an app → choose **QuickBooks Online (Accounting)** → name it
   anything (e.g. "Claude read-only").
4. Go to **Settings → Redirect URIs → Development** tab → **Add URI** and paste
   this **exactly**:

   ```
   http://localhost:8000/callback
   ```

   > This is the step people get wrong. It must be `http` (not `https`), with no
   > trailing slash. If it doesn't match character-for-character, Intuit refuses
   > the sign-in and setup fails.

5. Open **Keys & credentials** and keep the tab open. You need the **Client ID**
   and **Client Secret** from the **Development** section. (Sandbox and
   production have different keys — use Development.)

### 4. Connect your company

In a terminal, from the folder you cloned into:

```bash
uv run qbo-mcp-setup
```

It asks for the environment (press Enter for `sandbox`), then your Client ID and
Client Secret. Your browser opens; sign in and choose the sandbox company. The
secret isn't shown as you type and isn't saved to your shell history.

You should see:

```
Saved credentials to C:\Users\you\AppData\Local\qbo-mcp\credentials.json
Verifying the connection...
Success - connected to "Sandbox Company US 4768".
```

### 5. Tell Claude Desktop about it

Open Claude Desktop → **Settings → Developer → Edit Config**. That opens
`claude_desktop_config.json`. Add the `quickbooks` block inside `mcpServers`:

```json
{
  "mcpServers": {
    "quickbooks": {
      "command": "uv",
      "args": [
        "--directory",
        "C:\\Users\\you\\path\\to\\Local-MCP-server-for-QuickBooks-Sandbox",
        "run",
        "qbo-mcp"
      ]
    }
  }
}
```

Two things to get right:

- Replace the path with **your** folder from step 2. On Windows use **double**
  backslashes (`\\`) as shown.
- If Claude Desktop reports it can't find `uv`, replace `"uv"` with its full
  path. Find it by running `where uv` (Windows) or `which uv` (Mac/Linux) — it's
  usually `C:\Users\you\.local\bin\uv.exe`.

**No credentials go in this file** — that's deliberate. This file gets shared and
screenshotted when people ask for help. Your secrets live in the separate
credentials file from step 4.

### 6. Restart Claude Desktop

Fully quit and reopen it — closing the window isn't enough. Then ask:

> Who owes us money and how overdue is it?

---

## Checking it works

```bash
uv run qbo-mcp-doctor
```

This runs without Claude and checks the whole chain. What matters is that every
line says `ok` — the company name and figures will be your sandbox's, not these:

```
[ ok ] Credentials loaded from C:\Users\you\AppData\Local\qbo-mcp\credentials.json
       environment=sandbox  realm=93414576XXXXXXXX
[ ok ] Token refresh succeeded
[ ok ] Refresh token unchanged this time (rotation is periodic); file is current
[ ok ] Refresh token expires in 101 days
[ ok ] Connected to "Sandbox Company US 4768"
[ ok ] Receivables aging: 20 open invoice(s), total outstanding 5281.52
         current      3,756.02
         1-30         1,128.50
         31-60          241.00
         61-90          156.00
         90+              0.00
```

Run this first whenever something seems wrong — it separates "the server is
broken" from "Claude Desktop isn't talking to it".

---

## Troubleshooting

**"No credentials found"** — Setup hasn't been run, or it ran as a different
Windows user. Run `uv run qbo-mcp-setup`.

**The QuickBooks tools don't appear in Claude Desktop** — Almost always the
config file. Check the path uses double backslashes, then confirm the file is
valid JSON (a stray comma breaks it silently). Then fully quit and reopen the
app. If it still fails, check
`%APPDATA%\Claude\logs\mcp-server-quickbooks.log`.

**"Port 8000 is already in use"** during setup — Something else is using that
port. Stop it and retry. The port must match the redirect URI registered with
Intuit, so changing it means changing both.

**Setup opens the browser but fails after sign-in** — The redirect URI doesn't
match. It must be exactly `http://localhost:8000/callback` in **Settings →
Redirect URIs → Development**.

**"Intuit rejected the client credentials (401)"** — Wrong keys, or production
keys used for a sandbox company. Copy them again from the **Development**
section and re-run setup.

**It worked for weeks, now it says "invalid_grant"** — The connection needs
re-authorising. This happens if the token expires (see below), if it was revoked
in QuickBooks, or if two copies of the server fought over a refresh. Fix:
`uv run qbo-mcp-setup`.

---

## Keeping it working

Access tokens last 1 hour and the server refreshes them automatically. The
refresh token rotates roughly daily, and **Intuit invalidates the old one
immediately**, so the new value is written to disk atomically, under a
cross-process lock, before it's used. If two copies of the server start at once,
the second adopts the first's tokens instead of refreshing again — which would
otherwise disconnect both.

The refresh token itself is currently good for **~101 days from setup**, and
that window resets every time it's used. So the server keeps working
indefinitely as long as it's used occasionally. If it goes unused for that long,
re-run `uv run qbo-mcp-setup`. The doctor warns you when fewer than 30 days
remain.

---

## Where things live

| | |
|---|---|
| Credentials | `%LOCALAPPDATA%\qbo-mcp\credentials.json` (Windows)<br>`~/.config/qbo-mcp/credentials.json` (Linux), `~/Library/Application Support/qbo-mcp/` (Mac) |
| Claude Desktop config | `%APPDATA%\Claude\claude_desktop_config.json` |
| Server logs | `%APPDATA%\Claude\logs\mcp-server-quickbooks.log` |

Credentials are stored **outside this repository** so they cannot be committed by
accident. That's the actual protection — `.gitignore` is a backstop. Setup will
refuse outright to write them anywhere inside a git repository. The file is
locked to your user account (`icacls` on Windows, mode `600` elsewhere).

---

## Rolling it out to colleagues

Send them this repository and point them at the install steps above. There is
nothing to hand over besides the link — no keys, no config, no credentials file.

Each person follows the whole guide, including creating their own Intuit
developer account. They get their own sandbox company, their own app, and their
own Client ID and Secret, so **there is nothing to share and nothing to keep
secret between you.** That is the intended setup, not a workaround.

**Never copy a `credentials.json` between machines**, even though it might look
like a shortcut. Two people sharing one credentials file will fight over the
refresh token — Intuit invalidates the old value each time it rotates, so
whoever refreshes second gets logged out, and eventually both do. Each person
needs their own OAuth grant.

### When someone genuinely cannot make their own account

There is a second entry point for the case where a colleague must be got working
without an Intuit account of their own — evaluating the tool before committing to
accounts, or a non-technical user who should not be walked through a developer
signup:

```bash
uv run qbo-mcp-import
```

It asks for the Client ID, Client Secret, Realm ID and **refresh token** of an
existing connection and writes the same credentials file `qbo-mcp-setup` writes.
No code path differs after that: the file format, the refresh logic and the
read-only guarantee are identical. The only thing skipped is the OAuth flow that
would have produced those four values.

Know what you are trading away before using it:

- **A refresh token works on one machine at a time.** This is the same rotation
  problem as copying `credentials.json`, because it is the same token. Whoever
  supplies it must stop using it. If both machines refresh, the second one is
  disconnected permanently.
- **The recipient cannot repair their own installation.** Every other failure in
  this README is fixed by re-running setup. They can't — that needs an account.
  Every outage becomes a request to whoever issued the token, and outages are
  guaranteed eventually: the refresh token expires after ~100 days of disuse.
- **Access tokens are not refresh tokens.** The access token is the value people
  reach for, because it is the one that visibly works in `curl`. It lasts an hour
  and cannot be renewed. `importer.py` detects a pasted JWT or authorization code
  and says so, rather than failing opaquely three steps later.

For anyone who will use this for more than a trial, their own account remains the
right answer. It takes about five minutes and removes every bullet above.

**Tested end to end on Windows 11 only.** The macOS and Linux credential paths
are covered by tests, so they resolve to the right place, but nobody has run the
real OAuth flow on those platforms yet. If a colleague is the first Mac user,
expect to iron something out — most likely the browser handoff during setup, or
file permissions on the credentials directory
(`~/Library/Application Support/qbo-mcp/`). Please report back if so.

---

## Using it with a real company

This is built and tested against a **sandbox**. The code will work unchanged
against live books — answer `production` at the setup prompt and use Production
keys — but there is a real gate in front of you:

**Intuit requires an app assessment before issuing production keys.** Every app
that touches production data must complete a legal, technical and security
questionnaire and be approved by Intuit's security team. This applies *whether
or not* the app is listed in the QuickBooks app store, and **whether or not it
is only ever used internally on your own company's books**. Budget about an hour
for the questionnaire, plus review time, and expect to supply your host domain,
launch URL, disconnect URL, and the countries and IP addresses the app runs
from — questions that assume a hosted web app and take some thought to answer
for something that runs on a laptop.

Two other things worth deciding before you point this at real books:

- **Everything the tools return is sent to Claude** in order to answer your
  question. Real customer names, balances and financial statements leave your
  machine. Whether that's acceptable is a policy call, not a technical one.
- **The read-only guarantee is unchanged** — same code, same test — but the
  stakes of the honest caveat above go up. On real books, "the token could write,
  the code cannot" is worth someone else reading `client.py` to confirm.

---

## For developers

```bash
uv run --group dev pytest       # 101 tests, no network or credentials needed
```

| Module | Role |
|---|---|
| `client.py` | The only route to the QuickBooks API. GET-only, path-allowlisted. |
| `auth.py` | OAuth setup and refresh. Holds the package's only POST. |
| `importer.py` | Adopts a connection someone else authorised. No network of its own. |
| `config.py` | Credential storage: atomic writes, cross-process lock, git refusal. |
| `tools.py` | QuickBooks operations, callable without an MCP session. |
| `server.py` | MCP tool definitions and descriptions. |
| `qbo_sql.py` | `run_query` validation and result caps. |
| `formatting.py` | Aging arithmetic, report flattening. |
| `schema.py` | Static entity reference for `describe_schema`. |

Notes on design decisions worth knowing before changing things:

- **Tools raise plain exceptions, never `MCPError`.** In this SDK a plain
  exception becomes a tool error whose message the model reads and can act on;
  `MCPError` becomes a protocol error the model never sees. Every failure here is
  one the user needs told about.
- **AR aging is computed from `Invoice` rows, not Intuit's aging report.** Invoice
  rows already embed `CustomerRef.name`, so no join is needed, and it keeps the
  bucket boundaries under our control. Their Reports API is used for the
  financial statements, where the aggregation genuinely has to come from
  QuickBooks.
- **`describe_schema` is a tool, not an MCP resource**, even though resources fit
  static reference data better. Claude Desktop requires resources to be attached
  by hand, so it would never be read — and `run_query` without it is guesswork.
- **The QuickBooks query language is not SQL.** No `JOIN`, `GROUP BY`, `OR`,
  `HAVING`, or `!=`. `qbo_sql.py` catches these and explains the workaround
  rather than passing them through to an opaque Intuit 400.
- **`minorversion` is pinned** (currently 75). Intuit changes response shapes
  between minor versions; an unattended server shouldn't have its output shift
  underneath it.

---

## License

MIT — see [LICENSE](LICENSE).

TDQS

A4.4/5.0

Scored across 8 tools

Disambiguation5/5

Each tool targets a distinct domain area: receivables, payables, profit/loss, balance sheet, company info, contacts, querying, and schema discovery. Even the two aging tools are clear mirrors for AR vs AP. No overlap or ambiguity.

Naming Consistency4/5

Most tools follow a consistent get_<noun> pattern, with find_contacts as a slight deviation and run_query/describe_schema as verb_noun. All are lowercase snake_case, so the pattern is predictable and readable.

Tool Count5/5

8 tools is well-scoped for a read-only QuickBooks reporting server. Each tool has a distinct purpose, and the count is neither too thin nor overwhelming.

Completeness5/5

The server covers core financial statements, aging reports, contact lookup, company info, and a flexible query tool with schema discovery. This provides comprehensive coverage for typical accounting questions, with run_query bridging any remaining gaps.

Maintenance

ActivitySlowing
ResponsivenessNo issues