docs: initial import design sistema documentale + docugen-mcp
- Docs/sistema-documentale/: master architettura multi-wiki - Docs/docugen-mcp/: design + implementation plan MCP server Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,600 @@
|
||||
# DocuGen MCP — Design Document
|
||||
|
||||
> MCP server standalone per generazione di documenti Markdown
|
||||
> a partire da un template (Markdown + frontmatter + prompt),
|
||||
> contenuto grezzo e variabili. LLM via OpenRouter.
|
||||
> Deploy Docker su VPS esterna, accessibile da qualsiasi host MCP
|
||||
> (Claude Code, Claude Desktop, altri client conformi).
|
||||
|
||||
---
|
||||
|
||||
## Indice
|
||||
|
||||
1. [Panoramica](#1-panoramica)
|
||||
2. [Architettura generale](#2-architettura-generale)
|
||||
3. [Componenti interni](#3-componenti-interni)
|
||||
4. [Modello dati](#4-modello-dati)
|
||||
5. [API MCP — tool esposti](#5-api-mcp--tool-esposti)
|
||||
6. [Dataflow `document_generate`](#6-dataflow-document_generate)
|
||||
7. [Gestione errori](#7-gestione-errori)
|
||||
8. [Strategia di test](#8-strategia-di-test)
|
||||
9. [Deploy e operatività](#9-deploy-e-operatività)
|
||||
10. [Stack tecnologico](#10-stack-tecnologico)
|
||||
|
||||
---
|
||||
|
||||
## 1. Panoramica
|
||||
|
||||
### Scopo
|
||||
|
||||
`docugen-mcp` è un server MCP che espone strumenti per creare documenti Markdown professionali (fatture, lettere, preventivi, report) a partire da:
|
||||
|
||||
- un **template** persistito sul server (nome + frontmatter YAML + body Markdown con prompt e placeholder)
|
||||
- un **contenuto grezzo** Markdown fornito dal client (es. "# Ordine ACME\n- 10 unità X...")
|
||||
- un **dict di variabili** strutturate (es. `{cliente: "ACME", data: "2026-04-21", foto_prodotto: {kind: image, data_b64, mime}}`)
|
||||
- **istruzioni libere** opzionali per tweak stilistici ("tono formale", "in inglese")
|
||||
|
||||
Il server assembla un prompt, chiama un modello LLM via OpenRouter e restituisce il Markdown finale al client.
|
||||
|
||||
### Principi
|
||||
|
||||
- **MCP protocol-first** — il server è un MCP server, non una API REST con wrapper MCP. FastMCP è il framework principale.
|
||||
- **Standalone** — repo dedicato, nessuna dipendenza da DocuGen. DocuGen e altri sistemi lo usano come client.
|
||||
- **Stateless sui documenti generati** — il server non tiene archivio dei MD prodotti (solo log/cost tracking in SQLite). Il client conserva l'output.
|
||||
- **Persistenza solo dove serve** — template sul filesystem (ispezionabili a mano), asset efimeri con TTL, metrica generazioni in SQLite.
|
||||
- **Validazione strict** — se il template dichiara `required_variables` e mancano, errore duro, niente chiamata LLM sprecata.
|
||||
- **Errori espliciti** — codici stringa ben definiti, retry solo su errori transitori upstream.
|
||||
|
||||
### Cosa NON fa
|
||||
|
||||
- Non genera PDF né HTML (solo Markdown). Conversione fuori scope.
|
||||
- Non tiene un archivio ricercabile dei documenti prodotti.
|
||||
- Non fa OCR, estrazione metadati da documenti, ricerca semantica (ruolo di DocuGen).
|
||||
- Non espone gestione utenti: una sola API key condivisa.
|
||||
|
||||
---
|
||||
|
||||
## 2. Architettura generale
|
||||
|
||||
### Approccio: FastMCP principale + FastAPI sub-app
|
||||
|
||||
Un singolo processo ASGI, un container Docker, un volume disco.
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ Container docugen-mcp (VPS esterna) │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────┐ │
|
||||
│ │ FastMCP (ASGI root) │ │
|
||||
│ │ Streamable HTTP su /mcp │ │
|
||||
│ │ │ │
|
||||
│ │ Tool esposti: │ │
|
||||
│ │ • template_create │ │
|
||||
│ │ • template_update │ │
|
||||
│ │ • template_delete │ │
|
||||
│ │ • template_list │ │
|
||||
│ │ • template_get │ │
|
||||
│ │ • document_generate │ │
|
||||
│ └────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ mount FastAPI sub-app │
|
||||
│ ▼ │
|
||||
│ ┌────────────────────────────────────────┐ │
|
||||
│ │ FastAPI sub-app │ │
|
||||
│ │ GET /assets/{template}/{file} │ │
|
||||
│ │ GET /generated/{gen_id}/{file} │ │
|
||||
│ │ GET /health │ │
|
||||
│ └────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Moduli (librerie Python): │
|
||||
│ • auth — middleware API key │
|
||||
│ • template_store — filesystem │
|
||||
│ • llm_client — OpenRouter (httpx) │
|
||||
│ • renderer — prompt assembly │
|
||||
│ • generation_store — SQLite + fs │
|
||||
│ │
|
||||
│ Volume Docker montato su /data: │
|
||||
│ ├── templates/<nome>/ │
|
||||
│ │ ├── template.md │
|
||||
│ │ └── assets/* │
|
||||
│ ├── generated/<gen_id>/ │
|
||||
│ │ └── <var_name>.<ext> (TTL 30gg) │
|
||||
│ └── docugen_mcp.db │
|
||||
└──────────────────────────────────────────────┘
|
||||
▲ │
|
||||
│ HTTP(S) + API key │ HTTPS
|
||||
│ ▼
|
||||
┌─────┴──────────┐ ┌──────────┐
|
||||
│ Host MCP │ │OpenRouter│
|
||||
│ (Claude Code, │ │ │
|
||||
│ Claude Desktop│ └──────────┘
|
||||
│ altri) │
|
||||
└────────────────┘
|
||||
```
|
||||
|
||||
### Motivazioni architetturali
|
||||
|
||||
- **MCP è il prodotto.** FastMCP guida la struttura. FastAPI compare solo per ciò che l'MCP protocol non copre: file statici per asset immagini e health check.
|
||||
- **Un processo unico** mantiene deploy banale: un container, un comando, un volume.
|
||||
- **TLS fuori container.** Reverse proxy esterno (Nginx/Traefik/Caddy) gestisce certificati e termina HTTPS. Il container espone HTTP su porta interna.
|
||||
- **Storage semplice.** Template sul filesystem (leggibili a mano, backup = copia cartella, git-friendly se l'operatore vuole). Metriche e indice TTL in SQLite per query strutturate. Nessun DB esterno.
|
||||
|
||||
### Scalabilità
|
||||
|
||||
Processo singolo basta per l'uso previsto (pochi client, decine di generazioni/minuto al massimo). Per picchi futuri: replicare il container dietro load balancer, spostare il volume su storage condiviso (NFS) o estrarre SQLite in Postgres. Scelte rimandate a quando il bisogno sarà concreto.
|
||||
|
||||
---
|
||||
|
||||
## 3. Componenti interni
|
||||
|
||||
### `auth` — middleware API Key
|
||||
|
||||
- Legge header `Authorization: Bearer <key>` da ogni richiesta (MCP e FastAPI).
|
||||
- Confronta con env var `API_KEY` (singola chiave condivisa).
|
||||
- Respinge 401 se assente/errata.
|
||||
- Esenzione: `/health`.
|
||||
|
||||
### `template_store` — persistenza filesystem
|
||||
|
||||
- Path base: `{DATA_DIR}/templates/<nome>/`.
|
||||
- File `template.md` = frontmatter YAML + body Markdown.
|
||||
- Directory `assets/` contiene risorse statiche referenziate dal template.
|
||||
- Frontmatter standard:
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: fattura-commerciale
|
||||
description: Fattura commerciale in italiano
|
||||
model: anthropic/claude-sonnet-4 # opzionale, override default
|
||||
required_variables:
|
||||
- name: cliente
|
||||
type: string
|
||||
- name: data
|
||||
type: string
|
||||
- name: foto_prodotto
|
||||
type: image # trigger per shape {kind, data_b64, mime}
|
||||
instructions_hint: "tono formale, struttura blocchi"
|
||||
---
|
||||
```
|
||||
|
||||
- Body: Markdown con prompt di sistema e placeholder `{{cliente}}`, `{{foto_prodotto}}`, `./assets/logo.png`.
|
||||
- API Python (async): `create(name, frontmatter, body, assets=None)`, `get(name)`, `update(name, ...)`, `delete(name)`, `list()`.
|
||||
- Nome template: slug `^[a-z0-9][a-z0-9-]*$`. Rifiuta path traversal, chars speciali.
|
||||
- Validazione frontmatter via Pydantic `TemplateFrontmatter`.
|
||||
|
||||
### `llm_client` — wrapper OpenRouter
|
||||
|
||||
- Unica responsabilità: mandare una richiesta OpenRouter e ritornare risposta + metrica utilizzo.
|
||||
- httpx async. Retry con backoff esponenziale (max 3 tentativi) su timeout/5xx/429.
|
||||
- Modello scelto da `frontmatter.model or env.LLM_MODEL_DEFAULT`.
|
||||
- Ritorna `LLMResponse(text: str, model: str, tokens_in: int, tokens_out: int, cost_usd: float, latency_ms: int)`.
|
||||
|
||||
### `renderer` — orchestratore della generazione
|
||||
|
||||
Riceve `(template_name, content_md, variables, instructions)` e ritorna il Markdown finale.
|
||||
|
||||
Passi:
|
||||
1. `template_store.get(template_name)` → `(frontmatter, body, assets_dir)`.
|
||||
2. Validazione variabili (strict): ogni `required_variables` dev'essere presente con il tipo dichiarato. Fallimento → eccezione tipizzata senza chiamare LLM.
|
||||
3. Generazione `gen_id = uuid4()`.
|
||||
4. Per ogni variabile di tipo `image`: decodifica base64, valida mime, salva in `{DATA_DIR}/generated/{gen_id}/{var_name}.{ext}`, registra in `ephemeral_assets` con `expires_at = now + ASSET_TTL_DAYS`, sostituisce la variabile con URL assoluto `{PUBLIC_BASE_URL}/generated/{gen_id}/{var_name}.{ext}`.
|
||||
5. Rewrite asset paths del template: `./assets/<file>` → `{PUBLIC_BASE_URL}/assets/{template_name}/{file}`.
|
||||
6. Assembla prompt finale:
|
||||
- **system**: body del template con placeholder sostituiti.
|
||||
- **user**: contenuto grezzo + variabili formattate + `instructions` opzionali.
|
||||
7. Chiama `llm_client.chat(...)` con il modello selezionato.
|
||||
8. Registra la generazione in `generation_store` (success o error).
|
||||
9. Ritorna `GenerationResult(generation_id, markdown, model, tokens, cost_usd, ephemeral_assets_urls, ephemeral_expires_at)`.
|
||||
|
||||
### `generation_store` — persistenza generazioni
|
||||
|
||||
- SQLite via aiosqlite. Due tabelle:
|
||||
- `generations(id TEXT PK, timestamp INTEGER, template_name TEXT, model TEXT, tokens_in INTEGER, tokens_out INTEGER, cost_usd REAL, success INTEGER, error_msg TEXT)`
|
||||
- `ephemeral_assets(generation_id TEXT, var_name TEXT, file_path TEXT, mime TEXT, created_at INTEGER, expires_at INTEGER, PRIMARY KEY (generation_id, var_name))`
|
||||
- Cleanup job: task asyncio lanciato al bootstrap che ogni 24h cancella file e record con `expires_at < now()`.
|
||||
|
||||
### `mcp_server` — entry point FastMCP
|
||||
|
||||
- Definisce i 6 tool MCP con decoratori `@mcp.tool()`.
|
||||
- Ogni tool fa validazione input (Pydantic), invoca il modulo responsabile, serializza la risposta.
|
||||
- Monta la sub-app FastAPI sul path root.
|
||||
|
||||
### `config` — Pydantic Settings
|
||||
|
||||
Variabili d'ambiente (tutte con default dove sensato):
|
||||
|
||||
| Variabile | Default | Descrizione |
|
||||
|-----------|---------|-------------|
|
||||
| `API_KEY` | *(obbligatoria)* | Chiave di autenticazione client MCP |
|
||||
| `OPENROUTER_API_KEY` | *(obbligatoria)* | Chiave OpenRouter |
|
||||
| `OPENROUTER_BASE_URL` | `https://openrouter.ai/api/v1` | Endpoint OpenRouter |
|
||||
| `LLM_MODEL_DEFAULT` | `anthropic/claude-sonnet-4` | Modello usato se il template non specifica override |
|
||||
| `PUBLIC_BASE_URL` | *(obbligatoria)* | URL pubblico del server, usato per costruire link asset |
|
||||
| `DATA_DIR` | `/data` | Path volume persistenza |
|
||||
| `ASSET_TTL_DAYS` | `30` | TTL asset efimeri generati |
|
||||
| `MAX_IMAGE_SIZE_MB` | `10` | Limite dimensione immagine base64 in `variables` |
|
||||
| `LLM_TIMEOUT_SECONDS` | `60` | Timeout singola richiesta OpenRouter |
|
||||
|
||||
### Struttura sorgente
|
||||
|
||||
```
|
||||
src/docugen_mcp/
|
||||
├── __init__.py
|
||||
├── main.py # bootstrap FastMCP + FastAPI mount
|
||||
├── config.py
|
||||
├── auth.py
|
||||
├── mcp_tools.py # definizione tool MCP
|
||||
├── http_routes.py # static asset + health (FastAPI)
|
||||
├── template_store.py
|
||||
├── llm_client.py
|
||||
├── renderer.py
|
||||
├── generation_store.py
|
||||
└── models.py # Pydantic models condivisi
|
||||
tests/
|
||||
├── unit/
|
||||
├── integration/
|
||||
└── e2e/
|
||||
pyproject.toml
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
.env.example
|
||||
README.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Modello dati
|
||||
|
||||
### Filesystem
|
||||
|
||||
```
|
||||
{DATA_DIR}/
|
||||
├── templates/
|
||||
│ └── <nome-template>/
|
||||
│ ├── template.md # frontmatter + body
|
||||
│ └── assets/
|
||||
│ └── logo.png # risorse statiche del template
|
||||
├── generated/
|
||||
│ └── <gen_id>/ # uuid4, uno per generazione
|
||||
│ └── <var_name>.<ext> # immagini inline dal client (TTL)
|
||||
└── docugen_mcp.db # SQLite
|
||||
```
|
||||
|
||||
### SQLite
|
||||
|
||||
**`generations`**
|
||||
|
||||
| Campo | Tipo | Note |
|
||||
|-------|------|------|
|
||||
| `id` | TEXT PK | uuid4 della generazione |
|
||||
| `timestamp` | INTEGER | unix epoch ms |
|
||||
| `template_name` | TEXT | |
|
||||
| `model` | TEXT | modello effettivamente usato |
|
||||
| `tokens_in` | INTEGER | da OpenRouter usage |
|
||||
| `tokens_out` | INTEGER | da OpenRouter usage |
|
||||
| `cost_usd` | REAL | calcolato da OpenRouter |
|
||||
| `success` | INTEGER | 0/1 |
|
||||
| `error_msg` | TEXT NULL | codice errore se success=0 |
|
||||
|
||||
Indice su `timestamp` per query temporali sui costi.
|
||||
|
||||
**`ephemeral_assets`**
|
||||
|
||||
| Campo | Tipo | Note |
|
||||
|-------|------|------|
|
||||
| `generation_id` | TEXT | FK logica a `generations.id` |
|
||||
| `var_name` | TEXT | nome variabile (es. `foto_prodotto`) |
|
||||
| `file_path` | TEXT | path assoluto sul filesystem |
|
||||
| `mime` | TEXT | `image/png`, `image/jpeg`, `image/webp` |
|
||||
| `created_at` | INTEGER | unix epoch ms |
|
||||
| `expires_at` | INTEGER | unix epoch ms |
|
||||
|
||||
PRIMARY KEY `(generation_id, var_name)`. Indice su `expires_at` per cleanup rapido.
|
||||
|
||||
---
|
||||
|
||||
## 5. API MCP — tool esposti
|
||||
|
||||
### `template_create`
|
||||
|
||||
**Input:**
|
||||
```json
|
||||
{
|
||||
"name": "fattura-commerciale",
|
||||
"frontmatter": {
|
||||
"name": "fattura-commerciale",
|
||||
"description": "...",
|
||||
"model": "anthropic/claude-sonnet-4",
|
||||
"required_variables": [
|
||||
{"name": "cliente", "type": "string"},
|
||||
{"name": "data", "type": "string"}
|
||||
],
|
||||
"instructions_hint": "..."
|
||||
},
|
||||
"body": "# Template Markdown con {{cliente}} placeholder...",
|
||||
"assets": [
|
||||
{"filename": "logo.png", "data_b64": "...", "mime": "image/png"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Output:** `{"name": "fattura-commerciale", "created_at": "..."}` o errore.
|
||||
|
||||
### `template_update`
|
||||
|
||||
Stesso input di `template_create`, richiede nome esistente. Sovrascrive `template.md`; `assets` (se forniti) sostituiscono interamente la directory (nessun merge).
|
||||
|
||||
### `template_delete`
|
||||
|
||||
**Input:** `{"name": "fattura-commerciale"}`. Rimuove directory template.
|
||||
|
||||
### `template_list`
|
||||
|
||||
**Input:** vuoto. **Output:** lista di `{name, description, updated_at}`.
|
||||
|
||||
### `template_get`
|
||||
|
||||
**Input:** `{"name": "fattura-commerciale"}`. **Output:** frontmatter + body + elenco asset (nomi, mime, size).
|
||||
|
||||
### `document_generate`
|
||||
|
||||
**Input:**
|
||||
```json
|
||||
{
|
||||
"template_name": "fattura-commerciale",
|
||||
"content_md": "# Ordine ACME\n- 10 unità X",
|
||||
"variables": {
|
||||
"cliente": "ACME S.r.l.",
|
||||
"data": "2026-04-21",
|
||||
"foto_prodotto": {
|
||||
"kind": "image",
|
||||
"data_b64": "iVBOR...",
|
||||
"mime": "image/png"
|
||||
}
|
||||
},
|
||||
"instructions": "tono molto formale"
|
||||
}
|
||||
```
|
||||
|
||||
**Output:**
|
||||
```json
|
||||
{
|
||||
"generation_id": "<uuid>",
|
||||
"markdown": "...output MD...",
|
||||
"model": "anthropic/claude-sonnet-4",
|
||||
"tokens": {"in": 1234, "out": 2345},
|
||||
"cost_usd": 0.0123,
|
||||
"ephemeral_assets_urls": [
|
||||
"https://mcp.vps/generated/<uuid>/foto_prodotto.png"
|
||||
],
|
||||
"ephemeral_expires_at": "2026-05-21T10:33:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Dataflow `document_generate`
|
||||
|
||||
```
|
||||
Client MCP ──► document_generate(...)
|
||||
│
|
||||
▼
|
||||
auth: valida API key (401 se KO)
|
||||
│
|
||||
▼
|
||||
Pydantic: valida shape input
|
||||
│
|
||||
▼
|
||||
renderer.generate():
|
||||
1. template_store.get(name)
|
||||
2. validate variables (strict)
|
||||
3. gen_id = uuid4()
|
||||
4. per ogni var image:
|
||||
- decode base64, valida mime/size
|
||||
- salva /data/generated/<gen_id>/<var>.<ext>
|
||||
- insert ephemeral_assets (expires_at)
|
||||
- sostituisce var con URL assoluto
|
||||
5. rewrite asset paths del template
|
||||
6. assembla prompt finale
|
||||
7. llm_client.chat(prompt, model)
|
||||
8. generation_store.record(success|error)
|
||||
│
|
||||
▼
|
||||
Ritorna GenerationResult al client
|
||||
```
|
||||
|
||||
**Flusso secondario — accesso asset:**
|
||||
|
||||
Quando il client (o il renderer downstream) dereferenzia ``:
|
||||
|
||||
- `GET /assets/<template>/<file>` → auth check → serve file da `{DATA_DIR}/templates/<template>/assets/`.
|
||||
- `GET /generated/<gen_id>/<file>` → auth check → serve file. 410 Gone se record scaduto.
|
||||
|
||||
Anche gli asset richiedono API key: il Markdown non è pubblico.
|
||||
|
||||
---
|
||||
|
||||
## 7. Gestione errori
|
||||
|
||||
### Errori di validazione (pre-LLM, nessuna registrazione)
|
||||
|
||||
| Caso | Errore |
|
||||
|------|--------|
|
||||
| API key mancante/errata | HTTP 401 `invalid_api_key` |
|
||||
| `template_name` non esiste | `TemplateNotFound` |
|
||||
| Nome template invalido | `InvalidTemplateName` |
|
||||
| Frontmatter YAML malformato | `InvalidFrontmatter` (con dettagli Pydantic) |
|
||||
| `required_variables` mancanti | `MissingVariables: [lista]` |
|
||||
| Variabile con tipo sbagliato | `InvalidVariableType: <var> expected <type>` |
|
||||
| Immagine base64 non decodificabile | `InvalidImageEncoding: <var>` |
|
||||
| Mime image non supportato | `UnsupportedImageMime` |
|
||||
| Immagine > `MAX_IMAGE_SIZE_MB` | `ImageTooLarge` |
|
||||
|
||||
### Errori di generazione (post-validazione)
|
||||
|
||||
| Caso | Comportamento |
|
||||
|------|---------------|
|
||||
| I/O salvataggio asset | Rollback file già scritti, `AssetStorageError` |
|
||||
| OpenRouter timeout | Retry 3x backoff (1s, 2s, 4s), poi `LLMTimeout` |
|
||||
| OpenRouter 5xx | Retry 3x, poi `LLMUpstreamError` |
|
||||
| OpenRouter 401/403 (config server) | No retry, `LLMAuthError` |
|
||||
| OpenRouter 429 | Retry backoff lungo (5s, 10s, 20s), poi `LLMRateLimit` |
|
||||
| Risposta non parseable | `LLMInvalidResponse` |
|
||||
| Risposta vuota | `LLMEmptyResponse` |
|
||||
|
||||
Ogni fallimento post-validazione → record `generations` con `success=0`. Asset efimeri non rimossi (scadranno col TTL).
|
||||
|
||||
### Errori tool CRUD template
|
||||
|
||||
| Caso | Errore |
|
||||
|------|--------|
|
||||
| Create su nome esistente | `TemplateAlreadyExists` |
|
||||
| Update/delete su nome inesistente | `TemplateNotFound` |
|
||||
| I/O filesystem | `TemplateStorageError` |
|
||||
|
||||
### Errori serving asset HTTP
|
||||
|
||||
| Caso | Codice |
|
||||
|------|--------|
|
||||
| Missing/invalid API key | 401 |
|
||||
| Path traversal | 400 |
|
||||
| `/assets/<template>/<file>` non trovato | 404 |
|
||||
| `/generated/<gen_id>/<file>`: record `ephemeral_assets` scaduto o `gen_id`/file assente | 410 Gone |
|
||||
|
||||
Semantica: gli asset dei template sono persistenti, la loro assenza è 404. Gli asset generati sono effimeri: assenti = già esistiti o mai esistiti → 410 Gone in entrambi i casi (il client riconosce che è scaduto o invalido, non deve riprovare con path diverso).
|
||||
|
||||
### Filosofia
|
||||
|
||||
- Nessuna fallback silenziosa. Validazione strict → errore esplicito, niente chiamata LLM sprecata.
|
||||
- Retry solo su errori transitori upstream. Mai su 4xx.
|
||||
- Codici errore sono stringhe ben definite: il client sa cosa correggere.
|
||||
- Errori di config server (LLM auth, fs non scrivibile) → log ERROR, 500 con codice.
|
||||
|
||||
---
|
||||
|
||||
## 8. Strategia di test
|
||||
|
||||
### Piramide
|
||||
|
||||
```
|
||||
┌────────────┐
|
||||
│ E2E (1-2)│ Container Docker reale
|
||||
├────────────┤
|
||||
│ Integration│ FastMCP test client + mock OpenRouter
|
||||
├────────────┤
|
||||
│ Unit │ Moduli isolati
|
||||
└────────────┘
|
||||
```
|
||||
|
||||
### Unit
|
||||
|
||||
- `test_template_store.py` — CRUD, nomi invalidi, frontmatter malformato, gestione asset dir.
|
||||
- `test_renderer.py` — validazione strict, shape image, rewrite asset paths, assemblaggio prompt, LLM mock per verifica modello scelto.
|
||||
- `test_auth.py` — header presente/assente, `/health` senza auth.
|
||||
- `test_generation_store.py` — insert, query, TTL cleanup con `expires_at` forzato.
|
||||
- `test_llm_client.py` — retry su 5xx/timeout/429, no retry su 4xx, parsing usage.
|
||||
|
||||
### Integration
|
||||
|
||||
- `test_mcp_tools.py` — tool call end-to-end via FastMCP test client, ogni tool happy path + error paths.
|
||||
- `test_http_routes.py` — assets auth, generated fresco vs scaduto (410), health sempre 200.
|
||||
- `test_document_generate_flow.py` — create template → generate → verifica record + file + asset URL 200.
|
||||
|
||||
### E2E
|
||||
|
||||
- `test_e2e_docker.py` — build container, compose up, client MCP reale, OpenRouter mockato via env var URL.
|
||||
|
||||
### Copertura
|
||||
|
||||
Target 85%+ sui moduli logici. `main.py` esente.
|
||||
|
||||
### Cosa NON testiamo
|
||||
|
||||
- OpenRouter reale (costi, flaky).
|
||||
- Qualità output LLM (responsabilità del modello).
|
||||
- Performance/load (inserire se nascerà bisogno).
|
||||
|
||||
### Fixture principali
|
||||
|
||||
- `tmp_data_dir` — DATA_DIR pulito per test.
|
||||
- `fake_template` — factory template valido.
|
||||
- `mock_openrouter` — respx con risposta + usage.
|
||||
- `client` — FastMCP test client autenticato.
|
||||
|
||||
---
|
||||
|
||||
## 9. Deploy e operatività
|
||||
|
||||
### Container
|
||||
|
||||
- Dockerfile basato su `python:3.11-slim`.
|
||||
- `uv` per installazione dipendenze.
|
||||
- Utente non-root.
|
||||
- Porta interna esposta: 8000.
|
||||
- Volume: `/data`.
|
||||
|
||||
### docker-compose
|
||||
|
||||
```yaml
|
||||
services:
|
||||
docugen-mcp:
|
||||
build: .
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8000:8000"
|
||||
env_file: .env
|
||||
volumes:
|
||||
- ./data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
||||
interval: 30s
|
||||
```
|
||||
|
||||
### TLS / reverse proxy
|
||||
|
||||
Fuori scope del container. Esempi:
|
||||
- Traefik con label Docker.
|
||||
- Nginx con Let's Encrypt.
|
||||
- Caddy (TLS automatico).
|
||||
|
||||
Il reverse proxy termina HTTPS e inoltra a `docugen-mcp:8000`. `PUBLIC_BASE_URL` nell'env var deve puntare al dominio esterno (`https://mcp.tuavps.com`).
|
||||
|
||||
### Configurazione client MCP
|
||||
|
||||
Claude Code:
|
||||
```bash
|
||||
claude mcp add --transport http docugen-mcp https://mcp.tuavps.com/mcp \
|
||||
--header "Authorization: Bearer <api-key>"
|
||||
```
|
||||
|
||||
Claude Desktop: entry in `claude_desktop_config.json` con URL + header.
|
||||
|
||||
### Backup
|
||||
|
||||
- Cartella `{DATA_DIR}/templates/` → rsync periodico o copia nel git.
|
||||
- `docugen_mcp.db` → dump SQLite regolare.
|
||||
- `{DATA_DIR}/generated/` → effimero, non critico.
|
||||
|
||||
### Osservabilità
|
||||
|
||||
- Log strutturati (JSON) su stdout. Tracciamento via docker logs.
|
||||
- Metriche dai record `generations`: spesa mensile, success rate, latenza media per modello. Query SQL ad-hoc o piccolo endpoint admin in fasi successive.
|
||||
|
||||
---
|
||||
|
||||
## 10. Stack tecnologico
|
||||
|
||||
| Layer | Tecnologia |
|
||||
|-------|------------|
|
||||
| Runtime | Python 3.11+ |
|
||||
| Package manager | uv |
|
||||
| Validazione | Pydantic v2, Pydantic Settings |
|
||||
| MCP | `mcp` SDK (FastMCP) |
|
||||
| HTTP (sub-app) | FastAPI |
|
||||
| Client HTTP | httpx (async) |
|
||||
| Retry | httpx + wrapper custom (retry exponential backoff) |
|
||||
| DB | SQLite via aiosqlite |
|
||||
| Immagini | Pillow (validazione mime/dimensione) |
|
||||
| Test | pytest, pytest-asyncio, respx |
|
||||
| Deploy | Docker, docker-compose |
|
||||
| Reverse proxy | Nginx/Traefik/Caddy (fuori container) |
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user