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:

  1. Authenticates the agent (via bearer token, OAuth2, etc.)

  2. Binds the agent to a Django User or AnonymousUser

  3. Enforces all DRF permission checks

  4. 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