Sooner or later someone drops a photo of a passport into a chat with an assistant and asks it to "just fill in the form". A general-purpose vision model will have a go. It may even get the name right. What it will not do is tell you whether the machine-readable zone's check digits passed, return dates in one format every time, or say "I could not read this" instead of guessing.
That gap is a good fit for a tool. The Model Context Protocol lets an agent call one, and this post shows how to give Claude Desktop, Claude Code, Cursor and other clients a document-recognition tool, what the agent gets back, how to keep the cost bounded, and what to think about before you point an agent at identity documents at all.
This is the blog of doc.cheap, the API behind the MCP server used here. The server is MIT-licensed, and the setup questions apply to any tool of this kind.
What the agent gets
The server is @doc-cheap/mcp on npm (MIT, Node 20 or later), and it exposes three tools:
| Tool | What it does | Spends credit? |
|---|---|---|
scan_document |
Recognises a passport, national ID card or driver's licence from a photo or scan and returns the structured result | Yes, only when a document is recognised |
check_balance |
Reads the remaining credits and this month's counters | No (read-only) |
search_docs |
Searches the API documentation bundled with the server, offline | No (read-only) |
Each tool carries a title, a description (the two that touch the account state the price), and the MCP behaviour hints a client reads before deciding whether to ask you first: scan_document is marked not read-only, the other two read-only. The server also sends instructions the model reads before any call, saying what it recognises and what a call costs. On top of the tools there are four prompts (scan_document_to_json, check_document_expiry, batch_scan, explain_error), and every docs page is exposed as a read-only resource, such as doccheap://docs/reference/fields.
scan_document answers with the full result as structured JSON plus a one-line summary, for example:
Scan 01a0af18-cd8d-7a61-9f2d-4c7b8e105da3: recognized · passport (GRC) · PARADEIGMA ELENI SOFIA · billed · 684 ms
(An invented specimen holder from the docs.) The JSON behind it has the holder, the document number and dates in ISO 8601, every field with a confidence band, the MRZ lines with a passed / failed / absent verdict, and a billed flag. That structure is the point: the agent does not have to interpret pixels, it reads fields.
Install: the local server
Every client below launches the server with npx. With no key set, it uses the public sandbox key, which gives 10 free recognised documents per IP address in total and at most 10 requests an hour. That is enough to try it. Registering gives 20 free credits; after that a recognised document costs $0.01.
Claude Desktop, Cursor and Windsurf read the same block, in claude_desktop_config.json, ~/.cursor/mcp.json and ~/.codeium/windsurf/mcp_config.json respectively:
{
"mcpServers": {
"doc-cheap": {
"command": "npx",
"args": ["-y", "@doc-cheap/mcp"],
"env": { "DOC_CHEAP_API_KEY": "sk_live_your_key" }
}
}
}
Leave out the env line to run on the sandbox key.
Claude Code:
claude mcp add-json doc-cheap '{"command":"npx","args":["-y","@doc-cheap/mcp"],"env":{"DOC_CHEAP_API_KEY":"sk_live_your_key"}}'
VS Code:
code --add-mcp '{"name":"doc-cheap","command":"npx","args":["-y","@doc-cheap/mcp"]}'
Gemini CLI and Kiro use the same mcpServers block in their own settings files; the MCP guide has each path. Restart the client after editing: the server picks up a changed environment only on a fresh launch.
Install: the hosted server
If your client connects to a URL instead of launching a command, the same three tools are hosted at https://mcp.doc.cheap/mcp over Streamable HTTP, with no login. The key goes in a header, X-Doc-Cheap-Api-Key or Authorization: Bearer (the named header wins if both are sent); with no key the sandbox key is used.
Claude Code:
claude mcp add --transport http doc-cheap https://mcp.doc.cheap/mcp --header "Authorization: Bearer sk_live_your_key"
Cursor:
{
"mcpServers": {
"doc-cheap": {
"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 that URL.
The hosted server cannot see files on your machine, so there scan_document takes the image as image_base64 or as a public image_url.
Local files and URLs are fenced on purpose
A tool argument is chosen by a model, and a model can be talked into things. So the local server does not read arbitrary paths:
image_pathis off until you setDOC_CHEAP_IMAGE_ROOTto one directory. Paths are resolved through symlinks first, and..or a link pointing out of the directory is refused. A missing file and an out-of-bounds file get the same message, so the tool cannot be used to probe whether a file exists.image_urlmust behttps:, must resolve only to public addresses (loopback, private, link-local and similar ranges are refused), follows at most three redirects with every hop re-checked, and is capped at 25 MB.
If you have wired up a file-reading tool for an agent before, compare it with this list. "The model will only pass sensible paths" is not a security boundary.
Keeping the cost bounded
Two properties make agent use predictable:
- Only recognised documents are billed. A call is charged when the document type was determined and data was actually extracted: an MRZ whose check digits pass, at least five printed fields, or a correctly decoded barcode. No document found, an unreadable image, an unsupported type, an internal error or a timeout costs nothing. The result's
billedflag says which happened, every time. On the sandbox key nothing is charged at all, and the flag then says whether the same scan would have been billed on a live key. - Retries can be made free.
scan_documentaccepts anidempotency_key; on a live key, a repeat with the same key returns the stored first result instead of charging again. A retry without a key is a second scan. The key is remembered only as long as the result is kept, soretain_hours: 0and replayable retries are one or the other.
In practice:
- Have the agent call
check_balancebefore a batch. The server's own instructions tell the model to do this, and thebatch_scanprompt does it first. Under the sandbox key the balance isnull, and the tool says there is no balance rather than showing zeros. - Auto-approve only the read-only tools. Kiro's config, for example, supports
"autoApprove": ["check_balance", "search_docs"]. Leavescan_documentbehind a confirmation prompt, since it is the one that spends. - Let the agent look things up.
search_docsworks offline against the bundled docs, so "what doesunsupported_documentmean" costs nothing and does not rely on the model's memory.
Privacy: questions to ask before you do this
Identity documents are about as sensitive as data gets, and an agent adds parties to the flow. Here is what is true on the API side, and what depends on your setup.
On the API side (as documented):
- The uploaded image is held in memory for the request and never written to durable storage.
- The recognition result is kept so it can be read back later, for a window set on the account (24 hours, 7 days, 30 days or one year). The default for a new account is one year. Per call,
retain_hours: 0writes no row at all, andscan_documentacceptsretain_hourstoo. If the agent only needs the answer once, set it. - Processing happens in the European Union. The data is not used to train models.
return_portrait: falseleaves outimages.main_photo, the crop of the holder's photo. The crop of the whole page still comes back, and so does the faint second copy of the face some documents print into the page.
On your side:
- The result goes into the model's context. Whatever
scan_documentreturns (names, numbers, dates) is now in the conversation, and is processed by whichever LLM provider runs your client under that provider's terms. That is inherent to any MCP tool, not specific to this one. - How the image travels matters. With
image_pathon the local server, the server reads the file and sends it straight to the API. Withimage_base64, the image bytes are in the model's tool-call arguments. If you want the pixels to stay out of the model's context, use a fenced local directory. - This is recognition, not verification. The result's
authenticitygroup readsnot_checked. A passing MRZ means the zone was read and is internally consistent, not that the document is genuine. There is no liveness or face-match step. If your use case is KYC, this is one input, not the decision. - Use synthetic documents while you build. Specimens and generated MRZs are enough to wire everything up.
A short session
With the server installed, a prompt like "Read this passport scan and tell me whether it expires in the next six months" with a synthetic specimen attached is enough. A well-behaved agent calls scan_document, reads document.expiry_date and document.days_remaining from the result, and answers from those fields rather than from its impression of the image. If the scan comes back unreadable, it should say so and ask for a better photo, and you were not charged for it.
That last behaviour is the real reason to use a tool here: the agent gets an explicit "could not read" instead of the temptation to fill a gap.
Links
- MCP page with every client snippet: https://doc.cheap/mcp
- Full guide: https://doc.cheap/docs/guides/use-the-mcp-server
- Source (MIT): https://gitlab.com/doccheap/ocr-mcp
- npm: https://www.npmjs.com/package/@doc-cheap/mcp
If you build something with it, or find a client where the config above does not work, write to admin@doc.cheap.
Written with the help of AI. Every statement about doc.cheap and its MCP server was checked against their code.