Skip to main content
Developer Guide

Odoo MCP Server: Connect AI Tools to Your ERP

Build a minimal MCP server in Python, connect it to Claude Code or Cursor, and give your AI tools live read/write access to Odoo. Query CRM records, check inventory, create contacts. All from natural language.

15 min read
Updated April 2026
Odoo 8+

What MCP Is and Why Odoo Needs It

MCP stands for Model Context Protocol. It's an open standard published by Anthropic that defines how AI tools communicate with external systems. Before MCP, connecting an AI agent to a new data source meant writing bespoke glue code. Custom Python functions for Claude, different wrappers for Cursor, separate middleware for every agent framework. MCP replaces all of that with a single server interface. Build it once, and any MCP-compatible client can call it.

The protocol is straightforward. An MCP server exposes two things:

  • Tools are functions the AI can invoke, like search_records or create_invoice
  • Resources are data the AI can read into context, like a list of open opportunities

The AI client discovers what's available, calls tools when needed, and gets structured results back. Everything happens over a well-defined JSON-RPC-like protocol.

For Odoo, this matters because Odoo holds the operational data that AI agents actually need to do useful work. Customer records, sales pipeline, inventory levels, project tasks, invoices. Without an MCP server, an AI working on an Odoo-related task is flying blind. It only knows what you paste into the chat. With an MCP server, your AI can answer “how many open sales orders are waiting on the warehouse team?” by querying Odoo directly, instead of asking you to go look it up.

The companion guide Vibe Coding for Odoo covers AI-assisted module development. This guide is different. It's about giving AI tools real-time access to live data from a running Odoo instance. The two approaches stack well together.

How Odoo's External API Works

Before building the MCP server, it helps to understand what it's wrapping. Odoo has a stable external API based on XML-RPC (and a compatible JSON-RPC variant) that has been available since version 6. This API is the official, documented way to interact with Odoo from outside the application. Third-party integrations, ETL pipelines, and automation scripts all use it.

The API is split into two endpoints:

  • /xmlrpc/2/common for version info and authentication (unauthenticated)
  • /xmlrpc/2/object for all model operations (authenticated)

Authentication is simple. Call authenticate() with your database name, username, and password. You get back a numeric user ID (uid). That UID plus the password authenticates every subsequent call.

auth.pypython
import xmlrpc.client

url = 'https://your-odoo.oec.sh'
db = 'mydb'
username = 'api-user@example.com'
password = 'your-api-key-or-password'

# Get the UID
common = xmlrpc.client.ServerProxy(f'{url}/xmlrpc/2/common')
uid = common.authenticate(db, username, password, {})

print(uid)  # e.g. 6 - your user ID for subsequent calls

Once you have a uid, all data operations go through execute_kw() on the object endpoint:

query.pypython
models = xmlrpc.client.ServerProxy(f'{url}/xmlrpc/2/object')

# Search for confirmed sales orders
order_ids = models.execute_kw(
    db, uid, password,
    'sale.order',       # model name
    'search',           # method
    [[('state', '=', 'sale')]],  # domain filter
)

# Read fields from those records
orders = models.execute_kw(
    db, uid, password,
    'sale.order',
    'read',
    [order_ids],
    {'fields': ['name', 'partner_id', 'amount_total', 'date_order']},
)

The execute_kw() signature is always: (db, uid, password, model, method, args, kwargs). The methods you'll use most are search, read, search_read (combines both),create, write, and unlink. Domain filters follow Odoo's triplet format: [('field', 'operator', 'value')]. This is exactly what the MCP server wraps. The MCP layer adds tool definitions, parameter schemas, and the protocol plumbing so that AI clients can discover and call these operations automatically.

Installing the Odoo MCP Server

There are community MCP packages for Odoo, but the simplest approach is a minimal custom server. You control it, you can extend it, and it's only about 60 lines of Python.

Prerequisites

You need Python 3.10+ and uv (the fast Python package manager). If you don't have uv:

Terminalbash
curl -LsSf https://astral.sh/uv/install.sh | sh

Create the project

Terminalbash
mkdir odoo-mcp-server
cd odoo-mcp-server
uv init --no-workspace
uv add mcp

