#!/usr/bin/env python3 """ MCP Server - Model Context Protocol implementation. This server exposes tools via JSON-RPC 2.0 protocol. """ import json import logging import sys from pathlib import Path from typing import Any, Dict, List, Optional from fastapi import FastAPI, HTTPException from fastapi.responses import JSONResponse, Response, HTMLResponse from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel # Add parent directory to path to import tools # This allows running from mcp-server/ directory parent_dir = Path(__file__).parent.parent if str(parent_dir) not in sys.path: sys.path.insert(0, str(parent_dir)) from tools.registry import ToolRegistry # Import dashboard API router try: from server.dashboard_api import router as dashboard_router HAS_DASHBOARD = True except ImportError as e: logger.warning(f"Dashboard API not available: {e}") HAS_DASHBOARD = False dashboard_router = None # Import admin API router try: from server.admin_api import router as admin_router HAS_ADMIN = True except ImportError as e: logger.warning(f"Admin API not available: {e}") HAS_ADMIN = False admin_router = None # Configure logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) app = FastAPI(title="MCP Server", version="0.1.0") # CORS middleware for web dashboard app.add_middleware( CORSMiddleware, allow_origins=["*"], # In production, restrict to local network allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Initialize tool registry tool_registry = ToolRegistry() # Include dashboard API router if available if HAS_DASHBOARD and dashboard_router: app.include_router(dashboard_router) logger.info("Dashboard API enabled") # Include admin API router if available if HAS_ADMIN and admin_router: app.include_router(admin_router) logger.info("Admin API enabled") else: logger.warning("Dashboard API not available") class JSONRPCRequest(BaseModel): """JSON-RPC 2.0 request model.""" jsonrpc: str = "2.0" method: str params: Optional[Dict[str, Any]] = None id: Optional[Any] = None class JSONRPCResponse(BaseModel): """JSON-RPC 2.0 response model.""" jsonrpc: str = "2.0" result: Optional[Any] = None error: Optional[Dict[str, Any]] = None id: Optional[Any] = None def create_error_response( code: int, message: str, data: Optional[Any] = None, request_id: Optional[Any] = None ) -> JSONRPCResponse: """Create a JSON-RPC error response.""" error = {"code": code, "message": message} if data is not None: error["data"] = data return JSONRPCResponse( jsonrpc="2.0", error=error, id=request_id ) def create_success_response( result: Any, request_id: Optional[Any] = None ) -> JSONRPCResponse: """Create a JSON-RPC success response.""" return JSONRPCResponse( jsonrpc="2.0", result=result, id=request_id ) @app.post("/mcp") async def handle_mcp_request(request: JSONRPCRequest): """ Handle MCP JSON-RPC requests. Supported methods: - tools/list: List all available tools - tools/call: Execute a tool """ try: method = request.method params = request.params or {} request_id = request.id logger.info(f"Received MCP request: method={method}, id={request_id}") if method == "tools/list": # List all available tools tools = tool_registry.list_tools() return create_success_response({"tools": tools}, request_id) elif method == "tools/call": # Execute a tool tool_name = params.get("name") arguments = params.get("arguments", {}) if not tool_name: return create_error_response( -32602, # Invalid params "Missing required parameter: name", request_id=request_id ) try: result = tool_registry.call_tool(tool_name, arguments) return create_success_response(result, request_id) except ValueError as e: # Tool not found or invalid arguments return create_error_response( -32602, # Invalid params str(e), request_id=request_id ) except Exception as e: # Tool execution error logger.error(f"Tool execution error: {e}", exc_info=True) return create_error_response( -32603, # Internal error "Tool execution failed", data=str(e), request_id=request_id ) else: # Unknown method return create_error_response( -32601, # Method not found f"Unknown method: {method}", request_id=request_id ) except Exception as e: logger.error(f"Request handling error: {e}", exc_info=True) return create_error_response( -32603, # Internal error "Internal server error", data=str(e), request_id=request.id if hasattr(request, 'id') else None ) @app.get("/health") async def health_check(): """Health check endpoint.""" return { "status": "healthy", "tools_registered": len(tool_registry.list_tools()) } @app.get("/", response_class=HTMLResponse) async def root(): """Root endpoint - serve dashboard.""" dashboard_path = Path(__file__).parent.parent.parent / "clients" / "web-dashboard" / "index.html" if dashboard_path.exists(): return dashboard_path.read_text() # Fallback to JSON if dashboard not available try: tools = tool_registry.list_tools() tool_count = len(tools) tool_names = [tool["name"] for tool in tools] except Exception as e: logger.error(f"Error getting tools: {e}") tool_count = 0 tool_names = [] return JSONResponse({ "name": "MCP Server", "version": "0.1.0", "protocol": "JSON-RPC 2.0", "status": "running", "tools_registered": tool_count, "tools": tool_names, "endpoints": { "mcp": "/mcp", "health": "/health", "docs": "/docs", "dashboard": "/api/dashboard" } }) @app.get("/dashboard", response_class=HTMLResponse) async def dashboard(): """Dashboard endpoint.""" dashboard_path = Path(__file__).parent.parent.parent / "clients" / "web-dashboard" / "index.html" if dashboard_path.exists(): return dashboard_path.read_text() raise HTTPException(status_code=404, detail="Dashboard not found") @app.get("/api") async def api_info(): """API information endpoint (JSON).""" try: tools = tool_registry.list_tools() tool_count = len(tools) tool_names = [tool["name"] for tool in tools] except Exception as e: logger.error(f"Error getting tools: {e}") tool_count = 0 tool_names = [] return { "name": "MCP Server", "version": "0.1.0", "protocol": "JSON-RPC 2.0", "status": "running", "tools_registered": tool_count, "tools": tool_names, "endpoints": { "mcp": "/mcp", "health": "/health", "docs": "/docs" } } @app.get("/favicon.ico") async def favicon(): """Handle favicon requests - return 204 No Content.""" return Response(status_code=204) if __name__ == "__main__": import uvicorn # Ensure we're running from the mcp-server directory import os script_dir = Path(__file__).parent.parent os.chdir(script_dir) uvicorn.run(app, host="0.0.0.0", port=8000, log_level="info")