Instalar una herramienta de reconocimiento de documentos en un editor lleva un minuto. Saber qué hacer con ella después lleva más. Este artículo se salta el recorrido de instalación y va directo a tres tareas que los desarrolladores llevan de verdad a Claude Code y Cursor cuando hay pasaportes de por medio:

  1. Convertir una carpeta de escaneos de pasaportes en un CSV, de forma que resista un fallo a mitad de camino.
  2. Comprobar un pasaporte contra el formulario de alta que está construyendo.
  3. Grabar los resultados de escaneo una sola vez y usarlos como fixtures de prueba, para que sus tests nunca llamen a un servicio real.

Los tres usan el servidor MCP de doc.cheap, el paquete @doc-cheap/mcp (MIT, Node 20 o posterior). Este es el blog de doc.cheap, la API de OCR para pasaportes y documentos de identidad que hay detrás de ese servidor, así que valore las elecciones de producto en consecuencia. Los patrones en sí (un directorio de imágenes acotado, una clave por archivo, un fixture grabado una vez) sirven para cualquier herramienta de este tipo.

Qué le da el servidor al editor

Seis herramientas, cada una con las indicaciones que un cliente lee antes de decidir si debe preguntarle primero:

Herramienta Qué hace ¿Gasta un crédito?
scan_document Lee un pasaporte, una tarjeta de identidad o un permiso de conducir y lo devuelve como JSON estructurado Solo cuando se reconoce un documento
check_balance Créditos restantes y contadores del mes No
search_docs Busca en la documentación de la API incluida en el servidor, sin conexión No
list_scans Lista los escaneos guardados de la cuenta, del más reciente al más antiguo No
get_scan Recupera un escaneo guardado por su id No
delete_scan Borra un escaneo guardado de forma definitiva No

scan_document recibe la imagen como image_base64, image_url o, en el servidor local, image_path, además de los opcionales expect_country, return_portrait, retain_hours, reference e idempotency_key. Esas cinco opciones sostienen la mayor parte de los flujos que siguen.

Sin clave configurada, el servidor usa la clave pública de sandbox: 10 documentos reconocidos gratis por dirección en total, y como máximo 10 solicitudes por dirección cada hora. Basta para los flujos 2 y 3. El flujo 1 necesita una clave propia; al registrarse obtiene 100 reconocimientos gratuitos cada mes.

Si nunca ha conectado un servidor MCP a un agente, Permitir que un agente lea documentos de identidad por MCP explica lo básico y las cuestiones de privacidad con más detalle.

Flujo 1: una carpeta de escaneos a un CSV, en Claude Code

La tarea. Alguien le entrega una carpeta de escaneos de pasaportes (los huéspedes atrasados de un hotel, la lista de asistentes de un evento, la migración de un archivo en papel) y quiere una hoja de cálculo: una fila por archivo, los campos que importan y una nota clara para cada archivo que no se pudo leer.

La configuración. Ponga un .mcp.json en la raíz del proyecto. Claude Code lo lee como la lista de servidores propia del proyecto, así que la configuración viaja con el repositorio:

