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 <Allegati> 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`).
This server cannot be deployed
Maintenance
ActivityStale
ResponsivenessNo issues