Odoo 20's Built-in MCP Server: What It Exposes
Odoo 20 ships a native MCP server at /mcp. Which tools it exposes by default, how MCP-scoped keys and OAuth work, and where it stops being a general data API.
ODXProxy Team · Oct 1, 2026 · 7 min read

Until this release, connecting an AI agent to Odoo meant building or installing a separate Model
Context Protocol server that translated tool calls into Odoo API calls. Odoo 20 ships one. The new
Odoo 20 MCP server lives in the Enterprise addon ai_mcp, answers at POST /mcp, and lets
clients such as Claude, ChatGPT or Claude Code query the ERP directly. This guide reads the 20.0
source to answer what developers need to know: which tools it exposes out of the box, how
authentication works, how to add your own tools, and what it is not designed to do.
Where it comes from
ai_mcp is an Enterprise module (licence OEEL-1). It depends on ai and is marked
auto_install, so it arrives as soon as the ai app is installed. There is no Community
equivalent in 20.0.
The endpoint is declared in odoo/addons/ai_mcp/controllers/mcp_controller.py:
@http.route('/mcp', type='http', auth='bearer', bearer_scope='mcp', csrf=False, methods=['POST'])
def handle_mcp_request(self):
...It speaks JSON-RPC 2.0 over streamable HTTP, and the dispatcher
(models/ai_mcp_request_dispatcher.py) handles exactly four methods: initialize, ping,
tools/list and tools/call. Anything else returns "method not found". A notification (a request
without an id) gets an empty 202 response, as the MCP transport spec requires. There are no MCP
resources or prompts; it is a tools-only server.
Authentication: MCP-scoped keys and OAuth
bearer_scope='mcp' is the important part. In Odoo 20 every API key has exactly one scope, and the
credential check matches it exactly. A key created for the JSON-2 or XML-RPC API (scope rpc) is
rejected at /mcp, and an MCP key is rejected everywhere else. Part 2 of this
series explains the scope change in detail.
There are two ways to get an MCP credential:
- An API key with scope MCP. The module's own instructions: open your user profile's security tab, generate an API key, and set its scope to MCP. Paste it into your MCP client's password or credential field, never a plain text field.
- OAuth.
ai_mcpincludes an OAuth authorization server (oauth.client,oauth.authorization_code). It issues bearer tokens withscope: mcp. Client metadata URLs are checked against an allow-list that ships pre-seeded with four entries:
https://chatgpt.com/oauth/client.json
https://claude.ai/oauth/mcp-oauth-client-metadata
https://claude.ai/oauth/claude-code-client-metadata
https://grok.com/oauth/mcp-client.jsonDynamic client registration is off by default (enable_dcr is False). Both values are system
parameters an administrator can change.
The tools it exposes by default
Tools are not hard-coded. An MCP tool is a server action (ir.actions.server) that is an AI-tool
candidate and has the new use_in_mcp flag set. Out of the box, five actions are flagged, all
marked read-only:
| Tool name | What it does |
|---|---|
ai_tool_get_models | Lists non-transient, non-abstract models the current user can access |
ai_tool_get_fields | Describes a model's fields |
ai_tool_search | Searches records with a domain; returns records and total_count |
ai_tool_read_group | Groups and aggregates (sums, counts, averages) |
ai_tool_mcp_retrieve_initial_context | Returns the current user, timezone and companies; the client is told to call it first |
So a default install can answer questions across the data model, within the user's access rights,
but cannot write. ai_tool_search checks that the user has read access to the model before it
runs. By default it fetches only display_name, returns 50 rows (at most 200 per call, with a
pagination hint), and replaces binary fields with references instead of inlining file content.
A tools/list call over curl:
curl -s -X POST https://erp.example.com/mcp \
-H "Authorization: bearer $ODOO_MCP_KEY" \
-H "Content-Type: application/json" \
-d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}}'Each tool comes back in the standard MCP shape, built in _mcp_build_tool_definition:
{
"name": "ai_tool_search",
"title": "ai_tool_search",
"description": "Search for records and their values in the database. ...",
"inputSchema": {
"type": "object",
"properties": {
"model_name": { "type": "string" },
"domain": { "type": "string" },
"fields": { "type": "array", "items": { "type": "string" } },
"offset": { "type": "number" },
"limit": { "type": "number" },
"order": { "type": "string" }
},
"required": ["model_name", "domain"]
},
"annotations": {
"title": "ai_tool_search",
"readOnlyHint": true,
"destructiveHint": false
},
"execution": { "taskSupport": "forbidden" }
}Note that domain is a string in this schema, not a JSON array. The model writes an Odoo domain
as text and the tool parses it. Calling the tool:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "ai_tool_search",
"arguments": {
"model_name": "sale.order",
"domain": "[('state', '=', 'sale')]",
"fields": ["name", "partner_id", "amount_total"],
"limit": 5
}
}
}The result is returned as one text content item holding JSON, with isError: false.
Adding your own tools
To let an agent do something, you create a server action, typically a Python code action on the
target model, describe it as an AI tool (name, description, JSON input schema), and tick Available
in MCP. A constraint refuses use_in_mcp on an action that is not a valid AI-tool candidate.
Before tools/call runs a tool, it checks _can_execute_action_on_records for the calling user on
the action's model. Two design points follow from the source:
is_readonlyis a declaration, not a guarantee. It is a plain boolean you set on the action, and it only becomesreadOnlyHint/destructiveHintin the tool listing. Odoo does not inspect the action's code to confirm it. A mis-flagged write action will be advertised as read-only, and MCP clients use that hint to decide whether to ask the user before calling it.- The tool is the unit of control. You don't grant an agent "write access to
sale.order". You grant it "Confirm quotation" and nothing else. That is a good fit for agents, which benefit from a small number of well-described, bounded actions.
What it is, and what it is not
The native server is built for an AI client asking the ERP questions, and performing actions you curated in advance. It is not a general data API, and it does not replace one:
- There is no generic
create,writeorunlinktool. Writes exist only as actions you build. - Results are shaped for a model to read (text content, capped page sizes, binary references), not for an app to bind to.
- Keys are per user and per database. One MCP connection reaches one Odoo database.
- It requires Enterprise, the
aiapp, and 20.0. Community and older databases don't have it.
For those cases (a mobile app, a reporting job, a bot that needs typed CRUD), you still want the
External API, which in 20.0 means JSON-2 or the deprecated classic endpoints
(part 3). Many teams put a gateway in front of it.
ODXProxy is one: a fixed allow-list of nine actions, a proxy key that
is separate from the Odoo user's key, per-request instance selection, and Prometheus metrics at
/_/metrics. The two are complementary. Odoo's MCP server exposes hand-picked actions to AI
clients; a proxy gives ordinary apps controlled access to data. If you need an agent on a database
without the native server, an MCP server built on the External API, such as
odxproxy-mcpserver, fills that gap.
A checklist before you connect an agent
- Create a dedicated Odoo user for each agent, with only the groups it needs. The default search tool can read anything that user can read.
- Generate an MCP-scoped key for that user, or connect over OAuth from an allow-listed client.
- Review the five default tools. Untick
use_in_mcpon any you don't want exposed. - Build write actions as narrow server actions, and set
is_readonlyhonestly. - Test with
tools/listandtools/callover curl before you connect a real client. - Watch the server log. The controller logs every MCP request at INFO level: the method and tool name, elapsed time, success or error, and the client address.
The rest of the series
- Part 1: Odoo 19 vs Odoo 20: what changed for developers
- Part 2: Odoo 20 API changes that break integrations silently
- Part 3: Odoo XML-RPC is deprecated: migrating to the JSON-2 API
- Part 5: Migrating custom modules to Odoo 20: the silent breaks
To give an LLM Odoo tools through your own code instead, see Odoo function calling. The proxy's request contract is in the ODXProxy API reference.