Skip to main content
Glama

MCP Server for Odoo

uri_schema.py•9.78 kB
"""URI schema module for Odoo MCP Server. This module implements the URI schema for accessing Odoo data through MCP resources. It provides parsing, validation, and building utilities for Odoo URIs following the pattern: odoo://{model}/{operation}?{parameters} Supported operations: - record/{id}: Fetch a specific record - search: Search for records using domain - browse: Retrieve multiple records by IDs - count: Count matching records - fields: Get field definitions """ import re import urllib.parse from dataclasses import dataclass from enum import Enum from typing import Dict, List, Optional class OdooOperation(Enum): """Supported Odoo URI operations.""" RECORD = "record" SEARCH = "search" BROWSE = "browse" COUNT = "count" FIELDS = "fields" @dataclass class OdooURI: """Parsed Odoo URI representation.""" model: str operation: OdooOperation record_id: Optional[int] = None domain: Optional[str] = None fields: Optional[List[str]] = None limit: Optional[int] = None offset: Optional[int] = None order: Optional[str] = None ids: Optional[List[int]] = None def to_uri(self) -> str: """Convert the parsed URI back to string format.""" return build_uri( model=self.model, operation=self.operation.value, record_id=self.record_id, domain=self.domain, fields=self.fields, limit=self.limit, offset=self.offset, order=self.order, ids=self.ids, ) class URIError(Exception): """Base exception for URI-related errors.""" pass class URIParseError(URIError): """Exception raised when URI parsing fails.""" pass class URIValidationError(URIError): """Exception raised when URI validation fails.""" pass # URI pattern for matching odoo:// URIs # This pattern is permissive in what it captures, validation happens later # It requires at least one non-slash character for the model name URI_PATTERN = re.compile(r"^odoo://([^/]+)/([^/?]+)(?:/(\d+))?(?:\?(.*))?$") def parse_uri(uri: str) -> OdooURI: """Parse an Odoo URI string into its components. Args: uri: The URI string to parse (e.g., "odoo://res.partner/search?domain=[('is_company','=',True)]") Returns: OdooURI object with parsed components Raises: URIParseError: If the URI format is invalid URIValidationError: If the URI components are invalid """ if not uri.startswith("odoo://"): raise URIParseError(f"URI must start with 'odoo://', got: {uri}") match = URI_PATTERN.match(uri) if not match: raise URIParseError(f"Invalid URI format: {uri}") model, operation_str, record_id_str, query_string = match.groups() # Validate model name (check for empty model first) if not model: raise URIValidationError("Invalid model name: empty model name") if not _is_valid_model_name(model): raise URIValidationError(f"Invalid model name: {model}") # Parse operation and record ID try: if operation_str == "record" and record_id_str: operation = OdooOperation.RECORD record_id = int(record_id_str) elif operation_str in [op.value for op in OdooOperation]: operation = OdooOperation(operation_str) record_id = None else: raise URIValidationError(f"Invalid operation: {operation_str}") except ValueError: raise URIValidationError(f"Invalid record ID: {record_id_str}") from None # Validate operation-specific requirements if operation == OdooOperation.RECORD and not record_id: raise URIValidationError("Record operation requires an ID") # Parse query parameters params = _parse_query_parameters(query_string) if query_string else {} # Extract and validate parameters domain = params.get("domain") fields = _parse_fields_parameter(params.get("fields")) limit = _parse_int_parameter(params.get("limit"), "limit") offset = _parse_int_parameter(params.get("offset"), "offset") order = params.get("order") ids = _parse_ids_parameter(params.get("ids")) # Validate operation-specific parameters if operation == OdooOperation.BROWSE and not ids: raise URIValidationError("Browse operation requires 'ids' parameter") return OdooURI( model=model, operation=operation, record_id=record_id, domain=domain, fields=fields, limit=limit, offset=offset, order=order, ids=ids, ) def build_uri( model: str, operation: str, record_id: Optional[int] = None, domain: Optional[str] = None, fields: Optional[List[str]] = None, limit: Optional[int] = None, offset: Optional[int] = None, order: Optional[str] = None, ids: Optional[List[int]] = None, ) -> str: """Build an Odoo URI from components. Args: model: Odoo model name (e.g., "res.partner") operation: Operation type (record, search, browse, count, fields) record_id: Record ID for record operation domain: Odoo domain expression (should be URL-encoded if needed) fields: List of field names to return limit: Maximum number of records offset: Pagination offset order: Sorting criteria ids: List of IDs for browse operation Returns: Formatted URI string Raises: URIValidationError: If the components are invalid """ # Validate model name if not _is_valid_model_name(model): raise URIValidationError(f"Invalid model name: {model}") # Validate operation if operation not in [op.value for op in OdooOperation]: raise URIValidationError(f"Invalid operation: {operation}") # Build base URI if operation == "record": if not record_id: raise URIValidationError("Record operation requires an ID") uri = f"odoo://{model}/record/{record_id}" else: uri = f"odoo://{model}/{operation}" # Add query parameters params = {} if domain is not None: params["domain"] = domain if fields: params["fields"] = ",".join(fields) if limit is not None: params["limit"] = str(limit) if offset is not None: params["offset"] = str(offset) if order is not None: params["order"] = order if ids: params["ids"] = ",".join(str(id_val) for id_val in ids) if params: query_string = urllib.parse.urlencode(params) uri = f"{uri}?{query_string}" return uri def build_search_uri( model: str, domain: Optional[str] = None, fields: Optional[List[str]] = None, limit: Optional[int] = None, offset: Optional[int] = None, order: Optional[str] = None, ) -> str: """Build a search URI for the given model and parameters. This is a convenience function for building search URIs. """ return build_uri( model, "search", domain=domain, fields=fields, limit=limit, offset=offset, order=order ) def build_record_uri(model: str, record_id: int) -> str: """Build a record URI for a specific record. This is a convenience function for building record URIs. """ return build_uri(model, "record", record_id=record_id) def build_pagination_uri(base_uri: str, offset: int, limit: int) -> str: """Build a pagination URI from a base URI. Args: base_uri: The base URI to paginate offset: The new offset value limit: The limit value Returns: URI with updated offset and limit parameters """ parsed = parse_uri(base_uri) parsed.offset = offset parsed.limit = limit return parsed.to_uri() def extract_model_from_uri(uri: str) -> str: """Extract just the model name from a URI. Args: uri: The URI to extract from Returns: The model name Raises: URIParseError: If the URI is invalid """ parsed = parse_uri(uri) return parsed.model def _is_valid_model_name(model: str) -> bool: """Check if a model name is valid. Odoo model names must: - Start with a letter - Contain only letters, numbers, dots, and underscores - Not be empty """ if not model: return False return bool(re.match(r"^[a-zA-Z][a-zA-Z0-9_.]*$", model)) def _parse_query_parameters(query_string: str) -> Dict[str, str]: """Parse URL query parameters.""" # parse_qsl returns a list of tuples, filter out empty values params = {} for key, value in urllib.parse.parse_qsl(query_string, keep_blank_values=True): params[key] = value return params def _parse_fields_parameter(fields_str: Optional[str]) -> Optional[List[str]]: """Parse comma-separated fields parameter.""" if not fields_str: return None return [f.strip() for f in fields_str.split(",") if f.strip()] def _parse_int_parameter(value: Optional[str], param_name: str) -> Optional[int]: """Parse an integer parameter.""" if value is None: return None try: result = int(value) if result < 0: raise URIValidationError(f"{param_name} must be non-negative, got: {result}") return result except ValueError: raise URIValidationError(f"Invalid {param_name} value: {value}") from None def _parse_ids_parameter(ids_str: Optional[str]) -> Optional[List[int]]: """Parse comma-separated IDs parameter.""" if not ids_str: return None try: return [int(id_str.strip()) for id_str in ids_str.split(",") if id_str.strip()] except ValueError: raise URIValidationError(f"Invalid IDs parameter: {ids_str}") from 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