Files
nomad_mcp/app/main.py
Nicolas Koehl ba9201dfa6 Implement FastAPI MCP zero-config integration
- 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>
2025-04-15 11:50:55 +07:00

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)