Ein Werkzeug zur Dokumentenerkennung im Editor zu installieren dauert eine Minute. Zu wissen, was man danach damit anfängt, dauert länger. Dieser Beitrag überspringt die Einrichtungstour und geht direkt zu drei Aufgaben, mit denen Entwickler tatsächlich zu Claude Code und Cursor kommen, wenn Reisepässe im Spiel sind:

  1. Einen Ordner mit Pass-Scans in eine CSV-Datei verwandeln, und zwar so, dass ein Absturz auf halber Strecke nichts kaputt macht.
  2. Einen Pass mit dem Onboarding-Formular abgleichen, das Sie gerade bauen.
  3. Scan-Ergebnisse einmal aufzeichnen und als Test-Fixtures verwenden, damit Ihre Testsuite nie einen echten Dienst aufruft.

Alle drei nutzen den MCP-Server von doc.cheap, das Paket @doc-cheap/mcp (MIT, Node 20 oder neuer). Dies ist der Blog von doc.cheap, der Passport- und ID-OCR-API hinter diesem Server, also gewichten Sie die Produktentscheidungen entsprechend. Die Muster selbst (ein abgegrenztes Bildverzeichnis, ein Schlüssel pro Datei, ein einmal aufgezeichnetes Fixture) gelten für jedes Werkzeug dieser Art.

Was der Server dem Editor gibt

Sechs Werkzeuge, jedes mit den Hinweisen, die ein Client liest, bevor er entscheidet, ob er Sie zuerst fragt:

Werkzeug Was es tut Kostet es ein Guthaben?
scan_document Liest einen Reisepass, Personalausweis oder Führerschein als strukturiertes JSON aus Nur wenn ein Dokument erkannt wird
check_balance Verbleibendes Guthaben und die Zähler dieses Monats Nein
search_docs Durchsucht die mit dem Server gebündelte API-Dokumentation, offline Nein
list_scans Listet die gespeicherten Scans des Kontos auf, die neuesten zuerst Nein
get_scan Liest einen gespeicherten Scan anhand seiner ID zurück Nein
delete_scan Löscht einen gespeicherten Scan endgültig Nein

scan_document nimmt das Bild als image_base64, image_url oder, beim lokalen Server, als image_path entgegen, dazu die optionalen Parameter expect_country, return_portrait, retain_hours, reference und idempotency_key. Diese fünf Optionen tragen den Großteil der folgenden Abläufe.

Ohne gesetzten Schlüssel verwendet der Server den öffentlichen Sandbox-Schlüssel: insgesamt 10 kostenlos erkannte Dokumente pro Adresse und höchstens 10 Anfragen pro Adresse und Stunde. Das reicht für die Abläufe 2 und 3. Ablauf 1 braucht einen eigenen Schlüssel; mit einer Registrierung erhalten Sie jeden Monat 100 kostenlose Erkennungen.

Wenn Sie noch nie einen MCP-Server mit einem Agenten verbunden haben, erklärt Einen Agenten Ausweisdokumente über MCP lesen lassen die Grundlagen und die Datenschutzfragen ausführlicher.

Ablauf 1: ein Ordner mit Scans als CSV, in Claude Code

Die Aufgabe. Jemand gibt Ihnen einen Ordner mit Pass-Scans (der Gästerückstand eines Hotels, die Teilnehmerliste einer Veranstaltung, die Übernahme aus einem Papierarchiv) und möchte eine Tabelle: eine Zeile pro Datei, die wichtigen Felder und einen klaren Vermerk für jede Datei, die nicht gelesen werden konnte.

Die Einrichtung. Legen Sie eine .mcp.json ins Wurzelverzeichnis des Projekts. Claude Code liest sie als die eigene Serverliste des Projekts, sodass die Konfiguration mit dem Repository wandert:

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

Beim ersten Start in diesem Ordner bittet Claude Code Sie, den Server aus .mcp.json freizugeben.

