Skip to main content
Glama

MCP Server for Odoo

config.py•8.83 kB
"""Configuration management for Odoo MCP Server. This module handles loading and validation of environment variables for connecting to Odoo via XML-RPC. """ import os from dataclasses import dataclass from pathlib import Path from typing import Dict, Literal, Optional from dotenv import load_dotenv @dataclass class OdooConfig: """Configuration for Odoo connection and MCP server settings.""" # Required fields url: str # Authentication (one method required) api_key: Optional[str] = None username: Optional[str] = None password: Optional[str] = None # Optional fields with defaults database: Optional[str] = None log_level: str = "INFO" default_limit: int = 10 max_limit: int = 100 max_smart_fields: int = 15 # MCP transport configuration transport: Literal["stdio", "streamable-http"] = "stdio" host: str = "localhost" port: int = 8000 # YOLO mode configuration yolo_mode: str = "off" # "off", "read", or "true" def __post_init__(self): """Validate configuration after initialization.""" # Validate URL if not self.url: raise ValueError("ODOO_URL is required") # Ensure URL format if not self.url.startswith(("http://", "https://")): raise ValueError("ODOO_URL must start with http:// or https://") # Validate YOLO mode valid_yolo_modes = {"off", "read", "true"} if self.yolo_mode not in valid_yolo_modes: raise ValueError( f"Invalid YOLO mode: {self.yolo_mode}. " f"Must be one of: {', '.join(valid_yolo_modes)}" ) # Validate authentication (relaxed for YOLO mode) has_api_key = bool(self.api_key) has_credentials = bool(self.username and self.password) # In YOLO mode, we might need username even with API key for standard auth if self.is_yolo_enabled: if not has_credentials and not (has_api_key and self.username): raise ValueError("YOLO mode requires either username/password or username/API key") else: if not has_api_key and not has_credentials: raise ValueError( "Authentication required: provide either ODOO_API_KEY or " "both ODOO_USER and ODOO_PASSWORD" ) # Validate numeric fields if self.default_limit <= 0: raise ValueError("ODOO_MCP_DEFAULT_LIMIT must be positive") if self.max_limit <= 0: raise ValueError("ODOO_MCP_MAX_LIMIT must be positive") if self.default_limit > self.max_limit: raise ValueError("ODOO_MCP_DEFAULT_LIMIT cannot exceed ODOO_MCP_MAX_LIMIT") # Validate log level valid_log_levels = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"} if self.log_level.upper() not in valid_log_levels: raise ValueError( f"Invalid log level: {self.log_level}. " f"Must be one of: {', '.join(valid_log_levels)}" ) # Validate transport valid_transports = {"stdio", "streamable-http"} if self.transport not in valid_transports: raise ValueError( f"Invalid transport: {self.transport}. " f"Must be one of: {', '.join(valid_transports)}" ) # Validate port if self.port <= 0 or self.port > 65535: raise ValueError("Port must be between 1 and 65535") @property def uses_api_key(self) -> bool: """Check if configuration uses API key authentication.""" return bool(self.api_key) @property def uses_credentials(self) -> bool: """Check if configuration uses username/password authentication.""" return bool(self.username and self.password) @property def is_yolo_enabled(self) -> bool: """Check if any YOLO mode is active.""" return self.yolo_mode != "off" @property def is_write_allowed(self) -> bool: """Check if write operations are allowed in current mode.""" return self.yolo_mode == "true" def get_endpoint_paths(self) -> Dict[str, str]: """Get appropriate endpoint paths based on mode. Returns: Dict[str, str]: Mapping of endpoint names to paths """ if self.is_yolo_enabled: # Use standard Odoo endpoints in YOLO mode return {"db": "/xmlrpc/db", "common": "/xmlrpc/2/common", "object": "/xmlrpc/2/object"} else: # Use MCP-specific endpoints in standard mode return { "db": "/mcp/xmlrpc/db", "common": "/mcp/xmlrpc/common", "object": "/mcp/xmlrpc/object", } @classmethod def from_env(cls, env_file: Optional[Path] = None) -> "OdooConfig": """Create configuration from environment variables. Args: env_file: Optional path to .env file Returns: OdooConfig: Validated configuration object """ return load_config(env_file) def load_config(env_file: Optional[Path] = None) -> OdooConfig: """Load configuration from environment variables and .env file. Args: env_file: Optional path to .env file. If not provided, looks for .env in current directory. Returns: OdooConfig: Validated configuration object Raises: ValueError: If required configuration is missing or invalid """ # Check if we have a .env file or environment variables if env_file: if not env_file.exists(): raise ValueError( f"Configuration file not found: {env_file}\n" "Please create a .env file based on .env.example" ) load_dotenv(env_file) else: # Check current directory for .env default_env = Path(".env") if default_env.exists(): load_dotenv() elif not os.getenv("ODOO_URL"): # No .env file and no ODOO_URL in environment raise ValueError( "No .env file found and ODOO_URL not set in environment.\n" "Please create a .env file based on .env.example or set environment variables." ) # Helper function to get int with default def get_int_env(key: str, default: int) -> int: value = os.getenv(key) if value is None: return default try: return int(value) except ValueError: raise ValueError(f"{key} must be a valid integer") from None # Helper function to parse YOLO mode def get_yolo_mode() -> str: yolo_env = os.getenv("ODOO_YOLO", "off").strip().lower() # Map various inputs to valid modes if yolo_env in ["", "false", "0", "off", "no"]: return "off" elif yolo_env in ["read", "readonly", "read-only"]: return "read" elif yolo_env in ["true", "1", "yes", "full"]: return "true" else: # Invalid value - will be caught by validation return yolo_env # Create configuration config = OdooConfig( url=os.getenv("ODOO_URL", "").strip(), api_key=os.getenv("ODOO_API_KEY", "").strip() or None, username=os.getenv("ODOO_USER", "").strip() or None, password=os.getenv("ODOO_PASSWORD", "").strip() or None, database=os.getenv("ODOO_DB", "").strip() or None, log_level=os.getenv("ODOO_MCP_LOG_LEVEL", "INFO").strip(), default_limit=get_int_env("ODOO_MCP_DEFAULT_LIMIT", 10), max_limit=get_int_env("ODOO_MCP_MAX_LIMIT", 100), max_smart_fields=get_int_env("ODOO_MCP_MAX_SMART_FIELDS", 15), transport=os.getenv("ODOO_MCP_TRANSPORT", "stdio").strip(), host=os.getenv("ODOO_MCP_HOST", "localhost").strip(), port=get_int_env("ODOO_MCP_PORT", 8000), yolo_mode=get_yolo_mode(), ) return config # Singleton configuration instance _config: Optional[OdooConfig] = None def get_config() -> OdooConfig: """Get the singleton configuration instance. Returns: OdooConfig: The configuration object Raises: ValueError: If configuration is not yet loaded """ global _config if _config is None: _config = load_config() return _config def set_config(config: OdooConfig) -> None: """Set the singleton configuration instance. This is primarily useful for testing. Args: config: The configuration object to set """ global _config _config = config def reset_config() -> None: """Reset the singleton configuration instance. This is primarily useful for testing. """ global _config _config = None

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/AlejandroLaraPolanco/mcp-odoo'

If you have feedback or need assistance with the MCP directory API, please join our Discord server