Passport OCR MCP server for 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 two ways: over stdio, installed from npm and launched by your client as a command, or over Streamable HTTP, hosted at mcp.doc.cheap. Either way 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 GitLab.

Listed on the official MCP Registry, Smithery, cursor.directory, npm.

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, number, series, date of issue, date of expiry, 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).

Or connect to the hosted server

The same six tools are hosted at https://mcp.doc.cheap/mcp over Streamable HTTP, so a client that connects to a URL needs nothing installed. No login: send your key as X-Doc-Cheap-Api-Key or as Authorization: Bearer – the named header wins when both are sent – or send none and the public sandbox key is used. The hosted server cannot read files on your machine, so it takes the image as base64 or as an https URL.

https://mcp.doc.cheap/mcp

Claude Code

one command

claude mcp add --transport http doc-cheap https://mcp.doc.cheap/mcp --header "Authorization: Bearer sk_live_your_key"

Cursor

~/.cursor/mcp.json

{
  "mcpServers": {
    "doc-cheap": {
      "url": "https://mcp.doc.cheap/mcp",
      "headers": {
        "Authorization": "Bearer sk_live_your_key"
      }
    }
  }
}

VS Code

.vscode/mcp.json

{
  "servers": {
    "doc-cheap": {
      "type": "http",
      "url": "https://mcp.doc.cheap/mcp",
      "headers": {
        "Authorization": "Bearer sk_live_your_key"
      }
    }
  }
}

In Claude Desktop and claude.ai, add it under Settings, Connectors, as a custom connector with the URL https://mcp.doc.cheap/mcp; without a key it runs on the sandbox key.

The six 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.
list_scans List stored scansLists the scans the account has stored, newest firstOne page of rows – id, status, time, reference – and the cursor to the nextRead-only and open-world: the rows are the account's live history. Under a sandbox key nothing is stored, so the list is empty.
get_scan Fetch a stored scanReads back one stored result by its idThe whole structured result, as first returned, without the image cropsRead-only and open-world. It never recognises the image again and never draws a credit.
delete_scan Delete a stored scanDeletes one stored result by its idThe id, and confirmation that it is deletedDestructive, and final – it cannot be undone, and the credit the scan drew is not returned. Idempotent: a second call finds nothing left to delete.
  • scan_document takes the image as image_base64, image_path or image_url, and the same options a direct call takes, among them expect_country, return_portrait, 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.
  • Every tool declares an output schema and returns structured content that matches it, so an agent can use the fields without parsing text.
  • Every page of the documentation is also a resource the agent can read, and four prompts – read a document into JSON, check an expiry date, scan a batch, explain an error code – start the common jobs in one step.

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 free recognised documents per IP address in all, at most 10 requests an hour whatever their answer. An account gets 100 free documents every month, used before paid 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 the server reports

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

Featured on TheDevToolsDirFeatured on neeed.directoryFeatured on Twelve ToolsListed on JustSimple ToolsFeatured on TheDevToolsListFeatured on Noonlaunchdoc.cheap on StartupTrustedListed on Uno DirectoryFeatured on aat.eeFeatured on Findly.toolsDomain Rating 7 on outdr.loldoc.cheap on SaaSGrowFeatured on DailyPings