260 lines
6.0 KiB
Python
260 lines
6.0 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
from mcp.server.fastmcp import FastMCP
|
|
|
|
from core.config import (
|
|
WORKSPACE_ROOT,
|
|
API_HOST,
|
|
API_PORT,
|
|
SERVER_NAME,
|
|
DEBUG,
|
|
)
|
|
from core.events import bus, Event
|
|
from core.metrics import metrics
|
|
from core.safety import safety
|
|
from core.executor import executor
|
|
from core.tools.registry import registry
|
|
from core.tools.base import ToolContext
|
|
from tools.discovery import load_all_tools
|
|
|
|
|
|
# =========================
|
|
# LOGGING SETUP
|
|
# =========================
|
|
|
|
log_level = logging.DEBUG if DEBUG else logging.INFO
|
|
logging.basicConfig(
|
|
level=log_level,
|
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# =========================
|
|
# RUNTIME BOOTSTRAP
|
|
# =========================
|
|
|
|
executor.start()
|
|
|
|
|
|
# =========================
|
|
# MCP SERVER
|
|
# =========================
|
|
|
|
mcp = FastMCP(SERVER_NAME)
|
|
|
|
|
|
# =========================
|
|
# REGISTRY → MCP AUTO BIND
|
|
# =========================
|
|
|
|
def bind_registry_tools() -> None:
|
|
"""Bind all registered tools to MCP server with proper error handling."""
|
|
for tool in registry.all_tools():
|
|
|
|
tool_name = tool.name
|
|
tool_description = getattr(
|
|
tool,
|
|
"description",
|
|
f"{tool_name} tool"
|
|
)
|
|
|
|
# Fix issue #2: Use default argument to avoid closure binding bug
|
|
def make_handler(bound_tool: Any) -> Any:
|
|
"""Create handler with proper binding to prevent late-binding issues."""
|
|
def handler(**kwargs: Any) -> Any:
|
|
ctx = ToolContext(
|
|
dry_run=safety.dry_run
|
|
)
|
|
|
|
try:
|
|
return registry.run(
|
|
name=bound_tool.name,
|
|
payload=kwargs,
|
|
ctx=ctx
|
|
)
|
|
except Exception as e:
|
|
# Fix issue #3: Add error handling in tool handler
|
|
logger.exception(f"Error executing tool {bound_tool.name}")
|
|
bus.emit(Event(
|
|
name="tool_handler_error",
|
|
source="CORE",
|
|
level="ERROR",
|
|
data={
|
|
"tool": bound_tool.name,
|
|
"error": str(e),
|
|
"error_type": type(e).__name__
|
|
}
|
|
))
|
|
raise
|
|
|
|
return handler
|
|
|
|
mcp.tool(
|
|
name=tool_name,
|
|
description=tool_description
|
|
)(make_handler(tool))
|
|
|
|
bus.log(
|
|
"CORE",
|
|
"mcp_tool_bound",
|
|
"INFO",
|
|
{
|
|
"tool": tool_name
|
|
}
|
|
)
|
|
|
|
|
|
# =========================
|
|
# SYSTEM BOOT
|
|
# =========================
|
|
|
|
def boot_system() -> None:
|
|
"""Initialize and boot the system."""
|
|
load_all_tools()
|
|
bind_registry_tools()
|
|
|
|
tool_count = len(registry.all_tools())
|
|
|
|
bus.emit(Event(
|
|
name="system_boot",
|
|
source="CORE",
|
|
level="INFO",
|
|
data={
|
|
"workspace": str(WORKSPACE_ROOT),
|
|
"server": SERVER_NAME,
|
|
"tool_count": tool_count
|
|
}
|
|
))
|
|
|
|
logger.info(f"System boot complete. Loaded {tool_count} tools.")
|
|
|
|
|
|
# =========================
|
|
# METRICS
|
|
# =========================
|
|
|
|
def get_metrics_snapshot() -> dict[str, Any]:
|
|
"""Return current metrics snapshot."""
|
|
return metrics.snapshot()
|
|
|
|
|
|
# =========================
|
|
# DEBUG EVENTS (OPTIONAL)
|
|
# =========================
|
|
|
|
def debug_event_printer(event: Event) -> None:
|
|
"""Print debug information for events."""
|
|
print(
|
|
f"[EVENT] "
|
|
f"{event.source} - "
|
|
f"{event.level} - "
|
|
f"{event.name}"
|
|
)
|
|
|
|
|
|
# bus.subscribe(debug_event_printer)
|
|
|
|
|
|
# =========================
|
|
# GRACEFUL SHUTDOWN
|
|
# =========================
|
|
|
|
def shutdown_gracefully() -> None:
|
|
"""Clean shutdown of executor and other resources."""
|
|
logger.info("Initiating graceful shutdown...")
|
|
try:
|
|
executor.shutdown() # type: ignore[attr-defined]
|
|
logger.info("Executor shut down successfully.")
|
|
except Exception as e:
|
|
logger.error(f"Error during executor shutdown: {e}")
|
|
|
|
|
|
# =========================
|
|
# MCP RUNNER
|
|
# =========================
|
|
|
|
def run_mcp() -> None:
|
|
"""Run the MCP server."""
|
|
boot_system()
|
|
|
|
bus.emit(Event(
|
|
name="mcp_start",
|
|
source="CORE",
|
|
level="INFO",
|
|
data={
|
|
"server": SERVER_NAME
|
|
}
|
|
))
|
|
|
|
logger.info(f"Starting MCP server: {SERVER_NAME}")
|
|
mcp.run(transport="stdio")
|
|
|
|
|
|
# =========================
|
|
# FASTAPI HOOK (ISOLATED)
|
|
# =========================
|
|
|
|
async def run_api() -> None:
|
|
"""Run FastAPI metrics server (intended for separate process or async context)."""
|
|
from fastapi import FastAPI
|
|
import uvicorn
|
|
|
|
app = FastAPI()
|
|
|
|
@app.get("/metrics")
|
|
def metrics_route() -> dict[str, Any]:
|
|
return get_metrics_snapshot()
|
|
|
|
@app.get("/health")
|
|
def health() -> dict[str, str]:
|
|
return {
|
|
"status": "ok"
|
|
}
|
|
|
|
config = uvicorn.Config(
|
|
app,
|
|
host=API_HOST,
|
|
port=API_PORT,
|
|
log_level="info"
|
|
)
|
|
|
|
server = uvicorn.Server(config)
|
|
|
|
await server.serve()
|
|
|
|
|
|
# =========================
|
|
# ENTRYPOINT
|
|
# =========================
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
run_mcp()
|
|
|
|
except KeyboardInterrupt:
|
|
logger.info("KeyboardInterrupt received.")
|
|
bus.emit(Event(
|
|
name="system_shutdown",
|
|
source="CORE",
|
|
level="INFO"
|
|
))
|
|
shutdown_gracefully()
|
|
|
|
except Exception as e:
|
|
logger.exception("Fatal error occurred.")
|
|
bus.emit(Event(
|
|
name="fatal_error",
|
|
source="CORE",
|
|
level="ERROR",
|
|
data={
|
|
"error": str(e),
|
|
"error_type": type(e).__name__
|
|
}
|
|
))
|
|
shutdown_gracefully()
|
|
raise
|
