npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@saketsawrav/odoo-mcp-server

v0.5.4

Published

MCP server for Odoo ERP — project management, tasks, and comments via XML-RPC

Downloads

321

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

  1. Log into your Odoo instance
  2. Go to Settings > Users & Companies > Users
  3. Select your user
  4. Go to the "Account Security" tab (or "Preferences" in older versions)
  5. Under "API Keys", click "New API Key"
  6. 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 build

Configuration

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_here

Usage

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.ts

By 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.0

Restart 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.js

Tools 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_ids from res.users (name, email)
  • stage_id from project.task.type (name)
  • project_id from project.project (name)
  • parent_id from project.task (name)
  • tag_ids from project.tags (name)
  • sub_stage_id from project.task.sub.stage (name) -- conditional
  • depend_on_ids from project.task (name) -- conditional, "Blocked By"
  • dependent_ids from project.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 (authenticate method)
  • /xmlrpc/2/object -- All CRUD operations via execute_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.tags model name may be project.tag (singular) on some Odoo versions. If get_tags fails, this is likely the cause.
  • subtype_xmlid (e.g., mail.mt_comment) does not work on Odoo instances with custom addons that override mail.message. The server uses integer subtype_id values instead.
  • create_date on project.task is 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_attachments returns 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_customisation addon have additional date fields (antz_requested_date, antz_completed_date, antz_approved_date, antz_delivered_date). The addon enforces that antz_completed_date must be after antz_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_hours nor planned_hours exists 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_attachments

OdooClient (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.js

Dependencies

| 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

  1. Create or edit a file in src/tools/
  2. Define the tool with server.registerTool(name, { title, description, inputSchema }, handler)
  3. Use z.object({...}) from Zod for the input schema
  4. Use client.searchRead(), client.create(), etc. for Odoo operations
  5. Use resolveRelations() to resolve relationship fields in read responses
  6. Return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }
  7. 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