The complete MCP server

Create server.py with the following content. This exposes three tools that cover the most common use cases:

server.pypython
import os
import xmlrpc.client
from mcp.server.fastmcp import FastMCP

# Configuration
ODOO_URL = os.environ["ODOO_URL"]          # e.g. https://mydb.oec.sh
ODOO_DB = os.environ["ODOO_DB"]            # e.g. mydb
ODOO_USER = os.environ["ODOO_USER"]        # e.g. api@example.com
ODOO_PASSWORD = os.environ["ODOO_PASSWORD"]  # API key or password

# Odoo connection
def get_client():
    common = xmlrpc.client.ServerProxy(f"{ODOO_URL}/xmlrpc/2/common")
    uid = common.authenticate(ODOO_DB, ODOO_USER, ODOO_PASSWORD, {})
    if not uid:
        raise ValueError("Odoo authentication failed -- check credentials")
    models = xmlrpc.client.ServerProxy(f"{ODOO_URL}/xmlrpc/2/object")
    return uid, models

# MCP server
mcp = FastMCP("Odoo")

@mcp.tool()
def search_records(
    model: str,
    domain: list,
    fields: list[str],
    limit: int = 20,
) -> list[dict]:
    """Search Odoo records and return field values.

    Args:
        model: Odoo model name, e.g. 'sale.order', 'res.partner', 'project.task'
        domain: Odoo domain filter, e.g. [['state', '=', 'sale']]
        fields: Field names to return, e.g. ['name', 'partner_id', 'amount_total']
        limit: Maximum records to return (default 20, max 100)
    """
    uid, models = get_client()
    limit = min(limit, 100)
    return models.execute_kw(
        ODOO_DB, uid, ODOO_PASSWORD,
        model, "search_read",
        [domain],
        {"fields": fields, "limit": limit},
    )

@mcp.tool()
def read_record(model: str, record_id: int, fields: list[str]) -> dict:
    """Read a single Odoo record by ID.

    Args:
        model: Odoo model name, e.g. 'res.partner'
        record_id: The integer ID of the record
        fields: Field names to return
    """
    uid, models = get_client()
    results = models.execute_kw(
        ODOO_DB, uid, ODOO_PASSWORD,
        model, "read",
        [[record_id]],
        {"fields": fields},
    )
    if not results:
        raise ValueError(f"No record found: {model} id={record_id}")
    return results[0]

@mcp.tool()
def create_record(model: str, values: dict) -> int:
    """Create a new Odoo record and return its ID.

    Args:
        model: Odoo model name, e.g. 'res.partner', 'project.task'
        values: Field values as a dict, e.g. {'name': 'Acme Corp', 'email': 'acme@example.com'}
    """
    uid, models = get_client()
    new_id = models.execute_kw(
        ODOO_DB, uid, ODOO_PASSWORD,
        model, "create",
        [values],
    )
    return new_id

if __name__ == "__main__":
    mcp.run()

Run it

Terminalbash
ODOO_URL=https://mydb.oec.sh \
ODOO_DB=mydb \
ODOO_USER=api@example.com \
ODOO_PASSWORD=your-api-key \
uv run server.py

The server starts and listens on stdio by default, which is what MCP clients expect. You won't see output unless there's an error. That's normal.

Connecting Claude Code to Odoo

Claude Code reads MCP server configuration from .mcp.json in your project root (or ~/.claude/mcp.json for global config). Add the Odoo server like this:

.mcp.jsonjson
{
  "mcpServers": {
    "odoo": {
      "command": "uv",
      "args": ["run", "/path/to/odoo-mcp-server/server.py"],
      "env": {
        "ODOO_URL": "https://mydb.oec.sh",
        "ODOO_DB": "mydb",
        "ODOO_USER": "api@example.com",
        "ODOO_PASSWORD": "your-api-key"
      }
    }
  }
}

Replace the path with the absolute path to your server file. After saving, restart Claude Code (or run /mcp to reload servers). You should see “odoo” listed as a connected server.

What it looks like in practice

Once connected, Claude Code can call your Odoo tools directly. Here's a real example session:

You:

How many open sales orders do I have, and what's the total value?

Claude Code calls search_records:

