ocrmypdf-mcp
by chemysterium
README.md
# ocrmypdf-mcp
A local [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server that wraps [OCRmyPDF](https://github.com/ocrmypdf/OCRmyPDF), letting Claude Desktop (or any MCP client) add searchable text layers to scanned PDFs — with first-class support for batch-processing a [Zotero](https://www.zotero.org) library.
Everything runs locally. No documents leave your machine.
## ⚠️ Important: files are modified in place
By default, the OCR tools **overwrite your original PDFs** with the OCR'd version. This is intentional (so Zotero re-indexes the same attachment), but it means:
- **Back up your PDFs** (or your Zotero storage folder) before a large batch run.
- Use `dry_run: true` (supported by all three OCR tools) to preview what would be processed.
- Use the `limit` parameter of `batch_ocr_folder` for small trial runs before processing an entire library.
### ⚠️ Risk of PDF damage or data loss
Any tool that rewrites PDFs can, in rare cases, produce a broken or degraded file. Be aware of the following before running OCR over documents you cannot re-download:
- **A rewritten PDF is a different file.** Even in the safest mode the whole document is re-serialized. A malformed-but-viewable source PDF (some viewers tolerate damage that PDF libraries do not) may fail to convert, or convert with missing content. Files that OCRmyPDF cannot parse are reported as `failed` and left untouched — but verify a sample of outputs after a large batch.
- **Interrupted runs:** OCRmyPDF writes to a temporary file and only replaces the original on success, so a crash or forced shutdown mid-run should not corrupt the source — but do not kill the process while it is copying the final output into place.
- **`force=true` is destructive.** It rasterizes every page: existing text layers, vector graphics, and annotations are permanently replaced by a flat image plus new OCR text. Use it only on documents whose existing text layer is garbage.
- **Wrong language = garbage text layer.** OCR with a language that doesn't match the document produces nonsense text that then makes the file look "already searchable" to future runs.
- **There is no undo.** The tools do not keep backups. If the documents are irreplaceable, copy the folder first.
## Tools
| Tool | Description | Modifies files |
|---|---|---|
| `check_needs_ocr` | Report whether a PDF already has a searchable text layer | No |
| `ocr_pdf` | OCR a single PDF (in place, or to a new output path); supports `dry_run` | Yes |
| `batch_ocr_folder` | Start a **background job** that OCRs all PDFs under a folder, skipping ones that already have text; supports `dry_run` and `limit` | Yes |
| `batch_status` | Progress and per-file results of background jobs (or list all jobs) | No |
| `cancel_batch` | Stop a running background job before its next file | No |
| `ocr_zotero_attachment` | OCR the PDF(s) under a Zotero storage key, in place; supports `dry_run` | Yes |
All tools follow the same rule: **a PDF that already has a text layer is never touched.** A PDF without one gets a text layer added while the original page images are preserved exactly (no deskew, no rotation, no recompression, no PDF/A conversion — unless you explicitly opt in via the `deskew`, `rotate`, or `optimize` parameters, which do alter the page images).
## Prerequisites
| What | Why | How to get it |
|---|---|---|
| Python 3.10+ | Runs the server | [python.org](https://www.python.org/downloads/) |
| [Tesseract OCR](https://tesseract-ocr.github.io/tessdoc/Installation.html) 5.x | The OCR engine | Windows: `winget install UB-Mannheim.TesseractOCR` (the [UB-Mannheim build](https://github.com/UB-Mannheim/tesseract/wiki)); Linux: `apt install tesseract-ocr`; macOS: `brew install tesseract` |
| Tesseract **language packs** for every language you OCR | Wrong/missing packs produce garbage text | Select them in the Windows installer, or drop `<lang>.traineddata` files from [tessdata_fast](https://github.com/tesseract-ocr/tessdata_fast) into Tesseract's `tessdata` folder (e.g. `ces` for Czech, `deu` for German, `jpn` + `jpn_vert` for Japanese) |
| [Ghostscript](https://www.ghostscript.com/releases/gsdnld.html) | Used by OCRmyPDF for PDF rendering | Windows: `winget install ArtifexSoftware.GhostScript` |
| Python packages (`mcp`, `ocrmypdf`, `pikepdf`) | Server + PDF processing | `pip install -r requirements.txt` |
Note: Tesseract cannot auto-detect document language — it only detects script/orientation. Set `ocr_default_lang` to a `+`-joined list matching your library (e.g. `eng+ces`), and pass `language` explicitly per call for outliers (e.g. `jpn+jpn_vert`). Keep the list short: every extra language slows OCR and increases misreads.
For documents in **archaic orthography** (old Czech with *w/g/au* spellings, Fraktur-era texts, …), pass `no_dict: true` to the OCR tools. It disables Tesseract's built-in wordlists (`load_system_dawg=0`, `load_freq_dawg=0` via the bundled `nodict.cfg`), so historical spellings are not "corrected" toward the modern-language dictionary.
Tested with Python 3.12, OCRmyPDF 16.x, Tesseract 5.4, Ghostscript 10.x on Windows 11.
## Installation
```bash
git clone https://github.com/chemysterium/ocrmypdf-mcp.git
cd ocrmypdf-mcp
pip install -r requirements.txt
```
Verify OCRmyPDF can find its dependencies:
```bash
ocrmypdf --version
```
## Configuration
Copy the template and edit it with your paths:
```bash
cp config.example.json config.json
```
`config.json` (machine-specific, **gitignored** — never commit it):
```json
{
"zotero_storage": "C:\\Users\\<you>\\Zotero\\storage",
"ocr_default_lang": "eng",
"tesseract_path": "C:\\Program Files\\Tesseract-OCR"
}
```
| Key | Meaning |
|---|---|
| `zotero_storage` | Root of your Zotero `storage` folder. Leave `""` to disable the Zotero tool. |
| `ocr_default_lang` | Tesseract language code(s), `+`-joined: `eng`, `eng+ces`, `eng+deu`, … Requires matching Tesseract language packs. |
| `tesseract_path` | Folder containing the Tesseract executable. Leave `""` to rely on your system `PATH`. |
Settings can also be supplied (and are overridden) by environment variables:
`ZOTERO_STORAGE`, `OCR_DEFAULT_LANG`, `TESSERACT_PATH`.
To keep `config.json` somewhere else, point the `OCRMYPDF_MCP_CONFIG` env var at it.
## Claude Desktop setup
Add to your `claude_desktop_config.json` (Settings → Developer → Edit Config), inside `"mcpServers"`:
```json
"ocrmypdf": {
"command": "python",
"args": ["C:\\path\\to\\ocrmypdf-mcp\\ocrmypdf_server.py"]
}
```
If Claude Desktop can't find `python`, use the full path to your interpreter. Restart Claude Desktop; four new tools should appear.
## Usage examples
Ask Claude things like:
- *"Check whether `C:\scans\paper.pdf` needs OCR."*
- *"Do a dry run of batch OCR on my Zotero storage folder and tell me how many files would be processed."*
- *"OCR the first 10 scanned PDFs in `D:\archive`, English plus Czech."*
- *"OCR the Zotero attachment `AB12CD34` in place."*
A sensible workflow for a large Zotero library:
1. `batch_ocr_folder` with `dry_run: true`, then poll `batch_status` → see the scope
2. `batch_ocr_folder` with `limit: 5` → verify quality on a handful of files
3. Full run — start the job, then check `batch_status` occasionally (OCR is CPU-intensive; a large library can take hours)
## Long runs and client timeouts
MCP clients abort tool calls that take too long — **Claude Desktop's limit is about 60 seconds and is not configurable**. Even scanning a large library for text layers can exceed that, let alone OCR-ing it.
`batch_ocr_folder` therefore returns immediately with a `job_id` while the work continues inside the server process. Poll `batch_status` for counters (`scanned`, `processed`, `skipped`, `failed`), the file currently being OCR'd, and recent per-file results. `cancel_batch` stops a job cleanly between files.
Notes:
- Jobs live in server memory. If the MCP client (and thus the server) restarts, the job stops — files already OCR'd keep their text layer, no file is left half-written (OCRmyPDF replaces files atomically on success), and you can simply start the batch again: already-processed files are skipped.
- `ocr_pdf` and `ocr_zotero_attachment` run synchronously. For a single normal-sized document they finish well within the timeout; a very large single PDF (hundreds of scanned pages) may exceed it — the client shows an error, but the OCR still completes in the background and the file gets its text layer.
## How it decides what to skip
A PDF is considered "already searchable" if its content streams (including Form XObjects) contain a meaningful number of PDF text-show operators. This is a fast heuristic — a stray stamp, watermark, or publisher header won't count as a real text layer.
Files that pass the check are never opened for writing. Files that fail it are OCR'd in OCRmyPDF's `redo_ocr` mode, which handles mixed pages correctly: existing real text (e.g. a journal header on a scanned page) is kept, image regions get OCR text, and nothing is rasterized. If you enable `deskew` or `rotate` (incompatible with `redo_ocr`), the tool falls back to `skip_text` mode, which skips any page already containing text.
## License
MIT — see [LICENSE](LICENSE). OCRmyPDF itself is licensed under MPL-2.0 and is used here as an unmodified dependency.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues