commit 2f722f541cdd6081d2dd786ed7a046ec43a0e554 Author: AdrianoDev Date: Tue Apr 21 11:54:46 2026 +0200 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) diff --git a/Docs/docugen-mcp/2026-04-21-design.md b/Docs/docugen-mcp/2026-04-21-design.md new file mode 100644 index 0000000..ad25212 --- /dev/null +++ b/Docs/docugen-mcp/2026-04-21-design.md @@ -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// │ +│ │ ├── template.md │ +│ │ └── assets/* │ +│ ├── generated// │ +│ │ └── . (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 ` 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//`. +- 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/` → `{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/ +│ └── / +│ ├── template.md # frontmatter + body +│ └── assets/ +│ └── logo.png # risorse statiche del template +├── generated/ +│ └── / # uuid4, uno per generazione +│ └── . # 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": "", + "markdown": "...output MD...", + "model": "anthropic/claude-sonnet-4", + "tokens": {"in": 1234, "out": 2345}, + "cost_usd": 0.0123, + "ephemeral_assets_urls": [ + "https://mcp.vps/generated//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//. + - 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 `![alt](https://mcp.vps/assets/...)`: + +- `GET /assets/