Use this skill to query, create, update, delete, export, or import records in any Odoo instance via MCP tools.
skillsy install vauxoo/mcp.odoo@odoo-mcp-toolsComplete reference for the 11 MCP tools provided by odoo-mcp-multi.
Use this skill when interacting with any Odoo instance via an MCP client
(Antigravity, Claude Desktop, Cursor, VS Code).
odoo-mcp-multi installed (pip install odoo-mcp-multi)odoo-mcp add-profile)list_available_profiles to discover which Odoo environments are available.profile parameter in any tool call to target a specific environment.list_models to search. If unsure of field names, use list_fields.Register the server in your AI client's MCP config file:
{
"mcpServers": {
"odoo": {
"command": "odoo-mcp",
"args": ["run"]
}
}
}
Config file paths by client and OS:
| Client | macOS | Linux | Windows |
|---|---|---|---|
| Antigravity | ~/.gemini/antigravity/mcp_config.json |
same | %USERPROFILE%\.gemini\antigravity\mcp_config.json |
| Claude Desktop | ~/Library/Application Support/Claude/claude_desktop_config.json |
~/.config/Claude/claude_desktop_config.json |
%APPDATA%\Claude\claude_desktop_config.json |
| Cursor | .cursor/mcp.json (project root) |
same | same |
| VS Code | .vscode/mcp.json (project root) |
same | same |
Note: Cursor and VS Code configs are workspace-scoped — place the file at the root of your project.
Tip: After installing
odoo-mcp-multi, runodoo-mcp skills install <agent>(e.g.,antigravity,claude,gemini) to symlink these skills into your IDE's global skills directory.
list_available_profiles — Discover EnvironmentsLists all configured Odoo profiles with name, URL, database, and default status.
list_available_profiles()
search_read — Query Records| Parameter | Type | Default | Description |
|---|---|---|---|
model |
string | (required) | Model name (e.g., res.partner) |
domain |
string | [] |
Search domain |
fields |
string | "" |
Comma-separated field names |
limit |
int | 100 |
Max records to return |
offset |
int | 0 |
Records to skip (pagination) |
order |
string | "" |
Sort order (e.g., name asc) |
format |
string | json |
Response format (see below) |
profile |
string | (default) | Target profile name |
Choose the format based on what you need to do with the data:
| Format | Data key | Token cost | Data loss | Best for |
|---|---|---|---|---|
json |
records |
High (baseline) | None | Parsing/processing values programmatically |
compact |
headers + rows |
~40% of json | None | Exploring large datasets efficiently |
table |
data (Markdown) |
~40% of json | Truncates >50 chars | Showing summaries to the user in chat |
html |
data (HTML) |
~50% of json | None | Pasting into Odoo chatter/Knowledge/reports |
csv |
data (CSV) |
~30% of json | None | Spreadsheet export or feeding back to import_records |
All formats include the same pagination envelope (total, limit, offset, has_more, next_offset, format).
Default (json):
{
"records": [{"id": 1, "name": "Alice"}],
"total": 1500,
"limit": 100,
"offset": 0,
"has_more": true,
"next_offset": 100,
"format": "json"
}
compact:
{
"headers": ["id", "name"],
"rows": [[1, "Alice"], [2, "Bob"]],
"total": 1500,
"has_more": true,
"format": "compact"
}
Important: Always check
has_more— iftrue, usenext_offsetto fetch the next page.
# Default JSON format
search_read(model="res.partner", domain="[('is_company', '=', True)]", fields="name,email", limit=10, profile="prod")
# Compact format for large datasets
search_read(model="res.partner", fields="name,email,phone", limit=500, format="compact")
# Markdown table for user-facing summaries
search_read(model="sale.order", fields="name,partner_id,amount_total,state", format="table")
# HTML for pasting into Odoo
search_read(model="res.partner", fields="name,email", format="html")
# CSV for spreadsheet export
search_read(model="res.partner", fields="name,email,phone", format="csv")
Always check has_more — if true, call again with next_offset:
# Page 1
result = search_read(model="res.partner", fields="name", limit=100, offset=0)
# result["has_more"] == true, result["next_offset"] == 100
# Page 2
result = search_read(model="res.partner", fields="name", limit=100, offset=100)
# Continue until has_more == false
write — Update Records| Parameter | Type | Default | Description |
|---|---|---|---|
model |
string | (required) | Model name |
ids |
string | (required) | Record IDs as JSON array or comma-separated |
values |
string | (required) | Field values as JSON object |
profile |
string | (default) | Target profile name |
write(model="res.partner", ids="[1, 2]", values='{"phone": "+52 555 1234"}', profile="prod")
unlink — Delete Records| Parameter | Type | Default | Description |
|---|---|---|---|
model |
string | (required) | Model name |
ids |
string | (required) | Record IDs as JSON array or comma-separated |
profile |
string | (default) | Target profile name |
unlink(model="res.partner", ids="[10, 11, 12]", profile="prod")
create — Create Records| Parameter | Type | Default | Description |
|---|---|---|---|
model |
string | (required) | Model name |
values |
string | (required) | Field values as JSON object |
profile |
string | (default) | Target profile name |
create(model="res.partner", values='{"name": "Alice", "email": "[email protected]"}', profile="prod")
export_records — Native ExportExport via Odoo's export_data. Returns a pagination envelope with an array of
dicts — ideal for retrieving External IDs.
| Parameter | Type | Default | Description |
|---|---|---|---|
model |
string | (required) | Model name |
domain |
string | [] |
Search domain |
fields |
string | id,name |
Comma-separated field names |
limit |
int | 500 |
Max records to export |
offset |
int | 0 |
Records to skip (pagination) |
profile |
string | (default) | Target profile name |
export_records(model="res.partner", domain="[('active', '=', True)]", fields="id,name,country_id/id")
Tip: Use
field_id/idsyntax to export External IDs of relational fields.
Checkhas_morein the response to know if more pages exist.
import_records — Native Import (Bulk)Import via Odoo's load. Updates records with matching External IDs; creates new ones otherwise.
| Parameter | Type | Default | Description |
|---|---|---|---|
model |
string | (required) | Model name |
fields |
string | (required) | Comma-separated field names |
rows |
string | (required) | JSON array of dicts with data |
profile |
string | (default) | Target profile name |
import_records(model="res.partner", fields="id,name,phone", rows='[{"id": "base.res_partner_1", "name": "Updated", "phone": "12345"}, {"name": "New Partner", "phone": "67890"}]')
execute_kw — Execute Any Method| Parameter | Type | Default | Description |
|---|---|---|---|
model |
string | (required) | Model name |
method |
string | (required) | Method to execute |
args |
string | [] |
Positional arguments as JSON array |
kwargs |
string | {} |
Keyword arguments as JSON object |
profile |
string | (default) | Target profile name |
execute_kw(model="sale.order", method="action_confirm", args="[[42]]")
list_models — Discover Modelslist_models(search="partner", profile="prod")
list_fields — Inspect Model Schemalist_fields(model="account.move", profile="prod")
get_version — Server Versionget_version(profile="prod")
| Natural Language | Odoo Domain |
|---|---|
| name contains "Juan" | [('name', 'ilike', 'Juan')] |
| state is "sale" | [('state', '=', 'sale')] |
| amount > 1000 | [('amount_total', '>', 1000)] |
| date after 2024-01-01 | [('create_date', '>=', '2024-01-01')] |
| country is Mexico or USA | ['|', ('country_id.code', '=', 'MX'), ('country_id.code', '=', 'US')] |
| Operator | Meaning |
|---|---|
= |
Equal |
!= |
Not equal |
ilike |
Case-insensitive contains |
>, <, >=, <= |
Comparison |
in |
Value in list |
not in |
Value not in list |
User: "Find all contacts with 'John' in their name"
Action:
search_read(model="res.partner", domain="[('name', 'ilike', 'John')]", fields="name,email,phone")
User: "Export all active products from staging and import them to prod"
Action:
# Step 1: Export from staging
export_records(model="product.template", domain="[('active', '=', True)]", fields="id,name,list_price", profile="staging")
# Step 2: Import to prod (use the exported rows as input)
import_records(model="product.template", fields="id,name,list_price", rows="[...]", profile="prod")
All tools return JSON. On error, the response contains an error key:
{"error": "Profile 'staging' not found."}
| Error | Cause | Fix |
|---|---|---|
| Profile not found | Invalid profile name | Run list_available_profiles |
| Connection refused | Odoo server down or wrong URL | Check profile URL and port |
| Model not found | Typo in model name | Use list_models to discover |
| Access denied | Wrong credentials or permissions | Verify profile credentials |