113 lines
3.6 KiB
Python
113 lines
3.6 KiB
Python
"""
|
||
main.py
|
||
Application entrypoint.
|
||
Starts the FastAPI HTTP server (web UI + REST API) and mounts the FastMCP
|
||
streamable-HTTP endpoint under /mcp.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
from contextlib import asynccontextmanager
|
||
|
||
import uvicorn
|
||
from fastapi import FastAPI, Request
|
||
from fastapi.responses import FileResponse, JSONResponse
|
||
from fastapi.staticfiles import StaticFiles
|
||
|
||
from app.api import routes_config, routes_instructions, routes_status
|
||
from app.api.auth import TokenAuthMiddleware
|
||
from app.config import APP_VERSION, settings
|
||
from app.database import init_db
|
||
from app.logging_setup import configure_logging
|
||
from app.mcp_server import mcp, mcp_asgi_app
|
||
from app.services import instruction_service
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Logging must be configured before any module emits log messages
|
||
# ---------------------------------------------------------------------------
|
||
configure_logging(settings)
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Build the FastAPI application
|
||
# ---------------------------------------------------------------------------
|
||
|
||
@asynccontextmanager
|
||
async def lifespan(app: FastAPI):
|
||
# Startup
|
||
logger.info("local-mcp v%s starting up – initialising database …", APP_VERSION)
|
||
init_db(settings.db_path)
|
||
await instruction_service.init_wakeup()
|
||
logger.info(
|
||
"local-mcp v%s ready http://%s:%d | MCP http://%s:%d/mcp (stateless=%s)",
|
||
APP_VERSION,
|
||
settings.host, settings.http_port,
|
||
settings.host, settings.http_port,
|
||
settings.mcp_stateless,
|
||
)
|
||
# Run the MCP session manager for the duration of the app lifetime
|
||
async with mcp.session_manager.run():
|
||
yield
|
||
# Shutdown
|
||
logger.info("local-mcp shutting down")
|
||
|
||
|
||
def create_app() -> FastAPI:
|
||
app = FastAPI(
|
||
title="local-mcp",
|
||
description="Localhost MCP server with instruction queue management UI",
|
||
version=APP_VERSION,
|
||
lifespan=lifespan,
|
||
)
|
||
|
||
# --- Token auth middleware (no-op when API_TOKEN is not set) ---
|
||
app.add_middleware(TokenAuthMiddleware, token=settings.api_token)
|
||
|
||
# --- Global exception handler ---
|
||
@app.exception_handler(Exception)
|
||
async def global_exception_handler(request: Request, exc: Exception):
|
||
logger.exception("Unhandled exception for %s %s", request.method, request.url)
|
||
return JSONResponse(
|
||
status_code=500,
|
||
content={"detail": "Internal server error", "error": str(exc)},
|
||
)
|
||
|
||
# --- Public: lets the UI know whether it needs a token (no auth required) ---
|
||
@app.get("/auth-check", include_in_schema=False)
|
||
def auth_check():
|
||
return {"auth_required": bool(settings.api_token)}
|
||
|
||
# --- API routers ---
|
||
app.include_router(routes_status.router)
|
||
app.include_router(routes_instructions.router)
|
||
app.include_router(routes_config.router)
|
||
|
||
# --- MCP streamable-HTTP transport mounted at /mcp ---
|
||
# mcp_asgi_app was pre-built in mcp_server.py; session manager is
|
||
# started explicitly in the lifespan above.
|
||
app.mount("/mcp", mcp_asgi_app)
|
||
|
||
# --- Static files for the web UI ---
|
||
app.mount("/static", StaticFiles(directory="static", html=False), name="static")
|
||
|
||
@app.get("/", include_in_schema=False)
|
||
def serve_index():
|
||
return FileResponse("static/index.html")
|
||
|
||
return app
|
||
|
||
|
||
app = create_app()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
uvicorn.run(
|
||
"main:app",
|
||
host=settings.host,
|
||
port=settings.http_port,
|
||
log_level=settings.log_level.lower(),
|
||
reload=False,
|
||
)
|