mirror of
https://github.com/hanzoai/agent.git
synced 2026-09-11 21:08:51 +00:00
Consolidate all Hanzo agent infrastructure into one repo: - control-plane/: Go control plane server (was agents-new) - sdk/: Go, Python, TypeScript SDKs - deployments/: Docker, Kubernetes, Helm configs - Dockerfile.sdk: Python SDK image (ghcr.io/hanzoai/agent:latest) - CI builds both SDK + control-plane images No more agentfield — everything is hanzoai/agent.
341 lines
12 KiB
Python
341 lines
12 KiB
Python
import asyncio
|
|
import json
|
|
import os
|
|
import subprocess
|
|
from typing import Any, Dict, List, Optional
|
|
from dataclasses import dataclass
|
|
|
|
from .logger import get_logger
|
|
from .mcp_stdio_bridge import StdioMCPBridge
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
@dataclass
|
|
class MCPServerConfig:
|
|
alias: str
|
|
run_command: str
|
|
working_dir: str
|
|
environment: Dict[str, str]
|
|
health_check: Optional[str] = None
|
|
port: Optional[int] = None
|
|
transport: str = "http"
|
|
|
|
|
|
@dataclass
|
|
class MCPServerProcess:
|
|
config: MCPServerConfig
|
|
process: Optional[subprocess.Popen] = None
|
|
port: Optional[int] = None
|
|
status: str = "stopped" # stopped, starting, running, failed
|
|
|
|
|
|
class MCPManager:
|
|
def __init__(self, agent_directory: str, dev_mode: bool = False):
|
|
self.agent_directory = agent_directory
|
|
self.dev_mode = dev_mode
|
|
self.servers: Dict[str, MCPServerProcess] = {}
|
|
self.stdio_bridges: Dict[str, StdioMCPBridge] = {}
|
|
self.port_range_start = 8100 # Start assigning ports from 8100
|
|
self.used_ports = set()
|
|
|
|
def discover_mcp_servers(self) -> List[MCPServerConfig]:
|
|
"""Discover MCP servers from packages/mcp/ directory"""
|
|
mcp_dir = os.path.join(self.agent_directory, "packages", "mcp")
|
|
servers = []
|
|
|
|
if not os.path.exists(mcp_dir):
|
|
if self.dev_mode:
|
|
logger.debug(f"No MCP directory found at {mcp_dir}")
|
|
return servers
|
|
|
|
for item in os.listdir(mcp_dir):
|
|
server_dir = os.path.join(mcp_dir, item)
|
|
config_file = os.path.join(server_dir, "config.json")
|
|
|
|
if os.path.isdir(server_dir) and os.path.exists(config_file):
|
|
try:
|
|
with open(config_file, "r") as f:
|
|
config_data = json.load(f)
|
|
|
|
config = MCPServerConfig(
|
|
alias=config_data.get("alias", item),
|
|
run_command=config_data.get("run", ""),
|
|
working_dir=server_dir,
|
|
environment=config_data.get("environment", {}),
|
|
health_check=config_data.get("health_check"),
|
|
transport=config_data.get("transport", "http"),
|
|
)
|
|
servers.append(config)
|
|
|
|
if self.dev_mode:
|
|
logger.debug(f"Discovered MCP server: {config.alias}")
|
|
|
|
except Exception as e:
|
|
if self.dev_mode:
|
|
logger.warning(f"Failed to load config for {item}: {e}")
|
|
|
|
return servers
|
|
|
|
def _get_next_available_port(self) -> int:
|
|
"""Get next available port for MCP server"""
|
|
import socket
|
|
|
|
for port in range(self.port_range_start, self.port_range_start + 1000):
|
|
if port not in self.used_ports:
|
|
# Test if port is actually available
|
|
try:
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
s.bind(("localhost", port))
|
|
self.used_ports.add(port)
|
|
return port
|
|
except OSError:
|
|
continue
|
|
|
|
raise RuntimeError("No available ports for MCP servers")
|
|
|
|
def _detect_transport(self, config: MCPServerConfig) -> str:
|
|
"""Detect transport type from config"""
|
|
return config.transport
|
|
|
|
async def _start_stdio_server(self, config: MCPServerConfig) -> bool:
|
|
"""Start stdio MCP server using bridge"""
|
|
try:
|
|
# Assign port for the bridge
|
|
port = self._get_next_available_port()
|
|
config.port = port
|
|
|
|
if self.dev_mode:
|
|
logger.info(f"Starting stdio MCP server: {config.alias} on port {port}")
|
|
logger.debug(f"Command: {config.run_command}")
|
|
|
|
# Prepare server config for bridge
|
|
server_config = {
|
|
"run": config.run_command,
|
|
"working_dir": config.working_dir,
|
|
"environment": config.environment,
|
|
}
|
|
|
|
# Create and start stdio bridge
|
|
bridge = StdioMCPBridge(
|
|
server_config=server_config, port=port, dev_mode=self.dev_mode
|
|
)
|
|
|
|
success = await bridge.start()
|
|
if success:
|
|
self.stdio_bridges[config.alias] = bridge
|
|
if self.dev_mode:
|
|
logger.info(f"Stdio MCP server {config.alias} started successfully")
|
|
return True
|
|
else:
|
|
if self.dev_mode:
|
|
logger.error(f"Stdio MCP server {config.alias} failed to start")
|
|
return False
|
|
|
|
except Exception as e:
|
|
if self.dev_mode:
|
|
logger.error(f"Error starting stdio MCP server {config.alias}: {e}")
|
|
return False
|
|
|
|
async def _start_http_server(self, config: MCPServerConfig) -> bool:
|
|
"""Start HTTP MCP server (original implementation)"""
|
|
try:
|
|
# Assign port
|
|
port = self._get_next_available_port()
|
|
config.port = port
|
|
|
|
# Prepare command with port substitution
|
|
run_command = config.run_command.replace("{{port}}", str(port))
|
|
|
|
# Prepare environment
|
|
env = os.environ.copy()
|
|
env.update(config.environment)
|
|
|
|
if self.dev_mode:
|
|
logger.info(f"Starting HTTP MCP server: {config.alias} on port {port}")
|
|
logger.debug(f"Command: {run_command}")
|
|
|
|
# Start process
|
|
process = subprocess.Popen(
|
|
run_command.split(),
|
|
cwd=config.working_dir,
|
|
env=env,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
text=True,
|
|
)
|
|
|
|
# Create server process object
|
|
server_process = MCPServerProcess(
|
|
config=config, process=process, port=port, status="starting"
|
|
)
|
|
|
|
self.servers[config.alias] = server_process
|
|
|
|
# Wait a moment for startup
|
|
await asyncio.sleep(2)
|
|
|
|
# Check if process is still running
|
|
if process.poll() is None:
|
|
server_process.status = "running"
|
|
if self.dev_mode:
|
|
logger.info(f"HTTP MCP server {config.alias} started successfully")
|
|
return True
|
|
else:
|
|
server_process.status = "failed"
|
|
if self.dev_mode:
|
|
logger.error(f"HTTP MCP server {config.alias} failed to start")
|
|
return False
|
|
|
|
except Exception as e:
|
|
if self.dev_mode:
|
|
logger.error(f"Error starting HTTP MCP server {config.alias}: {e}")
|
|
if config.alias in self.servers:
|
|
self.servers[config.alias].status = "failed"
|
|
return False
|
|
|
|
async def start_server(self, config: MCPServerConfig) -> bool:
|
|
"""Start individual MCP server"""
|
|
transport = self._detect_transport(config)
|
|
if transport == "stdio":
|
|
return await self._start_stdio_server(config)
|
|
else:
|
|
return await self._start_http_server(config)
|
|
|
|
async def start_all_servers(self) -> Dict[str, bool]:
|
|
"""Start all discovered MCP servers"""
|
|
configs = self.discover_mcp_servers()
|
|
results = {}
|
|
|
|
if self.dev_mode:
|
|
logger.info(f"Starting {len(configs)} MCP servers...")
|
|
|
|
for config in configs:
|
|
success = await self.start_server(config)
|
|
results[config.alias] = success
|
|
|
|
return results
|
|
|
|
def get_server_status(self, alias: str) -> Optional[Dict[str, Any]]:
|
|
"""Get status of specific MCP server"""
|
|
# Check stdio bridges first
|
|
if alias in self.stdio_bridges:
|
|
bridge = self.stdio_bridges[alias]
|
|
return {
|
|
"alias": alias,
|
|
"transport": "stdio",
|
|
"port": bridge.port,
|
|
"status": "running" if bridge.running else "stopped",
|
|
"initialized": bridge.initialized,
|
|
}
|
|
|
|
# Check HTTP servers
|
|
if alias in self.servers:
|
|
server_process = self.servers[alias]
|
|
return {
|
|
"alias": alias,
|
|
"transport": "http",
|
|
"port": server_process.port,
|
|
"status": server_process.status,
|
|
"config": server_process.config,
|
|
}
|
|
|
|
return None
|
|
|
|
def get_all_status(self) -> Dict[str, Dict[str, Any]]:
|
|
"""Get status of all MCP servers"""
|
|
all_status = {}
|
|
|
|
# Add stdio bridges
|
|
for alias, bridge in self.stdio_bridges.items():
|
|
all_status[alias] = {
|
|
"alias": alias,
|
|
"transport": "stdio",
|
|
"port": bridge.port,
|
|
"status": "running" if bridge.running else "stopped",
|
|
"initialized": bridge.initialized,
|
|
}
|
|
|
|
# Add HTTP servers
|
|
for alias, server_process in self.servers.items():
|
|
all_status[alias] = {
|
|
"alias": alias,
|
|
"transport": "http",
|
|
"port": server_process.port,
|
|
"status": server_process.status,
|
|
"config": server_process.config,
|
|
}
|
|
|
|
return all_status
|
|
|
|
async def stop_server(self, alias: str) -> bool:
|
|
"""Stop specific MCP server"""
|
|
# Check if it's a stdio bridge
|
|
if alias in self.stdio_bridges:
|
|
bridge = self.stdio_bridges[alias]
|
|
await bridge.stop()
|
|
if bridge.port:
|
|
self.used_ports.discard(bridge.port)
|
|
del self.stdio_bridges[alias]
|
|
if self.dev_mode:
|
|
logger.info(f"Stopped stdio MCP server: {alias}")
|
|
return True
|
|
|
|
# Check if it's an HTTP server
|
|
if alias in self.servers:
|
|
server_process = self.servers[alias]
|
|
if server_process.process and server_process.process.poll() is None:
|
|
server_process.process.terminate()
|
|
try:
|
|
server_process.process.wait(timeout=5)
|
|
except subprocess.TimeoutExpired:
|
|
server_process.process.kill()
|
|
|
|
server_process.status = "stopped"
|
|
if server_process.port:
|
|
self.used_ports.discard(server_process.port)
|
|
|
|
if self.dev_mode:
|
|
logger.info(f"Stopped HTTP MCP server: {alias}")
|
|
return True
|
|
|
|
return False
|
|
|
|
async def start_server_by_alias(self, alias: str) -> bool:
|
|
"""Start MCP server by alias"""
|
|
# Find the config for this alias
|
|
configs = self.discover_mcp_servers()
|
|
for config in configs:
|
|
if config.alias == alias:
|
|
return await self.start_server(config)
|
|
|
|
if self.dev_mode:
|
|
logger.warning(f"No configuration found for MCP server: {alias}")
|
|
return False
|
|
|
|
async def restart_server(self, alias: str) -> bool:
|
|
"""Restart MCP server by alias"""
|
|
# Stop first
|
|
stop_success = await self.stop_server(alias)
|
|
if self.dev_mode:
|
|
logger.info(f"Stopped '{alias}' for restart: {stop_success}")
|
|
|
|
# Wait a moment for cleanup
|
|
await asyncio.sleep(1)
|
|
|
|
# Start again
|
|
return await self.start_server_by_alias(alias)
|
|
|
|
async def shutdown_all(self) -> None:
|
|
"""Stop all MCP servers"""
|
|
if self.dev_mode:
|
|
logger.info("Shutting down all MCP servers...")
|
|
|
|
# Stop all stdio bridges
|
|
for alias in list(self.stdio_bridges.keys()):
|
|
await self.stop_server(alias)
|
|
|
|
# Stop all HTTP servers
|
|
for alias in list(self.servers.keys()):
|
|
await self.stop_server(alias)
|