7b169fb8db
Layout stile CerberoSuite/Cerbero: - services/ per MCP workspace members (futuri: mcp-docugen, mcp-convert, mcp-inbox) - docs/ flat con design+implementation per componente - scripts/, secrets/ placeholder - pyproject.toml root (uv workspace vuoto + ruff + pytest) - .gitignore, .env.example, .mcp.json.example, README.md Rinominate le doc: Docs/<comp>/2026-04-21-*.md -> docs/<comp>-*.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
601 lines
23 KiB
Markdown
601 lines
23 KiB
Markdown
# 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) |
|