Skip to main content
Glama
sebaldar

mcp-ocr-anagrafica

by sebaldar

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

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.

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/.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

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 &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).

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);

  • 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:

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 --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)). 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 — 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 a ExtractionService.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 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

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).