Installare uno strumento di riconoscimento dei documenti in un editor richiede un minuto. Capire cosa farne dopo richiede di più. Questo articolo salta il giro della configurazione e passa subito a tre lavori che gli sviluppatori portano davvero a Claude Code e Cursor quando ci sono di mezzo dei passaporti:
- Trasformare una cartella di scansioni di passaporti in un CSV, in un modo che regga un'interruzione a metà strada.
- Confrontare un passaporto con il modulo di onboarding che state costruendo.
- Registrare una volta i risultati delle scansioni e usarli come fixture di test, così la vostra suite non chiama mai un servizio reale.
Tutti e tre usano il server MCP di doc.cheap, il pacchetto @doc-cheap/mcp (MIT, Node 20 o successivo). Questo è il blog di doc.cheap, l'API OCR per passaporti e carte d'identità dietro quel server, quindi valutate le scelte di prodotto di conseguenza. Gli schemi in sé (una directory di immagini recintata, una chiave per file, una fixture registrata una sola volta) valgono per qualsiasi strumento di questo tipo.
Cosa offre il server all'editor
Sei strumenti, ognuno con le indicazioni che un client legge prima di decidere se chiedervi conferma:
| Strumento | Cosa fa | Consuma un credito? |
|---|---|---|
scan_document |
Legge un passaporto, una carta d'identità o una patente e restituisce JSON strutturato | Solo quando un documento viene riconosciuto |
check_balance |
Crediti rimanenti e contatori del mese | No |
search_docs |
Cerca nella documentazione dell'API inclusa nel server, offline | No |
list_scans |
Elenca le scansioni salvate dell'account, dalla più recente | No |
get_scan |
Rilegge una scansione salvata tramite il suo id | No |
delete_scan |
Elimina definitivamente una scansione salvata | No |
scan_document riceve l'immagine come image_base64, image_url oppure, sul server locale, image_path, più le opzioni facoltative expect_country, return_portrait, retain_hours, reference e idempotency_key. Sono queste cinque opzioni a reggere gran parte dei flussi che seguono.
Senza una chiave impostata, il server usa la chiave sandbox pubblica: 10 documenti riconosciuti gratuiti per indirizzo in tutto, e al massimo 10 richieste per indirizzo all'ora. Basta per i flussi 2 e 3. Il flusso 1 richiede una chiave vostra; con la registrazione avete 100 riconoscimenti gratuiti ogni mese.
Se non avete mai collegato un server MCP a un agente, Far leggere documenti d'identità a un agente tramite MCP spiega le basi e approfondisce le questioni di privacy.
Flusso 1: una cartella di scansioni in un CSV, in Claude Code
Il lavoro. Qualcuno vi consegna una cartella di scansioni di passaporti (gli ospiti arretrati di un hotel, l'elenco dei partecipanti a un evento, una migrazione da un archivio cartaceo) e vuole un foglio di calcolo: una riga per file, i campi che contano e una nota chiara per ogni file che non si è potuto leggere.
La configurazione. Mettete un .mcp.json nella radice del progetto. Claude Code lo legge come l'elenco dei server del progetto, quindi la configurazione viaggia con il repository:
{
"mcpServers": {
"doc-cheap": {
"command": "npx",
"args": ["-y", "@doc-cheap/mcp"],
"env": {
"DOC_CHEAP_API_KEY": "${DOC_CHEAP_API_KEY:-}",
"DOC_CHEAP_IMAGE_ROOT": "/Users/you/guest-backlog/scans"
}
}
}
}
La prima volta che Claude Code si avvia in quella cartella, vi chiede di approvare il server di .mcp.json.
Due righe fanno il lavoro:
DOC_CHEAP_API_KEYviene presa dalla vostra shell, quindi nessuna chiave finisce in un commit. Quando la variabile non è impostata, il valore è vuoto e il server ripiega sulla chiave sandbox.DOC_CHEAP_IMAGE_ROOTè l'unica directory da cui il server può leggere file. Senza di essa,image_pathviene rifiutato del tutto. Con essa, un percorso viene prima risolto attraverso i link simbolici, e tutto ciò che esce dalla directory (un.., un link che punta fuori) viene rifiutato. Unimage_pathrelativo viene preso da questa directory, quindi l'agente può passare i soli nomi dei file.
Qui quel recinto conta più che altrove. I nomi dei file arrivano dall'agente, e l'agente sta leggendo una cartella che non avete creato voi. Inoltre le immagini non passano mai per la conversazione: il server legge il file e lo invia direttamente all'API.
Il prompt.
Chiama prima check_balance. Poi, per ogni JPEG e PNG nella directory delle scansioni, chiama scan_document con image_path impostato sul nome del file, reference impostato sul nome del file, idempotency_key impostato su "backlog-" più lo SHA-256 del file, return_portrait false e retain_hours 24. Scrivi backlog.csv con le colonne file, status, billed, kind, country, number, surname, given_names, birth_date, expiry_date, is_expired, mrz_status. Per ogni status diverso da recognized, lascia vuote le colonne dei dati. Alla fine, elenca i file non riconosciuti e il motivo.
Perché c'è ogni opzione:
referenceviene restituito nel campometa.referencedel risultato e compare in ogni riga dilist_scans, così ogni scansione salvata si può ricondurre al suo file senza aprirla.idempotency_keyè ciò che rende il lotto sicuro da riprendere. Con una chiave live, una ripetizione con la stessa chiave restituisce il primo risultato invece di riconoscere e addebitare di nuovo. Se la sessione si interrompe al file 140 di 300, rilanciate lo stesso prompt: i primi 139 tornano come riproduzioni senza costo, e il lavoro riprende da lì. È una chiave costruita dall'hash del file, e non un UUID casuale, a permettere a una nuova sessione di ricrearla. Claude Code può calcolarla conshasum -a 256.retain_hours: 24è il compromesso che la guida all'idempotenza descrive. Una riproduzione restituisce il risultato salvato, quindi una scansione fatta conretain_hours: 0non si può riprodurre: un nuovo tentativo con la sua chiave entro 24 ore viene rifiutato conidempotency_replay_unavailable. Una finestra di un giorno mantiene gratuiti i nuovi tentativi e il giorno dopo non lascia comunque nulla di salvato.return_portrait: falseesclude il ritaglio della foto del titolare, che a un CSV non serve.
Cosa torna. Lo status di ogni riga è uno tra recognized, no_document_found, unreadable, unsupported_document o rejected, e billed dice se quel file ha consumato un credito. Solo recognized viene addebitato, quindi una cartella con venti foto sfocate costa venti crediti in meno rispetto al numero dei suoi file. La colonna billed è anche il vostro conteggio di quanto ha speso il lotto, senza fatture da riconciliare.
Un'abitudine in più. Ora il CSV contiene dati personali. Aggiungetelo a .gitignore prima che l'agente lo scriva, non dopo.
Flusso 2: verificare un passaporto mentre costruite un modulo, in Cursor
Il lavoro. State costruendo un modulo di onboarding o di check-in, e volete vedere cosa restituisce una scansione reale prima di decidere nomi dei campi, formati e validazione.
La configurazione. Per un'occhiata veloce, Cursor può usare il server ospitato, che non richiede installazione. Nel file .cursor/mcp.json del progetto:
{
"mcpServers": {
"doc-cheap": {
"url": "https://mcp.doc.cheap/mcp"
}
}
}
Senza un'intestazione con la chiave, il server ospitato usa la chiave sandbox. Non può vedere i file sulla vostra macchina, quindi lì scan_document riceve l'immagine come image_base64 o come image_url pubblico https:. Se i vostri campioni sono file locali, avviate invece il server in locale. Cursor scrive le variabili con una sintassi propria, ${workspaceFolder} per la cartella del progetto e ${env:NAME} per una variabile della shell, quindi la voce del flusso 1 non si copia così com'è:
{
"mcpServers": {
"doc-cheap": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@doc-cheap/mcp"],
"env": {
"DOC_CHEAP_IMAGE_ROOT": "${workspaceFolder}/specimens"
}
}
}
}
Senza la riga DOC_CHEAP_API_KEY anche questa voce usa la chiave sandbox, e il server legge immagini solo dalla cartella specimens del progetto. Usate qui documenti sintetici o di esempio, mai il vostro passaporto.
Il prompt.
Scansiona td3-specimen.jpg con scan_document. Confronta il risultato con i campi di components/OnboardingForm.tsx e proponi modifiche perché ogni campo del modulo venga compilato dalla scansione. Usa search_docs per ogni campo su cui hai dubbi.
Cosa emerge di solito dal confronto, e cosa risponde la forma del risultato:
- I nomi arrivano separati.
holder.surnameeholder.given_namessono distinti, eholder.full_nameli unisce. Un unico campo "Nome completo" perde la separazione che il vostro back office vorrà in seguito. - Le date sono già in ISO
YYYY-MM-DD.holder.birth_dateedocument.expiry_datevanno dritte in un<input type="date">, il cui valore usa lo stesso formato. Niente parsing diDD.MM.YYYYné delle date a sei cifre della MRZ. - I paesi arrivano come codici con dettagli.
holder.nationalityè il codice a tre lettere del documento, eholder.nationality_detailaggiunge le forme ISO alpha-3 e alpha-2. Se il vostro selettore di paese usa codici a due lettere, mappate dall'oggetto di dettaglio invece di scrivere una tabella vostra. - La scadenza è già calcolata.
document.is_expirededocument.days_remaining(negativo una volta scaduto) permettono al modulo di avvisare di un passaporto scaduto senza calcoli sulle date. - Qualsiasi valore può essere null. Ogni chiave è sempre presente, e un valore sconosciuto è
null, mai una chiave mancante. Un modulo che presume l'arrivo di ogni campo si romperà al primo passaporto consumato. - Il verdetto sulla MRZ è separato dai campi.
mrz.statusvalepassed,failedoabsent. Dice se la zona a lettura ottica è stata letta e se le sue cifre di controllo tornano; non dice che il documento è autentico. Il gruppoauthenticitydel risultato riportanot_checked.
In questo ciclo search_docs merita di essere usato. Legge la documentazione inclusa nel server, funziona offline e non costa nulla, quindi "cosa significa unsupported_document" o "quali campi ha una carta d'identità" ricevono la risposta documentata invece di un'ipotesi.
Flusso 3: registrare le fixture di test una volta sola
Il lavoro. Il vostro codice chiama l'API, e i suoi test non dovrebbero farlo. Una suite che a ogni esecuzione invia un passaporto a un servizio reale è lenta, richiede una chiave in CI e, sulla sandbox, incontra presto i suoi limiti: 10 richieste all'ora e un rifiuto document_repeated per lo stesso documento inviato troppe volte.
La soluzione è registrare una volta ogni esito che il vostro codice deve gestire, salvarlo come JSON e far girare i test su quei file.
Il prompt, in Claude Code o in Cursor:
Scansiona td3-specimen.jpg e blank-page.png con scan_document e salva ogni risultato strutturato come test/fixtures/scan-
.json. Sostituisci ogni valore personale (nomi, numeri, date di nascita, righe MRZ) con un segnaposto dello stesso tipo, e rimuovi i data URL dall'oggetto images.
Il risultato strutturato dello strumento è lo stesso oggetto Scan che restituisce l'API HTTP, quindi la fixture corrisponde a ciò che il vostro codice riceverà in produzione. Una fixture mascherata appare così:
{
"meta": { "status": "recognized", "billed": false, "reference": null },
"document": { "kind": "passport", "country": "<ICAO3>", "number": "<NUMBER>", "expiry_date": "<YYYY-MM-DD>", "is_expired": false, "days_remaining": 0 },
"holder": { "surname": "<SURNAME>", "given_names": "<GIVEN NAMES>", "birth_date": "<YYYY-MM-DD>", "nationality": "<ICAO3>" },
"mrz": { "status": "passed", "reason": null, "lines": ["P<…", "…"], "text": "P<…" }
}
(Qui è accorciata; conservate ogni chiave che ha il risultato reale, così un test fallisce quando il vostro codice ne legge una che manca.)
Tre dettagli rendono oneste le fixture:
- Registrate anche i fallimenti. Una pagina bianca vi dà
no_document_foundconbilled: falsee i gruppi di dati anull. È quel file a cogliere il codice che presume che una scansione abbia sempre un titolare. - Sulla sandbox,
billedè una previsione. Lì non viene addebitato nulla, e il flag dice se la stessa scansione sarebbe stata addebitata con una chiave live. È proprio ciò su cui un test di fatturazione deve fare le sue verifiche. - Con una chiave live, rileggete invece di scansionare di nuovo.
list_scanstrova una scansione precedente tramite il suo riferimento o il momento in cui è stata fatta, eget_scanla restituisce come era stata restituita la prima volta, senza riconoscere di nuovo l'immagine e senza addebiti. Due differenze: ogni slot diimagesènullequalityriportanot_checked, perché i ritagli delle immagini non vengono mai salvati.
Se preferite registrare da uno script invece che dall'editor, la stessa chiamata in semplice HTTP è:
curl -X POST https://api.doc.cheap/v1/scans \
-H "Authorization: Bearer sk_sandbox_public" \
-H "Content-Type: application/json" \
-d "{\"image\": \"$(base64 < specimens/td3-specimen.jpg | tr -d '\n')\"}" \
-o test/fixtures/scan-recognized.json
Mascherate il file prima di farne il commit.
Cosa serve a ciascun flusso
| Chiave | Server | Le immagini arrivano all'API da | |
|---|---|---|---|
| Cartella in CSV | La vostra (oltre i 10 documenti) | Locale, npx |
La directory recintata |
| Verifica del modulo | Basta la sandbox | Ospitato o locale | Un URL o la directory recintata |
| Fixture di test | Basta la sandbox | Uno dei due | La directory recintata, una volta |
La configurazione per ogni client, compresi Windsurf, VS Code, Gemini CLI e Kiro, si trova nella pagina del server MCP OCR per Claude e Cursor e nella guida al server MCP. Se state ancora scegliendo un servizio, il confronto delle API OCR per passaporti mette doc.cheap accanto agli altri in base a ciò che ciascuno pubblica.
Se uno di questi flussi si rompe nel vostro editor, o ne avete un quarto che vale la pena raccontare, scrivete a admin@doc.cheap.
Una domanda per voi: cosa affidate più spesso all'agente del vostro editor – un documento alla volta o un'intera cartella? Raccontatecelo nei commenti qui sotto.