feat(mcp): add health endpoint with public-path auth exemption

This commit is contained in:
CREDO23 2026-07-07 18:05:56 +02:00
parent 2acc1426bf
commit 2b9daead58
2 changed files with 21 additions and 8 deletions

View file

@ -6,10 +6,13 @@ not reach the tool handler. A pure middleware binds the key in the request's own
task, from which the SDK's per-request handling inherits it.
Requests without a key are rejected here so no tool ever runs unauthenticated.
Paths in ``public_paths`` (e.g. the health probe) skip the check entirely.
"""
from __future__ import annotations
from collections.abc import Iterable
from starlette.datastructures import Headers
from starlette.responses import JSONResponse
from starlette.types import ASGIApp, Receive, Scope, Send
@ -21,11 +24,12 @@ from .identity import bind_api_key, unbind_api_key
class ApiKeyIdentityMiddleware:
"""Binds the per-request API key into the identity contextvar, or 401s."""
def __init__(self, app: ASGIApp) -> None:
def __init__(self, app: ASGIApp, public_paths: Iterable[str] = ()) -> None:
self._app = app
self._public_paths = frozenset(public_paths)
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if scope["type"] != "http":
if scope["type"] != "http" or scope["path"] in self._public_paths:
await self._app(scope, receive, send)
return

View file

@ -1,23 +1,32 @@
"""Assemble the streamable-http ASGI app for the remote transport.
"""Wrap the SDK's MCP endpoint with identity + CORS for the remote transport.
Wraps the SDK's MCP endpoint with the API-key identity middleware and CORS.
CORS sits outermost so browser preflight (which carries no key) is answered
before the identity middleware, and clients can read the ``Mcp-Session-Id``
header the streamable-http protocol relies on.
CORS is outermost so keyless browser preflight is answered before the identity
middleware. ``/health`` is a public path, exempt from the key check.
"""
from __future__ import annotations
from mcp.server.fastmcp import FastMCP
from starlette.requests import Request
from starlette.responses import JSONResponse
from starlette.middleware.cors import CORSMiddleware
from starlette.types import ASGIApp
from ..auth.middleware import ApiKeyIdentityMiddleware
HEALTH_PATH = "/health"
async def _health(_request: Request) -> JSONResponse:
return JSONResponse({"status": "ok"})
def build_http_app(mcp: FastMCP) -> ASGIApp:
"""Return the MCP streamable-http app wrapped with identity + CORS."""
app: ASGIApp = ApiKeyIdentityMiddleware(mcp.streamable_http_app())
mcp.custom_route(HEALTH_PATH, methods=["GET"])(_health)
app: ASGIApp = ApiKeyIdentityMiddleware(
mcp.streamable_http_app(), public_paths={HEALTH_PATH}
)
return CORSMiddleware(
app,
allow_origins=["*"],