Skip to content
Guide

AI agents (MCP)

Run an MCP (Model Context Protocol) server that turns your viewsets into tools. AI clients like Claude Desktop or VS Code can then call your CRUD endpoints and custom actions.

Set up the server

Install the extra

Bash
pip install "django-ninja-aio-crud[mcp]"

Add the app

Add "ninja_aio" to INSTALLED_APPS so Django finds the mcp_server command:

settings.py
INSTALLED_APPS = [
    # ...
    "ninja_aio",
    "blog",
]

Point it at your API

Set the dotted path to your NinjaAIO instance:

settings.py
NINJA_AIO_MCP_API = "blog.api.api"

Run it

Bash
python manage.py mcp_server

The server talks over stdio. Your MCP client starts it, so you don't keep it running yourself.

Command options

Bash
python manage.py mcp_server blog.api.api --name blog-api
Option What it does
api Dotted path to the NinjaAIO instance. Defaults to NINJA_AIO_MCP_API
--name The server name shown to clients. Defaults to django-ninja-aio-crud

Without the path and without the setting, the command stops with an error.

Know which tools you get

Every viewset registered with @api.viewset, or on a NinjaAIORouter added with api.add_router(...), becomes a set of tools. Nested routers count too. Tool names are the model class name in lower case, plus the operation:

Tool Input
article_list The viewset query_params filters, plus the pagination parameters (page, page_size)
article_create The create fields
article_retrieve pk
article_update pk and the update fields
article_delete pk
article_bulk_create items: a list of create objects
article_bulk_update items: a list of update objects, each with its id
article_bulk_delete ids: a list of primary keys
article_<method> Custom actions. pk for detail actions, plus the method parameters

A tool exists only when its endpoint exists. disable, missing schemas and bulk_operations apply to tools too. Actions with include_in_schema=False are skipped. See Viewsets.

When two viewsets serve the same model, their tool names start with the URL prefix: viewsets on sync/articles and async/articles give sync_articles_article_list and async_articles_article_list. Two viewsets with the same model and the same prefix raise ImproperlyConfigured.

blog/api.py
@api.viewset(model=Article, prefix="articles")
class ArticleViewSet(APIViewSet):
    bulk_operations = ["create", "delete"]

    @on("publish")
    async def publish(self, request, obj):
        obj.is_published = True
        await obj.asave()
        return {"published": obj.pk}

This gives article_list, article_create, article_retrieve, article_update, article_delete, article_bulk_create, article_bulk_delete and article_publish. An @on action takes only pk.

The tool description of an action is its description, or its summary. See Custom actions.

Endpoints of an APIView become tools too, named after the view class, the function or action name, and the HTTP method: reportsview_totals_get. Endpoints with include_in_schema=False are skipped.

api.registered_viewsets() and api.registered_views() return everything the server exposes, including what is registered on routers.

Choose what to expose

Write a small script and pass the viewsets and views you want:

mcp_server.py
import asyncio
import os

import django

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings")
django.setup()

from ninja_aio.mcp import run_mcp_server

from blog.api import api, ArticleViewSet

if __name__ == "__main__":
    asyncio.run(run_mcp_server(api, viewsets=[ArticleViewSet], views=[]))
Bash
python mcp_server.py
Argument What it does
viewsets The viewsets to expose. Defaults to api.registered_viewsets()
views The views to expose. Defaults to api.registered_views()
name The server name shown to clients
request_factory A function that builds the request passed to each tool call

This script also works without "ninja_aio" in INSTALLED_APPS.

Handle authentication

Tool calls skip the auth of your API and viewsets. Each call gets a request with an anonymous request.user and no request.auth.

Your viewset hooks and permission checks still run. Use request_factory to give tool calls an identity your checks accept:

mcp_server.py
from django.contrib.auth import get_user_model
from django.test.client import AsyncRequestFactory

agent = get_user_model().objects.get(username="mcp-agent")


def mcp_request():
    request = AsyncRequestFactory().get("/mcp/")
    request.user = agent
    request.auth = agent
    return request


if __name__ == "__main__":
    asyncio.run(run_mcp_server(api, request_factory=mcp_request))

The factory is a plain function called for each tool call. Load the user before the server starts, not inside the factory.

With PermissionViewSetMixin, a denied check returns a 403 error to the client. See Permissions.

Warning

Anyone who can start the MCP server can do what your hooks allow. Protect it like a Django shell.

Connect a client

Use absolute paths, because the client starts the server from its own folder.

claude_desktop_config.json
{
  "mcpServers": {
    "blog": {
      "command": "/path/to/project/.venv/bin/python",
      "args": ["/path/to/project/manage.py", "mcp_server"]
    }
  }
}
.vscode/mcp.json
{
  "servers": {
    "blog": {
      "type": "stdio",
      "command": "/path/to/project/.venv/bin/python",
      "args": ["/path/to/project/manage.py", "mcp_server"]
    }
  }
}

Errors like a missing object or invalid input come back to the client as a tool error with the same message as the HTTP API.

See also