Installer un outil de reconnaissance de documents dans un éditeur prend une minute. Savoir quoi en faire ensuite prend plus de temps. Cet article saute la visite guidée de l'installation et passe directement à trois tâches que les développeurs confient vraiment à Claude Code et à Cursor quand des passeports sont en jeu :

  1. Transformer un dossier de scans de passeports en CSV, d'une façon qui résiste à un plantage en cours de route.
  2. Vérifier un passeport par rapport au formulaire d'inscription que vous êtes en train de construire.
  3. Enregistrer des résultats de scan une seule fois et s'en servir comme fixtures de test, pour que votre suite de tests n'appelle jamais un service en ligne.

Les trois utilisent le serveur MCP de doc.cheap, le paquet @doc-cheap/mcp (MIT, Node 20 ou plus récent). Ceci est le blog de doc.cheap, l'API OCR pour passeports et cartes d'identité derrière ce serveur : tenez-en compte pour les choix de produit. Les schémas eux-mêmes (un dossier d'images clôturé, une clé par fichier, une fixture enregistrée une fois) valent pour tout outil de ce type.

Ce que le serveur apporte à l'éditeur

Six outils, chacun avec les indications qu'un client lit avant de décider s'il doit d'abord vous demander votre accord :

Outil Ce qu'il fait Consomme un crédit ?
scan_document Lit un passeport, une carte d'identité ou un permis de conduire et renvoie un JSON structuré Seulement si un document est reconnu
check_balance Crédits restants et compteurs du mois en cours Non
search_docs Cherche dans la documentation de l'API fournie avec le serveur, hors ligne Non
list_scans Liste les scans conservés du compte, du plus récent au plus ancien Non
get_scan Relit un scan conservé à partir de son id Non
delete_scan Supprime définitivement un scan conservé Non

scan_document reçoit l'image sous forme de image_base64, de image_url ou, sur le serveur local, de image_path, ainsi que les options expect_country, return_portrait, retain_hours, reference et idempotency_key. Ces cinq options portent l'essentiel des workflows ci-dessous.

Sans clé configurée, le serveur utilise la clé sandbox publique : 10 documents reconnus gratuits par adresse au total, et au plus 10 requêtes par adresse et par heure. Cela suffit pour les workflows 2 et 3. Le workflow 1 demande votre propre clé ; l'inscription donne 100 reconnaissances gratuites chaque mois.

Si vous n'avez encore jamais connecté de serveur MCP à un agent, l'article Laisser un agent lire des documents d'identité via MCP couvre les bases et les questions de confidentialité plus en détail.

Workflow 1 : un dossier de scans vers un CSV, dans Claude Code

La tâche. On vous confie un dossier de scans de passeports (l'arriéré de clients d'un hôtel, la liste des participants d'un événement, la migration d'une archive papier) et on veut un tableur : une ligne par fichier, les champs utiles et une note claire pour chaque fichier qui n'a pas pu être lu.

La configuration. Placez un .mcp.json à la racine du projet. Claude Code le lit comme la liste de serveurs propre au projet, et la configuration voyage donc avec le dépôt :

{
  "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 première fois que Claude Code démarre dans ce dossier, il vous demande d'approuver le serveur de .mcp.json.

Deux lignes font le travail :

  • DOC_CHEAP_API_KEY est repris de votre shell, si bien qu'aucune clé n'est commitée. Quand la variable n'est pas définie, la valeur est vide et le serveur se rabat sur la clé sandbox.
  • DOC_CHEAP_IMAGE_ROOT est le seul répertoire où le serveur a le droit de lire des fichiers. Sans lui, image_path est refusé d'emblée. Avec lui, un chemin est d'abord résolu à travers les liens symboliques, et tout ce qui sort du répertoire (un .., un lien qui pointe ailleurs) est refusé. Un image_path relatif part de ce répertoire, et l'agent peut donc passer de simples noms de fichiers.

Cette clôture compte ici plus que partout ailleurs. Les noms de fichiers viennent de l'agent, et l'agent lit un dossier que vous n'avez pas créé. Les images ne passent pas non plus par la conversation : le serveur lit le fichier et l'envoie directement à l'API.

Le prompt.

Appelle d'abord check_balance. Ensuite, pour chaque JPEG et PNG du répertoire de scans, appelle scan_document avec image_path égal au nom du fichier, reference égal au nom du fichier, idempotency_key égal à "backlog-" suivi du SHA-256 du fichier, return_portrait à false et retain_hours à 24. Écris backlog.csv avec les colonnes file, status, billed, kind, country, number, surname, given_names, birth_date, expiry_date, is_expired, mrz_status. Pour tout statut autre que recognized, laisse les colonnes de données vides. À la fin, liste les fichiers qui n'ont pas été reconnus et pourquoi.

Pourquoi chaque option est là :

  • reference est renvoyée dans le champ meta.reference du résultat et apparaît sur chaque ligne de list_scans : chaque scan conservé peut ainsi être rattaché à son fichier sans l'ouvrir.
  • idempotency_key est ce qui permet de reprendre le lot sans risque. Avec une clé live, une répétition avec la même clé renvoie le premier résultat au lieu de reconnaître et de facturer à nouveau. Si la session s'interrompt au fichier 140 sur 300, relancez le même prompt : les 139 premiers reviennent en rejeu sans frais, et le travail reprend à partir de là. C'est une clé construite à partir de l'empreinte du fichier, et non un UUID aléatoire, qui permet à une nouvelle session de la reproduire. Claude Code peut la calculer avec shasum -a 256.
  • retain_hours: 24 est le compromis que détaille le guide sur l'idempotence. Un rejeu renvoie le résultat conservé ; un scan fait avec retain_hours: 0 ne peut donc pas être rejoué : une nouvelle tentative sous sa clé dans les 24 heures est refusée avec idempotency_replay_unavailable. Une fenêtre d'un jour garde les nouvelles tentatives gratuites et ne laisse pourtant rien de conservé le lendemain.
  • return_portrait: false omet le recadrage de la photo du titulaire, dont un CSV n'a que faire.

Ce qui revient. Le status de chaque ligne vaut recognized, no_document_found, unreadable, unsupported_document ou rejected, et billed indique si ce fichier a consommé un crédit. Seul recognized est facturé : un dossier qui contient vingt photos floues coûte donc vingt crédits de moins qu'il n'a de fichiers. La colonne billed est aussi votre propre décompte de ce que le lot a dépensé, sans facture à rapprocher.

Une habitude de plus. Le CSV contient désormais des données personnelles. Ajoutez-le à .gitignore avant que l'agent ne l'écrive, pas après.

Workflow 2 : vérifier un passeport pendant que vous construisez un formulaire, dans Cursor

La tâche. Vous construisez un formulaire d'inscription ou de check-in, et vous voulez voir ce que renvoie un vrai scan avant de choisir les noms de champs, les formats et la validation.

La configuration. Pour un coup d'œil rapide, Cursor peut utiliser le serveur hébergé, qui ne demande aucune installation. Dans le .cursor/mcp.json du projet :

{
  "mcpServers": {
    "doc-cheap": {
      "url": "https://mcp.doc.cheap/mcp"
    }
  }
}

Sans en-tête de clé, le serveur hébergé utilise la clé sandbox. Il ne voit pas les fichiers de votre machine : là, scan_document reçoit l'image en image_base64 ou via une image_url publique en https:. Si vos spécimens sont des fichiers locaux, lancez plutôt le serveur en local. Cursor écrit les variables avec sa propre syntaxe, ${workspaceFolder} pour le dossier du projet et ${env:NAME} pour une variable du shell : l'entrée du workflow 1 ne se recopie donc pas telle quelle :

{
  "mcpServers": {
    "doc-cheap": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@doc-cheap/mcp"],
      "env": {
        "DOC_CHEAP_IMAGE_ROOT": "${workspaceFolder}/specimens"
      }
    }
  }
}

Sans ligne DOC_CHEAP_API_KEY, cette entrée utilise elle aussi la clé sandbox, et le serveur ne lit les images que dans le dossier specimens du projet. Utilisez ici des documents synthétiques ou des spécimens, jamais votre propre passeport.

Le prompt.

Scanne td3-specimen.jpg avec scan_document. Compare le résultat avec les champs de components/OnboardingForm.tsx et propose des modifications pour que chaque champ du formulaire soit rempli à partir du scan. Utilise search_docs pour tout champ dont tu n'es pas sûr.

Ce que la comparaison fait généralement ressortir, et ce que la forme du résultat y répond :

  • Les noms arrivent séparés. holder.surname et holder.given_names sont distincts, et holder.full_name les réunit. Un seul champ « Nom complet » perd la séparation que votre back-office voudra plus tard.
  • Les dates sont déjà au format ISO YYYY-MM-DD. holder.birth_date et document.expiry_date vont directement dans un <input type="date">, dont la valeur utilise le même format. Pas besoin d'analyser du DD.MM.YYYY ni les dates à six chiffres propres à la MRZ.
  • Les pays arrivent sous forme de codes avec détails. holder.nationality est le code à trois lettres du document, et holder.nationality_detail y ajoute les formes ISO alpha-3 et alpha-2. Si votre sélecteur de pays repose sur des codes à deux lettres, partez de l'objet de détail plutôt que d'écrire votre propre table.
  • L'expiration est calculée pour vous. document.is_expired et document.days_remaining (négatif une fois expiré) permettent au formulaire d'avertir d'un passeport expiré sans calcul de dates.
  • Tout peut être null. Chaque clé est toujours présente, et une valeur inconnue vaut null, jamais une clé absente. Un formulaire qui suppose que chaque champ arrive cassera au premier passeport usé.
  • Le verdict MRZ est distinct des champs. mrz.status vaut passed, failed ou absent. Il indique si la zone de lecture automatique a été lue et si ses chiffres de contrôle concordent ; il ne dit pas que le document est authentique. Le groupe authenticity du résultat indique not_checked.

search_docs mérite sa place dans cette boucle. Il lit la documentation fournie avec le serveur, fonctionne hors ligne et ne coûte rien : « que signifie unsupported_document » ou « quels champs a une carte d'identité » obtient la réponse documentée au lieu d'une supposition.

Workflow 3 : enregistrer les fixtures de test une seule fois

La tâche. Votre propre code appelle l'API, et ses tests ne devraient pas le faire. Une suite de tests qui envoie un passeport à un service en ligne à chaque exécution est lente, demande une clé dans la CI et, sur la sandbox, atteint vite ses limites : 10 requêtes par heure, et un refus document_repeated pour un même document soumis trop souvent.

La solution consiste à enregistrer une fois chaque issue que votre code doit gérer, à la sauvegarder en JSON et à faire tourner les tests sur ces fichiers.

Le prompt, dans Claude Code comme dans Cursor :

Scanne td3-specimen.jpg et blank-page.png avec scan_document et enregistre chaque résultat structuré sous test/fixtures/scan-.json. Remplace chaque valeur personnelle (noms, numéros, dates de naissance, lignes MRZ) par un espace réservé du même type, et retire les URL de données de l'objet images.

Le résultat structuré de l'outil est le même objet Scan que renvoie l'API HTTP : la fixture correspond donc à ce que votre code recevra en production. Une fixture masquée ressemble à ceci :

{
  "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<…" }
}

