Turns Django/Wagtail code into MCP tools (and resources) for Claude clients
Project description
django-mcp-kit
Turns Django/Wagtail code into MCP tools (and resources) for Claude clients — without coupling your business logic to any MCP framework.
- Transport-neutral core. A registry + dispatcher own
initialize/tools.list/tools.call; the official MCP SDK is used only at the wire (transports/sdk.py) and is swappable. No FastMCP. - Native Django authz. DRF-style
permission_classeschecked beforerun(); object permissions (Wagtailpermissions_for_user) stay in your service layer. - Pluggable auth. Static bearer tokens and OAuth 2.1 resource server
(django-oauth-toolkit) behind one
Authenticatorinterface, with RFC 9728 discovery metadata and the401 + WWW-Authenticatehandshake. - On-ramps. Register a DRF
ViewSet, expose a model (read-only by default), or declare MCP resources.
Install
pip install django-mcp-kit # core: MCP SDK wire transport, OAuth, DRF on-ramp, singleserver
pip install "django-mcp-kit[wagtail]" # + the optional Wagtail admin settings page
Batteries included. The MCP SDK +
uvicorn(the only wire transport today), django-oauth-toolkit (OAuth resource server), DRF (the ViewSet/model on-ramp), andsingleserverare all core dependencies — their imports stay lazy/contained, so the architectural boundary holds in code even though the packages ship by default. The one optional extra is[wagtail], for the Wagtail admin settings page (Django-only projects don't need Wagtail).
Quickstart
# settings.py
INSTALLED_APPS += ["django_mcp_kit"]
DJANGO_MCP_KIT = {
"SERVER_NAME": "my-content",
"AUTH_BACKENDS": [
"django_mcp_kit.auth.OAuthResourceServer",
"django_mcp_kit.auth.StaticBearer",
],
"STATIC_BEARER_RESOLVER": "myapp.models:UserProfile.user_for_token",
"OAUTH_ISSUER_URL": "https://example.com",
"RESOURCE_SERVER_URL": "https://example.com/mcp",
"REQUIRED_SCOPES": ["mcp"],
}
# urls.py — discovery + health for the Django site
path("", include("django_mcp_kit.urls")), # /healthz, /.well-known/oauth-protected-resource
# myapp/mcp_tools.py — autodiscovered on app load
from django_mcp_kit import Tool, Schema, tool
from . import services
class PatchBlock(Tool):
name = "patch_block"
description = "Replace one homepage block by id; saves a DRAFT."
class Input(Schema):
block_id: str
value: dict
def run(self, user, block_id, value): # sync — runs off the event loop
return services.save_homepage_draft(user, block_id=block_id, value=value)
@tool(name="get_draft", description="Latest homepage draft.")
def get_draft(user) -> dict:
return services.homepage_draft()
# Register a DRF ViewSet (one tool per action) or a model (read-only by default)
from django_mcp_kit.drf import register_drf_viewset
from django_mcp_kit.models import ModelToolset
register_drf_viewset(OrderViewSet, prefix="order") # order_list, order_create, …
class ProductToolset(ModelToolset):
model = Product
actions = ["list", "retrieve"] # add "create"/"update"/"delete" to opt in
as_resource = True
Run it
python manage.py runserver_mcp --port 8810 # ASGI/uvicorn MCP process
Deployment
The MCP endpoint is always ASGI (Streamable HTTP uses SSE), but how it runs is a
choice. All topologies run the same code — runserver_mcp serving the app from
django_mcp_kit.asgi:get_application — only the process management differs. Pick by how
your site is served and how much isolation you want:
| Topology | Site stays WSGI | Process | Best for |
|---|---|---|---|
| A — Co-located | no (site → ASGI) | one ASGI process | single-service / already-ASGI sites |
| B — singleserver aux | yes | gunicorn boots the MCP process, workers share it | dev == prod, no systemd |
| C — separate systemd unit | yes | independent ASGI daemon | production SSE (recommended) |
Health checks point at /healthz (a plain 200) — never /mcp, which is the
Streamable-HTTP endpoint and returns 4xx to a bare GET.
A — Co-located (one ASGI process)
Mount the MCP app beside your Django ASGI app; requests to /mcp (and /healthz,
/.well-known/...) go to MCP, everything else to Django:
# asgi.py
from django.core.asgi import get_asgi_application
from django_mcp_kit.asgi import mount
application = mount(get_asgi_application()) # serves /mcp on the same process
B — singleserver-managed aux (deploy/gunicorn.conf.py)
Your site stays WSGI/gunicorn. The first gunicorn worker boots the MCP process; all
workers share it via an atomic socket lock (no systemd). Wire it in post_fork:
# gunicorn.conf.py
def post_fork(server, worker):
from django_mcp_kit.services import connect
connect()
Point your front-end proxy /mcp at DJANGO_MCP_KIT["PORT"] (default 8810). The
shipped SingleServer uses health_check_url="/healthz" and a bounded graceful shutdown.
C — Separate systemd unit (deploy/django-mcp.service)
Run the MCP server as its own daemon — decoupled lifecycle, real graceful restart. The
sample unit runs runserver_mcp with a bounded --timeout-graceful-shutdown (so
long-lived SSE streams don't stall stop/restart) and Restart=always. Edit the paths/user,
then:
sudo cp deploy/django-mcp.service /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable --now django-mcp
nginx (deploy/nginx-mcp.conf)
For B and C, proxy /mcp to the MCP process. SSE requires buffering off and long read
timeouts — the sample sets proxy_buffering off and proxy_read_timeout 3600s, and also
proxies the /.well-known/oauth-protected-resource metadata. Add it inside your server {}
block and reload nginx.
Authorization Server (OAuth) setup
This library is the Resource Server — it validates bearer tokens and serves the
RFC 9728 discovery metadata. It does not provide the Authorization Server (the
login, /o/authorize, /o/token, and the consent page). That role is
django-oauth-toolkit (DOT), which ships as a dependency but must be wired up by the
project:
# settings.py
INSTALLED_APPS += ["oauth2_provider"]
OAUTH2_PROVIDER = {
"SCOPES": {"mcp": "Access MCP tools"},
"PKCE_REQUIRED": True, # public clients (browser/native, e.g. claude.ai)
}
# urls.py — mount the Authorization Server endpoints
path("o/", include("oauth2_provider.urls", namespace="oauth2_provider")),
Provisioning the OAuth client
Register an OAuth client (a DOT Application) per connector with the bundled command —
idempotent, public + PKCE by default:
python manage.py create_mcp_oauth_client https://claude.ai/api/mcp/auth_callback \
--name "My connector" # name shown on the consent page; --skip-consent to auto-approve
The client name defaults to DJANGO_MCP_KIT["OAUTH_APP_NAME"] ("MCP connector") when
--name is omitted. The command prints the Client ID to paste into the connector.
django_mcp_kit.oauth_client.ensure_oauth_application(...) is the same helper if you'd
rather provision from code.
The consent page
The consent screen is rendered by DOT's AuthorizationView at /o/authorize/ using
its default template oauth2_provider/authorize.html (a plain approve/deny form).
Whether it appears is controlled per-client by Application.skip_authorization:
skip_authorization=False(DOT default) — the user is shown the consent page on first authorization.skip_authorization=True— consent is auto-approved (no page). Reasonable for a trusted first-party connector.
The name shown on that consent page is the Application.name — set it per client with
create_mcp_oauth_client --name, or change the default via DJANGO_MCP_KIT["OAUTH_APP_NAME"].
To customise the consent UI, override oauth2_provider/authorize.html in your own
templates directory. This library has no opinion on and no default for the consent page —
it only consumes the access token DOT issues.
Configuring it from the Wagtail admin (optional)
For Wagtail projects, add the optional app to get a "MCP connector" page under the admin Settings menu that provisions/updates the client on save:
pip install "django-mcp-kit[wagtail]" # Wagtail 6.x or 7.x
INSTALLED_APPS += ["django_mcp_kit.wagtail_connector"]
# then: python manage.py migrate
Fields: enable, the consent-page name, redirect URIs, and skip-consent. Access is
gated by the change_mcpconnectorsettings permission — i.e. superusers only by default;
delegate to specific staff by granting that permission via a Group. Wagtail is not a
core dependency — it's the optional [wagtail] extra, so Django-only projects skip it.
Develop
poetry install
poetry run pytest
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_mcp_kit-0.1.0.tar.gz.
File metadata
- Download URL: django_mcp_kit-0.1.0.tar.gz
- Upload date:
- Size: 29.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/2.1.4 CPython/3.13.7 Darwin/25.3.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ba851279edd80930fff1c9502558cf02b2a134753b4c467369c78c1afadea59f
|
|
| MD5 |
a1724e98e4bf95709093f32dde3dfa65
|
|
| BLAKE2b-256 |
7238f1bf78409e4bbb5528db478286d36263412deea4fd89009d5fc04af2d36b
|
File details
Details for the file django_mcp_kit-0.1.0-py3-none-any.whl.
File metadata
- Download URL: django_mcp_kit-0.1.0-py3-none-any.whl
- Upload date:
- Size: 38.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/2.1.4 CPython/3.13.7 Darwin/25.3.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
21a8e6475875e2eebf8bf480c81bcc71c49d0242287fbe6d2f795e8f50b0b2e2
|
|
| MD5 |
dd97aec74900055a3b6e01e6ee256833
|
|
| BLAKE2b-256 |
7018d079191015ed947c22521702849998ae28b3227e45defe95d8bb828dbba1
|