
MCP Server for Odoo | Claude & ChatGPT Connector
An MCP server for Odoo so Claude or ChatGPT can read only what you allow: per-token model and field allowlists, call caps and a kill switch.
Available for Odoo 16.0, Odoo 17.0, Odoo 18.0, Odoo 19.0. Technical name bambooforge_ai_bridge.
MCP Server for Odoo | Claude & ChatGPT Connector
An MCP (Model Context Protocol) server built into Odoo. It lets Claude Desktop, Claude Code, or any other MCP client answer questions about your Odoo data — and nothing else. A bridge token grants nothing until you list the models it may read, the fields it may see, the rows it may take and the calls it may make per day. Every question is logged with its cost.
This page is the complete manual. Follow it top to bottom and you can install the app, issue a token, connect an AI client, understand the guard rails and diagnose the usual problems without contacting support.
Why this exists
The usual way to give an AI assistant access to Odoo is to hand it a user account — an API key, XML-RPC, or an "AI connector" that logs in as somebody. From that moment the assistant inherits that user's rights: one badly-phrased prompt, one injected instruction inside a customer email it was asked to summarise, and it can read payroll, export the customer list, or write.
The AI Bridge inverts the default:
a token starts with zero models allowed;
the protocol surface has no write tool at all — not "write is disabled", there is no create/write/unlink/call-method entry point to disable;
every field you did not list is invisible, and password/secret/token-like fields are stripped even if you do list them;
every call is capped (rows per call, calls per day) and logged with an estimate of what the answer cost in model tokens;
any log line has a Revoke this token button.
Install
Copy bambooforge_ai_bridge into your addons path and update the app list, or install the ZIP from the Odoo Apps store.
Install the app BambooForge AI Bridge (MCP).
Give yourself the group AI Bridge / Manager (Settings → Users → your user → Other → AI Bridge Manager). Without it the menu stays hidden.
The menu appears under AI Bridge with two entries: Tokens and Query Log.
There is no external service, no outbound call and no API key of ours: the endpoint lives inside your own Odoo.
Issue a token
AI Bridge → Tokens → New
Field |
Meaning |
|---|---|
Name |
What the token is for, e.g. Claude Desktop — sales team. It shows on every log line. |
Runs as user |
The Odoo user whose record rules and access rights apply to every query. Use a dedicated, low-privilege user — the allowlist is a second fence, not a replacement for ACLs. |
Expires on |
Optional. After this date the token stops authenticating. |
Max rows per call |
Hard ceiling on rows returned by one tool call (default 200). A per-model limit can lower it. |
Max calls per day |
Rolling 24-hour cap (default 500). 0 means unlimited — not recommended. |
Add at least one line under Allowed models, then press Generate secret. The secret is shown once, in a banner at the top of the form, and looks like:
bfai_9f2c1e7ad4b3...
Only a SHA-256 digest is stored; nobody — including you — can read the secret back afterwards. Lost it? Press Generate secret again: the previous secret stops working immediately.
Allowed models: the keyhole
Each line under Allowed models answers four questions:
- Model
The only models the token can name. Anything else is refused before a query is built, and the refusal is logged.
- Fields
A comma-separated allowlist, e.g. name,email,city,country_id. Leave it empty to expose every stored, non-binary field of the model. Regardless of what you type, these are always stripped: password, password_crypt, api_key, totp_secret, access_token and any field whose name contains password, token or secret; binary fields are excluded too (an agent has no use for a 4 MB PDF and you would pay for it twice).
- Extra filter (domain)
An Odoo domain the agent can never widen, e.g. [('company_id','=',1)] or [('state','=','sale')]. It is AND-ed with whatever domain the agent sends. This is how you expose confirmed orders of one company rather than the sale.order table.
- Row limit
Optional per-model ceiling, lower than the token's own.
- Allow aggregate
Whether odoo_aggregate (grouped count/sum) may run on this model. Aggregates are cheap and leak little, so this is on by default.
Connect an MCP client
The endpoint is:
POST https://<your-odoo>/bf_ai/mcp Authorization: Bearer bfai_<your secret> Content-Type: application/json
It speaks JSON-RPC 2.0 and implements the MCP methods initialize, tools/list, tools/call and ping. There is no session cookie and no CSRF token involved: the bearer token is the whole authentication.
Check it with curl before touching any client:
curl -s https://your-odoo.example.com/bf_ai/mcp \
-H "Authorization: Bearer bfai_xxx" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize"}'
A healthy answer names the server and lists exactly the models you allowed:
{"jsonrpc": "2.0", "id": 1, "result": {
"protocolVersion": "2024-11-05",
"serverInfo": {"name": "BambooForge Odoo Bridge", "version": "1.0.0"},
"capabilities": {"tools": {"listChanged": false}},
"instructions": "This Odoo bridge is read-only. Allowed models:
product.template, res.partner, sale.order"}}
A tools/call answers in MCP shape — human-readable content plus machine-readable structuredContent:
{"jsonrpc": "2.0", "id": 4, "result": {"isError": false,
"content": [{"type": "text", "text": "..."}],
"structuredContent": {"model": "sale.order",
"groups": [{"count": 73, "state": "sale", "amount_total": 116313.91}]}}}
Note the group list: the rule on sale.order carried the extra filter [('state','in',('sale','done'))], so draft quotations never enter the answer — the agent cannot see they exist, whatever it asks.
Claude Desktop / Claude Code. Clients that speak HTTP MCP take the URL and header directly. For a client that only speaks stdio, put a standard remote bridge in front — no code of ours is involved:
{
"mcpServers": {
"odoo": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://your-odoo.example.com/bf_ai/mcp",
"--header", "Authorization: Bearer bfai_xxx"]
}
}
}
Use HTTPS. A bearer token on plain HTTP is a bearer token in everyone's logs.
The five tools
- odoo_list_models
What may I read? Returns each allowed model with its exposed fields, row limit and whether aggregates are allowed. Agents call this first.
- odoo_describe_model — {"model": "sale.order"}
Field names, types, labels and selection values for one allowed model, so the agent writes correct domains instead of guessing.
- odoo_search — {"model", "domain", "fields", "limit", "order"}
The workhorse. The domain is validated (see below), AND-ed with the rule's filter, and the limit is clamped.
- odoo_read — {"model", "ids", "fields"}
Specific records by id, same field allowlist.
- odoo_aggregate — {"model", "domain", "group_by", "measure"}
Grouped count, or sum of one numeric field. This is what makes "how much did we sell per salesperson last month" cost 12 rows instead of 12,000.
There is no sixth tool. Nothing in this list writes, and no tool takes a model method name.
What the bridge refuses
A model that is not on the allowlist — refused before any SQL runs.
A field that is not on the allowlist — the call is refused rather than silently trimmed, so the agent learns instead of hallucinating a value.
A domain sent as a string — string domains are executable Python in Odoo; only list domains are accepted.
A malformed domain — every condition must be a 3-part [field, operator, value]; only &, | and ! are accepted as operators.
Anything above the caps — row counts are clamped, and past the daily cap every call is refused until the rolling window moves.
An expired, revoked or unknown token — JSON-RPC error -32001.
Refusals are answered as an MCP tool error (isError: true), which is what lets the assistant explain the limit to the user instead of crashing.
The query log
AI Bridge → Query Log records, per call: token, tool, model, the exact arguments, row count, duration in milliseconds, an estimated token cost of the payload (payload size ÷ 4, the usual rule of thumb), status (Answered / Refused / Failed), the message of any refusal and the remote address.
Use it three ways:
Cost control — group by token, sum estimated tokens. If one assistant burns 400k tokens a day pulling res.partner in full, tighten its field list.
Security review — filter on Refused. A healthy integration refuses almost nothing; a burst of refusals means either a badly configured client or an assistant being talked into wandering.
Kill switch — the Revoke button on a log line disables the token that produced it, immediately, without leaving the log.
The token form shows calls today, calls in total, estimated tokens today and the last-used timestamp.
Security notes
Secrets are stored as SHA-256 digests; the plaintext exists only in the banner shown at creation.
Queries run with_user() as the token's user, so record rules, multi-company rules and access rights all still apply. The allowlist narrows that further — it never widens it.
The controller is auth="none" with save_session=False: no session is created, so a stolen bearer token cannot be turned into a browser session.
The endpoint answers only POST, and only JSON-RPC.
Nothing leaves your server. The bridge has no outbound calls, no telemetry and no dependency on any AI vendor.
Troubleshooting
- "Unknown or expired token" (-32001)
The secret is wrong, the token was revoked, or Expires on has passed. Regenerate and update the client — remember the whole secret including the bfai_ prefix.
- "This token may not read X."
Add X under Allowed models, or let the assistant ask odoo_list_models first.
- "Fields not allowed on X: y, z"
Either add the fields to the rule, or leave Fields empty to expose the whole model except the sensitive ones.
- The agent gets fewer rows than it asked for
That is the row cap doing its job. Raise Max rows per call on the token or the Row limit on the rule — or better, teach the agent to use odoo_aggregate.
- "This token has used its N calls for today."
The rolling 24-hour cap. Raise Max calls per day, or find out why the client is looping — the log shows the repeated call.
- 404 on /bf_ai/mcp
The app is not installed on that database, or your reverse proxy strips the path. Test on the Odoo port directly.
- The menu is missing
You are not in the AI Bridge / Manager group.
What changed in 1.1
A real cost ceiling: cap the estimated LLM tokens per rolling day, not just the number of calls - 500 calls of huge payloads no longer fit under the radar. Plus a per-minute rate limit for runaway agent loops.
Grants are audited too: every change to a token's allowlist lands in the token's chatter, next to the queries it produced.
The query log now expires (180 days by default, tunable with the bf_ai_bridge.log_retention_days system parameter) instead of storing request arguments forever.
Tokens, rules and logs are isolated per company, and the query log is readable by AI Bridge managers only.
What changed in 1.1
A real cost ceiling: cap the estimated LLM tokens per rolling day, not only the number of calls - 500 calls of huge payloads no longer fit under the radar. Plus a per-minute rate limit for runaway agent loops.
Grants are audited too: every change to a token's allowlist lands in the token's chatter, next to the queries it produced.
The query log expires (180 days by default, tunable with the bf_ai_bridge.log_retention_days system parameter) instead of storing request arguments forever.
Tokens, rules and logs are isolated per company, and the query log is readable by AI Bridge managers only.
Support
support@bambooforge.dev — questions, bug reports and feature requests are answered by the developers who wrote the module.
Screens



