mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-10 22:32:16 +02:00
feat(mcp): add health endpoint with public-path auth exemption
This commit is contained in:
parent
2acc1426bf
commit
2b9daead58
2 changed files with 21 additions and 8 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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=["*"],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue