Files
2025-03-28 02:05:55 -05:00

382 lines
13 KiB
Python

"""Meta MCP Server for Hanzo MCP.
This module provides a comprehensive MetaMCPServer that manages both a main MCP server
and multiple sub-MCP servers, allowing for a unified interface to a wide range of
capabilities from AI assistants.
"""
import os
import json
import asyncio
import logging
import signal
from typing import Dict, List, Optional, Any, Set, Tuple, Union
from pathlib import Path
from mcp.server.fastmcp import FastMCP
from hanzo_mcp.server import HanzoMCPServer
from hanzo_mcp.tools.mcp_manager import MCPServerManager, MCPServer
from hanzo_mcp.tools.mcp_orchestrator import MCPOrchestrator
from hanzo_mcp.tools.common.context import DocumentContext, ToolContext
# Configure logging
logger = logging.getLogger(__name__)
class MetaMCPServer:
"""Meta MCP Server that manages main and sub-MCP servers.
This class provides a unified interface for managing a main MCP server
and multiple sub-MCP servers, allowing AI assistants to access a wide
range of capabilities through a single interface.
"""
def __init__(
self,
name: str = "hanzo-meta",
allowed_paths: Optional[List[str]] = None,
mcp_config: Optional[Dict[str, Any]] = None,
sub_mcps_config: Optional[Dict[str, Dict[str, Any]]] = None,
enable_proxy_tools: bool = True,
auto_start_sub_mcps: bool = True,
):
"""Initialize the Meta MCP Server.
Args:
name: Name of the main MCP server
allowed_paths: List of paths that the server is allowed to access
mcp_config: Configuration for the main MCP server
sub_mcps_config: Configuration for sub-MCP servers
enable_proxy_tools: Whether to enable proxy tools for sub-MCP servers
auto_start_sub_mcps: Whether to automatically start sub-MCP servers on initialization
"""
self.name = name
self.allowed_paths = allowed_paths or []
self.mcp_config = mcp_config or {}
self.sub_mcps_config = sub_mcps_config or {}
self.enable_proxy_tools = enable_proxy_tools
self.auto_start_sub_mcps = auto_start_sub_mcps
# Initialize main MCP server
self.main_server = self._init_main_server()
# Get MCP orchestrator from main server
self.orchestrator = self.main_server.mcp_orchestrator
# Initialize server manager for sub-MCP servers
self.server_manager = MCPServerManager()
# Apply custom sub-MCP configurations if provided
if self.sub_mcps_config:
self._apply_sub_mcp_configs()
# Auto-start sub-MCP servers if enabled
if self.auto_start_sub_mcps:
# Use asyncio.create_task to start servers asynchronously
self._start_sub_mcps_task = asyncio.create_task(self._start_sub_mcps())
def _init_main_server(self) -> HanzoMCPServer:
"""Initialize the main MCP server.
Returns:
The initialized HanzoMCPServer instance
"""
# Create the main MCP server
server = HanzoMCPServer(
name=self.name,
allowed_paths=self.allowed_paths,
enable_external_servers=self.enable_proxy_tools
)
return server
def _apply_sub_mcp_configs(self):
"""Apply custom sub-MCP server configurations."""
for name, config in self.sub_mcps_config.items():
# Check if this server already exists
server = self.server_manager.get_server(name)
if server:
# Update existing server configuration
server.command = config.get("command", server.command)
server.args = config.get("args", server.args)
server.env = config.get("env", server.env)
server.description = config.get("description", server.description)
else:
# Add new server
self.server_manager.add_server(
name=name,
command=config.get("command", ""),
args=config.get("args", []),
env=config.get("env", {}),
description=config.get("description", f"MCP Server: {name}"),
save=True
)
async def _start_sub_mcps(self):
"""Start all configured sub-MCP servers."""
# Get all servers
servers = self.server_manager.get_servers()
# Start servers in parallel
start_tasks = []
for name, server in servers.items():
# Check if server is enabled
# If enabled is "auto", check if the required environment variable is set
enabled = self._is_server_enabled(server)
if enabled:
# Start the server
logger.info(f"Starting sub-MCP server: {name}")
result = self.server_manager.start_server(name)
if not result.get("success", False):
logger.error(f"Failed to start sub-MCP server {name}: {result.get('error', 'Unknown error')}")
def _is_server_enabled(self, server: MCPServer) -> bool:
"""Check if a server is enabled.
Args:
server: The server to check
Returns:
True if the server is enabled, False otherwise
"""
# Check if the server has a custom configuration
if server.name in self.sub_mcps_config:
# Use the enabled value from the custom configuration
enabled = self.sub_mcps_config[server.name].get("enabled", "false").lower()
else:
# Use the default enabled value
enabled = "false"
# Check if the server is explicitly enabled
if enabled == "true":
return True
elif enabled == "false":
return False
elif enabled == "auto":
# Check if the auto_enable_env is set
auto_enable_env = self.sub_mcps_config.get(server.name, {}).get("auto_enable_env")
if auto_enable_env and auto_enable_env in os.environ and os.environ[auto_enable_env]:
return True
# Check if the environment variable is in the server's env
for key, value in server.env.items():
if value: # If the value is set
return True
return False
async def start(self):
"""Start the Meta MCP Server and its sub-MCP servers."""
logger.info(f"Starting Meta MCP Server: {self.name}")
# Start sub-MCP servers if not already started
if not hasattr(self, "_start_sub_mcps_task"):
self._start_sub_mcps_task = asyncio.create_task(self._start_sub_mcps())
# Wait for sub-MCP servers to start
await self._start_sub_mcps_task
def run(self, transport: str = "stdio", allowed_paths: Optional[List[str]] = None):
"""Run the Meta MCP Server.
This method runs the main MCP server, which will handle all requests
and forward them to sub-MCP servers as needed.
Args:
transport: The transport mechanism to use (stdio or sse)
allowed_paths: Additional allowed paths
"""
# Add additional allowed paths if provided
if allowed_paths:
for path in allowed_paths:
if path not in self.allowed_paths:
self.allowed_paths.append(path)
self.main_server.permission_manager.add_allowed_path(path)
self.main_server.document_context.add_allowed_path(path)
# Start the main MCP server
try:
self.main_server.run(transport=transport, allowed_paths=self.allowed_paths)
except KeyboardInterrupt:
logger.info("Received keyboard interrupt, stopping servers...")
self.cleanup()
finally:
self.cleanup()
def cleanup(self):
"""Clean up resources before shutdown."""
logger.info("Cleaning up resources...")
# Clean up main server
if hasattr(self, "main_server"):
self.main_server.cleanup()
# Clean up sub-MCP servers
if hasattr(self, "server_manager"):
self.server_manager.stop_all_servers()
def get_server_status(self) -> Dict[str, Any]:
"""Get the status of all MCP servers.
Returns:
Dictionary with server status information
"""
# Get main server status
status = {
"main_server": {
"name": self.name,
"allowed_paths": self.allowed_paths,
"running": True,
}
}
# Get sub-MCP servers status
status["sub_mcps"] = self.server_manager.get_all_server_info()
return status
async def restart_server(self, name: str) -> Dict[str, Any]:
"""Restart a sub-MCP server.
Args:
name: Name of the server to restart
Returns:
Dictionary with restart status information
"""
# Check if server exists
server = self.server_manager.get_server(name)
if not server:
return {
"success": False,
"error": f"Server not found: {name}"
}
# Stop the server
stop_result = self.server_manager.stop_server(name)
# Wait a moment for the server to fully stop
await asyncio.sleep(1)
# Start the server
start_result = self.server_manager.start_server(name)
return {
"success": start_result.get("success", False),
"stop_result": stop_result,
"start_result": start_result
}
def get_available_tools(self) -> Dict[str, Dict[str, Any]]:
"""Get all available tools from main and sub-MCP servers.
Returns:
Dictionary mapping tool names to tool information
"""
# Get tools from main server
tools = {}
# Get tools from sub-MCP servers
if hasattr(self, "orchestrator"):
sub_mcp_tools = self.orchestrator.get_available_tools()
tools.update(sub_mcp_tools)
return tools
def main():
"""Run the Meta MCP Server from the command line."""
import argparse
parser = argparse.ArgumentParser(
description="Meta MCP Server for Hanzo MCP"
)
parser.add_argument(
"--name",
default="hanzo-meta",
help="Name of the Meta MCP Server (default: hanzo-meta)"
)
parser.add_argument(
"--transport",
choices=["stdio", "sse"],
default="stdio",
help="Transport protocol to use (default: stdio)"
)
parser.add_argument(
"--allow-path",
action="append",
dest="allowed_paths",
help="Add an allowed path (can be specified multiple times)"
)
parser.add_argument(
"--config",
type=str,
help="Path to Meta MCP Server configuration file (JSON)"
)
parser.add_argument(
"--disable-proxy-tools",
action="store_true",
help="Disable proxy tools for sub-MCP servers"
)
parser.add_argument(
"--disable-auto-start",
action="store_true",
help="Disable automatic starting of sub-MCP servers"
)
args = parser.parse_args()
# Load configuration from file if provided
mcp_config = {}
sub_mcps_config = {}
if args.config and os.path.exists(args.config):
try:
with open(args.config, "r") as f:
config = json.load(f)
mcp_config = config.get("mcp", {})
sub_mcps_config = config.get("sub_mcps", {})
except (json.JSONDecodeError, IOError) as e:
print(f"Error loading configuration file: {e}")
return
# Create and run the Meta MCP Server
meta_server = MetaMCPServer(
name=args.name,
allowed_paths=args.allowed_paths,
mcp_config=mcp_config,
sub_mcps_config=sub_mcps_config,
enable_proxy_tools=not args.disable_proxy_tools,
auto_start_sub_mcps=not args.disable_auto_start
)
# Register signal handlers for graceful shutdown
def signal_handler(sig, frame):
print("\nReceived signal to stop, shutting down...")
meta_server.cleanup()
import sys
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
# Run the server
meta_server.run(
transport=args.transport,
allowed_paths=args.allowed_paths
)
if __name__ == "__main__":
main()