mcp-ocr-anagrafica
Remote MCP server for extracting structured data from images, PDFs, and XML documents using LLM Vision models. Provides specialized tools for demographic and invoice data extraction with support for multi-part documents.
README
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. Questo README ne è la documentazione operativa; in caso di conflitto prevale la specifica.
Indice
- Architettura
- Tool disponibili
- Documenti multi-parte (fronte/retro)
- Responsabilità dei layer
- Flusso completo di una richiesta
- Protocollo MCP
- Configurazione
- Sicurezza dell'endpoint remoto
- Docker
- Reverse proxy nginx con TLS (certbot)
- Configurazione client MCP remoti
- Strategia di merge multipagina
- Considerazioni GDPR
- Estendibilità futura
- 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_invoiceusaparseInvoiceXml, 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.
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.
{
"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.
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/.p7mviene 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_PARTSviene 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
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)). 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.
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 unicoPOST /mcp. - Modalità stateless: nessuna gestione di sessione lato server,
enableJsonResponse: trueper ottenere risposte JSON dirette invece dello streaming SSE opzionale. - Non implementati: trasporto
stdio, trasporto legacy HTTP+SSE. - Entrambi i tool (
extract_demographics,extract_invoice) dichiaranoinputSchemaeoutputSchema(derivati da Zod) e restituiscono il risultato tramitestructuredContent, 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'interfacciaAuthStrategyper 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_PARTSpiù l'overhead base64 (una richiesta può contenere fino aMAX_DOCUMENT_PARTSparti immagine, es. fronte/retro, ciascuna fino aMAX_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-Forper 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 dichiarazioneDOCTYPEviene 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).
docker compose up -d --build
Il container:
- è stateless e condivisibile tra più client MCP contemporanei;
- non pubblica alcuna porta sulla rete pubblica:
docker-compose.ymllega la porta esterna (3501, porta interna sempre3000) esclusivamente all'indirizzo dell'interfaccia WireGuard locale (10.10.0.4,wg0), non a0.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);
- monta
/tmpcome tmpfs (docker-compose.yml). Motivo:pdftoppmnon è 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/tmpcome 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,/tmpsarebbe 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
3501sull'interfaccia WireGuard locale (wg0, IP10.10.0.4; porta interna sempre3000, 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:
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):
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) per lo schema
di path-routing multi-MCP); richiede comunque l'header
Authorization: Bearer <MCP_AUTH_TOKEN>.
Claude Desktop (claude_desktop_config.json):
{
"mcpServers": {
"ocr-anagrafica": {
"url": "https://mcp.baldoweb.it/ocr",
"headers": {
"Authorization": "Bearer <MCP_AUTH_TOKEN>"
}
}
}
}
Claude Code (CLI):
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 --helpper 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)).
Un fronte/retro produce quindi esattamente due "pagine", elaborate ed
unificate con le stesse regole di seguito:
- Per ogni campo vince il valore proveniente dalla pagina con
confidencepiù alta. - Se due pagine forniscono valori diversi e non nulli per uno stesso campo
identificativo, viene aggiunto un warning esplicito in
warningscon entrambi i valori in conflitto (il valore a confidence più alta viene comunque scelto). Perextract_demographics:partita_iva,codice_fiscale,ragione_sociale. Perextract_invoice:numero,cedente.partita_iva,cessionario.partita_iva. - La
confidencecomplessiva del risultato riflette il merge e non è mai superiore alla confidence del valore scelto per ciascun campo. - Il superamento di
MAX_PDF_PAGES(per un PDF) o diMAX_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 — 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.
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 aExtractionService.extractStructuredData(). Il core non viene modificato. Il supporto multi-parte (fronte/retro, vedi Documenti multi-parte (fronte/retro)) è 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 viaparseStructuredDocument, 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
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).
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.