# django-drf-mcp

Add [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) to any Django REST Framework project. Auto-discovers all DRF endpoints and exposes them as MCP tools — zero boilerplate.

## Features

- **Zero config** — add `"django_drf_mcp"` to `INSTALLED_APPS`, include the URLs, done
- **Auto-discovery** — every DRF ViewSet/APIView becomes an MCP tool automatically
- **Endpoint filtering** — include or exclude specific endpoints with `METHOD:PATH` glob patterns
- **Multiple transports** — STDIO, SSE, Streamable HTTP, or embedded Django view
- **Authentication** — pass auth headers (Token, JWT, Basic, API key) to MCP tool calls via `HEADERS` setting
- **DRF-integrated MCP view** — the `/mcp/` endpoint is a DRF `APIView`, inheriting authentication and permissions from `REST_FRAMEWORK` settings
- **MCP Docs UI** — interactive Swagger-style docs for your MCP tools at `/mcp/docs`
- **DRF Swagger UI** — optional built-in Swagger UI and OpenAPI schema endpoints
- **Fully configurable** — control every feature via a single `DJANGO_MCP` settings dict
- **Auto-configures drf-spectacular** — no manual setup needed

## Installation

```bash
pip install django-drf-mcp
```

### Dependencies (installed automatically)

- Django >= 4.0
- Django REST Framework
- [drf-spectacular](https://github.com/tfranzel/drf-spectacular)
- [FastMCP](https://github.com/jlowin/fastmcp) >= 2.0.0
- [fastmcp-docs](https://github.com/orco82/fastmcp-docs)
- httpx

## Quick Start

### 1. Add to INSTALLED_APPS

```python
# settings.py
INSTALLED_APPS = [
    ...
    "rest_framework",
    "django_drf_mcp",
]
```

> `drf-spectacular` is auto-injected — you don't need to add it yourself.

### 2. Include URLs

```python
# urls.py
from django.urls import path, include

urlpatterns = [
    ...
    path("", include("django_drf_mcp.urls")),
]
```

That's it. The MCP endpoint is now live at `/mcp/`.

### 3. Enable MCP Docs UI (requires ASGI)

For interactive documentation of your MCP tools, replace your `asgi.py`:

```python
# asgi.py
import os
import django

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

from django_drf_mcp.asgi import get_asgi_application

application = get_asgi_application()
```

Run with an ASGI server:

```bash
uvicorn myproject.asgi:application --host 0.0.0.0 --port 8000
```

MCP Docs will be available at `/mcp/docs`.

## Transports

### STDIO (Claude Code / Claude Desktop)

```bash
python manage.py runmcp --transport stdio
```

### SSE

```bash
python manage.py runmcp --transport sse --host 0.0.0.0 --port 8001
```

### Streamable HTTP

```bash
python manage.py runmcp --transport streamable-http --host 0.0.0.0 --port 8001
```

### Embedded Django View

The MCP protocol is also served at `/mcp/` as a DRF `APIView` — no separate process needed. Supports `GET` (health check) and `POST` (JSON-RPC). Inherits authentication and permission classes from `REST_FRAMEWORK` settings.

## MCP Client Configuration

### STDIO

```json
{
  "mcpServers": {
    "my-app": {
      "command": "python",
      "args": ["manage.py", "runmcp", "--transport", "stdio"]
    }
  }
}
```

With a remote Django server:

```json
{
  "mcpServers": {
    "my-app": {
      "command": "python",
      "args": ["manage.py", "runmcp", "--transport", "stdio", "--base-url", "https://my-app.example.com"]
    }
  }
}
```

### Streamable HTTP — Embedded (same port as Django)

The `/mcp/` Django view serves MCP on the same port as your app (e.g. 8000):

```json
{
  "mcpServers": {
    "my-app": {
      "type": "streamable-http",
      "url": "http://localhost:8000/mcp/"
    }
  }
}
```

With authentication:

```json
{
  "mcpServers": {
    "my-app": {
      "type": "streamable-http",
      "url": "http://localhost:8000/mcp/",
      "headers": {
        "Authorization": "Token abc123..."
      }
    }
  }
}
```

### Streamable HTTP — Standalone (separate port)

`runmcp --transport streamable-http` starts a standalone FastMCP server (e.g. port 8001) that proxies tool calls to Django on port 8000:

```json
{
  "mcpServers": {
    "my-app": {
      "type": "streamable-http",
      "url": "http://localhost:8001/mcp"
    }
  }
}
```

With authentication:

```json
{
  "mcpServers": {
    "my-app": {
      "type": "streamable-http",
      "url": "http://localhost:8001/mcp",
      "headers": {
        "Authorization": "Token abc123..."
      }
    }
  }
}
```

### SSE — Standalone (separate port)

`runmcp --transport sse` starts a standalone FastMCP server (e.g. port 8001) that proxies tool calls to Django on port 8000:

```json
{
  "mcpServers": {
    "my-app": {
      "type": "sse",
      "url": "http://localhost:8001/sse"
    }
  }
}
```

With authentication:

```json
{
  "mcpServers": {
    "my-app": {
      "type": "sse",
      "url": "http://localhost:8001/sse",
      "headers": {
        "Authorization": "Token abc123..."
      }
    }
  }
}
```

> **Note:** The client `headers` authenticate the MCP client to the MCP endpoint. The server-side `DJANGO_MCP["HEADERS"]` setting (see [Authentication](#authentication)) authenticates MCP tool calls to the Django API. When using DRF authentication, configure **both** for full security.

## Settings

All configuration is optional. Add a `DJANGO_MCP` dict to your `settings.py`:

```python
DJANGO_MCP = {
    # --- Server ---
    "NAME": "django-drf-mcp",              # MCP server name
    "BASE_URL": "http://localhost:8000",    # Django server URL
    "MCP_PATH": "/mcp/",                   # MCP endpoint path

    # --- Endpoint Filtering ---
    "INCLUDE": [],                          # Only expose matching METHOD:PATH patterns
    "EXCLUDE": [],                          # Remove matching METHOD:PATH patterns

    # --- Authentication ---
    "HEADERS": {},                          # HTTP headers sent with every MCP tool call
    "INTERNAL_TOKEN": "",                   # Shared secret for MCP internal requests (optional)

    # --- MCP Docs UI ---
    "MCP_DOCS_ENABLED": True,              # Enable/disable /mcp/docs
    "MCP_DOCS_TITLE": None,                # Docs page title (defaults to NAME)
    "MCP_DOCS_DESCRIPTION": None,          # Docs description (defaults to OpenAPI description)
    "MCP_DOCS_VERSION": None,              # Docs version (defaults to OpenAPI version)
    "MCP_DOCS_EMOJI": None,                # Emoji shown before the title
    "MCP_DOCS_LINKS": [],                  # Extra links: [{"text": "...", "url": "..."}]
    "MCP_DOCS_ENABLE_CORS": True,          # Enable CORS on docs routes
    "MCP_DOCS_VERBOSE": True,              # Show tools load during server startup

    # --- DRF Swagger / Schema ---
    "SWAGGER_ENABLED": False,              # Enable Swagger UI + OpenAPI schema endpoints
    "SCHEMA_PATH": "/api/schema/",         # Schema endpoint path
    "SWAGGER_PATH": "/api/docs/",          # Swagger UI path
}
```

### Settings Reference

| Setting | Type | Default | Description |
|---|---|---|---|
| `NAME` | `str` | `"django-drf-mcp"` | MCP server name, shown in health check and docs |
| `BASE_URL` | `str` | `"http://localhost:8000"` | URL of the running Django server. Used as the base URL for proxying MCP tool calls and injected into the OpenAPI schema `servers` list |
| `MCP_PATH` | `str` | `"/mcp/"` | Path where the MCP endpoint is mounted |
| `INCLUDE` | `list` | `[]` | Only expose endpoints matching these `METHOD:PATH` glob patterns. Empty = include all |
| `EXCLUDE` | `list` | `[]` | Remove endpoints matching these `METHOD:PATH` glob patterns. Applied after `INCLUDE` |
| `HEADERS` | `dict` | `{}` | HTTP headers sent with every MCP tool call (e.g. `{"Authorization": "Token ..."}`) |
| `INTERNAL_TOKEN` | `str` | `""` | Shared secret for MCP internal requests. If empty, a per-process auto-generated token is used. Set this for multi-worker or standalone `runmcp` deployments |
| `MCP_DOCS_ENABLED` | `bool` | `True` | Enable the MCP Docs UI (Starlette-based, requires ASGI) |
| `MCP_DOCS_TITLE` | `str\|None` | `None` | Docs page title. Falls back to `NAME` |
| `MCP_DOCS_DESCRIPTION` | `str\|None` | `None` | Docs description. Falls back to the OpenAPI schema description |
| `MCP_DOCS_VERSION` | `str\|None` | `None` | Docs version string. Falls back to the OpenAPI schema version |
| `MCP_DOCS_EMOJI` | `str\|None` | `None` | Emoji displayed before the docs title |
| `MCP_DOCS_LINKS` | `list` | `[]` | Extra links shown in docs. Each item: `{"text": "...", "url": "..."}` |
| `MCP_DOCS_ENABLE_CORS` | `bool` | `True` | Enable CORS on the MCP Docs routes |
| `MCP_DOCS_VERBOSE` | `bool` | `True` | Show tools load during server startup |
| `SWAGGER_ENABLED` | `bool` | `False` | Enable the DRF Swagger UI and OpenAPI schema endpoints |
| `SCHEMA_PATH` | `str` | `"/api/schema/"` | Path for the OpenAPI schema endpoint |
| `SWAGGER_PATH` | `str` | `"/api/docs/"` | Path for the Swagger UI endpoint |

### Example: Full Configuration

```python
DJANGO_MCP = {
    "NAME": "products-api",
    "BASE_URL": "http://localhost:8000",
    "MCP_PATH": "/mcp/",
    "HEADERS": {
        "Authorization": "Token abc123...",
    },
    "MCP_DOCS_ENABLED": True,
    "MCP_DOCS_TITLE": "Products MCP Tools",
    "MCP_DOCS_DESCRIPTION": "MCP tools for the Products API",
    "MCP_DOCS_EMOJI": "\U0001f6d2",
    "MCP_DOCS_LINKS": [
        {"text": "Admin", "url": "/admin/"},
        {"text": "GitHub", "url": "https://github.com/myorg/myrepo"},
    ],
    "MCP_DOCS_ENABLE_CORS": True,
    "MCP_DOCS_VERBOSE": False,
    "SWAGGER_ENABLED": True,
}
```

### Example: Minimal (disable docs)

```python
DJANGO_MCP = {
    "NAME": "my-api",
    "BASE_URL": "https://my-api.example.com",
    "MCP_DOCS_ENABLED": False,
}
```

## Endpoint Filtering

Control which DRF endpoints are exposed as MCP tools using `INCLUDE` and `EXCLUDE` patterns.

Patterns use the format `"METHOD:PATH"` with glob-style wildcards (`*`):

```python
# Only expose GET endpoints under /api/
DJANGO_MCP = {
    "INCLUDE": ["GET:/api/*"],
}

# Expose everything except DELETE operations
DJANGO_MCP = {
    "EXCLUDE": ["DELETE:*"],
}

# Expose /api/ endpoints, but exclude DELETE on users
DJANGO_MCP = {
    "INCLUDE": ["*:/api/*"],
    "EXCLUDE": ["DELETE:/api/users/*"],
}
```

- `METHOD` — HTTP method (`GET`, `POST`, `PUT`, `PATCH`, `DELETE`) or `*` for any method
- `PATH` — URL path with `*` wildcard (e.g. `/api/*`, `/api/users/{id}/`)
- Method matching is case-insensitive
- If `INCLUDE` is empty (default), all endpoints are included
- If `EXCLUDE` is empty (default), nothing is excluded
- When both are set, `INCLUDE` is applied first, then `EXCLUDE` removes from the result

## Authentication

If your DRF endpoints require authentication, there are two layers to configure:

### 1. Server-side: MCP tool calls to the Django API

Configure the `HEADERS` setting so MCP tool HTTP calls include credentials:

```python
# DRF TokenAuthentication
DJANGO_MCP = {
    "HEADERS": {
        "Authorization": "Token abc123...",
    },
}

# JWT (e.g. SimpleJWT)
DJANGO_MCP = {
    "HEADERS": {
        "Authorization": "Bearer eyJ...",
    },
}

# Basic Auth
DJANGO_MCP = {
    "HEADERS": {
        "Authorization": "Basic dXNlcjpwYXNz",
    },
}

# Custom API key header
DJANGO_MCP = {
    "HEADERS": {
        "X-API-Key": "my-secret-key",
    },
}
```

These headers are passed to the underlying `httpx.AsyncClient` and included in every HTTP request made by MCP tools to your Django API.

### SessionAuthentication & MCP Internal Requests

`SessionAuthentication` uses cookies and CSRF tokens — neither of which the MCP internal proxy has. If your views use `SessionAuthentication`, MCP tool calls will get **403 Forbidden**.

`django-drf-mcp` solves this with a built-in shared secret. The library automatically generates a per-process cryptographic token and injects it into every internal MCP proxy request via the `X-MCP-Internal-Token` header.

To use this feature, you **must** add the `IsMCPInternalRequest` permission class to any view you want MCP tools to access. Without it, the token header is ignored and normal authentication applies.

Add it to your views using the `|` (OR) operator:

```python
from rest_framework.permissions import IsAuthenticated
from django_drf_mcp.permissions import IsMCPInternalRequest

class MyViewSet(viewsets.ModelViewSet):
    permission_classes = [IsAuthenticated | IsMCPInternalRequest]
```

This means:
- **Browser users** — must be authenticated via session (or any other auth method)
- **MCP tool calls** — bypass session auth via the auto-injected secret token

The token is 256-bit cryptographically random and regenerated on every server restart. External clients cannot guess it.

#### Multi-worker & standalone `runmcp` deployments

The auto-generated token is per-process. If the MCP proxy and your DRF views run in **different processes** (e.g., `runmcp` standalone mode, or gunicorn with multiple workers behind a load balancer), set a shared `INTERNAL_TOKEN`:

```python
DJANGO_MCP = {
    "INTERNAL_TOKEN": "your-shared-secret-here",  # Use a strong random value
}
```

When `INTERNAL_TOKEN` is set, it is used instead of the auto-generated per-process token. All workers and the `runmcp` process must share the same value.

> **Tip:** Generate a strong token with `python -c "import secrets; print(secrets.token_urlsafe(32))"`.

### 2. Client-side: MCP client to the `/mcp/` endpoint

The embedded `/mcp/` view is a DRF `APIView` and inherits `DEFAULT_AUTHENTICATION_CLASSES` and `DEFAULT_PERMISSION_CLASSES` from `REST_FRAMEWORK` settings. If your DRF config requires authentication globally, MCP clients must also send auth headers:

```json
{
  "mcpServers": {
    "my-app": {
      "type": "streamable-http",
      "url": "http://localhost:8000/mcp/",
      "headers": {
        "Authorization": "Token abc123..."
      }
    }
  }
}
```

### Full authentication example

```python
# settings.py
REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework.authentication.TokenAuthentication",
    ],
    "DEFAULT_PERMISSION_CLASSES": [
        "rest_framework.permissions.IsAuthenticated",
    ],
}

DJANGO_MCP = {
    "NAME": "my-api",
    "HEADERS": {
        "Authorization": "Token abc123...",
    },
}
```

```json
// .mcp.json (MCP client)
{
  "mcpServers": {
    "my-api": {
      "type": "streamable-http",
      "url": "http://localhost:8000/mcp/",
      "headers": {
        "Authorization": "Token abc123..."
      }
    }
  }
}
```

## Endpoints

| Path | Description | Controlled by |
|---|---|---|
| `/mcp/` | MCP protocol endpoint (JSON-RPC) | `MCP_PATH` |
| `/mcp/docs` | MCP Docs UI | `MCP_DOCS_ENABLED` |
| `/mcp/openapi.json` | MCP OpenAPI schema | `MCP_DOCS_ENABLED` |
| `/mcp/api/tools` | MCP tools JSON API (list) | `MCP_DOCS_ENABLED` |
| `/mcp/api/tools/{tool_name}` | MCP tool detail JSON API | `MCP_DOCS_ENABLED` |
| `/mcp/favicon` | MCP Docs favicon | `MCP_DOCS_ENABLED` |
| `/api/schema/` | DRF OpenAPI schema (YAML) | `SWAGGER_ENABLED` |
| `/api/docs/` | DRF Swagger UI | `SWAGGER_ENABLED` |

## How It Works

1. `django_drf_mcp` auto-injects `drf-spectacular` into `INSTALLED_APPS` and configures `DEFAULT_SCHEMA_CLASS`
2. On startup, it generates an OpenAPI schema from all registered DRF views
3. The base URL is injected into the OpenAPI schema `servers` list
4. FastMCP converts each API endpoint into an MCP tool via `OpenAPIProvider`
5. An `httpx.AsyncClient` is created with the configured `BASE_URL` and `HEADERS` for proxying tool calls
6. Tools are served via STDIO, SSE, Streamable HTTP, or the embedded Django view
7. When running under ASGI, `fastmcp-docs` provides interactive documentation at `/mcp/docs`

## Management Command

```
python manage.py runmcp [options]
```

| Option | Default | Description |
|---|---|---|
| `--transport` | `stdio` | Transport type: `stdio`, `sse`, `streamable-http` |
| `--host` | `0.0.0.0` | Bind host (for `sse` / `streamable-http`) |
| `--port` | `8001` | Bind port (for `sse` / `streamable-http`) |
| `--base-url` | from settings | Override `BASE_URL` for proxying API calls |

## Requirements

- Python >= 3.10
- Django >= 4.0
- Django REST Framework
