Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/ad_buyer/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
37 changes: 25 additions & 12 deletions src/ad_buyer/interfaces/api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@
import logging
import sqlite3
import uuid
from contextlib import asynccontextmanager
from datetime import datetime
from typing import Any, Optional

from fastapi import BackgroundTasks, Depends, FastAPI, HTTPException, Request
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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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.

Expand Down
16 changes: 11 additions & 5 deletions src/ad_buyer/interfaces/mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
)


Expand Down Expand Up @@ -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")
Loading