{
  "model": "sale.order",
  "domain": [["state", "=", "sale"]],
  "fields": ["name", "amount_total", "partner_id"],
  "limit": 100
}

Claude Code:

You have 23 confirmed sales orders currently open. The total value across all of them is $184,320. The three largest are SO/2026/0441 ($28,500, Apex Technologies), SO/2026/0438 ($22,100, Meridian Group), and SO/2026/0429 ($19,800, Barton & Associates).

That's the loop: natural language question, Claude calls the MCP tool, Odoo returns data, Claude answers with context. No copy-pasting, no switching tabs, no manual lookups.

Connecting Cursor to Odoo via MCP

Cursor added MCP support in version 0.44. The configuration file is ~/.cursor/mcp.json (global) or .cursor/mcp.json in a specific project directory. The config format is identical to Claude Code:

~/.cursor/mcp.jsonjson
{
  "mcpServers": {
    "odoo": {
      "command": "uv",
      "args": ["run", "/path/to/odoo-mcp-server/server.py"],
      "env": {
        "ODOO_URL": "https://mydb.oec.sh",
        "ODOO_DB": "mydb",
        "ODOO_USER": "api@example.com",
        "ODOO_PASSWORD": "your-api-key"
      }
    }
  }
}

After saving, go to Cursor Settings → MCP and you should see the Odoo server listed as “Connected”. If it shows as disconnected, check that uv is on your PATH and that the server path is correct.

Cursor chat example

Where Cursor's MCP integration really shines is when you're editing custom Odoo module code and need to reference real production data. Say you're building a custom report and need to know the actual field structure of sale.order records.

You (in Cursor chat):

Look up a recent sales order so I can see what fields it actually has in production. Pull order SO/2026/0441.

Cursor calls search_records then read_record:

{
  "id": 441,
  "name": "SO/2026/0441",
  "partner_id": [142, "Apex Technologies"],
  "date_order": "2026-04-12 09:23:11",
  "amount_untaxed": 25000.0,
  "amount_tax": 3500.0,
  "amount_total": 28500.0,
  "state": "sale",
  "user_id": [3, "Sarah Chen"],
  "team_id": [1, "Sales"],
  "order_line": [1102, 1103, 1104]
}

Your model has team_id and user_id as Many2one fields. For the report, you'll likely want partner_id[1] (the display name) rather than the raw tuple.

You're now writing code with real field names and real data shapes. No guessing from documentation. For more on Cursor workflows with Odoo, see the Cursor for Odoo Development guide.

Practical Examples: What You Can Build

The three tools in the minimal server (search_records, read_record, create_record) are enough to build a wide range of useful AI workflows.

1. Inventory fulfillment agent

An AI agent that answers “can I fulfill this order?” by checking stock in real time.

inventory-check.pypython
# Agent checks inventory before confirming a custom order
result = search_records(
    model="stock.quant",
    domain=[
        ["product_id.default_code", "=", "WIDGET-PRO-500"],
        ["location_id.usage", "=", "internal"],
    ],
    fields=["product_id", "quantity", "reserved_quantity", "location_id"],
)

# available = quantity - reserved_quantity
available = sum(
    r["quantity"] - r["reserved_quantity"] for r in result
)
# Agent returns: "Available stock: 47 units across 3 warehouses.
# Your order of 20 units can be fulfilled."

More useful than a static spreadsheet export because the agent queries at the moment it needs the answer.

2. Project status reports from Claude Code

If you manage projects in Odoo, Claude Code can read tasks and produce a structured status summary:

Prompt:

Read all in-progress project tasks assigned to the dev team and write a weekly status report.

{
  "model": "project.task",
  "domain": [
    ["stage_id.name", "in", ["In Progress", "In Review"]],
    ["user_ids.name", "ilike", "dev"]
  ],
  "fields": ["name", "project_id", "user_ids", "date_deadline", "description"]
}

Claude gets back real tasks with names, assignees, deadlines, and descriptions, then writes a markdown report you can drop into Slack or a wiki.

3. Cursor autocompleting with real field names

When writing a domain filter in a custom module, you often need the exact field name from production. Instead of checking the Odoo UI or reading the source model:

