Auth¶
Backends, permissions, response builders, rate limits, and OAuth-related views
(RFC 9728 PRM + the opt-in contrib oauth/ mount).
Protocols¶
MCPAuthBackend ¶
Bases: Protocol
Pluggable authentication for the MCP transport.
The transport calls :meth:authenticate on every request. A backend that
returns None signals "no valid credentials" — the transport then emits
a 401 with WWW-Authenticate built from
:meth:www_authenticate_challenge. protected_resource_metadata powers
the /.well-known/oauth-protected-resource view (RFC 9728) and returns
a :class:ProtectedResourceMetadata dataclass; the contrib PRM ViewSet
calls .to_dict() for the wire shape.
:meth:authorization_server_metadata is consumed by the optional
rest_framework_mcp.contrib.oauth mount (Phase 10d+) — backends
that host an authorization server return an :class:AuthorizationServerMetadata
dataclass; backends that don't host one raise :class:NotImplementedError
so the contrib code can skip the AS endpoint matrix cleanly.
Backends MUST be safe to instantiate without arguments — settings-driven configuration belongs inside the backend's own module.
TokenInfo
dataclass
¶
Authenticated principal carried alongside an MCP request.
Backends construct this once per request and attach it to the Django
HttpRequest (as request.mcp_token). Permission classes consult it
to gate tool/resource access.
user: the resolved Django user (orAnonymousUser-equivalent).Anyhere because the user model is project-defined.scopes: OAuth scopes proven by the bearer token.audience: theaudclaim — must match the canonical/mcpURL per RFC 8707; backends are responsible for that comparison.raw: backend-specific opaque payload (theAccessTokenrow, the JWT claims dict, etc.) for advanced use cases.
MCPPermission ¶
Bases: Protocol
Per-tool / per-resource gate evaluated after authentication.
The transport pulls authenticated state from the request as a
:class:TokenInfo and asks each permission whether the call may proceed.
Returning False becomes a 403 + WWW-Authenticate; raising lets the
permission supply a richer payload via the JSON-RPC error path.
Permissions MUST be cheap to construct (we instantiate them per-binding at discovery time) and side-effect free at evaluation.
required_scopes ¶
Scopes to advertise in WWW-Authenticate when this permission denies.
Defaulting via Protocol: implementations that don't override this
should return []. We type it as a method (not a property) so it's
easy to compute dynamically from constructor args.
Backends¶
AllowAnyBackend ¶
Development / test backend that authenticates every request as anonymous.
DO NOT use in production. The protected_resource_metadata payload is
intentionally minimal so misconfiguration is loud rather than silent.
DjangoOAuthToolkitBackend ¶
Resource-server adapter for django-oauth-toolkit (DOT).
Validates the bearer token using DOT's own validators, then projects the
result into a :class:TokenInfo. The oauth2_provider package is
imported lazily inside authenticate because it's an optional extra
(pip install "djangorestframework-mcp-server[oauth]") — importing this module
never blows up just because DOT is absent; the ImportError only fires
when a request actually reaches authentication.
Audience enforcement (RFC 8707): when REST_FRAMEWORK_MCP["RESOURCE_URL"]
is configured, the token's resource claim must match exactly. Tokens
without a bound resource are rejected. Setting RESOURCE_URL to None
(the default) disables enforcement — appropriate for dev or for setups
where audience binding is performed by an upstream gateway.
authorization_server_metadata ¶
Return the RFC 8414 metadata payload for the DOT-hosted authorization server.
Pulls the issuer, endpoint URLs, supported grant / response types,
and scopes from :setting:REST_FRAMEWORK_MCP['SERVER_INFO']'s
authorization_servers key (first entry) plus the
contrib.oauth mount convention that endpoints live at
/oauth/authorize/, /oauth/token/, /oauth/register/.
Missing values fall through as empty strings / lists so the wire
shape is always valid JSON; consumers are expected to populate
SERVER_INFO for production deployments.
Permissions¶
ScopeRequired ¶
Allow only requests whose token carries every listed OAuth scope.
Constructor takes the scopes positionally so usage stays compact:
ScopeRequired(["invoices:write"]).
DjangoPermRequired ¶
Allow only requests whose user has the given Django permission(s).
Wraps user.has_perm from the standard Django auth backend. A token
backed by AnonymousUser will always be rejected — that is the point
of using this class instead of :class:ScopeRequired.
DRFPermissionAdapter ¶
Bridge a DRF BasePermission class into the :class:MCPPermission Protocol.
Sister-repo 0.12.0 added permission_classes on ServiceSpec and
SelectorSpec — a sequence of DRF BasePermission classes. The MCP
transport doesn't go through DRF views, so each class is wrapped in this
adapter at registration time; the wrapped instance is constructed once
(mirrors sister-repo's [perm() for perm in spec.permission_classes]
in its view get_permissions).
The DRF instance receives a synthesised :class:rest_framework.request.Request
with user set to token.user and a lightweight view stand-in
sufficient for the DRF permission contract (request, action).
The HTTP method on the underlying HttpRequest is left untouched
(unlike :func:build_internal_drf_request, which forces POST for
mutation-flow dispatch) — permission evaluation is method-agnostic.
Rate limits¶
MCPRateLimit ¶
Bases: Protocol
Per-binding rate limiter, evaluated after authentication and permissions.
The single consume call is the gate AND the bookkeeping update — there
is no separate "check then commit" because that pattern races under
concurrency. Implementations decrement quotas atomically in storage; the
return value is the suggested Retry-After in seconds when the limit
has been hit, or None to allow the call.
Returning 0 is allowed (e.g. "denied but window resets immediately")
but most implementations will return a positive integer.
Limiters are constructed per-binding at registration time; keep them cheap to construct and thread-safe at evaluation. State that crosses requests must live in shared storage (Django cache, Redis, …), not on the instance — instance state would be lost the moment the binding is re-used across worker processes.
FixedWindowRateLimit ¶
A fixed-window-counter rate limiter backed by django.core.cache.
The window is bucketed by absolute time: each integer multiple of
per_seconds since the epoch starts a new counter. Simple and
sufficient for protecting against runaway clients; not as smooth as a
sliding window but doesn't require a sorted set.
The cache key is namespaced with namespace so multiple limits on the
same binding (e.g. burst + steady-state) don't share counters. key
customises the bucket dimension — defaults to per-token-user.
The cache must be a shared backend in multi-process deployments;
Django's locmem cache is fine for tests but won't enforce a global
limit across worker processes.
SlidingWindowRateLimit ¶
Sliding-window rate limiter using a list of timestamps in cache.
Avoids the well-known fixed-window edge case where a client can issue
2 * max_calls requests across two adjacent windows. Stores the
timestamps of recent calls in a Django cache entry; on each call,
expired entries are pruned and the live count is compared against
max_calls.
Trade-offs vs :class:FixedWindowRateLimit:
- Smoother: limits actual request rate over the trailing
per_secondswindow, not bucketed counts. - Memory cost: the timestamp list grows with traffic up to
max_callsentries per key; negligible for typical limits. - Read-modify-write: doesn't have the atomic guarantees of the
fixed-window's
cache.add+cache.incrprimitives. Concurrent calls can see slightly stale state and admit a small number of extra requests under contention. For strict atomicity in multi-worker deployments, use a Redis-backed limiter with Lua.
The cache must be a shared backend in multi-process deployments;
Django's locmem works for tests but won't share state across
workers.
TokenBucketRateLimit ¶
Token-bucket rate limiter using Django cache for state.
A bucket has at most capacity tokens. Each accepted call consumes
one token; the bucket refills continuously at refill_per_second
tokens per second. When empty, the limiter returns the suggested
retry-after time until at least one token is available again.
Trade-offs vs the sliding-window scheme:
- Burst-friendly: a full bucket can absorb a burst of
capacityrequests instantly, then rate-limit the steady state atrefill_per_second. Useful when consumers naturally batch. - Read-modify-write: like the sliding-window class, this is not strictly atomic across concurrent workers. Under contention a small number of extra tokens may slip through. For strict atomicity in multi-worker deployments, back the limiter with a Redis-Lua script.
The cache must be a shared backend (Memcached or Redis) in
multi-process deployments; Django's locmem is fine for tests but
won't share state across workers.
Response helpers¶
build_unauthenticated_response ¶
Build a spec-compliant 401 response with the supplied WWW-Authenticate value.
The body is a small JSON envelope so MCP clients that surface error payloads to the user see a meaningful message rather than an empty body.
build_insufficient_scope_response ¶
Build a 403 response signalling missing OAuth scope.
Per RFC 6750, the error="insufficient_scope" value belongs in the
WWW-Authenticate header — that's already baked into challenge.
Protected Resource Metadata (RFC 9728)¶
ProtectedResourceMetadataViewSet ¶
Bases: ViewSet
RFC 9728 OAuth 2.0 Protected Resource Metadata endpoint.
Mounted at /.well-known/oauth-protected-resource by :class:MCPServer.
Single-action ViewSet — the canonical GET is wired as list via
ProtectedResourceMetadataViewSet.as_view({"get": "list"}, auth_backend=...)
so the URL conf doesn't need a router.
Delegates payload construction to the configured
:meth:MCPAuthBackend.protected_resource_metadata, which returns a
:class:ProtectedResourceMetadata dataclass. The backend is
instance-scoped — passed in via as_view — so multiple servers in
one process can advertise different metadata.
DRF's default authentication / permission / throttling layers are
disabled (empty lists below): PRM is a public discovery endpoint by
design and the MCP transport owns its own auth pipeline through
:class:MCPAuthBackend. The renderer is pinned to JSON because the
payload shape is RFC-defined; content negotiation would be noise.
ProtectedResourceMetadata
dataclass
¶
RFC 9728 OAuth 2.0 Protected Resource Metadata payload.
Returned by :meth:MCPAuthBackend.protected_resource_metadata and
serialised onto the wire by the PRM ViewSet. Keys map 1:1 to the
RFC 9728 field names; warning is the package-local extension
that AllowAnyBackend uses to make dev-mode misconfiguration
loud.
OAuth contrib (opt-in)¶
build_oauth_urlpatterns(server, *, include_dcr=False, include_aliases=True,
include_openid_discovery=True) returns a list of URL patterns ready to mount
alongside your MCPServer.urls. Exposes RFC 8414 / OIDC discovery /
RFC 7591 Dynamic Client Registration + the alias paths different LLM hosts
probe (aliases render the canonical payload — they are not HTTP redirects).
build_oauth_urlpatterns ¶
build_oauth_urlpatterns(
*,
server: MCPServer,
include_dcr: bool = False,
include_aliases: bool = True,
include_openid_discovery: bool = True,
include_authorize: bool = False,
) -> list[URLPattern]
Return URL patterns for the OAuth endpoint matrix.
Endpoint matrix when all flags are True:
+---------------------------------------------------------+--------------------------------------+
| Path | View |
+=========================================================+======================================+
| /.well-known/oauth-protected-resource | ProtectedResourceMetadataViewSet |
+---------------------------------------------------------+--------------------------------------+
| /.well-known/oauth-protected-resource/mcp | alias |
+---------------------------------------------------------+--------------------------------------+
| /mcp/.well-known/oauth-protected-resource | alias |
+---------------------------------------------------------+--------------------------------------+
| /.well-known/oauth-authorization-server | AuthorizationServerMetadataViewSet |
+---------------------------------------------------------+--------------------------------------+
| /.well-known/oauth-authorization-server/oauth | alias |
+---------------------------------------------------------+--------------------------------------+
| /oauth/.well-known/oauth-authorization-server | alias |
+---------------------------------------------------------+--------------------------------------+
| /.well-known/openid-configuration | OpenIDDiscoveryViewSet |
+---------------------------------------------------------+--------------------------------------+
| /.well-known/openid-configuration/oauth | alias |
+---------------------------------------------------------+--------------------------------------+
| /oauth/register/ | DynamicClientRegistrationViewSet |
+---------------------------------------------------------+--------------------------------------+
The DOT-provided /oauth/authorize/ and /oauth/token/ views
are NOT mounted here — consumers include oauth2_provider.urls
separately. The contrib mount focuses on the discovery / DCR
surface; the AS endpoints themselves belong to whichever framework
actually hosts the AS.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
server
|
MCPServer
|
The :class: |
required |
include_dcr
|
bool
|
Mount |
False
|
include_aliases
|
bool
|
Mount the alias URLs alongside the canonical ones.
Default |
True
|
include_openid_discovery
|
bool
|
Mount the OIDC discovery alias. Default
|
True
|
include_authorize
|
bool
|
Mount |
False
|
AuthorizationServerMetadataViewSet ¶
Bases: ViewSet
RFC 8414 OAuth 2.0 Authorization Server Metadata endpoint.
Mounted by :func:build_oauth_urlpatterns at the canonical
/.well-known/oauth-authorization-server URL plus aliases. The
canonical GET wires up as the list action via
AuthorizationServerMetadataViewSet.as_view({"get": "list"}, auth_backend=...).
Delegates payload construction to
:meth:MCPAuthBackend.authorization_server_metadata, which returns
an :class:AuthorizationServerMetadata dataclass. Backends that
don't host an authorization server raise :class:NotImplementedError
on the method; this view surfaces that as 501 Not Implemented so
clients see a deterministic "no AS here" signal rather than a 500.
DRF's default auth / permission / throttling layers are disabled — discovery is public and the MCP transport owns its own pipeline. The renderer is pinned to JSON; the payload shape is RFC-defined.
OpenIDDiscoveryViewSet ¶
Bases: ViewSet
OIDC discovery alias — /.well-known/openid-configuration.
Some MCP / LLM-host clients probe /.well-known/openid-configuration
before falling back to RFC 8414. This ViewSet returns the same
payload as :class:AuthorizationServerMetadataViewSet plus a small
set of OIDC defaults so the probe succeeds even though
:mod:rest_framework_mcp doesn't implement an actual ID-token
endpoint.
Single-action ViewSet — the canonical GET wires up as list via
OpenIDDiscoveryViewSet.as_view({"get": "list"}, auth_backend=...).
Concretely, the payload is the backend's AS metadata composed with:
subject_types_supported: ["public"]— DOT-style pseudonymous identifiers.id_token_signing_alg_values_supported: ["RS256"]— the most common OIDC algorithm, advertised even though we don't actually mint ID tokens (clients that walk this list and pick one work fine because they only use it for verification — which they never get to do without an ID-token endpoint anyway).response_modes_supported: ["query"]— standard.
Backends that don't host an authorization server raise
:class:NotImplementedError; this view surfaces that as 501 for
parity with :class:AuthorizationServerMetadataViewSet.
DynamicClientRegistrationViewSet ¶
Bases: ViewSet
RFC 7591 Dynamic Client Registration endpoint.
Default state is locked down: DCR_ENABLED=False produces a 403
on every request. To turn DCR on, set the flag in
REST_FRAMEWORK_MCP settings and (recommended) also set
DCR_INITIAL_ACCESS_TOKEN to a static bearer that clients must
present.
Single-action ViewSet — wired as the create action (POST) via
DynamicClientRegistrationViewSet.as_view({"post": "create"}).
Successful POST returns the RFC 7591 client information response
(client_id / client_secret / echoed registration metadata)
and persists a DOT Application.
DOT (oauth2_provider) is imported lazily inside the action so
this module remains importable without the [oauth] extra. A
request that arrives with DCR enabled but DOT absent surfaces a
clear ImportError at first use rather than at server startup.
DRF's default auth / permission / throttling layers are disabled —
DCR is gated by the dedicated DCR_INITIAL_ACCESS_TOKEN setting,
not by DRF's session/token authenticators. The CSRF / session
middleware is sidestepped because DRF's APIView.dispatch (which
ViewSet inherits) wraps responses with csrf_exempt semantics
when no SessionAuthentication class is configured.
DynamicClientRegistrationSerializer ¶
Bases: DataclassSerializer
RFC 7591 dynamic client registration request shape.
Wraps :class:DynamicClientRegistrationRequest so the validated
payload arrives at :class:DynamicClientRegistrationViewSet as a typed
dataclass instance (via .save()). Field overrides below replace
the dataclass-derived auto-generated fields with shapes that
actually validate the wire contract:
redirect_urisis required + non-empty + child is a URL.client_type/authorization_grant_typeareChoiceFieldwith choices sourced from DOT'sApplicationmodel constants at instance construction. Declaring them here lets us reject malformed values with a per-field error before the request ever reaches the database. The lazy import keeps this module importable without the[oauth]extra.
Other RFC 7591 fields are silently ignored — DOT doesn't model them and inventing a richer shape would diverge from the underlying authorization server.
AuthorizationServerMetadata
dataclass
¶
RFC 8414 OAuth 2.0 Authorization Server Metadata payload.
Returned by :meth:MCPAuthBackend.authorization_server_metadata
and serialised by the contrib AS metadata ViewSet. Backends that
don't host an AS raise :class:NotImplementedError instead of
returning this dataclass; the calling ViewSet maps the exception
to 501 Not Implemented.
Field shapes mirror RFC 8414; str-typed endpoints default to
"" so the wire shape is always valid JSON even when the
configuration is incomplete (callers can populate SERVER_INFO
to fill them).
OpenIDDiscoveryPayload
dataclass
¶
OIDC discovery alias payload — extends :class:AuthorizationServerMetadata.
Composes (not subclasses) the AS metadata so the underlying type
stays exactly RFC 8414. The OIDC additions (subject_types_supported,
id_token_signing_alg_values_supported, response_modes_supported)
are advertised because some MCP / LLM-host clients probe
/.well-known/openid-configuration first and skip the probe
silently if these keys are absent.
See :class:OpenIDDiscoveryViewSet for the rationale on returning
OIDC-shaped metadata without implementing an actual ID-token
endpoint.
DynamicClientRegistrationRequest
dataclass
¶
RFC 7591 dynamic client registration request payload.
Mutable dataclass so :class:DynamicClientRegistrationSerializer
(a DataclassSerializer over this type) can apply defaults via
setdefault-style normalisation if needed. Frozen would force
consumers to build a second instance just to change a defaulted
field, which is awkward for a request-shape type.
Whitelists the fields the DCR view actually forwards to DOT's
Application model. Other RFC 7591 fields are silently ignored
by the serializer — DOT doesn't model them.
DynamicClientRegistrationResponse
dataclass
¶
RFC 7591 client information response.
The wire shape returned by :class:DynamicClientRegistrationViewSet
on a successful registration. scope is optional (only emitted
when the request supplied one).
DCR is gated behind two settings:
REST_FRAMEWORK_MCP["DCR_ENABLED"](defaultFalse) — DCR endpoint returns501 Not Implementedwhile disabled.REST_FRAMEWORK_MCP["DCR_INITIAL_ACCESS_TOKEN"](defaultNone) — optional bearer required on the DCR POST, per RFC 7591 §3.
User-adapter hook (cookie-session bridge for /authorize)¶
AuthUserAdapter ¶
Bases: Protocol
Hydrate request.user before DOT's AuthorizationView dispatches.
The common production setup is "DRF backend with SimpleJWT cookies on
the same host". DOT's AuthorizationView only knows about Django's
standard session-based request.user — without an adapter, a
JWT-authenticated user appears anonymous to the OAuth flow and the
consent screen gets shown again. The adapter is the seam where the
consumer's preferred authentication scheme decides which user the
OAuth flow should attribute the grant to.
Implementations return:
- The authenticated :class:
AbstractBaseUserto set on the request before delegating to DOT. Noneto leaverequest.useruntouched — DOT then falls back to its own session-based flow (which may redirect to login).
Adapters MUST be safe to instantiate without arguments — settings-driven configuration belongs inside the adapter's own module so the dotted-path setting can resolve it directly.
SimpleJWTCookieAdapter ¶
Reference :class:AuthUserAdapter for SimpleJWT cookie-authenticated apps.
Reads the access-token cookie (name configured via
REST_FRAMEWORK_MCP['SIMPLEJWT_ACCESS_COOKIE'], default
"access"), decodes it with
:class:rest_framework_simplejwt.tokens.AccessToken, looks the user
up by primary key, and returns it. Returns None for any failure
mode (no cookie, malformed token, expired token, unknown user) —
DOT's view then falls back to its session-based flow.
rest_framework_simplejwt is imported lazily inside :meth:hydrate
so this module remains importable without the [jwt] extra. A
consumer who configures this adapter without the extra installed
surfaces a clear ImportError at first request, not at import.