# 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**:
```
<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**:
```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=<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:

```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
