""" Base Plugin Interface All project plugins must inherit from this base class. This ensures consistency across different project types. """ import logging from abc import ABC, abstractmethod from typing import Any logger = logging.getLogger(__name__) class BasePlugin(ABC): """ Base class for all project plugins. Each plugin represents a specific project type (WordPress, Supabase, etc.) and provides MCP tools for managing that project. """ def __init__(self, config: dict[str, Any], project_id: str | None = None): """ Initialize plugin with project configuration. **Option B Architecture (New)**: plugin = WordPressPlugin(config) # project_id extracted from config or generated **Legacy Architecture**: plugin = WordPressPlugin(config, project_id="wordpress_site1") # Explicit project_id Args: config: Project-specific configuration (URLs, credentials, etc.) project_id: Optional unique identifier (auto-generated if not provided) """ # Auto-generate project_id if not provided (Option B) if project_id is None: # Generate from class name + config class_name = self.__class__.__name__.lower().replace("plugin", "") site_id = config.get("site_id", config.get("url", "unknown")) # Simple hash for unique ID import hashlib if isinstance(site_id, str): hash_suffix = hashlib.md5(site_id.encode()).hexdigest()[:8] project_id = f"{class_name}_{hash_suffix}" else: import time project_id = f"{class_name}_{int(time.time())}" self.project_id = project_id self.config = config self.logger = logging.getLogger(f"{self.__class__.__name__}.{project_id}") # Validate required configuration self._validate_config() self.logger.info(f"Initialized plugin for project: {project_id}") @abstractmethod def get_plugin_name(self) -> str: """ Return the plugin type name (e.g., 'wordpress', 'supabase'). Returns: str: Plugin type identifier """ pass @staticmethod def get_tool_specifications() -> list[dict[str, Any]]: """ Return tool specifications for Option B architecture (ToolGenerator). This is a STATIC method that returns tool specifications without needing a plugin instance. ToolGenerator uses these specifications to create unified tools with site parameter routing. Each specification should contain: - name: Tool name (without site prefix) - method_name: Method to call on plugin instance - description: What the tool does - schema: JSON Schema for input validation (without site parameter) - scope: Required scope (read, write, admin) Example: [ { "name": "list_posts", "method_name": "list_posts", "description": "List WordPress posts", "schema": {...}, "scope": "read" } ] Returns: List[Dict]: List of tool specifications for ToolGenerator Note: Override this in subclasses for Option B architecture. If not implemented, returns empty list (legacy plugins). """ return [] def get_tools(self) -> list[dict[str, Any]]: """ Return list of MCP tools provided by this plugin (LEGACY). **DEPRECATED in Option B Architecture** This method is kept for backward compatibility with legacy per-site tool architecture. New plugins should implement get_tool_specifications() instead, which is used by ToolGenerator for unified tools. Legacy per-site tools format: - name: Tool name (prefixed with plugin type and project_id) - description: What the tool does - inputSchema: JSON Schema for input validation - handler: Async function that implements the tool Returns: List[Dict]: List of tool definitions (empty for Option B plugins) Note: Option B plugins should return [] here as tools are registered via get_tool_specifications() + ToolGenerator instead. """ return [] def _validate_config(self) -> None: """ Validate that required configuration keys are present. Override in subclasses to add specific validation. """ required_keys = self.get_required_config_keys() missing_keys = [key for key in required_keys if key not in self.config] if missing_keys: raise ValueError( f"Missing required configuration keys for {self.project_id}: " f"{', '.join(missing_keys)}" ) def get_required_config_keys(self) -> list[str]: """ Return list of required configuration keys. Override in subclasses. Returns: List[str]: Required config keys """ return [] async def health_check(self) -> dict[str, Any]: """ Check if the project is accessible and healthy. Override in subclasses to implement specific health checks. Returns: Dict with 'healthy' (bool) and 'message' (str) keys """ return {"healthy": True, "message": "Health check not implemented for this plugin"} def get_project_info(self) -> dict[str, Any]: """ Return basic information about this project instance. Returns: Dict with project metadata """ return { "project_id": self.project_id, "plugin_type": self.get_plugin_name(), "config_keys": list(self.config.keys()), } def _create_tool_name(self, action: str) -> str: """ Create a standardized tool name for per-site tools. FORMAT: {plugin_type}_{site_id}_{action} Example: wordpress_site1_list_posts This is used for backward-compatible per-site tools. Unified tools (wordpress_list_posts) are created separately by UnifiedToolGenerator. Args: action: The action this tool performs Returns: str: Formatted tool name with project_id for per-site tools """ # Extract just the site_id from project_id (e.g., "site1" from "wordpress_site1") # project_id format is {plugin_type}_{site_id} site_id = self.project_id.replace(f"{self.get_plugin_name()}_", "") return f"{self.get_plugin_name()}_{site_id}_{action}" def _format_error_response(self, error: Exception, action: str) -> str: """ Format an error into a user-friendly message. Args: error: The exception that occurred action: The action that was being performed Returns: str: Formatted error message """ error_msg = f"Error performing {action} on {self.project_id}: {str(error)}" self.logger.error(error_msg, exc_info=True) return error_msg def _format_success_response(self, data: Any, action: str) -> str: """ Format a successful response. Args: data: The data to return action: The action that was performed Returns: str: Formatted success message """ if isinstance(data, (dict, list)): import json return json.dumps(data, indent=2, ensure_ascii=False) return str(data) class PluginRegistry: """ Registry for managing available plugin types. """ def __init__(self): self._plugin_classes: dict[str, type] = {} self.logger = logging.getLogger("PluginRegistry") def register(self, plugin_type: str, plugin_class: type) -> None: """ Register a plugin class. Args: plugin_type: Type identifier (e.g., 'wordpress') plugin_class: Plugin class (must inherit from BasePlugin) """ if not issubclass(plugin_class, BasePlugin): raise TypeError(f"{plugin_class} must inherit from BasePlugin") self._plugin_classes[plugin_type] = plugin_class self.logger.info(f"Registered plugin type: {plugin_type}") def create_instance( self, plugin_type: str, project_id: str, config: dict[str, Any] ) -> BasePlugin: """ Create a plugin instance. **Option B Compatible**: Uses new BasePlugin signature (config, project_id) Args: plugin_type: Type of plugin to create project_id: Unique project identifier config: Project configuration Returns: BasePlugin: Instantiated plugin Raises: KeyError: If plugin_type is not registered """ if plugin_type not in self._plugin_classes: raise KeyError(f"Unknown plugin type: {plugin_type}") plugin_class = self._plugin_classes[plugin_type] # Option B signature: config first, project_id optional return plugin_class(config, project_id=project_id) def get_registered_types(self) -> list[str]: """Get list of registered plugin types.""" return list(self._plugin_classes.keys()) def is_registered(self, plugin_type: str) -> bool: """Check if a plugin type is registered.""" return plugin_type in self._plugin_classes