Skip to main content
Glama
Andrewraof

odoo-bookkeeping-mcp

by Andrewraof
README.md
# odoo-bookkeeping-mcp

An MCP (Model Context Protocol) server that gives an LLM agent -- Claude,
ChatGPT, or any other MCP-compatible client -- full read/write access to one
Odoo instance's accounting (invoices, vendor bills, journal entries,
payments, reconciliation, P&L/balance sheet/aged reports) and, if the
instance has the `project_budget_boq_sunderland` addon installed, its
BOQ/Project Budget data.

It is **not** an Odoo addon. It talks to Odoo the same way any external
integration does: Odoo's standard `/xmlrpc/2/*` API, authenticated as a
normal Odoo user via an API key. No code needs to be installed on the Odoo
server.

## ⚠️ Read this before pointing it at a real database

This server was built with **no restrictions by explicit request**: every
tool that can create, edit, post, or reconcile a real accounting record
will do so immediately when called, with no draft/review/approval gate and
no human-in-the-loop step. That is a deliberate choice, not an oversight --
but it means:

- **A wrong or hallucinated tool call becomes a real entry in your books.**
  A posted journal entry cannot be edited or un-posted -- correcting it
  needs a reversal or credit note (this server can do that too, but the
  original mistake still shows up in every report until it's reversed).
- **Whoever/whatever can reach this server can act as its Odoo user.**
  Give it a **dedicated Odoo user** (not an admin's personal login) scoped
  to exactly the access rights, companies, and record rules you want an
  agent to have -- Odoo's own security model is still the only thing
  standing between "the agent read this wrong" and "the books are wrong."
- **Deletion (`odoo_unlink`) is the one thing still off by default** --
  see `ODOO_MCP_ALLOW_UNLINK` below. Everything else (create/write/post/
  reconcile) is on by default per the request that led to this server.
- Treat the `.env` file / API key like a production database password,
  because functionally it is one.

## What's inside

```
src/odoo_mcp/
  client.py     Odoo XML-RPC wrapper (auth, generic CRUD, error mapping)
  config.py     Reads connection + safety-valve settings from the environment
  server.py     MCP server entrypoint; registers every tool module below;
               dispatches to stdio or HTTP transport
  http_auth.py  Bearer-token ASGI middleware, used only in HTTP transport
  tools/
    generic.py     odoo_search_read / odoo_create / odoo_write / odoo_unlink /
                   odoo_call_method / odoo_fields_get -- works against ANY
                   model, the "no restrictions" escape hatch every other
                   tool module is really just a friendlier wrapper over.
    accounting.py   Customer invoices, vendor bills, journal entries,
                   posting, reversal, payment registration, reconciliation.
    reports.py      P&L, balance sheet, trial balance, general ledger,
                   aged receivable/payable -- computed from posted
                   account.move.line data (not Odoo's PDF report engine).
    boq.py          project.budget / project.budget.line: list/get budgets,
                   create/update BOQ lines, submit/approve/activate
                   workflow, budget-vs-actual. Only useful if your Odoo
                   instance has project_budget_boq_sunderland installed;
                   otherwise fall back to the generic tools for whatever
                   budgeting model it does have.
tests/           Unit tests against a mocked XML-RPC server (no live Odoo
                 instance is available while building this) -- verify
                 request shaping, auth caching, and error handling. They do
                 NOT prove your specific Odoo instance/version behaves
                 identically; test against a real (ideally staging) database
                 before trusting this in production.
```

## Setup

```bash
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
cp .env.example .env
# edit .env with your real Odoo URL / DB / username / API key
python -m pytest tests/ -v
```

### Getting an Odoo API key

In Odoo: click your avatar → **My Profile** → **Account Security** → **New
API Key**. Do this for the **dedicated integration user** you created for
this server, not your own login.

### Environment variables

See `.env.example` for the full list with comments. Required:
`ODOO_URL`, `ODOO_DB`, `ODOO_USERNAME`, `ODOO_API_KEY`.

Safety valves (optional, all env-var controlled so you never need to edit
code to change them):

| Variable | Default | Gates |
|---|---|---|
| `ODOO_MCP_ALLOW_UNLINK` | `false` | `odoo_unlink` (permanent record deletion) |
| `ODOO_MCP_ALLOW_POST` | `true` | posting invoices/journal entries, registering payments |
| `ODOO_MCP_ALLOW_RECONCILE` | `true` | `reconcile_lines` |

Transport (optional):

| Variable | Default | Notes |
|---|---|---|
| `ODOO_MCP_TRANSPORT` | `stdio` | `stdio` or `http` |
| `ODOO_MCP_BEARER_TOKEN` | *(none)* | **required** when `ODOO_MCP_TRANSPORT=http` -- server refuses to start over HTTP without it |
| `ODOO_MCP_HOST` | `127.0.0.1` | HTTP bind address |
| `ODOO_MCP_PORT` | `8000` | HTTP bind port |

## Connecting it to Claude

**Claude Desktop / Claude Code** -- add to your MCP config
(`claude_desktop_config.json` or the CLI's `.mcp.json`):

```json
{
  "mcpServers": {
    "odoo-bookkeeping": {
      "command": "odoo-mcp",
      "env": {
        "ODOO_URL": "https://your-instance.odoo.com",
        "ODOO_DB": "your-db",
        "ODOO_USERNAME": "mcp-integration@yourcompany.com",
        "ODOO_API_KEY": "your-api-key"
      }
    }
  }
}
```

(Run `pip install -e .` first so the `odoo-mcp` command exists on PATH, or
use the full path to `python -m odoo_mcp.server` instead of `odoo-mcp`.)

## Running it on a server (HTTP transport, for ChatGPT or remote access)

ChatGPT's MCP/connector support (and any remote Claude session) needs a
server reachable over HTTP, not a local stdio process. Set the transport
and a bearer token, then run it:

```bash
export ODOO_MCP_TRANSPORT=http
export ODOO_MCP_BEARER_TOKEN=$(python3 -c "import secrets; print(secrets.token_urlsafe(32))")
echo "Save this token -- callers must send it as: Authorization: Bearer $ODOO_MCP_BEARER_TOKEN"
odoo-mcp
# or: python -m odoo_mcp.server
```

This serves `POST /mcp` (streamable-HTTP) on `ODOO_MCP_HOST:ODOO_MCP_PORT`
(default `127.0.0.1:8000`), gated by a self-contained ASGI middleware
(`http_auth.py`) that rejects any request without an exact
`Authorization: Bearer <token>` match -- **the server refuses to start
over HTTP at all if `ODOO_MCP_BEARER_TOKEN` is unset**, since this server
has no other access control of its own.

Before pointing a real client at it:

- **Keep the bind address at `127.0.0.1`** and put a reverse proxy
  (nginx, Caddy, Cloudflare Tunnel) in front for TLS + a real domain --
  this process speaks plain HTTP and does not terminate TLS itself.
- The bearer token is the *entire* network-facing security boundary here
  (Odoo's own access rights are the boundary behind it). Rotate it if it
  ever leaks, and don't put it in a URL, query string, or client-side
  code -- header only.
- Configure your MCP client (ChatGPT connector settings, or a remote
  Claude MCP config) to call `https://your-domain/mcp` with that bearer
  token as its auth header.

## What is NOT here

- No Odoo module/addon changes -- this only uses Odoo's existing external
  API, so it works against any reasonably recent Odoo version without
  touching the target instance at all.
- No real Odoo test run -- this development environment has no reachable
  Odoo/Postgres instance. Everything above `tests/` is verified with a
  mocked XML-RPC backend (request shaping, auth, error handling) and a
  live tool-registration smoke test against the actual MCP server object;
  none of it has been exercised against a real database. Test against a
  staging Odoo database before pointing this at production.
- No UI, no scheduling, no webhook listener -- it only responds to tool
  calls a connected MCP client makes.

TDQS

B3.2/5.0

Scored across 34 tools

Disambiguation3/5

The generic odoo_* tools (odoo_create, odoo_search_read, odoo_write) overlap with domain-specific ones like create_invoice, search_invoices, and update_invoice, though descriptions clearly prefer the dedicated tools for accounting. Search tools are distinct per entity, but the broad generic tools introduce ambiguity about which to use.

Naming Consistency3/5

Naming is a mix of verb_noun patterns (create_invoice, post_invoice), a consistent odoo_ prefix group (odoo_create, odoo_write), and noun-phrase report names (balance_sheet, profit_and_loss). Subgroups are internally consistent, but the overall convention is inconsistent.

Tool Count2/5

With 34 tools, this exceeds the typical well-scoped range of 3-15. The six generic odoo_* tools inflate the count, and the budget/invoice/report tools could be streamlined. It feels heavy for a bookkeeping server.

Completeness4/5

The toolset covers the full bookkeeping lifecycle: invoices (create/post/search/get/update), journal entries (create/update/post/reverse), payments and reconciliation, financial reports (P&L, balance sheet, trial balance, GL, aged), and budgets (list/get/create/update/approve/activate/vs actual). Minor gaps exist, such as a dedicated tax report, but generic odoo_call_method fills most gaps.

Maintenance

ActivitySlowing
ResponsivenessNo issues