diff --git a/src/ad_buyer/config/settings.py b/src/ad_buyer/config/settings.py index 6c8ac08..c3c7829 100644 --- a/src/ad_buyer/config/settings.py +++ b/src/ad_buyer/config/settings.py @@ -63,7 +63,7 @@ def get_seller_endpoints(self) -> list[str]: crew_max_iterations: int = 15 # CORS - cors_allowed_origins: str = "http://localhost:3000,http://localhost:8080" + cors_allowed_origins: str = "*" def get_cors_origins(self) -> list[str]: """Parse CORS allowed origins from comma-separated string.""" diff --git a/src/ad_buyer/interfaces/api/main.py b/src/ad_buyer/interfaces/api/main.py index 61acb72..60166cf 100644 --- a/src/ad_buyer/interfaces/api/main.py +++ b/src/ad_buyer/interfaces/api/main.py @@ -7,6 +7,7 @@ import logging import sqlite3 import uuid +from contextlib import asynccontextmanager from datetime import datetime from typing import Any, Optional @@ -14,6 +15,7 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from pydantic import BaseModel, Field +from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware import sys @@ -53,17 +55,38 @@ def _current_settings(): ], ) +app.add_middleware(ProxyHeadersMiddleware, trusted_hosts="*") + app.add_middleware( CORSMiddleware, allow_origins=settings.get_cors_origins(), allow_methods=["*"], allow_headers=["*"], + allow_credentials=False, + expose_headers=["*"], ) -# Mount MCP SSE server -from ..mcp_server import mount_mcp +# Mount MCP server (Streamable HTTP at /mcp) +# Starlette doesn't call mounted sub-app lifespans, so we must run the +# session manager ourselves to keep its task group alive. +from ..mcp_server import mcp as _mcp_server, mount_mcp mount_mcp(app) + +@asynccontextmanager +async def lifespan(application): + store = _get_order_store() + if store is not None: + application.include_router(_create_order_router(store)) + else: + logger.warning("OrderStore unavailable at startup; order endpoints not mounted") + + async with _mcp_server.session_manager.run(): + yield + + +app.router.lifespan_context = lifespan + # Mount order status/audit router (buyer-nz9) from .order_endpoints import create_order_router @@ -185,16 +208,6 @@ def _get_order_store() -> Optional[OrderStore]: from .order_endpoints import create_order_router as _create_order_router -@app.on_event("startup") -async def _mount_order_router() -> None: - """Mount order router once the OrderStore is available at startup.""" - store = _get_order_store() - if store is not None: - app.include_router(_create_order_router(store)) - else: - logger.warning("OrderStore unavailable at startup; order endpoints not mounted") - - def _persist_job(job_id: str, job: dict[str, Any]) -> None: """Best-effort dual-write of a job dict to the DealStore. diff --git a/src/ad_buyer/interfaces/mcp_server.py b/src/ad_buyer/interfaces/mcp_server.py index 8eadbd7..2405415 100644 --- a/src/ad_buyer/interfaces/mcp_server.py +++ b/src/ad_buyer/interfaces/mcp_server.py @@ -85,6 +85,13 @@ "Use the available tools to check system status, review configuration, " "and manage buyer workflows." ), + # streamable_http_path="/" so that when mounted at /mcp in FastAPI the + # endpoint resolves to /mcp (not /mcp/mcp which is the default). + streamable_http_path="/", + # host="0.0.0.0" disables the auto DNS-rebinding protection that FastMCP + # applies when host is 127.0.0.1/localhost. That protection blocks requests + # from Cloud Run (Host header is the public *.run.app domain) with 421. + host="0.0.0.0", ) @@ -2875,13 +2882,12 @@ async def help_prompt() -> list[Message]: def mount_mcp(app: FastAPI) -> None: - """Mount the MCP SSE server onto a FastAPI application. + """Mount the MCP server onto a FastAPI application. - Creates an SSE endpoint at /mcp/sse that MCP clients can connect to. + Creates a Streamable HTTP endpoint at /mcp (MCP standard 2025-06-18). Args: app: The FastAPI application to mount onto. """ - sse_app = mcp.sse_app() - app.mount("/mcp/sse", sse_app) - logger.info("MCP SSE server mounted at /mcp/sse") + app.mount("/mcp", mcp.streamable_http_app()) + logger.info("MCP server mounted: Streamable HTTP at /mcp")