feat(mcp-docugen): Task 7-10 renderer, http_routes, mcp_tools, main bootstrap
- Renderer: orchestratore generate() — validazione strict variabili,
materializzazione image vars come asset effimeri su disco + URL rewrite,
asset paths template da ./assets/X -> {PUBLIC_BASE_URL}/assets/<t>/X,
integrazione LLM error -> record success=0
- FastAPI sub-app: GET /health (no auth), /assets/{t}/{f} (auth+traversal check),
/generated/{gen_id}/{f} (410 su scaduto o mancante)
- FastMCP server con 6 tool: template_create/update/delete/list/get,
document_generate. Tools esposti anche via mcp.tools dict per test.
- main.build_app() compone http_app + FastMCP mount su /mcp + auth middleware
+ lifespan cleanup task TTL (24h). run() entry point per script console.
68 test passed. Build Docker arca-mcp-docugen:dev verificata,
/health endpoint risponde correttamente nel container.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
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_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)
|
||||
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,
|
||||
)
|
||||
|
||||
mcp = build_mcp_server(template_store, renderer)
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
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
|
||||
app.mount("/mcp", mcp.streamable_http_app())
|
||||
|
||||
app.add_middleware(
|
||||
ApiKeyAuthMiddleware,
|
||||
api_key=settings.api_key,
|
||||
exempt_paths={"/health"},
|
||||
)
|
||||
|
||||
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)
|
||||
Reference in New Issue
Block a user