Skip to main content

django-ninja-mcp-server

Let MCP clients such as Claude Code and Codex call your Django project.

You pick which Django Ninja operations and Python functions to expose. Each one becomes an MCP tool that runs as a real Django user, through the same auth, validation and permission code as your HTTP API.

  • Local (stdio): manage.py mcp --user alice. For a developer's own machine, with no tokens.
  • Remote (Streamable HTTP): OAuth 2.1, with django-oauth-toolkit as the authorization server. Tokens must be issued for your MCP URL (RFC 8707), and each tool can require scopes.

Built on the official MCP Python SDK 2.x and Django Ninja 1.7+. Supports Python 3.10+ and Django 4.2+.

Install

pip install django-ninja-mcp-server            # stdio only
pip install "django-ninja-mcp-server[oauth]"   # adds django-oauth-toolkit for HTTP

Add "ninja_mcp" to INSTALLED_APPS (for the mcp management command).

Expose tools

# myproject/mcp.py
from ninja_mcp import NinjaMCP, get_identity

from orders.api import api
from orders.models import Order

mcp = NinjaMCP("Shop", instructions="Tools for the shop's order data.")

# Django Ninja operations, by view name or operation ID.
mcp.add_operations(api, ["list_orders", "get_order"], scopes=["orders:read"])
mcp.add_operations(api, ["refund_order"], scopes=["orders:write"])


# Plain functions. Sync functions run in a worker thread, so the ORM works.
@mcp.tool(scopes=["orders:read"])
def count_open_orders() -> int:
    """Count the caller's open orders."""
    return Order.objects.filter(owner=get_identity().user, status="open").count()
# settings.py
NINJA_MCP_SERVER = "myproject.mcp.mcp"

An operation must allow MCP calls in its auth:

from ninja.security import django_auth
from ninja_mcp import MCPAuth


@api.get("/orders", response=list[OrderOut], auth=[django_auth, MCPAuth()])
def list_orders(request, status: str | None = None):
    return Order.objects.filter(owner=request.user)

MCPAuth matches only requests this package builds for a tool call; a real HTTP request never passes it. During a tool call request.user is the caller and request.auth is their MCPIdentity (user, client_id, scopes).

What becomes a tool

  • The tool name is the name you passed: a view function name, or an operation ID when view names clash. To choose other names, pass a mapping: mcp.add_operations(api, {"orders": "list_orders"}).
  • Path and query parameters become top-level arguments. A JSON body becomes the body argument. Schemas come from Django Ninja's OpenAPI output.
  • GET operations are marked read-only, DELETE destructive, PUT idempotent.
  • A 4xx response becomes a tool error that carries the response body, so the model can read validation errors and correct itself. A 5xx response, an exception, a redirect or a binary body becomes a tool error with no internal detail; 5xx bodies go to the ninja_mcp.operations logger instead.
  • Path parameters must match their URL converter (int, slug, uuid, and str, which refuses /), as Django's resolver would require.
  • Unknown arguments are refused, and every tool schema says additionalProperties: false.

Some operations can't be exposed: those with header, cookie, form or file parameters, streaming responses, several HTTP methods, or auth without MCPAuth. add_operations raises ImproperlyConfigured for them at startup, not on the first call.

Tool calls go to the operation directly, not through Django's URL resolver or middleware. Do anything your views depend on (such as setting a tenant) in the view or in an auth class. The request's scheme and host are those of resource_url, or http://localhost over stdio, so build_absolute_uri() works when that host is in ALLOWED_HOSTS.

Scopes

A tool's scopes must all be in the caller's token. A caller without them does not see the tool in tools/list, and calling it fails as if it didn't exist. A tool with no scopes needs only a valid caller.

Local use: Claude Code and Codex over stdio

python manage.py mcp --user alice                      # all tools
python manage.py mcp --user alice --scope orders:read  # only read tools

The command speaks MCP on stdin/stdout, and every call runs as alice. Anyone who can run manage.py can already act as any user, so stdio takes no token.

Claude Code:

claude mcp add shop -- uv run python manage.py mcp --user alice

Codex:

codex mcp add shop -- uv run python manage.py mcp --user alice

