diff --git a/mcp_server/app/core/lifecycle.py b/mcp_server/app/core/lifecycle.py deleted file mode 100644 index b3aa295..0000000 --- a/mcp_server/app/core/lifecycle.py +++ /dev/null @@ -1,36 +0,0 @@ -import structlog -from fastapi import FastAPI - -from app.core.config import settings -from app.payments.omniclaw_client import OmniclawPaymentClient -from omniclaw.onboarding import load_managed_entity_secret - -logger = structlog.get_logger(__name__) - - -async def startup_event(app: FastAPI) -> None: - """Application startup validation and warmup.""" - logger.info( - "mcp_startup", - env=settings.ENVIRONMENT, - project=settings.PROJECT_NAME, - network=settings.OMNICLAW_NETWORK, - ) - - if settings.ENVIRONMENT == "prod": - if not settings.CIRCLE_API_KEY: - raise RuntimeError("Missing CIRCLE_API_KEY in production") - api_key = settings.CIRCLE_API_KEY.get_secret_value() if settings.CIRCLE_API_KEY else None - managed_secret = load_managed_entity_secret(api_key) if api_key else None - if not settings.ENTITY_SECRET and not managed_secret: - raise RuntimeError("Missing ENTITY_SECRET in production") - - # Warm up singleton early so configuration issues fail fast. - await OmniclawPaymentClient.get_instance() - logger.info("mcp_startup_ready") - - -async def shutdown_event(app: FastAPI) -> None: - """Application shutdown cleanup.""" - await OmniclawPaymentClient.close_instance() - logger.info("mcp_shutdown_complete") diff --git a/mcp_server/app/main.py b/mcp_server/app/main.py index c807340..93ccbe5 100644 --- a/mcp_server/app/main.py +++ b/mcp_server/app/main.py @@ -8,7 +8,6 @@ from fastapi.responses import JSONResponse from app.core.config import settings -from app.core.lifecycle import shutdown_event, startup_event from app.core.logging import setup_logging from app.mcp.fastmcp_server import mcp from app.webhooks.circle import router as circle_webhook_router @@ -16,26 +15,8 @@ setup_logging() logger = structlog.get_logger(__name__) -# Create FastMCP app first to get its lifespan -mcp_app = mcp.http_app(path="/", stateless_http=True) - - -@asynccontextmanager -async def lifespan(app: FastAPI): - # Start FastMCP lifespan (it's a function that returns async context manager) - async with mcp_app.lifespan(app): - # Start our custom startup - await startup_event(app) - yield - # Shutdown our custom logic - await shutdown_event(app) - - -app = FastAPI( - title=settings.PROJECT_NAME, - lifespan=lifespan, - openapi_url=f"{settings.API_V1_STR}/openapi.json", -) +# Create unified FastMCP app (which is a FastAPI app under the hood) +app = mcp.http_app(path="/", stateless_http=True) # Middleware for Correlation ID and Request Logging @@ -81,11 +62,7 @@ async def global_exception_handler(request: Request, exc: Exception): allow_headers=["*"], ) -# Mount FastMCP server at /mcp endpoint (stateless mode for horizontal scaling) -# Note: path="/" because FastAPI mount strips the /mcp prefix -app.mount("/mcp", mcp_app) - -# Include webhook router +# Include webhook router directly on the unified app app.include_router( circle_webhook_router, prefix=f"{settings.API_V1_STR}/webhooks", tags=["webhooks"] ) diff --git a/mcp_server/app/mcp/fastmcp_server.py b/mcp_server/app/mcp/fastmcp_server.py index f14f1b4..0ef4225 100644 --- a/mcp_server/app/mcp/fastmcp_server.py +++ b/mcp_server/app/mcp/fastmcp_server.py @@ -32,6 +32,36 @@ logger = structlog.get_logger(__name__) +async def mcp_lifespan(server: FastMCP): + """Lifecycle manager for the MCP server resources.""" + logger.info( + "mcp_startup_init", + env=settings.ENVIRONMENT, + project=settings.PROJECT_NAME, + network=settings.OMNICLAW_NETWORK, + ) + + # Validate production environment + if settings.ENVIRONMENT == "prod": + from omniclaw.onboarding import load_managed_entity_secret + + api_key = settings.CIRCLE_API_KEY.get_secret_value() if settings.CIRCLE_API_KEY else None + managed_secret = load_managed_entity_secret(api_key) if api_key else None + if not settings.ENTITY_SECRET and not managed_secret: + raise RuntimeError("Missing ENTITY_SECRET in production") + + # Warm up client singleton + await OmniclawPaymentClient.get_instance() + logger.info("mcp_startup_ready") + + try: + yield + finally: + # Shutdown: Clean up your resources here + await OmniclawPaymentClient.close_instance() + logger.info("mcp_shutdown_complete") + + mcp = FastMCP( name="OmniClaw MCP Server", instructions=( @@ -40,6 +70,7 @@ "transaction sync, and trust checks." ), auth=get_auth_provider() if settings.MCP_REQUIRE_AUTH else None, + lifespan=mcp_lifespan, )