OCR in Claude Code: connect the space ocr MCP server
Give Claude Code OCR by connecting the hosted space ocr MCP server: named fields with the coordinates they were read from, a review list for what does not check out, and a workspace the assistant can file documents into and query.
Claude Code can hand a document image to an OCR service and get named fields back instead of a wall of loose text — an invoice, a receipt, a business card, an ID, a form. The way in is the space ocr MCP server: one hosted endpoint you register once, after which its tools sit beside Claude Code's own and the assistant reaches for them when a document turns up.
It is worth being precise about what that changes. Pasting an image into a chat works until you need the same fields every time, a record of where each value sat on the page, or somewhere to keep the results. Standing up your own stack — an OCR engine, a parsing layer, a database — answers those and leaves you maintaining all three. Here the extraction runs server-side and returns a fixed shape, every value carries the coordinates it was read from, and the same server files documents into a workspace you query later rather than reading them a second time.
Connect it
The server is at https://mcp.space-ocr.com/mcp, speaking MCP over Streamable HTTP. Nothing is installed and no process has to stay alive: you register the URL, and the API key travels as a bearer header on each request. In Claude Code that is a single command.
claude mcp add --transport http space-ocr https://mcp.space-ocr.com/mcp \
--header "Authorization: Bearer YOUR_API_KEY"Cursor, VS Code and Windsurf take the same URL and header in mcp.json. Clients that cannot set a header — claude.ai, Claude Desktop, Claude mobile — add the URL as a custom connector instead, and an OAuth consent screen asks which API key the server should act with. Either way the key is used for that request only and is not stored server-side.
{
"mcpServers": {
"space-ocr": {
"url": "https://mcp.space-ocr.com/mcp",
"headers": { "Authorization": "Bearer YOUR_API_KEY" }
}
}
}The key, and what it costs
Create a key at space-ocr.com → Developer → API Keys; it begins with spocr_. Every account gets 100 free credits a month, no card required, and the counter resets monthly. Beyond that a credit is $0.05.
The unit is a page. Any of the three reading tools, or one uploaded page, costs exactly 1 credit, and a read that fails is refunded automatically. Everything else is free — browsing the tree, querying stored rows, correcting a cell, opening an upload link. space_balance reports what is left: the free allowance, any plan allowance, then prepaid balance, spent in that order. It is worth a call before a large batch.
Thirteen tools
Start with space_guide, which is the server explaining itself — six short topics (start, upload, schemas, verification, queries, workflows) read straight into the conversation, with no network call and no credits.
Three tools read a page and store nothing:
ocr_extract— named fields, from afieldsschema you declare or fromautoFields: true, which has the model propose one.ocr_markdown— layout-preserving Markdown: headings, lists, and tables carrying per-cellrow/col, with coordinates per element.ocr_text— plain text with the reading order restored, so multi-column pages do not come back interleaved, plus per-block coordinates.
Ten more run the workspace: space_list browses the tree, space_view reads and queries an item, space_create makes a folder / sheet / doc bundle / memo, space_inbox opens an upload link, space_upload takes images that are already URLs, space_job checks upload jobs, space_edit corrects a cell or rewrites a memo, space_balance reports credits, and space_delete removes something in two steps. The full table, with the REST route behind each tool, is in the API docs.
Getting the images in
A tool call cannot carry image bytes, and this is where most first attempts stall. space_upload takes images that are already public https:// URLs — up to 20 per call, 20MB each. Everything else goes through space_inbox: a file on your disk, a photo attached to the conversation, a scan somebody is about to take. It opens a short-lived upload link aimed at one sheet or bundle, and the bytes travel from that machine straight to space ocr without passing through the conversation. The response carries both endings — a curl line when shell commands are available, a link to show the user when they are not.
Uploads are asynchronous. space_upload returns jobs immediately, a page takes roughly twenty seconds, and you can either poll with space_job or simply read the target with space_view once it has had a moment. Images only: a PDF uploads without complaint and is then never read, so turn its pages into images before sending them.
One-shot reads, or rows you keep
The ocr_* tools store nothing, which suits a lookup nobody will repeat — or sampling an unfamiliar document with autoFields before designing a sheet's columns. Anything a user may come back to belongs in the workspace instead.
space_create builds it. A folder groups things, and folders are the only thing the root accepts. A sheet extracts a fixed set of columns from every image dropped into it, one row per image: that is where values you will compare or filter go. A doc bundle converts each page to markdown (prose to read) or text (words to search) and keeps the pages together. A memo is plain text.
One rule about addresses saves an afternoon. A folder is addressed by name, but everything else is addressed by the path a list or create call handed you, whose last segment is the item's uniqueKey rather than its display name — a sheet called March does not live at /invoices/March. Keep the path you were given instead of rebuilding one from names.
With rows in place, space_view is also the query tool: where filters (repeat it to AND conditions; operators = != > >= < <= and ~ for contains), sort orders, select projects columns, limit and offset paginate. Coordinates are left out by default to keep responses small — pass boxes: true when you need to point at the page or read the per-cell verdicts. Reads cost nothing, so pushing the filter to the server is simply how the answer, and the context window, stay small.
What comes back
The three reading tools and the rows an upload produces share one shape. data.values is your data in exactly the schema you asked for. data.cells is a flat map keyed by path — total, items[0].price — where each entry carries the axis-aligned box and four-point quad the value was read from, a verified verdict, a review block, and the evidence behind it. data.review holds the document-level tally and, in flagged, every path worth a second look with its reasons. data.normalized appears when a scalar type was declared, putting the parsed reading beside the printed one. data.image is the pixel frame the 0–1000 coordinates are relative to.
{
"status": "success",
"data": {
"values": {
"store_name": "Supermarket ABC",
"date": "2025-04-10",
"invoice_no": "",
"total": "$4.94"
},
"cells": {
"total": {
"box": { "xmin": 380, "ymin": 720, "xmax": 530, "ymax": 742 },
"quad": [{"x":380,"y":720},{"x":530,"y":720},{"x":530,"y":742},{"x":380,"y":742}],
"verified": true,
"review": null,
"evidence": { "text_match": true, "source": "vision_symbol_match", "match_ratio": 1.0 },
"normalized": { "value": 4.94, "type": "number", "method": "deterministic" }
}
},
"review": {
"unit": "field",
"declared": 4,
"returned": 3,
"boxed": 3,
"verified": 3,
"flagged": [ { "path": "invoice_no", "reasons": ["missing"] } ],
"by_reason": { "missing": 1 }
},
"normalized": { "total": 4.94 },
"image": { "width": 1654, "height": 2339 }
}
}Why the values are checkable. The coordinates are not an LLM's guess at where a value sits: they are re-anchored to the OCR symbols actually found on the page, on a 0–1000 normalized grid that data.image converts back to pixels. Because the location is real, a value can be drawn on the document and checked by eye against the spot it was read from. Beside it, verified is the verdict — false when review reasons were raised, true when the checks ran and raised nothing, null when there was nothing to check — while evidence.text_match reports the character cross-check itself, so a cell caught by a rule you declared whose characters still agree is a normal combination rather than a contradiction. Both are evidence rather than proof: two engines can agree on the same misread, so keep your own business checks downstream.
Deleting takes two calls
space_delete never deletes on its first call. Without confirm it reports what the path holds — the target, how many folders, sheets, bundles, memos and images sit under it, a sample — and returns a signed confirm token good for about ten minutes. The agent shows that summary, waits for an explicit yes, then calls again with the token. The token is bound to the caller's key and that exact path, so it cannot be invented, and one issued for a sheet will not delete one of its rows.
The ceremony exists because deletion cascades and cannot be undone: a folder takes the images inside it with it. Deleting a row does not refund the page either — it was read and billed when it was uploaded.
Four habits
The server states its own operating rules, and they are what separates an agent that stays cheap and citable from one that burns credits and guesses.
- Store, don't dump. Past one document,
space_createthenspace_inboxrather than a directocr_*call, so the heavy data stays behind the API instead of being pasted back into the conversation. - Check before you scan.
space_balancebefore a batch;space_listbefore creating a second sheet for something you already have. A document that is already a row does not need paying for twice. - Answer from stored rows. Query the sheet with
where/sort/select/limitinstead of pulling every row into context. Reads are free; reads of images are not. - Cite the location, flag what is uncertain. Every value carries the box it was read from and a verdict. Surface what
data.review.flaggedlists as needing a look rather than asserting it, and ask for values exactly as printed — a value the page does not contain cannot be anchored to it.