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:
63
core/endpoints/__init__.py
Normal file
63
core/endpoints/__init__.py
Normal 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
350
core/endpoints/config.py
Normal 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
295
core/endpoints/factory.py
Normal 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
|
||||
350
core/endpoints/middleware.py
Normal file
350
core/endpoints/middleware.py
Normal 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
226
core/endpoints/registry.py
Normal 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
|
||||
Reference in New Issue
Block a user