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.1.5.tar.gz (60.0 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.1.5-py3-none-any.whl (21.2 kB view details)

Uploaded Python 3

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

Hashes for django_rest_mcp-0.1.5.tar.gz
Algorithm Hash digest
SHA256 e93056230219d0f37cfb535d37c4d1d01b25d5628af7d40b5f4d4de0b9d76f62
MD5 1c7513213ee7d82787152409ef108375
BLAKE2b-256 1cff87756abb6be631cbcbc3ed15ae3c0ba4e1f5c62ddbba0c259093c449b53e

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_rest_mcp-0.1.5.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.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

Hashes for django_rest_mcp-0.1.5-py3-none-any.whl
Algorithm Hash digest
SHA256 c2b719bd9d6ef036b080a1b9264a8bc2f07738498c5b1e701826731b4cbd083f
MD5 ae96a3e71af0f4a40f86dc8dc4490b6d
BLAKE2b-256 68d927b6da030bb07267ca0389f281639db5e05659e1c6444e2ead3a85d50d57

See more details on using hashes here.

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

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