Skip to main content

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:

  1. Walks a DRF router (or individual ViewSets) and registers one tool per action with mcp.server.fastmcp.FastMCP.
  2. Converts each action's DRF serializer into a pydantic model so the tool has a typed input schema.
  3. Hosts the MCP streamable-HTTP transport behind a normal Django URL, passing the authenticated request.user through to your ViewSet so existing permission_classes and get_queryset() filtering still apply.
  4. Ships an IsOAuth2Authenticated DRF permission plus RFC 8414 / RFC 9728 .well-known/ metadata views, for the common case of fronting it with django-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:

  1. The description= kwarg to register_view.
  2. The docstring of the action method, if defined directly on the ViewSet (docstrings inherited from mixins like ListModelMixin are ignored).
  3. The ViewSet's class docstring, prefixed with the action name.
  4. 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

django_rest_mcp-0.2.0.tar.gz (60.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_rest_mcp-0.2.0-py3-none-any.whl (22.6 kB view details)

Uploaded Python 3

File details

Details for the file django_rest_mcp-0.2.0.tar.gz.

File metadata

  • Download URL: django_rest_mcp-0.2.0.tar.gz
  • Upload date:
  • Size: 60.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for django_rest_mcp-0.2.0.tar.gz
Algorithm Hash digest
SHA256 c8b44e57242e01fe3f30e3dc966a0163f9cb22ddc5a669bb29dcc5c982e2fb42
MD5 836df6abb3d17e74a2b1fc075ed4c81d
BLAKE2b-256 9602b0e22fa1293f40c6b8107312e44eb9f9511741451a3d68066afecbfe269c

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_rest_mcp-0.2.0.tar.gz:

Publisher: ci.yml on pescheckit/django-rest-mcp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file django_rest_mcp-0.2.0-py3-none-any.whl.

File metadata

File hashes

Hashes for django_rest_mcp-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 aa8c640c203853a8b3926c57389ecebd00b5bcbe6cf6bede61bf6e61f6ac0087
MD5 861e888466c4523efed33efc45e59eaa
BLAKE2b-256 cb60a049f8339f008969c64a54190e6ee66a5b3e586eb324184d87c3bd3b6582

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_rest_mcp-0.2.0-py3-none-any.whl:

Publisher: ci.yml on pescheckit/django-rest-mcp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page