One server process can front several named Odoo databases. Shipped in v0.4.0; this guide covers configuration, routing, and the isolation model.
odoo_config.json (or the path in ODOO_CONFIG_FILE):
{
"default": "production",
"instances": {
"production": {
"url": "https://mycompany.odoo.com",
"db": "mycompany",
"username": "agent@mycompany.com",
"password": "api-key-1",
"timeout": 30,
"verify_ssl": true
},
"staging": {
"url": "https://staging.mycompany.com",
"db": "mycompany_staging",
"username": "agent@mycompany.com",
"password": "api-key-2",
"transport": "json2"
}
}
}Rules:
- Instance names match
[A-Za-z0-9_-]{1,64}. - Each entry is self-contained — credentials and transport never
inherit from another entry. Global env vars only act as fallback
defaults for
timeout/verify_sslstyle keys. - If all four legacy env vars (
ODOO_URL/ODOO_DB/ODOO_USERNAME/ODOO_PASSWORD) are set, they win and define a singledefaultinstance; a warning is printed when a config file is ignored because of this. - See
odoo_config.multi.json.examplefor a copyable template.
Every Odoo-facing tool accepts an optional instance argument:
search_records(model="sale.order", instance="staging", ...)
- Omitted → the
defaultinstance. list_instancesreturns names, URLs, databases, and transports — never credentials.- There is no HTTP-header-based routing into the MCP server; the
instancetool argument is the only switch. (X-Odoo-Databaseis a different thing: a JSON-2 header between this server and Odoo, controlled byODOO_JSON2_DATABASE_HEADER.) - In prompts, just name the instance: "Using the
staginginstance, …".
| Mechanism | Guarantee |
|---|---|
| Lazy clients | Instances connect on first use; one bad credential does not block others. |
| Approval tokens | Tokens encode the instance; a write validated against staging can never execute on production. execute_approved_write runs on the instance recorded in the approval — no override at execution time. |
| Schema caches | Partitioned per instance ({instance}:{model}), bounded by TTL + LRU (ODOO_MCP_SCHEMA_CACHE_TTL, ODOO_MCP_SCHEMA_CACHE_MAX). |
| Audit log | Each JSONL entry records the instance (see ODOO_MCP_AUDIT_LOG). |
- MCP resources (
odoo://...) always use the default instance; tools are the multi-instance surface. - Verified end-to-end by
scripts/odoo_multi_instance_smoke.py(three databases, two accounts, cross-instance token replay rejection).
One question across many instances at once — see the
partner playbook for recipes. Three tools:
search_across_instances, aggregate_across_instances, and
accounting_health_across_instances. Each returns per-instance results
merged and attributed (_instance tag), with a partial-failure errors
map so one instance being down never fails the whole query.
Two optional per-instance config keys feed selection:
"tags": ["eu", "retail"]— select withinstances={"tags": ["eu"]}."cross_instance": false— opt out of all fan-out queries (sandboxes, internal DBs). Default is opt-in.
Each instance is queried under its own field ACL, applied before merge,
so denied fields never cross instance boundaries. Concurrency is bounded
(ODOO_MCP_CROSS_INSTANCE_WORKERS, default 4) and each target counts against
its own rate-limit budget. Gated writes remain single-instance by design.