from fastapi import FastAPI, HTTPException, Depends from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles import os import logging from dotenv import load_dotenv from fastapi_mcp import FastApiMCP from app.routers import jobs, logs, configs, repositories, claude from app.services.nomad_client import get_nomad_client from app.services.gitea_client import GiteaClient from app.schemas.claude_api import McpRequest, McpResponse # Load environment variables load_dotenv() # Configure logging logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", ) logger = logging.getLogger(__name__) # Initialize the FastAPI app app = FastAPI( title="Nomad MCP", description="Service for AI agents to manage Nomad jobs via MCP protocol", version="0.1.0", ) # Add CORS middleware app.add_middleware( CORSMiddleware, allow_origins=["*"], # Can be set to specific origins in production allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Include routers app.include_router(jobs.router, prefix="/api/jobs", tags=["jobs"]) app.include_router(logs.router, prefix="/api/logs", tags=["logs"]) app.include_router(configs.router, prefix="/api/configs", tags=["configs"]) app.include_router(repositories.router, prefix="/api/repositories", tags=["repositories"]) app.include_router(claude.router, prefix="/api/claude", tags=["claude"]) # Initialize the FastAPI MCP base_url = os.getenv("BASE_URL", "http://localhost:8000") mcp = FastApiMCP( app, base_url=base_url, name="Nomad MCP Tools", description="Tools for managing Nomad jobs via MCP protocol", include_tags=["jobs", "logs", "configs", "repositories"], ) mcp.mount() @app.get("/api/health", tags=["health"]) async def health_check(): """Health check endpoint.""" health_status = { "status": "healthy", "services": {} } # Check Nomad connection try: client = get_nomad_client() nomad_status = client.agent.get_agent() health_status["services"]["nomad"] = { "status": "connected", "version": nomad_status.get("config", {}).get("Version", "unknown"), } except Exception as e: logger.error(f"Nomad health check failed: {str(e)}") health_status["services"]["nomad"] = { "status": "failed", "error": str(e), } # Check Gitea connection try: gitea_client = GiteaClient() if gitea_client.api_base_url: # Try to list repositories as a connection test repos = gitea_client.list_repositories(limit=1) health_status["services"]["gitea"] = { "status": "connected", "api_url": gitea_client.api_base_url, } else: health_status["services"]["gitea"] = { "status": "not_configured", } except Exception as e: logger.error(f"Gitea health check failed: {str(e)}") health_status["services"]["gitea"] = { "status": "failed", "error": str(e), } # Overall status is unhealthy if any service is failed if any(service["status"] == "failed" for service in health_status["services"].values()): health_status["status"] = "unhealthy" return health_status # Find the static directory def find_static_directory(): """Find the static directory by checking multiple possible locations.""" logger.info("Starting static directory search...") # First check if STATIC_DIR environment variable is set static_dir_env = os.getenv("STATIC_DIR") if static_dir_env: logger.info(f"STATIC_DIR environment variable found: '{static_dir_env}'") if os.path.isdir(static_dir_env): logger.info(f"✅ Confirmed '{static_dir_env}' exists and is a directory") return static_dir_env else: logger.warning(f"❌ STATIC_DIR '{static_dir_env}' does not exist or is not a directory") # List parent directory contents if possible parent_dir = os.path.dirname(static_dir_env) if os.path.exists(parent_dir): logger.info(f"Contents of parent directory '{parent_dir}':") try: for item in os.listdir(parent_dir): item_path = os.path.join(parent_dir, item) item_type = "directory" if os.path.isdir(item_path) else "file" logger.info(f" - {item} ({item_type})") except Exception as e: logger.error(f"Error listing parent directory: {str(e)}") else: logger.info("STATIC_DIR environment variable not set") # Possible locations for the static directory possible_paths = [ "static", # Local development "/app/static", # Docker container "/local/nomad_mcp/static", # Nomad with artifact os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "static") # Relative to this file ] logger.info(f"Checking {len(possible_paths)} possible static directory locations:") # Check each path and use the first one that exists for path in possible_paths: logger.info(f"Checking path: '{path}'") if os.path.isdir(path): logger.info(f"✅ Found valid static directory at: '{path}'") return path else: logger.info(f"❌ Path '{path}' does not exist or is not a directory") # If no static directory is found, log a warning but don't fail # This allows the API to still function even without the UI logger.warning("No static directory found in any of the checked locations. UI will not be available.") # Try to create the static directory if STATIC_DIR is set if static_dir_env: try: logger.info(f"Attempting to create static directory at '{static_dir_env}'") os.makedirs(static_dir_env, exist_ok=True) if os.path.isdir(static_dir_env): logger.info(f"✅ Successfully created static directory at '{static_dir_env}'") return static_dir_env else: logger.error(f"Failed to create static directory at '{static_dir_env}'") except Exception as e: logger.error(f"Error creating static directory: {str(e)}") return None # Mount static files if the directory exists static_dir = find_static_directory() if static_dir: app.mount("/", StaticFiles(directory=static_dir, html=True), name="static") else: logger.warning("Static files not mounted. API endpoints will still function.") if __name__ == "__main__": import uvicorn port = int(os.getenv("PORT", "8000")) uvicorn.run("app.main:app", host="0.0.0.0", port=port, reload=True)