In Cursor:

What are the available fields on res.partner for a domain filter? I need to filter by country and customer status.

Cursor calls search_records on res.partner with a small limit to inspect the shape, then reads the field list. It comes back with: country_id (Many2one to res.country), customer_rank (Integer, >0 means customer), is_company (Boolean). Correct field names pasted directly. No typos, no docs hunting.

4. Creating contacts from business card descriptions

This is where create_record becomes genuinely useful. You have a stack of business card photos from a networking event. Feed descriptions to an AI agent:

Prompt:

Create an Odoo contact for: “Dr. Maria Santos, CTO at NovaTech Solutions, maria@novatech.io, +1-555-0192, based in Austin TX”

{
  "model": "res.partner",
  "values": {
    "name": "Dr. Maria Santos",
    "function": "CTO",
    "company_name": "NovaTech Solutions",
    "email": "maria@novatech.io",
    "phone": "+1-555-0192",
    "city": "Austin",
    "country_id": 233,
    "is_company": false
  }
}

The agent returns the new record ID. Run this in a loop over 50 business cards and you've populated your CRM in minutes. The AI handles all the name parsing and field mapping.

Security Considerations

An MCP server with write access to your Odoo instance is a serious piece of infrastructure. Take these steps before putting it in front of anyone other than yourself.

Credentials in environment variables

Never hardcode credentials in server.py. The example above uses os.environ correctly. Credentials come from the shell environment or from the MCP client's env block in .mcp.json. Not in version control. Not visible in process listings.

If you're storing .mcp.json in a shared repo, use references to shell variables instead of literal values:

"env": {
  "ODOO_PASSWORD": "${ODOO_API_KEY}"
}

Or better: store secrets in a local .env file that's in .gitignore, and load them into your shell before starting Claude Code or Cursor.

Dedicated read-only API user

Create a dedicated Odoo user specifically for MCP/API access. Do not use your admin account.

In Odoo Settings → Users, create a user like api-readonly@yourdomain.com with:

  • User type: Internal user (minimum required for API access)
  • Access rights: Only the models you actually need
  • No confidential data: If the AI doesn't need HR or payroll data, don't grant access

If you only need read operations, don't include create_record in your MCP server at all. Expose only the tools that match the actual access rights. Defense in depth means even if the MCP server is misused, the Odoo-side permissions act as a hard limit.

Rate limiting the MCP server

The minimal server above has no rate limiting. An AI agent in a loop (or a buggy tool call) can easily fire hundreds of requests per minute. Add a simple rate limiter:

rate_limiter.pypython
import time
from functools import wraps

_last_call = 0
_min_interval = 0.1  # 10 requests/second max

def rate_limited(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
        global _last_call
        elapsed = time.time() - _last_call
        if elapsed < _min_interval:
            time.sleep(_min_interval - elapsed)
        _last_call = time.time()
        return fn(*args, **kwargs)
    return wrapper

Decorate your tool functions with @rate_limited above @mcp.tool().

IP allowlisting on the Odoo server

If your Odoo instance is on OEC.sh (or any hosting provider that allows firewall rules), restrict the xmlrpc endpoint to known IP addresses. For Nginx:

nginx.confnginx
location /xmlrpc/ {
    allow 203.0.113.0/24;  # your office/VPN range
    allow 198.51.100.5;    # your CI server
    deny all;
    proxy_pass http://odoo;
}

This prevents the API from being reachable at all from unauthorized sources, regardless of credential validity.

Important: Never expose your Odoo admin credentials to an MCP server. The admin account can install modules, delete databases, and bypass all access controls. Create a least-privilege API user and treat it like an API key, not a login. For guidance on keeping your Odoo instance resilient, see Odoo Backup and Recovery on OEC.sh.

Deploying Your MCP-Connected Odoo on OEC.sh

Every Odoo instance deployed on OEC.sh exposes the JSON-RPC/xmlrpc API at https://<your-instance>.oec.sh/xmlrpc/2/. No extra configuration needed. The API is on by default.

Finding your instance URL and database name

Log into your OEC.sh dashboard. Your instance URL is listed on the overview page and follows the pattern https://<slug>.oec.sh. Your database name is the same as the slug by default. You can confirm it by checking Settings → General Settings → Database name in the Odoo UI.

Creating an API user

In your Odoo instance:

  1. Go to Settings → Users & Companies → Users
  2. Click “New”
  3. Set the name to something identifiable like “MCP API User”
  4. Enter an email address you control
  5. Under “User Type,” select “Internal User”
  6. Grant only the access rights the AI needs (Sales, CRM, Project, etc.)
  7. Save and set a strong password

To use an API key instead of the account password (recommended for Odoo 16+), go to the user's preferences and generate an API key. Use that key as ODOO_PASSWORD in your MCP config.

Point your MCP config at the instance

.mcp.jsonjson
{
  "mcpServers": {
    "odoo": {
      "command": "uv",
      "args": ["run", "/path/to/odoo-mcp-server/server.py"],
      "env": {
        "ODOO_URL": "https://mycompany.oec.sh",
        "ODOO_DB": "mycompany",
        "ODOO_USER": "mcp-api@mycompany.com",
        "ODOO_PASSWORD": "abc123xyz-your-api-key"
      }
    }
  }
}

That's it. Your AI tools now have live access to your production Odoo ERP. For initial deployment and setup, see Deploying Odoo on OEC.sh. For pricing tiers and instance options, see OEC.sh Pricing.

If you're also working on custom module development alongside this AI integration workflow, Cursor for Odoo Development covers the code-writing side in detail.

Ready to connect AI to your Odoo?

OEC.sh deploys Odoo on any cloud provider with the xmlrpc API ready to go. Point your MCP server at it and start querying live ERP data from Claude Code or Cursor.

  • Free tier available
  • API access out of the box
  • Any cloud provider
Try OEC.sh Free

Frequently Asked Questions

Does this work with Odoo Community Edition?

Yes. The xmlrpc API is available in both Community and Enterprise editions and has been since Odoo 8. The API surface is essentially the same. The only difference is which models are available. Enterprise editions have more models (e.g., hr.payslip, sign.request) but the connection and call patterns are identical. The MCP server code in this guide works with Community Edition without modification.

What's the difference between MCP and Odoo's REST API?

Odoo 16 introduced a proper REST API (/api/ endpoints with JSON bodies and Bearer token auth) that's more developer-friendly than the older xmlrpc interface. The MCP server can wrap either one. The xmlrpc approach in this guide is used because it's available on all Odoo versions from 8 onwards and has better Python tooling. If you're on Odoo 16+ and prefer REST, you can replace the xmlrpc calls with requests calls to the /api/ endpoints. The MCP layer above them stays identical.

Can I use this in production or just development?

You can use it in production. The tools in this guide are read-heavy, and create_record is a standard API operation that Odoo handles the same way whether it comes from the UI or an API call. The security considerations section covers what you need to do before production use: dedicated API user, minimal permissions, rate limiting, IP allowlisting. Treat the MCP server like any other system integration. Set it up carefully and it's safe to run against production data.

How do I handle authentication tokens securely?

Three approaches, in order of preference. First, Odoo API keys (Odoo 16+): generate a per-integration API key in user preferences. It can be revoked independently of the password. Store it as an environment variable. Second, environment variables in .mcp.json: the env block passes secrets to the server process without them appearing in code. Make sure .mcp.json is in .gitignore if it contains literal credentials. Third, a system keychain or secrets manager (HashiCorp Vault, AWS Secrets Manager, 1Password CLI) for team setups. Never commit credentials to a git repository, even a private one.

Is there an official Odoo MCP server?

As of April 2026, there is no official MCP server maintained by Odoo S.A. The community project odoo-mcp on GitHub wraps the JSON-RPC API in a similar way to the server in this guide. The minimal custom approach described here is preferable for most setups because you own the code, you can add tools tailored to your specific models and workflows, and you're not dependent on a third-party package to stay current with MCP spec updates. Building your own from the template above takes about an hour and gives you full control.

Give Your AI Tools Access to Odoo

The MCP server takes about an hour to build. Once it's running, Claude Code and Cursor can query your CRM, check inventory, create records, and generate reports from live Odoo data. No more copy-pasting between tabs.