{
  "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 primera vez que Claude Code arranca en esa carpeta, le pide aprobar el servidor de .mcp.json.

Dos líneas hacen el trabajo:

  • DOC_CHEAP_API_KEY se toma de su shell, así que no se sube ninguna clave al repositorio. Si la variable no está definida, el valor queda vacío y el servidor recurre a la clave de sandbox.
  • DOC_CHEAP_IMAGE_ROOT es el único directorio del que el servidor puede leer archivos. Sin él, image_path se rechaza sin más. Con él, la ruta se resuelve primero a través de los enlaces simbólicos, y todo lo que se sale del directorio (un .., un enlace que apunta fuera) se rechaza. Un image_path relativo se toma desde este directorio, así que el agente puede pasar solo los nombres de archivo.

Esa barrera importa aquí más que en ningún otro sitio. Los nombres de archivo los pone el agente, y el agente está leyendo una carpeta que usted no creó. Además, las imágenes nunca pasan por la conversación: el servidor lee el archivo y lo envía directamente a la API.

El prompt.

Llama primero a check_balance. Después, para cada JPEG y PNG del directorio de escaneos, llama a scan_document con image_path igual al nombre del archivo, reference igual al nombre del archivo, idempotency_key igual a "backlog-" más el SHA-256 del archivo, return_portrait false y retain_hours 24. Escribe backlog.csv con las columnas file, status, billed, kind, country, number, surname, given_names, birth_date, expiry_date, is_expired, mrz_status. Para cualquier status distinto de recognized, deja vacías las columnas de datos. Al final, enumera los archivos que no se reconocieron y por qué.

Por qué está cada opción:

  • reference vuelve en el campo meta.reference del resultado y aparece en cada fila de list_scans, así que cada escaneo guardado se puede vincular a su archivo sin abrirlo.
  • idempotency_key es lo que permite reanudar el lote sin riesgo. Con una clave real, una repetición con la misma clave devuelve el primer resultado en lugar de reconocer y cobrar otra vez. Si la sesión se cae en el archivo 140 de 300, ejecute el mismo prompt de nuevo: los primeros 139 vuelven como repeticiones sin coste, y el trabajo sigue desde ahí. Una clave construida a partir del hash del archivo, y no un UUID aleatorio, es lo que permite que una sesión nueva la reproduzca. Claude Code puede calcularla con shasum -a 256.
  • retain_hours: 24 es el compromiso que detalla la guía de idempotencia. Una repetición devuelve el resultado guardado, así que un escaneo hecho con retain_hours: 0 no se puede repetir: un reintento con su clave dentro de las 24 horas se rechaza con idempotency_replay_unavailable. Una ventana de un día mantiene los reintentos gratis y aun así no deja nada guardado al día siguiente.
  • return_portrait: false omite el recorte de la foto del titular, que a un CSV no le sirve de nada.

Qué se obtiene. El status de cada fila es recognized, no_document_found, unreadable, unsupported_document o rejected, y billed dice si ese archivo consumió un crédito. Solo se cobra recognized, así que una carpeta con veinte fotos borrosas cuesta veinte créditos menos que archivos tiene. La columna billed es además su propia cuenta de lo que gastó el lote, sin factura que cuadrar.

Un hábito más. El CSV ya contiene datos personales. Añádalo a .gitignore antes de que el agente lo escriba, no después.

Flujo 2: comprobar un pasaporte mientras construye un formulario, en Cursor

La tarea. Está construyendo un formulario de alta o de check-in y quiere ver qué devuelve un escaneo real antes de decidir los nombres de los campos, los formatos y la validación.

La configuración. Para un vistazo rápido, Cursor puede usar el servidor alojado, que no necesita instalación. En el .cursor/mcp.json del proyecto:

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

Sin cabecera de clave, el servidor alojado usa la clave de sandbox. No puede ver los archivos de su equipo, así que allí scan_document recibe la imagen como image_base64 o como un image_url público https:. Si sus muestras son archivos locales, ejecute el servidor en local. Cursor escribe las variables con su propia sintaxis, ${workspaceFolder} para la carpeta del proyecto y ${env:NAME} para una variable del shell, así que la entrada del flujo 1 no se copia tal cual:

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

Sin la línea DOC_CHEAP_API_KEY, esta entrada también usa la clave de sandbox, y el servidor solo lee imágenes de la carpeta specimens del proyecto. Use aquí documentos sintéticos o de muestra, nunca su propio pasaporte.

El prompt.

Escanea td3-specimen.jpg con scan_document. Compara el resultado con los campos de components/OnboardingForm.tsx y propón cambios para que cada campo del formulario se rellene a partir del escaneo. Usa search_docs para cualquier campo del que no estés seguro.

Lo que la comparación suele revelar, y lo que responde la forma del resultado:

  • Los nombres llegan separados. holder.surname y holder.given_names van por separado, y holder.full_name los une. Un único campo "Nombre completo" pierde la separación que su back office querrá más adelante.
  • Las fechas ya vienen en ISO YYYY-MM-DD. holder.birth_date y document.expiry_date encajan directamente en un <input type="date">, cuyo valor usa el mismo formato. Nada de analizar DD.MM.YYYY ni las fechas de seis dígitos propias de la MRZ.
  • Los países llegan como códigos con detalles. holder.nationality es el código de tres letras del documento, y holder.nationality_detail añade las formas ISO alfa-3 y alfa-2. Si su selector de países usa códigos de dos letras, haga la correspondencia desde el objeto de detalle en lugar de escribir su propia tabla.
  • La caducidad se calcula por usted. document.is_expired y document.days_remaining (negativo una vez caducado) permiten que el formulario avise de un pasaporte caducado sin aritmética de fechas.
  • Cualquier valor puede ser null. Todas las claves están siempre presentes, y un valor desconocido es null, nunca una clave ausente. Un formulario que da por hecho que llegan todos los campos fallará con el primer pasaporte desgastado.
  • El veredicto de la MRZ va aparte de los campos. mrz.status es passed, failed o absent. Indica si se leyó la zona de lectura mecánica y si sus dígitos de control cuadran; no dice que el documento sea auténtico. El grupo authenticity del resultado marca not_checked.

Merece la pena usar search_docs en este ciclo. Lee la documentación incluida en el servidor, funciona sin conexión y no cuesta nada, así que "qué significa unsupported_document" o "qué campos tiene una tarjeta de identidad" obtienen la respuesta documentada en lugar de una suposición.

Flujo 3: grabar fixtures de prueba una sola vez

La tarea. Su propio código llama a la API, y sus tests no deberían hacerlo. Una batería de tests que envía un pasaporte a un servicio real en cada ejecución es lenta, necesita una clave en CI y, en el sandbox, pronto choca con sus límites: 10 solicitudes por hora y un rechazo document_repeated cuando el mismo documento se envía demasiadas veces.

La solución es grabar una vez cada resultado que su código debe manejar, guardarlo como JSON y dejar que los tests trabajen con los archivos.

El prompt, en Claude Code o en Cursor:

Escanea td3-specimen.jpg y blank-page.png con scan_document y guarda cada resultado estructurado como test/fixtures/scan-.json. Sustituye cada valor personal (nombres, números, fechas de nacimiento, líneas MRZ) por un marcador del mismo tipo, y elimina las data URL del objeto images.

El resultado estructurado de la herramienta es el mismo objeto Scan que devuelve la API HTTP, así que el fixture coincide con lo que su código recibirá en producción. Un fixture enmascarado tiene este aspecto:

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

(Recortado aquí; conserve todas las claves que tiene el resultado real, para que un test falle cuando su código lea una que falta.)

Tres detalles hacen que los fixtures sean fieles:

  • Grabe también los fallos. Una página en blanco le da no_document_found con billed: false y los grupos de datos en null. Ese archivo es el que atrapa el código que da por hecho que un escaneo siempre tiene titular.
  • En el sandbox, billed es una previsión. Allí no se cobra nada, y el indicador dice si el mismo escaneo se habría cobrado con una clave real. Es justo lo que un test de facturación necesita comprobar.
  • Con una clave real, recupere en lugar de escanear de nuevo. list_scans encuentra un escaneo anterior por su referencia o por la hora, y get_scan lo devuelve tal como se devolvió la primera vez, sin volver a reconocer la imagen y sin cobrar. Dos diferencias: cada hueco de images es null y quality marca not_checked, porque los recortes de imagen nunca se guardan.

Si prefiere grabar desde un script en lugar de desde el editor, la misma llamada por HTTP simple es:

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

Enmascare el archivo antes de subirlo al repositorio.

Qué necesita cada flujo

Clave Servidor Las imágenes llegan a la API desde
Carpeta a CSV La suya (más allá de 10 documentos) Local, npx El directorio acotado
Comprobación del formulario Basta con el sandbox Alojado o local Una URL o el directorio acotado
Fixtures de prueba Basta con el sandbox Cualquiera El directorio acotado, una vez

La configuración por cliente para cada editor, incluidos Windsurf, VS Code, Gemini CLI y Kiro, está en la página del servidor MCP de OCR para Claude y Cursor y en la guía del servidor MCP. Si todavía está eligiendo un servicio, la comparativa de APIs de OCR de pasaportes sitúa a doc.cheap junto a los demás según lo que publica cada uno.

Si alguno de estos flujos falla en su editor, o tiene un cuarto que merezca un artículo, escriba a admin@doc.cheap.

Una pregunta para usted: ¿qué le pasa con más frecuencia al agente de su editor: un documento cada vez o una carpeta entera? Cuéntenoslo en los comentarios.