# Analisi: ASGI Delegation per protocolli non-REST

## Contesto

Un server genro-asgi deve servire due applicazioni dallo stesso processo:
- **MCP** (`/mcp/`) — protocollo JSON-RPC 2.0 (MCP Streamable HTTP)
- **OpenAPI** (`/api/`) — HTTP REST con documentazione Swagger/ReDoc

Entrambe condividono la stessa API (e quindi lo stesso DB, client esterni, ecc.).

Il framework genro-asgi fornisce `AsgiServer`, `AsgiApplication`, `McpApplication`, middleware chain, e dispatcher basato su genro-routes.

---

## 1. Problemi riscontrati

### P1. McpApplication non può essere montata dentro genro-asgi

Quando `McpApplication` viene montata su `AsgiServer` tramite `attach_instance()`, il Dispatcher risolve le richieste via `router.node(path)` — che cerca handler `@route`.

Ma MCP funziona diversamente: **tutto arriva su POST `/`** come JSON-RPC. `McpApplication.__call__` legge il body, parsa il JSON-RPC, e dispatcha a `tools/list` / `tools/call`. Il Dispatcher di genro-asgi **non sa delegare l'intera richiesta ASGI** a una sub-app.

Risultato: è impossibile montare `McpApplication` dentro genro-asgi in modo che funzioni con il protocollo MCP. L'unico workaround è fare override di `__call__` nel server:

```python
class MyServer(AsgiServer):
    async def __call__(self, scope, receive, send):
        if scope.get("path", "").startswith("/mcp"):
            await self.mcp_app(scope, receive, send)  # bypass totale
        else:
            await super().__call__(scope, receive, send)
```

### P2. Il bypass esclude la middleware chain

Con il workaround sopra, le richieste MCP **non passano** per la middleware chain di genro-asgi:
- `ErrorMiddleware` non protegge MCP (eccezioni non gestite → errore grezzo uvicorn)
- `CORSMiddleware` non aggiunge header
- `AuthMiddleware` non autentica
- `RequestRegistry` non traccia le richieste

### P3. Auth wrappata manualmente all'esterno

Per compensare P2, l'auth viene wrappata come outer ASGI middleware sull'intero server:

```python
asgi_app = CustomAuthMiddleware(server, db, require_token=True)
uvicorn.run(asgi_app, ...)
```

Questo funziona ma:
- Bypassa completamente il sistema middleware di genro-asgi
- Non può avere configurazioni auth diverse per app diverse
- `db.closeConnection()` finisce nel finally dell'auth, non nel framework

### P4. config.yaml inutilizzato

Il config.yaml non può dichiarare la McpApplication come app perché non funzionerebbe. Risultato: niente `apps:`, niente `middleware:`, niente `openapi:` — tutte feature di genro-asgi ignorate.

---

## 2. Causa radice: il Dispatcher non supporta ASGI delegation

Il `Dispatcher` (innermost layer della middleware chain) ha un solo flusso:

```
scope → RequestRegistry.create() → Request
     → router.node(path, auth_tags, env_capabilities, errors)
     → handler(**query)
     → response.set_result(result)
     → response(scope, receive, send)
```

Non esiste un percorso alternativo per app che gestiscono il protocollo ASGI direttamente (MCP, WebSocket-based, gRPC su ASGI, ecc.).

---

## 3. Soluzione proposta: ASGI Delegation nel Dispatcher

### Concetto

Aggiungere al Dispatcher la capacità di **delegare l'intera richiesta ASGI** a una sub-app montata, quando l'app dichiara di voler gestire il protocollo direttamente.

### Meccanismo

1. Un class attribute `is_asgi_delegate = True` su `McpApplication` (e su qualsiasi futura app che gestisce il protocollo direttamente)
2. Il Dispatcher, **prima** di `router.node()`, controlla se il primo segmento del path corrisponde a un'app montata con `is_asgi_delegate = True`
3. Se sì, fa strip del prefisso dal path e delega l'intera request ASGI all'app

### Implementazione nel Dispatcher

