Passport OCR MCP server for AI agents

Give an assistant the ability to read a passport, an identity card or a travel document — and to get structured fields back, not a text blob. The server speaks Model Context Protocol over stdio, your client launches it as a command, and it is a thin client of the public HTTP API: it holds no data of its own.

npx -y @doc-cheap/mcp

Published as @doc-cheap/mcp on npm and as cheap.doc/mcp in the official MCP registry; the source is the public mirror on GitHub.

What your agent gets back

This is the difference that matters. An OCR tool that returns a page of text hands the model something to re-parse, and a model asked to re-parse a date will eventually invent one. This tool returns the fields already separated:

  • The holder — given names, surname, birth date, sex, nationality.
  • The document — kind, country, issuing state, whether it has expired, and how many days remain.
  • Every field found, each with its own confidence, read from the machine-readable zone and from the printed visual zone separately — so the model can see that the two readings agree, instead of assuming it.
  • The machine-readable zone with a verdict: passed, failed or absent, with the reason, and the lines themselves.
  • A one-line summary the assistant can show you as it works.

Failures come back as one readable line in an error block, never as a silent empty result: the assistant can act on it and can show it to you. A refusal from the API keeps the API's own wording, its error code, and the link to the page that explains it.

Install it in your client

Add the block to your client's MCP configuration and restart it. Set DOC_CHEAP_API_KEY to your own key, or leave it out and the public sandbox key is used instead.

Claude Desktop

claude_desktop_config.json

{
  "mcpServers": {
    "doc-cheap": {
      "command": "npx",
      "args": [
        "-y",
        "@doc-cheap/mcp"
      ],
      "env": {
        "DOC_CHEAP_API_KEY": "sk_live_your_key"
      }
    }
  }
}

Claude Code

one command, from the project directory

claude mcp add-json doc-cheap '{"command":"npx","args":["-y","@doc-cheap/mcp"],"env":{"DOC_CHEAP_API_KEY":"sk_live_your_key"}}'

Cursor

~/.cursor/mcp.json, or .cursor/mcp.json in a project

{
  "mcpServers": {
    "doc-cheap": {
      "command": "npx",
      "args": [
        "-y",
        "@doc-cheap/mcp"
      ],
      "env": {
        "DOC_CHEAP_API_KEY": "sk_live_your_key"
      }
    }
  }
}

VS Code

.vscode/mcp.json — note that it nests under servers, not mcpServers

{
  "servers": {
    "doc-cheap": {
      "command": "npx",
      "args": [
        "-y",
        "@doc-cheap/mcp"
      ],
      "type": "stdio",
      "env": {
        "DOC_CHEAP_API_KEY": "sk_live_your_key"
      }
    }
  }
}

Gemini CLI

~/.gemini/settings.json

{
  "mcpServers": {
    "doc-cheap": {
      "command": "npx",
      "args": [
        "-y",
        "@doc-cheap/mcp"
      ],
      "env": {
        "DOC_CHEAP_API_KEY": "sk_live_your_key"
      }
    }
  }
}

Windsurf

~/.codeium/windsurf/mcp_config.json

{
  "mcpServers": {
    "doc-cheap": {
      "command": "npx",
      "args": [
        "-y",
        "@doc-cheap/mcp"
      ],
      "env": {
        "DOC_CHEAP_API_KEY": "sk_live_your_key"
      }
    }
  }
}

Kiro

.kiro/settings/mcp.json in the workspace, or ~/.kiro/settings/mcp.json

{
  "mcpServers": {
    "doc-cheap": {
      "command": "npx",
      "args": [
        "-y",
        "@doc-cheap/mcp"
      ],
      "disabled": false,
      "autoApprove": [
        "check_balance",
        "search_docs"
      ],
      "env": {
        "DOC_CHEAP_API_KEY": "sk_live_your_key"
      }
    }
  }
}

Kiro, in one click

The client's own install link. It asks for confirmation and shows the command and argument list before it writes anything.

Add to Kiro

  • Restart the client after adding the block. The server is launched by the client, so it only picks up a changed configuration and a changed environment on a fresh start.
  • Without DOC_CHEAP_API_KEY the server falls back to the public sandbox key: scans then run inside its free allowance and there is no balance to report. That is the fastest way to see it work.
  • Optional settings: DOC_CHEAP_DOCS_BASE (where search results link to), DOC_CHEAP_DOCS_DIR (which copy of the documentation to search), and DOC_CHEAP_IMAGE_ROOT (see the guards below).

The three tools

The three tools the MCP server publishes
ToolWhat it doesAnswers withBehaviour it declares
scan_document Recognise a passport or ID documentRecognises a document imageThe whole structured result, plus the one-line summaryNot read-only — it can draw a credit. Not destructive. Idempotent when you send an idempotency key, and only then. Open-world: the answer comes from a remote service.
check_balance Check remaining creditsReads the account's usageThe balance and this period's countersRead-only, and open-world: the figures are the account's live standing.
search_docs Search the doc.cheap API documentationSearches the documentationMatching sections with titles, links and snippets — offlineRead-only, and closed-world: the corpus is the documentation copy shipped beside the server, so the same query gives the same answer with no network at all.
  • scan_document takes the image as image_base64, image_path or image_url, and the same options a direct call takes: expect_country, return_portrait, retain_hours, reference and idempotency_key.
  • search_docs reads a copy of the documentation shipped beside the server, so it answers with no network at all — which means the agent can look up the field vocabulary or an error code without spending a call.
  • check_balance needs a key with an account behind it. Under the public sandbox key it says plainly that there is no balance, rather than answering with zeros that look like a reading.

What it costs

$0.01 per recognised document. Flat, every account, at any volume — one number, and nothing to negotiate. A document is charged only when it was recognised: a scan that finds nothing, cannot read the image, or cannot identify the type answers with its verdict and costs nothing. Every result says which it was, so the agent — and you — always know whether that call spent anything.

Before you have an account: the public sandbox key runs 10 recognitions per IP address, and registering adds 20 credits. One credit is one cent is one document.

Recognition takes about 275 ms at the median in production, and each result carries its own timing, so an agent loop can budget against a real number.

The same price, the same key and the same response shape as a direct call: how billing works, and how it compares.

What happens to the image

The image is never written to disk. It exists in memory for the length of the request and is gone when the answer is sent. A result is kept only for the retain_hours the call asks for, and the default is 0 — nothing retained.

The server reports nothing anywhere by default: failure reporting is off unless you set a reporting endpoint yourself, and without one the tracker library is never even loaded.

Two guards worth knowing about

The server runs on your machine with your privileges, and its arguments are chosen by a model. Two of them are therefore fenced:

Local files are off until you open one directory

image_path refuses to do anything until DOC_CHEAP_IMAGE_ROOT names a directory, and tells the assistant to send image_base64 instead. With the variable set, both the directory and the requested file are resolved through symlinks before the containment check; a .. segment and a link pointing outward are both refused, and a relative path is taken from that directory rather than from wherever the client happened to start the process. A path outside the root and a path that does not exist give the same message — a different one for each would answer “does this file exist?” for any path on your machine.

Remote images must be public https

image_url is fetched by the server, so the scheme must be https: and the host must resolve only to public internet addresses: loopback, private, link-local, carrier-grade NAT, multicast and reserved ranges are refused, including their IPv6-mapped spellings. One non-public answer refuses the whole URL. Redirects are followed by hand, at most three hops, and every hop is checked again. The body is capped at 25 MB, counted as it arrives rather than trusted from a header.

image_base64 carries none of these constraints, because the caller already holds the bytes — which is why every refusal above points at it.

Read the guide