"""FastAPI application entrypoint."""
import logging
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from sqlalchemy import text
from gnexus_creds.api import admin_router
from gnexus_creds.api import router as api_router
from gnexus_creds.config import get_settings
from gnexus_creds.db import SessionLocal
from gnexus_creds.errors import AppError, app_error_handler, validation_error_handler
from gnexus_creds.logging import configure_logging
from gnexus_creds.mcp import router as mcp_router
from gnexus_creds.mcp_protocol import create_mcp_protocol_server
from gnexus_creds.oauth import router as auth_router
from gnexus_creds.oauth import webhook_router
PROJECT_ROOT = Path(__file__).resolve().parent.parent
FRONTEND_DIST = PROJECT_ROOT / "frontend" / "dist"
UI_KIT_ASSETS = PROJECT_ROOT / "frontend" / "node_modules" / "gnexus-ui-kit" / "dist" / "assets"
def create_app() -> FastAPI:
configure_logging()
settings = get_settings()
mcp_protocol_server = create_mcp_protocol_server()
mcp_protocol_app = mcp_protocol_server.streamable_http_app()
@asynccontextmanager
async def lifespan(_: FastAPI):
configure_logging()
async with mcp_protocol_server.session_manager.run():
yield
logger = logging.getLogger("gnexus_creds")
logger.info("Shutting down gracefully")
app = FastAPI(title=settings.app_name, version="0.1.0", lifespan=lifespan)
app.state.mcp_protocol_server = mcp_protocol_server
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.add_exception_handler(AppError, app_error_handler)
app.add_exception_handler(RequestValidationError, validation_error_handler)
app.include_router(auth_router)
app.include_router(webhook_router)
app.include_router(api_router)
app.include_router(admin_router)
app.include_router(mcp_router)
app.mount("/mcp-protocol", mcp_protocol_app, name="mcp-protocol")
@app.get("/health", tags=["system"])
async def health() -> dict[str, str]:
return {"status": "ok"}
@app.get("/ready", tags=["system"])
async def ready() -> dict[str, str]:
with SessionLocal() as db:
db.execute(text("select 1"))
return {"status": "ok"}
if FRONTEND_DIST.exists():
ui_kit_fonts_dir = UI_KIT_ASSETS / "fonts"
if ui_kit_fonts_dir.exists():
app.mount("/assets/fonts", StaticFiles(directory=ui_kit_fonts_dir), name="ui-kit-fonts")
assets_dir = FRONTEND_DIST / "assets"
if assets_dir.exists():
app.mount("/assets", StaticFiles(directory=assets_dir), name="assets")
@app.get("/", include_in_schema=False)
async def index() -> FileResponse:
return FileResponse(FRONTEND_DIST / "index.html")
@app.get("/{path:path}", include_in_schema=False)
async def spa_fallback(path: str) -> FileResponse:
if path.startswith(("api/", "auth/", "mcp/", "mcp-protocol/", "webhooks/")):
raise AppError("not_found", "Not found.", status_code=404)
return FileResponse(FRONTEND_DIST / "index.html")
return app
app = create_app()