xero-oauth-mcp-server
# xero-oauth-mcp-server
A custom [Model Context Protocol](https://modelcontextprotocol.io) server for
Xero, using the standard **OAuth2 authorization-code + refresh-token** flow
(no paid Custom Connection required). Supports **multiple connected
organisations (tenants)** — every tool takes an optional `org` argument.
## Capabilities
- **Contacts:** `list_contacts`, `get_contact`, `create_contact`, `update_contact`
- **Invoices:** `list_invoices`, `get_invoice`, `get_invoice_pdf`,
`download_invoice_pdfs`, `create_invoice`, `update_invoice`,
`list_overdue_invoices`, `list_recurring_invoices`
- **Utility:** `list_organisations`, `list_accounts`
## How org selection works
Each tool accepts an optional `org`. It is resolved (case-insensitive) against:
1. an alias from `XERO_ORG_ALIASES` (e.g. `main` → a tenantId),
2. an exact `tenantId`, then
3. a substring of the org's name in Xero.
If `org` is omitted, the server uses `XERO_DEFAULT_ORG` (which may itself be an
alias), or the first connected org when that is unset.
## One-time setup
### 1. Create a Xero app
[developer.xero.com](https://developer.xero.com/app/manage) → **New app → Web app**.
- Add redirect URI: `http://localhost:49831/callback`
- Copy the **Client ID** and generate a **Client Secret**.
> Newer Xero apps use **granular scopes**. This server requests
> `accounting.contacts`, `accounting.invoices`, and `accounting.settings.read`
> (plus `openid profile email offline_access`). Make sure those are enabled on
> the app.
### 2. Build
```bash
npm install
npm run build
```
### 3. Authorize (interactive, once)
```bash
XERO_CLIENT_ID=xxx XERO_CLIENT_SECRET=yyy npm run auth
```
A browser opens — log in and authorize each organisation you want to use
(re-run to add more later). The refresh token and the connected tenant list are
saved to `~/.xero-mcp-id/tokens.json` (chmod 600). The refresh token rotates
automatically on every use.
### 4. Register with an MCP client
Example (Claude Code):
```bash
claude mcp add xero -s user \
-e XERO_CLIENT_ID=xxx \
-e XERO_CLIENT_SECRET=yyy \
-e XERO_DEFAULT_ORG=main \
-e XERO_ORG_ALIASES='{"main":"<tenant-guid>","secondary":"<tenant-guid>"}' \
-- node /absolute/path/to/dist/index.js
```
## Environment variables
| Var | Default | Purpose |
|-----|---------|---------|
| `XERO_CLIENT_ID` / `XERO_CLIENT_SECRET` | — | App credentials (required) |
| `XERO_DEFAULT_ORG` | first org | Org used when a tool omits `org` (alias/tenantId/name) |
| `XERO_ORG_ALIASES` | `{}` | JSON map of friendly alias → tenantId |
| `XERO_REDIRECT_PORT` | `49831` | Local callback port (must match the app's redirect URI) |
| `XERO_PDF_DIR` | `~/Downloads` | Where the PDF tools save files when no path is given |
| `XERO_READONLY` | unset | When `true`, all write tools refuse |
| `XERO_TOKEN_DIR` | `~/.xero-mcp-id` | Where the token store lives |
## Security
- No credentials or tokens are stored in this repository.
- Client ID/secret are passed via environment; the refresh token + tenant list
live only in `XERO_TOKEN_DIR` (default `~/.xero-mcp-id/`, chmod 600), which is
git-ignored.
- Write tools (`create_*` / `update_*`) can be globally disabled with
`XERO_READONLY=true`.
## License
MIT
TDQS
Scored across 12 tools
Each tool targets a distinct resource and action (contact, invoice, accounts, etc.). Even specialized invoice tools like list_overdue_invoices and list_recurring_invoices have clear names that prevent ambiguity.
All tools follow a consistent verb_noun pattern (create_*, get_*, list_*, update_*), making it predictable and easy for an agent to infer purpose from the name.
With 12 tools covering core CRUD for contacts and invoices plus useful listing filters, the set is well-scoped for a Xero accounting API server—neither too sparse nor too overwhelming.
Covers the main workflows for contacts and invoices (create, read, update, list) with additional filters. Missing delete operations and account creation/update, but these are often handled externally or are less critical for basic usage.