Files
ArcaSuite/docs/mcp-docugen-design.md
T
Adriano 7b169fb8db chore: organize repo as uv workspace multi-MCP monorepo
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>
2026-04-21 12:06:38 +02:00

23 KiB

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
  2. Architettura generale
  3. Componenti interni
  4. Modello dati
  5. API MCP — tool esposti
  6. Dataflow document_generate
  7. Gestione errori
  8. Strategia di test
  9. Deploy e operatività
  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:
---
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:

{
  "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:

{
  "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:

{
  "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 ![alt](https://mcp.vps/assets/...):

  • 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

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:

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)