Initial commit: MCP Hub Community Edition v3.0.0

Community edition generated from private repo via sync pipeline.
Includes 9 plugins (WordPress, WooCommerce, WP Advanced, Gitea, n8n,
Supabase, OpenPanel, Appwrite, Directus) with ~587 tools.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
airano
2026-02-17 08:34:44 +03:30
commit cf62e65c55
237 changed files with 87596 additions and 0 deletions

View File

@@ -0,0 +1,63 @@
"""
Multi-Endpoint Architecture for MCP Hub
This module provides a factory pattern for creating scoped MCP endpoints.
Each endpoint exposes only the tools relevant to its purpose.
Endpoints:
/mcp - Admin endpoint (all tools, requires Master API Key)
/mcp/wordpress - WordPress tools only (92 tools)
/mcp/wordpress-advanced - WordPress Advanced tools (22 tools)
/mcp/gitea - Gitea tools only (55 tools)
/mcp/project/{id} - Project-specific tools
Benefits:
- Better security: Users only see tools they can access
- Optimized context: Smaller tool lists for AI assistants
- Scalability: Easy to add new plugin endpoints
- Clear separation of concerns
"""
from .config import (
ENDPOINT_CONFIGS,
EndpointConfig,
EndpointType,
create_project_endpoint_config,
get_endpoint_config,
)
from .factory import MCPEndpointFactory
from .middleware import (
AuthContext,
EndpointAuditMiddleware,
EndpointAuthMiddleware,
EndpointRateLimitMiddleware,
create_endpoint_middleware,
)
from .registry import (
EndpointInfo,
EndpointRegistry,
get_endpoint_registry,
initialize_endpoint_registry,
)
__all__ = [
# Config
"EndpointConfig",
"EndpointType",
"ENDPOINT_CONFIGS",
"get_endpoint_config",
"create_project_endpoint_config",
# Factory
"MCPEndpointFactory",
# Registry
"EndpointRegistry",
"EndpointInfo",
"get_endpoint_registry",
"initialize_endpoint_registry",
# Middleware
"EndpointAuthMiddleware",
"EndpointRateLimitMiddleware",
"EndpointAuditMiddleware",
"create_endpoint_middleware",
"AuthContext",
]

350
core/endpoints/config.py Normal file
View File

@@ -0,0 +1,350 @@
"""
Endpoint Configuration Module
Defines configurations for different MCP endpoints.
Each endpoint has specific plugin types, scopes, and access requirements.
"""
from dataclasses import dataclass, field
from enum import Enum
class EndpointType(Enum):
"""Types of MCP endpoints"""
ADMIN = "admin"
SYSTEM = "system" # Phase X.3 - System tools only
WORDPRESS = "wordpress"
WOOCOMMERCE = "woocommerce"
WORDPRESS_ADVANCED = "wordpress_advanced"
GITEA = "gitea"
N8N = "n8n"
SUPABASE = "supabase" # Phase G
OPENPANEL = "openpanel" # Phase H
APPWRITE = "appwrite" # Phase I
DIRECTUS = "directus" # Phase J
PROJECT = "project" # Dynamic per-project endpoint
CUSTOM = "custom"
@dataclass
class EndpointConfig:
"""
Configuration for a single MCP endpoint.
Attributes:
path: URL mount path for this endpoint (e.g., "/wordpress" → /wordpress/mcp)
name: Display name for the endpoint
description: Human-readable description
endpoint_type: Type of endpoint (admin, wordpress, etc.)
plugin_types: List of plugin types to include
require_master_key: Whether Master API Key is required
allowed_scopes: Allowed API key scopes (empty = all)
tool_whitelist: Specific tools to include (None = all from plugins)
tool_blacklist: Specific tools to exclude
site_filter: Filter to specific site (for project endpoints)
max_tools: Maximum number of tools (for safety)
"""
path: str
name: str
description: str
endpoint_type: EndpointType
plugin_types: list[str] = field(default_factory=list)
require_master_key: bool = False
allowed_scopes: set[str] = field(default_factory=set)
tool_whitelist: set[str] | None = None
tool_blacklist: set[str] = field(default_factory=set)
site_filter: str | None = None
max_tools: int = 200
def __post_init__(self):
"""Validate configuration after initialization"""
if not self.path.startswith("/"):
raise ValueError(f"Endpoint path must start with '/': {self.path}")
if self.tool_whitelist and self.tool_blacklist:
overlap = self.tool_whitelist & self.tool_blacklist
if overlap:
raise ValueError(f"Tools cannot be in both whitelist and blacklist: {overlap}")
def allows_plugin(self, plugin_type: str) -> bool:
"""Check if this endpoint allows a specific plugin type"""
if not self.plugin_types:
return True # Empty list = all plugins
return plugin_type in self.plugin_types
def allows_tool(self, tool_name: str) -> bool:
"""Check if this endpoint allows a specific tool"""
# Check blacklist first
if tool_name in self.tool_blacklist:
return False
# If whitelist exists, tool must be in it
if self.tool_whitelist is not None:
return tool_name in self.tool_whitelist
return True
def allows_scope(self, scope: str) -> bool:
"""Check if this endpoint allows a specific API key scope"""
if not self.allowed_scopes:
return True # Empty set = all scopes
return scope in self.allowed_scopes
# Predefined endpoint configurations
ENDPOINT_CONFIGS = {
# Admin endpoint - all tools, requires Master API Key
# Mounted at "/" → /mcp (FastMCP adds /mcp automatically)
EndpointType.ADMIN: EndpointConfig(
path="/",
name="Coolify Admin",
description="Full administrative access to all tools and plugins",
endpoint_type=EndpointType.ADMIN,
plugin_types=[], # Empty = all plugins
require_master_key=True,
allowed_scopes={"admin"},
max_tools=400,
),
# System endpoint - system tools only (17 tools) - Phase X.3
# For API key management, OAuth, rate limiting without loading all plugins
EndpointType.SYSTEM: EndpointConfig(
path="/system",
name="System Manager",
description="System management tools (API keys, OAuth, health, rate limiting)",
endpoint_type=EndpointType.SYSTEM,
plugin_types=["system"], # Only system tools
require_master_key=True,
allowed_scopes={"admin"},
# Whitelist only system tools
tool_whitelist={
# API Key Management (6)
"manage_api_keys_create",
"manage_api_keys_list",
"manage_api_keys_get_info",
"manage_api_keys_revoke",
"manage_api_keys_delete",
"manage_api_keys_rotate",
# Health & Status (4)
"list_projects",
"get_endpoints",
"get_system_info",
"get_audit_log",
# OAuth Management (4)
"oauth_register_client",
"oauth_list_clients",
"oauth_revoke_client",
"oauth_get_client_info",
# Rate Limiting (3)
"get_rate_limit_stats",
"reset_rate_limit",
"set_rate_limit_config",
},
max_tools=20,
),
# WordPress endpoint - core WordPress tools only (64 tools)
EndpointType.WORDPRESS: EndpointConfig(
path="/wordpress",
name="WordPress Manager",
description="WordPress content management tools (posts, pages, media, SEO, menus)",
endpoint_type=EndpointType.WORDPRESS,
plugin_types=["wordpress"],
require_master_key=False,
allowed_scopes={"read", "write", "admin"},
# Blacklist system and admin tools
tool_blacklist={
"manage_api_keys_create",
"manage_api_keys_delete",
"manage_api_keys_rotate",
"oauth_register_client",
"oauth_revoke_client",
},
max_tools=70,
),
# WooCommerce endpoint - e-commerce tools (28 tools)
EndpointType.WOOCOMMERCE: EndpointConfig(
path="/woocommerce",
name="WooCommerce Manager",
description="WooCommerce e-commerce tools (products, orders, customers, coupons, reports)",
endpoint_type=EndpointType.WOOCOMMERCE,
plugin_types=["woocommerce"],
require_master_key=False,
allowed_scopes={"read", "write", "admin"},
# Blacklist system and admin tools
tool_blacklist={
"manage_api_keys_create",
"manage_api_keys_delete",
"manage_api_keys_rotate",
"oauth_register_client",
"oauth_revoke_client",
},
max_tools=35,
),
# WordPress Advanced endpoint - advanced operations
EndpointType.WORDPRESS_ADVANCED: EndpointConfig(
path="/wordpress-advanced",
name="WordPress Advanced",
description="WordPress advanced operations (database, bulk, system)",
endpoint_type=EndpointType.WORDPRESS_ADVANCED,
plugin_types=["wordpress_advanced"],
require_master_key=False,
allowed_scopes={"admin"}, # Admin scope required
max_tools=30,
),
# Gitea endpoint - Git repository management
EndpointType.GITEA: EndpointConfig(
path="/gitea",
name="Gitea Manager",
description="Git repository management tools (repos, issues, PRs)",
endpoint_type=EndpointType.GITEA,
plugin_types=["gitea"],
require_master_key=False,
allowed_scopes={"read", "write", "admin"},
# Blacklist system and admin tools
tool_blacklist={
"manage_api_keys_create",
"manage_api_keys_delete",
"manage_api_keys_rotate",
"oauth_register_client",
"oauth_revoke_client",
},
max_tools=60,
),
# n8n endpoint - Workflow automation management (60 tools)
EndpointType.N8N: EndpointConfig(
path="/n8n",
name="n8n Automation",
description="Workflow automation management (workflows, executions, credentials, tags)",
endpoint_type=EndpointType.N8N,
plugin_types=["n8n"],
require_master_key=False,
allowed_scopes={"read", "write", "admin"},
# Blacklist system and admin tools
tool_blacklist={
"manage_api_keys_create",
"manage_api_keys_delete",
"manage_api_keys_rotate",
"oauth_register_client",
"oauth_revoke_client",
},
max_tools=70,
),
# Supabase endpoint - Self-Hosted management (70 tools) - Phase G
EndpointType.SUPABASE: EndpointConfig(
path="/supabase",
name="Supabase Manager",
description="Supabase Self-Hosted management (database, auth, storage, functions, admin)",
endpoint_type=EndpointType.SUPABASE,
plugin_types=["supabase"],
require_master_key=False,
allowed_scopes={"read", "write", "admin"},
# Blacklist system and admin tools
tool_blacklist={
"manage_api_keys_create",
"manage_api_keys_delete",
"manage_api_keys_rotate",
"oauth_register_client",
"oauth_revoke_client",
},
max_tools=80,
),
# OpenPanel endpoint - Product Analytics (73 tools) - Phase H
EndpointType.OPENPANEL: EndpointConfig(
path="/openpanel",
name="OpenPanel Analytics",
description="OpenPanel product analytics management (events, export, funnels, dashboards)",
endpoint_type=EndpointType.OPENPANEL,
plugin_types=["openpanel"],
require_master_key=False,
allowed_scopes={"read", "write", "admin"},
# Blacklist system and admin tools
tool_blacklist={
"manage_api_keys_create",
"manage_api_keys_delete",
"manage_api_keys_rotate",
"oauth_register_client",
"oauth_revoke_client",
},
max_tools=80,
),
# Appwrite endpoint - Backend-as-a-Service (100 tools) - Phase I
EndpointType.APPWRITE: EndpointConfig(
path="/appwrite",
name="Appwrite Manager",
description="Appwrite Self-Hosted management (databases, documents, users, teams, storage, functions, messaging)",
endpoint_type=EndpointType.APPWRITE,
plugin_types=["appwrite"],
require_master_key=False,
allowed_scopes={"read", "write", "admin"},
# Blacklist system and admin tools
tool_blacklist={
"manage_api_keys_create",
"manage_api_keys_delete",
"manage_api_keys_rotate",
"oauth_register_client",
"oauth_revoke_client",
},
max_tools=110,
),
# Directus endpoint - Headless CMS (100 tools) - Phase J
EndpointType.DIRECTUS: EndpointConfig(
path="/directus",
name="Directus CMS",
description="Directus Self-Hosted CMS management (items, collections, files, users, roles, flows, dashboards)",
endpoint_type=EndpointType.DIRECTUS,
plugin_types=["directus"],
require_master_key=False,
allowed_scopes={"read", "write", "admin"},
# Blacklist system and admin tools
tool_blacklist={
"manage_api_keys_create",
"manage_api_keys_delete",
"manage_api_keys_rotate",
"oauth_register_client",
"oauth_revoke_client",
},
max_tools=110,
),
}
def get_endpoint_config(endpoint_type: EndpointType) -> EndpointConfig:
"""Get configuration for a specific endpoint type"""
if endpoint_type not in ENDPOINT_CONFIGS:
raise ValueError(f"Unknown endpoint type: {endpoint_type}")
return ENDPOINT_CONFIGS[endpoint_type]
def create_project_endpoint_config(
project_id: str, plugin_type: str, site_alias: str | None = None
) -> EndpointConfig:
"""
Create a dynamic endpoint configuration for a specific project.
Args:
project_id: Full project ID (e.g., "wordpress_site4")
plugin_type: Plugin type (e.g., "wordpress")
site_alias: Optional site alias for the path
Returns:
EndpointConfig for the project-specific endpoint
"""
path_suffix = site_alias or project_id
# FastMCP adds /mcp automatically, so /project/xxx → /project/xxx/mcp
return EndpointConfig(
path=f"/project/{path_suffix}",
name=f"Project: {project_id}",
description=f"Tools for project {project_id}",
endpoint_type=EndpointType.PROJECT,
plugin_types=[plugin_type],
require_master_key=False,
site_filter=project_id,
# Blacklist admin tools for project endpoints
tool_blacklist={
"manage_api_keys_create",
"manage_api_keys_delete",
"manage_api_keys_rotate",
"oauth_register_client",
"oauth_revoke_client",
"list_projects", # Only show own project
"oauth_list_clients",
},
max_tools=120,
)

