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:
Imports
ROOT_URLCONFfrom Django settingsRecursively traverses URLconf tree (URLResolver → URLPattern)
Identifies DRF APIView and ViewSet classes
Extracts HTTP methods and action names
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:
Takes a DRF Serializer instance
Introspects field types (CharField, IntegerField, etc.)
Extracts constraints (required, read-only, help_text)
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:
Receives MCP tool parameters and user context
Creates a mocked HTTP request via
APIRequestFactoryBinds authenticated user to request
Instantiates the DRF view class
Calls
check_permissions(request)before executionInvokes the view method (list, create, retrieve, etc.)
Returns serialized response data
Async/Sync Handling:
All public methods are
asyncUses
asgiref.sync_to_asyncto wrap sync DRF viewsRuns 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:
Uses
EndpointScannerto discover DRF viewsUses
SchemaConverterto generate tool schemasUses
ViewInvokerto execute toolsRegisters 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 ViewSetregister_apiview()- Register a specific APIViewautodiscover()- Auto-discover and register all viewscall_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
Security Guide - Implement OAuth2, token validation
API Reference - Detailed component docs
Examples - Real-world implementations