MCP server for Django REST Framework — auto-discovers DRF views and exposes them as MCP tools
Project description
django-rest-mcp
Turn the Django REST Framework API you already have into MCP tools, without rewriting a thing.
Why
You have a DRF API. You want an MCP client (Claude, an agent, your own tooling) to call it.
The usual way is to hand-write an MCP tool for every endpoint, re-describe your serializers as tool inputs, and re-check your permissions in a second place that quietly drifts from the real API.
This package skips that. Point it at your existing DRF router and every ViewSet
action becomes an MCP tool, input types pulled straight from your serializers,
every call running through your real permissions and querysets. One source of
truth: your API. If curl can hit it, an MCP client can too.
It owns no models, no business logic, no views of its own. It is glue.
What you get
Register a BookViewSet under books and an MCP client sees six tools:
books_list books_retrieve books_create books_update books_partial_update books_destroy
For each one:
- Typed inputs, generated from the action's serializer, so the model knows exactly what fields to send.
- Runs as the authenticated user, so your
permission_classes, OAuth scopes, object-level permissions, andget_queryset()filtering all apply unchanged. - Returns whatever your API already returns.
Install
pip install django-rest-mcp
Python 3.12+, Django 5.1+, DRF 3.14+, mcp>=1.26, pydantic>=2.
How
You already have a router. Three lines:
# myapp/urls.py
from django.urls import path
from drf_mcp import DRFMCP
from myapp.urls import router # your existing DefaultRouter
mcp = DRFMCP("myapp")
mcp.autodiscover(router)
urlpatterns = [path("mcp/", mcp.as_view()), ...]
That exposes every standard action on every registered ViewSet. That's it.
Pick what to expose
mcp.autodiscover(router, include=["books"]) # only these basenames
mcp.autodiscover(router, exclude=["internal"]) # all but these
# or register one action at a time, with a custom name + description:
mcp.register_view(BookViewSet, action="list", name="list_books",
description="Return all books the current user can read.")
Add OAuth (production shape)
Front it with django-oauth-toolkit plus a permission class, and serve the
.well-known/ discovery endpoints so MCP clients can find your auth server:
from drf_mcp import (
DRFMCP, IsOAuth2Authenticated,
AuthorizationServerMetadataView, ProtectedResourceMetadataView,
)
mcp = DRFMCP("myapp")
mcp.autodiscover(router)
urlpatterns = [
path("mcp/", mcp.as_view(permission_classes=[IsOAuth2Authenticated]), name="mcp"),
path(".well-known/oauth-authorization-server", AuthorizationServerMetadataView.as_view()),
path(".well-known/oauth-protected-resource", ProtectedResourceMetadataView.as_view()),
]
pip install 'django-rest-mcp[oauth]'
Authentication
Each tool runs as the request's authenticated user. That works whether you authenticate with OAuth2 tokens, session/cookie auth, or a custom backend, so your permissions and querysets behave exactly as they do over HTTP.
IsOAuth2Authenticated accepts a request only if request.user is
authenticated and request.auth looks like an OAuth2 token. Pair it with:
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"oauth2_provider.contrib.rest_framework.OAuth2Authentication",
],
}
Reach the live request from inside a tool:
from drf_mcp import get_current_request
request = get_current_request()
request.user # the authenticated user
request.auth # the OAuth2 access token
Tool inputs
For write actions (create, update, partial_update) the input schema is
built from the serializer (get_serializer_class(), falling back to
serializer_class). Each writable field maps to a Python type:
| DRF field | Python type |
|---|---|
CharField, EmailField, URLField, ... |
str |
IntegerField |
int |
FloatField, DecimalField |
float |
BooleanField |
bool |
ListField |
list |
DictField |
dict |
JSONField |
Any |
nested Serializer |
typed submodel |
Serializer(many=True) |
List[submodel] |
required=False fields become Optional[...]; read-only and hidden fields are
skipped. The generated model is named <Serializer>Input.
Tool descriptions
What the model reads as a tool's description comes from, in order:
- The
description=you pass toregister_view. - The action method's docstring (only if defined on the ViewSet itself, not inherited from a mixin).
- The ViewSet's class docstring.
- A generated fallback like
"List all Book".
Write docstrings to steer the model on when to use a tool:
class BookViewSet(viewsets.ModelViewSet):
"""Books in the catalogue."""
def create(self, request):
"""Create a book. Call books_list first to avoid duplicates."""
...
Multi-tenant OAuth
For "one user belongs to many orgs, and each MCP connection binds to exactly one of them", the package ships drop-in replacements for django-oauth-toolkit's authorize and token views, a Dynamic Client Registration endpoint (RFC 7591), and a consent page with an org picker.
# settings.py
INSTALLED_APPS = [..., "oauth2_provider", "drf_mcp"]
DRF_MCP = {
"RESOURCE_PATH": "/api/mcp/",
"SCOPES": ["read:api", "create:api"],
# Org picker on the consent page. Returns objects with `.id` and `.name`.
"GET_USER_ORGS": "myapp.mcp_hooks.get_user_orgs",
# Bind the issued token's Application to the org the user picked, so
# `request.auth.application.organisation` resolves to that org.
"GET_OR_CREATE_PER_ORG_APP": "myapp.mcp_hooks.get_or_create_per_org_app",
# HTTPS hosts allowed to register a redirect_uri via DCR (loopback HTTP is
# always allowed).
"REGISTRATION_HTTPS_HOST_SUFFIXES": ["claude.ai", "anthropic.com"],
}
# urls.py
from drf_mcp import (
DRFMCP, IsOAuth2Authenticated, MCPView,
MCPAuthorizationView, MCPTokenView, StaticClientRegistrationView,
AuthorizationServerMetadataView, ProtectedResourceMetadataView,
)
mcp = DRFMCP("myapi")
mcp.autodiscover(router)
class MyMCPView(MCPView):
mcp_server = mcp
permission_classes = [IsOAuth2Authenticated]
urlpatterns = [
path("o/authorize/", MCPAuthorizationView.as_view(), name="authorize"),
path("o/token/", MCPTokenView.as_view(), name="token"),
path("mcp/", MyMCPView.as_view(), name="mcp"),
path("mcp/register/", StaticClientRegistrationView.as_view()),
path(".well-known/oauth-authorization-server", AuthorizationServerMetadataView.as_view()),
path(".well-known/oauth-protected-resource", ProtectedResourceMetadataView.as_view()),
]
Both hooks are optional: omit GET_USER_ORGS for a consent page without an org
picker; omit GET_OR_CREATE_PER_ORG_APP to leave tokens on the shared
Application. The .well-known/ metadata URLs are built from the incoming
request host, so one deployment serves correct values via localhost, a tunnel,
staging, or production with no per-env config.
Customising the request
Attach extra state (tenant, feature flags, trace ids) before the ViewSet runs:
def attach_tenant(request, original_request):
request.tenant = original_request.tenant
mcp = DRFMCP("myapp", prepare_request=attach_tenant)
Running the tests
uv sync
uv run pytest
License
MIT. See LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_rest_mcp-0.2.4.tar.gz.
File metadata
- Download URL: django_rest_mcp-0.2.4.tar.gz
- Upload date:
- Size: 60.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
714705a1d87a1bcc1916f91324af0f5376e44e6e4e6011a11055a3fc4b637c32
|
|
| MD5 |
9ccb29e5a1a659ccc277049ec7884089
|
|
| BLAKE2b-256 |
2fa29a5a54bdbd5fd1436ed62a41fe82dd2ab0a05d00bfa3e1e13864da97a0bc
|
Provenance
The following attestation bundles were made for django_rest_mcp-0.2.4.tar.gz:
Publisher:
ci.yml on pescheckit/django-rest-mcp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_rest_mcp-0.2.4.tar.gz -
Subject digest:
714705a1d87a1bcc1916f91324af0f5376e44e6e4e6011a11055a3fc4b637c32 - Sigstore transparency entry: 1936662840
- Sigstore integration time:
-
Permalink:
pescheckit/django-rest-mcp@041d6f7d64af83209911d09680226dc38c4d61b5 -
Branch / Tag:
refs/tags/0.2.4 - Owner: https://github.com/pescheckit
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@041d6f7d64af83209911d09680226dc38c4d61b5 -
Trigger Event:
push
-
Statement type:
File details
Details for the file django_rest_mcp-0.2.4-py3-none-any.whl.
File metadata
- Download URL: django_rest_mcp-0.2.4-py3-none-any.whl
- Upload date:
- Size: 22.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4f2b296c376e023c95b6f018f59fded92b092853eb1a9dedf506463a120dd4f9
|
|
| MD5 |
3585e856948152fee1052f75bda6507d
|
|
| BLAKE2b-256 |
35d13942c6ba8f6479d162ee66c2b6b1a8f1357f26976592aca28eafa6931106
|
Provenance
The following attestation bundles were made for django_rest_mcp-0.2.4-py3-none-any.whl:
Publisher:
ci.yml on pescheckit/django-rest-mcp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_rest_mcp-0.2.4-py3-none-any.whl -
Subject digest:
4f2b296c376e023c95b6f018f59fded92b092853eb1a9dedf506463a120dd4f9 - Sigstore transparency entry: 1936663018
- Sigstore integration time:
-
Permalink:
pescheckit/django-rest-mcp@041d6f7d64af83209911d09680226dc38c4d61b5 -
Branch / Tag:
refs/tags/0.2.4 - Owner: https://github.com/pescheckit
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@041d6f7d64af83209911d09680226dc38c4d61b5 -
Trigger Event:
push
-
Statement type: