@saketsawrav/odoo-mcp-server
v0.5.4
Published
MCP server for Odoo ERP — project management, tasks, and comments via XML-RPC
Downloads
321
Maintainers
Readme
odoo-mcp-server
MCP server that exposes Odoo ERP project management operations as tools via XML-RPC. Connects any MCP-compatible client (Claude Code, Claude Desktop, etc.) to any Odoo instance for managing projects, tasks, comments, activities, and users.
Overview
This server implements the Model Context Protocol (MCP) over STDIO transport, providing 25 tools across 6 categories (22 base + 3 conditional). It communicates with Odoo using XML-RPC, the standard external API for Odoo.
The server is version-agnostic -- it dynamically resolves field names at startup to handle differences between Odoo versions (e.g., planned_hours in Odoo 16 vs allocated_hours in Odoo 18). Relationship fields (many2one, many2many) are automatically resolved to human-readable names using batch queries.
Prerequisites
- Node.js >= 18 (or Bun)
- An Odoo instance with XML-RPC enabled (most Odoo instances have this by default)
- An API key (or password) for an Odoo user with appropriate permissions
- The database name for your Odoo instance (cannot be auto-discovered)
Generating an Odoo API Key
- Log into your Odoo instance
- Go to Settings > Users & Companies > Users
- Select your user
- Go to the "Account Security" tab (or "Preferences" in older versions)
- Under "API Keys", click "New API Key"
- Give it a description and copy the generated key
Installation
Clone and build from source:
git clone <repository-url>
cd odoo-mcp-server
bun install
bun run buildConfiguration
Environment Variables
| Variable | Required | Description |
|----------|----------|-------------|
| ODOO_URL | Yes | Odoo instance URL (e.g., https://mycompany.odoo.com) |
| ODOO_DB | Yes | Database name (cannot be auto-discovered via API) |
| ODOO_USERNAME | Yes | Login username or email |
| ODOO_API_KEY | Yes | API key or password |
Create a .env file (see .env.example):
ODOO_URL=https://mycompany.odoo.com
ODOO_DB=mycompany
[email protected]
ODOO_API_KEY=your_api_key_hereUsage
With Claude Code (.mcp.json)
Add to your project's .mcp.json:
{
"mcpServers": {
"odoo": {
"type": "stdio",
"command": "bun",
"args": ["run", "--cwd", "/path/to/odoo-mcp-server", "src/index.ts"],
"env": {
"ODOO_URL": "https://mycompany.odoo.com",
"ODOO_DB": "mycompany",
"ODOO_USERNAME": "[email protected]",
"ODOO_API_KEY": "your_api_key_here"
}
}
}
}With Claude Code (CLI)
Alternatively, use the claude mcp add command:
claude mcp add --transport stdio \
--env ODOO_URL=https://mycompany.odoo.com \
--env ODOO_DB=mycompany \
--env [email protected] \
--env ODOO_API_KEY=your_api_key_here \
odoo -- bun run --cwd /path/to/odoo-mcp-server src/index.tsBy default this saves to your local scope. Use --scope project to save to .mcp.json (team-shared) or --scope user to save to ~/.claude.json (available across all projects).
With Codex CLI
Add to ~/.codex/config.toml (global) or .codex/config.toml (project-scoped):
[mcp_servers.odoo]
command = "bun"
args = ["run", "--cwd", "/path/to/odoo-mcp-server", "src/index.ts"]
env = { "ODOO_URL" = "https://mycompany.odoo.com", "ODOO_DB" = "mycompany", "ODOO_USERNAME" = "[email protected]", "ODOO_API_KEY" = "your_api_key_here" }
startup_timeout_sec = 15.0Restart Codex after changing the config. Codex supports STDIO transport only for local MCP servers.
With Claude Desktop
Add to your Claude Desktop configuration (claude_desktop_config.json):
{
"mcpServers": {
"odoo": {
"command": "bun",
"args": ["run", "--cwd", "/path/to/odoo-mcp-server", "src/index.ts"],
"env": {
"ODOO_URL": "https://mycompany.odoo.com",
"ODOO_DB": "mycompany",
"ODOO_USERNAME": "[email protected]",
"ODOO_API_KEY": "your_api_key_here"
}
}
}
}Standalone
# Development (with hot reload)
bun run dev
# Production
bun run build
node dist/index.jsTools Reference
Projects (4 tools)
list_projects
List all accessible projects.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| limit | number | No | Max results (default: 50) |
Returns: Array of projects with id, name, task_count, user_id, date_start.
get_project
Get detailed information about a specific project.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| project_id | number | Yes | The Odoo project ID |
Returns: Project object with id, name, description, task_count, user_id, partner_id, date_start, date, type_ids.
get_stages
List all stages (kanban columns) for a project.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| project_id | number | Yes | The Odoo project ID |
Returns: Array of stages with id, name, sequence, fold. Ordered by sequence.
get_tags
List available task tags. Use the returned tag IDs when creating or updating tasks.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| search | string | No | Filter tags by name (case-insensitive) |
| limit | number | No | Max results (default: 50) |
Returns: Array of tags with id, name. Ordered by name.
Tasks (12 tools, 9 base + 3 conditional)
get_tasks
List tasks with optional filters. Returns tasks with resolved assignee and stage names.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| project_id | number | No | Filter by project ID |
| stage_id | number | No | Filter by stage ID |
| user_id | number | No | Filter by assignee user ID |
| priority | "0" | "1" | No | Filter by priority: 0=normal, 1=urgent |
| has_deadline | boolean | No | If true, only tasks with a deadline set |
| state | string | No | Filter by task status (conditional): 01_in_progress, 02_changes_requested, 03_approved, 1_done, 1_canceled, 04_waiting_normal |
| limit | number | No | Max results (default: 50) |
| offset | number | No | Offset for pagination |
Returns: Array of tasks with resolved user_ids_resolved and stage_id_resolved fields.
get_task
Get detailed task information with resolved names for assignees, stage, project, parent task, and tags.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| task_id | number | Yes | The Odoo task ID |
Returns: Task object with all fields plus resolved relations and computed subtask_count.
Resolves relations in parallel:
user_idsfromres.users(name, email)stage_idfromproject.task.type(name)project_idfromproject.project(name)parent_idfromproject.task(name)tag_idsfromproject.tags(name)sub_stage_idfromproject.task.sub.stage(name) -- conditionaldepend_on_idsfromproject.task(name) -- conditional, "Blocked By"dependent_idsfromproject.task(name) -- conditional, "Blocks"
create_task
Create a new task. Use parent_id to create it as a subtask.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| name | string | Yes | Task title |
| project_id | number | Yes | Project ID |
| description | string | No | Task description (HTML format) |
| stage_id | number | No | Stage ID (defaults to first stage) |
| user_ids | number[] | No | Assignee user IDs |
| date_deadline | string | No | Deadline date (YYYY-MM-DD) |
| priority | "0" | "1" | No | 0=normal, 1=urgent |
| parent_id | number | No | Parent task ID (makes this a subtask) |
| tag_ids | number[] | No | Tag IDs to assign |
| planned_hours | number | No | Estimated hours |
| sub_stage_id | number | No | Sub-stage ID (conditional, see below) |
| state | string | No | Task status (conditional): 01_in_progress, 02_changes_requested, 03_approved, 1_done, 1_canceled, 04_waiting_normal |
Returns: Confirmation message with the new task ID.
Note: planned_hours is automatically mapped to the correct Odoo field name for your version (allocated_hours on Odoo 18+, planned_hours on older versions). sub_stage_id is only available on instances with the antz_project_customisation addon -- some stages require a sub-stage to be set. state is only available on Odoo 17+ Enterprise.
update_task
Update fields on an existing task.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| task_id | number | Yes | The task ID to update |
| name | string | No | New task title |
| description | string | No | New description (HTML) |
| stage_id | number | No | New stage ID |
| user_ids | number[] | No | New assignee user IDs |
| date_deadline | string | No | New deadline (YYYY-MM-DD) |
| priority | "0" | "1" | No | 0=normal, 1=urgent |
| parent_id | number | null | No | Parent task ID (null to remove parent) |
| tag_ids | number[] | No | Tag IDs (replaces all existing tags) |
| planned_hours | number | No | Estimated hours |
| sub_stage_id | number | No | Sub-stage ID (conditional) |
| depend_on_ids | number[] | No | Task IDs that block this task (replaces all, conditional) |
| state | string | No | Task status (conditional): 01_in_progress, 02_changes_requested, 03_approved, 1_done, 1_canceled, 04_waiting_normal |
Returns: Confirmation message. Returns "No fields to update." if no fields are provided.
Note: Setting tag_ids or depend_on_ids replaces all existing values (uses Odoo's [[6, 0, ids]] command syntax). Use add_task_dependency/remove_task_dependency for single dependency changes. Setting parent_id to null removes the parent relationship.
move_task
Move a task to a different stage (kanban column).
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| task_id | number | Yes | The task ID |
| stage_id | number | Yes | Target stage ID |
| sub_stage_id | number | No | Sub-stage ID (conditional) |
| state | string | No | Task status to set simultaneously (conditional): 01_in_progress, 1_done, etc. |
Returns: Confirmation message.
search_tasks
Search tasks by name and optionally by description, across all projects or within a specific project.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| query | string | Yes | Text to search for |
| search_description | boolean | No | Also search in task description (default: false) |
| project_id | number | No | Limit search to a specific project |
| limit | number | No | Max results (default: 20) |
Returns: Array of matching tasks with resolved assignee and stage names.
Note: When search_description is true, uses an OR domain to match either name or description.
get_subtasks
List all subtasks of a parent task.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| parent_task_id | number | Yes | The parent task ID |
| limit | number | No | Max results (default: 50) |
Returns: Array of subtasks with resolved assignee and stage names.
create_subtask
Create a subtask under a parent task. The subtask automatically inherits the parent's project.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| parent_task_id | number | Yes | Parent task ID |
| name | string | Yes | Subtask title |
| description | string | No | Description (HTML format) |
| user_ids | number[] | No | Assignee user IDs |
| date_deadline | string | No | Deadline (YYYY-MM-DD) |
| planned_hours | number | No | Estimated hours |
Returns: Confirmation message with the new subtask ID.
delete_task
Permanently delete a task. Uses a two-step safety pattern: call without confirm to preview what will be deleted, then call again with confirm=true to execute.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| task_id | number | Yes | The task ID to delete |
| confirm | boolean | No | Must be true to actually delete |
Without confirm: Returns a preview showing the task name, project, and subtask count.
With confirm=true: Deletes the task and returns confirmation.
This action is irreversible.
get_sub_stages (conditional)
List available sub-stages. Only registered on instances with the antz_project_customisation addon that adds the sub_stage_id field to project.task.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| search | string | No | Filter sub-stages by name (case-insensitive) |
| limit | number | No | Max results (default: 50) |
Returns: Array of sub-stages with id, name. Ordered by name.
add_task_dependency (conditional)
Add a dependency link: mark a task as blocked by another task. Only registered on instances where depend_on_ids exists on project.task.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| task_id | number | Yes | The task that is blocked |
| depends_on_task_id | number | Yes | The task that blocks it (must be completed first) |
Returns: Confirmation message.
Note: Task dependencies must be enabled on the project level (allow_task_dependencies setting in project configuration).
remove_task_dependency (conditional)
Remove a dependency link: unblock a task from another task.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| task_id | number | Yes | The task to unblock |
| depends_on_task_id | number | Yes | The blocking task to remove from dependencies |
Returns: Confirmation message.
Messages (3 tools)
get_task_comments
Get chatter messages (comments and notes) for a task with resolved author names.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| task_id | number | Yes | The task ID |
| limit | number | No | Max messages (default: 30) |
Returns: Array of messages with id, body, date, message_type, subtype_id, and resolved author_id_resolved (name, email from res.partner). Ordered newest first.
add_comment
Post a comment on a task's chatter. Visible to all followers.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| task_id | number | Yes | The task ID |
| body | string | Yes | Comment body (HTML format) |
Returns: Confirmation message with the new message ID.
add_internal_note
Post an internal note on a task's chatter. Only visible to internal users, not external followers or portal users.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| task_id | number | Yes | The task ID |
| body | string | Yes | Note body (HTML format) |
Returns: Confirmation message with the new message ID.
Users (1 tool)
get_users
List Odoo users. Use this to find user IDs for task assignment.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| search | string | No | Search by name or email (case-insensitive) |
| active_only | boolean | No | Only active users (default: true) |
| limit | number | No | Max results (default: 50) |
Returns: Array of users with id, name, login, email, active. Ordered by name.
Activities (3 tools)
get_activities
List scheduled activities for a task. Activities are action items with deadlines (to-dos, calls, meetings) attached to a task.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| task_id | number | Yes | The task ID |
| limit | number | No | Max results (default: 20) |
Returns: Array of activities with resolved user_id_resolved (name, email) and activity_type_id_resolved (name, category). Ordered by deadline ascending.
create_activity
Schedule an activity on a task.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| task_id | number | Yes | The task ID |
| activity_type_id | number | Yes | Activity type ID (e.g., 4=To-Do) |
| summary | string | Yes | Short title/summary |
| date_deadline | string | Yes | Due date (YYYY-MM-DD) |
| note | string | No | Detailed notes (HTML format) |
| user_id | number | No | Assigned user ID (defaults to current user) |
Returns: Confirmation message with the new activity ID.
Use discover_fields on mail.activity.type to find available activity types for your instance.
complete_activity
Mark a scheduled activity as done, with optional feedback that appears in the chatter.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| activity_id | number | Yes | The activity ID |
| feedback | string | No | Completion feedback (HTML format) |
Returns: Confirmation message.
Uses a version-safe cascade: tries action_feedback (Odoo 16+), falls back to action_done (older), then falls back to unlink (universal).
Meta (2 tools)
discover_fields
Get field definitions for any Odoo model. Useful for exploring what fields are available on a model you are working with.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| model | string | Yes | Odoo model name (e.g., project.task) |
| attributes | string[] | No | Field metadata to return (default: string, type, required, readonly, help, relation) |
Returns: Object keyed by field name with requested metadata attributes.
Common models: project.task, project.project, project.task.type, res.users, mail.activity, mail.activity.type, project.tags.
get_task_attachments
List attachment metadata on a task. Returns metadata only, not file content.
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| task_id | number | Yes | The task ID |
| limit | number | No | Max results (default: 20) |
Returns: Array of attachments with id, name, mimetype, file_size, create_date, create_uid. Ordered newest first.
Key Features
Batch Relationship Resolution
All read tools automatically resolve Odoo's many2one and many2many ID references to human-readable names. For example, a raw Odoo response with user_ids: [5, 8] becomes:
{
"user_ids": [5, 8],
"user_ids_resolved": [
{ "id": 5, "name": "Alice" },
{ "id": 8, "name": "Bob" }
]
}Resolution uses batch reads (one query per relation type, not per record) to avoid N+1 query performance issues. Multiple relations on a single tool call are resolved in parallel.
Version-Agnostic Field Resolution
The server detects your Odoo version's field names at startup using fields_get(). Known version differences are handled automatically:
| Stable Parameter Name | Odoo 18+ | Odoo 16-17 |
|----------------------|----------|------------|
| planned_hours | allocated_hours | planned_hours |
Tool schemas always use the stable parameter name. The mapping to the correct Odoo field happens internally and is invisible to the client.
Conditional Features
Some tools and parameters are conditionally registered based on your Odoo instance's capabilities (detected at startup via fields_get):
| Feature | Required Field | Tools/Parameters Affected |
|---------|---------------|--------------------------|
| Sub-stages | sub_stage_id on project.task | get_sub_stages tool, sub_stage_id param on create/update/move |
| Task dependencies | depend_on_ids on project.task | add_task_dependency, remove_task_dependency tools, depend_on_ids param on update_task, dependency resolution in all read tools |
| Task state | state on project.task | state param on get_tasks, create_task, update_task, move_task; state value in all read tools |
If a feature's required field doesn't exist on your instance, the associated tools and parameters are silently omitted.
Two-Step Delete Safety
The delete_task tool requires two calls: first without confirm (returns a preview of what will be deleted), then with confirm=true to execute. This prevents accidental deletions.
HTML Content
Task descriptions (description) and message bodies (body) must be HTML. The server does not convert plain text or Markdown -- pass HTML directly:
<p>This is a task description with <strong>bold</strong> text.</p>Odoo API Notes
This server communicates with Odoo using XML-RPC (not JSON-RPC). Odoo exposes two XML-RPC endpoints:
/xmlrpc/2/common-- Authentication only (authenticatemethod)/xmlrpc/2/object-- All CRUD operations viaexecute_kw
Authentication returns an integer UID. Every subsequent call passes (db, uid, api_key, model, method, args, kwargs). There are no session tokens or cookies.
Supported CRUD Methods
| Method | Description |
|--------|-------------|
| search | Find record IDs matching a domain |
| read | Read specific records by ID |
| search_read | Search and read in one call |
| create | Create a record |
| write | Update records |
| unlink | Delete records |
| fields_get | Get field metadata for a model |
Odoo Domain Syntax
Filters use Odoo's domain syntax: an array of [field, operator, value] tuples. OR conditions use the "|" prefix operator. Examples:
[["project_id", "=", 42]]
["|", ["name", "ilike", "bug"], ["description", "ilike", "bug"]]
[["priority", "=", "1"], ["date_deadline", "!=", false]]Limitations and Known Issues
Content Format
- Task descriptions and message bodies must be HTML. Plain text and Markdown are not automatically converted.
Field and Model Names
- The
project.tagsmodel name may beproject.tag(singular) on some Odoo versions. Ifget_tagsfails, this is likely the cause. subtype_xmlid(e.g.,mail.mt_comment) does not work on Odoo instances with custom addons that overridemail.message. The server uses integersubtype_idvalues instead.create_dateonproject.taskis immutable. Writes to this field succeed silently but are ignored by Odoo.
Authentication and Discovery
- Only API key (or password) authentication is supported. OAuth is not supported.
- The database name cannot be auto-discovered --
db.list()returns "Access Denied" on most production instances. You must know and configure the database name.
Transport
- STDIO transport only. HTTP/SSE transport is not currently supported.
Attachments
get_task_attachmentsreturns metadata only (name, mimetype, size). File content download is not supported.
Chatter Ordering
- Odoo's chatter displays messages newest-first. If posting multiple comments programmatically, post them oldest-first for correct chronological display.
Custom Addon Fields
- Instances with the
antz_project_customisationaddon have additional date fields (antz_requested_date,antz_completed_date,antz_approved_date,antz_delivered_date). The addon enforces thatantz_completed_datemust be afterantz_requested_date.
Many2Many Write Syntax
- Tag and assignee updates use Odoo's
[[6, 0, ids]]command syntax, which replaces all existing values. There is no append or remove -- you must pass the complete desired set.
Hours Field
- If neither
allocated_hoursnorplanned_hoursexists on your Odoo instance (unlikely but possible), the hours parameter is silently ignored.
Architecture
src/
├── index.ts # Entry point, server setup, transport
├── odoo-client.ts # XML-RPC client with auth, CRUD, field caching
├── helpers/
│ └── resolve.ts # Batch relationship resolver (N+1 safe)
└── tools/
├── projects.ts # list_projects, get_project, get_stages, get_tags
├── tasks.ts # 12 task management tools (9 base + 3 conditional)
├── messages.ts # get_task_comments, add_comment, add_internal_note
├── users.ts # get_users
├── activities.ts # get_activities, create_activity, complete_activity
└── meta.ts # discover_fields, get_task_attachmentsOdooClient (src/odoo-client.ts)
Core XML-RPC client. Handles authentication, provides typed CRUD methods (search, read, searchRead, create, write, unlink, fieldsGet), and implements field name caching via getFieldNames() and resolveField() for version-agnostic operation.
Batch Resolver (src/helpers/resolve.ts)
Handles three shapes of Odoo relationship data:
- Many2one tuples:
[42, "Alice"]-- resolves to full object - Many2many arrays:
[1, 2, 3]-- resolves all IDs in one batch read - Raw integer IDs:
42-- resolves to full object - Null/false/undefined: sets resolved field to
null
resolveRelations() runs all relation resolutions in parallel via Promise.all.
Tool Modules (src/tools/*.ts)
Each module exports a register*Tools(server, client) function that registers tools with the MCP server. Tool handlers return MCP-formatted responses: { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }.
Development
Scripts
bun install # Install dependencies
bun run dev # Run with tsx (hot reload via stdin/stdout)
bun run build # Compile TypeScript to dist/
bun run start # Run compiled dist/index.jsDependencies
| Package | Purpose |
|---------|---------|
| @modelcontextprotocol/sdk | MCP server framework and STDIO transport |
| xmlrpc | XML-RPC client for Odoo communication |
| zod | Schema validation for tool inputs |
Adding a New Tool
- Create or edit a file in
src/tools/ - Define the tool with
server.registerTool(name, { title, description, inputSchema }, handler) - Use
z.object({...})from Zod for the input schema - Use
client.searchRead(),client.create(), etc. for Odoo operations - Use
resolveRelations()to resolve relationship fields in read responses - Return
{ content: [{ type: "text", text: JSON.stringify(result, null, 2) }] } - If the file is new, import and call its register function in
src/index.ts
Odoo Models Used
| Model | Description |
|-------|-------------|
| project.project | Projects |
| project.task | Tasks and subtasks |
| project.task.type | Stages (kanban columns) |
| project.task.sub.stage | Sub-stages (conditional, custom addon) |
| project.tags | Task tags |
| mail.message | Chatter messages (comments, notes) |
| mail.activity | Scheduled activities |
| mail.activity.type | Activity type definitions |
| res.users | System users |
| res.partner | Contacts (linked to message authors) |
| ir.attachment | File attachments |
| ir.model | Model registry (for introspection) |
License
MIT
