"""FastAPI route assembly for the stable MCP dataplane.""" from __future__ import annotations from typing import Any from fastapi import APIRouter, Request, Response, status from fastapi.responses import JSONResponse, StreamingResponse from .proxy import DataplaneProxy MCP_TRANSPORT_STATE_HEADERS = frozenset({b"mcp-session-id"}) def _without_transport_state(request: Request) -> Request: """Return a request view that cannot carry MCP transport session state. The public Hub is deliberately stateless for every supported MCP revision. MCP 2025-11-25 permits servers to omit ``Mcp-Session-Id`` entirely; the client only has to echo an ID when the server chooses to issue one. Keeping this boundary sessionless avoids coupling ChatGPT requests to ContextForge process-local or pooled transport state. """ scope = dict(request.scope) scope["headers"] = [ (key, value) for key, value in request.scope.get("headers", []) if key.lower() not in MCP_TRANSPORT_STATE_HEADERS ] return Request(scope, receive=request.receive) def _strip_response_transport_state(response: StreamingResponse | JSONResponse) -> None: """Never expose an upstream MCP transport session identifier publicly.""" if "mcp-session-id" in response.headers: del response.headers["mcp-session-id"] def build_router(proxy: DataplaneProxy) -> APIRouter: router = APIRouter() @router.api_route( "/mcp/{project_slug}/{environment}", methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"], response_model=None, ) async def mcp_proxy( request: Request, project_slug: str, environment: str ) -> StreamingResponse | JSONResponse: upstream_request = _without_transport_state(request) response = await proxy.handle(upstream_request, project_slug, environment) _strip_response_transport_state(response) return response @router.get("/healthz") @router.get("/health") async def health() -> dict[str, Any]: return {"status": "healthy", "component": "dataplane-router"} @router.get("/ready") @router.get("/health/ready") async def ready(response: Response) -> dict[str, Any]: result = proxy.registry.health() if result["status"] != "ready": response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE return result | {"component": "dataplane-router"} return router