from __future__ import annotations import asyncio import logging import os from contextlib import asynccontextmanager import uvicorn from fastapi import FastAPI from mcp_docugen.auth import ApiKeyAuthMiddleware from mcp_docugen.config import Settings from mcp_docugen.generation_store import GenerationStore from mcp_docugen.http_routes import build_http_app from mcp_docugen.llm_client import OpenRouterClient from mcp_docugen.mcp_tools import build_mcp_server from mcp_docugen.renderer import Renderer from mcp_docugen.template_seed import seed_templates from mcp_docugen.template_store import TemplateStore logger = logging.getLogger("mcp_docugen") async def build_app(settings: Settings | None = None) -> FastAPI: settings = settings or Settings() settings.data_dir.mkdir(parents=True, exist_ok=True) templates_dir = settings.data_dir / "templates" generated_dir = settings.data_dir / "generated" db_path = settings.data_dir / "mcp_docugen.db" template_store = TemplateStore(base_dir=templates_dir) seed_templates(settings.templates_seed_dir, templates_dir) generation_store = GenerationStore( db_path=db_path, generated_dir=generated_dir ) await generation_store.init() llm = OpenRouterClient( api_key=settings.openrouter_api_key, base_url=settings.openrouter_base_url, timeout=settings.llm_timeout_seconds, ) renderer = Renderer( template_store=template_store, generation_store=generation_store, llm=llm, public_base_url=settings.public_base_url, default_model=settings.llm_model_default, asset_ttl_days=settings.asset_ttl_days, max_image_size_mb=settings.max_image_size_mb, inline_stylesheet_path=settings.inline_stylesheet_path, ) mcp = build_mcp_server(template_store, renderer) mcp_asgi = mcp.streamable_http_app() @asynccontextmanager async def lifespan(app: FastAPI): async with mcp.session_manager.run(): cleanup_task = asyncio.create_task( _periodic_cleanup(generation_store, interval_seconds=24 * 3600) ) try: yield finally: cleanup_task.cancel() try: await cleanup_task except asyncio.CancelledError: pass app = build_http_app(template_store, generation_store) app.router.lifespan_context = lifespan # streamable_http_app() espone /mcp internamente; monto a root. app.mount("/", mcp_asgi) app.add_middleware( ApiKeyAuthMiddleware, api_key=settings.api_key, exempt_paths={"/health", "/docs", "/redoc", "/openapi.json"}, ) app.state.settings = settings app.state.template_store = template_store app.state.generation_store = generation_store app.state.renderer = renderer app.state.llm = llm return app async def _periodic_cleanup( generation_store: GenerationStore, interval_seconds: int ) -> None: while True: try: removed = await generation_store.cleanup_expired() if removed: logger.info("cleanup_expired removed %d assets", removed) except Exception as exc: # noqa: BLE001 logger.error("cleanup task error: %s", exc) await asyncio.sleep(interval_seconds) def run() -> None: logging.basicConfig( level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s %(message)s", ) host = os.environ.get("HOST", "0.0.0.0") port = int(os.environ.get("PORT", "9100")) app = asyncio.run(build_app()) uvicorn.run(app, host=host, port=port)