# 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` ```python @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**: ``` __ 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**: ```python 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= ) 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: ```python 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: ```python from fastmcp.server import Provider class CustomProvider(Provider): async def call_tool(self, name, arguments): # Custom implementation pass ``` ## Next Steps - [Security Guide](./security.md) - Implement OAuth2, token validation - [API Reference](../api/) - Detailed component docs - [Examples](../examples/) - Real-world implementations