django-admin-fastmcp
A reusable Django app that exposes the Django admin as an MCP server, built on FastMCP.
Every tool call runs as the staff user who owns the bearer token. Every tool call asks the
ModelAdmin for permission first. A superuser can do everything a superuser can do in the
admin. A staff user can do exactly what that staff user can do in the admin, and nothing
more.
SPEC.md is the full specification.
How it works
Three rules define the package:
- No parallel permission system. Authorization delegates to the
ModelAdminmethods:has_view_permission,has_add_permission,has_change_permission,has_delete_permission,get_queryset,get_readonly_fields, andget_actions. Aget_querysetoverride that hides rows hides them from MCP too. - No parallel data surface. Writes go through the admin's own
ModelFormandsave_model, then record aLogEntry. The admin history page stays truthful. - Fail closed. Every unresolved lookup, missing
ModelAdmin, unknown action, unknown field, and unknown tool denies the call.
Installation
uv add django-admin-fastmcp
Add the app to your settings:
INSTALLED_APPS = [
...,
"django.contrib.admin",
"django_admin_fastmcp",
]
ADMIN_FASTMCP = {
"SERVER_NAME": "acme-admin",
"EXCLUDE_MODELS": ("auth.Permission", "auth.Group"),
"WRITABLE_MODELS": (), # empty means no writes at all
}
Mount the OAuth endpoints on the same site as the admin:
# urls.py
urlpatterns = [
# RFC 8414 fixes this one at the site root.
path("", include("django_admin_fastmcp.well_known_urls")),
# This prefix is yours to choose. Match it to the path in MCP_URL.
path("admin/mcp/", include("django_admin_fastmcp.urls")),
path("admin/", admin.site.urls),
]
The package hardcodes no prefix. Every URL the metadata document advertises comes
from reverse(), so a project that mounts the endpoints at /backoffice/oauth/
gets that in discovery and clients follow it. Two rules: the well-known document
belongs at the root, because a client derives its URL from the issuer, and the
endpoints should sit on the same site as the admin, because the consent page
rides the admin session cookie.
Apply the migrations:
python manage.py migrate django_admin_fastmcp
Nothing else. No per-model registration, no mixin, no decorators. The server exposes whatever the admin already exposes.
Connect a client
Run the server (see Deployment), then register it:
# Claude Code
claude mcp add --transport http acme-admin https://<host>/admin/mcp
Use the path with no trailing slash. /admin/mcp/ answers a 307 redirect to
/admin/mcp, and not every client follows a redirect on POST.
No token, no header. The first call starts the standard MCP OAuth flow:
- The client opens your browser at the authorize page on the Django site.
- Your admin session cookie identifies you. If you are logged out, the normal admin login appears first.
- A consent page shows the client name and what approval means. You approve.
- The client receives its tokens and connects. It refreshes them by itself.
Any MCP client that speaks streamable HTTP with OAuth works the same way, for example
Cursor or a FastMCP Client.
Rules around access:
- Any staff user can authorize a client, for themselves only.
- A grant acts with your own admin permissions, never more. There is no separate
permission system: whoever may change a model in the admin may change it over MCP,
when the server lists that model in
WRITABLE_MODELS. - Refresh tokens expire after
REFRESH_TOKEN_TTL_DAYS(default 90), so re-consent happens that often. Revocation is an admin action on the grant changelist.
Tools
Eleven generic tools, mounted under the namespace admin, so wire names are
admin_list_models and so on. Each takes model as "app_label.ModelName". The tool
list is static. What varies per user is what each tool lets that user see and do.
Read
| Tool | Arguments | Returns |
|---|---|---|
list_models |
none | Every exposed model this caller may view, with permission flags. |
describe_model |
model |
Fields, list display, filters, search fields, readonly fields, and available actions. |
search_objects |
model, q, filters, order_by, page, page_size |
Rows plus total. q uses the admin's own search. An unknown filter is an error. |
get_object |
model, pk |
One serialized instance. |
object_history |
model, pk |
Admin log entries for that object, newest first. |
recent_actions |
limit |
Admin log entries, scoped to the caller unless the caller is a superuser. |
Write
Write tools need the model listed in WRITABLE_MODELS; your own admin permissions
decide the rest, per model and per object. A model outside the list refuses every write
and every action, whoever calls. Leave sensitive models out, an event log for example,
and no MCP client can ever write to them.
| Tool | Arguments | Behavior |
|---|---|---|
create_object |
model, data |
Validates through the admin form, then saves and logs. |
update_object |
model, pk, data |
Partial update. Readonly fields are ignored. |
delete_object |
model, pk, confirm |
Without confirm, returns the exact deletion cascade and changes nothing. |
run_action |
model, action, pks, confirm |
Runs an admin action. Without confirm, returns a preview. |
autocomplete |
model, field, q |
Resolves a foreign-key value to a primary key by searching the related model. |
Every returned row carries pk as a string and an admin_url, so an agent can hand a
person a link into the real admin.
Settings
All keys live in the ADMIN_FASTMCP dict. An unknown key is an error at startup.
| Key | Default | Meaning |
|---|---|---|
SERVER_NAME |
"django-admin" |
Name the MCP server advertises. |
ADMIN_SITE |
"django.contrib.admin.site" |
Dotted path to the AdminSite. |
MODELS |
() |
Allowlist of "app_label.ModelName". When non-empty, nothing else is exposed. |
EXCLUDE_MODELS |
() |
Denylist. Supports "app_label.*". |
WRITABLE_MODELS |
() |
Models that accept writes. Empty means no writes, whoever calls. |
DISABLED_TOOLS |
() |
Tool names removed from the catalogue entirely. |
REDACT_FIELDS |
("password", "token", "secret", "api_key", "private_key") |
Substring match on field names. Values read "[redacted]". |
MAX_PAGE_SIZE |
200 |
Cap on search_objects page size. |
MAX_PKS |
1000 |
Cap on pks per run_action. |
ACCESS_TOKEN_TTL_MINUTES |
60 |
Access token lifetime. Clients renew with the refresh token. |
REFRESH_TOKEN_TTL_DAYS |
90 |
Refresh token lifetime. Re-consent happens this often. |
SITE_URL |
"http://127.0.0.1:8000" |
Public URL of the Django site. It is the OAuth issuer, and the MCP server names it as its authorization server. |
MCP_URL |
"http://127.0.0.1:8765/admin/mcp" |
Public URL of the MCP endpoint. |
Set SITE_URL and MCP_URL for any real deployment. MCP_URL is the single
source of three things that must agree: the path the endpoint is served on, the
resource that discovery advertises, and the audience every token is bound to.
Its path defaults to /admin/mcp. A startup check refuses a MCP_URL with no
path, because then the whole origin would be advertised as the protected
resource.
Per-ModelAdmin knobs
Set these on a ModelAdmin class, no mixin needed:
class InvoiceAdmin(admin.ModelAdmin):
mcp_expose = False # hide this model from MCP entirely
mcp_fields = ("number", "total") # allowlist of serialized fields
mcp_exclude_fields = ("internal_note",) # denylist of serialized fields
REDACT_FIELDS wins over mcp_fields. Listing a password field explicitly does not
reveal it.
Safety
An admin MCP server for a superuser is a remote shell over the production database, driven by a language model. The rails:
WRITABLE_MODELSdefaults to empty, so no model accepts writes until the deployment names it. Everything else is your ordinary Django permissions, asked through theModelAdminon every call.- Access tokens are short-lived. Only salted hashes are stored, so a leaked database row cannot be replayed.
delete_objectandrun_actionpreview by default and change nothing untilconfirm=True.- Every mutation records a
LogEntryattributed to the grant's user, with the client name in the change message, for example"Changed status. Via MCP (client: Claude Code).". A write that cannot record aLogEntryrolls back. - The package's own models,
sessions.Session, andauthtoken.Tokenare never exposed, whatever the settings say. - Keep
auth.Permissionandauth.Groupout ofWRITABLE_MODELS. An agent that can grant permissions can escape the permission model.
Deployment
Separate process. Run the MCP server next to your Django project:
python manage.py admin_mcp_serve
It serves the path from MCP_URL, which is /admin/mcp by default, on the port from
MCP_URL, or 8765 when that URL names no port. Both are overridable with --host and
--port. Nothing about your existing serving configuration changes. Route
/admin/mcp through your ingress to that port, and make sure the Authorization
header passes through.
Mounted (M3). Mount the server at /admin/mcp inside your project's asgi.py. One
constraint: dispatch on the exact path. The OAuth endpoints live directly below the same
prefix (/admin/mcp/authorize and friends) and Django must keep serving those, so a
dispatcher that sends everything under /admin/mcp to FastMCP would swallow them. The
recipe ships with milestone M3.
The server is stateless, so any instance behind a load balancer can serve any request.
Development
make install # bootstrap uv, pin Python, install dependencies
make test # run the permission matrix
make check # format, lint, typecheck, and test
make migrate # migrate the test project
make serve # run the MCP server against the test project on :8765/admin/mcp
make help # everything else
License
MIT
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_fastmcp-0.1.0.tar.gz.
File metadata
- Download URL: django_admin_fastmcp-0.1.0.tar.gz
- Upload date:
- Size: 30.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Manjaro Linux","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
160c42e811ed60a571639d59b96671fb78e68cf2d9bb9f08205a48312f2ad24f
|
|
| MD5 |
bda664b411721d1ed1344c4cecfb4705
|
|
| BLAKE2b-256 |
8d40527da2aec96dd80d77e58ec8fc0329dfeae9757a9fa4faeb7a9e899193e2
|
File details
Details for the file django_admin_fastmcp-0.1.0-py3-none-any.whl.
File metadata
- Download URL: django_admin_fastmcp-0.1.0-py3-none-any.whl
- Upload date:
- Size: 40.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Manjaro Linux","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ee434ad4800b76cf5175493a4ccc68de14f5d87f841e966c5cdf10e8e67c8716
|
|
| MD5 |
47a6432b24f3e6691c96a8b1cfe3807d
|
|
| BLAKE2b-256 |
6b9ee2ef98aa9537f4d86a23876a0455867e1c4e2b36d457263fd89f9cda8d10
|