Odoo MCP Server

by tuanle96
Verified
""" Odoo XML-RPC client for MCP server integration """ import json import os import re import socket import urllib.parse import http.client import xmlrpc.client class OdooClient: """Client for interacting with Odoo via XML-RPC""" def __init__( self, url, db, username, password, timeout=10, 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 timeout: Connection timeout in seconds verify_ssl: Whether to verify SSL certificates """ # 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.uid = None # Set timeout and SSL verification self.timeout = timeout self.verify_ssl = verify_ssl # Setup connections self._common = None self._models = None # Parse hostname for logging parsed_url = urllib.parse.urlparse(self.url) self.hostname = parsed_url.netloc # Connect self._connect() def _connect(self): """Initialize the XML-RPC connection and authenticate""" # Tạo transport với timeout phù hợp is_https = self.url.startswith("https://") transport = RedirectTransport( timeout=self.timeout, use_https=is_https, verify_ssl=self.verify_ssl ) print(f"Connecting to Odoo at: {self.url}", file=os.sys.stderr) print(f" Hostname: {self.hostname}", file=os.sys.stderr) print( f" Timeout: {self.timeout}s, Verify SSL: {self.verify_ssl}", file=os.sys.stderr, ) # Thiết lập endpoints self._common = xmlrpc.client.ServerProxy( f"{self.url}/xmlrpc/2/common", transport=transport ) self._models = xmlrpc.client.ServerProxy( f"{self.url}/xmlrpc/2/object", transport=transport ) # Xác thực và lấy user ID print( f"Authenticating with database: { self.db}, username: {self.username}", file=os.sys.stderr, ) try: print( f"Making request to { self.hostname}/xmlrpc/2/common (attempt 1)", file=os.sys.stderr, ) self.uid = self._common.authenticate( self.db, self.username, self.password, {} ) if not self.uid: raise ValueError( "Authentication failed: Invalid username or password") except (socket.error, socket.timeout, ConnectionError, TimeoutError) as e: print(f"Connection error: {str(e)}", file=os.sys.stderr) raise ConnectionError( f"Failed to connect to Odoo server: {str(e)}") except Exception as e: print(f"Authentication error: {str(e)}", file=os.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""" return self._models.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=os.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=os.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=os.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=os.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=os.sys.stderr) return [] class RedirectTransport(xmlrpc.client.Transport): """Transport that adds timeout, SSL verification, and redirect handling""" def __init__( self, timeout=10, use_https=True, verify_ssl=True, max_redirects=5, proxy=None ): super().__init__() self.timeout = timeout self.use_https = use_https self.verify_ssl = verify_ssl self.max_redirects = max_redirects self.proxy = proxy or os.environ.get("HTTP_PROXY") if use_https and not verify_ssl: import ssl self.context = ssl._create_unverified_context() def make_connection(self, host): if self.proxy: proxy_url = urllib.parse.urlparse(self.proxy) connection = http.client.HTTPConnection( proxy_url.hostname, proxy_url.port, timeout=self.timeout ) connection.set_tunnel(host) else: if self.use_https and not self.verify_ssl: connection = http.client.HTTPSConnection( host, timeout=self.timeout, context=self.context ) else: if self.use_https: connection = http.client.HTTPSConnection( host, timeout=self.timeout) else: connection = http.client.HTTPConnection( host, timeout=self.timeout) return connection def request(self, host, handler, request_body, verbose): """Send HTTP request with retry for redirects""" redirects = 0 while redirects < self.max_redirects: try: print(f"Making request to {host}{handler}", file=os.sys.stderr) return super().request(host, handler, request_body, verbose) except xmlrpc.client.ProtocolError as err: if err.errcode in (301, 302, 303, 307, 308) and err.headers.get( "location" ): redirects += 1 location = err.headers.get("location") parsed = urllib.parse.urlparse(location) if parsed.netloc: host = parsed.netloc handler = parsed.path if parsed.query: handler += "?" + parsed.query else: raise except Exception as e: print(f"Error during request: {str(e)}", file=os.sys.stderr) raise raise xmlrpc.client.ProtocolError( host + handler, 310, "Too many redirects", {}) def load_config(): """ Load Odoo configuration from environment variables or config file Returns: dict: Configuration dictionary with url, db, username, password """ # Define config file paths to check config_paths = [ "./odoo_config.json", os.path.expanduser("~/.config/odoo/config.json"), os.path.expanduser("~/.odoo_config.json"), ] # Try environment variables first 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"], } # Try to load from file for path in config_paths: expanded_path = os.path.expanduser(path) if os.path.exists(expanded_path): with open(expanded_path, "r") as f: return json.load(f) raise FileNotFoundError( "No Odoo configuration found. Please create an odoo_config.json file or set environment variables." ) def get_odoo_client(): """ Get a configured Odoo client instance Returns: OdooClient: A configured Odoo client instance """ config = load_config() # 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=os.sys.stderr) print(f" URL: {config['url']}", file=os.sys.stderr) print(f" Database: {config['db']}", file=os.sys.stderr) print(f" Username: {config['username']}", file=os.sys.stderr) print(f" Timeout: {timeout}s", file=os.sys.stderr) print(f" Verify SSL: {verify_ssl}", file=os.sys.stderr) return OdooClient( url=config["url"], db=config["db"], username=config["username"], password=config["password"], timeout=timeout, verify_ssl=verify_ssl, )