""" MCP Endpoint Factory Creates and configures FastMCP instances for different endpoints. Each endpoint has its own set of tools based on configuration. """ import logging from collections.abc import Callable from functools import wraps from typing import TYPE_CHECKING, Any from fastmcp import FastMCP from fastmcp.server.middleware import Middleware from .config import EndpointConfig if TYPE_CHECKING: from core.site_manager import SiteManager from core.tool_registry import ToolRegistry logger = logging.getLogger(__name__) class MCPEndpointFactory: """ Factory for creating scoped MCP endpoints. Each endpoint is a separate FastMCP instance with only the tools relevant to its configuration. """ def __init__( self, site_manager: "SiteManager", tool_registry: "ToolRegistry", middleware_classes: list[type] | None = None, ): """ Initialize the endpoint factory. Args: site_manager: Site manager for accessing site configurations tool_registry: Central tool registry middleware_classes: List of middleware classes to apply to endpoints """ self.site_manager = site_manager self.tool_registry = tool_registry self.middleware_classes = middleware_classes or [] self.endpoints: dict[str, FastMCP] = {} self._tool_counts: dict[str, int] = {} self._tool_handlers: dict[str, Callable] = {} def register_tool_handler(self, tool_name: str, handler: Callable): """ Register a tool handler function. Args: tool_name: Name of the tool handler: Async handler function for the tool """ self._tool_handlers[tool_name] = handler def create_endpoint( self, config: EndpointConfig, custom_middleware: list[Middleware] | None = None ) -> FastMCP: """ Create a new MCP endpoint with scoped tools. Args: config: Endpoint configuration custom_middleware: Additional middleware for this endpoint Returns: Configured FastMCP instance """ logger.info(f"Creating endpoint: {config.path} ({config.name})") # Create FastMCP instance mcp = FastMCP(config.name) # Get tools for this endpoint tools = self._get_tools_for_endpoint(config) logger.info(f" - Registering {len(tools)} tools for {config.path}") # Register tools for tool_info in tools: self._register_tool(mcp, tool_info, config) # Apply middleware if custom_middleware: for middleware in custom_middleware: mcp.add_middleware(middleware) # Store endpoint and tool count self.endpoints[config.path] = mcp self._tool_counts[config.path] = len(tools) logger.info(f" - Endpoint {config.path} created successfully") return mcp def _get_tools_for_endpoint(self, config: EndpointConfig) -> list[dict[str, Any]]: """ Get list of tools that should be available on this endpoint. Args: config: Endpoint configuration Returns: List of tool definitions """ tools = [] # Get all tools from registry all_tools = self.tool_registry.get_all() for tool_def in all_tools: tool_name = tool_def.name # Check plugin type filter plugin_type = self._extract_plugin_type(tool_name) if plugin_type and not config.allows_plugin(plugin_type): continue # Check tool whitelist/blacklist if not config.allows_tool(tool_name): continue # For project endpoints, filter by site if config.site_filter and plugin_type: # Tool should work with the specific site pass # Site filtering happens at execution time tools.append( { "name": tool_name, "description": tool_def.description, "parameters": tool_def.parameters, "handler": tool_def.handler, "plugin_type": plugin_type, } ) # Check max tools limit if len(tools) > config.max_tools: logger.warning( f"Endpoint {config.path} has {len(tools)} tools, " f"exceeding max_tools={config.max_tools}" ) return tools def _extract_plugin_type(self, tool_name: str) -> str | None: """ Extract plugin type from tool name. Args: tool_name: Name of the tool Returns: Plugin type or None for system tools """ # Check the multi-word WordPress variant BEFORE the bare # ``wordpress_`` prefix — tool names are # ``wordpress_specialist_wp_plugin_list``, # ``wordpress_create_post`` etc. (``wordpress_advanced`` was # sunset in F.19.3.2-.3 / 2026-05-04.) if tool_name.startswith("wordpress_specialist_"): return "wordpress_specialist" if tool_name.startswith("wordpress_"): return "wordpress" elif tool_name.startswith("woocommerce_"): return "woocommerce" elif tool_name.startswith("gitea_"): return "gitea" elif tool_name.startswith("n8n_"): return "n8n" elif tool_name.startswith("supabase_"): return "supabase" elif tool_name.startswith("openpanel_"): return "openpanel" elif tool_name.startswith("appwrite_"): return "appwrite" elif tool_name.startswith("directus_"): return "directus" # System tools have no plugin type return None def _register_tool(self, mcp: FastMCP, tool_info: dict[str, Any], config: EndpointConfig): """ Register a single tool with the FastMCP instance. Args: mcp: FastMCP instance tool_info: Tool definition config: Endpoint configuration """ tool_name = tool_info["name"] # Get handler handler = tool_info.get("handler") or self._tool_handlers.get(tool_name) if not handler: logger.warning(f"No handler found for tool: {tool_name}") return # Wrap handler with endpoint context wrapped_handler = self._wrap_handler(handler, tool_name, config) # Register with FastMCP # We need to create a function with the correct signature mcp.tool()(wrapped_handler) def _wrap_handler(self, handler: Callable, tool_name: str, config: EndpointConfig) -> Callable: """ Wrap a tool handler with endpoint-specific logic. Args: handler: Original handler function tool_name: Name of the tool config: Endpoint configuration Returns: Wrapped handler function """ @wraps(handler) async def wrapped(*args, **kwargs): # For project endpoints, always inject site filter # This locks all tools to the specific project's site if config.site_filter: # Extract site_id from project's site_filter (format: plugin_type_site_id) # The site parameter expects just the site identifier (site_id or alias) if "_" in config.site_filter: # site_filter is full_id like "wordpress_site1" parts = config.site_filter.split("_", 1) if len(parts) == 2: # Use the site_id part (site1) kwargs["site"] = parts[1] else: kwargs["site"] = config.site_filter else: kwargs["site"] = config.site_filter # Call original handler return await handler(*args, **kwargs) # Preserve function metadata wrapped.__name__ = tool_name wrapped.__doc__ = handler.__doc__ return wrapped def get_endpoint(self, path: str) -> FastMCP | None: """ Get an endpoint by path. Args: path: Endpoint path Returns: FastMCP instance or None """ return self.endpoints.get(path) def get_all_endpoints(self) -> dict[str, FastMCP]: """Get all registered endpoints""" return self.endpoints.copy() def get_endpoint_info(self) -> list[dict[str, Any]]: """ Get information about all endpoints. Returns: List of endpoint info dictionaries """ info = [] for path, mcp in self.endpoints.items(): tool_count = self._tool_counts.get(path, 0) info.append( { "path": path, "name": mcp.name, "tool_count": tool_count, } ) return info