Zwei Zeilen erledigen die Arbeit:

  • DOC_CHEAP_API_KEY wird aus Ihrer Shell übernommen, sodass kein Schlüssel im Commit landet. Ist die Variable nicht gesetzt, bleibt der Wert leer, und der Server greift auf den Sandbox-Schlüssel zurück.
  • DOC_CHEAP_IMAGE_ROOT ist das einzige Verzeichnis, aus dem der Server Dateien lesen darf. Ohne diese Angabe wird image_path grundsätzlich abgelehnt. Mit ihr wird ein Pfad zuerst über symbolische Links aufgelöst, und alles, was aus dem Verzeichnis ausbricht (ein .., ein Link nach draußen), wird abgelehnt. Ein relativer image_path wird von diesem Verzeichnis aus gelesen, sodass der Agent bloße Dateinamen übergeben kann.

Diese Abgrenzung zählt hier mehr als irgendwo sonst. Die Dateinamen kommen vom Agenten, und der Agent liest einen Ordner, den Sie nicht angelegt haben. Außerdem laufen die Bilder nie durch die Unterhaltung: Der Server liest die Datei und schickt sie direkt an die API.

Der Prompt.

Rufe zuerst check_balance auf. Rufe dann für jedes JPEG und PNG im Scan-Verzeichnis scan_document auf, mit image_path gleich dem Dateinamen, reference gleich dem Dateinamen, idempotency_key gleich "backlog-" plus dem SHA-256 der Datei, return_portrait false und retain_hours 24. Schreibe backlog.csv mit den Spalten file, status, billed, kind, country, number, surname, given_names, birth_date, expiry_date, is_expired, mrz_status. Lass bei jedem status außer recognized die Datenspalten leer. Liste am Ende die Dateien auf, die nicht erkannt wurden, und den Grund.

Warum jede Option dabei ist:

  • reference kommt im Feld meta.reference des Ergebnisses zurück und erscheint in jeder Zeile von list_scans, sodass sich jeder gespeicherte Scan seiner Datei zuordnen lässt, ohne ihn zu öffnen.
  • idempotency_key macht den Stapel sicher fortsetzbar. Mit einem Live-Schlüssel liefert eine Wiederholung mit demselben Schlüssel das erste Ergebnis, statt erneut zu erkennen und erneut abzurechnen. Bricht die Sitzung bei Datei 140 von 300 ab, starten Sie denselben Prompt noch einmal: Die ersten 139 kommen kostenlos als Wiederholungen zurück, und die Arbeit geht von dort aus weiter. Ein Schlüssel aus dem Hash der Datei statt einer zufälligen UUID ist das, was eine neue Sitzung ihn reproduzieren lässt. Claude Code kann ihn mit shasum -a 256 berechnen.
  • retain_hours: 24 ist der Kompromiss, den der Leitfaden zur Idempotenz erläutert. Eine Wiederholung gibt das gespeicherte Ergebnis zurück, also lässt sich ein Scan mit retain_hours: 0 nicht wiederholen: Ein erneuter Versuch unter seinem Schlüssel innerhalb von 24 Stunden wird mit idempotency_replay_unavailable abgelehnt. Ein Fenster von einem Tag hält Wiederholungen kostenlos und lässt am Tag danach trotzdem nichts gespeichert.
  • return_portrait: false lässt den Ausschnitt des Inhaberfotos weg, mit dem eine CSV nichts anfangen kann.

Was zurückkommt. Der status jeder Zeile ist recognized, no_document_found, unreadable, unsupported_document oder rejected, und billed sagt, ob diese Datei ein Guthaben verbraucht hat. Nur recognized wird berechnet, also kostet ein Ordner mit zwanzig verwackelten Fotos zwanzig Guthaben weniger, als er Dateien enthält. Die Spalte billed ist zugleich Ihre eigene Aufstellung dessen, was der Stapel verbraucht hat, ohne Rechnung, die abgeglichen werden müsste.

Noch eine Gewohnheit. Die CSV enthält jetzt personenbezogene Daten. Tragen Sie sie in .gitignore ein, bevor der Agent sie schreibt, nicht danach.

