Skip to content

Changelog

All notable changes to Django Admin MCP are documented here.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[0.8.1] - 2026-09-29

Added

  • Choice fields serialize with a <field>_display label sidecar. Every field defined with choices (IntegerChoices, TextChoices, plain or grouped lists) keeps its raw stored value and gains a <field>_display key carrying the human-readable label ("status": 2, "status_display": "Active"), so MCP clients no longer need a schema round-trip to interpret enum values. Sidecars honor mcp_fields/mcp_exclude_fields visibility and never shadow a real model field of the same name (#113)

Fixed

  • describe_* now flattens grouped (optgroup) choices into individual value/label pairs; previously each group serialized as a single broken entry with the stringified inner list as its label (#113)

[0.8.0] - 2026-09-17

Added

  • MCP_ALLOW_URL_TOKEN accepts the bearer token as a URL path segment. Web MCP clients (claude.ai and ChatGPT custom connectors) register a plain URL and authenticate over OAuth, with no field for a static Authorization header, leaving them unable to reach the header route at all. With the setting enabled, POST <mount_point>/mcp_<key>.<secret>/ authenticates the same token through the same pipeline; the route returns 404 while the setting is false (the default), and the header route is unaffected either way. The URL is now the credential — it reaches access logs and browser history — so use a dedicated, narrowly-scoped token for it

[0.7.2] - 2026-09-16

Changed

  • list_* rejects invalid filters and order_by instead of silently dropping them. Unknown fields, disallowed lookups, and relation traversal now return an error response naming every offending key; previously the query ran without those filters, handing the caller a success-shaped but unfiltered result (#111)

[0.7.1] - 2026-09-16

Fixed

  • describe_* (and any handler response) no longer crashes with PydanticSerializationError when admin config or model metadata contains Django lazy translation proxies — gettext_lazy fieldset names, verbose_names, and the like now serialize as their string value

Changed

  • Minimum Pydantic version is now 2.7 (the log-redaction path already relied on a 2.7 feature; the floor now reflects reality)

[0.7.0] - 2026-09-15

Security

  • tools/list is filtered by the requesting token's permissions. Models failing the has_module_permission / view permission checks are skipped, mirroring find_models and resources/list; a minimally-privileged token can no longer enumerate every exposed model's tool schemas (#101)
  • Fields hidden by mcp_fields/mcp_exclude_fields disappear from every schema surface. Tool descriptions, describe_*, and the models://{model}/schema resource previously listed the name, type, and constraints of deliberately hidden fields (password/token/PII columns); they now apply the same include/exclude resolution as serialization. Reverse relations stay discoverable for related_* under an include list (#102)
  • Bulk update redacts sensitive values in LogEntry messages. bulk_<model> with operation: "update" wrote plaintext secrets into django_admin_log while the single-update path redacted them; both now share the same redacting serializer (which also drops the stray quote the old truncation branch appended) (#104)
  • Inline writes honor the inline admin's fields, exclude, and readonly_fields. The inline form is now resolved through the inline's get_formset() like the Django admin, and keys targeting readonly or undeclared fields are rejected with the same error shapes as top-level update instead of being silently written (#105)
  • MCPToken.has_perm/has_perms/has_module_perms are capped by the linked user's permissions. These public methods answered from the token's raw grants, bypassing the 0.5.0 cap rule that MCP requests already enforce; they now answer from get_effective_permissions(), matching the TokenUser proxy. get_all_permissions() remains raw-grant introspection and is documented as such (#109)

Fixed

  • Admin hooks calling self.message_user() in save_model/delete_model/actions no longer crash MCP writes with MessageFailure: the synthetic MCP requests now carry an in-memory messages storage. This also un-breaks the create_mcptoken tool, which could never succeed (#100)
  • search_fields operator prefixes (^, =, @) and field__lookup forms no longer break list_* search and autocomplete_*: searching goes through ModelAdmin.get_search_results(), so custom overrides are honored too (#103)
  • related_* on a null forward FK/O2O returns {type: "single", result: null} instead of the undocumented value branch stringifying None, and an empty reverse one-to-one returns the same null result instead of "An internal error occurred" (#106)
  • Grouped (tupled) entries in the admin's fields — e.g. fields = [("name", "email")] — no longer make get_*/list_* return empty objects; include/exclude lists are flattened before use (#107)
  • Input-validation gaps: a legitimate pk=0 is looked up instead of rejected as missing; bulk_* with a non-list items returns {"error": "items must be a list"} instead of a success-shaped no-op; history_* honors the offset it accepts (#110)

Changed

  • JSON-RPC notifications never receive a response body: the whole notifications/ namespace (e.g. notifications/cancelled, notifications/roots/list_changed) and any request without an id return an empty HTTP 202, per JSON-RPC 2.0. Unknown request methods with an id keep the -32601 error envelope (#108)

[0.6.0] - 2026-09-15

Security

  • All row lookups now honor ModelAdmin.get_queryset() scoping. update_*, delete_*, bulk_*, related_*, history_*, and autocomplete_* previously used model.objects directly, so rows hidden from the admin changelist (multi-tenant filters, soft-delete, proxy scoping) could still be read, updated, or deleted by pk (#88)
  • mcp_expose = False models are no longer callable. Tools for non-exposed models were hidden from tools/list but executed when called by name; they now return the same "Model not found" error as unregistered models (#89)
  • related_* only serves actual relations. Any attribute name (plain fields, properties, methods) was previously returned as a string value, bypassing mcp_fields/mcp_exclude_fields; non-relation names are now rejected (#90)
  • Related and inline reads check permissions on the related model. related_* returns permission_denied and include_related/include_inlines omit models the token may not view; related rows also come from the related admin's queryset scope (#91)
  • Inline updates and deletes are scoped to the parent object. Inline items in update_* were looked up by pk alone, allowing any row of the inline model to be modified or deleted through an unrelated parent (#92)
  • Admin token regeneration requires POST and change permission. The regenerate view previously invalidated and reissued credentials on a plain GET (CSRF-able) and admitted any active staff user regardless of MCPToken permissions (#96)

Fixed

  • get_* with include_related: true no longer fails with "An internal error occurred" on models that have a forward ForeignKey (#93)
  • Negative or non-integer limit/offset in related_*, history_*, and autocomplete_* now return a JSON validation error instead of an unhandled HTTP 500; these handlers also honor the MCP_MAX_LIST_LIMIT cap, and call_tool gained a last-resort sanitized-error guard (#94)
  • Bulk and action paths follow the standard admin pipeline: delete_selected writes LogEntry records and calls delete_queryset(); bulk create/update go through save_model() and apply the same unknown-field/readonly guards as single-object update; bulk delete calls delete_model() (#95)
  • A cross-app model name collision in the MCP registry is now logged instead of silently dropping the second model's registration (#99)

Changed

  • The JSON-RPC endpoint returns spec-compliant error envelopes: parse errors (-32700), unknown methods (-32601), and invalid params (-32602, sanitized details in error.data) come back with HTTP 200 instead of bare 400 bodies; tool-result failures return their envelope with 200 instead of 500; notifications/initialized returns an empty HTTP 202 (#97)
  • MCPToken.last_used_at writes are throttled to once per MCP_LAST_USED_RESOLUTION seconds (default 60; set 0 to record every use) instead of every authenticated request (#98)

[0.5.0] - 2026-09-15

Changed — BREAKING

  • Token-level permissions are now enforced. Authorization answers from the token's effective permissions — its own permissions and groups fields intersected with the linked user's Django permissions — via a permission proxy placed on request.user. A token can narrow its user's access but never exceed it: a token with no grants has no access even when bound to a superuser, a grant the linked user lacks stays ineffective, and deactivating the linked user disables the token's access. The linked user remains the audit identity for LogEntry records. Upgrade note: existing tokens had access equal to their user's permissions; after upgrading they must also be granted the needed permissions/groups on the token itself, or their requests will be denied.
  • Leaving Expires At blank in the admin form now creates a token that never expires, matching the field's help text. Programmatic creation without an expires_at kwarg still defaults to 90 days.

Added

  • MCPToken.get_effective_permissions() — the token's grants capped by the linked user's permissions (what requests actually authorize with)
  • MCPToken.has_module_perms(app_label) — module-level permission check mirroring Django's User.has_module_perms, used by find_models discovery
  • TokenUser permission proxy (django_admin_mcp.models.TokenUser) answering Django's permission API from the token's effective permissions while delegating identity attributes to the linked user

Fixed

  • find_models now reports tools_exposed based on each admin's mcp_expose flag instead of always true
  • The advertised autocomplete_* limit default now matches the handler default (10, was 20 in the schema)
  • The generated list_* tool description now advertises the full filter-lookup whitelist (contains, gt, and lt were missing)

Documentation

  • Documentation audited against the code: corrected request/response shapes (JSON-RPC envelope, params-nested tools/call), error payloads, permission semantics (checks run against the token's linked user), serialization details, and settings reference (MCP_MAX_LIST_LIMIT, MCP_ACTION_MAX_FILE_BYTES); documented the action confirmation flow, file downloads, inline editing, and queryset scoping

[0.4.0] - 2026-09-15

Added

  • MCP Prompts support: prompts/list and prompts/get serve workflow guides (explore_models, understand_model, crud_guide, bulk_operations_guide) (#65)
  • MCP Resources support: resources/list, resources/templates/list, and resources/read expose model schemas (models://<model>/schema) and instance data (data://<model>/, data://<model>/<id>), permission-filtered (#66)
  • Two-step confirmation workflow for admin actions: actions returning an HTML page report requires_confirmation: true; re-calling with confirm: true (plus optional confirmation_data) executes them. Actions also receive Django's standard POST fields (action, _selected_action) (#63)
  • min_num/max_num inline constraints are enforced when editing inlines via update_<model> (#61, #62)

Changed

  • find_models respects ModelAdmin.has_module_permission(): hidden modules are excluded from discovery (#64)
  • list_<model> validates limit/offset and caps page size at MCP_MAX_LIST_LIMIT (default 1000) (#47)
  • The two HTTP view code paths now share one auth/parse/validate/execute pipeline (#43)

[0.3.3] - 2026-09-15

Added

  • mcp_use_admin_queryset option (default True): list/get and admin actions now start from ModelAdmin.get_queryset(request), so proxy models, soft-delete, and multi-tenant scoping match the admin changelist (#84, #85)
  • Admin action HttpResponse / StreamingHttpResponse downloads are returned as structured file payloads (UTF-8 or base64) instead of str(response), with a size cap via MCP_ACTION_MAX_FILE_BYTES (default 5 MiB) (#86)
  • MCP Inspector troubleshooting guide for Bearer token setup (#81)

Fixed

  • FileField/ImageField values serialize as storage-path strings; empty image fields no longer crash list_* responses (#87)
  • describe_* no longer crashes when ModelAdmin.ordering is None (the Django default); filter classes and callables in admin config serialize as dotted paths (#83)
  • The MCP initialize response now reports the real package version instead of a hardcoded one

Security

  • Filter lookups are restricted to a whitelist (exact, contains, icontains, gt, gte, lt, lte, in, isnull) on direct fields only — relation traversal and regex lookups are rejected (#40)
  • Sensitive values (passwords, tokens, secrets) are redacted from admin LogEntry audit messages (#42)
  • mcp_exclude_fields is now applied on list and related serialization, not just get/create/update (#82)
  • delete_selected checks delete permission before any ID lookup to avoid leaking row existence (#85)

[0.3.2] - 2026-07-09

Changed

  • License changed from GPL-3.0 to MIT

[0.3.1] - 2026-03-12

Changed

  • CRUD handlers use ModelAdmin.save_model() and delete_model() for the standard Django admin pipeline (#74)

Fixed

  • M2M fields serialize as lists of PKs in list/get responses (#77)

[0.3.0] - 2026-02-08

Added

  • non_atomic_requests decorator to async views for ATOMIC_REQUESTS compatibility (#72)
  • require_registered_model and require_permission decorators for handler authorization
  • Comprehensive MkDocs Material documentation with getting started guides, tool reference, and example conversations

Changed

  • Use Django's get_actions() to resolve admin actions for listing and execution (#73)
  • Replace json module with Pydantic TypeAdapter across all handlers (#53, #54, #55, #56)
  • Wrap CRUD and bulk operations in transaction.atomic() for data integrity (#57)
  • Apply require_registered_model and require_permission decorators to CRUD, relations, and meta handlers
  • Narrow broad exception handling in authenticate_token

Fixed

  • URL path duplication when mounting at custom path (e.g., path("mcp/", ...) produced /mcp/mcp/) (#68, #71)

Security

  • Sanitize error responses to prevent internal detail leakage (#70)
  • Add permission checks to handle_history(), handle_related(), handle_autocomplete(), handle_describe(), and handle_find_models()
  • Inline permission checks to prevent privilege escalation
  • Add field filtering to serialize_instance() to prevent sensitive data exposure (#58)

[0.2.1] - 2025

Changed

  • Restrict model lookups to MCPAdminMixin registry only
  • Remove redundant str() calls in _get_action_info

[0.2.0] - 2025

Added

  • Hashed token authentication with mcp_<key>.<secret> format
  • O(1) token lookup via indexed token_key field
  • Constant-time secret comparison to prevent timing attacks
  • require_registered_model and require_permission decorators
  • Bulk create support (handle_bulk_create)
  • mcp_fields and mcp_exclude_fields for field filtering

Changed

  • Split handle_bulk into handle_bulk_create, handle_bulk_update, handle_bulk_delete
  • Extracted permission decorators to handlers/decorators.py

Security

  • Token secret is now hashed with per-token salt (SHA-256)
  • Token format changed to mcp_<key>.<secret> for structured authentication

[0.1.0] - 2024-01-15

Added

  • Initial release
  • MCPAdminMixin for exposing Django admin models
  • Token-based authentication with MCPToken model
  • CRUD operations: list_*, get_*, create_*, update_*, delete_*
  • Model introspection: describe_*, find_models
  • Admin actions: actions_*, action_*, bulk_*
  • Relationships: related_*, history_*, autocomplete_*
  • Full Django admin permission integration
  • Support for Django 3.2, 4.0, 4.1, 4.2, 5.0
  • Support for Python 3.10, 3.11, 3.12

Security

  • Bearer token authentication
  • Token expiration (default 90 days)
  • Django permission checking on all operations
  • Principle of least privilege (tokens start with no permissions)

Version History

Version Python Django
0.7.2 3.10+ 3.2+
0.7.1 3.10+ 3.2+
0.7.0 3.10+ 3.2+
0.6.0 3.10+ 3.2+
0.5.0 3.10+ 3.2+
0.4.0 3.10+ 3.2+
0.3.x 3.10+ 3.2+
0.2.x 3.10+ 3.2+
0.1.0 3.10+ 3.2+

Upgrade Guide

From 0.6.x to 0.7.0

No migrations. Behavior changes to review:

  • tools/list is now permission-filtered. Tools appear only for models the token holds view (and module) permission on, matching find_models. Clients that enumerated all tools with a low-privilege token will see a shorter list; grant view permissions for models that should stay discoverable.
  • Hidden fields left every schema surface. Fields excluded via mcp_fields/mcp_exclude_fields no longer appear in tool descriptions, describe_*, or models://{model}/schema. Clients introspecting those fields' metadata must expose them deliberately.
  • MCPToken.has_perm/has_perms/has_module_perms now answer from effective permissions (grants capped by the linked user). Downstream code that used them for raw-grant introspection should use get_all_permissions() instead.
  • Inline writes are validated like the admin. Inline items targeting fields that are readonly or not declared on the inline now return errors instead of writing. Clients depending on the old behavior must update the inline's fields/readonly_fields.
  • Notifications get no response body. Any JSON-RPC request without an id (including all of notifications/*) returns an empty HTTP 202; only requests with an id receive envelopes.
  • related_* null relations changed shape. Empty forward FK/O2O and reverse O2O now return {"type": "single", "result": null} instead of a value string or an internal error.

From 0.5.x to 0.6.0

No migrations. Behavior changes to review:

  • mcp_expose = False models are no longer callable. If any client relied on calling tools for a registered-but-not-exposed model, set mcp_expose = True on that admin.
  • Row scoping now applies everywhere. update_*, delete_*, bulk_*, related_*, history_*, and autocomplete_* honor ModelAdmin.get_queryset(request); rows outside the admin queryset now return "not found".
  • related_* no longer returns plain field values. Only actual relations are served; clients reading non-relation attributes through it must use get_* instead.
  • Related/inline reads require view permission on the related model. Grant the token view permission on inline/related models it should keep seeing through include_inlines/include_related/related_*.
  • JSON-RPC error responses changed shape. Parse errors, unknown methods, and invalid params now arrive as JSON-RPC error envelopes (-32700/-32601/-32602) with HTTP 200, and notifications/initialized returns an empty HTTP 202. Clients checking for HTTP 400/500 on these paths must read error.code instead.
  • MCPToken.last_used_at now updates at most once per MCP_LAST_USED_RESOLUTION seconds (default 60). Set the setting to 0 to restore per-request writes.

From 0.4.x to 0.5.0

Authorization changed from the linked user's permissions to the token's own permissions (capped by the user's). Before or immediately after upgrading:

  1. Update the package and run migrations:

    pip install --upgrade django-admin-mcp
    python manage.py migrate django_admin_mcp
    

  2. Grant each existing token the permissions or groups it needs (in the MCP Token admin, or via token.permissions / token.groups). Tokens without grants are denied every operation, regardless of their user's permissions — including superusers.

  3. Optionally revisit expires_at: blank in the admin form now means "never expires" instead of the 90-day default.

From 0.1.x to 0.2.x

Token format has changed. You must:

  1. Update the package:

    pip install --upgrade django-admin-mcp
    

  2. Run migrations:

    python manage.py migrate django_admin_mcp
    

  3. Recreate all tokens (token format changed to mcp_<key>.<secret>)

From Pre-release to 0.1.0

If you were using a pre-release version:

  1. Update the package:

    pip install --upgrade django-admin-mcp
    

  2. Run migrations:

    python manage.py migrate django_admin_mcp
    

  3. Recreate any tokens (token format may have changed)


Deprecation Policy

  • Features are deprecated for at least one minor version before removal
  • Deprecated features will emit warnings
  • Migration guides are provided for breaking changes