```python
# dispatcher.py — nel __call__, dopo aver creato la request

path = request.path
segments = path.strip("/").split("/", 1)
first_segment = segments[0] if segments[0] else ""

if first_segment and first_segment in self.server.apps:
    app = self.server.apps[first_segment]
    if getattr(app, 'is_asgi_delegate', False):
        # Strip il prefisso dal path per l'app
        scope = dict(scope)
        remaining = f"/{segments[1]}" if len(segments) > 1 else "/"
        scope['path'] = remaining
        try:
            await app(scope, receive, send)
        finally:
            set_current_request(None)
            self.request_registry.unregister()
        return

# ... flusso normale: router.node() ecc.
```

### McpApplication

```python
class McpApplication(AsgiApplication):
    is_asgi_delegate: ClassVar[bool] = True
    # ... resto invariato
```

### Vantaggi

1. **La middleware chain protegge tutte le app**: errors, cors, auth girano prima del Dispatcher, quindi l'app delegata è protetta
2. **config.yaml funziona**: le app si dichiarano normalmente
   ```yaml
   apps:
     mcp:
       module: "my_mcp_app:Application"
     api:
       module: "my_rest_app:Application"
   middleware:
     errors: on
     cors: on
     auth:
       bearer: ...
   ```
3. **Nessun override di `__call__`** necessario nei server custom
4. **Pattern generale**: qualsiasi protocollo non-REST (WebSocket manager, gRPC, Server-Sent Events) può usare lo stesso meccanismo
5. **Backward-compatible**: le app senza `is_asgi_delegate` continuano a funzionare via `router.node()` come prima

### Considerazioni

- **Path stripping**: l'app riceve il path senza il prefisso di mount. Es. se montata come `mcp`, una richiesta a `/mcp/foo` arriva all'app come `/foo`. MCP tipicamente riceve tutto su `/`, quindi `/mcp/` → `/`.
- **Middleware chain**: è già gestita correttamente perché il Dispatcher è l'ultimo layer. I middleware (auth, cors, errors) girano prima e impostano `scope["auth"]` ecc. L'app delegata li trova già nel scope.
- **Request tracking**: la request viene registrata e de-registrata nel try/finally, come per il flusso normale.

---

## 4. Auth extensible (proposta separata)

Il `AuthMiddleware` attuale supporta bearer/basic/jwt con lookup statico (config.yaml). Per applicazioni che validano token su database serve un backend custom.

### Proposta: backend `callback`

```yaml
middleware:
  auth:
    callback:
      factory: "myapp.auth:auth_factory"
```

`AuthMiddleware._configure_callback()` importa la factory e la chiama. La factory restituisce un callable `(scope) -> dict | None` che il middleware invoca al posto del lookup statico.

Questo è un cambiamento separato dalla ASGI delegation e può essere implementato indipendentemente. L'alternativa immediata è che il progetto consumer registri il proprio middleware auth come `BaseMiddleware` subclass nella chain, senza modificare `AuthMiddleware`.

---

## 5. Esempio d'uso completo

### config.yaml

```yaml
server:
  host: "0.0.0.0"
  port: 8081

apps:
  mcp:
    module: "my_mcp_app:Application"
  api:
    module: "my_rest_app:Application"

middleware:
  errors: on
  cors: on
```

### Server

```python
class MyServer(AsgiServer):
    def __init__(self, **kwargs):
        # Setup risorse condivise
        self.shared_api = MyAPI(db=...)
        super().__init__(**kwargs)
        # Le app accedono a self.shared_api via self.server

# Nessun override di __call__ necessario
```

### MCP App

```python
class MyMcpApp(McpApplication):
    # is_asgi_delegate = True (ereditato da McpApplication)
    mcp_name = "my-service"

    def on_startup(self):
        self.set_router(self.server.shared_api.router)
```

### REST App

```python
class MyRestApp(AsgiApplication):
    def on_startup(self):
        self.main = self.server.shared_api.router
```

### Risultato

- `POST /mcp/` → McpApplication gestisce JSON-RPC (tools/list, tools/call)
- `GET /api/code/search_symbols?query=foo` → Dispatcher → router.node() → handler
- Entrambi passano per ErrorMiddleware + CORSMiddleware
- Entrambi hanno `scope["auth"]` popolato (se auth configurata)
