QBO-Multicompany-MCP
by LatinumLabs
README.md
# QBO Multicompany MCP
A self-hosted remote **MCP server that gives an AI assistant access to many
QuickBooks Online company files through a single connection.**
Most QuickBooks MCP servers are single-tenant: the realm id and tokens come
from environment variables, so one process serves one company file. If you
keep the books for twenty clients, that is twenty processes and twenty
connector entries. This one is built around a **company registry** — you
authorize each QuickBooks company once, they all attach to your connector
identity, and every tool call names the company it should act on.
```
list_companies → Acme Books (9130350000000001), Beta Holdings (…0002)
run_report company="Acme Books" report="ProfitAndLoss" params={start_date: "2026-01-01", …}
query company="Beta" query="SELECT * FROM Invoice WHERE Balance > '0'"
```
You run it yourself, on your own Azure subscription, under your own Intuit
app. Nobody else's infrastructure sits between the assistant and your books.
## Is this for you?
It is a good fit if you keep books for several QuickBooks Online companies
and want an assistant to work across them, and you are comfortable deploying
a container and registering an app with Intuit.
It is **not** a fit for QuickBooks Desktop or Enterprise, which have no cloud
API and need an entirely different (Windows-resident, polling) bridge. And it
is more machinery than you need for a single company file — Intuit's own
[`quickbooks-online-mcp-server`](https://github.com/intuit/quickbooks-online-mcp-server)
is simpler for that case.
## What you will need
| Thing | Why | Cost |
| ------------------------ | -------------------------------------------------------------- | ------------------------------- |
| Intuit developer account | To create the app your company files are authorized through | Free |
| Azure subscription | Container Apps, Key Vault, Table Storage, Log Analytics | A few USD/month at low volume |
| A domain (optional) | A stable hostname, so connections survive infrastructure moves | Whatever your registrar charges |
| An MCP client | Claude, or any client supporting remote MCP with OAuth | — |
**Setup takes about an hour**, most of it waiting on Azure and on Intuit's
review for production credentials.
→ **[Full setup guide](docs/setup.md)** — Intuit app, Azure deployment,
Cloudflare DNS, and connecting your assistant.
## How it works
The connector is an OAuth 2.1 **authorization server** for the assistant and
an OAuth 2.0 **client** of Intuit. The two sides never meet: the assistant
never receives an Intuit token, and Intuit never sees an assistant's client.
```
Assistant ──OAuth 2.1──▶ Connector ──OAuth 2.0──▶ Intuit
──MCP/HTTP───▶ (Azure Container Apps) ──REST───────▶ QuickBooks Online
│
├─ Key Vault (wrapping key, app secrets)
└─ Table Storage (companies, clients, OAuth records)
```
Intuit authorizes **one company file per sign-in**, so each file is added the
same way: run `connect_company`, follow the one-time link, pick the next
company. They accumulate under one connector identity because that identity
is derived from the Intuit OpenID `sub` — the same person signing in again
resolves to the same user.
Intuit refresh tokens are the sensitive value at rest, and are envelope
encrypted: a per-record AES-256-GCM key, itself wrapped by an RSA key that
never leaves Key Vault. Compromising the storage account alone yields
nothing usable.
→ **[Architecture](docs/architecture.md)** for the full design.
## Tools
| Tool | Access | Notes |
| -------------------- | ------ | ----------------------------------------------------------------------------------- |
| `list_companies` | read | Start here; returns the realm ids to pass as `company`. |
| `connect_company` | — | Returns a one-time link for authorizing another file. |
| `get_company_info` | read | CompanyInfo for one file. |
| `query` | read | QuickBooks SQL. `SELECT` only, enforced. |
| `get_entity` | read | Includes the `SyncToken` the write tools need. |
| `run_report` | read | Financial statements and lists. |
| `create_entity` | write | |
| `update_entity` | write | Sparse by default. |
| `void_transaction` | write | Preferred over deleting — keeps the audit trail. |
| `delete_entity` | write | Permanent. |
| `disconnect_company` | — | Revokes the grant at Intuit. Available even read-only, so access can always be cut. |
`company` accepts a realm id **or** a company name (case-insensitive). An
ambiguous name is an error listing the candidates rather than a guess —
silently picking the wrong client's books is the worst failure this connector
could have.
Deploy with `ALLOW_WRITES=false` and the write tools are not registered at
all.
## Security posture
- **Enrollment is gated by an allow-list** of Intuit accounts or email
domains (`ALLOWED_INTUIT_EMAILS`). Leave it empty and _anyone_ who can
reach your endpoint can enroll on your Intuit app's credentials and rate
limits. The deploy script warns; set it.
- The container holds **no credentials of its own** — Key Vault and Storage
are reached through an Azure managed identity, and storage shared-key
access is disabled.
- Authorization codes and refresh tokens are stored **as hashes**, are
single-use, and rotate on every use.
- Logs are structured JSON carrying realm ids and hashed user ids. Tokens,
codes, and sealed values are never logged.
- QuickBooks accounting records are **not** copied into any database here;
requests pass through to Intuit and results are returned.
To report a vulnerability, see [`SECURITY.md`](SECURITY.md).
## Quick start (local)
Requires Node.js 20+ (see `.nvmrc`).
```bash
npm install
npm run build
npm test
cp .env.example .env # fill in at least QBO_CLIENT_ID and QBO_CLIENT_SECRET
npm start
```
Locally the connector uses an in-memory store and a local encryption key —
state is lost on restart, and both are refused once `STORAGE_ACCOUNT_NAME`
and `KEY_VAULT_KEY_URL` are set. Intuit will not redirect to a localhost
callback unless you register it, and hosted assistants cannot reach
localhost, so use a tunnel and point `PUBLIC_BASE_URL` at it for a full round
trip.
Tests cover a full authorization-and-tool-call round trip over real HTTP
against a scripted Intuit — no Azure resources or network access needed.
## Documentation
| Document | What is in it |
| ------------------------------------------------ | ------------------------------------------------------------- |
| [`docs/setup.md`](docs/setup.md) | Intuit app, Azure deploy, Cloudflare DNS, connecting a client |
| [`docs/architecture.md`](docs/architecture.md) | Design: OAuth flows, identity, storage, encryption, limits |
| [`docs/operations.md`](docs/operations.md) | Logs, key rotation, revocation, troubleshooting, teardown |
| [`docs/configuration.md`](docs/configuration.md) | Every environment variable |
## Contributing
Issues and pull requests are welcome — see [`CONTRIBUTING.md`](CONTRIBUTING.md)
and [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md).
## License
[MIT](LICENSE).
This project is not affiliated with, endorsed by, or supported by Intuit
Inc. QuickBooks is a trademark of Intuit Inc. You are responsible for your
own compliance with Intuit's developer terms, and for the accuracy of any
books changed through this connector.
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues