loading…
Search for a command to run...
loading…
Security-first MCP gateway for Odoo 17/18/19 — 27 tools, YAML-driven security
Security-first MCP gateway for Odoo 17/18/19 — 27 tools, YAML-driven security
Security-first, version-agnostic MCP gateway for Odoo 17/18/19. Works with stock and custom modules via YAML configuration. Zero Odoo-side code required.
Python 3.10+ License: MIT Odoo Tests Coverage
pip install odoo-mcp-gateway
Add to Claude Desktop config (claude_desktop_config.json):
{
"mcpServers": {
"odoo": {
"command": "python",
"args": ["-m", "odoo_mcp_gateway"],
"env": {
"ODOO_URL": "http://localhost:8069",
"ODOO_DB": "your_database"
}
}
}
}
Restart Claude Desktop. In any conversation:
login with method "password", username "admin", credential "your_password"
You're connected. Ask Claude to query, create, or update Odoo records — every call is rate-limited, audit-logged, and runs through two layers of security checks before reaching Odoo.
No Odoo addon required. No Python code to write. Just YAML config for fine-grained access control (optional — secure defaults work out of the box).
dry_run mode on create_record, update_record, delete_record, execute_method — validate without executingget_defaults (preview Odoo defaults), get_onchange (preview field side effects)read_group: create_date:month, date:quarter, etc.has_group('base.group_system') (was trusted from auth response)repr()/tracebackOdooSessionExpiredError (was retrying on every auth error)See CHANGELOG.md for the full list of 21 security fixes.
Existing Odoo MCP servers share common problems: hardcoded model lists that miss custom modules, security as an afterthought, mandatory custom Odoo addons, and single-version targets. This gateway solves all of them:
ir.model, add YAML config and it workspip install + YAML config = done. No custom addon requiredMCP Client (Claude Desktop / Claude Code / HTTP)
| User calls login tool with Odoo credentials
v
MCP Server (FastMCP)
|
|-- security_gate() --> Rate limit + RBAC tool access + audit logging
|-- restrictions --> Model/method/field block lists (YAML + hardcoded)
|-- rbac --> Field-level filtering + write sanitization
|
|-- tools/ --> 31 MCP tools (auth + schema + CRUD + workflow + plugins)
|-- resources/ --> 6 MCP resources (odoo:// URIs)
|-- prompts/ --> 12 reusable prompt templates
|-- plugins/ --> Entry-point plugin system (HR, Sales, Project, Helpdesk)
|
| JSON-RPC / XML-RPC as authenticated user
v
Odoo 17/18/19 (security enforced per user via ir.model.access + ir.rule)
Every tool and resource call passes through this pipeline:
Request --> Rate Limit --> Authentication Check --> RBAC Tool Access
--> Model Restriction --> Method Restriction --> Field Validation
--> Handler Execution --> RBAC Field Filtering --> Audit Log --> Response
Hardcoded safety guardrails that cannot be overridden by YAML:
__repr__/__str__, cleared on closehas_group('base.group_system') overrides auth-response is_adminpip install odoo-mcp-gateway
# Copy and edit config files
cp config/restrictions.yaml.example config/restrictions.yaml
cp config/model_access.yaml.example config/model_access.yaml
cp config/rbac.yaml.example config/rbac.yaml
# Set environment variables
export ODOO_URL=http://localhost:8069
export ODOO_DB=mydb
# Run (stdio mode for Claude Desktop / Claude Code)
python -m odoo_mcp_gateway
# Or HTTP mode for web clients
MCP_TRANSPORT=streamable-http python -m odoo_mcp_gateway
Add to claude_desktop_config.json:
{
"mcpServers": {
"odoo": {
"command": "python",
"args": ["-m", "odoo_mcp_gateway"],
"env": {
"ODOO_URL": "http://localhost:8069",
"ODOO_DB": "mydb"
}
}
}
}
# Add as MCP server
claude mcp add odoo -- python -m odoo_mcp_gateway
| Variable | Default | Description |
|---|---|---|
ODOO_URL |
http://localhost:8069 |
Odoo server URL |
ODOO_DB |
(required) | Odoo database name |
MCP_TRANSPORT |
stdio |
Transport mode (stdio or streamable-http) |
MCP_HOST |
127.0.0.1 |
HTTP host (streamable-http mode) |
MCP_PORT |
8080 |
HTTP port (streamable-http mode) |
MCP_LOG_LEVEL |
INFO |
Logging level |
CONFIG_DIR |
. |
Directory for YAML config files |
SESSION_TIMEOUT_SECONDS |
1800 |
Session inactivity timeout |
MAX_CONCURRENT_SESSIONS |
100 |
Maximum concurrent sessions |
RATE_LIMIT_GLOBAL |
60 |
Requests per minute (global) |
RATE_LIMIT_WRITE |
20 |
Write operations per minute |
ir.model.access and ir.rule| Tier | Effect | Example |
|---|---|---|
always_blocked |
Nobody can access, including admins | ir.config_parameter, res.users.apikeys |
admin_only |
Only admin users | ir.model, ir.model.fields |
admin_write_only |
Read OK for all, write needs admin | res.company, res.currency |
These cannot be overridden by YAML configuration:
Blocked models (32): ir.config_parameter, res.users, res.users.apikeys, res.users.log, ir.cron, ir.module.module, ir.model.access, ir.rule, ir.mail_server, ir.ui.view, ir.actions.server, ir.logging, ir.attachment, ir.exports, ir.exports.line, iap.account, auth.totp.wizard, auth.totp.device, payment.token, payment.provider, base.automation, digest.digest, res.config.settings, change.password.wizard, change.password.user, base.module.update, base.module.upgrade, base.module.uninstall, fetchmail.server, bus.bus, mail.mail, mail.template
Read-only models (8): mail.message, mail.followers, mail.activity, discuss.channel, mail.notification, mail.compose.message, mail.alias, discuss.channel.member (reads allowed, writes blocked for everyone)
Blocked write fields (10): password, password_crypt, groups_id, totp_secret, signup_token, signup_type, signup_expiration, api_key, share, active
Blocked methods (29): sudo, with_user, with_company, with_context, with_env, with_prefetch, _auto_init, _sql, _register_hook, _write, _create, _read, _setup_base, _setup_fields, _setup_complete, init, _table_query, _read_group_raw, name_create, load, import_data, export_data, flush_recordset, invalidate_recordset, _search_panel_select_range, _search_panel_select_multi_range, _search_panel_domain_image, _search, _read_progress_bar
Credential class with leak-safe __repr__/__str__, explicit .reveal() for use, and .clear() on closeis_admin is re-verified via has_group('base.group_system') after authentication, defending against tampered auth responses_compute_*, _inverse_*, etc.) blocked for everyone including admin unless explicitly whitelistedreturn_dropped=TruedefusedxmlOdooSessionExpiredError, not generic auth errors (no double round-trips on access denials)Three stock Odoo auth methods — no custom addon needed:
| Method | Protocol | Use Case |
|---|---|---|
api_key |
XML-RPC | Server-to-server, CI/CD pipelines |
password |
JSON-RPC | Interactive users, Claude Desktop |
session |
JSON-RPC | Reuse existing browser session (development) |
# Example: login via the MCP tool
> login(method="password", username="admin", credential="admin", database="mydb")
| Tool | Description |
|---|---|
login |
Authenticate with Odoo (api_key / password / session) |
list_models |
List accessible models with metadata and keyword filter |
get_model_fields |
Get field definitions for a model with optional filter |
search_read |
Search records with domain filters, field selection, ordering |
get_record |
Get a single record by ID |
search_count |
Count matching records |
create_record |
Create a new record (supports dry_run for validation-only) |
update_record |
Update existing record (supports dry_run for validation-only) |
delete_record |
Delete a single record by ID (supports dry_run) |
read_group |
Aggregated grouped reads with temporal operators (date:month, date:quarter, etc.) |
get_defaults |
Preview Odoo default values before create_record |
get_onchange |
Preview field side effects (with RBAC filtering) |
execute_method |
Call allowed model methods (supports dry_run) |
| Tool | Description |
|---|---|
get_create_requirements |
Get required fields and validation rules before creating a record |
get_record_actions |
Get available workflow actions for an existing record |
| URI | Description |
|---|---|
odoo://models |
List all accessible models |
odoo://models/{name} |
Model detail with field definitions |
odoo://record/{model}/{id} |
Single record data with RBAC field filtering |
odoo://schema/{model} |
Field schema with type info and importance ranking |
odoo://categories |
Model categories with counts |
odoo://workflow/{model} |
Workflow definition with stages and actions for a model |
| Prompt | Description |
|---|---|
analyze_model |
Comprehensive model structure analysis |
explore_data |
Natural language data exploration guide |
create_workflow |
Guide through model-specific workflows |
compare_records |
Side-by-side record comparison |
generate_report |
Analytical report generation |
discover_custom_modules |
Find and understand custom modules |
debug_access |
Troubleshoot access and permission issues |
workflow_guide |
Step-by-step workflow execution guide for a model |
record_creation_guide |
Guided record creation with field validation |
bulk_operations |
Guide for performing bulk operations safely |
field_mapping |
Map fields between Odoo versions (v17/v18/v19) |
data_migration |
Guide for migrating data between models or versions |
| Tool | Description |
|---|---|
check_in |
Record attendance check-in |
check_out |
Record attendance check-out |
get_my_attendance |
View attendance records (with month filter) |
get_my_leaves |
View leave requests (with state filter) |
request_leave |
Submit a leave request |
get_my_profile |
View employee profile |
| Tool | Description |
|---|---|
get_my_quotations |
List quotations/orders (with state filter) |
get_order_details |
Full order details with line items |
confirm_order |
Confirm a draft/sent quotation |
get_sales_summary |
Aggregated sales statistics (with period filter) |
| Tool | Description |
|---|---|
get_my_tasks |
List assigned tasks (with state/project filter) |
get_project_summary |
Project stats: task counts by stage, overdue |
update_task_stage |
Move a task to a different stage |
| Tool | Description |
|---|---|
get_my_tickets |
List assigned tickets (with state/priority filter) |
create_ticket |
Create a new helpdesk ticket |
update_ticket_stage |
Move a ticket to a different stage |
Add custom Odoo modules without writing Python code. Edit model_access.yaml:
custom_models:
full_crud:
- custom.delivery.route
- custom.warehouse.zone
read_only:
- custom.delivery.log
allowed_methods:
custom.delivery.route:
- action_dispatch
- action_complete
- action_cancel
Then all CRUD tools (search_read, create_record, update_record, delete_record) and execute_method work on the custom models with full security enforcement.
Extend the gateway with pip-installable plugins:
from odoo_mcp_gateway.plugins.base import OdooPlugin
class ManufacturingPlugin(OdooPlugin):
@property
def name(self) -> str:
return "manufacturing"
@property
def required_odoo_modules(self) -> list[str]:
return ["mrp"]
@property
def required_models(self) -> list[str]:
return ["mrp.production", "mrp.bom"]
def register(self, server, context):
@server.tool()
async def get_production_orders(...):
...
Register via pyproject.toml entry points:
[project.entry-points."odoo_mcp_gateway.plugins"]
manufacturing = "my_package:ManufacturingPlugin"
| File | Purpose |
|---|---|
config/restrictions.yaml |
Model/method/field block lists (3 tiers) |
config/model_access.yaml |
Per-model access policies, allowed methods, sensitive fields |
config/rbac.yaml |
Role-based tool access and field filtering by group |
config/gateway.yaml |
Server, connection, auth settings |
All files have .example templates with extensive inline documentation. Copy and customize:
cp config/restrictions.yaml.example config/restrictions.yaml
cp config/model_access.yaml.example config/model_access.yaml
cp config/rbac.yaml.example config/rbac.yaml
# restrictions.yaml
restrictions:
always_blocked:
- my.secret.model
admin_only:
- hr.salary.rule
admin_write_only:
- res.company
blocked_write_fields:
- password_crypt
- api_key
- totp_secret
# rbac.yaml
rbac:
tool_group_requirements:
delete_record:
- base.group_system
execute_method:
- base.group_erp_manager
sensitive_fields:
hr.employee:
salary:
required_group: hr.group_hr_manager
cp .env.example .env # Edit with your Odoo settings
docker compose up
Services:
The gateway runs as a non-root user in a minimal Python image.
# Test Odoo connectivity
odoo-mcp-tools test-connection --url http://localhost:8069
# Validate all YAML config files
odoo-mcp-tools validate-config --config-dir config
# List configured model access policies
odoo-mcp-tools list-models --config-dir config
git clone https://github.com/parth-unjiya/odoo-mcp-gateway.git
cd odoo-mcp-gateway
pip install -e ".[dev]"
# Run tests
pytest tests/ -v
# Run with coverage
pytest tests/ --cov=odoo_mcp_gateway --cov-report=term-missing
# Lint
ruff check src/ tests/
# Type check (strict mode)
mypy src/
src/odoo_mcp_gateway/
├── __main__.py # Entry point (stdio + HTTP)
├── server.py # FastMCP server setup, tool registration
├── config.py # Pydantic settings (env + .env)
├── client/
│ ├── base.py # OdooClientBase ABC, AuthResult
│ ├── jsonrpc.py # JSON-RPC client (session auth)
│ ├── xmlrpc.py # XML-RPC client (API key auth, defusedxml)
│ └── exceptions.py # OdooError hierarchy (7 types)
├── core/
│ ├── auth/manager.py # 3 auth strategies
│ ├── connection/manager.py # Circuit breaker + retry
│ ├── version/ # Odoo 17/18/19 detection + adapters
│ ├── workflow/
│ │ ├── definitions.py # WorkflowDef, StateDef, TransitionDef dataclasses
│ │ ├── registry.py # Workflow registration and lookup
│ │ └── stock_workflows/ # Built-in workflows (sale, purchase, HR, etc.)
│ ├── security/
│ │ ├── restrictions.py # 3-tier model/method restrictions + hardcoded guardrails
│ │ ├── rbac.py # Tool access + field filtering
│ │ ├── middleware.py # Security pipeline + security_gate()
│ │ ├── rate_limit.py # Token bucket rate limiter
│ │ ├── audit.py # Structured audit logging
│ │ ├── sanitizer.py # Error message sanitization
│ │ └── config_loader.py # YAML config → Pydantic models
│ └── discovery/
│ ├── model_registry.py # ir.model auto-discovery
│ ├── field_inspector.py # fields_get with TTL cache
│ └── suggestions.py # Category search + related models
├── tools/
│ ├── auth.py # login tool
│ ├── schema.py # list_models, get_model_fields
│ ├── crud.py # search_read, create/update/delete, execute_method
│ └── workflow.py # get_create_requirements, get_record_actions
├── resources/handlers.py # 6 MCP resources (odoo:// URIs)
├── prompts/handlers.py # 12 MCP prompt templates
├── plugins/
│ ├── base.py, registry.py # Plugin ABC + entry_point discovery
│ └── core/ # Built-in plugins (HR, Sales, Project, Helpdesk)
├── cli/tools.py # CLI: test-connection, validate-config
└── utils/ # Domain builder, formatting, token budget
1,476 tests passing, 93% code coverage, mypy strict clean, ruff clean:
tests/unit/
├── client/ # JSON-RPC, XML-RPC, auth manager, XXE protection
├── security/ # Restrictions, RBAC, audit, rate limit, sanitizer, security_gate
├── discovery/ # Model registry, field inspector, suggestions
├── tools/ # All 13 MCP tools + input validation + dry_run
├── plugins/ # Plugin system + 4 domain plugins + IDOR protection
└── cli/ # CLI utility tools
# Run all tests
pytest tests/ -v
# Run specific area
pytest tests/unit/security/ -v
pytest tests/unit/tools/ -v
pytest tests/unit/plugins/ -v
# Coverage report
pytest tests/ --cov=odoo_mcp_gateway --cov-report=html
All Odoo errors are classified into 7 types:
| Error | Cause |
|---|---|
OdooConnectionError |
Cannot reach Odoo server |
OdooAuthError |
Invalid credentials |
OdooAccessError |
ir.model.access denied |
OdooValidationError |
Field validation failure |
OdooUserError |
Business logic error |
OdooMissingError |
Record not found |
OdooVersionError |
Unsupported Odoo version |
All error messages are sanitized before reaching the MCP client — internal URLs, SQL fragments, file paths, and stack traces are automatically stripped.
Credential wrapper that prevents repr()/traceback leakage.)streamable-http transport currently has known session isolation limitations — the _current_session_key ContextVar is set inside the login tool but subsequent tool calls from different request contexts may fall back to the first available session. Deploy HTTP mode as single-tenant only (one user per server process) until per-request middleware lands in v0.3.0. stdio mode is single-session by design and unaffected.git checkout -b feature/my-feature)pytest && ruff check src/ tests/ && mypy src/Выполни в терминале:
claude mcp add odoo-mcp-gateway -- npx Не уверен что выбрать?
Найди свой стек за 60 секунд
Автор?
Embed-бейдж для README
Похожее
Все в категории development