71 lines
2.4 KiB
Python
71 lines
2.4 KiB
Python
"""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
|