"""Cotizador E3 — FastAPI Application REST API + MCP Server for digital marketing quotation management. Optimized for n8n workflows and AI agents (OpenClaw, Claude, ChatGPT). """ from __future__ import annotations from contextlib import asynccontextmanager from datetime import datetime, timezone from fastapi import Depends, FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from app.auth import require_auth from app.config import settings from app.database import close_db, init_db @asynccontextmanager async def lifespan(app: FastAPI): await init_db() yield await close_db() app = FastAPI( title="Cotizador E3 API", description=( "API REST para el sistema de cotizaciones de marketing digital de Consultoría E3. " "Optimizada para integración con n8n y agentes de IA (OpenClaw, Claude, ChatGPT) " "via MCP (Model Context Protocol)." ), version="1.0.0", lifespan=lifespan, docs_url="/docs", redoc_url="/redoc", openapi_url="/openapi.json", openapi_tags=[ {"name": "Health", "description": "Health check y información de la API"}, {"name": "Auth", "description": "Autenticación (API Key + JWT)"}, {"name": "Clientes", "description": "Gestión de clientes"}, {"name": "Catálogo", "description": "Catálogo de servicios de marketing digital"}, {"name": "Categorías", "description": "Categorías de servicios"}, {"name": "Cotizaciones", "description": "Cotizaciones — CRUD completo con servicios y plan CRM"}, {"name": "Paquetes", "description": "Paquetes de servicios con fases"}, {"name": "Configuración", "description": "Configuración de la empresa (key-value)"}, {"name": "Bonos", "description": "Bonos disponibles para cotizaciones"}, {"name": "Financiamiento", "description": "Planes y cálculo de financiamiento"}, {"name": "Export", "description": "Exportación de PDF, Excel y CSV"}, {"name": "Import", "description": "Importación masiva de servicios"}, ], ) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) @app.exception_handler(Exception) async def global_exception_handler(request: Request, exc: Exception): return JSONResponse( status_code=500, content={"error": "Error interno del servidor", "detail": str(exc)}, ) # Register routers from app.routers import ( auth, bonos, catalogo, categorias, clientes, configuracion, cotizaciones, export_, financiamiento, health, import_, paquetes, ) app.include_router(health.router) app.include_router(auth.router) app.include_router(clientes.router) app.include_router(catalogo.router) app.include_router(categorias.router) app.include_router(cotizaciones.router) app.include_router(paquetes.router) app.include_router(configuracion.router) app.include_router(bonos.router) app.include_router(financiamiento.router) app.include_router(export_.router) app.include_router(import_.router) # Mount MCP server try: from app.mcp.server import server as mcp_server from mcp.server.streamable_http import StreamableHTTPServerTransport @app.post("/mcp") async def mcp_endpoint(request: Request, _auth: dict = Depends(require_auth)): """MCP (Model Context Protocol) endpoint for AI agents. Requiere autenticacion (X-API-Key o Bearer JWT), igual que el resto del API. Sin esto el endpoint quedaba abierto a internet: el servicio recibe dominio publico en produccion (docker-compose.coolify.yml, SERVICE_FQDN_API_8000) y las herramientas MCP leen y escriben cotizaciones directamente. """ transport = StreamableHTTPServerTransport(mcp_server) return await transport.handle_request(request) @app.get("/mcp") async def mcp_info(): """MCP server info. Use POST for actual MCP communication.""" return { "name": "cotizador-e3", "version": "1.0.0", "protocol": "mcp", "description": "MCP server for Cotizador E3 quotation system", } except Exception as exc: # noqa: BLE001 # Antes esto capturaba solo ImportError. Cuando el SDK de MCP salto a 2.0.0 y # elimino Server.list_tools(), el import lanzo AttributeError, se escapo del # except y tumbo TODA la aplicacion: el REST tambien dejo de responder. # El servidor MCP es opcional; el REST no. Si el MCP no monta, se registra y # la API sigue de pie. import logging logging.getLogger("uvicorn.error").error( "No se pudo montar el servidor MCP (%s: %s). El REST sigue disponible.", type(exc).__name__, exc, ) @app.get("/", include_in_schema=False) async def root(): return { "name": "Cotizador E3 API", "version": "1.0.0", "docs": "/docs", "openapi": "/openapi.json", "health": "/health", "mcp": "/mcp", } if __name__ == "__main__": import uvicorn uvicorn.run("main:app", host=settings.API_HOST, port=settings.API_PORT, reload=True)