Plugin Reference
Related Topics: Python Reference (runtime API) | DuckDB Integration (extensions) | Type System (type mapping)
MXCP plugins extend DuckDB with custom User Defined Functions (UDFs) written in Python. Plugins provide domain-specific functionality, API integrations, and custom data processing.
Overview
Section titled “Overview”Plugins are Python modules that:
- Inherit from
MXCPBasePlugin - Use
@udfdecorator to expose methods as SQL functions - Support automatic DuckDB type mapping
- Access authenticated user context
- Have lifecycle hooks for resource management
- Support hot reload - plugins are re-initialized when configuration changes
Quick Start
Section titled “Quick Start”1. Define Plugin in Site Config
Section titled “1. Define Plugin in Site Config”plugin: - name: my_cipher # Instance name module: my_plugin # Python module config: dev_config # Config reference2. Configure Settings
Section titled “2. Configure Settings”projects: my-project: profiles: dev: plugin: config: dev_config: rotation: "13" enable_logging: "true"3. Create Plugin Module
Section titled “3. Create Plugin Module”from typing import Dict, Anyfrom mxcp.plugins import MXCPBasePlugin, udf
class MXCPPlugin(MXCPBasePlugin): def __init__(self, config: Dict[str, Any], user_context=None): super().__init__(config, user_context) self.rotation = int(config.get("rotation", 13))
@udf def encrypt(self, text: str) -> str: """Encrypt text using Caesar cipher.""" return self._rotate_text(text, self.rotation)
@udf def decrypt(self, text: str) -> str: """Decrypt text using Caesar cipher.""" return self._rotate_text(text, -self.rotation)
def _rotate_text(self, text: str, shift: int) -> str: # Implementation result = [] for char in text: if char.isalpha(): base = ord('A') if char.isupper() else ord('a') result.append(chr((ord(char) - base + shift) % 26 + base)) else: result.append(char) return ''.join(result)4. Use in SQL
Section titled “4. Use in SQL”Functions are named {function_name}_{plugin_instance_name}:
SELECT encrypt_my_cipher('Hello World') as encrypted;SELECT decrypt_my_cipher(encrypted) as decrypted;Configuration
Section titled “Configuration”Site Configuration
Section titled “Site Configuration”plugin: - name: string_utils # Required: Instance name module: utils.strings # Required: Python module config: default # Optional: Config name
- name: api_client module: integrations.api config: api_settings
- name: simple_plugin module: simple # No config = empty {}User Configuration
Section titled “User Configuration”projects: my-project: profiles: dev: plugin: config: default: api_key: "${API_KEY}" # Environment variable timeout: "30" debug: "true"
api_settings: base_url: "https://api.example.com" rate_limit: "100"Plugin Structure
Section titled “Plugin Structure”Required Elements
Section titled “Required Elements”from typing import Dict, Anyfrom mxcp.plugins import MXCPBasePlugin, udf
class MXCPPlugin(MXCPBasePlugin): def __init__(self, config: Dict[str, Any], user_context=None): super().__init__(config, user_context) # Initialize plugin state
@udf def my_function(self, param: str) -> str: """Function documentation.""" return process(param)UDF Requirements
Section titled “UDF Requirements”- Must have
@udfdecorator - Complete type hints for all parameters and return
- First parameter is
self(handled automatically) - Type hints generate DuckDB signatures
Type Mapping
Section titled “Type Mapping”Basic Types
Section titled “Basic Types”| Python Type | DuckDB Type | Example |
|---|---|---|
str | VARCHAR | "hello" |
int | INTEGER | 42 |
float | DOUBLE | 3.14 |
bool | BOOLEAN | True |
bytes | BLOB | b"data" |
Decimal | DECIMAL | Decimal("123.45") |
Date/Time Types
Section titled “Date/Time Types”| Python Type | DuckDB Type | Example |
|---|---|---|
date | DATE | date(2024, 1, 1) |
time | TIME | time(14, 30) |
datetime | TIMESTAMP | datetime.now() |
timedelta | INTERVAL | timedelta(hours=1) |
Complex Types
Section titled “Complex Types”| Python Type | DuckDB Type | Example |
|---|---|---|
list[T] | T[] | [1, 2, 3] |
dict[K, V] | MAP(K, V) | {"key": "value"} |
Optional[T] | Nullable T | None or value |
Struct Types
Section titled “Struct Types”Use a dataclass to define the struct schema, but return a dict with matching keys:
from dataclasses import dataclass
@dataclassclass UserInfo: name: str age: int active: bool
@udfdef create_user(self, name: str, age: int) -> UserInfo: # Return a dict with keys matching the dataclass fields return {"name": name, "age": age, "active": True}Note: The dataclass defines the DuckDB STRUCT schema. At runtime, return a dict with matching keys, not a dataclass instance.
Authentication Integration
Section titled “Authentication Integration”Accessing User Context
Section titled “Accessing User Context”class MXCPPlugin(MXCPBasePlugin): def __init__(self, config: Dict[str, Any], user_context=None): super().__init__(config, user_context)
@udf def get_current_user(self) -> str: """Get authenticated user's username.""" if self.is_authenticated(): return self.get_username() or "unknown" return "not authenticated"User Context Methods
Section titled “User Context Methods”# Check authenticationself.is_authenticated() -> bool
# User informationself.get_username() -> Optional[str]self.get_user_email() -> Optional[str]self.get_user_provider() -> Optional[str] # 'github', 'atlassian', etc.
# OAuth token for API callsself.get_user_token() -> Optional[str]
# Full context objectself.user_context -> Optional[UserContext]External API Calls
Section titled “External API Calls”import httpx
@udfasync def fetch_user_repos(self) -> str: """Fetch GitHub repositories using user's token.""" if not self.is_authenticated(): return "Authentication required"
token = self.get_user_token() if not token: return "No external token available"
async with httpx.AsyncClient() as client: response = await client.get( "https://api.github.com/user/repos", headers={"Authorization": f"Bearer {token}"} ) repos = response.json() return f"Found {len(repos)} repositories"Lifecycle Management
Section titled “Lifecycle Management”Plugins have a formal lifecycle that allows for graceful startup and shutdown:
- Initialization: When the server starts, plugin instances are created via
__init__ - Registration: Each plugin instance is registered in a global registry
- Shutdown: On server shutdown or reload, shutdown hooks are called to clean up resources
- Hot Reload: During configuration reload, plugins are gracefully shut down and re-initialized
Shutdown Hook (Override Method)
Section titled “Shutdown Hook (Override Method)”import httpx
class MXCPPlugin(MXCPBasePlugin): def __init__(self, config: Dict[str, Any], user_context=None): super().__init__(config, user_context) self.client = httpx.Client(base_url=config.get("api_url"))
def shutdown(self): """Called on server shutdown or reload.""" if hasattr(self, 'client'): self.client.close()
@udf def fetch_data(self, endpoint: str) -> str: return self.client.get(endpoint).textShutdown Behavior
Section titled “Shutdown Behavior”Important notes about shutdown execution:
- Use
shutdown()method: Override theshutdown()method for cleanup logic that needs access to instance state (self) - Reverse order: Shutdown is called in reverse order of plugin registration (last registered, first called)
- Error resilience: If shutdown raises an exception, it’s logged but other plugins continue shutting down
- Hot reload: Shutdown is triggered during configuration hot reloads
Note: The
@on_shutdowndecorator exists but is designed for module-level functions, not instance methods. For plugin cleanup, always override theshutdown()method instead.
Advanced Examples
Section titled “Advanced Examples”File Processing Plugin
Section titled “File Processing Plugin”import base64from pathlib import Path
class MXCPPlugin(MXCPBasePlugin): def __init__(self, config: Dict[str, Any], user_context=None): super().__init__(config, user_context) self.base_path = Path(config.get("base_path", "."))
@udf def read_file(self, filename: str) -> str: """Read file contents as string.""" file_path = self.base_path / filename if not file_path.exists(): return f"File not found: {filename}" return file_path.read_text()
@udf def read_file_base64(self, filename: str) -> str: """Read file contents as base64.""" file_path = self.base_path / filename if not file_path.exists(): return f"File not found: {filename}" return base64.b64encode(file_path.read_bytes()).decode('ascii')
@udf def list_files(self, pattern: str) -> list[str]: """List files matching pattern.""" return [str(p.name) for p in self.base_path.glob(pattern)]Web API Plugin
Section titled “Web API Plugin”import httpx
class MXCPPlugin(MXCPBasePlugin): def __init__(self, config: Dict[str, Any], user_context=None): super().__init__(config, user_context) self.api_key = config.get("api_key") self.base_url = config.get("base_url")
@udf def fetch_weather(self, city: str) -> str: """Fetch weather data for a city.""" with httpx.Client() as client: response = client.get( f"{self.base_url}/weather", params={"q": city, "appid": self.api_key} ) if response.status_code == 200: data = response.json() return f"{city}: {data['main']['temp']}C" return f"Error fetching weather for {city}"
@udf def geocode(self, address: str) -> dict[str, float]: """Geocode address to coordinates.""" # Returns MAP(VARCHAR, DOUBLE) in DuckDB return {"lat": 40.7128, "lng": -74.0060}SQL Usage Patterns
Section titled “SQL Usage Patterns”Basic Usage
Section titled “Basic Usage”-- Simple function callSELECT encrypt_cipher('secret') as encrypted;
-- With table dataSELECT id, original_text, encrypt_cipher(original_text) as encryptedFROM documents;Complex Queries
Section titled “Complex Queries”-- Arrays and mapsSELECT list_files_processor('*.csv') as csv_files, geocode_location('123 Main St') as coords;
-- WHERE clauseSELECT * FROM usersWHERE validate_email_utils(email) = true;
-- AggregationsSELECT category, SUM(calculate_score_analytics(data)) as total_scoreFROM analyticsGROUP BY category;Authentication-Aware
Section titled “Authentication-Aware”-- User-specific processingSELECT id, encrypt_with_user_cipher(content) as encryptedFROM documentsWHERE owner = get_username();
-- External APISELECT fetch_user_repos_github() as repos;Best Practices
Section titled “Best Practices”1. Error Handling
Section titled “1. Error Handling”@udfdef safe_divide(self, a: float, b: float) -> float: try: if b == 0: return float('inf') return a / b except Exception: return float('nan')2. Configuration Validation
Section titled “2. Configuration Validation”def __init__(self, config: Dict[str, Any], user_context=None): super().__init__(config, user_context)
if "api_key" not in config: raise ValueError("api_key is required")
self.timeout = int(config.get("timeout", "30")) if self.timeout <= 0: raise ValueError("timeout must be positive")3. Resource Management
Section titled “3. Resource Management”# Short-lived: context manager@udfdef query_db(self, query: str) -> int: with psycopg2.connect(self._config["url"]) as conn: with conn.cursor() as cur: cur.execute(query) return cur.rowcount
# Long-lived: lifecycle hooksclass DatabasePlugin(MXCPBasePlugin): def __init__(self, config: Dict[str, Any], user_context=None): super().__init__(config, user_context) self.pool = create_pool(config["url"])
def shutdown(self): self.pool.close()4. Complete Type Hints
Section titled “4. Complete Type Hints”# Good - complete hints@udfdef process(self, items: list[str], limit: int) -> dict[str, int]: return {"processed": len(items[:limit])}
# Bad - missing hints (will be skipped!)@udfdef process(self, items, limit): return {"processed": len(items[:limit])}5. Documentation
Section titled “5. Documentation”@udfdef complex_calc(self, data: list[float], threshold: float) -> dict[str, float]: """Perform statistical calculation on data.
Calculates mean, std dev, and percentage above threshold.
Args: data: List of numeric values threshold: Threshold for percentage calculation
Returns: Dictionary with 'mean', 'std_dev', 'pct_above_threshold'
Example: SELECT complex_calc_stats([1.0, 2.0, 3.0], 2.0); """ # ImplementationProject Structure
Section titled “Project Structure”my-project/├── mxcp-site.yml├── plugins/│ ├── my_plugin/│ │ └── __init__.py│ ├── utils/│ │ └── strings.py│ └── integrations/│ └── api.py├── tools/├── resources/└── sql/Troubleshooting
Section titled “Troubleshooting”Plugin Not Loading
Section titled “Plugin Not Loading”- Check module is in
plugins/directory - Verify
MXCPPluginclass exists - Check YAML syntax
UDF Not Available
Section titled “UDF Not Available”- Ensure
@udfdecorator - Verify complete type hints
- Check naming:
{function}_{instance_name}
Type Errors
Section titled “Type Errors”- All parameters need type hints
- Use supported DuckDB types
- Avoid
Anytype
Configuration Issues
Section titled “Configuration Issues”- Config name must match user config
- Check
${VAR}syntax for env vars - Verify required keys exist
# Enable debug loggingmxcp serve --debug-- List available functionsSELECT function_name FROM duckdb_functions()WHERE function_name LIKE '%_pluginname';Next Steps
Section titled “Next Steps”- Python Reference - Runtime API
- SQL Reference - SQL capabilities
- Authentication - User context