"""Public-origin resolution + OAuth discovery metadata documents. The OAuth issuer / discovery URLs / token audience must all be absolute ``https://`` URLs that point at *this* deployment. We prefer the pinned ``settings.PUBLIC_BASE_URL`` and otherwise derive the origin from the request (``Host`` + ``X-Forwarded-Proto``). Both a FastAPI ``Request`` and a raw ASGI ``scope`` are supported so the resource-server 401 in the MCP middleware (which has no ``Request``) and the discovery routes share one source of truth. """ from __future__ import annotations from app.core.config import settings #: Path the MCP Streamable-HTTP app is mounted at (see ``app.main``). MCP_MOUNT_PATH = "/api/v1/mcp" def _origin_from_headers(host: str | None, proto: str | None) -> str | None: if not host: return None scheme = (proto or "https").split(",")[0].strip() or "https" return f"{scheme}://{host.strip()}" def origin_from_request(request) -> str: """Resolve the canonical origin (``scheme://host``) for a Request.""" if settings.PUBLIC_BASE_URL: return settings.PUBLIC_BASE_URL host = request.headers.get("host") proto = request.headers.get("x-forwarded-proto") or request.url.scheme return _origin_from_headers(host, proto) or "http://localhost:8000" def origin_from_scope(scope) -> str: """Resolve the canonical origin from a raw ASGI scope.""" if settings.PUBLIC_BASE_URL: return settings.PUBLIC_BASE_URL headers = { k.decode("latin1").lower(): v.decode("latin1") for k, v in scope.get("headers", []) } proto = headers.get("x-forwarded-proto") or scope.get("scheme") or "https" return _origin_from_headers(headers.get("host"), proto) or "http://localhost:8000" def mcp_resource_uri(origin: str) -> str: """Canonical MCP resource identifier (RFC 8707 audience). No trailing slash, per the MCP authorization spec's interoperability guidance.""" return f"{origin.rstrip('/')}{MCP_MOUNT_PATH}" def acceptable_audiences(origin: str) -> list[str]: """Audiences the MCP middleware accepts. The connector advertises the no-slash canonical form, but some clients append the mount's trailing slash — accept both so a stray ``/`` never bounces a valid token.""" base = mcp_resource_uri(origin) return [base, base + "/"] def protected_resource_metadata(origin: str) -> dict: """RFC 9728 Protected Resource Metadata for the MCP endpoint.""" return { "resource": mcp_resource_uri(origin), "authorization_servers": [origin.rstrip("/")], "scopes_supported": ["mcp"], "bearer_methods_supported": ["header"], "resource_documentation": f"{origin.rstrip('/')}/help", } def authorization_server_metadata(origin: str) -> dict: """RFC 8414 Authorization Server Metadata.""" base = origin.rstrip("/") return { "issuer": base, "authorization_endpoint": f"{base}/api/v1/auth/oauth/authorize", "token_endpoint": f"{base}/api/v1/auth/oauth/token", "registration_endpoint": f"{base}/api/v1/auth/oauth/register", "revocation_endpoint": f"{base}/api/v1/auth/oauth/revoke", "scopes_supported": ["mcp"], "response_types_supported": ["code"], "grant_types_supported": ["authorization_code", "refresh_token"], "code_challenge_methods_supported": ["S256"], # Public clients only — PKCE is the proof-of-possession, no secret. "token_endpoint_auth_methods_supported": ["none"], } __all__ = [ "MCP_MOUNT_PATH", "acceptable_audiences", "authorization_server_metadata", "mcp_resource_uri", "origin_from_request", "origin_from_scope", "protected_resource_metadata", ]