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>_displaylabel sidecar. Every field defined withchoices(IntegerChoices,TextChoices, plain or grouped lists) keeps its raw stored value and gains a<field>_displaykey 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 honormcp_fields/mcp_exclude_fieldsvisibility 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_TOKENaccepts 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 staticAuthorizationheader, 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 returns404while 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 invalidfiltersandorder_byinstead 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 withPydanticSerializationErrorwhen admin config or model metadata contains Django lazy translation proxies —gettext_lazyfieldset 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/listis filtered by the requesting token's permissions. Models failing thehas_module_permission/ view permission checks are skipped, mirroringfind_modelsandresources/list; a minimally-privileged token can no longer enumerate every exposed model's tool schemas (#101)- Fields hidden by
mcp_fields/mcp_exclude_fieldsdisappear from every schema surface. Tool descriptions,describe_*, and themodels://{model}/schemaresource 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 forrelated_*under an include list (#102) - Bulk update redacts sensitive values in
LogEntrymessages.bulk_<model>withoperation: "update"wrote plaintext secrets intodjango_admin_logwhile 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, andreadonly_fields. The inline form is now resolved through the inline'sget_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_permsare 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 fromget_effective_permissions(), matching theTokenUserproxy.get_all_permissions()remains raw-grant introspection and is documented as such (#109)
Fixed¶
- Admin hooks calling
self.message_user()insave_model/delete_model/actions no longer crash MCP writes withMessageFailure: the synthetic MCP requests now carry an in-memory messages storage. This also un-breaks thecreate_mcptokentool, which could never succeed (#100) search_fieldsoperator prefixes (^,=,@) andfield__lookupforms no longer breaklist_*search andautocomplete_*: searching goes throughModelAdmin.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 undocumentedvaluebranch stringifyingNone, 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 makeget_*/list_*return empty objects; include/exclude lists are flattened before use (#107) - Input-validation gaps: a legitimate
pk=0is looked up instead of rejected as missing;bulk_*with a non-listitemsreturns{"error": "items must be a list"}instead of a success-shaped no-op;history_*honors theoffsetit 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 anidreturn an empty HTTP 202, per JSON-RPC 2.0. Unknown request methods with anidkeep the-32601error envelope (#108)
[0.6.0] - 2026-09-15¶
Security¶
- All row lookups now honor
ModelAdmin.get_queryset()scoping.update_*,delete_*,bulk_*,related_*,history_*, andautocomplete_*previously usedmodel.objectsdirectly, 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 = Falsemodels are no longer callable. Tools for non-exposed models were hidden fromtools/listbut 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, bypassingmcp_fields/mcp_exclude_fields; non-relation names are now rejected (#90)- Related and inline reads check permissions on the related model.
related_*returnspermission_deniedandinclude_related/include_inlinesomit 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_*withinclude_related: trueno longer fails with "An internal error occurred" on models that have a forward ForeignKey (#93)- Negative or non-integer
limit/offsetinrelated_*,history_*, andautocomplete_*now return a JSON validation error instead of an unhandled HTTP 500; these handlers also honor theMCP_MAX_LIST_LIMITcap, andcall_toolgained a last-resort sanitized-error guard (#94) - Bulk and action paths follow the standard admin pipeline:
delete_selectedwritesLogEntryrecords and callsdelete_queryset(); bulk create/update go throughsave_model()and apply the same unknown-field/readonly guards as single-object update; bulk delete callsdelete_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 inerror.data) come back with HTTP 200 instead of bare 400 bodies; tool-result failures return their envelope with 200 instead of 500;notifications/initializedreturns an empty HTTP 202 (#97) MCPToken.last_used_atwrites are throttled to once perMCP_LAST_USED_RESOLUTIONseconds (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
permissionsandgroupsfields intersected with the linked user's Django permissions — via a permission proxy placed onrequest.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 forLogEntryrecords. 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_atkwarg 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'sUser.has_module_perms, used byfind_modelsdiscoveryTokenUserpermission 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_modelsnow reportstools_exposedbased on each admin'smcp_exposeflag instead of alwaystrue- 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, andltwere missing)
Documentation¶
- Documentation audited against the code: corrected request/response shapes (JSON-RPC envelope,
params-nestedtools/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/listandprompts/getserve workflow guides (explore_models,understand_model,crud_guide,bulk_operations_guide) (#65) - MCP Resources support:
resources/list,resources/templates/list, andresources/readexpose 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 withconfirm: true(plus optionalconfirmation_data) executes them. Actions also receive Django's standard POST fields (action,_selected_action) (#63) min_num/max_numinline constraints are enforced when editing inlines viaupdate_<model>(#61, #62)
Changed¶
find_modelsrespectsModelAdmin.has_module_permission(): hidden modules are excluded from discovery (#64)list_<model>validateslimit/offsetand caps page size atMCP_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_querysetoption (defaultTrue): list/get and admin actions now start fromModelAdmin.get_queryset(request), so proxy models, soft-delete, and multi-tenant scoping match the admin changelist (#84, #85)- Admin action
HttpResponse/StreamingHttpResponsedownloads are returned as structured file payloads (UTF-8 or base64) instead ofstr(response), with a size cap viaMCP_ACTION_MAX_FILE_BYTES(default 5 MiB) (#86) - MCP Inspector troubleshooting guide for Bearer token setup (#81)
Fixed¶
FileField/ImageFieldvalues serialize as storage-path strings; empty image fields no longer crashlist_*responses (#87)describe_*no longer crashes whenModelAdmin.orderingisNone(the Django default); filter classes and callables in admin config serialize as dotted paths (#83)- The MCP
initializeresponse 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
LogEntryaudit messages (#42) mcp_exclude_fieldsis now applied on list and related serialization, not just get/create/update (#82)delete_selectedchecks 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()anddelete_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_requestsdecorator to async views forATOMIC_REQUESTScompatibility (#72)require_registered_modelandrequire_permissiondecorators 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
jsonmodule with PydanticTypeAdapteracross all handlers (#53, #54, #55, #56) - Wrap CRUD and bulk operations in
transaction.atomic()for data integrity (#57) - Apply
require_registered_modelandrequire_permissiondecorators 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(), andhandle_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_keyfield - Constant-time secret comparison to prevent timing attacks
require_registered_modelandrequire_permissiondecorators- Bulk create support (
handle_bulk_create) mcp_fieldsandmcp_exclude_fieldsfor field filtering
Changed¶
- Split
handle_bulkintohandle_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
MCPAdminMixinfor exposing Django admin models- Token-based authentication with
MCPTokenmodel - 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/listis now permission-filtered. Tools appear only for models the token holds view (and module) permission on, matchingfind_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_fieldsno longer appear in tool descriptions,describe_*, ormodels://{model}/schema. Clients introspecting those fields' metadata must expose them deliberately. MCPToken.has_perm/has_perms/has_module_permsnow answer from effective permissions (grants capped by the linked user). Downstream code that used them for raw-grant introspection should useget_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 ofnotifications/*) returns an empty HTTP 202; only requests with anidreceive envelopes. related_*null relations changed shape. Empty forward FK/O2O and reverse O2O now return{"type": "single", "result": null}instead of avaluestring or an internal error.
From 0.5.x to 0.6.0¶
No migrations. Behavior changes to review:
mcp_expose = Falsemodels are no longer callable. If any client relied on calling tools for a registered-but-not-exposed model, setmcp_expose = Trueon that admin.- Row scoping now applies everywhere.
update_*,delete_*,bulk_*,related_*,history_*, andautocomplete_*honorModelAdmin.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 useget_*instead.- Related/inline reads require view permission on the related model. Grant the token
viewpermission on inline/related models it should keep seeing throughinclude_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, andnotifications/initializedreturns an empty HTTP 202. Clients checking for HTTP 400/500 on these paths must readerror.codeinstead. MCPToken.last_used_atnow updates at most once perMCP_LAST_USED_RESOLUTIONseconds (default 60). Set the setting to0to 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:
-
Update the package and run migrations:
-
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. -
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:
-
Update the package:
-
Run migrations:
-
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:
-
Update the package:
-
Run migrations:
-
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