Ablauf 2: einen Pass prüfen, während Sie ein Formular bauen, in Cursor

Die Aufgabe. Sie bauen ein Onboarding- oder Check-in-Formular und möchten sehen, was ein echter Scan zurückgibt, bevor Sie Feldnamen, Formate und Validierung festlegen.

Die Einrichtung. Für einen schnellen Blick kann Cursor den gehosteten Server verwenden, der keine Installation braucht. In der .cursor/mcp.json des Projekts:

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

Ohne Schlüssel-Header nutzt der gehostete Server den Sandbox-Schlüssel. Er sieht keine Dateien auf Ihrem Rechner, deshalb nimmt scan_document dort das Bild als image_base64 oder als öffentliche https:-image_url entgegen. Liegen Ihre Muster als lokale Dateien vor, starten Sie den Server stattdessen lokal. Cursor schreibt Variablen in eigener Syntax, ${workspaceFolder} für den Projektordner und ${env:NAME} für eine Shell-Variable, deshalb wird der Eintrag aus Ablauf 1 nicht unverändert übernommen:

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

Ohne Zeile DOC_CHEAP_API_KEY nutzt auch dieser Eintrag den Sandbox-Schlüssel, und der Server liest Bilder nur aus dem Ordner specimens des Projekts. Verwenden Sie hier synthetische oder Musterdokumente, niemals Ihren eigenen Pass.

Der Prompt.

Scanne td3-specimen.jpg mit scan_document. Vergleiche das Ergebnis mit den Feldern in components/OnboardingForm.tsx und schlage Änderungen vor, damit jedes Formularfeld aus dem Scan befüllt wird. Nutze search_docs für jedes Feld, bei dem du unsicher bist.

Was der Vergleich meist zutage fördert, und was die Form des Ergebnisses darauf antwortet:

  • Namen kommen getrennt. holder.surname und holder.given_names sind getrennte Felder, und holder.full_name fügt sie zusammen. Ein einzelnes Feld "Vollständiger Name" verliert die Trennung, die Ihr Backoffice später haben möchte.
  • Datumsangaben sind bereits ISO YYYY-MM-DD. holder.birth_date und document.expiry_date passen direkt in ein <input type="date">, dessen Wert dasselbe Format verwendet. Kein Parsen von DD.MM.YYYY oder der sechsstelligen Datumsangaben der MRZ.
  • Länder kommen als Codes mit Details. holder.nationality ist der dreistellige Code des Dokuments, und holder.nationality_detail ergänzt die ISO-Formen Alpha-3 und Alpha-2. Wenn Ihre Länderauswahl mit zweistelligen Codes arbeitet, übernehmen Sie die Zuordnung aus dem Detailobjekt, statt eine eigene Tabelle zu schreiben.
  • Der Ablauf der Gültigkeit wird für Sie berechnet. document.is_expired und document.days_remaining (negativ nach Ablauf) lassen das Formular vor einem abgelaufenen Pass warnen, ohne Datumsrechnung.
  • Jeder Wert kann null sein. Jeder Schlüssel ist immer vorhanden, und ein unbekannter Wert ist null, nie ein fehlender Schlüssel. Ein Formular, das davon ausgeht, dass jedes Feld ankommt, bricht beim ersten abgenutzten Pass.
  • Das MRZ-Urteil steht getrennt von den Feldern. mrz.status ist passed, failed oder absent. Es sagt, ob die maschinenlesbare Zone gelesen wurde und ihre Prüfziffern stimmen; es sagt nicht, dass das Dokument echt ist. Die Gruppe authenticity des Ergebnisses zeigt not_checked.

search_docs lohnt sich in dieser Schleife. Es liest die mit dem Server gebündelte Dokumentation, funktioniert offline und kostet nichts, sodass "was bedeutet unsupported_document" oder "welche Felder hat ein Personalausweis" die dokumentierte Antwort bekommen statt einer Vermutung.

Ablauf 3: Test-Fixtures einmal aufzeichnen

