Instalar uma ferramenta de reconhecimento de documentos em um editor leva um minuto. Saber o que fazer com ela depois leva mais tempo. Este post pula o tour de instalação e vai direto a três tarefas que desenvolvedores realmente levam ao Claude Code e ao Cursor quando há passaportes envolvidos:

  1. Transformar uma pasta de digitalizações de passaportes em um CSV, de um jeito que sobreviva a uma falha no meio do caminho.
  2. Conferir um passaporte com o formulário de cadastro que você está construindo.
  3. Gravar os resultados das leituras uma única vez e usá-los como fixtures de teste, para que sua suíte de testes nunca chame um serviço real.

Os três usam o servidor MCP da doc.cheap, o pacote @doc-cheap/mcp (MIT, Node 20 ou superior). Este é o blog da própria doc.cheap, a API de OCR para passaportes e documentos de identidade por trás desse servidor, então pese as escolhas de produto levando isso em conta. Os padrões em si (um diretório de imagens cercado, uma chave por arquivo, um fixture gravado uma vez) valem para qualquer ferramenta desse tipo.

O que o servidor oferece ao editor

Seis ferramentas, cada uma com as dicas que um cliente lê antes de decidir se deve perguntar a você primeiro:

Ferramenta O que faz Consome um crédito?
scan_document Lê um passaporte, uma carteira de identidade ou uma carteira de motorista e devolve JSON estruturado Só quando um documento é reconhecido
check_balance Créditos restantes e os contadores do mês Não
search_docs Pesquisa a documentação da API embutida no servidor, offline Não
list_scans Lista as leituras armazenadas da conta, das mais recentes para as mais antigas Não
get_scan Recupera uma leitura armazenada pelo seu id Não
delete_scan Apaga uma leitura armazenada de forma definitiva Não

scan_document recebe a imagem como image_base64, image_url ou, no servidor local, image_path, além dos opcionais expect_country, return_portrait, retain_hours, reference e idempotency_key. Essas cinco opções sustentam a maior parte dos fluxos abaixo.

Sem chave configurada, o servidor usa a chave pública de sandbox: 10 documentos reconhecidos de graça por endereço no total, e no máximo 10 requisições por endereço por hora. Isso basta para os fluxos 2 e 3. O fluxo 1 precisa de uma chave própria; ao se cadastrar, você recebe 100 reconhecimentos gratuitos todo mês.

Se você nunca conectou um servidor MCP a um agente, Deixar um agente ler documentos de identidade via MCP explica o básico e as questões de privacidade com mais profundidade.

Fluxo 1: uma pasta de digitalizações para um CSV, no Claude Code

A tarefa. Alguém entrega a você uma pasta de digitalizações de passaportes (o acúmulo de hóspedes de um hotel, a lista de participantes de um evento, a migração de um arquivo em papel) e quer uma planilha: uma linha por arquivo, os campos que importam e uma observação clara para cada arquivo que não pôde ser lido.

A configuração. Coloque um .mcp.json na raiz do projeto. O Claude Code o lê como a lista de servidores do próprio projeto, então a configuração acompanha o repositório:

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

Na primeira vez que o Claude Code inicia nessa pasta, ele pede que você aprove o servidor do .mcp.json.

Duas linhas fazem o trabalho:

  • DOC_CHEAP_API_KEY vem do seu shell, então nenhuma chave vai para o commit. Quando a variável não está definida, o valor fica vazio e o servidor recorre à chave de sandbox.
  • DOC_CHEAP_IMAGE_ROOT é o único diretório de onde o servidor pode ler arquivos. Sem ele, image_path é recusado de imediato. Com ele, o caminho é resolvido primeiro através dos links simbólicos, e tudo o que escapa do diretório (um .., um link apontando para fora) é recusado. Um image_path relativo é tomado a partir desse diretório, então o agente pode passar apenas os nomes dos arquivos.

Essa cerca importa aqui mais do que em qualquer outro lugar. Os nomes dos arquivos vêm do agente, e o agente está lendo uma pasta que você não criou. Além disso, as imagens nunca passam pela conversa: o servidor lê o arquivo e o envia direto para a API.

O prompt.

