Architecture Guide

This guide explains how Django MCP works internally and how its components interact.

High-Level Architecture

Django URLconf
       ↓
   Discovery Module (EndpointScanner)
       ↓ [DRF Views & Serializers]
   Schema Converter (SchemaConverter)
       ↓ [JSON Schemas]
   FastMCP Provider (DRFProvider)
       ↓ [MCP Tools]
   FastMCP Server
       ↓
   Transport (Stdio, HTTP, SSE)
       ↓
   AI Agent / LLM

Core Components

1. EndpointScanner (discovery.py)

Purpose: Automatically discover all DRF views and ViewSets in your Django project.

How it works:

  1. Imports ROOT_URLCONF from Django settings

  2. Recursively traverses URLconf tree (URLResolver → URLPattern)

  3. Identifies DRF APIView and ViewSet classes

  4. Extracts HTTP methods and action names

  5. Caches results at startup

Key class: EndpointMetadata

@dataclass
class EndpointMetadata:
    view_class: Type[Any]          # The DRF view class
    path: str                       # URL path pattern
    http_methods: List[str]         # Supported HTTP methods
    is_viewset: bool                # True if ViewSet
    basename: Optional[str]         # Router-provided name
    actions: Optional[Dict[str, str]]  # Custom @action mappings

Caching: Results are cached after first scan to avoid repeated URLconf traversal.

2. SchemaConverter (schema.py)

Purpose: Convert DRF Serializers to JSON schemas for MCP tool parameters.

Process:

  1. Takes a DRF Serializer instance

  2. Introspects field types (CharField, IntegerField, etc.)

  3. Extracts constraints (required, read-only, help_text)

  4. Converts to JSON Schema format (with type, properties, required, etc.)

Field Type Mapping:

  • BooleanField"type": "boolean"

  • IntegerField"type": "integer"

  • CharField"type": "string"

  • ListField"type": "array"

  • Nested Serializers → Recursive schema generation

Read-only vs Write-only:

  • For POST/PUT/PATCH: Excludes read-only fields (e.g., id, created_at)

  • For GET: Includes read-only fields for response documentation

Integration points:

  • Primary: drf-spectacular (OpenAPI 3 standard)

  • Fallback: DRF’s internal schema generation

3. ViewInvoker (executor.py)

Purpose: Execute DRF views with async/await support and permission checks.

Process:

  1. Receives MCP tool parameters and user context

  2. Creates a mocked HTTP request via APIRequestFactory

  3. Binds authenticated user to request

  4. Instantiates the DRF view class

  5. Calls check_permissions(request) before execution

  6. Invokes the view method (list, create, retrieve, etc.)

  7. Returns serialized response data

Async/Sync Handling:

  • All public methods are async

  • Uses asgiref.sync_to_async to wrap sync DRF views

  • Runs DRF code in thread pool → non-blocking LLM execution

Permission Flow:

MCP Request
    ↓
Create fake DRF Request
    ↓
Bind user (from MCP context)
    ↓
Check view-level permissions
    ↓
Execute view method
    ↓
(Optionally) Check object-level permissions
    ↓
Return response

4. DRFProvider (provider.py)

Purpose: FastMCP Custom Provider that bridges DRF views to MCP tools.

What it does:

  1. Uses EndpointScanner to discover DRF views

  2. Uses SchemaConverter to generate tool schemas

  3. Uses ViewInvoker to execute tools

  4. Registers all discovered endpoints as MCP tools

Tool Naming Convention:

<namespace>_<http_method>_<action_name>

Examples:
- crm_get_list_customers
- crm_post_create_customer
- crm_put_update_customer
- crm_delete_destroy_customer

Provider Methods:

  • register_viewset() - Register a specific ViewSet

  • register_apiview() - Register a specific APIView

  • autodiscover() - Auto-discover and register all views

  • call_tool() - Execute a registered tool (async)

  • list_tools() - Return all registered tools with metadata

5. DRFMCP (server.py)

Purpose: High-level API for creating and running MCP servers.

Simplifies:

  • Initialization of FastMCP

  • Registration of DRF views

  • Server execution with different transports

Usage:

mcp = DRFMCP("MyAPI")
mcp.autodiscover()  # or register_viewset(...)
mcp.run()           # stdio, HTTP, or SSE

Data Flow Example

Scenario: LLM calls “crm_get_list_customers”

1. LLM sends MCP request:
   {
     "jsonrpc": "2.0",
     "method": "tools/call",
     "params": {
       "name": "crm_get_list_customers",
       "arguments": {"page": 1}
     }
   }

2. DRFProvider.call_tool("crm_get_list_customers", {"page": 1})

3. ViewInvoker.invoke(
     view_class=CustomerViewSet,
     method="GET",
     path="/api/customers/",
     data=None,
     user=<authenticated_user>
   )

4. APIRequestFactory creates fake request

5. DRF checks permissions (IsAuthenticated, etc.)

6. ViewSet.list() executes, returns Response

7. SchemaConverter validates response against schema

8. Response serialized back to LLM:
   {
     "results": [...],
     "count": 42
   }

Permission Model

Django MCP enforces DRF’s permission system:

┌─────────────────────────────────────┐
│ DRF Permission Classes              │
│ (IsAuthenticated, IsAdminUser, etc) │
└──────────────┬──────────────────────┘
               ↓
┌──────────────────────────────────┐
│ MCP Request arrives              │
│ + User context (from FastMCP)    │
└──────────────┬───────────────────┘
               ↓
┌──────────────────────────────────┐
│ ViewInvoker binds user to        │
│ fake request                      │
└──────────────┬───────────────────┘
               ↓
┌──────────────────────────────────┐
│ View.check_permissions()         │
│ Enforces DRF permission classes  │
└──────────────┬───────────────────┘
               ↓
        ✓ Allowed / ✗ Denied

Transport Architecture

Django MCP uses FastMCP’s transport layer:

  • Stdio (default): For Claude Desktop, Cursor, local development

  • HTTP: For remote MCP servers, REST API exposure

  • SSE: Server-Sent Events for streaming responses

All transports support:

  • JSON-RPC 2.0 protocol

  • Tool discovery

  • Parameter validation

  • Error handling

OpenTelemetry Integration

Django MCP integrates OpenTelemetry for observability:

Every tool invocation → Span
  ├── Tool name
  ├── View class
  ├── HTTP method
  ├── User ID
  ├── Execution time
  ├── Result status (success/permission_denied/error)
  └── Response size

Exports to:

  • Jaeger (default)

  • Datadog

  • New Relic

  • Custom backends

Performance Considerations

Caching Strategy

  • URLconf scan: Cached at startup (one-time cost)

  • Schema generation: Cached by serializer ID

  • Avoid per-request discovery overhead

Async Execution

  • DRF sync views run in thread pool

  • Non-blocking for concurrent LLM requests

  • Connection pooling recommended

Large Result Sets

  • DRF pagination still applies

  • Schema validation efficient (Pydantic v2)

  • Streaming responses via SSE

Extensibility

Custom Transforms

Modify tool names/schemas before MCP client sees them:

from fastmcp import tool_transform

@tool_transform
def my_transform(tool):
    tool.name = f"my_prefix_{tool.name}"
    return tool

provider.add_transform(my_transform)

Custom Providers

Bridge non-DRF APIs:

from fastmcp.server import Provider

class CustomProvider(Provider):
    async def call_tool(self, name, arguments):
        # Custom implementation
        pass

Next Steps