Skip to content

Commit 958b2ea

Browse files
authored
Merge pull request #17 from ivnvxd/dev
Add YOLO mode
2 parents 5a023da + 4e1b484 commit 958b2ea

17 files changed

+2114
-68
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.3.0] - 2025-09-14
9+
10+
### Added
11+
- **YOLO Mode**: Development mode for testing without MCP module installation
12+
- Read-Only: Safe demo mode with read-only access to all models
13+
- Full Access: Unrestricted access for development (never use in production)
14+
- Works with any standard Odoo instance via native XML-RPC endpoints
15+
816
## [0.2.2] - 2025-08-04
917

1018
### Added

README.md

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
An MCP server that enables AI assistants like Claude to interact with Odoo ERP systems. Access business data, search records, create new entries, update existing data, and manage your Odoo instance through natural language.
1212

13+
**Works with any Odoo instance!** Use [YOLO mode](#yolo-mode-developmenttesting-only-) for quick testing and demos with any standard Odoo installation. For enterprise security, access controls, and production use, install the [Odoo MCP module](https://apps.odoo.com/apps/modules/18.0/mcp_server).
14+
1315
## Features
1416

1517
- 🔍 **Search and retrieve** any Odoo record (customers, products, invoices, etc.)
@@ -22,15 +24,16 @@ An MCP server that enables AI assistants like Claude to interact with Odoo ERP s
2224
- 🔐 **Secure access** with API key or username/password authentication
2325
- 🎯 **Smart pagination** for large datasets
2426
- 💬 **LLM-optimized output** with hierarchical text formatting
27+
- 🚀 **YOLO Mode** for quick access with any Odoo instance (no module required)
2528

2629
## Installation
2730

2831
### Prerequisites
2932

3033
- Python 3.10 or higher
3134
- Access to an Odoo instance (version 17.0+)
32-
- The [Odoo MCP module](https://apps.odoo.com/apps/modules/18.0/mcp_server) installed on your Odoo server
33-
- (optional) An API key generated in Odoo (Settings > Users > API Keys)
35+
- For production use: The [Odoo MCP module](https://apps.odoo.com/apps/modules/18.0/mcp_server) installed on your Odoo server
36+
- For testing/demos: Any standard Odoo instance (use YOLO mode)
3437

3538
### Install UV First
3639

@@ -202,6 +205,7 @@ The server requires the following environment variables:
202205
| `ODOO_USER` | Yes* | Username (if not using API key) | `admin` |
203206
| `ODOO_PASSWORD` | Yes* | Password (if not using API key) | `admin` |
204207
| `ODOO_DB` | No | Database name (auto-detected if not set) | `mycompany` |
208+
| `ODOO_YOLO` | No | YOLO mode - bypasses MCP security (⚠️ DEV ONLY) | `off`, `read`, `true` |
205209

206210
*Either `ODOO_API_KEY` or both `ODOO_USER` and `ODOO_PASSWORD` are required.
207211

@@ -285,6 +289,94 @@ The HTTP endpoint will be available at: `http://localhost:8000/mcp/`
285289
- Under the "API Keys" tab, create a new key
286290
- Copy the key for your MCP configuration
287291

292+
### YOLO Mode (Development/Testing Only) ⚠️
293+
294+
YOLO mode allows the MCP server to connect directly to any standard Odoo instance **without requiring the MCP module**. This mode bypasses all MCP security controls and is intended **ONLY for development, testing, and demos**.
295+
296+
**🚨 WARNING: Never use YOLO mode in production environments!**
297+
298+
#### YOLO Mode Levels
299+
300+
1. **Read-Only Mode** (`ODOO_YOLO=read`):
301+
- Allows all read operations (search, read, count)
302+
- Blocks all write operations (create, update, delete)
303+
- Safe for demos and testing
304+
- Shows "READ-ONLY" indicators in responses
305+
306+
2. **Full Access Mode** (`ODOO_YOLO=true`):
307+
- Allows ALL operations without restrictions
308+
- Full CRUD access to all models
309+
- **EXTREMELY DANGEROUS** - use only in isolated environments
310+
- Shows "FULL ACCESS" warnings in responses
311+
312+
#### YOLO Mode Configuration
313+
314+
<details>
315+
<summary>Read-Only YOLO Mode (safer for demos)</summary>
316+
317+
```json
318+
{
319+
"mcpServers": {
320+
"odoo-demo": {
321+
"command": "uvx",
322+
"args": ["mcp-server-odoo"],
323+
"env": {
324+
"ODOO_URL": "http://localhost:8069",
325+
"ODOO_USER": "admin",
326+
"ODOO_PASSWORD": "admin",
327+
"ODOO_DB": "demo",
328+
"ODOO_YOLO": "read"
329+
}
330+
}
331+
}
332+
}
333+
```
334+
</details>
335+
336+
<details>
337+
<summary>Full Access YOLO Mode (⚠️ use with extreme caution)</summary>
338+
339+
```json
340+
{
341+
"mcpServers": {
342+
"odoo-test": {
343+
"command": "uvx",
344+
"args": ["mcp-server-odoo"],
345+
"env": {
346+
"ODOO_URL": "http://localhost:8069",
347+
"ODOO_USER": "admin",
348+
"ODOO_PASSWORD": "admin",
349+
"ODOO_DB": "test",
350+
"ODOO_YOLO": "true"
351+
}
352+
}
353+
}
354+
}
355+
```
356+
</details>
357+
358+
#### When to Use YOLO Mode
359+
360+
**Appropriate Uses:**
361+
- Local development with test data
362+
- Quick demos with non-sensitive data
363+
- Testing MCP clients before installing the MCP module
364+
- Prototyping in isolated environments
365+
366+
**Never Use For:**
367+
- Production environments
368+
- Instances with real customer data
369+
- Shared development servers
370+
- Any environment with sensitive information
371+
372+
#### YOLO Mode Security Notes
373+
374+
- Connects directly to Odoo's standard XML-RPC endpoints
375+
- Bypasses all MCP access controls and model restrictions
376+
- No rate limiting is applied
377+
- All operations are logged but not restricted
378+
- Model listing shows 200+ models instead of just enabled ones
379+
288380
## Usage Examples
289381

290382
Once configured, you can ask Claude:

mcp_server_odoo/access_control.py

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,16 @@ def __init__(self, config: OdooConfig, cache_ttl: int = CACHE_TTL):
8282
# Parse base URL
8383
self.base_url = config.url.rstrip("/")
8484

85-
# Validate API key is available
85+
# In YOLO mode, skip API key validation and MCP checks
86+
if config.is_yolo_enabled:
87+
mode_desc = "READ-ONLY" if config.yolo_mode == "read" else "FULL ACCESS"
88+
logger.warning(
89+
f"🚨 YOLO mode ({mode_desc}): Access control bypassed! "
90+
f"All models accessible, MCP security disabled."
91+
)
92+
return # Skip API validation
93+
94+
# Validate API key is available for standard mode
8695
if not config.api_key:
8796
raise AccessControlError(
8897
"API key required for access control. Please configure ODOO_API_KEY."
@@ -170,6 +179,11 @@ def get_enabled_models(self) -> List[Dict[str, str]]:
170179
Raises:
171180
AccessControlError: If request fails
172181
"""
182+
# In YOLO mode, return empty list (all models are allowed)
183+
if self.config.is_yolo_enabled:
184+
logger.debug("YOLO mode: All models are accessible")
185+
return [] # Empty list indicates all models allowed
186+
173187
cache_key = "enabled_models"
174188

175189
# Check cache
@@ -196,6 +210,11 @@ def is_model_enabled(self, model: str) -> bool:
196210
Returns:
197211
True if model is enabled, False otherwise
198212
"""
213+
# In YOLO mode, all models are enabled
214+
if self.config.is_yolo_enabled:
215+
logger.debug(f"YOLO mode: Model '{model}' is accessible")
216+
return True
217+
199218
try:
200219
enabled_models = self.get_enabled_models()
201220
return any(m["model"] == model for m in enabled_models)
@@ -215,6 +234,29 @@ def get_model_permissions(self, model: str) -> ModelPermissions:
215234
Raises:
216235
AccessControlError: If request fails
217236
"""
237+
# In YOLO mode, return permissions based on mode level
238+
if self.config.is_yolo_enabled:
239+
if self.config.yolo_mode == "read":
240+
# Read-only mode: only read operations allowed
241+
return ModelPermissions(
242+
model=model,
243+
enabled=True,
244+
can_read=True,
245+
can_write=False,
246+
can_create=False,
247+
can_unlink=False,
248+
)
249+
else: # yolo_mode == "true"
250+
# Full access mode: all operations allowed
251+
return ModelPermissions(
252+
model=model,
253+
enabled=True,
254+
can_read=True,
255+
can_write=True,
256+
can_create=True,
257+
can_unlink=True,
258+
)
259+
218260
cache_key = f"permissions_{model}"
219261

220262
# Check cache
@@ -253,8 +295,34 @@ def check_operation_allowed(self, model: str, operation: str) -> Tuple[bool, Opt
253295
Returns:
254296
Tuple of (allowed, error_message)
255297
"""
298+
# In YOLO mode, check based on mode level
299+
if self.config.is_yolo_enabled:
300+
# Define read operations
301+
read_operations = {
302+
"read",
303+
"search",
304+
"search_read",
305+
"fields_get",
306+
"count",
307+
"search_count",
308+
}
309+
310+
# Check operation based on mode
311+
if operation in read_operations:
312+
# Read operations always allowed in YOLO mode
313+
return True, None
314+
elif self.config.yolo_mode == "true":
315+
# All operations allowed in full mode
316+
return True, None
317+
else:
318+
# Write operations blocked in read-only mode
319+
return False, (
320+
f"Write operation '{operation}' not allowed in read-only YOLO mode. "
321+
f"Only read operations are permitted for safety."
322+
)
323+
256324
try:
257-
# Get model permissions
325+
# Standard mode: Get model permissions from MCP
258326
permissions = self.get_model_permissions(model)
259327

260328
# Check if model is enabled
@@ -294,6 +362,11 @@ def filter_enabled_models(self, models: List[str]) -> List[str]:
294362
Returns:
295363
List of enabled model names
296364
"""
365+
# In YOLO mode, all models are enabled
366+
if self.config.is_yolo_enabled:
367+
logger.debug(f"YOLO mode: All {len(models)} models are accessible")
368+
return models # Return all models unfiltered
369+
297370
try:
298371
enabled_models = self.get_enabled_models()
299372
enabled_set = {m["model"] for m in enabled_models}

mcp_server_odoo/config.py

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import os
88
from dataclasses import dataclass
99
from pathlib import Path
10-
from typing import Literal, Optional
10+
from typing import Dict, Literal, Optional
1111

1212
from dotenv import load_dotenv
1313

@@ -36,6 +36,9 @@ class OdooConfig:
3636
host: str = "localhost"
3737
port: int = 8000
3838

39+
# YOLO mode configuration
40+
yolo_mode: str = "off" # "off", "read", or "true"
41+
3942
def __post_init__(self):
4043
"""Validate configuration after initialization."""
4144
# Validate URL
@@ -46,15 +49,28 @@ def __post_init__(self):
4649
if not self.url.startswith(("http://", "https://")):
4750
raise ValueError("ODOO_URL must start with http:// or https://")
4851

49-
# Validate authentication
52+
# Validate YOLO mode
53+
valid_yolo_modes = {"off", "read", "true"}
54+
if self.yolo_mode not in valid_yolo_modes:
55+
raise ValueError(
56+
f"Invalid YOLO mode: {self.yolo_mode}. "
57+
f"Must be one of: {', '.join(valid_yolo_modes)}"
58+
)
59+
60+
# Validate authentication (relaxed for YOLO mode)
5061
has_api_key = bool(self.api_key)
5162
has_credentials = bool(self.username and self.password)
5263

53-
if not has_api_key and not has_credentials:
54-
raise ValueError(
55-
"Authentication required: provide either ODOO_API_KEY or "
56-
"both ODOO_USER and ODOO_PASSWORD"
57-
)
64+
# In YOLO mode, we might need username even with API key for standard auth
65+
if self.is_yolo_enabled:
66+
if not has_credentials and not (has_api_key and self.username):
67+
raise ValueError("YOLO mode requires either username/password or username/API key")
68+
else:
69+
if not has_api_key and not has_credentials:
70+
raise ValueError(
71+
"Authentication required: provide either ODOO_API_KEY or "
72+
"both ODOO_USER and ODOO_PASSWORD"
73+
)
5874

5975
# Validate numeric fields
6076
if self.default_limit <= 0:
@@ -96,6 +112,33 @@ def uses_credentials(self) -> bool:
96112
"""Check if configuration uses username/password authentication."""
97113
return bool(self.username and self.password)
98114

115+
@property
116+
def is_yolo_enabled(self) -> bool:
117+
"""Check if any YOLO mode is active."""
118+
return self.yolo_mode != "off"
119+
120+
@property
121+
def is_write_allowed(self) -> bool:
122+
"""Check if write operations are allowed in current mode."""
123+
return self.yolo_mode == "true"
124+
125+
def get_endpoint_paths(self) -> Dict[str, str]:
126+
"""Get appropriate endpoint paths based on mode.
127+
128+
Returns:
129+
Dict[str, str]: Mapping of endpoint names to paths
130+
"""
131+
if self.is_yolo_enabled:
132+
# Use standard Odoo endpoints in YOLO mode
133+
return {"db": "/xmlrpc/db", "common": "/xmlrpc/2/common", "object": "/xmlrpc/2/object"}
134+
else:
135+
# Use MCP-specific endpoints in standard mode
136+
return {
137+
"db": "/mcp/xmlrpc/db",
138+
"common": "/mcp/xmlrpc/common",
139+
"object": "/mcp/xmlrpc/object",
140+
}
141+
99142
@classmethod
100143
def from_env(cls, env_file: Optional[Path] = None) -> "OdooConfig":
101144
"""Create configuration from environment variables.
@@ -152,6 +195,20 @@ def get_int_env(key: str, default: int) -> int:
152195
except ValueError:
153196
raise ValueError(f"{key} must be a valid integer") from None
154197

198+
# Helper function to parse YOLO mode
199+
def get_yolo_mode() -> str:
200+
yolo_env = os.getenv("ODOO_YOLO", "off").strip().lower()
201+
# Map various inputs to valid modes
202+
if yolo_env in ["", "false", "0", "off", "no"]:
203+
return "off"
204+
elif yolo_env in ["read", "readonly", "read-only"]:
205+
return "read"
206+
elif yolo_env in ["true", "1", "yes", "full"]:
207+
return "true"
208+
else:
209+
# Invalid value - will be caught by validation
210+
return yolo_env
211+
155212
# Create configuration
156213
config = OdooConfig(
157214
url=os.getenv("ODOO_URL", "").strip(),
@@ -166,6 +223,7 @@ def get_int_env(key: str, default: int) -> int:
166223
transport=os.getenv("ODOO_MCP_TRANSPORT", "stdio").strip(),
167224
host=os.getenv("ODOO_MCP_HOST", "localhost").strip(),
168225
port=get_int_env("ODOO_MCP_PORT", 8000),
226+
yolo_mode=get_yolo_mode(),
169227
)
170228

171229
return config

0 commit comments

Comments
 (0)