Chame check_balance primeiro. Depois, para cada JPEG e PNG do diretório de digitalizações, chame scan_document com image_path igual ao nome do arquivo, reference igual ao nome do arquivo, idempotency_key igual a "backlog-" mais o SHA-256 do arquivo, return_portrait false e retain_hours 24. Escreva backlog.csv com as colunas file, status, billed, kind, country, number, surname, given_names, birth_date, expiry_date, is_expired, mrz_status. Para qualquer status diferente de recognized, deixe as colunas de dados vazias. No final, liste os arquivos que não foram reconhecidos e o motivo.

Por que cada opção está ali:

  • reference volta no campo meta.reference do resultado e aparece em cada linha de list_scans, então cada leitura armazenada pode ser ligada ao seu arquivo sem abri-lo.
  • idempotency_key é o que torna seguro retomar o lote. Com uma chave real, uma repetição com a mesma chave devolve o primeiro resultado em vez de reconhecer e cobrar de novo. Se a sessão cair no arquivo 140 de 300, rode o mesmo prompt outra vez: os primeiros 139 voltam como repetições sem custo, e o trabalho continua dali. Uma chave montada a partir do hash do arquivo, e não um UUID aleatório, é o que permite que uma sessão nova a reproduza. O Claude Code pode calculá-la com shasum -a 256.
  • retain_hours: 24 é o meio-termo que o guia de idempotência detalha. Uma repetição devolve o resultado armazenado, então uma leitura feita com retain_hours: 0 não pode ser repetida: uma nova tentativa com a mesma chave dentro de 24 horas é recusada com idempotency_replay_unavailable. Uma janela de um dia mantém as novas tentativas gratuitas e ainda não deixa nada armazenado no dia seguinte.
  • return_portrait: false deixa de fora o recorte da foto do titular, que não tem utilidade em um CSV.

O que volta. O status de cada linha é recognized, no_document_found, unreadable, unsupported_document ou rejected, e billed diz se aquele arquivo consumiu um crédito. Só recognized é cobrado, então uma pasta com vinte fotos borradas custa vinte créditos a menos do que o número de arquivos. A coluna billed é também a sua própria conta do que o lote gastou, sem fatura para conferir.

Mais um hábito. O CSV agora contém dados pessoais. Adicione-o ao .gitignore antes de o agente escrevê-lo, não depois.

Fluxo 2: conferir um passaporte enquanto você constrói um formulário, no Cursor

A tarefa. Você está construindo um formulário de cadastro ou de check-in e quer ver o que uma leitura real devolve antes de decidir nomes de campos, formatos e validação.

A configuração. Para uma olhada rápida, o Cursor pode usar o servidor hospedado, que não precisa de instalação. No .cursor/mcp.json do projeto:

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

Sem cabeçalho de chave, o servidor hospedado usa a chave de sandbox. Ele não enxerga arquivos da sua máquina, então ali scan_document recebe a imagem como image_base64 ou como um image_url público https:. Se as suas amostras são arquivos locais, rode o servidor localmente. O Cursor escreve variáveis com sintaxe própria, ${workspaceFolder} para a pasta do projeto e ${env:NAME} para uma variável do shell, então a entrada do fluxo 1 não é copiada como está:

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

Sem a linha DOC_CHEAP_API_KEY, esta entrada também usa a chave de sandbox, e o servidor só lê imagens da pasta specimens do projeto. Use aqui documentos sintéticos ou de amostra, nunca o seu próprio passaporte.

O prompt.

Leia td3-specimen.jpg com scan_document. Compare o resultado com os campos de components/OnboardingForm.tsx e proponha mudanças para que cada campo do formulário seja preenchido a partir da leitura. Use search_docs para qualquer campo sobre o qual você tenha dúvida.

