QuickBooks Online MCP
by AshciR
README.md
# QuickBooks Online MCP
A FastMCP server that exposes QuickBooks Online operations (read invoices,
look up customers/items, and create invoices) over streamable HTTP with static
bearer-token auth.
## What's here
- `src/qbo_mcp/config.py` — typed env-var settings.
- `src/qbo_mcp/token_store.py` — Upstash Redis REST persistence for the
OAuth token bundle.
- `src/qbo_mcp/qbo_client.py` — async QBO API client (generic transport) with
auto-refresh, refresh-token rotation, retry-on-401, rate-limit + Fault
handling, and the id/date/escape validators.
- `src/qbo_mcp/service.py` — `QBOService`, the business layer that builds
validated/escaped queries and invoice payloads on top of the client.
- `src/qbo_mcp/server.py` — root FastMCP server (bearer auth + `/health`) that
`mount`s the per-entity tool sub-servers in `src/qbo_mcp/tools/`.
- `scripts/bootstrap_oauth.py` — one-time OAuth flow.
- `scripts/smoke_test.py` — verifies tokens + connectivity.
## Tools
| Tool | Purpose |
| --- | --- |
| `search_customers(name)` | Find active customers by partial display name → `customer_id`. |
| `list_items(name?)` | List active catalog items (Service/NonInventory/Inventory) → `item_id` + unit price. |
| `get_invoices(customer_id, status?, from_date?, to_date?)` | A customer's invoices (status `all`/`open`/`paid`, optional ISO date range), newest first, with a one-line summary each. |
| `get_invoice(doc_number)` | Full detail of one invoice by its human-facing document number, with a deep link. |
| `create_invoice(customer_id, lines[], due_date?, customer_memo?)` | Create an invoice; each line's `unit_price` defaults to the item's catalog price. **Confirm details with the user first — this writes to QuickBooks.** |
Workflow the docstrings teach the LLM: `search_customers` → `list_items` →
confirm lines with the user → `create_invoice`.
## Prerequisites
- Python 3.12
- [uv](https://docs.astral.sh/uv/)
## 1. Create an Intuit developer app
1. Sign in at <https://developer.intuit.com> and create a new app under
"QuickBooks Online and Payments".
2. Enable the **`com.intuit.quickbooks.accounting`** scope.
3. Under **Keys & OAuth**, copy the **Client ID** and **Client Secret**
(use the Development keys for the sandbox).
4. Add this redirect URI: `http://localhost:8000/callback`.
## 2. Create an Upstash Redis database
1. Sign in at <https://console.upstash.com> and create a Redis database.
2. From the database's REST section, copy the **REST URL** and **REST
Token**.
## 3. Configure environment
```bash
cp .env.example .env
# Fill in INTUIT_CLIENT_ID, INTUIT_CLIENT_SECRET, UPSTASH_REDIS_REST_URL,
# UPSTASH_REDIS_REST_TOKEN, MCP_BEARER_TOKEN.
# Leave QBO_REALM_ID blank — the bootstrap step prints it.
```
## 4. Install dependencies
```bash
uv sync
```
## 5. Authorize against QBO
```bash
uv run python scripts/bootstrap_oauth.py
```
A browser tab opens for Intuit consent. When it completes, the script
prints something like:
```
realmId=9341454... — set QBO_REALM_ID=9341454... in .env
```
Paste that value into `.env`.
## 6. Smoke test
```bash
uv run python scripts/smoke_test.py
```
Expected output: `Company: <your sandbox company name>`.
## 7. Run the MCP server
```bash
uv run python -m qbo_mcp.server # serves on 0.0.0.0:$PORT (default 8080)
```
- MCP endpoint: `http://localhost:8080/mcp` (streamable HTTP), authenticated
with `Authorization: Bearer <MCP_BEARER_TOKEN>`.
- Unauthenticated `GET /health` returns `ok` (for health checks).
Connect from Claude Code:
```bash
claude mcp add --transport http qbo http://localhost:8080/mcp \
--header "Authorization: Bearer <MCP_BEARER_TOKEN>"
```
Or inspect it with the [MCP Inspector](https://github.com/modelcontextprotocol/inspector)
(`npx @modelcontextprotocol/inspector`) → Transport "Streamable HTTP" → URL
`http://localhost:8080/mcp` → header `Authorization: Bearer <MCP_BEARER_TOKEN>`.
## Docker / Render deployment
The server ships as a container (self-hosted as a Docker image on Render). The image is a
multi-stage `uv` build on `python:3.12-slim`, runs as a non-root user, and bakes in **no**
config — every secret/setting is supplied at runtime via env vars (the same ones listed in
`.env.example`). It honors `$PORT` (Render injects it; defaults to 8080) and serves the
unauthenticated `GET /health` route for Render's health probe.
Build the image:
```bash
docker build -t quickbooks-online-mcp .
```
Run locally with Docker Compose, mirroring the Render runtime — config comes from a local
env file (never committed):
```bash
docker compose up --build # bearer mode (reads .env)
ENV_FILE=.env.oauth docker compose up --build # oauth mode
PORT=9090 docker compose up --build # custom port, as Render injects $PORT
```
Then `GET /health` and the `/mcp` endpoint are served on `$PORT` exactly as the `uv run`
path above (e.g. `curl localhost:8080/health` → `ok`; connect an MCP client to
`http://localhost:8080/mcp`).
CI builds and publishes the image to GitHub Container Registry at
`ghcr.io/ashcir/quickbooks-online-mcp` (tagged `:latest` plus a `sha-<commit>` tag). On
Render, deploy from that image and enter the runtime env vars (auth mode + QBO/Upstash
credentials) as encrypted dashboard settings — no `.env*` is part of the image.
## Tests
```bash
uv run pytest -q
```
## Project layout
```
src/qbo_mcp/ # config, token_store, qbo_client, service, server
src/qbo_mcp/tools/ # per-entity FastMCP sub-servers mounted by server.py
scripts/ # bootstrap + smoke test
tests/ # pytest suite (mocked httpx)
```
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessResponsive