"""Per-MCP-request context shared by middleware and tool handlers. The Streamable-HTTP middleware (``app.mcp.middleware``) authenticates each incoming request, then sets these contextvars before forwarding to FastMCP. Tool implementations call :func:`require_mcp_user` to read the authenticated user without having to thread anything through function signatures. Reset is automatic at end-of-task (FastAPI / Starlette runs each request in its own asyncio task with its own contextvar copy), so a slow tool call never leaks state into the next request. """ from __future__ import annotations from contextvars import ContextVar from uuid import UUID, uuid4 #: ID of the IdentityPerson on whose behalf the MCP request is running. mcp_user_id: ContextVar[UUID | None] = ContextVar( "helix_mcp_user_id", default=None, ) #: Trace id used by audit logs. The middleware mints a fresh UUID per #: request (Streamable HTTP doesn't carry our normal X-Trace-Id) so #: every tool call within one MCP request shares one trace. mcp_trace_id: ContextVar[UUID | None] = ContextVar( "helix_mcp_trace_id", default=None, ) #: Client IP that the middleware extracted (X-Real-IP / X-Forwarded-For). mcp_client_ip: ContextVar[str | None] = ContextVar( "helix_mcp_client_ip", default=None, ) #: User-Agent header — useful for "this MCP request came from Claude #: Desktop on macOS" in the audit log. mcp_user_agent: ContextVar[str | None] = ContextVar( "helix_mcp_user_agent", default=None, ) def require_mcp_user_id() -> UUID: """Return the authenticated user's id, raising if not set. The MCP middleware always sets this before invoking the FastMCP handler, so a missing value indicates an internal misconfiguration (e.g. a tool being called outside the HTTP path) and we want a loud failure rather than an anonymous-user fallback. """ val = mcp_user_id.get() if val is None: raise RuntimeError( "MCP tool invoked outside an authenticated request — " "did you forget to mount the PAT auth middleware?" ) return val def get_or_init_trace_id() -> UUID: """Return the request's trace_id, minting one if the middleware didn't (e.g. for direct in-process invocation in tests). """ tid = mcp_trace_id.get() if tid is None: tid = uuid4() mcp_trace_id.set(tid) return tid __all__ = [ "get_or_init_trace_id", "mcp_client_ip", "mcp_trace_id", "mcp_user_agent", "mcp_user_id", "require_mcp_user_id", ]