Content
# Local OCR MCP
> A minimized local OCR MCP service based on `FastMCP` and `PaddleOCR`, only retaining the main path of `stdio -> ocr_recognize -> PaddleOCR -> unified response envelope`, suitable for direct access from local IDE, Agent, and MCP Client.
## 1. Quick Start
**Environment Requirements**
- `Python 3.10+`
- `uv`
**PyPI Release Status**
- Released to PyPI: `local-ocr-mcp==0.2.0`
- Project page: <https://pypi.org/project/local-ocr-mcp/>
**1. Run directly with `uvx`**
```bash
uvx --from "local-ocr-mcp[paddleocr]==0.2.0" local-ocr-mcp
```
Suitable for direct access to MCP clients, no need to manually create a virtual environment.
The first run will install dependencies such as `paddleocr`, `paddlepaddle`, and `opencv`, which may take longer; subsequent runs will be faster with cached dependencies.
If you want to always follow the latest release version, you can omit the fixed version number:
```bash
uvx --from "local-ocr-mcp[paddleocr]" local-ocr-mcp
```
**2. Local development and running**
```bash
git clone <repository-url>
cd local-ocr-mcp
uv sync --extra dev --extra paddleocr
uv run python -m local_ocr_mcp
```
**3. MCP Client configuration example**
```json
{
"mcpServers": {
"local-ocr-mcp": {
"command": "uvx",
"args": ["--from", "local-ocr-mcp[paddleocr]==0.2.0", "local-ocr-mcp"]
}
}
}
```
> 📖 A synchronized configuration example is also retained in the repository → [mcp_config.json](/home/q/Desktop/START/repos/AI/ML/local-ocr-mcp/mcp_config.json)
## 2. Core Features
| Feature | Description |
|------|------|
| **Single-path main chain** | Only retains `stdio -> ocr_recognize -> PaddleOCR`, no transmission branch and engine routing |
| **Minimal tool surface** | Only exposes `ocr_recognize` and `ocr_health_check` tools |
| **Unified response contract** | All tools return unified `status / data / error / meta` envelope |
| **Fixed input model** | Currently only accepts local image path `image.path`, does not support URL, base64, or upload stream |
| **Stable error semantics** | Uses fixed error codes to expose common failure paths, facilitating client orchestration and recovery |
| **Low cognitive burden** | Does not retain multi-engine, analysis, progress, heartbeat, registry/factory, and other extended layers |
## 3. Technology Stack
- **Python 3.10+** / **uv** - Runtime and dependency management
- **FastMCP** - MCP service framework
- **PaddleOCR** - Fixed OCR engine
- **Pillow** - Image validity verification
- **pytest** - Minimal contract testing
## 4. Architecture Overview
```text
┌──────────────────────────────────────────────────────┐
│ MCP Client / IDE / Agent │
├──────────────────────────────────────────────────────┤
│ stdio transport │
├──────────────────────────────────────────────────────┤
│ FastMCP Tools: ocr_recognize / health_check │
├──────────────────────────────────────────────────────┤
│ RecognitionService HealthService │
├──────────────────────────────────────────────────────┤
│ PaddleOCREngine │
├──────────────────────────────────────────────────────┤
│ PaddleOCR / Filesystem / Image Validation │
└──────────────────────────────────────────────────────┘
```
The current implementation intentionally maintains a one-way dependency and short chain:
- CLI is only responsible for starting `stdio`
- Tool layer is only responsible for exposing MCP interface
- Service layer is responsible for verifying input and forming a unified envelope
- Engine layer is only responsible for calling `PaddleOCR` and normalizing results
## 5. Tool Contract
| Tool | Description |
|------|------|
| `ocr_recognize` | Recognizes local images and returns unified OCR results |
| `ocr_health_check` | Returns lightweight health status, runtime version, and startup duration |
### `ocr_recognize` Request Example
```json
{
"image": {
"path": "./example.png"
}
}
```
### `ocr_recognize` Input Constraints
- `image` must be an object
- `image.path` must be a non-empty string
- Currently only allows `image.path`
- Relative paths will be resolved to absolute paths based on the service process's current working directory
### `ocr_recognize` Successful Response Example
```json
{
"status": "ok",
"data": {
"text": "Example text",
"boxes": [
{"x1": 0.0, "y1": 0.0, "x2": 120.0, "y2": 32.0}
],
"confidence": 0.98,
"engine": "paddleocr",
"processing_ms": 143
},
"error": null,
"meta": {
"timestamp": "2026-03-14T00:00:00+00:00",
"runtime_version": "0.2.0",
"request_id": "8c4c9d5c-6b4d-4f1f-b6df-4ff80e3ff6b6",
"resolved_engine": "paddleocr",
"resolved_image_path": "/abs/path/example.png"
}
}
```
### `ocr_health_check` Response Example
```json
{
"status": "ok",
"data": {
"status": "healthy",
"uptime_ms": 1234,
"transport": "stdio",
"runtime_version": "0.2.0"
},
"error": null,
"meta": {
"timestamp": "2026-03-14T00:00:00+00:00",
"runtime_version": "0.2.0"
}
}
```
## 6. Error Semantics and Runtime Constraints
### Unified Error Structure
```json
{
"status": "error",
"data": null,
"error": {
"code": "file_not_found|invalid_image|engine_not_available|internal_error",
"message": "Specific error message",
"retryable": false
},
"meta": {
"timestamp": "...",
"runtime_version": "0.2.0",
"request_id": "...",
"resolved_engine": "paddleocr"
}
}
```
### Error Code Description
- `file_not_found`: The file pointed to by `image.path` does not exist
- `invalid_image`: The request structure is illegal, or the file exists but is not a valid image
- `engine_not_available`: `PaddleOCR` is not installed or initialized failed
- `internal_error`: An unclassified exception occurs in the recognition chain
### Current Runtime Constraints
- Only supports `stdio`
- Only supports `PaddleOCR`
- Does not automatically switch engines
- Does not support `streamable-http`
- Does not support analysis, progress, heartbeat
- Does not support URL, base64, upload stream, and other non-file inputs
### PaddleOCR Compatibility Behavior
Internally uses `PaddleOCR` with minimal compatibility:
- Prioritizes the newer `predict()`
- If the installed version only supports the old interface, falls back to `ocr()`
- Continues to return a unified response structure
## 7. Configuration and Scripts
### Optional Environment Variables
| Variable | Description | Default Value |
|------|------|--------|
| `PADDLEOCR_LANG` | PaddleOCR initialization language | `ch` |
| `LOG_LEVEL` | Log level | `INFO` |
Example:
```bash
PADDLEOCR_LANG=ch LOG_LEVEL=INFO uv run python -m local_ocr_mcp
```
### Auxiliary Scripts
The current `scripts/` directory only retains two scripts consistent with the minimal implementation:
- `uv run python scripts/list_tools.py`
- `uv run python scripts/recognize_image.py path/to/image.png --json`
> 📖 Detailed description → [scripts/README.md](/home/q/Desktop/START/repos/AI/ML/local-ocr-mcp/scripts/README.md)
## 8. Development and Verification
Common commands:
```bash
uv run python -m pytest -q
uv run python -m local_ocr_mcp --help
uv run python scripts/list_tools.py
uv run python scripts/recognize_image.py path/to/image.png --json
```
CI currently only covers minimal necessary verification:
- Compilation check
- Unit test
- CLI `--help`
- `stdio` startup smoke test
## 9. Frequently Asked Questions
### 1. Why doesn't the service exit after startup?
Because it is waiting for the MCP client to call the tool through `stdio`. This is normal behavior.
### 2. Why return `engine_not_available`?
Usually indicates that `PaddleOCR` or underlying dependencies are not correctly installed. Prioritize re-synchronizing dependencies:
```bash
uv sync --extra paddleocr
```
If you started with `uvx`, confirm that the command includes:
```text
uvx --from local-ocr-mcp[paddleocr]==0.2.0 local-ocr-mcp
```
If you intentionally choose to follow the latest version, you can also use a non-versioned form:
```text
uvx --from local-ocr-mcp[paddleocr] local-ocr-mcp
```
### 3. Why return `invalid_image`?
Common reasons:
- No `image.path` in the request
- `image.path` is not a string
- File exists but is not a valid image
### 4. Why can't relative paths find files?
Relative paths are resolved relative to the service process's current working directory. The safest approach is to directly pass absolute paths.
### 5. Why not retain multi-engine and HTTP?
Because the current version explicitly chooses a "core minimization" route, only retaining the local OCR main chain, and not reserving complex structures for future extensions.
**Last Updated**: 2026-03-14
**License**: MIT