Both clients start the command from their working directory. Run them from your project root, or use absolute paths.

Remote use: Streamable HTTP with OAuth

Give the server its public URL and the OAuth issuer:

mcp = NinjaMCP(
    "Shop",
    resource_url="https://shop.example.com/mcp",
    issuer_url="https://shop.example.com",
)

Serve it from your ASGI entry point, next to Django:

# myproject/asgi.py
from django.core.asgi import get_asgi_application

django_app = get_asgi_application()

from myproject.mcp import mcp  # noqa: E402
from ninja_mcp.asgi import MCPApplication  # noqa: E402

application = MCPApplication(mcp, django_app)

MCPApplication serves:

  • POST /mcp: the MCP endpoint (stateless Streamable HTTP, JSON responses).
  • GET /.well-known/oauth-protected-resource/mcp: RFC 9728 metadata naming your issuer and every scope your tools use.
  • Everything else goes to Django.

The ASGI server must support the lifespan protocol, as uvicorn and hypercorn do; without it the MCP endpoint answers 500. A WSGI site can run the same asgi.py under uvicorn as a second process and send /mcp and /.well-known/oauth-protected-resource/ to it. Requests whose Host is not the host of resource_url are refused (DNS rebinding protection). If your proxy changes Host, pass allowed_hosts=[...].

Token checks

DjangoOAuthTokenVerifier accepts a django-oauth-toolkit access token only if all of these hold:

  • it is unexpired;
  • its user is active;
  • its resource includes resource_url. A token with no resource is refused; django-oauth-toolkit would accept it anywhere.

To check your own grants on every request (for example, a per-user allowlist that admins can revoke), pass authorize:

from ninja_mcp.oauth import DjangoOAuthTokenVerifier


def authorize(token):
    grant = Grant.objects.filter(
        user=token.user, application=token.application, active=True
    ).first()
    return grant.scopes if grant else None  # None refuses the token


mcp = NinjaMCP(
    "Shop",
    resource_url="https://shop.example.com/mcp",
    issuer_url="https://shop.example.com",
    token_verifier=DjangoOAuthTokenVerifier(
        "https://shop.example.com/mcp", authorize=authorize
    ),
)

Authorization server settings

django-oauth-toolkit 3.4 has what MCP clients need. These settings let Claude Code and Codex log in with no manual client setup:

OAUTH2_PROVIDER = {
    "SCOPES": {"orders:read": "Read orders", "orders:write": "Change orders"},
    "DEFAULT_SCOPES": ["orders:read"],  # granted when a client asks for none
    "OIDC_ISS_ENDPOINT": "https://shop.example.com",  # the issuer: your origin
    "PKCE_REQUIRED": True,
    "COMPLIANT_BCP_RFC9700_PKCE_METHOD": True,  # S256 only
    "COMPLIANT_BCP_RFC9700_AUTHZ_RESPONSE_ISS": True,  # RFC 9207 `iss`
    "COMPLIANT_BCP_RFC9700_IMPLICIT_GRANT": True,
    "COMPLIANT_BCP_RFC9700_PASSWORD_GRANT": True,
    "ROTATE_REFRESH_TOKEN": True,
    "REFRESH_TOKEN_REUSE_PROTECTION": True,
    "ACCESS_TOKEN_EXPIRE_SECONDS": 3600,
    # CLI clients receive the code on http://localhost:<any port>/callback.
    "ALLOWED_REDIRECT_URI_SCHEMES": ["http", "https"],
    "ALLOW_LOCALHOST_LOOPBACK": True,
    "OAUTH2_RESPONSE_TYPES_SUPPORTED": ["code"],
    "OAUTH2_GRANT_TYPES_SUPPORTED": ["authorization_code", "refresh_token"],
    "OAUTH2_TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED": [
        "none",  # public clients; Codex needs it listed to use CIMD
        "client_secret_post",
        "client_secret_basic",
    ],
    # Client ID Metadata Documents: the client ID is a URL the client's
    # vendor publishes. Allow only the vendors you trust.
    "CIMD_ENABLED": True,
    "CIMD_REGISTRATION_PERMISSION_CLASSES": (
        "oauth2_provider.cimd.HostAllowlistCIMDPermission",
    ),
    "CIMD_ALLOWED_HOSTS": ["claude.ai", "chatgpt.com"],
}
# urls.py
from oauth2_provider.urls import metadata_urlpatterns

