Authentication

Authentication for the MCP endpoint is a Litestar middleware concern. Apps that already have an authentication middleware (JWT backends, Google IAP, custom token validators) get MCP authentication for free — the middleware populates request.user and request.auth before any route handler runs, including MCP tool handlers.

MCPAuthConfig is metadata-only: it describes the auth surface advertised by /.well-known/oauth-protected-resource so MCP clients can discover how to obtain a token. Enforcement lives in whichever middleware you install on the app.

Three Integration Paths

Path A — Bring your own auth middleware. Use this when your Litestar app already ships an AbstractAuthenticationMiddleware (or Litestar's built-in JWT backends). MCP tool handlers inherit request.user / request.auth automatically. No MCPAuthBackend needed. See docs/examples/notes/sqlspec/google_iap.py.

Path B — Built-in MCPAuthBackend. Install MCPAuthBackend via DefineMiddleware. It validates bearer tokens against OIDC providers and/or a custom token_validator, then populates connection.user via an optional user_resolver. See docs/examples/notes/sqlspec/cloud_run_jwt.py.

MCPAuthBackend with a custom validator
app = Litestar(
    route_handlers=[],
    plugins=[LitestarMCP(MCPConfig(auth=MCPAuthConfig(issuer="https://auth.example.com")))],
    middleware=[DefineMiddleware(MCPAuthBackend, token_validator=validate_token)],
)

Path C — MCPAuthBackend with OIDC providers. For production OIDC workloads, pass one or more OIDCProviderConfig entries. The backend handles JWKS discovery, caching, and signature verification.

MCPAuthBackend with OIDC auto-discovery
app = Litestar(
    route_handlers=[],
    plugins=[
        LitestarMCP(
            MCPConfig(
                auth=MCPAuthConfig(
                    issuer="https://auth.example.com",
                    audience="https://api.example.com/mcp",
                )
            )
        )
    ],
    middleware=[
        DefineMiddleware(
            MCPAuthBackend,
            providers=[
                OIDCProviderConfig(
                    issuer="https://auth.example.com",
                    audience="https://api.example.com/mcp",
                    algorithms=["RS256"],
                )
            ],
        )
    ],
)

Composable OIDC Factory

create_oidc_validator() returns an async callable that validates a single token against an OIDC issuer. Pass it as MCPAuthBackend(token_validator=...) or use it inside your own middleware. Both clock_skew and jwks_cache_ttl are configurable.

Injectable JWKS Cache

JWKSCache is a protocol-shaped seam for apps that already run their own JWKS / OIDC discovery cache. Pass a shared instance to every validator to avoid redundant network fetches:

docs/examples/snippets/jwks_cache_shared.py
from litestar_mcp import DefaultJWKSCache, create_oidc_validator
from litestar_mcp.auth import OIDCProviderConfig

shared_cache = DefaultJWKSCache()

validator_a = create_oidc_validator(
    "https://company.okta.com",
    "api://mcp-tools",
    jwks_cache=shared_cache,
)
provider_b = OIDCProviderConfig(
    issuer="https://company.okta.com",
    audience="api://admin",
    jwks_cache=shared_cache,
)

When no cache is passed, the validator uses a process-wide default — matching 0.4.0 behaviour — so existing apps need no code changes. Any object implementing async get / async set(*, ttl=int) / async invalidate satisfies the protocol, so a Redis-backed or application-specific cache can drop in cleanly.

Authorization via Guards

Scopes declared on @mcp_tool(scopes=[...]) are discovery metadata only — they surface under tools[].annotations.scopes in tools/list and in /.well-known/oauth-protected-resource. MCP tool dispatch does not enforce scopes inline; attach a Litestar Guard to the route / router / controller for authorization. Guards receive the same ASGIConnection that an HTTP request does, so existing requires_x guards work unchanged on MCP:

docs/examples/snippets/authorization_guard.py
from litestar import Controller, Litestar, get
from litestar.exceptions import PermissionDeniedException

from litestar_mcp import LitestarMCP

if TYPE_CHECKING:
    from litestar.connection import ASGIConnection
    from litestar.handlers.base import BaseRouteHandler


_UNAUTHENTICATED = "Unauthenticated"


def requires_authenticated_user(
    connection: "ASGIConnection[Any, Any, Any, Any]",
    handler: "BaseRouteHandler",
) -> None:
    """Reject callers whose middleware did not populate ``scope['auth']``."""
    _ = handler
    if connection.scope.get("auth") is None:
        raise PermissionDeniedException(_UNAUTHENTICATED)


class ReportController(Controller):
    path = "/reports"
    guards = [requires_authenticated_user]

    @get("/", mcp_tool="generate_report", scopes=["report:read"], sync_to_thread=False)
    def generate_report(self) -> dict[str, str]:
        return {"status": "ok"}


app = Litestar(route_handlers=[ReportController], plugins=[LitestarMCP()])

Discovery Metadata

When MCPAuthConfig is attached to MCPConfig, the plugin publishes /.well-known/oauth-protected-resource with the configured issuer, audience, and scopes. This endpoint is always unauthenticated (via Litestar's exclude_from_auth opt key) so clients can bootstrap their auth flow.

Mapping Claims to Users

Middleware populates request.auth with the validated claims dict and request.user with the resolved user object (if a user_resolver is configured). Tool handlers access these via normal Litestar DI:

  • Read request.user directly in the handler signature.

  • Write a Provide(...) dependency that extracts the identity from request.user and returns a domain type.

  • Enforce scopes or roles via guards that inspect request.auth.

See the reference-notes examples for end-to-end wiring across JWT, Dishka, Advanced Alchemy, and Google IAP variants.