- Add fastapi_mcp to provide automatic MCP tooling from API endpoints - Create MCP request/response schema models - Update main.py to initialize FastAPI MCP with zero config - Add comprehensive MCP integration documentation - Update README with zero-config MCP integration information 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
184 lines
6.8 KiB
Python
184 lines
6.8 KiB
Python
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) |