urlpatterns = [
    # RFC 8414 metadata at the root, where every client looks.
    path("", include((metadata_urlpatterns, "oauth2_metadata"))),
    path("o/", include("oauth2_provider.urls", namespace="oauth2_provider")),
    ...,
]

Then pass the same origin as the issuer: NinjaMCP(..., issuer_url="https://shop.example.com").

Why each setting matters:

  • Issuer at the origin. Claude Code documents only the root /.well-known/oauth-authorization-server path, and clients compare the issuer with the URL exactly.
  • RFC 9207 iss. Without it, Codex adds a /callback/<id> path to its redirect URI, which a pre-registered client would not match.
  • Loopback redirects. django-oauth-toolkit accepts any port on a port-less loopback redirect URI (RFC 8252); ALLOW_LOCALHOST_LOOPBACK extends that to localhost, which Claude Code uses.
  • CIMD. Claude Code and Codex identify themselves by a metadata URL (https://claude.ai/oauth/claude-code-client-metadata, https://chatgpt.com/oauth/codex/client.json). Your server fetches it, so it needs outbound HTTPS to those hosts.

tests/settings.py uses exactly these settings, and tests/test_oauth_flow.py runs the authorization code flow both clients use against them: S256 PKCE, a loopback redirect on an arbitrary port, and resource. The tests do not fetch the vendors' CIMD documents (that needs the network).

Connect the clients:

claude mcp add --transport http shop https://shop.example.com/mcp
codex mcp add shop --url https://shop.example.com/mcp && codex mcp login shop

Without CIMD, register a public client in the admin with the redirect URIs http://localhost/callback http://127.0.0.1/callback, then pass its ID: claude mcp add ... --client-id <id> or codex mcp add ... --oauth-client-id <id>. django-oauth-toolkit also supports dynamic client registration (DCR_ENABLED), but the MCP specification now deprecates it in favour of CIMD, and open registration lets anyone create clients.

Clients choose which scopes to request. Codex asks for the ones in the 401 challenge, which lists every scope your tools use. Claude Code does not ask for any unless you pin them with "oauth": {"scopes": "orders:read"} in its MCP config. A client that asks for none gets DEFAULT_SCOPES.

Development

uv sync
uv run pytest
uv run ruff check . && uv run ruff format --check .

License

BSD 3-Clause, the same license as Django. See LICENSE.

Metadata

Release files for django-ninja-mcp-server 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-ninja-mcp-server 0.1.0
File Size Uploaded Provenance Actions
django_ninja_mcp_server-0.1.0.tar.gz 26.4 kB With provenance Details for django_ninja_mcp_server-0.1.0.tar.gz

Built distribution (wheel)

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

Total release size: 48.2 kB

Release files / django_ninja_mcp_server-0.1.0.tar.gz

Download URL django_ninja_mcp_server-0.1.0.tar.gz
Size 26.4 kB
Tags Source
SHA-256 checksum
How to use checksums
cd041a59010afc1828c9f562aab8e6c8a7a2e4d7fcb43c03687fe3c5891a9a88
BLAKE2b-256 checksum
How to use checksums
be4297f8bf9888a9046fc5e6cd93fe2ce89898085272259c9f2e3d21bd19d76e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 Nov 5, 2026.

Transparency log

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

Download URL django_ninja_mcp_server-0.1.0-py3-none-any.whl
Size 21.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a337d24d240f8f57ab30d186574d273fe0ad6c07e8cf4b4d783c9108273df625
BLAKE2b-256 checksum
How to use checksums
611b1892acf52378e0405fbc7b693457c5940a31a87921e4b45c740d48d80e2e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 Nov 5, 2026.

Transparency log

Security for django-ninja-mcp-server 0.1.0

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.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for django_ninja_mcp_server-0.1.0.tar.gz

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 Nov 5, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files Full provenance

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