Die Aufgabe. Ihr eigener Code ruft die API auf, seine Tests sollten das nicht tun. Eine Testsuite, die bei jedem Lauf einen Pass an einen echten Dienst schickt, ist langsam, braucht einen Schlüssel in der CI und stößt in der Sandbox bald an ihre Grenzen: 10 Anfragen pro Stunde und eine Ablehnung document_repeated für dasselbe Dokument, wenn es zu oft eingereicht wird.

Die Lösung: jedes Ergebnis, das Ihr Code behandeln muss, einmal aufzeichnen, als JSON speichern und die Tests gegen die Dateien laufen lassen.

Der Prompt, in Claude Code oder Cursor:

Scanne td3-specimen.jpg und blank-page.png mit scan_document und speichere jedes strukturierte Ergebnis als test/fixtures/scan-.json. Ersetze jeden persönlichen Wert (Namen, Nummern, Geburtsdaten, MRZ-Zeilen) durch einen Platzhalter desselben Typs und entferne die Data-URLs aus dem Objekt images.

Das strukturierte Ergebnis des Werkzeugs ist dasselbe Scan-Objekt, das die HTTP-API zurückgibt, also entspricht das Fixture dem, was Ihr Code in der Produktion erhält. Ein maskiertes Fixture sieht so aus:

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

(Hier gekürzt; behalten Sie jeden Schlüssel, den das echte Ergebnis hat, damit ein Test fehlschlägt, wenn Ihr Code einen liest, der fehlt.)

Drei Details machen die Fixtures ehrlich:

  • Zeichnen Sie auch die Fehlschläge auf. Eine leere Seite liefert no_document_found mit billed: false und die Datengruppen als null. Genau diese Datei fängt Code ab, der annimmt, dass ein Scan immer einen Inhaber hat.
  • In der Sandbox ist billed eine Vorhersage. Dort wird nichts berechnet, und das Kennzeichen sagt, ob derselbe Scan mit einem Live-Schlüssel berechnet worden wäre. Genau das muss ein Abrechnungstest prüfen.
  • Mit einem Live-Schlüssel zurücklesen statt neu scannen. list_scans findet einen früheren Scan über seine Referenz oder seinen Zeitpunkt, und get_scan gibt ihn so zurück, wie er beim ersten Mal geliefert wurde, ohne das Bild erneut zu erkennen und ohne Abrechnung. Zwei Unterschiede: Jeder Platz in images ist null, und quality zeigt not_checked, weil Bildausschnitte nie gespeichert werden.

Wenn Sie lieber aus einem Skript statt aus dem Editor aufzeichnen, lautet derselbe Aufruf über einfaches 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

Maskieren Sie die Datei, bevor Sie sie committen.

Welcher Ablauf was braucht

Schlüssel Server Bilder erreichen die API aus
Ordner als CSV Ein eigener (ab mehr als 10 Dokumenten) Lokal, npx Dem abgegrenzten Verzeichnis
Formularprüfung Sandbox reicht Gehostet oder lokal Einer URL oder dem abgegrenzten Verzeichnis
Test-Fixtures Sandbox reicht Beides Dem abgegrenzten Verzeichnis, einmal

Die Konfiguration für jeden Editor, darunter Windsurf, VS Code, Gemini CLI und Kiro, steht auf der Seite OCR-MCP-Server für Claude und Cursor und im Leitfaden zum MCP-Server. Wenn Sie noch einen Dienst auswählen, stellt der Vergleich von Passport-OCR-APIs doc.cheap neben die anderen, anhand dessen, was jeder veröffentlicht.

Wenn einer dieser Abläufe in Ihrem Editor nicht funktioniert oder Sie einen vierten haben, der einen Beitrag wert ist, schreiben Sie an admin@doc.cheap.

Eine Frage an Sie: Was geben Sie dem Agenten in Ihrem Editor am häufigsten – ein Dokument nach dem anderen oder einen ganzen Ordner? Erzählen Sie es uns unten in den Kommentaren.