Security Guide
This guide covers security best practices for Django MCP servers.
Fundamental Principle: “Agent as User”
Django MCP treats AI agents as authenticated users. Every MCP tool invocation:
Authenticates the agent (via bearer token, OAuth2, etc.)
Binds the agent to a Django User or AnonymousUser
Enforces all DRF permission checks
Audits the invocation
You cannot bypass DRF’s permission system in Django MCP.
Authentication
Default: No Authentication
By default, Django MCP runs without authentication. Only use this for:
Local development
Private networks with network-level security
Testing and prototyping
Production: OAuth2/OIDC
For production, implement token-based authentication:
1. Install OAuth2 Toolkit
uv add django-oauth-toolkit
2. Configure Custom Authentication
# settings.py
INSTALLED_APPS = [
...
'oauth2_provider',
'rest_framework',
]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
],
}
3. Create Agent Clients
python manage.py creatersakey
python manage.py create_oauth2_client \
--name "claude-agent" \
--client-type confidential \
--grant-type client-credentials
4. Agents Use Token
curl -X POST https://your-api.com/o/token/ \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET" \
-d "grant_type=client_credentials"
# Returns: {"access_token": "...", "token_type": "Bearer"}
5. MCP Server Validates Token
# mcp_server.py
from drf_mcp.auth import MCPTokenAuthentication
# Token validation happens automatically in ViewInvoker
# when DRF permission checks run
OIDC / OpenID Connect
For federated identity (e.g., corporate SSO):
uv add django-oidc-auth
# settings.py
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'oidc_auth.authentication.BearerTokenAuthentication',
],
}
OIDC_RP_CLIENT_ID = "your-client-id"
OIDC_RP_CLIENT_SECRET = "your-client-secret"
OIDC_OP_AUTHORIZATION_ENDPOINT = "https://your-identity-provider.com/oauth/authorize"
OIDC_OP_TOKEN_ENDPOINT = "https://your-identity-provider.com/oauth/token"
OIDC_OP_USER_ENDPOINT = "https://your-identity-provider.com/oauth/userinfo"
Permission Classes
Your DRF permission classes automatically apply to MCP tools:
from rest_framework.permissions import (
IsAuthenticated,
IsAdminUser,
BasePermission,
)
class IsTeamMember(BasePermission):
"""Only team members can access"""
def has_permission(self, request, view):
return request.user.groups.filter(name='team').exists()
class CustomerViewSet(viewsets.ModelViewSet):
serializer_class = CustomerSerializer
permission_classes = [IsAuthenticated, IsTeamMember]
# Tools derived from this ViewSet enforce permissions
Row-Level Security (Object-Level Permissions)
Restrict access to specific records:
from rest_framework.permissions import BasePermission
class IsCustomerOwner(BasePermission):
"""Only access your own customer records"""
def has_object_permission(self, request, view, obj):
return obj.owner == request.user
class CustomerViewSet(viewsets.ModelViewSet):
serializer_class = CustomerSerializer
permission_classes = [IsAuthenticated, IsCustomerOwner]
def get_queryset(self):
# Filter to user's customers
return Customer.objects.filter(owner=self.request.user)
# When MCP agent calls retrieve_customer(id=123),
# has_object_permission() checks if customer.owner == agent's user
Scope-Based Access (Fine-Grained Permissions)
Use OAuth2 scopes to grant agents specific permissions:
# settings.py
OAUTH2_SCOPES = {
'customers:read': 'Read customer data',
'customers:write': 'Modify customer data',
'customers:delete': 'Delete customers',
}
# Grant agents different scopes:
# - Marketing agent: customers:read
# - Sales agent: customers:read + customers:write
# - Admin agent: customers:*
# views.py
from rest_framework.decorators import action
from rest_framework.permissions import BasePermission
class HasCustomerReadScope(BasePermission):
def has_permission(self, request, view):
scopes = request.auth.scopes if request.auth else []
return 'customers:read' in scopes
class CustomerViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated, HasCustomerReadScope]
Audit Logging
Log all MCP tool invocations:
# settings.py
LOGGING = {
'version': 1,
'handlers': {
'mcp_audit': {
'level': 'INFO',
'class': 'logging.FileHandler',
'filename': 'mcp_audit.log',
},
},
'loggers': {
'drf_mcp': {
'handlers': ['mcp_audit'],
'level': 'INFO',
},
},
}
Audit trail includes:
Agent/User ID
Tool name
Arguments (redacted for sensitive data)
Timestamp
Result (success/denied/error)
IP address (if available)
Transport Security
Stdio (Local)
Secure by default for local development. Claude Desktop runs on same machine.
HTTP
Always use HTTPS in production:
# settings.py
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
CORS Configuration:
uv add django-cors-headers
# settings.py
INSTALLED_APPS = [
'corsheaders',
...
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
...
]
CORS_ALLOWED_ORIGINS = [
"https://trusted-client.example.com",
]
CORS_ALLOW_CREDENTIALS = True
Stdio over SSH
For remote execution, tunnel via SSH:
ssh user@remote-server \
-L 8000:localhost:8000 \
"cd /path && python mcp_server.py --transport http --host 127.0.0.1"
Data Protection
Sensitive Fields
Exclude sensitive data from serializer schemas:
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'name', 'email'] # Don't include 'password'
# or use SerializerMethodField for computed data
Field-Level Redaction
from django_filters import BaseInFilter
class RedactedCharFilter(BaseInFilter, filters.CharFilter):
"""Custom filter that redacts sensitive data"""
pass
class SensitiveDataViewSet(viewsets.ModelViewSet):
filterset_fields = {
'ssn': [RedactedCharFilter()], # Hide SSN in logs
}
Read-Only Fields
Mark sensitive fields as read-only:
class CustomerSerializer(serializers.ModelSerializer):
# Agents can read but not modify
created_at = serializers.DateTimeField(read_only=True)
last_modified_by = serializers.StringRelatedField(read_only=True)
class Meta:
model = Customer
fields = ['id', 'name', 'email', 'created_at', 'last_modified_by']
Rate Limiting
Prevent abuse via rate limiting:
uv add djangorestframework-simplejwt
# settings.py
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle',
],
'DEFAULT_THROTTLE_RATES': {
'anon': '100/hour',
'user': '1000/hour', # Per-agent limit
},
}
Secrets Management
Never commit secrets to git:
# ✗ WRONG:
PASSWORD = "hardcoded-secret"
# ✓ CORRECT:
import os
from decouple import config
PASSWORD = config('OAUTH2_SECRET', default=None)
# .env (add to .gitignore)
OAUTH2_SECRET=your-secret-here
OIDC_RP_CLIENT_SECRET=your-oidc-secret
Compliance
GDPR
Implement data deletion for agents:
# views.py
class CustomerViewSet(viewsets.ModelViewSet):
@action(detail=True, methods=['delete'])
def gdpr_delete(self, request, pk=None):
"""Securely delete customer data"""
customer = self.get_object()
# Check permissions
self.check_object_permissions(request, customer)
# Delete with audit log
customer.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
SOC 2
Maintain audit logs:
uv add django-auditlog
from auditlog.registry import auditlog
class Customer(models.Model):
...
auditlog.register(Customer)
Security Checklist
Agents authenticate before tool access
DRF permission classes enforced on all tools
HTTPS only in production
CORS properly configured
Sensitive fields marked read-only
Rate limiting enabled
Audit logging configured
OAuth2 scopes implemented
Secrets in environment variables
Regular security audits scheduled
Next Steps
Getting Started - Implement basic auth
Architecture - Understand permission flow
API Reference - Detailed security APIs