Skip to main content

django-agent-content

Django + MCP: let AI agents edit your site's content, safely.

Claude Code, Cursor and other MCP clients (through agent-content-mcp), or any HTTP client, can read and edit the models you choose: blog posts, pages, profiles, products. You decide which models and fields are editable. Agents use revocable API keys that act as a Django user, so permissions and the admin's change history work as usual. New content can be forced to drafts, and nothing is ever deleted.

AI agent ──> MCP server ──HTTPS + API key──> your Django site: /api/agent/ ──> your models (via ModelForms)

Install

pip install "django-agent-content @ https://github.com/hexa01/django-agent-content/archive/refs/tags/v0.1.1.tar.gz"
# settings.py
INSTALLED_APPS = [..., 'agent_content']

# urls.py
path('api/agent/', include('agent_content.urls')),
python manage.py migrate

Requires Django 4.2+ and works with any database Django supports: it uses your project's DATABASES.

Say what agents may edit

In each app whose content agents should edit, add agent_resources.py (like admin.py). Start from a scaffold:

python manage.py agent_content_scaffold blog     # writes blog/agent_resources.py, everything commented out

Then uncomment and adjust:

from agent_content import Action, Relation, Resource, register, slug_defaults

from .models import Post

register(Resource(
    Post,
    lookup='slug',                                           # /api/agent/posts/<slug>/
    fields=['title', 'slug', 'excerpt', 'body', 'category', 'tags'],   # what agents may write
    read_only=['status', 'published_at'],                    # what they may only read
    relations={'category': Relation('name'),                 # refer to related objects by name, not id
               'tags': Relation('name', create=True, create_defaults=slug_defaults)},
    create=True, defaults={'status': 'draft'},               # new posts are always drafts
    locked_fields=['slug'], locked_if=lambda post: post.status == 'published',   # links never break
    filters=['status'],                                      # ?status=draft
    actions={'publish': Action(publish, 'Make the post public.',            # POST /api/agent/posts/<slug>/publish/
                               {'published_at': 'ISO date to show, optional'})},
))

Only listed fields are exposed. Saves go through a ModelForm and your model's save(), so validation and side effects match the admin.

Full example

tests/testapp is a small working app used by the tests: models.py and its agent_resources.py show most options together: references by name, tags created when missing, drafts by default, a slug that locks once published, a filter, search, a publish action with a parameter, an inline list (an article's notes) and a single-object resource (site settings).

Resource options

Option Meaning
fields Writable (and readable) fields.
name URL name; default: the model's plural, e.g. posts.
lookup Field used in item URLs; default pk.
singular One item's name, used for tool names (get_post); default: the model's name.
description What this content is, shown to agents.
read_only Readable but not writable.
computed Read-only values: {'url': lambda obj: obj.get_absolute_url()}.
list_fields Fields shown in lists.
relations {'field': Relation('name', create=False)} for ForeignKey/ManyToMany fields.
create, defaults Allow creating; defaults are forced on create (e.g. a draft status).
locked_fields, locked_if Fields that can't change while locked_if(obj) is true.
filters, search ?field=value filters; search(queryset, text) for ?q=.
queryset queryset() if not Model.objects.all().
singleton get() for one-object resources such as site settings: /api/agent/<name>/.
actions {'publish': Action(fn, 'what it does', {'param': 'what it is'})}: POST <lookup>/<name>/; fn(obj, data) returns the object. A plain function also works (its docstring describes it).
inlines {'notes': Inline(Note, fk='article', fields=[...], order_field='position')}: GET/PUT <lookup>/notes/ replaces the list.

API keys

python manage.py agent_content_key --label "Claude Code, laptop"           # never expires
python manage.py agent_content_key --label "CI" --days 90
python manage.py agent_content_key --list
python manage.py agent_content_key --revoke ac_Ab12cd      # by prefix, or --revoke all --user agent-bot

The key is printed once; only its SHA-256 hash is stored. By default it acts as the user agent-bot (created if missing, with no password and no admin access) and is granted exactly the permissions your registered resources need. Keys can also be created, revoked and checked (last used, expiry) under Admin › Agent content › API keys.

Agents send Authorization: Bearer <key>.

Endpoints

Request Does
GET /api/agent/ API version and a description of every resource the key may see: each field's kind, label, help text, limits, choices and whether it's required, plus actions with their params. agent-content-mcp builds its tools from this.
GET <name>/ List (?field=value, ?q=).
POST <name>/ Create (when create=True).
GET, PATCH <name>/<lookup>/ Read, or change only the fields sent.
POST <name>/<lookup>/<action>/ Run an action.
GET, PUT <name>/<lookup>/<inline>/ Read or replace child rows.

Errors are JSON: {"error": "...", "fields": {"slug": ["..."]}}, written so an agent can fix its request.

Settings

All optional.

Setting Default Meaning
AGENT_CONTENT_MODULE 'agent_resources' File name looked up in each app.
AGENT_CONTENT_ADMIN_BASE 'django.contrib.admin.ModelAdmin' Base class for the key admin, e.g. 'unfold.admin.ModelAdmin' for Unfold.
AGENT_CONTENT_RATE_LIMIT 120 Requests per minute per key.
AGENT_CONTENT_MAX_BODY 1000000 Largest request body in bytes.
AGENT_CONTENT_REQUIRE_HTTPS None Refuse plain HTTP; None means "unless DEBUG". Behind a proxy, set Django's SECURE_PROXY_SSL_HEADER.

Tip: add Disallow: /api/ to your robots.txt.

Security

  • Keys are hashed, can expire, and can be revoked at any time; keys of inactive users stop working.
  • What a key can do is its user's Django permissions. The API has no delete endpoints (inline lists replace their rows).
  • HTTPS is required outside DEBUG; requests are rate-limited and size-limited.
  • Every change is recorded in the admin's history under the key's user.

Development

python -m venv .venv && .venv/bin/pip install "Django>=5.2"
.venv/bin/python runtests.py            # SQLite, no project needed

Changelog

  • 0.1.2: package metadata for PyPI (summary, keywords, classifiers, links); README example app and wording.
  • 0.1.1: updates change only the fields sent (unsent text is left exactly as it was); actions take a description and params (Action); the API description adds field kinds, labels, required, singular and create defaults.
  • 0.1.0: first release.

License

MIT

Metadata

Release files for django-agent-content 0.1.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for django-agent-content 0.1.2
File Size Uploaded
django_agent_content-0.1.2.tar.gz 19.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for django-agent-content 0.1.2
File Interpreter ABI Platform
django_agent_content-0.1.2-py3-none-any.whl Python 3 none any Details

Total release size: 42.0 kB

Release files / django_agent_content-0.1.2.tar.gz

Download URL django_agent_content-0.1.2.tar.gz
Size 19.5 kB
Tags Source
SHA-256 checksum
How to use checksums
dd0b4bd6097015b4f1b3c9c2cfb946125204ee31ae117287c39579550905dde9
BLAKE2b-256 checksum
How to use checksums
e9fe829bca46e2238278fd523fa6fbb3934bc3f6d5127251f3dd64993f912564
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / django_agent_content-0.1.2-py3-none-any.whl

Download URL django_agent_content-0.1.2-py3-none-any.whl
Size 22.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3422d74f135a4d112384f9bc55832ff8c65c792f82f38c4dd41af147501fb875
BLAKE2b-256 checksum
How to use checksums
30a261cf8638912c6e0a1b2964997a6c5e31aeac4ace609b1f9f693e13a9a0b9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release history Release notifications | RSS feed

This release

0.1.2 This release

2 release 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