295
core/endpoints/factory.py Normal file
View File

@@ -0,0 +1,295 @@
"""
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_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
self.endpoints[config.path] = mcp
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 for wordpress_advanced first (before wordpress_)
# Tools are named: wordpress_advanced_wp_db_*, wordpress_advanced_bulk_*, wordpress_advanced_system_*
if tool_name.startswith("wordpress_advanced_"):
return "wordpress_advanced"
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():
# Get tool count
# Note: This requires accessing FastMCP internals
tool_count = len(mcp._tool_manager._tools) if hasattr(mcp, "_tool_manager") else 0
info.append(
{
"path": path,
"name": mcp.name,
"tool_count": tool_count,
}
)
return info

View File

@@ -0,0 +1,350 @@
"""
Middleware for Multi-Endpoint Architecture
Provides authentication, rate limiting, and audit logging
that works with the multi-endpoint architecture.
"""
import logging
import time
from collections.abc import Callable
from dataclasses import dataclass
from fastmcp.exceptions import ToolError
from fastmcp.server.dependencies import get_http_headers
from fastmcp.server.middleware import Middleware, MiddlewareContext
from core.api_keys import get_api_key_manager
from core.audit_log import EventType, LogLevel, get_audit_logger
from core.auth import get_auth_manager
from core.context import clear_api_key_context, set_api_key_context
from core.rate_limiter import get_rate_limiter
from .config import EndpointConfig
logger = logging.getLogger(__name__)
@dataclass
class AuthContext:
"""Authentication context for a request"""
key_id: str | None = None
project_id: str | None = None
scope: str = "read"
is_master_key: bool = False
is_oauth_token: bool = False
client_ip: str | None = None
class EndpointAuthMiddleware(Middleware):
"""
Authentication middleware for multi-endpoint architecture.
Validates API keys/tokens and enforces endpoint-specific access rules.
"""
def __init__(self, endpoint_config: EndpointConfig):
"""
Initialize middleware with endpoint configuration.
Args:
endpoint_config: Configuration for this endpoint
"""
self.config = endpoint_config
self.auth_manager = get_auth_manager()
self.api_key_manager = get_api_key_manager()
async def on_call_tool(self, context: MiddlewareContext, call_next: Callable):
"""
Handle tool call with authentication and authorization.
Args:
context: Middleware context
call_next: Next middleware in chain
"""
tool_name = getattr(context.message, "name", "unknown")
start_time = time.time()
try:
# Extract and validate authentication
auth_context = await self._authenticate(context)
# Check endpoint access
self._check_endpoint_access(auth_context)
# Check tool access
self._check_tool_access(tool_name, auth_context)
# Set context for downstream handlers
if auth_context.key_id:
set_api_key_context(
key_id=auth_context.key_id,
project_id=auth_context.project_id or "*",
scope=auth_context.scope,
is_global=auth_context.project_id == "*",
)
# Call the actual tool
result = await call_next(context)
# Log success
self._log_success(tool_name, auth_context, start_time)
return result
except ToolError:
raise
except Exception as e:
self._log_error(tool_name, str(e), start_time)
raise ToolError(f"Authentication error: {str(e)}")
finally:
clear_api_key_context()
async def _authenticate(self, context: MiddlewareContext) -> AuthContext:
"""
Extract and validate authentication from request.
Args:
context: Middleware context
Returns:
AuthContext with authentication details
"""
auth_context = AuthContext()
# Get headers
try:
headers = get_http_headers()
except Exception:
headers = {}
# Extract client IP
auth_context.client_ip = headers.get("x-forwarded-for", "unknown")
# Get authorization header
auth_header = headers.get("authorization", "")
if not auth_header:
# No auth provided
if self.config.require_master_key:
raise ToolError("Master API key required for this endpoint")
return auth_context
# Parse authorization
if auth_header.startswith("Bearer "):
token = auth_header[7:]
else:
token = auth_header
# Check token type
if token.startswith("sk-"):
# Master API key
if self.auth_manager.validate_master_key(token):
auth_context.is_master_key = True
auth_context.project_id = "*"
auth_context.scope = "admin"
auth_context.key_id = "master"
return auth_context
else:
raise ToolError("Invalid master API key")
elif token.startswith("cmp_"):
# Project API key
key = self.api_key_manager.get_key_by_token(token)
if not key:
raise ToolError("Invalid API key")
if key.revoked:
raise ToolError("API key has been revoked")
if key.is_expired():
raise ToolError("API key has expired")
auth_context.key_id = key.key_id
auth_context.project_id = key.project_id
auth_context.scope = key.scope
return auth_context
else:
# Possibly OAuth token (JWT)
try:
from core.oauth import get_token_manager
token_manager = get_token_manager()
payload = token_manager.validate_access_token(token)
if payload:
auth_context.is_oauth_token = True
auth_context.project_id = payload.get("project_id", "*")
auth_context.scope = payload.get("scope", "read")
auth_context.key_id = f"oauth_{payload.get('sub', 'unknown')}"
return auth_context
except Exception:
pass
raise ToolError("Invalid authentication token")
def _check_endpoint_access(self, auth_context: AuthContext):
"""
Check if auth context allows access to this endpoint.
Args:
auth_context: Authentication context
"""
# Master key always has access
if auth_context.is_master_key:
return
# Check if endpoint requires master key
if self.config.require_master_key:
raise ToolError(f"Endpoint {self.config.path} requires master API key")
# Check scope requirements
if self.config.allowed_scopes:
# Check if any of the user's scopes are allowed
user_scopes = set(auth_context.scope.split())
if not user_scopes & self.config.allowed_scopes:
raise ToolError(
f"Insufficient scope. Required: {self.config.allowed_scopes}, "
f"Got: {user_scopes}"
)
# Check plugin type access
if auth_context.project_id and auth_context.project_id != "*":
# Extract plugin type from project_id (e.g., "wordpress_site4" -> "wordpress")
if "_" in auth_context.project_id:
key_plugin_type = auth_context.project_id.split("_")[0]
# Check if endpoint allows this plugin type
if self.config.plugin_types and key_plugin_type not in self.config.plugin_types:
raise ToolError(
f"API key for {key_plugin_type} cannot access "
f"{self.config.endpoint_type.value} endpoint"
)
def _check_tool_access(self, tool_name: str, auth_context: AuthContext):
"""
Check if auth context allows access to specific tool.
Args:
tool_name: Name of the tool
auth_context: Authentication context
"""
# Master key has access to all tools
if auth_context.is_master_key:
return
# Check tool blacklist
if not self.config.allows_tool(tool_name):
raise ToolError(f"Access denied to tool: {tool_name}")
# Check site filter for project endpoints
if self.config.site_filter:
# Tool must be for the configured site
# This is handled by parameter injection in the wrapper
pass
def _log_success(self, tool_name: str, auth_context: AuthContext, start_time: float):
"""Log successful tool execution"""
duration_ms = int((time.time() - start_time) * 1000)
logger.debug(
f"Tool {tool_name} executed successfully "
f"(key={auth_context.key_id}, duration={duration_ms}ms)"
)
def _log_error(self, tool_name: str, error: str, start_time: float):
"""Log tool execution error"""
duration_ms = int((time.time() - start_time) * 1000)
logger.warning(f"Tool {tool_name} failed: {error} (duration={duration_ms}ms)")
class EndpointRateLimitMiddleware(Middleware):
"""
Rate limiting middleware for multi-endpoint architecture.
"""
def __init__(self, endpoint_config: EndpointConfig):
self.config = endpoint_config
self.rate_limiter = get_rate_limiter()
async def on_call_tool(self, context: MiddlewareContext, call_next: Callable):
"""Apply rate limiting before tool execution"""
# Get client identifier
try:
headers = get_http_headers()
client_id = headers.get("authorization", "anonymous")[:50]
except Exception:
client_id = "unknown"
# Check rate limit
allowed, info = self.rate_limiter.check_rate_limit(client_id)
if not allowed:
raise ToolError(
f"Rate limit exceeded. Retry after {info.get('retry_after', 60)} seconds"
)
# Proceed with request
return await call_next(context)
class EndpointAuditMiddleware(Middleware):
"""
Audit logging middleware for multi-endpoint architecture.
"""
def __init__(self, endpoint_config: EndpointConfig):
self.config = endpoint_config
self.audit_logger = get_audit_logger()
async def on_call_tool(self, context: MiddlewareContext, call_next: Callable):
"""Log tool execution to audit log"""
tool_name = getattr(context.message, "name", "unknown")
start_time = time.time()
try:
result = await call_next(context)
# Log success
self.audit_logger.log(
level=LogLevel.INFO,
event_type=EventType.TOOL_CALL,
message=f"Tool executed: {tool_name}",
details={
"tool": tool_name,
"endpoint": self.config.path,
"duration_ms": int((time.time() - start_time) * 1000),
"success": True,
},
)
return result
except Exception as e:
# Log failure
self.audit_logger.log(
level=LogLevel.WARNING,
event_type=EventType.TOOL_CALL,
message=f"Tool failed: {tool_name}",
details={
"tool": tool_name,
"endpoint": self.config.path,
"duration_ms": int((time.time() - start_time) * 1000),
"success": False,
"error": str(e),
},
)
raise
def create_endpoint_middleware(endpoint_config: EndpointConfig) -> list:
"""
Create middleware stack for an endpoint.
Args:
endpoint_config: Endpoint configuration
Returns:
List of middleware instances
"""
return [
EndpointAuthMiddleware(endpoint_config),
EndpointRateLimitMiddleware(endpoint_config),
EndpointAuditMiddleware(endpoint_config),
]

226
core/endpoints/registry.py Normal file
View File

@@ -0,0 +1,226 @@
"""
Endpoint Registry
Central registry for managing all MCP endpoints.
Handles routing requests to the correct endpoint based on path.
"""
import logging
from dataclasses import dataclass
from fastmcp import FastMCP
from starlette.routing import Mount, Route
from .config import (
ENDPOINT_CONFIGS,
EndpointConfig,
EndpointType,
create_project_endpoint_config,
)
from .factory import MCPEndpointFactory
logger = logging.getLogger(__name__)
@dataclass
class EndpointInfo:
"""Information about a registered endpoint"""
path: str
name: str
description: str
endpoint_type: EndpointType
tool_count: int
plugin_types: list[str]
require_master_key: bool
class EndpointRegistry:
"""
Central registry for all MCP endpoints.
Manages endpoint creation, routing, and discovery.
"""
def __init__(self, factory: MCPEndpointFactory):
"""
Initialize the endpoint registry.
Args:
factory: Endpoint factory for creating MCP instances
"""
self.factory = factory
self._endpoints: dict[str, FastMCP] = {}
self._configs: dict[str, EndpointConfig] = {}
self._initialized = False
def initialize_default_endpoints(self):
"""
Initialize the default set of endpoints.
Creates admin, wordpress, wordpress-advanced, and gitea endpoints.
"""
if self._initialized:
logger.warning("Endpoints already initialized")
return
logger.info("=" * 60)
logger.info("Initializing Multi-Endpoint Architecture")
logger.info("=" * 60)
# Create default endpoints
for _endpoint_type, config in ENDPOINT_CONFIGS.items():
try:
mcp = self.factory.create_endpoint(config)
self._endpoints[config.path] = mcp
self._configs[config.path] = config
logger.info(f"{config.path}: {config.name}")
except Exception as e:
logger.error(f" ✗ Failed to create {config.path}: {e}")
self._initialized = True
# Log summary
self._log_summary()
def create_project_endpoint(
self, project_id: str, plugin_type: str, site_alias: str | None = None
) -> FastMCP:
"""
Create a dynamic endpoint for a specific project.
Args:
project_id: Full project ID
plugin_type: Plugin type
site_alias: Optional site alias
Returns:
Created FastMCP instance
"""
config = create_project_endpoint_config(project_id, plugin_type, site_alias)
# Check if already exists
if config.path in self._endpoints:
logger.info(f"Endpoint {config.path} already exists")
return self._endpoints[config.path]
mcp = self.factory.create_endpoint(config)
self._endpoints[config.path] = mcp
self._configs[config.path] = config
logger.info(f"Created project endpoint: {config.path}")
return mcp
def get_endpoint(self, path: str) -> FastMCP | None:
"""
Get endpoint by path.
Args:
path: Endpoint path
Returns:
FastMCP instance or None
"""
# Exact match
if path in self._endpoints:
return self._endpoints[path]
# Try with trailing slash
if not path.endswith("/"):
return self._endpoints.get(path + "/")
return None
def get_config(self, path: str) -> EndpointConfig | None:
"""Get endpoint configuration by path"""
return self._configs.get(path)
def list_endpoints(self) -> list[EndpointInfo]:
"""
List all registered endpoints with their info.
Returns:
List of EndpointInfo objects
"""
endpoints = []
for path, config in self._configs.items():
mcp = self._endpoints.get(path)
tool_count = 0
if mcp:
# Try to get tool count from FastMCP
try:
tool_count = len(mcp._tool_manager._tools)
except AttributeError:
pass
endpoints.append(
EndpointInfo(
path=path,
name=config.name,
description=config.description,
endpoint_type=config.endpoint_type,
tool_count=tool_count,
plugin_types=config.plugin_types,
require_master_key=config.require_master_key,
)
)
return endpoints
def get_routes(self) -> list[Route]:
"""
Get Starlette routes for all endpoints.
Returns:
List of Route objects for mounting
"""
routes = []
for path, mcp in self._endpoints.items():
# Each MCP endpoint needs to handle its own routing
# FastMCP provides an ASGI app
routes.append(Mount(path, app=mcp.sse_app(), name=f"mcp_{path.replace('/', '_')}"))
return routes
def _log_summary(self):
"""Log a summary of all endpoints"""
logger.info("-" * 60)
logger.info("Endpoint Summary:")
logger.info("-" * 60)
total_tools = 0
for info in self.list_endpoints():
total_tools += info.tool_count
auth_note = " (Master Key required)" if info.require_master_key else ""
logger.info(f" {info.path}: {info.tool_count} tools{auth_note}")
logger.info("-" * 60)
logger.info(f"Total: {len(self._endpoints)} endpoints")
logger.info("=" * 60)
# Singleton instance
_registry: EndpointRegistry | None = None
def get_endpoint_registry() -> EndpointRegistry:
"""Get the global endpoint registry instance"""
global _registry
if _registry is None:
raise RuntimeError(
"Endpoint registry not initialized. " "Call initialize_endpoint_registry() first."
)
return _registry
def initialize_endpoint_registry(factory: MCPEndpointFactory) -> EndpointRegistry:
"""
Initialize the global endpoint registry.
Args:
factory: Endpoint factory to use
Returns:
Initialized EndpointRegistry
"""
global _registry
_registry = EndpointRegistry(factory)
return _registry