Skip to main content

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:

  1. No parallel permission system. Authorization delegates to the ModelAdmin methods: has_view_permission, has_add_permission, has_change_permission, has_delete_permission, get_queryset, get_readonly_fields, and get_actions. A get_queryset override that hides rows hides them from MCP too.
  2. No parallel data surface. Writes go through the admin's own ModelForm and save_model, then record a LogEntry. The admin history page stays truthful.
  3. 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:

  1. The client opens your browser at the authorize page on the Django site.
  2. Your admin session cookie identifies you. If you are logged out, the normal admin login appears first.
  3. A consent page shows the client name and what approval means. You approve.
  4. 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_MODELS defaults to empty, so no model accepts writes until the deployment names it. Everything else is your ordinary Django permissions, asked through the ModelAdmin on every call.
  • Access tokens are short-lived. Only salted hashes are stored, so a leaked database row cannot be replayed.
  • delete_object and run_action preview by default and change nothing until confirm=True.
  • Every mutation records a LogEntry attributed 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 a LogEntry rolls back.
  • The package's own models, sessions.Session, and authtoken.Token are never exposed, whatever the settings say.
  • Keep auth.Permission and auth.Group out of WRITABLE_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

django_admin_fastmcp-0.1.0.tar.gz (30.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_admin_fastmcp-0.1.0-py3-none-any.whl (40.0 kB view details)

Uploaded Python 3

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

Hashes for django_admin_fastmcp-0.1.0.tar.gz
Algorithm Hash digest
SHA256 160c42e811ed60a571639d59b96671fb78e68cf2d9bb9f08205a48312f2ad24f
MD5 bda664b411721d1ed1344c4cecfb4705
BLAKE2b-256 8d40527da2aec96dd80d77e58ec8fc0329dfeae9757a9fa4faeb7a9e899193e2

See more details on using hashes here.

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

Hashes for django_admin_fastmcp-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ee434ad4800b76cf5175493a4ccc68de14f5d87f841e966c5cdf10e8e67c8716
MD5 47a6432b24f3e6691c96a8b1cfe3807d
BLAKE2b-256 6b9ee2ef98aa9537f4d86a23876a0455867e1c4e2b36d457263fd89f9cda8d10

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 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