Skip to main content
Glama
altinkaya-opensource

mcp-server-odoo

README.md
# šŸ”Œ mcp-server-odoo

A [Model Context Protocol](https://modelcontextprotocol.io/) server that gives AI
assistants full access to your Odoo ERP instance over XML-RPC.

No Odoo module installation required. Just point it at any Odoo 12+ instance.

```
šŸ¤– AI Assistant (Claude Code, Claude Desktop, etc.)
       │ MCP (stdio)
       ā–¼
  šŸ“” mcp-server-odoo
       │ XML-RPC
       ā–¼
  šŸ¢ Odoo Instance
```

## ⚔ Quick Start

No cloning required — just run directly from GitHub with [uv](https://docs.astral.sh/uv/):

### šŸ–„ļø With Claude Code

```bash
claude mcp add odoo \
    -e ODOO_URL=http://localhost:8069 \
    -e ODOO_DB=mydb \
    -e ODOO_USER=admin \
    -e ODOO_PASSWORD=admin \
    -- uvx --from git+https://github.com/altinkaya-opensource/odoo-mcp mcp-server-odoo
```

### šŸ–±ļø With Claude Desktop

Add to your `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "odoo": {
      "command": "uvx",
      "args": [
        "--from",
        "git+https://github.com/altinkaya-opensource/odoo-mcp",
        "mcp-server-odoo"
      ],
      "env": {
        "ODOO_URL": "http://localhost:8069",
        "ODOO_DB": "mydb",
        "ODOO_USER": "admin",
        "ODOO_PASSWORD": "admin"
      }
    }
  }
}
```

### šŸ”’ Readonly Mode

Block all write operations by adding `READONLY_MODE=true`:

```bash
claude mcp add odoo \
    -e ODOO_URL=http://localhost:8069 \
    -e ODOO_DB=mydb \
    -e ODOO_USER=admin \
    -e ODOO_PASSWORD=admin \
    -e READONLY_MODE=true \
    -- uvx --from git+https://github.com/altinkaya-opensource/odoo-mcp mcp-server-odoo
```

## šŸ› ļø Tools

### šŸ“– Read Operations

| Tool                | Description                                                                       |
| ------------------- | --------------------------------------------------------------------------------- |
| `search_records`    | šŸ” Search any model with domain filters, field selection, pagination, and sorting |
| `read_record`       | šŸ“„ Read a single record by ID with smart field selection                          |
| `get_record_count`  | šŸ”¢ Count records matching a domain filter (lightweight, no data fetched)          |
| `list_models`       | šŸ“‹ List all non-transient models available in the database                        |
| `get_model_fields`  | šŸ—ļø Inspect field definitions (type, required, help text, etc.) for any model      |
| `read_group`        | šŸ“Š Group records and compute aggregations (sum, avg, count) with date granularity |
| `save_binary_field` | šŸ’¾ Save a binary/image field from a record directly to a local file               |

### āœļø Write Operations

| Tool             | Description                                                                           |
| ---------------- | ------------------------------------------------------------------------------------- |
| `create_record`  | āž• Create a new record in any model                                                   |
| `update_record`  | šŸ“ Update fields on an existing record                                                |
| `delete_record`  | šŸ—‘ļø Delete a record by ID                                                              |
| `copy_record`    | šŸ“‹ Duplicate an existing record with optional field overrides                         |
| `execute_method` | āš™ļø Call any business method (e.g. `action_confirm`, `button_validate`, `action_post`) |

> 🚫 Write tools are disabled when `READONLY_MODE=true`.

## ✨ Features

### 🧠 Smart Field Selection

When you don't specify which fields to fetch, the server automatically picks the most
useful ones. It scores fields based on type, importance patterns (`state`, `amount`,
`partner`, etc.), and excludes noisy fields like `message_ids`, binary blobs, and
computed non-stored fields. You always get `id`, `name`, `display_name`, and `active`.

Pass `fields=["__all__"]` to override and get everything.

### šŸ”„ Flexible Domain Parsing

Domains can be passed as JSON strings, Python repr strings, or native lists:

```python
# All of these work:
[["is_company", "=", true]]
'[["is_company", "=", true]]'
"[('is_company', '=', True)]"
```

### šŸ“… ISO 8601 Dates

Odoo's `"2025-06-07 21:55:52"` datetime strings are automatically converted to
`"2025-06-07T21:55:52+00:00"` for standard ISO 8601 output.

## šŸ’” Tool Usage Examples

**šŸ” Search for Turkish companies:**

```
search_records("res.partner", [["is_company", "=", true], ["country_id.code", "=", "TR"]], limit=20)
```

**šŸ”¢ Count open sale orders:**

```
get_record_count("sale.order", [["state", "=", "sale"]])
```

**šŸ“„ Read a specific product with all fields:**

```
read_record("product.product", 42, fields=["__all__"])
```

**āœ… Confirm a sale order:**

```
execute_method("sale.order", "action_confirm", [42])
```

**šŸ“¦ Validate a stock picking:**

```
execute_method("stock.picking", "button_validate", [15])
```

**šŸ’° Post an invoice:**

```
execute_method("account.move", "action_post", [100])
```

**šŸ“‹ Duplicate a sale order with a different partner:**

```
copy_record("sale.order", 10, default={"partner_id": 99})
```

**šŸ’¾ Save a product image to disk:**

```
save_binary_field("product.product", 42, "image_1920", "/tmp/product_image.png")
```

**šŸ“Š Total sales by partner:**

```
read_group("sale.order", "partner_id", domain=[["state", "=", "sale"]], fields=["amount_total:sum"])
```

**šŸ“… Monthly order counts:**

```
read_group("sale.order", "date_order:month", fields=["id:count"])
```

**šŸ“ˆ Average price by category:**

```
read_group("product.template", "categ_id", fields=["list_price:avg"])
```

**šŸ—ļø Discover fields on a model:**

```
get_model_fields("sale.order", attributes=["string", "type", "required"])
```

## āš™ļø Configuration

All configuration is done through environment variables:

| Variable                 | Required | Default | Description                                     |
| ------------------------ | -------- | ------- | ----------------------------------------------- |
| `ODOO_URL`               | āœ…       |         | Odoo server URL (e.g. `http://localhost:8069`)  |
| `ODOO_DB`                | āœ…       |         | Database name                                   |
| `ODOO_USER`              | āœ…       |         | Username                                        |
| `ODOO_PASSWORD`          | āœ…       |         | Password or API key                             |
| `READONLY_MODE`          |          | `false` | Set to `true` to disable all write operations   |
| `ODOO_MCP_LOG_LEVEL`     |          | `INFO`  | Log level (`DEBUG`, `INFO`, `WARNING`, `ERROR`) |
| `ODOO_MCP_LOG_FILE`      |          | stderr  | Path to log file                                |
| `ODOO_MCP_DEFAULT_LIMIT` |          | `10`    | Default record limit for search operations      |

Alternatively, create a `.env` file in the project root.

## šŸ“‹ Requirements

- šŸ Python >= 3.10
- šŸ“¦ [uv](https://docs.astral.sh/uv/) (recommended) or pip
- šŸ¢ Access to an Odoo instance (12.0+) with XML-RPC enabled

## šŸ“œ License

AGPL-3.0-or-later