Odoo MCP 服务

这是一个将 Odoo 19+ 接入 Model Context Protocol 的集成服务,通过模型发现、ORM 调用、安全分级与确认机制,让 AI 助手能够安全地查询和操作 Odoo 数据,适用于需要通过自然语言驱动 ERP 业务流程的开发与运维场景。

  • by AlanOgic
  • Python
  • 混合部署
  • 11 星标
  • 更新于
在 GitHub 查看 AlanOgic/odoo-mcp-19

使用说明

   ____      __               __  _____________     _______
  / __ \____/ /___  ____     /  |/  / ____/ __ \   <  / __ \  __
 / / / / __  / __ \/ __ \   / /|_/ / /   / /_/ /   / / /_/ /_/ /_
/ /_/ / /_/ / /_/ / /_/ /  / /  / / /___/ ____/   / /\__, /_  __/
\____/\__,_/\____/\____/  /_/  /_/\____/_/       /_//____/ /_/

Ask Claude to read and write your Odoo 19 data in plain language — and stop it from doing the dangerous parts by accident.

This server connects any Model Context Protocol (MCP) client to Odoo 19+ over the v2 JSON-2 API. An assistant discovers your models by reading schemas rather than guessing field names, then calls any ORM method through one tool. Before anything destructive runs, a safety layer classifies the operation and holds it behind a single-use confirmation token.

# The assistant reads the schema first, then queries — no guessed field names
read_resource("odoo://model/sale.order/quick-schema")
execute_method("sale.order", "search_read",
    kwargs_json='{"domain": [["state", "=", "sale"]], "fields": ["name", "amount_total"], "limit": 10}')

Who this is for

  • You run Odoo 19+ and want an assistant that queries and updates it directly, instead of copying data between a chat window and the ERP.
  • You need writes to be safe. Posting a journal entry or validating a picking is irreversible. Every such call is gated, and eight security-critical models refuse writes outright.
  • You care about token cost. Compact schemas, batched model bundles, and one-call session bootstrap keep discovery cheap on long conversations.

If you only need read-only reporting, this still works — set a readonly role and the safety layer blocks every non-safe method.

What you get

