odoo-mcp-server
Connects to an Odoo database (17/18/19) over its external XML-RPC API using a login and API key, exposing tools to discover models and fields, search/count/read records with domains, paging and ordering, and optionally create/update/delete records when writes are explicitly enabled. Includes a built-in safety layer with read-only defaults, model allow/deny lists, a non-disablable denylist of sensitive models, relation-path and argument validation, and result-size limits, while Odoo's own ACLs and record rules still apply to every call.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@odoo-mcp-servershow me US companies that haven't ordered this quarter"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
odoo-mcp-server
Let Claude (or any MCP client) talk to your Odoo database, read-only by default and guarded by a safety policy.
odoo-mcp-server is a Model Context Protocol server that
exposes an Odoo database to AI assistants as a small set of well-documented tools. You can
ask "Which US companies haven't ordered this quarter?" and the assistant finds the models,
reads their fields, builds the domain and pages through the results without any custom
integration code.
It connects with a login + API key over Odoo's external API. Every call runs as that user, so Odoo's own access rights and record rules still apply. On top of those, the server adds its own safety layer, which is the main point of this project.
Features
Discovery tools:
list_modelsfinds models anddescribe_modelreturns fields, types, required/readonly flags, relations, selection values, and what the user may do.Query tools:
search_records(domain, fields, limit, offset, order, paging hints),read_record,count_records.Opt-in write tools:
create_record,update_record,delete_record. They are only registered when enabled, so in read-only mode the assistant never sees them.Model-friendly output: many2one becomes
{"id", "name"}, empty values becomenullinstead of Odoo'sFalse, and binary fields are replaced by a size marker so they don't fill the context window.Model-friendly input: a many2one accepts
7or{"id": 7}, an x2many accepts a plain list of ids (or Odoo commands),nullclears a field, and a domain can be a list or a JSON string.Actionable errors: "Field 'emial' does not exist on 'res.partner'. Did you mean: email?" Odoo tracebacks are reduced to a single readable line.
Resources and prompts:
odoo://server-info,odoo://models/{model}, plus thesales_summaryandexplore_modelprompt templates.Operational extras: a
--checkconnection test, stdio and streamable-HTTP transports, request timeouts, thread-safe client, and afields_getcache.
Related MCP server: Odoo MCP Server
The safety model
Layer | Default | How to change |
Read-only mode | Only the five read tools exist |
|
Deletes | Disabled even when writes are on, including indirect deletes through x2many commands (see below) |
|
Built-in denylist | API keys, | Cannot be disabled |
Your denylist | none |
|
Allowlist | all models (except denied) |
|
Result size | 50 per page, hard cap 200 |
|
Batch size | max 100 ids per update/delete | n/a |
Relation guard | Dotted domain paths ( | n/a |
Argument validation | domains (shape, operators, field names on every hop, prefix-notation arity), field lists, | n/a |
Odoo permissions | Every call runs as the API-key user | Use a dedicated, least-privilege Odoo user |
Updates and deletes first check that all ids exist and are accessible. If any are missing, nothing is changed.
x2many writes and deletes. In Odoo, removing lines from a one2many on an existing record
(commands 3, 5, 6, or a plain list of ids, which becomes 6) deletes those lines when
the inverse field is ondelete="cascade", as it is for order lines. Command 2 always
deletes. Because of that, these commands need ODOO_MCP_ALLOW_DELETE=1 as well; in
write-only mode the assistant can still add lines ([[0, 0, {...}]]) or link them
([[4, id]]), and many2many sets ([1, 2]) work as usual.
Tip: create a dedicated Odoo user for the assistant (e.g.
assistant@yourcompany.com) with only the groups it needs, and generate the API key for that user. The server's policy and Odoo's ACLs then work together.
Architecture
flowchart LR
A["MCP client<br/>(Claude Desktop, Claude Code, ...)"] -- "stdio / HTTP<br/>JSON-RPC" --> B
subgraph B["odoo-mcp-server"]
direction TB
T["server.py<br/>tools · resources · prompts"] --> S["service.py<br/>OdooService"]
S --> P["safety.py<br/>SafetyPolicy"]
S --> Z["serialize.py<br/>JSON-safe in/out"]
S --> C["client.py<br/>OdooClient (XML-RPC)"]
end
C -- "/xmlrpc/2/common<br/>/xmlrpc/2/object<br/>login + API key" --> O[("Odoo 17/18/19<br/>ACLs + record rules")]A typical search_records call:
sequenceDiagram
participant M as AI model
participant S as odoo-mcp-server
participant O as Odoo
M->>S: search_records(model, domain, fields, limit)
S->>S: model allowed? (built-in denylist, deny/allow lists)
S->>O: fields_get (cached per model)
S->>S: validate domain, fields, order; check relation paths; clamp limit
S->>O: search_read + search_count (as the API-key user)
O-->>S: rows (Odoo ACLs and record rules applied)
S-->>M: {total, records, next_offset} (many2one → {id, name}, False → null)Module | Responsibility |
| Parses and validates environment variables into an immutable |
| Thread-safe XML-RPC client: lazy auth, timeouts, fault → readable error |
| Model policy, write gates, limits, domain/field/order validation |
| Odoo wire format ↔ JSON-friendly values |
| SDK-independent operations (unit-tested without MCP) |
| MCP tools/resources/prompts and the CLI |
Quick start
1. Install
Requires Python 3.10+.
# Recommended: an isolated install with pipx
pipx install git+https://github.com/Pirainuthal/odoo-mcp-server.git
# or with uv
uv tool install git+https://github.com/Pirainuthal/odoo-mcp-server.git
# or from a clone
git clone https://github.com/Pirainuthal/odoo-mcp-server.git
cd odoo-mcp-server
python -m venv .venv && . .venv/bin/activate
pip install -e .2. Create an Odoo API key
In Odoo: avatar (top-right) → My Profile / Preferences → Account Security → New API Key.
Name it, copy the key, and keep it secret. On Odoo 19 the rpc module must be installed;
it is auto-installed on standard databases.
3. Check the connection
export ODOO_URL=https://mycompany.odoo.com
export ODOO_DB=mycompany
export ODOO_USERNAME=assistant@mycompany.com
export ODOO_API_KEY=xxxxxxxxxxxxxxxx
odoo-mcp-server --check{
"url": "https://mycompany.odoo.com",
"database": "mycompany",
"username": "assistant@mycompany.com",
"uid": 2,
"server_version": "19.0",
"mode": "read-only",
"allowed_models": "all (except denied)",
"default_limit": 50,
"max_limit": 200,
"always_blocked_models": ["res.users.apikeys", "ir.config_parameter", "..."]
}4. Connect your AI assistant
Claude Code
claude mcp add odoo \
-e ODOO_URL=https://mycompany.odoo.com \
-e ODOO_DB=mycompany \
-e ODOO_USERNAME=assistant@mycompany.com \
-e ODOO_API_KEY=xxxxxxxxxxxxxxxx \
-- odoo-mcp-serverAdd --scope user to make it available in every project, then run /mcp inside Claude
Code to confirm that the odoo server is connected.
Claude Desktop: edit claude_desktop_config.json (Settings → Developer → Edit Config):
{
"mcpServers": {
"odoo": {
"command": "odoo-mcp-server",
"env": {
"ODOO_URL": "https://mycompany.odoo.com",
"ODOO_DB": "mycompany",
"ODOO_USERNAME": "assistant@mycompany.com",
"ODOO_API_KEY": "xxxxxxxxxxxxxxxx",
"ODOO_MCP_MODELS": "res.partner,sale.*,product.*,account.move"
}
}
}
}If odoo-mcp-server isn't on the PATH Claude Desktop sees, use the absolute path (for example
/home/you/.local/bin/odoo-mcp-server). More examples are in examples/.
Any other MCP client: run odoo-mcp-server as a stdio server. For HTTP clients:
odoo-mcp-server --transport streamable-http --host 127.0.0.1 --port 8000
# MCP endpoint: http://127.0.0.1:8000/mcpThe HTTP transport has no authentication of its own: anyone who can reach it acts as the configured Odoo user. Keep it on localhost or put it behind an authenticating proxy.
Configuration
Variable | Required | Default | Description |
| yes | Base URL, e.g. | |
| yes | Database name | |
| yes | Login of the API-key user | |
| yes | API key (a password also works, but isn't recommended) | |
|
|
| |
|
|
| |
| (all) | Comma-separated allowlist; globs allowed ( | |
| (none) | Extra denylist on top of the built-in one | |
|
| Page size when the model doesn't pass | |
|
| Hard cap on | |
|
| XML-RPC timeout in seconds |
A template is in .env.example. The server reads the process environment;
it doesn't load .env files itself.
Tools, resources and prompts
Tool | Mode | Purpose |
| read | Find models by technical name or label |
| read | Fields (required first), relations, selections, user's access rights |
| read | Paged search; returns |
| read | One record (archived ones included) |
| read | Fast count |
| write | Returns |
| write | All-or-nothing existence check, max 100 ids |
| delete | Returns the deleted records' names |
When fields is omitted, search_records returns a compact projection (display_name plus
common fields such as name, state, partner_id, amount_total, and required fields).
Pass ["*"] to get every non-binary field.
Resource / prompt | Description |
| Version, database, user, mode and limits (never the key) |
| The |
| Guided, read-only sales report for a period |
| Guided tour of a model: purpose, key fields, sample rows |
Example conversations
The traces below were produced against an Odoo 19 database with demo data.
"Which US companies do we have, and what are their emails?"
→ search_records(model="res.partner",
domain=[["is_company","=",true],["country_id.code","=","US"]],
fields=["name","country_id","email"], limit=3, order="name")
← {"total": 7, "returned": 3, "next_offset": 3,
"records": [
{"id": 9, "name": "Acme Corporation", "country_id": {"id": 233, "name": "United States"},
"email": "acme_corp@yourcompany.example.com"},
{"id": 15, "name": "Azure Interior", "country_id": {"id": 233, "name": "United States"}, ...},
{"id": 16, "name": "Lumber Inc", ...}]}
Claude: There are 7 US companies. The first three alphabetically are Acme Corporation,
Azure Interior and Lumber Inc. Want me to fetch the remaining four?"How many contacts have a child contact with an example.com email?"
→ count_records(model="res.partner",
domain=[["child_ids","any",[["email","ilike","example"]]]])
← {"model": "res.partner", "count": 8}Typos are caught before Odoo sees them
→ search_records(model="res.partner", fields=["emial"])
← error: Field 'emial' does not exist on 'res.partner'. Did you mean: email?Sensitive models are off-limits regardless of who asks
→ search_records(model="res.users.apikeys")
← error: Model 'res.users.apikeys' is security-sensitive and always blocked by this server.Odoo's permissions still apply (here as a non-admin user, with writes enabled)
→ create_record(model="res.country", values={"name": "Atlantis", "code": "XA"})
← error: You are not allowed to create 'Country' (res.country) records.
This operation is allowed for the following groups: - Role / AdministratorRead-only by default: with the default configuration, tools/list returns only the five
read tools, so a request like "rename Acme to Acme Inc" has no tool to act on. The
server's instructions tell the assistant that write tools appear only when they're enabled.
Testing
python -m venv .venv && . .venv/bin/activate
pip install -e ".[dev]"
pytest # unit tests, no Odoo needed
ruff check src tests && ruff format --check src testsThe unit tests use an in-memory fake Odoo (tests/conftest.py) that implements the
XML-RPC common/object services (domain evaluation, fields_get, search_read, create,
write, unlink, access checks and faults). They cover configuration, every safety rule,
serialization in both directions, fault translation, and the MCP layer end to end through an
in-process MCP client (tool listing, schemas, annotations, error results, prompts and
resources).
Live smoke test (optional, against a real server; use a test database):
export ODOO_URL=http://localhost:8069 ODOO_DB=test ODOO_USERNAME=admin ODOO_API_KEY=...
ODOO_MCP_LIVE_TEST=1 pytest tests/test_live.py # read-only checks
ODOO_MCP_LIVE_TEST=1 ODOO_MCP_LIVE_WRITE=1 \
ODOO_MCP_ALLOW_WRITE=1 ODOO_MCP_ALLOW_DELETE=1 pytest tests/test_live.py # + create/update/delete a temp partnerCompatibility notes
Odoo 17, 18, 19 are supported.
describe_modeluseshas_access(18+) and falls back tocheck_access_rightson 17.Odoo 19 marks
/xmlrpc/2as deprecated, with removal planned for Odoo 22, and logs a warning per call. You can mute it with--log-handler odoo.addons.rpc.controllers.xmlrpc:ERROR. A JSON-2 transport is on the roadmap.Built on MCP Python SDK 2.x (
MCPServer, the successor ofFastMCP).
Roadmap
JSON-2 API transport (
/json/2/<model>/<method>, bearer API key) for Odoo 19+read_group/ aggregation tool for reports without fetching rowsPer-model field denylist (e.g. hide
hr.employeeprivate fields)Optional confirmation step for writes through MCP elicitation
Audit log of write operations
Publish to PyPI so
uvx odoo-mcp-serverworks
Contributing
Issues and pull requests are welcome. Please run ruff format, ruff check and pytest
before opening a PR; CI runs the same checks on Python 3.10–3.13.
License
MIT © 2026 Pirainuthal S
Available Tools
5 toolscount_recordsCount Odoo recordsARead-only
Count the records matching a domain (cheap; no data is transferred).
| Name | Required | Description | Default |
|---|---|---|---|
| model | Yes | Technical model name, e.g. 'res.partner', 'sale.order', 'account.move'. | |
| domain | No | Odoo domain: a list of [field, operator, value] conditions, optionally combined with '&', '|', '!' in prefix notation. Conditions are AND-ed by default. Example: [["customer_rank", ">", 0], ["country_id.code", "=", "IN"]]. Omit or pass [] to match all records. |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true and openWorldHint=true, so the safety profile is covered. The description adds a genuine behavioral trait beyond them — that the call is cheap and transfers no record data — but says nothing about permissions, errors on invalid model names, or how an omitted domain affects results.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
A single front-loaded sentence with zero waste; the key verb and resource come first and the efficiency note is a compact qualifier. Nothing could be removed without losing information.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists and the parameters are fully documented, so return values and input format needn't be explained. The remaining gap is minor: no note on what happens with an unknown model or which models are accessible, but an agent has what it needs to call it correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, with both 'model' and 'domain' fully documented including operator syntax and the empty-domain behavior. The description mentions the domain concept but adds no syntax or format detail beyond the schema, so the baseline 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description gives a specific verb ('Count') and resource ('records matching a domain'), which is clearly distinguishable from search_records or read_record by the counting semantics. It does not explicitly name a sibling as the alternative, so it stops short of a 5.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The parenthetical '(cheap; no data is transferred)' implies you should reach for this when you only need a total rather than the records themselves, but the description never says 'use this instead of search_records when...'. Usage must be inferred from the efficiency hint rather than stated.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
describe_modelDescribe an Odoo modelARead-only
Describe a model's fields (type, label, required, readonly, relation, selection values) and what the current user is allowed to do with it.
Required fields are listed first.
| Name | Required | Description | Default |
|---|---|---|---|
| model | Yes | Technical model name, e.g. 'res.partner', 'sale.order', 'account.move'. | |
| field_query | No | Only show fields whose name or label contains this text. | |
| include_technical | No | Include chatter/activity/internal fields (message_*, activity_*). |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true and openWorldHint=true, so safety is covered. The description adds valuable context beyond annotations: it discloses which field attributes are returned (type, label, required, readonly, relation, selection values), that permissions for the current user are included, and that required fields are ordered first. It does not describe response size, performance, or error behavior, but the output schema covers return shape.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two short sentences with no wasted language. The key purpose and returned information are front-loaded, and the ordering detail is placed efficiently after the main description.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given a rich input schema with 100% description coverage, explicit read-only/open-world annotations, and a present output schema, the description is nearly complete for an agent to call it correctly. The main gap is guidance on when to choose this tool over siblings, though that is more of a usage concern.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so all three parameters are already well documented in the input schema. The description does not add parameter-specific semantics such as how field_query filters fields or when include_technical should be used, so baseline 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description uses a specific verb ('Describe') and resource ('a model's fields') and enumerates the returned field attributes and permission information. It clearly distinguishes schema introspection from sibling tools like list_models (which lists models) and read_record (which fetches records).
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description implies usage for inspecting a model before querying or reading records, but it does not explicitly state when to use this tool versus list_models, search_records, or read_record. No exclusions or alternative-routing guidance is provided.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_modelsList Odoo modelsARead-only
List the Odoo models this server can access, optionally filtered by name.
Models blocked by the safety policy are never listed.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | Maximum models to return. | |
| query | No | Case-insensitive filter on technical name or label, e.g. 'sale'. | |
| include_transient | No | Include transient (wizard) models. |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint and openWorldHint, so safety of the call is known. The description adds non-obvious context: models blocked by the safety policy are never listed, which explains why a model the agent expects may be absent. Pagination/limit behavior is left to the schema.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two short sentences, zero waste. The core purpose is front-loaded and the scope caveat follows immediately.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With annotations covering read-only semantics and an output schema present, the description only needs to establish scope and any surprising behavior — which it does via the access scope and safety-policy exclusion. Only minor gap is that limit/pagination behavior is not mentioned.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so each parameter is already fully documented, including that the query filters on technical name or label. The description only restates that filtering is by name, adding nothing beyond the schema. Baseline 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource: 'List the Odoo models this server can access.' The scoping phrase 'this server can access' plus the safety-policy note makes the domain clear. It does not explicitly contrast with siblings like describe_model, but the resource (models vs records) already separates it.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Usage is only implied ('optionally filtered by name'). There is no statement of when to prefer this over describe_model or search_records, and no prerequisites or exclusions beyond the safety-policy note.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
read_recordRead one Odoo recordBRead-only
Read a single record by id (all non-binary fields unless fields is given).
| Name | Required | Description | Default |
|---|---|---|---|
| model | Yes | Technical model name, e.g. 'res.partner', 'sale.order', 'account.move'. | |
| fields | No | Field names to return. Omit for a compact default set; pass ['*'] for all non-binary fields. Use describe_model to discover field names. | |
| record_id | Yes | The record id. |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint and openWorldHint, so safety is covered. The description's one behavioral claim — that omitting `fields` returns "all non-binary fields" — conflicts with the schema, which says omitting returns a compact default set and ['*'] is needed for all non-binary fields. That misleading default is a real defect for an agent deciding what to pass.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
One short sentence with the core operation front-loaded and no filler. It is arguably too terse given the tool's scope, but nothing in it is wasted.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return values need no explanation, and the read-only profile is covered by annotations. Still, the field-default discrepancy leaves an agent unsure what a bare call actually returns, which is the one thing this description should have settled.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the schema already explains model, record_id and the fields wildcard/default behavior in detail. The description only restates the `fields` behavior, and restates it inaccurately, adding no correct meaning beyond the schema.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ("Read a single record by id") and the id-based lookup implicitly separates it from search_records. It never names a sibling tool, so the differentiation is inferred rather than stated.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
"by id" implies you must already know the record id, which is a usable usage cue, but there is no explicit when-to-use versus search_records/count_records. The only routing hint ("Use describe_model...") lives in the schema, not the description.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
search_recordsSearch Odoo recordsARead-only
Search records matching a domain and return selected fields.
Returns the total number of matches, the current page of records and,
when more records exist, next_offset for the following page.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | Page size (default 50, max 200). | |
| model | Yes | Technical model name, e.g. 'res.partner', 'sale.order', 'account.move'. | |
| order | No | Sort clause, e.g. 'date_order desc, id'. | |
| domain | No | Odoo domain: a list of [field, operator, value] conditions, optionally combined with '&', '|', '!' in prefix notation. Conditions are AND-ed by default. Example: [["customer_rank", ">", 0], ["country_id.code", "=", "IN"]]. Omit or pass [] to match all records. | |
| fields | No | Field names to return. Omit for a compact default set; pass ['*'] for all non-binary fields. Use describe_model to discover field names. | |
| offset | No | Records to skip, for paging. |
Output Schema
| Name | Required | Description |
|---|---|---|
No output parameters | ||
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true and openWorldHint=true, so safety is covered, and the description goes further by disclosing the pagination contract: total match count, current page, and next_offset only when more records exist. That is genuinely useful behavioral context beyond the annotations, though it says nothing about cost, result caps beyond the schema's limit, or access restrictions.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two short sentences with the core action front-loaded and the return contract following. No filler, though the sentence break mid-thought and the absent sibling guidance mean it is efficient rather than exemplary.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists and the input schema is fully documented, so the description does not need to restate parameters or return shape, and it correctly focuses on the paging behavior. What is missing is the disambiguation from read_record and count_records, which is the one thing the structured fields cannot supply.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100% and the domain, fields, order, limit, and offset parameters are all documented in detail in the schema, so the schema carries the load. The description adds no parameter-level meaning (no default limit mention, no domain syntax note), so the baseline 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Names a specific verb (search) and resource (records) plus the two things it does: filter by a domain and project a field set. It is clear on its own, but nothing in the text distinguishes it from read_record or count_records, so an agent must infer the boundary from the names alone.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Usage is only implied: the mention of a domain and paging suggests bulk/filtered retrieval, but there is no explicit 'use this instead of count_records when you need the rows' or 'use read_record for a single known ID'. With three closely related siblings, the guidance an agent actually needs is left to inference.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
5 tool updates
v0.1.0- First observed
count_records - First observed
describe_model - First observed
list_models - First observed
read_record - First observed
search_records
TDQS
Scored across 5 tools
Each tool has a clearly distinct role: model discovery, schema description, multi-record search, single-record read, and cheap counting. Although count_records overlaps slightly with search_records' total-match behavior, the explicit cost distinction makes the boundary clear.
All tool names follow the same snake_case verb_noun convention: list_models, describe_model, search_records, read_record, count_records. Pluralization is also used predictably for collection vs. single-record operations.
Five tools is a compact, well-scoped set for read-only Odoo data access. Each tool earns its place: discovery, schema introspection, search, single-record read, and count are all non-redundant primitives.
The read-only lifecycle is well covered: model listing, field descriptions, paginated search, single-record read, and counting. Write operations are absent, but the safety-policy wording suggests this is intentional; minor gaps include no binary/attachment read or aggregate/group-by support.
Maintenance
Related MCP Connectors
Odoo MCP connector for Claude & ChatGPT: ask sales, invoices, stock in plain words. Read-only.
Odoo virtual employee via MCP: query, create, modify records, workflows, PDFs, and activities.
Read, write and run Odoo actions from any AI app, as each user, with approval rules per colleague.
- dataOAuthco.thinair
PostgreSQL, MySQL, and SQL Server in one session. 26 read-only MCP tools for AI agents.
Related MCP Servers
- AlicenseAqualityDmaintenanceEnables AI assistants to interact with Odoo data using natural language to search, read, create, and update records. It acts as a secure bridge between MCP clients and Odoo instances version 17.0 through 19.0.11Apache 2.0
- AlicenseNot gradedqualityDmaintenanceProvides read-only access to Odoo databases via OdooRPC, allowing AI assistants to query and analyze Odoo data through the MCP protocol.1MIT
- AlicenseNot gradedqualityDmaintenanceA read-only MCP server that enables AI assistants to query Odoo instances via XML-RPC, supporting search, read, count, and field inspection without requiring custom modules.68 PyPI1Mozilla Public 2.0
- AlicenseNot gradedqualityDmaintenanceAn MCP server that enables AI assistants to safely query live Odoo 19 data with a defense-in-depth security layer, providing read-only tools for partners, sales, inventory, and finance.MIT