Skip to main content
Glama

Odoo MCP Server Advanced

by AlanOgic
odoo_client.pyβ€’18.1 kB
""" Odoo JSON-RPC client for MCP server integration """ import json import os import sys import re import urllib.parse from typing import Any, Dict, List, Optional import requests from dotenv import load_dotenv class OdooClient: """Client for interacting with Odoo via JSON-RPC or JSON-2 API""" def __init__( self, url, db, username, password=None, api_key=None, api_version="json-rpc", timeout=30, verify_ssl=True, ): """ Initialize the Odoo client with connection parameters Args: url: Odoo server URL (with or without protocol) db: Database name username: Login username password: Login password (for JSON-RPC, deprecated in Odoo 20) api_key: API key for JSON-2 API (Odoo 19+, Bearer token) api_version: "json-rpc" (default, current) or "json-2" (Odoo 19+) timeout: Connection timeout in seconds verify_ssl: Whether to verify SSL certificates Note: - JSON-RPC API is deprecated and will be removed in Odoo 20 (fall 2026) - JSON-2 API requires api_key instead of password - Use api_key authentication for better security """ # Ensure URL has a protocol if not re.match(r"^https?://", url): url = f"http://{url}" # Remove trailing slash from URL if present url = url.rstrip("/") self.url = url self.db = db self.username = username self.password = password self.api_key = api_key self.api_version = api_version self.uid = None # Validate API version and credentials if api_version == "json-2": if not api_key: raise ValueError("api_key is required for JSON-2 API (Odoo 19+)") else: # json-rpc if not password: raise ValueError("password is required for JSON-RPC API") # Set timeout and SSL verification self.timeout = timeout self.verify_ssl = verify_ssl # Setup session self.session = requests.Session() self.session.verify = verify_ssl # Configure headers for JSON-2 API if api_version == "json-2": self.session.headers['Authorization'] = f'Bearer {api_key}' self.session.headers['X-Odoo-Database'] = db self.session.headers['Content-Type'] = 'application/json' # HTTP proxy support proxy = os.environ.get("HTTP_PROXY") if proxy: self.session.proxies = {"http": proxy, "https": proxy} # Parse hostname for logging parsed_url = urllib.parse.urlparse(self.url) self.hostname = parsed_url.netloc # JSON-RPC endpoint (for legacy API) self.jsonrpc_url = f"{self.url}/jsonrpc" # JSON-2 API base URL self.json2_base_url = f"{self.url}/api/v2" # Request ID counter (for JSON-RPC) self._request_id = 0 # Connect (only for JSON-RPC, JSON-2 uses Bearer token) if api_version == "json-rpc": self._connect() def _jsonrpc_call(self, service: str, method: str, *args) -> Any: """ Make a JSON-RPC 1.x call to Odoo Args: service: Service name ('common' or 'object') method: Method name to call *args: Arguments to pass to the method Returns: Result of the method call """ self._request_id += 1 payload = { "jsonrpc": "1.0", "method": "call", "params": { "service": service, "method": method, "args": list(args) }, "id": self._request_id } try: response = self.session.post( self.jsonrpc_url, json=payload, timeout=self.timeout, headers={"Content-Type": "application/json"} ) response.raise_for_status() result = response.json() if "error" in result: error_data = result["error"] error_msg = error_data.get("data", {}).get("message", str(error_data)) raise ValueError(f"Odoo error: {error_msg}") return result.get("result") except requests.exceptions.Timeout as e: raise TimeoutError(f"Request timeout after {self.timeout}s: {str(e)}") except requests.exceptions.ConnectionError as e: raise ConnectionError(f"Failed to connect to Odoo server: {str(e)}") except requests.exceptions.RequestException as e: raise ValueError(f"Request failed: {str(e)}") def _connect(self): """Initialize the JSON-RPC connection and authenticate""" print(f"Connecting to Odoo at: {self.url}", file=sys.stderr) print(f" Hostname: {self.hostname}", file=sys.stderr) print( f" Timeout: {self.timeout}s, Verify SSL: {self.verify_ssl}", file=sys.stderr, ) print(f" JSON-RPC endpoint: {self.jsonrpc_url}", file=sys.stderr) # Authenticate and get user ID print( f"Authenticating with database: {self.db}, username: {self.username}", file=sys.stderr, ) try: self.uid = self._jsonrpc_call( "common", "authenticate", self.db, self.username, self.password, {} ) if not self.uid: raise ValueError("Authentication failed: Invalid username or password") print(f" Authenticated successfully with UID: {self.uid}", file=sys.stderr) except (TimeoutError, ConnectionError) as e: print(f"Connection error: {str(e)}", file=sys.stderr) raise except Exception as e: print(f"Authentication error: {str(e)}", file=sys.stderr) raise ValueError(f"Failed to authenticate with Odoo: {str(e)}") def _execute(self, model, method, *args, **kwargs): """Execute a method on an Odoo model using JSON-RPC or JSON-2 API""" if self.api_version == "json-2": # JSON-2 API format url = f"{self.json2_base_url}/{model}/{method}" payload = { "args": list(args) if args else [], "kwargs": kwargs if kwargs else {} } try: response = self.session.post( url, json=payload, timeout=self.timeout ) response.raise_for_status() return response.json() except requests.exceptions.Timeout as e: raise TimeoutError(f"Request timeout after {self.timeout}s: {str(e)}") except requests.exceptions.ConnectionError as e: raise ConnectionError(f"Failed to connect to Odoo server: {str(e)}") except requests.exceptions.RequestException as e: raise ValueError(f"Request failed: {str(e)}") else: # JSON-RPC format (legacy) return self._jsonrpc_call( "object", "execute_kw", self.db, self.uid, self.password, model, method, args, kwargs ) def execute_method(self, model, method, *args, **kwargs): """ Execute an arbitrary method on a model Args: model: The model name (e.g., 'res.partner') method: Method name to execute *args: Positional arguments to pass to the method **kwargs: Keyword arguments to pass to the method Returns: Result of the method execution """ return self._execute(model, method, *args, **kwargs) def get_models(self): """ Get a list of all available models in the system Returns: List of model names Examples: >>> client = OdooClient(url, db, username, password) >>> models = client.get_models() >>> print(len(models)) 125 >>> print(models[:5]) ['res.partner', 'res.users', 'res.company', 'res.groups', 'ir.model'] """ try: # First search for model IDs model_ids = self._execute("ir.model", "search", []) if not model_ids: return { "model_names": [], "models_details": {}, "error": "No models found", } # Then read the model data with only the most basic fields # that are guaranteed to exist in all Odoo versions result = self._execute("ir.model", "read", model_ids, ["model", "name"]) # Extract and sort model names alphabetically models = sorted([rec["model"] for rec in result]) # For more detailed information, include the full records models_info = { "model_names": models, "models_details": { rec["model"]: {"name": rec.get("name", "")} for rec in result }, } return models_info except Exception as e: print(f"Error retrieving models: {str(e)}", file=sys.stderr) return {"model_names": [], "models_details": {}, "error": str(e)} def get_model_info(self, model_name): """ Get information about a specific model Args: model_name: Name of the model (e.g., 'res.partner') Returns: Dictionary with model information Examples: >>> client = OdooClient(url, db, username, password) >>> info = client.get_model_info('res.partner') >>> print(info['name']) 'Contact' """ try: result = self._execute( "ir.model", "search_read", [("model", "=", model_name)], {"fields": ["name", "model"]}, ) if not result: return {"error": f"Model {model_name} not found"} return result[0] except Exception as e: print(f"Error retrieving model info: {str(e)}", file=sys.stderr) return {"error": str(e)} def get_model_fields(self, model_name): """ Get field definitions for a specific model Args: model_name: Name of the model (e.g., 'res.partner') Returns: Dictionary mapping field names to their definitions Examples: >>> client = OdooClient(url, db, username, password) >>> fields = client.get_model_fields('res.partner') >>> print(fields['name']['type']) 'char' """ try: fields = self._execute(model_name, "fields_get") return fields except Exception as e: print(f"Error retrieving fields: {str(e)}", file=sys.stderr) return {"error": str(e)} def search_read( self, model_name, domain, fields=None, offset=None, limit=None, order=None ): """ Search for records and read their data in a single call Args: model_name: Name of the model (e.g., 'res.partner') domain: Search domain (e.g., [('is_company', '=', True)]) fields: List of field names to return (None for all) offset: Number of records to skip limit: Maximum number of records to return order: Sorting criteria (e.g., 'name ASC, id DESC') Returns: List of dictionaries with the matching records Examples: >>> client = OdooClient(url, db, username, password) >>> records = client.search_read('res.partner', [('is_company', '=', True)], limit=5) >>> print(len(records)) 5 """ try: kwargs = {} if offset: kwargs["offset"] = offset if fields is not None: kwargs["fields"] = fields if limit is not None: kwargs["limit"] = limit if order is not None: kwargs["order"] = order result = self._execute(model_name, "search_read", domain, **kwargs) return result except Exception as e: print(f"Error in search_read: {str(e)}", file=sys.stderr) return [] def read_records(self, model_name, ids, fields=None): """ Read data of records by IDs Args: model_name: Name of the model (e.g., 'res.partner') ids: List of record IDs to read fields: List of field names to return (None for all) Returns: List of dictionaries with the requested records Examples: >>> client = OdooClient(url, db, username, password) >>> records = client.read_records('res.partner', [1]) >>> print(records[0]['name']) 'YourCompany' """ try: kwargs = {} if fields is not None: kwargs["fields"] = fields result = self._execute(model_name, "read", ids, kwargs) return result except Exception as e: print(f"Error reading records: {str(e)}", file=sys.stderr) return [] def load_config(): """ Load Odoo configuration from .env file, environment variables, or config file Priority order: 1. Search for .env file in common locations and load it 2. Check environment variables (may have been set by .env) 3. Fall back to JSON config files Environment Variables: ODOO_CONFIG_DIR: Custom directory to search for .env file (highest priority) Returns: dict: Configuration dictionary with url, db, username, password """ # Define .env file paths to check (in priority order) env_paths = [] # Check for custom config directory (highest priority) custom_config_dir = os.environ.get("ODOO_CONFIG_DIR") if custom_config_dir: custom_env_path = os.path.join(os.path.expanduser(custom_config_dir), ".env") env_paths.append(custom_env_path) # Standard paths env_paths.extend([ ".env", # Current directory os.path.expanduser("~/.config/odoo/.env"), # User config directory os.path.expanduser("~/.env"), # User home directory ]) # Try to load .env file from common locations for env_path in env_paths: expanded_path = os.path.expanduser(env_path) if os.path.exists(expanded_path): print(f"Loading configuration from: {expanded_path}", file=sys.stderr) load_dotenv(dotenv_path=expanded_path, override=True) break # Check environment variables (may have been set by .env file) if all( var in os.environ for var in ["ODOO_URL", "ODOO_DB", "ODOO_USERNAME", "ODOO_PASSWORD"] ): return { "url": os.environ["ODOO_URL"], "db": os.environ["ODOO_DB"], "username": os.environ["ODOO_USERNAME"], "password": os.environ["ODOO_PASSWORD"], } # Fall back to JSON config files config_paths = [ "./odoo_config.json", os.path.expanduser("~/.config/odoo/config.json"), os.path.expanduser("~/.odoo_config.json"), ] for path in config_paths: expanded_path = os.path.expanduser(path) if os.path.exists(expanded_path): print(f"Loading configuration from: {expanded_path}", file=sys.stderr) with open(expanded_path, "r") as f: return json.load(f) raise FileNotFoundError( "No Odoo configuration found. Please create a .env file, set environment variables, or create an odoo_config.json file.\n" "Searched for .env in:\n " + "\n ".join(env_paths) + "\n" "Searched for JSON config in:\n " + "\n ".join(config_paths) ) def get_odoo_client(): """ Get a configured Odoo client instance Supports both JSON-RPC (legacy) and JSON-2 API (Odoo 19+) Environment variables: ODOO_API_VERSION: "json-rpc" (default) or "json-2" ODOO_API_KEY: API key for JSON-2 (replaces password) ODOO_PASSWORD: Password for JSON-RPC (deprecated in Odoo 20) Returns: OdooClient: A configured Odoo client instance """ config = load_config() # Get API version preference api_version = os.environ.get("ODOO_API_VERSION", "json-rpc").lower() # Get authentication credentials api_key = os.environ.get("ODOO_API_KEY") password = config.get("password") or os.environ.get("ODOO_PASSWORD") # Get additional options from environment variables timeout = int( os.environ.get("ODOO_TIMEOUT", "30") ) # Increase default timeout to 30 seconds verify_ssl = os.environ.get("ODOO_VERIFY_SSL", "1").lower() in ["1", "true", "yes"] # Print detailed configuration print("Odoo client configuration:", file=sys.stderr) print(f" URL: {config['url']}", file=sys.stderr) print(f" Database: {config['db']}", file=sys.stderr) print(f" Username: {config['username']}", file=sys.stderr) print(f" API Version: {api_version}", file=sys.stderr) if api_version == "json-2": print(f" Auth: API Key ({'set' if api_key else 'NOT SET'})", file=sys.stderr) else: print(f" Auth: Password ({'set' if password else 'NOT SET'})", file=sys.stderr) print(f" Timeout: {timeout}s", file=sys.stderr) print(f" Verify SSL: {verify_ssl}", file=sys.stderr) return OdooClient( url=config["url"], db=config["db"], username=config["username"], password=password, api_key=api_key, api_version=api_version, timeout=timeout, verify_ssl=verify_ssl, )

Latest Blog Posts

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/AlanOgic/mcp-odoo-adv'

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