3 tools execute_method reaches any method on any model; batch_execute and read_resource cover the rest
38 resources Model discovery, compact schemas, state-machine workflows, introspection, API reference (odoo://api/*), and runtime posture (odoo://server-status)
12 prompts Generic guided workflows (aging reports, inventory checks, domain building, pagination, …)
Safety layer Risk classification before execution, 8 blocked models, 6 sensitive models, cascade warnings
Locked mode One flag (MCP_SAFETY_MODE=locked) turns writes off, enforces a write allowlist, binds HTTP to localhost, and pre-flights payloads against live fields_get — each layer independently overridable
Multi-user mode Per-user bearer keys and personal Odoo clients, so every write is attributed to a real person
Reference data 30 documented ORM methods, 13 modules with special-method knowledge, including the Enterprise AI module

Built on FastMCP 4 (>=4.0.10,<5): one deployment speaks both MCP 2026-07-28 (sessionless — any replica behind a plain load balancer can answer) and MCP 2025-11-25 (session-based), negotiated per connection, so older clients keep working. Background tasks, progress tracking, icons and structured outputs are available in both. See the migration notes.

Hardened by default: regex-validated model and method names, a non-root Docker container, mandatory authentication on HTTP, thread-safe caches, and no traceback ever forwarded to a client.

Installation

Prerequisites

  • Docker Desktop (recommended) OR Python 3.10+
  • An Odoo 19+ instance with API access
  • An API key from your Odoo instance (Preferences → Account Security → New API Key)

Option A: Docker (recommended)

Build from source:

git clone https://github.com/AlanOgic/odoo-mcp-19.git
cd odoo-mcp-19
docker build -t odoo-mcp-19:latest .

Quick test (verify it connects):

docker run --rm -i \
  -e ODOO_URL=https://your-instance.odoo.com \
  -e ODOO_DB=your-database \
  -e ODOO_USERNAME=your-username \
  -e ODOO_API_KEY=your-api-key \
  odoo-mcp-19:latest

You should see the server start without errors. Press Ctrl+C to stop.

Option B: From source (Python venv)

# 1. Clone the repository
git clone https://github.com/AlanOgic/odoo-mcp-19.git
cd odoo-mcp-19

# 2. Create a virtual environment
python3 -m venv .venv

# 3. Activate it
source .venv/bin/activate        # macOS / Linux
# .venv\Scripts\activate         # Windows

# 4. Install the package
pip install -e .

# 5. Create your .env file
cp .env.example .env             # Then edit with your Odoo credentials
# Or create manually:
cat > .env << 'EOF'
ODOO_URL=https://your-instance.odoo.com
ODOO_DB=your-database
ODOO_USERNAME=your-username
ODOO_API_KEY=your-api-key
EOF

# 6. Test it
python -m odoo_mcp

Option C: pip install (from GitHub)

pip install git+https://github.com/AlanOgic/odoo-mcp-19.git

Setup wizard

Interactive wizard that generates .env, Docker commands, and Claude Desktop config:

# From source
python -m odoo_mcp --setup

# From pip
odoo-mcp-19 --setup

The wizard walks you through Odoo connection, transport (stdio or streamable-http), safety mode, and outputs ready-to-use configuration files.

Configure Claude Desktop

Edit your Claude Desktop configuration file:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json

Server name: register this server as odoo19-mcp (the name the --setup wizard generates and the one used in every example below). Skills installed alongside it (distributed separately) may refer to its tools under this name, so a different name can leave them unable to call the server's tools.

Using Docker (recommended)

Method 1: Using run-docker.sh wrapper (simplest)

Create a .env file in the project root with your credentials, then:

{
  "mcpServers": {
    "odoo19-mcp": {
      "command": "/path/to/odoo-mcp-19/run-docker.sh"
    }
  }
}

Method 2: Inline Docker command

{
  "mcpServers": {
    "odoo19-mcp": {
      "command": "docker",
      "args": ["run", "--rm", "-i",
        "-e", "ODOO_URL=https://your-instance.odoo.com",
        "-e", "ODOO_DB=your-database",
        "-e", "ODOO_USERNAME=your-username",
        "-e", "ODOO_API_KEY=your-api-key",
        "odoo-mcp-19:latest"
      ]
    }
  }
}

Using Python (venv)

{
  "mcpServers": {
    "odoo19-mcp": {
      "command": "/path/to/odoo-mcp-19/.venv/bin/python",
      "args": ["-m", "odoo_mcp"],
      "env": {
        "ODOO_URL": "https://your-instance.odoo.com",
        "ODOO_DB": "your-database",
        "ODOO_USERNAME": "your-username",
        "ODOO_API_KEY": "your-api-key"
      }
    }
  }
}

Using pip install

{
  "mcpServers": {
    "odoo19-mcp": {
      "command": "odoo-mcp-19",
      "env": {
        "ODOO_URL": "https://your-instance.odoo.com",
        "ODOO_DB": "your-database",
        "ODOO_USERNAME": "your-username",
        "ODOO_API_KEY": "your-api-key"
      }
    }
  }
}

Restart Claude Desktop after saving the config file.

Verify the connection

Ask Claude: "List the first 5 partners in Odoo". It should call execute_method on res.partner and come back with names.

Quick start examples

# Search partners
execute_method("res.partner", "search_read",
    kwargs_json='{"domain": [["is_company", "=", true]], "fields": ["name", "email"], "limit": 10}')

# Create a partner
execute_method("res.partner", "create",
    args_json='[{"name": "ACME Corp", "is_company": true, "email": "info@acme.com"}]')

# Update with auto-resolved Many2one (no need to know user ID)
execute_method("res.partner", "write",
    args_json='[[42], {"name": "ACME Corp Updated"}]',
    resolve_json='{"user_id": {"model": "res.users", "search": "John"}}')

# Confirm a sale order (2-step safety confirmation with token)
# Step 1: triggers safety gate, returns confirmation_token in hint
result = execute_method("sale.order", "action_confirm", args_json='[[15]]')
# Step 2: confirm with the token from step 1
execute_method("sale.order", "action_confirm", args_json='[[15]]',
    confirmed=true, confirmation_token='<token from step 1>')

# Several operations in one call: each is classified, and the batch confirms once,
# with one token bound to the whole operations list
batch_execute(operations=[
    {"model": "sale.order", "method": "action_confirm", "args_json": "[[15]]"},
    {"model": "sale.order", "method": "action_confirm", "args_json": "[[16]]"},
])

Note that confirmed=true on its own does nothing. The token is single-use, expires after 120 seconds, and is bound to the exact payload the gate inspected — so an agent cannot take a token issued for unlink([1]) and reuse it on unlink([1, 2, …, 1000]).

Architecture

Tools (3)

Tool Purpose
execute_method Call any method on any Odoo model
batch_execute Multiple operations with progress tracking
read_resource Read any odoo:// resource by URI

Resources (38)

Resource Description
odoo://models List all models
odoo://model/{name} Model info with fields
odoo://model/{name}/schema Full fields and relationships
odoo://model/{name}/fields Lightweight field list with labels
odoo://model/{name}/quick-schema Ultra-compact schema (~1.5KB, short keys)
odoo://model/{name}/workflow State machine transitions and side effects
odoo://model/{name}/docs Rich docs: labels, help text, selections
odoo://bundle/{models} Batch quick-schema for N models (max 10)
odoo://session-bootstrap Bootstrap: schemas + workflows for common models
odoo://record/{model}/{id} Get a specific record by ID
odoo://methods/{model} Available methods (enriched with live signatures)
odoo://docs/{model} Documentation URLs
odoo://concepts Business term to model mappings
odoo://find-model/{concept} Natural language to model name
odoo://tools/{query} Search available operations
odoo://actions/{model} Discover model actions
odoo://templates List all resource templates
odoo://tool-registry Pre-built workflows
odoo://module-knowledge Special methods knowledge
odoo://module-knowledge/{name} Knowledge for a specific module
odoo://workflows Business workflows
odoo://server/info Odoo server information
odoo://domain-syntax Domain operator reference
odoo://pagination Pagination guide
odoo://hierarchical Parent/child tree query patterns
odoo://aggregation Aggregation guide (formatted_read_group)
odoo://model-limitations Known model issues + runtime problems
odoo://server-status Runtime safety posture (mode, host, allowlist, warnings). Non-secret. New in v1.16.0.
odoo://api/json2-protocol JSON-2 contract: body keys, error shape, status codes, transaction rule. New in v1.18.0.
odoo://api/version-drift ORM renames since 15.2 (name_get, args→domain, read_group…) and what to call instead. New in v1.18.0.
odoo://api/x2many-commands One2many/Many2many command triples with examples. New in v1.18.0.
odoo://api-index Live catalogue of installed modules and readable models (/doc-bearer/index.json, cached per user). New in v1.18.0.
odoo://api/datetime Server date formats, UTC storage, client-side tz, dynamic domain values ('-3d +1H'), date parts. New in v1.18.0.
odoo://api/mail-thread message_post signature, notification kill-switch context keys, followers, activities. New in v1.18.0.
odoo://api/security-model How ACLs, record rules and field groups compose; has_access / has_group checks. New in v1.18.0.
odoo://api/web-read web_read / web_search_read / web_save nested specification. New in v1.18.0.
odoo://api/xmlids XML id resolution over JSON-2 (check_object_reference) and custom model/field constraints. New in v1.18.0.
odoo://session Live identity: uid, login, lang, tz, current and allowed companies, installed languages. New in v1.18.0.

Prompts (12)

12 generic guided prompts:

Prompt Purpose
odoo-exploration Discover instance capabilities
search-records Search for records in a model
odoo-api-reference Quick API reference card
ar-aging-report Accounts receivable aging
inventory-check Stock levels analysis
crm-pipeline Pipeline analysis
customer-360 Complete customer view
daily-operations Operations dashboard
domain-builder Build complex domain filters
hierarchical-query Query parent/child trees
paginated-search Paginate large result sets
aggregation-report Aggregation reports

Safety layer

Pre-execution safety classification gates dangerous operations behind confirmation.

Risk levels

Level Behavior Confirm?
SAFE Execute immediately Never
MEDIUM Confirm in strict / locked for any record count; proceed in permissive Conditional
HIGH Always require confirmation Always
BLOCKED Always refuse N/A

Blocked models (write always refused)

ir.rule, ir.model.access, ir.access, ir.module.module, ir.config_parameter, res.users, res.groups, res.users.apikeys, plus the models that act on one of those without naming it: password wizards (change.password.*), res.config.settings, res.groups.privilege, ir.model.data, base_import.import, the base.module.* wizards, mail servers, auth providers, portal wizards, and the credential / 2FA / session satellites of res.users.

Only writes are blocked — reads stay open on every model, the connected Odoo account's rights decide what is readable. A blocked model is also protected against indirect writes: x2many commands, default_<field> context keys and load column paths that would create, change or delete its records through an allowed model (res.partner.user_ids → res.users) are refused. Every value shape Odoo turns into such a write is judged the same way: null/false on an x2many is Odoo's clear (which deletes the lines of a one2many whose inverse cascades), and a bare dict in an x2many list is a new record.

Privileged models (admin only, always confirm)

ir.actions.server, base.automation, ir.cron, ir.model, ir.model.fields, ir.default, ir.ui.view

These run code or reshape the database. STDIO, the static MCP_API_KEY and the admin role may write to them — automations, scheduled actions, custom models and fields — and every side-effect call confirms, in every safety mode. Any other multi-user role is refused.

Sensitive models (write always confirms)

account.move, account.payment, account.bank.statement, hr.payslip

Cascade warnings

Side effects are surfaced for workflow actions:

  • sale.order + action_confirm → creates deliveries
  • account.move + action_post → creates journal entries (irreversible)
  • stock.picking + button_validate → updates stock levels
  • purchase.order + button_confirm → creates incoming receipts
  • account.payment + action_post → creates journal entries + reconciliation

Confirmation flow

  1. Caller sends execute_method(model, method, args_json)
  2. Safety layer classifies the operation
  3. If confirmation needed: returns pending_confirmation=true with safety classification and a confirmation_token in the hint field
  4. Caller reviews, then re-calls with confirmed=true AND confirmation_token='<token>'

Tokens are single-use, expire after 120s, and are bound to the specific model+method. This prevents agents from bypassing the safety gate by always passing confirmed=true.

Locked mode (v1.16.0)

MCP_SAFETY_MODE=locked adds four overlapping protections on top of the classifier and token gate. Designed for safe-by-default deployments where a colleague might point an over-eager AI agent at their own Odoo instance.

Protection Default under locked Override env var
Global write kill-switch on MCP_READ_ONLY=false
Side-effect allowlist enforced on (empty list = no writes) populate MCP_WRITE_ALLOWLIST
HTTP bind 127.0.0.1 (localhost-only) MCP_HOST=0.0.0.0
Live fields_get payload pre-flight on MCP_VALIDATE_PAYLOADS=false

Each protection is independently overridable — set locked and tune the individual flags as needed. permissive and strict semantics are unchanged from v1.14.0; no existing deploy changes posture on upgrade unless the operator opts in.

Allowlist syntax: MCP_WRITE_ALLOWLIST="sale.order.action_confirm,res.partner.message_post,product.product.*". Each entry is model.method or model.* (any method on the model). Cross-model wildcards (*.method) are rejected — too easy to over-grant.

Three typical configurations:

# Safe-by-default — paste this and forget
MCP_SAFETY_MODE=locked

# Locked but allow specific side-effect methods
MCP_SAFETY_MODE=locked
MCP_READ_ONLY=false
MCP_WRITE_ALLOWLIST=res.partner.message_post,sale.order.action_confirm

# Power user — current v1.14.0 behaviour, no changes
MCP_SAFETY_MODE=strict

Posture introspection: read odoo://server-status to see the resolved profile at runtime — current mode, host, allowlist contents, and any foot-gun warnings (e.g. locked + MCP_HOST=0.0.0.0, or locked + MCP_READ_ONLY=false). The startup banner shows a one-line summary: [SAFETY locked · READ-ONLY · BIND 127.0.0.1 · ALLOWLIST 0 entries].

Payload pre-flight (MCP_VALIDATE_PAYLOADS=true): before issuing a confirmation token for a write, the validator fetches fields_get for the target model (60s LRU cache) and rejects payloads that reference non-existent fields, write to readonly fields, or arrive when the connection is silently down (fields_get returns {}). Catches hallucinated field names before they reach Odoo.

DX Improvements (v1.11.0)

Token-efficient discovery

Quick schema

odoo://model/{model}/quick-schema — Ultra-compact schema with short keys: t (type), req (required), ro (readonly), rel (relation). ~60-80% smaller than /fields.

Bundle

odoo://bundle/res.partner,sale.order,stock.picking — Batch quick-schema for up to 10 models in one call.

Session bootstrap

odoo://session-bootstrap — One call to bootstrap a conversation with schemas + workflows for common models. Configure via MCP_BOOTSTRAP_MODELS env var.

Workflow

odoo://model/{model}/workflow — State machine transitions for 6 main models with side effects and irreversibility flags. Dynamic fallback for unmapped models.

Many2one resolution

Auto-resolve field names to IDs with resolve_json:

execute_method("res.partner", "write",
    args_json='[[1], {"user_id": null}]',
    resolve_json='{"user_id": {"model": "res.users", "search": "John"}}')

Default context

Set MCP_DEFAULT_CONTEXT to apply context to all operations:

export MCP_DEFAULT_CONTEXT='{"lang": "fr_FR", "tz": "Europe/Paris"}'

Error suggestions

~25 error patterns with actionable suggestions covering 422, 500, 403, 404 errors and fallback patterns.

HTTP transport

Run as an HTTP server with Bearer token authentication for remote or multi-client access.

Docker Compose (recommended)

Add to your .env:

MCP_TRANSPORT=streamable-http
MCP_API_KEY=your-secret-bearer-token

Then:

docker compose up -d

The MCP endpoint is available at http://localhost:8080/mcp.

Docker run

docker run -d -p 8080:8080 \
  -e ODOO_URL=https://your.odoo.com \
  -e ODOO_DB=mydb \
  -e ODOO_USERNAME=admin \
  -e ODOO_API_KEY=xxx \
  -e MCP_TRANSPORT=streamable-http \
  -e MCP_API_KEY=your-secret-token \
  odoo-mcp-19:latest

Claude Desktop config (HTTP)

{
  "mcpServers": {
    "odoo19-mcp": {
      "type": "streamable-http",
      "url": "http://localhost:8080/mcp",
      "headers": {
        "Authorization": "Bearer your-secret-token"
      }
    }
  }
}

Verify authentication

# Should succeed (200)
curl -i -X POST http://localhost:8080/mcp \
  -H "Authorization: Bearer your-secret-token" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc": "2.0", "method": "initialize", "id": 1}'

# Should fail (401/403) — wrong or missing token
curl -i -X POST http://localhost:8080/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc": "2.0", "method": "initialize", "id": 1}'

Multi-user mode

The HTTP transport above serves a single Odoo account behind one static MCP_API_KEY. For teams, set USERS_DB_PATH to switch into multi-user mode: many users share one server process, each authenticating with their own bearer token and acting as their own Odoo account — so every write is attributed to the real person, not a shared service user.

How it works

  • Registry — users, their per-server API keys and encrypted Odoo credentials live in a SQLite registry (users.db) owned and written by CLORAG (a companion app with an /admin/users page). This server is a pure reader — it opens the database with mode=ro and never writes.
  • Per-request identity — an incoming bearer token is hashed (sha256) and looked up in the registry. The caller gets a personal OdooClient built from their stored Odoo username + decrypted API key (cached 300s, re-checked on credential rotation). Plaintext keys are never stored; the static MCP_API_KEY still works and maps to an env-admin identity — the only one served by the env Odoo account.
  • Fail-closed identity — a call that reaches the server without the caller's key is refused, never served by the env account with the operator's role. Only a server started on STDIO by python -m odoo_mcp (or odoo-mcp-19), where nobody authenticates, treats a key-less call as the operator; that and the registry in use are fixed at startup, so no later environment change (.env reload, MCP_TRANSPORT, FASTMCP_TRANSPORT) reopens it.
  • Roles — admin → unrestricted; readonly → read-only, with the safety layer blocking every non-safe method; other roles → normal safety rules on their own account.

Deployment

# Requires: USERS_DB_PATH, TOKEN_ENCRYPTION_KEY (must equal CLORAG's), MCP_TRANSPORT=streamable-http
docker compose -f docker-compose.yml -f docker-compose.multiuser.yml up -d

The overlay mounts the CLORAG data dir read-only at /registry and injects the encryption key as a Docker secret. The TOKEN_ENCRYPTION_KEY and the .token_salt file must be identical to CLORAG's, or stored credentials cannot be decrypted. In multi-user mode the env ODOO_* credentials become optional (only the env-admin fallback uses them), though ODOO_URL / ODOO_DB are still needed to build per-user clients.

Configuration

Variable Required Default Description
ODOO_URL Yes — Odoo server URL
ODOO_DB Yes — Database name
ODOO_USERNAME Yes — Username
ODOO_API_KEY Yes — API key (Preferences → Account Security)
ODOO_PASSWORD No — Password (fallback if no API key)
ODOO_TIMEOUT No 30 Request timeout in seconds
ODOO_VERIFY_SSL No true SSL certificate verification
ODOO_CONFIG_DIR No — Extra directory searched first for a .env, ahead of ./.env then ~/.config/odoo/.env
MCP_TRANSPORT No stdio Transport: stdio or streamable-http
MCP_API_KEY HTTP: this or USERS_DB_PATH — Static bearer token (single-user HTTP, or admin fallback in multi-user mode). HTTP server sys.exit(1) if neither is set
USERS_DB_PATH No — Path to the CLORAG registry (users.db) → enables multi-user mode
TOKEN_ENCRYPTION_KEY / TOKEN_ENCRYPTION_KEY_FILE With USERS_DB_PATH — Secret to decrypt registry Odoo credentials — must equal CLORAG's value
MCP_HOST No 0.0.0.0 (127.0.0.1 under locked) HTTP bind address. Default flips to localhost when MCP_SAFETY_MODE=locked.
MCP_PORT No 8080 HTTP port
MCP_VERBOSE No true Slant-ASCII startup banner to stderr (version, transport, masked creds, safety mode, capability counts). false to silence
MCP_SAFETY_MODE No strict permissive, strict, or locked (v1.16.0). locked activates MCP_READ_ONLY=true, MCP_WRITE_ALLOWLIST enforcement, MCP_HOST=127.0.0.1 default, and MCP_VALIDATE_PAYLOADS=true.
MCP_READ_ONLY No derived from mode true to reject every method that is not a known read. Fail-closed: the gate is method not in SAFE_METHODS, so unrecognised methods (message_post, toggle_active, convert_opportunity, create_from_urls, …) are blocked rather than assumed safe. The 13 SAFE_METHODS (search_read, read, fields_get, …) pass through.
MCP_WRITE_ALLOWLIST No empty Comma-separated model.method (or model.*) entries permitted as side effects. Enforced under locked mode, or whenever set explicitly.
MCP_VALIDATE_PAYLOADS No derived from mode true to validate write payloads against live fields_get before issuing a confirmation token. Catches hallucinated fields and readonly writes.
MCP_SAFETY_AUDIT No — true to log safety audit to stderr
MCP_DEFAULT_CONTEXT No — JSON object merged into all contexts (max 4KB)
MCP_BOOTSTRAP_MODELS No res.partner,sale.order,account.move,product.product,stock.picking Models for session-bootstrap (max 20)

Documentation

Full documentation lives in the Wiki:

Start here

  • Getting started — install and connect in five minutes
  • Tools — the 3 tools, their parameters, and when to reach for each
  • Resources — the 27 odoo:// discovery URIs

Reference

Operations

Security

  • HTTP transport requires MCP_API_KEY — server refuses to start without it
  • Docker runs as non-root user (UID 1001)
  • Input validation — model names (dotted notation regex), method names (identifier regex), URI scheme (odoo:// only)
  • No traceback forwarding — Odoo server tracebacks logged to stderr, never returned to clients
  • Credential files gitignored — .env, .mcp.json, odoo_config.json
  • SSL warning — visible warning on startup when ODOO_VERIFY_SSL=false with HTTPS
  • Thread-safe caches — doc cache (100 entries max, LRU eviction) and runtime issue tracker use locks
  • MCP_DEFAULT_CONTEXT — capped at 4KB, MCP_BOOTSTRAP_MODELS capped at 20 models

Requirements

  • Python 3.10+
  • Odoo 19+
  • FastMCP >=4.0.10,<5 (with tasks extra)
  • requests 2.32.4+

License

MIT