Skip to main content
Glama
sebaldar

mcp-ocr-anagrafica

by sebaldar
README.md
# mcp-ocr-anagrafica

Server MCP remoto (Streamable HTTP), stateless e containerizzato, che espone
strumenti di estrazione dati strutturati da immagini, PDF e documenti XML
tramite modelli LLM con capacità Vision. `extract_demographics` ed
`extract_invoice` sono entrambi specializzazioni di un motore di estrazione
generico, pensati per l'inserimento di dati anagrafici e di fatture/note di
credito in un gestionale fiscale.

> **Documento vincolante**: l'architettura completa e i requisiti sono
> definiti in [`docs/SPEC.md`](docs/SPEC.md). Questo README ne è la
> documentazione operativa; in caso di conflitto prevale la specifica.

---

## Indice

- [Architettura](#architettura)
- [Tool disponibili](#tool-disponibili)
- [Documenti multi-parte (fronte/retro)](#documenti-multi-parte-fronteretro)
- [Responsabilità dei layer](#responsabilità-dei-layer)
- [Flusso completo di una richiesta](#flusso-completo-di-una-richiesta)
- [Protocollo MCP](#protocollo-mcp)
- [Configurazione](#configurazione)
- [Sicurezza dell'endpoint remoto](#sicurezza-dellendpoint-remoto)
- [Docker](#docker)
- [Reverse proxy nginx con TLS (certbot)](#reverse-proxy-nginx-con-tls-certbot)
- [Configurazione client MCP remoti](#configurazione-client-mcp-remoti)
- [Strategia di merge multipagina](#strategia-di-merge-multipagina)
- [Considerazioni GDPR](#considerazioni-gdpr)
- [Estendibilità futura](#estendibilità-futura)
- [Comandi di sviluppo](#comandi-di-sviluppo)

---

## Architettura

Il sistema **non** è progettato esclusivamente per l'anagrafica. Il nucleo è
un motore generico di estrazione strutturata:

```
ExtractionService.extractStructuredData(document, prompt, schema) -> T
```

I tool MCP `extract_demographics` ed `extract_invoice` sono entrambi
specializzazioni di questo motore: ciascuno fornisce solo un prompt di
sistema (`src/prompts/*.system.md`) e uno schema Zod
(`src/schemas/*.schema.ts`). In futuro, nuovi tool (`extract_receipt`,
`extract_identity_card`, `extract_shipping_label`, ...) potranno essere
aggiunti allo stesso modo, **senza modificare il core**.

Il layer di trasporto (HTTP) è isolato dal core: né i servizi, né i
provider, né gli schemi conoscono l'esistenza di HTTP, Express o del
protocollo MCP.

Il motore non è "sempre Vision": internamente sceglie tra due strategie di
estrazione in base al MIME type reale del documento, entrambe dietro la
stessa interfaccia `ExtractionStrategy`.

- **`VisionExtractionStrategy`** (immagine/PDF): la pipeline OCR classica —
  rendering PDF se necessario, chiamata al provider Vision con retry,
  validazione Zod dell'output del modello.
- **`ParserExtractionStrategy`** (XML, incluso XML firmato `.p7m`): lettura
  deterministica, senza alcuna chiamata LLM. Non esiste un mapper XML
  generico (a differenza del ramo Vision, dove prompt + schema bastano per
  qualunque dominio): ogni tool che vuole accettare XML fornisce il proprio
  mapper (`extract_invoice` usa `parseInvoiceXml`, il parser FatturaPA). Un
  tool che non lo fornisce (es. `extract_demographics`) riceve un rifiuto
  esplicito (`unsupported_mime_type_for_tool`) se gli viene passato un
  documento XML/`.p7m`, mai un comportamento silenzioso o un tentativo di
  interpretarlo come immagine.

`ExtractionService.selectStrategy()` è l'unico punto che conosce
l'esistenza di entrambe le strategie; nessun'altra parte del sistema sa
quale sia stata usata per produrre un dato risultato.

```mermaid
flowchart TB
    subgraph transport["Transport layer (src/mcp)"]
        HTTP["http.transport.ts<br/>Streamable HTTP · POST /mcp"]
        AUTH["bearer.middleware.ts<br/>AuthStrategy"]
        TOOL1["demographics.tool.ts"]
        TOOL2["invoice.tool.ts"]
        POST["anagrafica.postprocess.ts<br/>(condiviso dai due tool)"]
    end

    subgraph core["Core (nessuna dipendenza da HTTP)"]
        EXT["ExtractionService<br/>extractStructuredData() · selectStrategy()"]
        MERGE["MergeService"]
        subgraph strategies["ExtractionStrategy"]
            VSTRAT["VisionExtractionStrategy<br/>immagine / PDF"]
            PSTRAT["ParserExtractionStrategy<br/>XML / .p7m"]
        end
        PROV["VisionProvider (interfaccia)<br/>AnthropicVisionProvider"]
        DOC["Document Loader<br/>base64 / url + SSRF guard"]
        PDF["PDF Renderer<br/>(pdftoppm)"]
        XMLP["xml.parser.ts<br/>FatturaPA -> schema, no DTD/entità esterne"]
        P7M["p7m.unwrapper.ts<br/>solo estrazione, nessuna verifica firma"]
        SCHEMA["Zod Schemas<br/>input / output"]
        NORM["Normalizers"]
        VALID["Domain Validators<br/>P.IVA · CF · SDI · EAN · Natura IVA"]
    end

    CLIENT["Client MCP remoto<br/>(Claude Desktop / Claude Code)"] -->|Bearer token| HTTP
    HTTP --> AUTH --> TOOL1
    AUTH --> TOOL2
    TOOL1 --> POST
    TOOL2 --> POST
    TOOL1 --> EXT
    TOOL2 --> EXT
    EXT --> DOC
    EXT --> MERGE
    EXT --> strategies
    VSTRAT --> PDF
    VSTRAT --> PROV
    PSTRAT --> P7M
    PSTRAT --> XMLP
    EXT --> SCHEMA
    POST --> NORM
    POST --> VALID
    TOOL2 --> VALID
    PROV -.implementa.-> AI["Anthropic Vision API"]
```

## Tool disponibili

| Tool                   | Formati accettati                                     | Strategia                                                  | Output                                                                                                                                         |
| ---------------------- | ----------------------------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `extract_demographics` | `jpg`, `jpeg`, `png`, `webp`, `pdf`                   | Vision                                                     | Anagrafica: ragione sociale/nome/cognome, P.IVA, CF, indirizzo, contatti, REA, SDI                                                             |
| `extract_invoice`      | `jpg`, `jpeg`, `png`, `webp`, `pdf`, `xml`, `xml.p7m` | Vision (immagine/PDF) · Parser deterministico (XML/`.p7m`) | Fattura/nota di credito/nota di debito: cedente, cessionario, righe (codice, EAN, descrizione, quantità, prezzo, aliquota IVA), riepiloghi IVA |

Entrambi restituiscono `confidence`, `confidence_fields` (mappa
campo → confidence) e `warnings` (array di stringhe), mai valori inventati:
un campo assente nel documento è sempre `null`, mai una stringa vuota.

Per `extract_invoice` su XML/`.p7m`, `confidence` è sempre `1` per ogni
campo estratto: la lettura è deterministica, non c'è ambiguità visiva da
esprimere (a differenza dell'OCR, dove la confidence riflette l'incertezza
del modello).

## Documenti multi-parte (fronte/retro)

Per i formati immagine (non PDF, non XML/`.p7m`), il parametro `document`
accetta **un singolo oggetto oppure un array non vuoto** di 1..
`MAX_DOCUMENT_PARTS` oggetti — pensato per i casi in cui un unico documento
fisico è catturato in più foto: fronte/retro di una carta d'identità, di
una patente, di un biglietto da visita.

```json
{
  "document": [
    { "type": "base64", "data": "<fronte>", "mimeType": "image/jpeg" },
    { "type": "base64", "data": "<retro>", "mimeType": "image/jpeg" }
  ]
}
```

Ogni parte viene inviata al provider Vision separatamente (come se fosse
una pagina PDF) e i risultati vengono unificati da `MergeService` con la
stessa identica strategia campo-per-campo già usata per il PDF
multipagina — vedi [Strategia di merge multipagina](#strategia-di-merge-multipagina).
Non esiste una nozione di "questa è la faccia anteriore": il merge
semplicemente prende, per ogni campo, il valore con confidence più alta tra
tutte le parti fornite.

Vincoli:

- Ammesso **solo** per parti tutte di tipo immagine (`jpg`/`jpeg`/`png`/`webp`).
  Un array contenente un PDF o un documento XML/`.p7m` viene rifiutato
  esplicitamente (`mixed_or_unsupported_document_parts`): un PDF ha già il
  proprio meccanismo multipagina, un documento strutturato è per natura
  un'unica unità.
- Un array che supera `MAX_DOCUMENT_PARTS` viene rifiutato esplicitamente
  (`too_many_document_parts`), prima di caricare o elaborare qualunque
  parte.
- Un singolo oggetto (senza array) continua a funzionare esattamente come
  prima — nessuna modifica per i client esistenti.

Questa capacità vive interamente in `ExtractionService`/`documentInputOrPartsSchema`,
non nei singoli tool: sia `extract_demographics` che `extract_invoice` la
ereditano senza alcuna modifica al proprio file, e un futuro
`extract_identity_card` la avrebbe fin dal primo giorno.

## Responsabilità dei layer

| Layer               | Percorso                                                                          | Responsabilità                                                                                                                                       |
| ------------------- | --------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| Config              | `src/config/env.ts`                                                               | Unica fonte delle variabili d'ambiente, validate con Zod all'avvio (fail-fast)                                                                       |
| MCP transport       | `src/mcp/transport/http.transport.ts`                                             | Endpoint `POST /mcp`, Streamable HTTP stateless                                                                                                      |
| MCP auth            | `src/mcp/auth/bearer.middleware.ts`                                               | Verifica del bearer token in tempo costante, interfaccia `AuthStrategy`                                                                              |
| MCP tools           | `src/mcp/tools/*.tool.ts`                                                         | Dichiarazione `inputSchema`/`outputSchema`, delega totale la logica a `ExtractionService`. Nessuna logica di business qui                            |
| MCP tools (glue)    | `src/mcp/tools/anagrafica.postprocess.ts`                                         | Normalizzazione + validator di dominio per un record anagrafico, condivisa da `extract_demographics` e da cedente/cessionario di `extract_invoice`   |
| Providers           | `src/providers/*.provider.ts`                                                     | Interfaccia `VisionProvider` e implementazione `AnthropicVisionProvider`. Il resto del sistema conosce solo l'interfaccia                            |
| Services            | `src/services/extraction.service.ts`, `merge.service.ts`                          | Orchestrazione della pipeline di estrazione (inclusa la scelta della strategy) e strategia di merge multipagina                                      |
| Services (strategy) | `src/services/extraction-strategy.ts`, `vision.strategy.ts`, `parser.strategy.ts` | Interfaccia `ExtractionStrategy` e le sue due implementazioni (Vision, Parser deterministico)                                                        |
| Document            | `src/document/loader.ts`, `base64.loader.ts`, `url.loader.ts`, `pdf.renderer.ts`  | Caricamento documento (base64/URL con protezioni SSRF) e rendering PDF via `pdftoppm`                                                                |
| Document (XML/P7M)  | `src/document/xml.parser.ts`, `p7m.unwrapper.ts`                                  | Parsing FatturaPA deterministico (no DTD/entità esterne, allegati scartati) ed estrazione del contenuto da una busta `.p7m` (nessuna verifica firma) |
| Prompts             | `src/prompts/*.system.md`                                                         | Prompt di sistema versionati come file, mai hardcodati nel codice                                                                                    |
| Schemas             | `src/schemas/*.schema.ts`                                                         | Contratti Zod di input/output                                                                                                                        |
| Normalizers         | `src/normalizers/*`                                                               | Normalizzazione dei campi estratti (ragione sociale, P.IVA, indirizzo, telefono, email)                                                              |
| Validators          | `src/validators/*`                                                                | Validatori di dominio (P.IVA, Codice Fiscale, SDI, EAN, Natura IVA, ...): non scartano il valore, segnalano in `warnings`                            |
| Utils               | `src/utils/*`                                                                     | Sanitizzazione, retry con backoff, rilevamento MIME/magic bytes (incluso XML/`.p7m`), hash SHA-256, logger                                           |

## Flusso completo di una richiesta

```mermaid
sequenceDiagram
    participant C as Client MCP
    participant T as HTTP Transport (/mcp)
    participant A as Auth (Bearer)
    participant X as ExtractionService
    participant L as Document Loader
    participant R as PDF Renderer
    participant V as VisionProvider
    participant M as MergeService

    C->>T: POST /mcp (tools/call extract_demographics)
    T->>A: verifica Authorization: Bearer <token>
    alt token assente/non valido
        A-->>C: 401 (nessun dettaglio)
    else token valido
        A->>X: invoca extractStructuredData(document, prompt, schema)
        X->>L: carica ogni parte (una o più, es. fronte/retro)
        Note over L: SSRF guard se URL,<br/>verifica magic bytes vs MIME dichiarato,<br/>per ciascuna parte
        L-->>X: buffer per ogni parte
        opt una parte è un PDF
            X->>R: pdftoppm (rendering per pagina, limite MAX_PDF_PAGES)
            R-->>X: immagini per pagina
        end
        loop per ogni pagina
            X->>V: chiamata Vision (con retry/backoff su timeout, 429, 503)
            V-->>X: output grezzo
            X->>X: sanitizzazione + validazione Zod
        end
        X->>M: merge multipagina (campo per campo, confidence)
        M-->>X: risultato unificato + warnings
        X-->>T: structuredContent
        T-->>C: 200 JSON (structuredContent)
    end
```

Questo è il flusso quando il documento è un'immagine o un PDF (entrambi i
tool). Se `document` è un array (fronte/retro), ogni parte viene caricata e
poi trattata esattamente come una pagina: il "loop per ogni pagina" scorre
sull'unione di tutte le pagine di tutte le parti (vedi [Documenti
multi-parte (fronte/retro)](#documenti-multi-parte-fronteretro)). Per
`extract_invoice` con un documento XML o `.p7m`, la pipeline è diversa a
valle del Document Loader: nessun rendering PDF, nessuna chiamata al
provider Vision, nessun retry.

```mermaid
sequenceDiagram
    participant X as ExtractionService
    participant L as Document Loader
    participant P as ParserExtractionStrategy
    participant U as p7m.unwrapper.ts
    participant XP as xml.parser.ts (parseInvoiceXml)

    X->>L: carica documento (base64 o URL)
    Note over L: verifica magic bytes:<br/>dichiarazione XML oppure struttura ASN.1/OID PKCS#7
    L-->>X: buffer + mimeType (application/xml | application/pkcs7-mime)
    X->>P: selectStrategy() ha scelto ParserExtractionStrategy
    opt mimeType è application/pkcs7-mime
        P->>U: estrae il contenuto dalla busta CMS SignedData
        Note over U: nessuna verifica di firma/certificato:<br/>fuori scope, fuori dal perimetro OCR
        U-->>P: XML originale
    end
    P->>XP: parseInvoiceXml(xml)
    Note over XP: DOCTYPE rifiutato, blocco &lt;Allegati&gt; scartato<br/>prima di ogni elaborazione, mai loggato/restituito
    XP-->>P: dati mappati (confidence 1 per ogni campo)
    P->>P: validazione Zod (stesso schema del ramo Vision)
    P-->>X: risultato (un solo "page", MergeService lo restituisce invariato)
```

## Protocollo MCP

- SDK ufficiale `@modelcontextprotocol/sdk`, nessuna implementazione manuale
  del protocollo.
- Trasporto: **solo** Streamable HTTP (`StreamableHTTPServerTransport`),
  endpoint unico `POST /mcp`.
- Modalità **stateless**: nessuna gestione di sessione lato server,
  `enableJsonResponse: true` per ottenere risposte JSON dirette invece
  dello streaming SSE opzionale.
- **Non** implementati: trasporto `stdio`, trasporto legacy HTTP+SSE.
- Entrambi i tool (`extract_demographics`, `extract_invoice`) dichiarano
  `inputSchema` e `outputSchema` (derivati da Zod) e restituiscono il
  risultato tramite `structuredContent`, così il client riceve JSON
  tipizzato nativamente senza dover ri-parsare testo libero.

## Configurazione

Tutte le variabili sono validate con Zod all'avvio (`src/config/env.ts`):
configurazione invalida ⇒ il processo termina immediatamente con un errore
chiaro (fail-fast). Nessun `process.env.X` è utilizzato fuori da questo
modulo.

Copiare `.env.example` in `.env` e valorizzare:

| Variabile                 | Descrizione                                                   | Default          |
| ------------------------- | ------------------------------------------------------------- | ---------------- |
| `ANTHROPIC_API_KEY`       | API key del provider Anthropic                                | — (obbligatoria) |
| `VISION_MODEL`            | Modello Vision Anthropic da utilizzare                        | — (obbligatoria) |
| `MCP_AUTH_TOKEN`          | Bearer token richiesto su `/mcp`                              | — (obbligatoria) |
| `HTTP_PORT`               | Porta di ascolto HTTP interna                                 | `3000`           |
| `TRUST_PROXY`             | Rispetta `X-Forwarded-For` (dietro reverse proxy)             | `true`           |
| `RATE_LIMIT_WINDOW_MS`    | Finestra del rate limit                                       | `60000`          |
| `RATE_LIMIT_MAX_REQUESTS` | Richieste massime per finestra (per IP e per token)           | `30`             |
| `MAX_FILE_SIZE_MB`        | Dimensione massima **per parte** di documento                 | `10`             |
| `MAX_PDF_PAGES`           | Numero massimo di pagine PDF elaborabili                      | `10`             |
| `PDF_RENDER_DPI`          | Risoluzione di rendering PDF (`pdftoppm`)                     | `150`            |
| `MAX_DOCUMENT_PARTS`      | Numero massimo di parti immagine per richiesta (fronte/retro) | `4`              |
| `REQUEST_TIMEOUT_MS`      | Timeout richiesta HTTP/provider                               | `60000`          |
| `MAX_RETRIES`             | Tentativi massimi di retry (backoff esponenziale + jitter)    | `3`              |
| `LOG_LEVEL`               | Livello di log Pino (`debug`/`info`/`warn`/`error`)           | `info`           |

## Sicurezza dell'endpoint remoto

Il server è esposto su internet e processa dati personali: la sicurezza del
confine HTTP è un requisito primario.

- **Autenticazione**: header `Authorization: Bearer <token>` obbligatorio su
  `/mcp`, confrontato in tempo costante (`crypto.timingSafeEqual`).
  Richieste senza token o con token errato ⇒ `401`, senza dettagli interni.
  L'architettura predispone un'interfaccia `AuthStrategy` per una futura
  migrazione a OAuth 2.1 (non implementata in questa versione).
- **Rate limiting**: per IP e per token, configurabile via env.
- **Limite dimensione body**: applicato _prima_ del parsing JSON, coerente
  con `MAX_FILE_SIZE_MB × MAX_DOCUMENT_PARTS` più l'overhead base64 (una
  richiesta può contenere fino a `MAX_DOCUMENT_PARTS` parti immagine, es.
  fronte/retro, ciascuna fino a `MAX_FILE_SIZE_MB`).
- **Timeout** di richiesta a livello HTTP.
- **Header di sicurezza** standard (helmet).
- **Nessuna informazione interna** (stack trace, path, versioni) negli
  errori restituiti al client.
- **TLS**: il container espone solo HTTP in chiaro sulla rete interna; la
  terminazione TLS è demandata a un reverse proxy esterno (vedi sezione
  nginx). Il server rispetta `X-Forwarded-For` per il rate limiting dietro
  proxy (`TRUST_PROXY`).
- **SSRF** (caricamento documento da URL): solo schema `https`, risoluzione
  DNS con blocco di IP privati/loopback/link-local e metadata endpoint
  (`169.254.0.0/16`, `10/8`, `172.16/12`, `192.168/16`, `::1`), blocco dei
  redirect verso IP privati, timeout e limite dimensione sul download.
- **XML (`extract_invoice`)**: qualunque documento con dichiarazione
  `DOCTYPE` viene rifiutato esplicitamente, prima di qualunque parsing
  (protezione XXE/entity-expansion in profondità — il parser XML usato
  [`fast-xml-parser`] non risolve comunque entità esterne per progettazione,
  ma il rifiuto esplicito non dipende da questa garanzia implicita). Il
  blocco `<Allegati>` di una FatturaPA viene scartato dalla stringa XML
  **prima** di qualunque elaborazione: il suo contenuto (spesso un
  allegato base64 di dimensioni significative) non diventa mai un valore in
  memoria, non compare nell'output, non viene mai loggato.
- **`.p7m` (busta CMS/PKCS#7)**: viene solo **estratto** il contenuto
  originale firmato. **Non viene eseguita alcuna verifica della firma**
  (certificato, catena di fiducia, scadenza, revoca): è una scelta di
  design, non una lacuna — la validità legale di una firma digitale è una
  valutazione che spetta al gestionale/operatore a valle, non a un servizio
  di estrazione OCR. Solo le buste CMS SignedData con contenuto incorporato
  sono supportate; le firme distaccate (detached) sono rifiutate
  esplicitamente.

## Docker

Build multi-stage: stage di build (dipendenze complete, compilazione
TypeScript) e stage runtime minimale (`node:20-bookworm-slim` +
`poppler-utils` per `pdftoppm`), eseguito come utente non-root, con
`HEALTHCHECK` su `GET /health` (non autenticato, senza informazioni
interne).

```bash
docker compose up -d --build
```

Il container:

- è **stateless** e condivisibile tra più client MCP contemporanei;
- **non pubblica alcuna porta** sulla rete pubblica: `docker-compose.yml`
  lega la porta esterna (`3501`, porta interna sempre `3000`) esclusivamente
  all'indirizzo dell'interfaccia WireGuard locale (`10.10.0.4`, `wg0`), non a
  `0.0.0.0`;
- è raggiungibile solo dal reverse proxy (nginx) che sta dall'altro capo del
  tunnel WireGuard, su una VPS separata — vedi [Reverse proxy nginx con TLS
  (certbot)](#reverse-proxy-nginx-con-tls-certbot);
- monta `/tmp` come **tmpfs** (`docker-compose.yml`). Motivo: `pdftoppm` non
  è in grado di scrivere l'immagine renderizzata su stdout (l'argomento `-`
  come prefisso di output viene trattato come nome di file letterale, non
  come richiesta di stream), quindi ogni pagina viene scritta in un file
  temporaneo e riletta in memoria. Montando `/tmp` come tmpfs quei byte
  restano esclusivamente in RAM e non toccano mai uno storage persistente,
  in coerenza con il vincolo "documenti mai persistiti su disco". **Questo
  mount è obbligatorio in produzione**: senza tmpfs, `/tmp` sarebbe disco
  reale del filesystem del container.

## Reverse proxy nginx con TLS (certbot)

> **Nota sul deployment reale di questa istanza**: nginx e la gestione del
> certificato (certbot) **non fanno parte di questo progetto** e non vengono
> installati/gestiti da esso. Nel deployment attuale il reverse proxy e il
> TLS sono già gestiti su una VPS separata, indipendente da questo
> repository. Il container qui pubblica solo la porta `3501` sull'interfaccia
> WireGuard locale (`wg0`, IP `10.10.0.4`; porta interna sempre `3000`,
> numero esterno diverso perché su questo host convivono più server MCP) —
> irraggiungibile da chiunque non sia dall'altro capo del tunnel. La
> configurazione che segue è mantenuta come **esempio di riferimento
> generico** (richiesto da docs/SPEC.md) per chi clona questo progetto e non
> dispone già di un proprio reverse proxy con TLS.

Più server MCP possono condividere lo stesso dominio, instradati per
prefisso di path (`/ocr`, `/rag`, ...) verso upstream diversi, ciascuno sulla
propria porta `wg0`. Il backend espone sempre e solo l'endpoint fisso
`POST /mcp` (vincolo di docs/SPEC.md, non configurabile), quindi nginx deve
riscrivere il path esterno su `/mcp` in fase di `proxy_pass`, non limitarsi a
un passthrough:

```nginx
upstream mcp_ocr_anagrafica {
    server 10.10.0.4:3501;
}

# Esempio: un futuro secondo server MCP (es. RAG) sulla stessa VPS, sulla
# propria porta wg0 dedicata, seguirebbe lo stesso schema:
# upstream mcp_rag {
#     server 10.10.0.4:3502;
# }

server {
    listen 80;
    server_name mcp.baldoweb.it;

    # Redirect verso HTTPS, eccetto la sfida ACME di certbot
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }
    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl http2;
    server_name mcp.baldoweb.it;

    ssl_certificate     /etc/letsencrypt/live/mcp.baldoweb.it/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mcp.baldoweb.it/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;

    # Coerente con MAX_FILE_SIZE_MB × MAX_DOCUMENT_PARTS (10MB × 4) +
    # overhead base64 (~1.37x) - una richiesta può contenere più parti
    # immagine (fronte/retro)
    client_max_body_size 60m;

    # /ocr esterno -> /mcp interno (endpoint fisso del server OCR anagrafica)
    location = /ocr {
        proxy_pass http://mcp_ocr_anagrafica/mcp;
        proxy_http_version 1.1;

        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # Allineati a REQUEST_TIMEOUT_MS (60s di default)
        proxy_connect_timeout 10s;
        proxy_send_timeout    70s;
        proxy_read_timeout    70s;
    }

    location = /ocr/health {
        proxy_pass http://mcp_ocr_anagrafica/health;
        proxy_set_header Host $host;
    }

    # Esempio: un futuro MCP "rag" seguirebbe lo stesso schema, sul proprio
    # upstream/porta:
    # location = /rag {
    #     proxy_pass http://mcp_rag/mcp;
    #     ... (stessi header/timeout di cui sopra)
    # }
}
```

Rinnovo certificati con certbot (esempio, container dedicato o cron host):

```bash
certbot certonly --webroot -w /var/www/certbot -d mcp.baldoweb.it
```

## Configurazione client MCP remoti

Il server espone internamente un unico endpoint fisso `POST /mcp` (vincolo
di docs/SPEC.md). In questo deployment, il reverse proxy lo pubblica
all'esterno come `POST https://mcp.baldoweb.it/ocr` (vedi [Reverse proxy
nginx con TLS (certbot)](#reverse-proxy-nginx-con-tls-certbot) per lo schema
di path-routing multi-MCP); richiede comunque l'header
`Authorization: Bearer <MCP_AUTH_TOKEN>`.

**Claude Desktop** (`claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "ocr-anagrafica": {
      "url": "https://mcp.baldoweb.it/ocr",
      "headers": {
        "Authorization": "Bearer <MCP_AUTH_TOKEN>"
      }
    }
  }
}
```

**Claude Code** (CLI):

```bash
claude mcp add --transport http ocr-anagrafica \
  https://mcp.baldoweb.it/ocr \
  --header "Authorization: Bearer <MCP_AUTH_TOKEN>"
```

> La sintassi esatta del flag può variare in base alla versione della CLI:
> fare riferimento a `claude mcp add --help` per la sintassi corrente di
> registrazione di un server HTTP remoto con header custom.

Il client MCP **non deve contenere alcuna logica di business**: si limita a
inoltrare la chiamata al tool con il documento (base64 o URL) e a ricevere
`structuredContent` già validato.

## Strategia di merge multipagina

Si applica solo al ramo Vision (immagine/PDF): un documento XML o `.p7m`
viene letto come un unico documento, non ha un concetto di "pagina", quindi
`MergeService` riceve un solo risultato e lo restituisce invariato.

Le "pagine" che `MergeService` unifica possono avere due origini, trattate
in modo identico a valle: il rendering di un PDF multipagina, oppure le
parti di un documento multi-parte fornite direttamente dal client (vedi
[Documenti multi-parte (fronte/retro)](#documenti-multi-parte-fronteretro)).
Un fronte/retro produce quindi esattamente due "pagine", elaborate ed
unificate con le stesse regole di seguito:

1. Per ogni campo vince il valore proveniente dalla pagina con
   `confidence` più alta.
2. Se due pagine forniscono valori diversi e non nulli per uno stesso campo
   identificativo, viene aggiunto un warning esplicito in `warnings` con
   entrambi i valori in conflitto (il valore a confidence più alta viene
   comunque scelto). Per `extract_demographics`: `partita_iva`,
   `codice_fiscale`, `ragione_sociale`. Per `extract_invoice`: `numero`,
   `cedente.partita_iva`, `cessionario.partita_iva`.
3. La `confidence` complessiva del risultato riflette il merge e non è mai
   superiore alla confidence del valore scelto per ciascun campo.
4. Il superamento di `MAX_PDF_PAGES` (per un PDF) o di `MAX_DOCUMENT_PARTS`
   (per un documento multi-parte) produce un errore esplicito, senza
   elaborazione parziale silenziosa.

## Considerazioni GDPR

- I documenti **non vengono mai persistiti su disco**: l'elaborazione
  avviene esclusivamente in memoria, per l'intera pipeline. Unica eccezione
  tecnica: il rendering delle pagine PDF (`pdftoppm`) richiede un file di
  output reale (non supporta lo streaming su stdout); per questo motivo
  `/tmp` è montato come tmpfs (RAM) nel container — vedi sezione
  [Docker](#docker) — così anche questo passaggio intermedio non tocca mai
  storage persistente.
- Nessun dato personale (documenti, contenuto estratto, base64, URL
  completi) viene scritto nei log. È loggabile solo l'hash SHA-256 del
  documento come identificativo tecnico, insieme a request-id, tool
  invocato, esito, durata e numero di retry.
- I documenti caricati transitano verso il **provider AI configurato**
  (Anthropic). Questo comporta implicazioni di titolarità/responsabilità
  del trattamento (titolare/responsabile, eventuale subresponsabile) che
  devono essere valutate a livello contrattuale con il fornitore del
  provider, in base al caso d'uso specifico del gestionale che integra
  questo server.
- Il codice è predisposto per un futuro caching basato sull'hash SHA-256
  del documento, **non implementato in questa versione**. Un eventuale
  caching futuro conterrebbe dati personali e dovrà prevedere una politica
  di retention esplicita prima di essere attivato.
- Per `extract_invoice`, il blocco `<Allegati>` di una FatturaPA XML (spesso
  un secondo documento in base64, es. il DDT) viene scartato prima di
  qualunque elaborazione e non transita mai verso il provider Vision, non
  compare nell'output, non viene loggato — vedi
  [Sicurezza dell'endpoint remoto](#sicurezza-dellendpoint-remoto).

## Estendibilità futura

- **Nuovi tool di estrazione**: `extract_receipt`, `extract_identity_card`,
  `extract_shipping_label`, ... — ciascuno richiede solo un nuovo prompt
  (`src/prompts/*.system.md`), un nuovo schema Zod
  (`src/schemas/*.schema.ts`) e un nuovo tool MCP che delega a
  `ExtractionService.extractStructuredData()`. Il core non viene modificato.
  Il supporto multi-parte (fronte/retro, vedi [Documenti multi-parte
  (fronte/retro)](#documenti-multi-parte-fronteretro)) è già incluso fin dal
  primo giorno, senza bisogno di scriverlo di nuovo — è una proprietà del
  motore, non del singolo tool.
- **Nuovi formati strutturati (non-Vision)**: `ParserExtractionStrategy` è
  già generica rispetto al formato — non conosce FatturaPA, conosce solo
  "un mapper produce un oggetto, Zod lo valida". Un futuro formato
  strutturato diverso da XML (es. un tracciato a larghezza fissa, un CSV
  normato) richiederebbe solo un nuovo mapper passato via
  `parseStructuredDocument`, non una nuova strategy.
- **Nuovi provider Vision**: OpenAI, Gemini, Ollama, Mistral — ciascuno
  richiede solo una nuova implementazione dell'interfaccia `VisionProvider`.
  Il resto del sistema continua a conoscere solo l'interfaccia.
- **Nuovi trasporti**: l'aggiunta di trasporti futuri non richiede modifiche
  a servizi, provider o schemi, perché il core non conosce l'esistenza di
  HTTP.
- **OAuth 2.1**: l'interfaccia `AuthStrategy` è predisposta per accogliere
  una futura strategia OAuth 2.1 accanto a quella bearer-token attuale.

---

> Il server MCP costituisce il confine di sicurezza sia verso internet
> sia verso i sistemi legacy. Tutti i guardrail (autenticazione,
> rate limiting, validazione, limiti, timeout, sanitizzazione, retry,
> logging e controllo dell'output) risiedono nel server MCP e non nel client.

---

## Comandi di sviluppo

```bash
npm run dev         # avvio locale con watch (tsx)
npm run build       # compilazione TypeScript (dist/)
npm run lint        # ESLint
npm run typecheck   # tsc --noEmit
npm run test        # Vitest (unit + integration senza provider live)
npm run format      # Prettier (write)

docker compose up -d --build   # avvio containerizzato
```

I test di integrazione con provider AI reale sono opt-in tramite
`RUN_LIVE_TESTS=1` e non fanno parte del gate standard (`npm run test`).