O que a comparação costuma revelar, e o que a forma do resultado responde:

  • Os nomes vêm separados. holder.surname e holder.given_names são campos distintos, e holder.full_name junta os dois. Um único campo "Nome completo" perde a separação que o seu back office vai querer depois.
  • As datas já vêm em ISO YYYY-MM-DD. holder.birth_date e document.expiry_date entram direto em um <input type="date">, cujo valor usa o mesmo formato. Nada de interpretar DD.MM.YYYY ou as datas de seis dígitos da própria MRZ.
  • Os países vêm como códigos com detalhes. holder.nationality é o código de três letras do documento, e holder.nationality_detail acrescenta as formas ISO alfa-3 e alfa-2. Se o seu seletor de países usa códigos de duas letras, faça o mapeamento a partir do objeto de detalhe em vez de escrever a sua própria tabela.
  • A validade é calculada para você. document.is_expired e document.days_remaining (negativo depois do vencimento) permitem que o formulário avise sobre um passaporte vencido sem aritmética de datas.
  • Qualquer valor pode ser null. Todas as chaves estão sempre presentes, e um valor desconhecido é null, nunca uma chave ausente. Um formulário que presume que todos os campos chegam vai quebrar no primeiro passaporte gasto.
  • O veredito da MRZ fica separado dos campos. mrz.status é passed, failed ou absent. Ele diz se a zona de leitura mecânica foi lida e se os dígitos verificadores batem; não diz que o documento é autêntico. O grupo authenticity do resultado marca not_checked.

Vale a pena usar search_docs nesse ciclo. Ela lê a documentação embutida no servidor, funciona offline e não custa nada, então "o que significa unsupported_document" ou "quais campos uma carteira de identidade tem" recebem a resposta documentada em vez de um palpite.

Fluxo 3: gravar fixtures de teste uma única vez

A tarefa. O seu próprio código chama a API, e os testes dele não deveriam chamar. Uma suíte de testes que envia um passaporte a um serviço real a cada execução é lenta, precisa de uma chave no CI e, no sandbox, logo esbarra nos limites: 10 requisições por hora e uma recusa document_repeated para o mesmo documento enviado vezes demais.

A saída é gravar uma vez cada resultado que o seu código precisa tratar, salvá-lo como JSON e deixar os testes rodarem contra os arquivos.

O prompt, no Claude Code ou no Cursor:

Leia td3-specimen.jpg e blank-page.png com scan_document e salve cada resultado estruturado como test/fixtures/scan-.json. Substitua cada valor pessoal (nomes, números, datas de nascimento, linhas da MRZ) por um marcador do mesmo tipo e remova as data URLs do objeto images.

O resultado estruturado da ferramenta é o mesmo objeto Scan que a API HTTP devolve, então o fixture corresponde ao que o seu código vai receber em produção. Um fixture mascarado fica assim:

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

(Resumido aqui; mantenha todas as chaves que o resultado real tem, para que um teste falhe quando o seu código ler uma que esteja faltando.)

Três detalhes tornam os fixtures fiéis:

  • Grave as falhas também. Uma página em branco devolve no_document_found com billed: false e os grupos de dados em null. É esse arquivo que pega o código que presume que toda leitura tem um titular.
  • No sandbox, billed é uma previsão. Nada é cobrado ali, e o indicador diz se a mesma leitura teria sido cobrada com uma chave real. É exatamente o que um teste de cobrança precisa verificar.
  • Com uma chave real, recupere em vez de ler de novo. list_scans encontra uma leitura anterior pela referência ou pelo horário, e get_scan a devolve como foi devolvida da primeira vez, sem reconhecer a imagem de novo e sem cobrar. Duas diferenças: cada posição de images é null e quality marca not_checked, porque os recortes de imagem nunca são armazenados.

Se você prefere gravar a partir de um script em vez do editor, a mesma chamada por HTTP puro é:

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

Mascare o arquivo antes de fazer o commit.

Do que cada fluxo precisa

Chave Servidor As imagens chegam à API a partir de
Pasta para CSV A sua (acima de 10 documentos) Local, npx O diretório cercado
Conferência do formulário O sandbox basta Hospedado ou local Uma URL ou o diretório cercado
Fixtures de teste O sandbox basta Qualquer um O diretório cercado, uma vez

A configuração por cliente para cada editor, incluindo Windsurf, VS Code, Gemini CLI e Kiro, está na página do servidor MCP de OCR para Claude e Cursor e no guia do servidor MCP. Se você ainda está escolhendo um serviço, a comparação de APIs de OCR de passaportes coloca a doc.cheap ao lado das outras com base no que cada uma publica.

Se um desses fluxos quebrar no seu editor, ou se você tiver um quarto que mereça um post, escreva para admin@doc.cheap.

Uma pergunta para você: o que você entrega com mais frequência ao agente do seu editor – um documento de cada vez ou uma pasta inteira? Conte para a gente nos comentários abaixo.