Skip to main content

django-mcp-oauth

An OAuth 2.1 authorization server for MCP resource servers, as a Django app.

Built on Authlib's Django integration plus PyJWT/cryptography, with the MCP profile wired on top:

  • RFC 9728 protected-resource metadata, RFC 8414 authorization-server metadata, RFC 7517 JWKS — all under /.well-known/
  • RFC 7591 dynamic client registration, with the public-client rule (a token_endpoint_auth_method: none client is never issued a secret, and the response omits the field as the RFC requires)
  • Authorization-code grant with PKCE (S256) required, RS256 JWT access tokens bound to the resource audience
  • Refresh-token rotation with a configurable grace window, so concurrent agents sharing a connection do not strand each other with a dead credential
  • Client-credentials grant for service accounts, each bound to a Django user
  • RFC 6750 / 9728 WWW-Authenticate challenges for the resource server
  • Client secrets stored hashed

The one product decision — may this signed-in user authorize an MCP client? — is a dotted-path setting. Everything else is configuration.

Install

pip install django-mcp-oauth          # + [drf] for the REST Framework class
python manage.py generate_oauth_keys   # prints a private key PEM to set as MCP_OAUTH_PRIVATE_KEY
INSTALLED_APPS += ["django_mcp_oauth"]

MCP_OAUTH_ISSUER = "https://example.com"          # the AS origin (iss)
MCP_RESOURCE_URL = "https://example.com/mcp"      # the MCP server (aud)
MCP_OAUTH_PRIVATE_KEY = os.environ["MCP_OAUTH_PRIVATE_KEY"]
MCP_OAUTH_AUTHORIZE_USER = "myapp.access.may_use_mcp"   # callable(user) -> bool
MCP_OAUTH_CONSENT_TEMPLATE = "myapp/consent.html"       # optional; receives grant, client, scope
# urls.py — both lists at the site root
from django_mcp_oauth.urls import oauth_urlpatterns, wellknown_urlpatterns
urlpatterns += wellknown_urlpatterns + oauth_urlpatterns

Optional settings and their defaults are listed in django_mcp_oauth/conf.py.

Resource server side

from django_mcp_oauth.resource import user_from_bearer, www_authenticate
from django_mcp_oauth.validation import TokenError

try:
    user, validated = user_from_bearer(token)      # raises TokenError
except TokenError as exc:
    return JSONResponse({"error": exc.error}, status_code=401,
                        headers={"WWW-Authenticate": www_authenticate(exc.error, exc.description)})

www_authenticate(include_error=False) is the form for a request that carried no credentials at all (RFC 6750 §3.1). For Django REST Framework, add django_mcp_oauth.drf.OAuth2ResourceServerAuthentication to DEFAULT_AUTHENTICATION_CLASSES.

Service accounts

python manage.py create_service_client --user svc-bot --name "Nightly sync" --scope mcp

prints a client_id / client_secret pair once; the client authenticates to /oauth/token with grant_type=client_credentials and gets a token whose subject is the bound user.

Adopting from an in-tree copy

If your product already carries these models under its own app (tables <app>_oauth2client, <app>_oauth2token, <app>_oauth2authorizationcode), rename the tables and drop the models from that app's migration state; this app's initial migration finds the tables present and records state only:

class Migration(migrations.Migration):
    dependencies = [("myapp", "000N_previous")]
    run_before = [("mcp_oauth", "0001_initial")]
    operations = [
        migrations.SeparateDatabaseAndState(
            database_operations=[
                migrations.RunSQL(
                    "ALTER TABLE myapp_oauth2client RENAME TO mcp_oauth_oauth2client",
                    reverse_sql="ALTER TABLE mcp_oauth_oauth2client RENAME TO myapp_oauth2client",
                ),
                # ... oauth2token, oauth2authorizationcode likewise
            ],
            state_operations=[
                migrations.DeleteModel("OAuth2Client"),
                migrations.DeleteModel("OAuth2Token"),
                migrations.DeleteModel("OAuth2AuthorizationCode"),
            ],
        )
    ]

Keep re-exporting _now from your old models module while historical migrations reference it. Unapplying this app's initial migration never drops the tables (so unapplying the adoption migration afterwards renames your data back); on a fresh install, drop them by hand if you really mean to.

Tests

pip install -e '.[test]'
pytest

License

Apache-2.0.

Metadata

Release files for django-mcp-oauth 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for django-mcp-oauth 0.1.0
File Size Uploaded
django_mcp_oauth-0.1.0.tar.gz 37.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for django-mcp-oauth 0.1.0
File Interpreter ABI Platform
django_mcp_oauth-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 65.8 kB

Release files / django_mcp_oauth-0.1.0.tar.gz

Download URL django_mcp_oauth-0.1.0.tar.gz
Size 37.2 kB
Tags Source
SHA-256 checksum
How to use checksums
7ba658b3e1eb52320f0d3fca0dc846c2e80d69b1b238debb11eebe2f2c6c0c4d
BLAKE2b-256 checksum
How to use checksums
7954c339eaa196491373db9383c05fdd085105ca12f920c8afe5b6c73853485c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.

Transparency log

Release files / django_mcp_oauth-0.1.0-py3-none-any.whl

Download URL django_mcp_oauth-0.1.0-py3-none-any.whl
Size 28.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ec20529419717f7602d6eb5057011a255320c4c0a244449bf76d82fd1a8aa799
BLAKE2b-256 checksum
How to use checksums
4ea2989a4d500709aa9fa5092f97a9a0c458be1389073ae019bfcf884673d431
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page