MCP server for Django REST Framework — auto-discovers DRF views and exposes them as MCP tools
Project description
django-rest-mcp
A thin wrapper around the official mcp Python SDK that lets DRF ViewSets
show up as MCP tools. No new protocol, no re-implementation of the MCP server;
this package is glue.
Concretely, it does four small things:
- Walks a DRF router (or individual ViewSets) and registers one tool per
action with
mcp.server.fastmcp.FastMCP. - Converts each action's DRF serializer into a pydantic model so the tool has a typed input schema.
- Hosts the MCP streamable-HTTP transport behind a normal Django URL, passing
the authenticated
request.userthrough to your ViewSet so existingpermission_classesandget_queryset()filtering still apply. - Ships an
IsOAuth2AuthenticatedDRF permission plus RFC 8414 / RFC 9728.well-known/metadata views, for the common case of fronting it withdjango-oauth-toolkit.
It owns no domain models, no business logic, no views of its own. If you can
already hit your API with curl, this just lets an MCP client hit the same
code.
Install
pip install django-rest-mcp
Requires Python 3.12+, Django 5.1+, DRF 3.14+, mcp>=1.26, pydantic>=2.
Quickstart
Assuming you already have a DRF router with your ViewSets registered, the minimum is three lines:
# myapp/urls.py
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 as an MCP tool, with input schemas built from the matching serializer.
Production shape
Add OAuth discovery and a permission class:
# myapp/urls.py
from django.urls import path
from drf_mcp import (
DRFMCP, IsOAuth2Authenticated,
AuthorizationServerMetadataView, ProtectedResourceMetadataView,
)
from myapp.urls import router
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()),
]
For a BookViewSet registered as books, MCP clients see books_list,
books_create, books_retrieve, books_update, books_partial_update,
books_destroy.
Registering one action at a time
Skip autodiscover if you want finer control:
mcp = DRFMCP("myapp")
mcp.register_view(BookViewSet, action="list", name="list_books",
description="Return all books the current user can read.")
mcp.register_view(BookViewSet, action="create")
mcp.register_view(BookViewSet, action="retrieve")
include / exclude on autodiscover use the router basename:
mcp.autodiscover(router, include=["books"])
mcp.autodiscover(router, exclude=["internal_audit"])
How the input schema is built
For write actions (create, update, partial_update) the package reads the
serializer returned by view.get_serializer_class() (falling back to
view.serializer_class) and converts each writable field:
| DRF field | Python type |
|---|---|
CharField, EmailField, URLField, ... |
str |
IntegerField |
int |
FloatField, DecimalField |
float |
BooleanField |
bool |
Date/DateTime/Time/DurationField |
str |
ListField |
list |
DictField |
dict |
JSONField |
Any |
nested Serializer |
typed submodel |
Serializer(many=True) (ListSerializer) |
List[submodel] |
PrimaryKeyRelatedField(many=True) |
List[int] |
read-only / HiddenField |
skipped |
required=False fields become Optional[...]. Callable defaults like
default=list / default=dict are evaluated so the JSON schema has a
concrete default. Unknown field types fall back to str with a debug log.
The generated pydantic model is named <Serializer>Input, for example
BookSerializer becomes BookInput.
Multi-tenant OAuth flow
For deployments where one user belongs to multiple organisations and each
MCP integration should bind to one of them, the package ships drop-in
replacements for django-oauth-toolkit's AuthorizationView and
TokenView, plus a Dynamic Client Registration endpoint (RFC 7591).
# 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 an iterable of objects with `.id` and `.name`.
"GET_USER_ORGS": "myapp.mcp_hooks.get_user_orgs",
# Per-org Application reassignment after token exchange.
# `request.auth.application.organisation` then resolves to the org the
# user picked, instead of staying on the shared "MCP Public Client".
"GET_OR_CREATE_PER_ORG_APP": "myapp.mcp_hooks.get_or_create_per_org_app",
# Hosts allowed to register a redirect_uri via DCR. Loopback HTTP is
# always allowed; this list governs HTTPS hosts only.
"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 to render the consent page
without an org picker; omit GET_OR_CREATE_PER_ORG_APP to leave issued
tokens on the shared Application. The package ships a default
templates/oauth2_provider/authorize.html that includes the picker block;
project-level templates take precedence, so existing custom consent pages
continue to win — add an {% if organisations %} block to your version
when you want to surface the picker.
A migration (drf_mcp.0001_seed_mcp_public_client) seeds the shared
"MCP Public Client" Application row that DCR appends redirect URIs to.
It uses migrations.swappable_dependency against
OAUTH2_PROVIDER["APPLICATION_MODEL"], so swapped Application models
work. The migration declares
replaces = [("pescheck_api", "0018_seed_mcp_public_client")] for the
benefit of one specific upgrade path; remove that line in your fork if it
doesn't apply.
Install the OAuth-related dependency with the extra:
pip install 'django-rest-mcp[oauth]'
Authentication
IsOAuth2Authenticated is a thin DRF permission that accepts the request only
if request.user is authenticated and request.auth looks like an OAuth2
access token (has a .scope attribute). It plays well with
django-oauth-toolkit:
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"oauth2_provider.contrib.rest_framework.OAuth2Authentication",
],
}
Inside a tool, the authenticated request is available via:
from drf_mcp import get_current_request
request = get_current_request()
request.user # the authenticated user
request.auth # the OAuth2 access token
The package re-uses request.user when dispatching to your ViewSet, so your
existing permission_classes, object-level permissions, and get_queryset()
filtering all run as if the call came in over HTTP.
Customising the inner request
Pass a prepare_request callable if you need to attach extra state (tenant,
feature flags, trace IDs, etc.) before the ViewSet runs:
def attach_tenant(fake_request, original_request):
fake_request.tenant = original_request.tenant
mcp = DRFMCP("myapp", prepare_request=attach_tenant)
OAuth discovery metadata
The metadata views read their settings from DRF_MCP in Django settings:
DRF_MCP = {
"RESOURCE_PATH": "/mcp/",
"SCOPES": ["read", "write"],
}
The issuer, authorization_endpoint, token_endpoint, and
registration_endpoint URLs are built from the incoming request host, so the
same deployment serves correct metadata whether reached via localhost, a
tunnel, staging, or production. You do not need to set absolute URLs per env.
Tool descriptions
Descriptions shown to the model come from, in order of preference:
- The
description=kwarg toregister_view. - The docstring of the action method, if defined directly on the ViewSet
(docstrings inherited from mixins like
ListModelMixinare ignored). - The ViewSet's class docstring, prefixed with the action name.
- A generated fallback like
"List all Book".
Write action docstrings when you want to guide the model on when to use a tool:
class BookViewSet(viewsets.ModelViewSet):
"""Books in the catalogue."""
def create(self, request):
"""Create a new book.
Requires title and author_id. Use books_list first to check whether
the book already exists before creating a duplicate.
"""
...
Running the tests
uv sync
uv run pytest tests/ -v
License
MIT. See LICENSE.
Project details
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.1.5.tar.gz.
File metadata
- Download URL: django_rest_mcp-0.1.5.tar.gz
- Upload date:
- Size: 60.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e93056230219d0f37cfb535d37c4d1d01b25d5628af7d40b5f4d4de0b9d76f62
|
|
| MD5 |
1c7513213ee7d82787152409ef108375
|
|
| BLAKE2b-256 |
1cff87756abb6be631cbcbc3ed15ae3c0ba4e1f5c62ddbba0c259093c449b53e
|
Provenance
The following attestation bundles were made for django_rest_mcp-0.1.5.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.1.5.tar.gz -
Subject digest:
e93056230219d0f37cfb535d37c4d1d01b25d5628af7d40b5f4d4de0b9d76f62 - Sigstore transparency entry: 1396931030
- Sigstore integration time:
-
Permalink:
pescheckit/django-rest-mcp@b1dd939c0c8ce4c51684ccd91944358a2e96eac0 -
Branch / Tag:
refs/tags/0.1.5 - Owner: https://github.com/pescheckit
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@b1dd939c0c8ce4c51684ccd91944358a2e96eac0 -
Trigger Event:
push
-
Statement type:
File details
Details for the file django_rest_mcp-0.1.5-py3-none-any.whl.
File metadata
- Download URL: django_rest_mcp-0.1.5-py3-none-any.whl
- Upload date:
- Size: 21.2 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 |
c2b719bd9d6ef036b080a1b9264a8bc2f07738498c5b1e701826731b4cbd083f
|
|
| MD5 |
ae96a3e71af0f4a40f86dc8dc4490b6d
|
|
| BLAKE2b-256 |
68d927b6da030bb07267ca0389f281639db5e05659e1c6444e2ead3a85d50d57
|
Provenance
The following attestation bundles were made for django_rest_mcp-0.1.5-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.1.5-py3-none-any.whl -
Subject digest:
c2b719bd9d6ef036b080a1b9264a8bc2f07738498c5b1e701826731b4cbd083f - Sigstore transparency entry: 1396931036
- Sigstore integration time:
-
Permalink:
pescheckit/django-rest-mcp@b1dd939c0c8ce4c51684ccd91944358a2e96eac0 -
Branch / Tag:
refs/tags/0.1.5 - Owner: https://github.com/pescheckit
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@b1dd939c0c8ce4c51684ccd91944358a2e96eac0 -
Trigger Event:
push
-
Statement type: