MCP (Model Context Protocol) adapter for django-admin-rest-api. A wire-protocol-only layer that lets agents reach the existing REST API — no new functionality, permissions, or validation.
Project description
django-admin-mcp-api
An MCP server for the Django admin — same permissions, same
ModelAdmin, no new features.
django-admin-mcp-api lets AI agents — Claude, Cursor, anything that
speaks the Model Context Protocol —
drive your Django admin. Every ModelAdmin you've already registered
on django.contrib.admin.site becomes an MCP tool, with the same
permissions, the same form validation, and the same session
auth as the HTML admin.
It is the MCP face on top of
django-admin-rest-api.
No parallel permission system. No parallel form layer. No features the
Django admin doesn't already have.
| Project | Role | PyPI |
|---|---|---|
🟦 django-admin-react |
React single-page admin frontend | django-admin-react |
🟩 django-admin-rest-api |
JSON REST API over ModelAdmin |
django-admin-rest-api |
🟪 django-admin-mcp-api (this repo) |
MCP server exposing the same API to LLMs | django-admin-mcp-api |
✨ The one design principle
This package adds no new behavior. It is an MCP wire adapter.
Every one of these is owned by your existing Django setup — not by this library:
- 🔐 Authentication — Django's session + login. The MCP endpoint
enforces the same
is_active+is_staff+AdminSite.has_permissiongate the HTML admin uses. No tokens, no custom backends, no JWTs. - 🛡️ Authorization — every tool delegates to the matching
ModelAdmin.has_view_permission/has_add_permission/has_change_permission/has_delete_permissionvia django-admin-rest-api. If your admin says no, the tool returns the upstream 403. - 📋 Field validation —
admin.create/admin.updateroute the payload through the sameModelFormDjango would render in the HTML admin, plus a JSON Schema check on the wire so malformed calls fail fast with a json-pointer path of the offending field. - ⚙️ Actions —
admin.actionruns the same action callables registered onModelAdmin.actions. Your code runs unmodified. Each action's descriptor carries atarget(batchordetail), derived by rest-api from the callable's signature: signatures ending inquerysetare batch (changelist shape), signatures ending inobj_id/pk/idare detail (single-object shape). Agents pass the right number of pks for the action's target. - 🔎 Search & filters —
admin.listusesModelAdmin.get_search_resultsandlist_filter. No parallel implementation. - 📜 Audit log — writes go through Django's
LogEntry, surfaced byadmin.historyandadmin.recent_actions. - 🌐 CSRF & sessions — Django's middleware. Nothing is
@csrf_exempt.
If a behavior isn't in the HTML admin, it isn't here. If it is in the HTML admin, this library exposes it over MCP.
🚀 Plug-and-play install
pip install django-admin-mcp-api
Two changes to your project:
# settings.py
INSTALLED_APPS = [
# ... your existing apps ...
"django.contrib.admin",
"django_admin_rest_api", # ← the REST surface (mandatory)
"django_admin_mcp_api", # ← the MCP adapter
]
# urls.py
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("django_admin_rest_api.urls")), # REST
path("mcp/", include("django_admin_mcp_api.urls")), # MCP
]
That's it. Your admin now answers JSON-RPC at POST /mcp/, with the
same session cookie and CSRF token your HTML admin already uses.
Why two apps?
The MCP layer is a thin wire adapter — it has no admin logic of its
own and forwards every call to
django-admin-rest-api,
which is where the actual permission checks, querysets, forms, and
serialization live. That separation lets the REST API ship and
release on its own cadence, lets the SPA frontend
(django-admin-react)
share it, and keeps the MCP package small enough to audit in an
afternoon.
If you'd rather have one URL include() instead of two:
# urls.py — one-include alternative; rest-api auto-mounted under the same prefix
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("django_admin_mcp_api.bundle_urls")),
]
django_admin_mcp_api.bundle_urls mounts both apps under the
consumer's chosen prefix (rest-api at <prefix>/api/v1/..., MCP at
<prefix>/mcp/). You still need both apps in INSTALLED_APPS —
that's a Django app-registration concern that can't be hidden inside
a URL conf, and manage.py check will fail with E001 if you miss
it.
📡 The 18 tools
Each MCP tool is a 1:1 mirror of a django-admin-rest-api endpoint —
that's the whole design.
| MCP tool | What it does | rest-api endpoint |
|---|---|---|
admin.registry |
List every model the user can see | GET /api/v1/registry/ |
admin.schema |
Full admin metadata schema | GET /api/v1/schema/ |
admin.recent_actions |
The user's own LogEntry feed |
GET /api/v1/recent-actions/ |
admin.list |
A page of list-view results | GET /api/v1/<app>/<model>/ |
admin.retrieve |
A single object's detail view | GET /api/v1/<app>/<model>/<pk>/ |
admin.add_form |
Create-page field descriptors | GET /api/v1/<app>/<model>/add/ |
admin.form_spec |
ModelAdmin-resolved form (request-aware get_form + closed widget.kind enum) |
GET /api/v1/<app>/<model>/<pk>/form-spec/ (or /add/form-spec/) |
admin.create |
Create one object | POST /api/v1/<app>/<model>/ |
admin.update |
Partial-update one object | PATCH /api/v1/<app>/<model>/<pk>/ |
admin.form_submit |
Submit a form-spec's data (re-runs is_valid() with the same request-aware form) |
POST /api/v1/<app>/<model>/ or PATCH …/<pk>/ |
admin.destroy |
Delete one object | DELETE /api/v1/<app>/<model>/<pk>/ |
admin.bulk_update |
Apply the same patch to many objects | PATCH /api/v1/<app>/<model>/bulk/ |
admin.autocomplete |
Autocomplete a related model | GET /api/v1/<app>/<model>/autocomplete/ |
admin.action |
Run a ModelAdmin.actions action (batch or detail) |
POST /api/v1/<app>/<model>/actions/<name>/ |
admin.history |
One object's LogEntry timeline |
GET /api/v1/<app>/<model>/<pk>/history/ |
admin.delete_preview |
Cascade preview before a destroy | GET /api/v1/<app>/<model>/<pk>/delete-preview/ |
admin.set_password |
Set/change a user-like password | POST /api/v1/<app>/<model>/<pk>/password/ |
admin.panel |
A custom panel registered on the ModelAdmin |
GET /api/v1/<app>/<model>/<pk>/panel/<name>/ |
Two endpoints expose them — both gated by the same auth your admin already has:
POST /mcp/— the MCP JSON-RPC 2.0 entry point. Speaksinitialize,tools/list,tools/call. Full wire spec indocs/api-contract.md.GET /mcp/manifest/— a read-only catalogue (server info + every tool's name, description, JSON Schema) for humans and dashboards.
📸 See it run
Captured against the examples/quickstart/
demo — fresh pip install, runserver, python smoke.py. No mocks.
// POST /mcp/ method=initialize
{
"jsonrpc": "2.0", "id": 1,
"result": {
"protocolVersion": "2024-11-05",
"serverInfo": { "name": "django-admin", "version": "1.0.3" },
"capabilities": { "tools": { "listChanged": false } }
}
}
// POST /mcp/ method=tools/call name=admin.registry
{
"jsonrpc": "2.0", "id": 1,
"result": {
"content": [{ "type": "json", "json": {
"user": { "id": 1, "username": "admin", "is_staff": true },
"apps": [
{ "app_label": "auth",
"models": [
{ "model_name": "group",
"permissions": { "view": true, "add": true, "change": true, "delete": true } },
{ "model_name": "user",
"permissions": { "view": true, "add": true, "change": true, "delete": true } }
] }
]
} }],
"isError": false,
"status": 200
}
}
The permissions block above comes straight from
ModelAdmin.has_*_permission — the MCP layer doesn't decide a thing
about authorization. That's the prime directive.
🤖 How an agent uses admin.action
Each registered action on a ModelAdmin has one of two shapes — and
the agent picks the call form by reading the descriptor:
- Discover. Call
admin.registryoradmin.list. Each action in the response carries atargetfield:{ "name": "deactivate", "label": "Deactivate selected users", "target": "batch" // or "detail" } - Branch on
target.target = "batch"→ the action's third parameter is a queryset. Calladmin.actionwith one or more pks.target = "detail"→ the action's third parameter is a single object id. Calladmin.actionwith exactly one pk.
rest-api inspects the signature at registry time, so you don't need
to declare the shape — the same ModelAdmin.actions = [...] you
already use works. Passing the wrong number of pks for the target
returns rest-api's 400 with an explicit "expected 1, got N" message.
// batch action
{
"jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": {
"name": "admin.action",
"arguments": {
"app_label": "auth",
"model_name": "user",
"action_name": "deactivate",
"pks": ["7", "12", "33"]
}
}
}
// detail action — exactly one pk
{
"jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": {
"name": "admin.action",
"arguments": {
"app_label": "auth",
"model_name": "user",
"action_name": "send_password_reset",
"pks": ["7"]
}
}
}
See docs/tools-reference.md for the full
schema, and docs/api-contract.md for the
wire-level error codes.
⚙️ Configuration
All settings live under a single optional dict — defaults are sane, so most projects need no entry at all.
# settings.py (all keys optional)
DJANGO_ADMIN_MCP_API = {
# MCP protocol version advertised in the `initialize` result.
"PROTOCOL_VERSION": "2024-11-05",
# The `serverInfo.name` field. Useful per-environment labelling.
"SERVER_NAME": "django-admin",
# The `serverInfo.version`. None → falls back to the package version.
"SERVER_VERSION": None,
# Dotted path to the AdminSite the package introspects. Override
# for custom AdminSite subclasses — see "Custom AdminSite" below.
"ADMIN_SITE": "django.contrib.admin.site",
# Maximum POST body size for /mcp/, in bytes. Default 256 KiB —
# well above any realistic JSON-RPC envelope and well below
# Django's project-wide DATA_UPLOAD_MAX_MEMORY_SIZE (2.5 MiB)
# which targets form uploads.
"MAX_REQUEST_BYTES": 256 * 1024,
# Tools to suppress from the catalogue and refuse in tools/call.
# Read-only deployments typically set
# ("admin.destroy", "admin.bulk_update", "admin.set_password").
"DISABLED_TOOLS": (),
# Dotted path to a zero-arg callable returning a Dispatcher.
# None uses the built-in RestApiDispatcher.
"DISPATCHER_FACTORY": None,
}
A copy-paste-ready block lives at the bottom of
examples/quickstart/myproject/settings.py.
Custom AdminSite
If your project subclasses Django's AdminSite (multi-tenant flavours,
a staff-only admin alongside a partner admin, etc.), point the package
at the right instance via the ADMIN_SITE setting:
# myproject/admin_sites.py
from django.contrib.admin import AdminSite
class StaffAdminSite(AdminSite):
site_header = "Staff console"
staff_admin = StaffAdminSite(name="staff_admin")
# settings.py
DJANGO_ADMIN_MCP_API = {
"ADMIN_SITE": "myproject.admin_sites.staff_admin",
}
A single /mcp/ mount exposes exactly one AdminSite. If you need
two MCP surfaces (one per AdminSite), mount the package twice — once
under /staff-mcp/, once under /partner-mcp/ — each with the right
ADMIN_SITE pointer.
manage.py check integration
manage.py check validates the install at boot. It catches:
E001—django_admin_rest_apimissing fromINSTALLED_APPS.E002—ADMIN_SITEdotted path doesn't resolve.W001—DISABLED_TOOLSlists names that don't match any tool (typo guard).
🔒 Security
- The MCP endpoint is not a parallel auth surface. It refuses any caller the HTML admin would refuse, with the same gate.
- Anonymous →
401. Authenticated but non-staff →403. CSRF missing onPOST→ Django's middleware 403. - Every
tools/callis validated against the tool's JSON Schema before it reaches the database. Schema violations returnINVALID_PARAMSwith the json-pointer path of the failing field. - The dispatcher carries the caller's session / user / cookies / CSRF
state to django-admin-rest-api untouched. Per-tool permission is
enforced inside rest-api by the relevant
ModelAdmin.has_*_permission. - CSRF is enforced everywhere. No view in this package is
@csrf_exempt— a pre-commit hook and a test assert this. - No token-shaped string is permitted in the repo (gitleaks + a
pygrep hook +
tests/test_security.py).
Threat model: docs/threat-model.md. Report
a vulnerability privately
here.
🧪 Local development
git clone https://github.com/MartinCastroAlvarez/django-admin-mcp-api
cd django-admin-mcp-api
poetry install
poetry run pytest
poetry run bash scripts/lint.sh
poetry run bash scripts/audit-deps.sh
120 tests, 95% line coverage, including a real end-to-end run through
django-admin-rest-api. CI runs the same suite across Python
3.10–3.13 × Django 5.0/5.1/5.2/6.0 on every PR.
🔌 Use it from your agent
Drop-in config snippets for the major MCP clients live under
examples/clients/:
claude-desktop.json— Anthropic Claude Desktopcursor.json— Cursorvscode-mcp.json— VS Code MCP extensions
Each is a working template — replace the URL + session/CSRF placeholders with your deployment's values and the client can drive the admin.
Headless / scripted clients
For Python scripts, CI jobs, and services that can't drive a
browser, examples/headless-client/
ships a programmatic-login recipe: bootstrap.py logs in once and
writes a cookies file; call.py re-uses it for any MCP method call
(stdlib-only). The same Django session-auth flow your HTML admin
already uses — just scripted.
🤝 Contributing
Issues, PRs, and the roadmap are on GitHub:
- 📋 Issues
- 🗺️ Project board
- 📖
CONTRIBUTING.md— house rules - 🤖
CLAUDE.md— agent contract
The lint + security gate is ruff (check + format + import sorting),
mypy --strict, bandit, pip-audit, gitleaks. Every change must pass
all of them before merge.
📜 License
MIT. See LICENSE.
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_admin_mcp_api-1.5.0.tar.gz.
File metadata
- Download URL: django_admin_mcp_api-1.5.0.tar.gz
- Upload date:
- Size: 42.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
44616135a4ffe0ae45911019f224d6329e0c61482073754734c7bb6c13ea4b33
|
|
| MD5 |
759dc68093911e256fcaae38bc630f1e
|
|
| BLAKE2b-256 |
ddc6807bc425df7e1dcbb0fef753d53ff40f69a77fef022b57524534eeb8a211
|
Provenance
The following attestation bundles were made for django_admin_mcp_api-1.5.0.tar.gz:
Publisher:
publish.yml on MartinCastroAlvarez/django-admin-mcp-api
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_admin_mcp_api-1.5.0.tar.gz -
Subject digest:
44616135a4ffe0ae45911019f224d6329e0c61482073754734c7bb6c13ea4b33 - Sigstore transparency entry: 1704587026
- Sigstore integration time:
-
Permalink:
MartinCastroAlvarez/django-admin-mcp-api@7db4c8b20fccf479721da119ecbd2315e8c9861e -
Branch / Tag:
refs/tags/v1.5.0 - Owner: https://github.com/MartinCastroAlvarez
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@7db4c8b20fccf479721da119ecbd2315e8c9861e -
Trigger Event:
push
-
Statement type:
File details
Details for the file django_admin_mcp_api-1.5.0-py3-none-any.whl.
File metadata
- Download URL: django_admin_mcp_api-1.5.0-py3-none-any.whl
- Upload date:
- Size: 57.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e3702798d19323107cb0669e0c63fcd5dcee73ebe988ec41359e15c9e3e82944
|
|
| MD5 |
ae2b9f4d50161b81f613651077a1679c
|
|
| BLAKE2b-256 |
24da57c6d0b6335e0b0de9313b273a18baed0700859a5965878f88ff3f492241
|
Provenance
The following attestation bundles were made for django_admin_mcp_api-1.5.0-py3-none-any.whl:
Publisher:
publish.yml on MartinCastroAlvarez/django-admin-mcp-api
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_admin_mcp_api-1.5.0-py3-none-any.whl -
Subject digest:
e3702798d19323107cb0669e0c63fcd5dcee73ebe988ec41359e15c9e3e82944 - Sigstore transparency entry: 1704587073
- Sigstore integration time:
-
Permalink:
MartinCastroAlvarez/django-admin-mcp-api@7db4c8b20fccf479721da119ecbd2315e8c9861e -
Branch / Tag:
refs/tags/v1.5.0 - Owner: https://github.com/MartinCastroAlvarez
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@7db4c8b20fccf479721da119ecbd2315e8c9861e -
Trigger Event:
push
-
Statement type: