Skip to main content
Glama

MCP Server for Odoo

by ivnvxd
Mozilla Public License 2.0
88
  • Apple
  • Linux
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/ivnvxd/mcp-server-odoo'

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