(Abrégée ici ; gardez toutes les clés du vrai résultat, pour qu'un test échoue quand votre code en lit une qui manque.)

Trois détails rendent les fixtures fidèles :

  • Enregistrez aussi les échecs. Une page blanche donne no_document_found avec billed: false et les groupes de données à null. C'est ce fichier qui attrape le code qui suppose qu'un scan a toujours un titulaire.
  • Sur la sandbox, billed est une prévision. Rien n'y est facturé, et l'indicateur dit si le même scan aurait été facturé avec une clé live. C'est précisément ce qu'un test de facturation doit vérifier.
  • Avec une clé live, relisez au lieu de scanner à nouveau. list_scans retrouve un scan antérieur par sa référence ou sa date, et get_scan le renvoie tel qu'il a été rendu la première fois, sans reconnaître l'image à nouveau et sans facturer. Deux différences : chaque emplacement de images vaut null et quality indique not_checked, car les recadrages d'image ne sont jamais conservés.

Si vous préférez enregistrer depuis un script plutôt que depuis l'éditeur, le même appel en HTTP simple est :

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

Masquez le fichier avant de le commiter.

Ce dont chaque workflow a besoin

Clé Serveur Les images arrivent à l'API depuis
Dossier vers CSV La vôtre (au-delà de 10 documents) Local, npx Le répertoire clôturé
Vérification du formulaire La sandbox suffit Hébergé ou local Une URL ou le répertoire clôturé
Fixtures de test La sandbox suffit L'un ou l'autre Le répertoire clôturé, une fois

La configuration propre à chaque éditeur, y compris Windsurf, VS Code, Gemini CLI et Kiro, figure sur la page du serveur MCP d'OCR pour Claude et Cursor et dans le guide du serveur MCP. Si vous êtes encore en train de choisir un service, le comparatif des API OCR de passeport place doc.cheap à côté des autres d'après ce que chacun publie.

Si l'un de ces workflows ne fonctionne pas dans votre éditeur, ou si vous en avez un quatrième qui mérite d'être décrit, écrivez à admin@doc.cheap.

Une question pour vous : que confiez-vous le plus souvent à l'agent de votre éditeur – un document à la fois, ou un dossier entier ? Dites-le-nous dans les commentaires ci-dessous.