Install
$ agentstack add mcp-zacharypodbela-django-rest-framework-mcp ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Django REST Framework MCP
[](https://badge.fury.io/py/django-rest-framework-mcp) [](https://pypi.org/project/django-rest-framework-mcp/)
django-rest-framework-mcp allows you to spin up MCP servers that expose your Django Rest Framework APIs as MCP tools with just a few lines of code.
Supercharge your admin workflows (and make developing Admin interfaces a thing of the past):
- "Deactivate josh@gmail.com's account" → Actually deactivates it
- "Extend jack@teams.com's free trial by 1 week" → Updates their plan
- "How many new users joined week-over-week?" → Returns real data → LLMs can quickly create graphs (no more complex FE graphing libraries needed)
Or transform your traditional, boring SaaS UX with conversational interactions:
- Old way: User Clicks "Manage", Clicks "My Posts", Clicks a post from the list, Clicks "Edit", changes the title, Clicks "Save"
- With Django Rest Framework MCP: "Can you rename my post from 'Beginners Guide to Django' to 'Django 101'?"
Quick Start
- Install the package:
pip install django-rest-framework-mcp
- Add to your
INSTALLED_APPS:
INSTALLED_APPS = [
# ... your other apps
'djangorestframework_mcp',
]
- Add the MCP endpoint to your
urls.py:
urlpatterns = [
# ... your other URL patterns
path('mcp/', include('djangorestframework_mcp.urls')),
]
- Transform any DRF ViewSet into MCP tools with a single decorator:
from djangorestframework_mcp.decorators import mcp_viewset
@mcp_viewset()
class CustomerViewSet(ModelViewSet):
queryset = Customer.objects.all()
serializer_class = CustomerSerializer
When @mcp_viewset is applied to a ViewSet class that inherits from GenericViewSet (such as ModelViewSet or ReadOnlyModelViewSet), any of the following methods that are defined will be automatically exposed as MCP tools:
list-> List customers withcustomers_listtool.retrieve-> Retrieve a customer withcustomers_retrievetool.create-> Create new customers withcustomers_createtool.update-> Update customers withcustomers_updatetool. (All fields must be passed in)partial_update-> Update customers withcustomers_partial_updatetool. (A subset of fields can be passed in)destroy-> Delete customers withcustomers_destroytool.
For each tool the library automatically:
- Generates tool schemas from your DRF serializers
- Preserves your existing permissions, authentication, and filtering
- Returns context rich error messages to guide LLMs
(See: Custom Actions below for more info on how to expose additional endpoints you created using the @action decorator as tools).
- Connect any MCP client to
http://localhost:8000/mcp/and try it out!
Important Differences between MCP Requests and API Requests
MCP requests do not go through the full DRF request lifecycle.
View lifecycle methods that will be called:
perform_authentication(request)- Called unlessBYPASS_VIEWSET_AUTHENTICATION = Truecheck_permissions(request)- Called unlessBYPASS_VIEWSET_PERMISSIONS = Truecheck_throttles(request)- Always called (no bypass option)- **
determine_version(request, \*args, **kwargs)\\ - Always called to set request versioning
View lifecycle methods that won't be called:
dispatch(request, args, kwargs)- will not be called since we don't use HTTP method+path routing.initialize_request(request, args, kwargs)will not be called. We do create arest_framework.requests.Requestfrom thedjango.http.HttpRequest, but not using this handler.initial(request, args, kwargs)- will not be called.perform_content_negotiation(request)- will not be called since MCP/JSON-RPC dictates the input and output format.finalize_response(request, response, args, kwargs)will not be called.handle_exception(exc)will not be called in the event of an exception\*.
Additional considerations:
- The
requestobject will be missing API-specific properties likemethod,path, orpath_infosince it wasn't created from an actual HTTP API call - Content negotiation is bypassed since MCP always uses JSON
Connecting a STDIO MCP Client
Right now, the MCP server is only open to HTTP transport. To support stdio transport, you'll need a bridge. We recommend mcp-remote.
Example: Connect to Claude Desktop
Follow these instructions to use mcp-remote to connect to Claude Desktop:
- Install mcp-remote:
npm install -g mcp-remote
- Open Claude MCP Desktop Configuration by going to Settings > Developer > Edit Config and add your server configuration:
{
"mcpServers": {
"my-django-mcp": {
"command": "node",
"args": [
"path/to/mcp-remote",
"http://localhost:8000/mcp/",
"--transport",
"http-only"
]
}
}
}
- Restart Claude Desktop and test your tools
Development Tip: LLMs can be surprisingly effective at “manually” testing your MCP tools and uncovering bugs. In Claude Desktop, try a prompt like: "I'm developing a new set of MCP tools locally. Please extensively test them — including coming up with complex edge cases to try - and look for unexpected behavior or bugs. Make at least 30 tool calls."
Advanced Configuration
Authentication
On the subject of Authentication, the Model Context Protocol states:
- Implementations using an HTTP-based transport SHOULD conform to the OAuth specification detailed here.
- Implementations using an STDIO transport SHOULD NOT follow the above specification, and instead retrieve credentials from the environment.
- Additionally, clients and servers MAY negotiate their own custom authentication and authorization strategies.
Our library enables you to leverage DRF's authentication framework on both the MCP endpoint level and individual ViewSet level, giving you flexibility in how you secure your MCP tools.
Using Existing API Authentication on ViewSets
If your ViewSet specifies authentication_classes and/or permission_classes, MCP client requests will be required to authenticate and pass permission checks using the same methods as your normal API requests:
from rest_framework.authentication import TokenAuthentication
from rest_framework.permissions import IsAuthenticated
@mcp_viewset()
class CustomerViewSet(viewsets.ModelViewSet):
queryset = Customer.objects.all()
serializer_class = CustomerSerializer
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
MCP clients then authenticate via standard HTTP headers:
# HTTP headers
POST /mcp/ HTTP/1.1
Authorization: Token your-token-here
# HTTP body
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "list_customers",
"arguments": {}
},
"id": 1
}
If you are using custom BasePermission classes in your ViewSets, we strongly encourage setting the message property to add more specifics on why permission was denied to the error message as this will be passed back to the LLM in the response and help it determine the right next course of action.
Defining Authentication and Permissions on the MCP Endpoint
You can add authentication to requests made to the /mcp endpoint by subclassing MCPView and setting the authentication_classes property, just as you would for an APIView. You can add permissions to requests made to the /mcp endpoint by overriding and implementing the has_mcp_permission method. (The default implementation of has_mcp_permission() returns True, allowing all requests.)
from djangorestframework_mcp.views import MCPView
from rest_framework.authentication import TokenAuthentication
class AuthenticatedMCPView(MCPView):
authentication_classes = [TokenAuthentication]
def has_mcp_permission(self, request):
"""Override this method to implement custom permission logic."""
return request.user.is_authenticated
# Then in urls.py
urlpatterns = [
path('mcp/', AuthenticatedMCPView.as_view()),
]
The has_mcp_permission(self, request) method is called after authentication, so user and auth will be both set allowing you to implement any custom authorization logic:
class RestrictedMCPView(MCPView):
authentication_classes = [TokenAuthentication]
def has_mcp_permission(self, request):
# Only allow users in the 'mcp_users' group
return (
request.user.is_authenticated
and request.user.groups.filter(name='mcp_users').exists()
)
Just as with DRF, if you have authentication classes but no permission requirements, unauthenticated requests are allowed to continue and request.user will be an AnonymousUser.
Bypassing ViewSet Authentication
In cases where you want to apply different authentication methods and/or permissions rules for MCP clients versus regular API clients, you can bypass ViewSet-level authentication and/or permissions:
# settings.py
DJANGORESTFRAMEWORK_MCP = {
'BYPASS_VIEWSET_AUTHENTICATION': True, # Skip authentication on ViewSets
'BYPASS_VIEWSET_PERMISSIONS': True, # Skip permissions on ViewSets
}
Authentication Error Handling
When authentication fails, the default behavior is for the library to return proper HTTP status codes (401/403) and WWW-Authenticate headers in compliance with both HTTP and MCP specifications. The JSON-RPC response body also includes this information as human-readable error messages so it can be leveraged by LLMs.
Example response:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Token
Content-Type: application/json
{
"jsonrpc": "2.0",
"result": {
"content": [
{
"type": "text",
"text": "Unauthorized: Authentication credentials were not provided. (WWW-Authenticate: Token)"
}
],
"isError": true
},
"id": 1
}
While the MCP protocol specification clearly states that HTTP 401/403 status codes should be returned for authentication and permission errors, many MCP clients don't properly conform to this specification and are unable to handle these cases. (For example, MCP's own Typescript SDK doesn't currently handle 403 errors. See this Github Issue.) To better support compatibility with a wide variety of clients while the community and standards continue to evolve, you can enable the RETURN_200_FOR_ERRORS to return HTTP 200 status codes even for authentication/permission failures. This setting only affects the HTTP status code returned - the JSON-RPC error format remains the same, ensuring LLMs can still understand and react to failures.
# settings.py
DJANGORESTFRAMEWORK_MCP = {
'RETURN_200_FOR_ERRORS': True, # Default: False
}
Authenticating STDIO Transport Using MCP-Remote
When using STDIO transport through MCP-Remote, authentication credentials to be passed as HTTP headers can be set as environment variables like this:
{
"mcpServers": {
"my-django-mcp": {
"command": "node",
"args": [
"path/to/mcp-remote",
"http://localhost:8000/mcp/",
"--transport",
"http-only",
"--header",
"Authorization:${AUTH_HEADER}" // Some setups don't escape whitespaces of args, so we recommend setting the entire header as an env var
],
"env": {
"AUTH_HEADER": "your-header-here"
}
}
}
}
As of writing this, MCP-remote does not properly handle 403 response and always assumes the authentication framework is OAuth when receiving 401 responses, so you'll also need to enable RETURN_200_FOR_ERRORS in your settings file.
Custom Actions
Custom actions, created with the @action decorator, require explicit schema definition since there aren't standard input defaults like with CRUD endpoints. To create a tool from a custom action, apply the @mcp_tool decorator and pass in an input_serializer:
from djangorestframework_mcp.decorators import mcp_viewset, mcp_tool
class GenerateInputSerializer(serializers.Serializer):
user_prompt = serializers.CharField(help_text="The prompt to send to the LLM")
@mcp_viewset()
class ContentViewSet(viewsets.ViewSet):
@mcp_tool(input_serializer=GenerateInputSerializer)
@action(detail=False, methods=['post'])
def generate(self, request):
user_prompt = request.data['user_prompt']
llm_response = call_llm(user_prompt)
return Response({'llm_response': llm_response})
For custom actions that don't require input, set input_serializer=None:
@mcp_tool(input_serializer=None) # No input needed
@action(detail=False, methods=['get'])
def recent_posts(self, request):
recent_posts = Post.objects.filter(created_at__gte=timezone.now() - timedelta(days=7))
serializer = PostSerializer(recent_posts, many=True)
return Response(serializer.data)
For CRUD actions (list, retrieve, create, update, partial_update, destroy), input_serializer is optional. The library will default to inferring schemas from the ViewSet's serializer_class if input_serializer is not specified. You'll want to use this optional parameter if you've written custom business logic that changes the input schema of a CRUD endpoint.
class ExtendedPostSerializer(PostSerializer): # Inherits and extends standard CRUD serializer
add_created_at_footer = serializers.BooleanField(help_text="Setting to true appends the author name")
@mcp_tool(input_serializer=ExtendedPostSerializer)
def create(self, request, *args, **kwargs):
if request.data.get('add_created_at_footer'):
# Append text to the end of the content noting it was created via MCP
request.data['content'] += f"\n\n*Created by {request.user.name}*"
return super().create(request, *args, **kwargs)
Selective Action Registration
If you don't want to create a tool from every action of a ViewSet, you can whitelist which actions to expose by passing an actions array to @mcp_viewset:
@mcp_viewset(actions=['banish', 'list'])
class CustomerViewSet(viewsets.ModelViewSet):
queryset = Customer.objects.all()
serializer_class = CustomerSerializer
def banish(self, request, pk=None):
# ... Business Logic
Custom Tool Names and Descriptions
You can customize the names, titles, and descriptions of individual actions using the @mcp_tool decorator. (NOTE: The @mcp_tool decorator only works when the ViewSet class is also decorated with @mcp_viewset. Using @mcp_tool alone will not register any MCP tools.)
@mcp_viewset()
class CustomerViewSet(viewsets.ModelViewSet):
queryset = Customer.objects.all()
serializer_class = CustomerSerializer
@mcp_tool(
name="get_customer_details",
title="Get Customer Details",
description="Retrieve detailed information about a specific customer by their ID"
)
def retrieve(self, request, pk=None):
return super().retrieve(request, pk)
MCP-Specific Overrides
Sometimes you want different behavior for MCP requests vs regular API requests. You have two options for achieving this.
Option 1: Inheritance (Recommended)
Create a dedicated ViewSet for MCP that inherits from your existing ViewSet.
@mcp_viewset()
class CustomerMCPViewSet(CustomerViewSet):
# Limit MCP clients to active customers only
queryset = super().get_queryset().filter(is_active=True)
# Use a simplified serializer for MCP clients
serializer_class = CustomerMCPSerializer
# ... everything else is inherited
class CustomerViewSet(viewsets.ModelViewSet):
queryset = Customer.object
…
## Source & license
This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.
- **Author:** [zacharypodbela](https://github.com/zacharypodbela)
- **Source:** [zacharypodbela/django-rest-framework-mcp](https://github.com/zacharypodbela/django-rest-framework-mcp)
- **License:** BSD-3-Clause
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.