mcp-dharmamitra 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mcp_dharmamitra-0.1.0/.dockerignore +15 -0
- mcp_dharmamitra-0.1.0/.gitignore +20 -0
- mcp_dharmamitra-0.1.0/CLAUDE.md +60 -0
- mcp_dharmamitra-0.1.0/Dockerfile +25 -0
- mcp_dharmamitra-0.1.0/PKG-INFO +163 -0
- mcp_dharmamitra-0.1.0/README.md +150 -0
- mcp_dharmamitra-0.1.0/pyproject.toml +31 -0
- mcp_dharmamitra-0.1.0/server.json +30 -0
- mcp_dharmamitra-0.1.0/src/mcp_dharmamitra/__init__.py +3 -0
- mcp_dharmamitra-0.1.0/src/mcp_dharmamitra/__main__.py +4 -0
- mcp_dharmamitra-0.1.0/src/mcp_dharmamitra/ocr_client.py +149 -0
- mcp_dharmamitra-0.1.0/src/mcp_dharmamitra/server.py +77 -0
- mcp_dharmamitra-0.1.0/tests/__init__.py +0 -0
- mcp_dharmamitra-0.1.0/tests/test_ocr_client.py +107 -0
- mcp_dharmamitra-0.1.0/uv.lock +944 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# mcp-dharmamitra
|
|
2
|
+
|
|
3
|
+
Python MCP server that runs OCR on an image via `https://dharmamitra.org/bff/api/ocr`
|
|
4
|
+
and writes the extracted text to a local file. Aimed at Tibetan / Sanskrit /
|
|
5
|
+
Devanagari sources.
|
|
6
|
+
|
|
7
|
+
## Layout
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
src/mcp_dharmamitra/
|
|
11
|
+
server.py # MCPServer instance + @mcp.tool() ocr_image
|
|
12
|
+
ocr_client.py # submit_ocr() + poll_status() against Dharmamitra
|
|
13
|
+
__main__.py # python -m mcp_dharmamitra
|
|
14
|
+
tests/
|
|
15
|
+
test_ocr_client.py # respx-mocked
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The server exposes exactly one tool, `ocr_image(image_path, output_path, ...)`.
|
|
19
|
+
It writes only `result.extracted_text` to `output_path` and returns a small
|
|
20
|
+
JSON summary — full text is intentionally *not* returned to the MCP client
|
|
21
|
+
(keeps responses compact and cheap).
|
|
22
|
+
|
|
23
|
+
## Commands
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
uv sync --extra dev # install
|
|
27
|
+
uv run pytest -q # tests (5 cases, respx-mocked, offline)
|
|
28
|
+
uv run mcp-dharmamitra # run server over stdio
|
|
29
|
+
uv run mcp dev src/mcp_dharmamitra/server.py # MCP inspector
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Gotchas
|
|
33
|
+
|
|
34
|
+
- **`mcp` package is 2.x.** We use `from mcp.server.mcpserver import MCPServer`
|
|
35
|
+
(renamed from `FastMCP` in 2.0). Do not import `mcp.server.fastmcp` — it errors
|
|
36
|
+
at import time in 2.x. API surface (`.tool()`, `.run()`, `.list_tools()`) is
|
|
37
|
+
the same.
|
|
38
|
+
- **Cloudflare / `cf_clearance`.** The endpoint sits behind Cloudflare. First
|
|
39
|
+
try without a cookie — usually works. On 403 the client raises
|
|
40
|
+
`DharmamitraForbiddenError`; the fix is to set `DHARMAMITRA_COOKIE` env var to
|
|
41
|
+
a fresh `cf_clearance` value grabbed from a browser. Never hardcode it —
|
|
42
|
+
Cloudflare rotates it.
|
|
43
|
+
- **Polling.** `poll_status` loops with `asyncio.sleep(interval)` until status
|
|
44
|
+
is `completed` (success), any other terminal status like `failed`/`error`
|
|
45
|
+
(raises `DharmamitraError`), or the wall-clock deadline hits
|
|
46
|
+
(`TimeoutError`). Defaults: 2s interval, 300s timeout.
|
|
47
|
+
- **HTTP client lifecycle.** Both `submit_ocr` and `poll_status` create their
|
|
48
|
+
own `httpx.AsyncClient` if none is passed, and close it. Tests pass in a
|
|
49
|
+
mocked transport via `respx`, which patches the default client.
|
|
50
|
+
|
|
51
|
+
## When editing
|
|
52
|
+
|
|
53
|
+
- Keep the tool output small (summary dict). Do not return `extracted_text`
|
|
54
|
+
itself — it can be tens of KB of CJK / Tibetan text and blows up token cost
|
|
55
|
+
on the client side.
|
|
56
|
+
- Add new API params (`transliterate_*`, `model`, `instruction`) as tool
|
|
57
|
+
arguments with sensible defaults matching the Dharmamitra web UI (`false`,
|
|
58
|
+
`"auto"`, empty string).
|
|
59
|
+
- If you add polling backoff or retries, keep the terminal-status set in
|
|
60
|
+
`ocr_client.TERMINAL_STATUSES` in sync.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
FROM python:3.12-slim
|
|
2
|
+
|
|
3
|
+
ENV PYTHONDONTWRITEBYTECODE=1
|
|
4
|
+
ENV PYTHONUNBUFFERED=1
|
|
5
|
+
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
|
|
6
|
+
ENV PIP_NO_CACHE_DIR=1
|
|
7
|
+
ENV USER app
|
|
8
|
+
ENV GROUP app
|
|
9
|
+
ENV WORKDIR /data
|
|
10
|
+
|
|
11
|
+
WORKDIR ${WORKDIR}
|
|
12
|
+
|
|
13
|
+
COPY pyproject.toml README.md ./
|
|
14
|
+
COPY src ./src
|
|
15
|
+
|
|
16
|
+
RUN pip install --no-cache-dir .
|
|
17
|
+
|
|
18
|
+
RUN useradd --create-home --uid 1000 ${USER} \
|
|
19
|
+
&& mkdir -p ${WORKDIR} \
|
|
20
|
+
&& chown -R ${USER}:${GROUP} ${WORKDIR}
|
|
21
|
+
|
|
22
|
+
USER ${USER}
|
|
23
|
+
WORKDIR ${WORKDIR}
|
|
24
|
+
|
|
25
|
+
ENTRYPOINT ["mcp-dharmamitra"]
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: mcp-dharmamitra
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for Dharmamitra OCR (Tibetan/Sanskrit/Devanagari)
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: httpx>=0.27.0
|
|
7
|
+
Requires-Dist: mcp[cli]>=2.0.0
|
|
8
|
+
Provides-Extra: dev
|
|
9
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
10
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
11
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# mcp-dharmamitra
|
|
15
|
+
|
|
16
|
+
<!-- mcp-name: io.github.alex-deus/mcp-dharmamitra -->
|
|
17
|
+
|
|
18
|
+
MCP server that runs OCR on an image via the public
|
|
19
|
+
[Dharmamitra](https://dharmamitra.org) OCR endpoint and writes the extracted
|
|
20
|
+
text to a local file. Optimised for Tibetan / Sanskrit / Devanagari input.
|
|
21
|
+
|
|
22
|
+
## Tool
|
|
23
|
+
|
|
24
|
+
### `ocr_image`
|
|
25
|
+
|
|
26
|
+
| Argument | Type | Default | Notes |
|
|
27
|
+
| --- | --- | --- | --- |
|
|
28
|
+
| `image_path` | `str` | — | Absolute path to a local image (png/jpg/…). |
|
|
29
|
+
| `output_path` | `str` | — | File to write UTF-8 text into. Parent dirs are created. |
|
|
30
|
+
| `transliterate_devanagari_to_iast` | `bool` | `false` | Passed to the API. |
|
|
31
|
+
| `transliterate_tibetan_to_wylie` | `bool` | `false` | Passed to the API. |
|
|
32
|
+
| `instruction` | `str` | `""` | Optional model instruction. |
|
|
33
|
+
| `model` | `str` | `"auto"` | OCR model id. |
|
|
34
|
+
| `poll_interval_seconds` | `float` | `2.0` | Delay between status polls. |
|
|
35
|
+
| `timeout_seconds` | `float` | `300.0` | Overall polling deadline. |
|
|
36
|
+
|
|
37
|
+
Returns a small JSON summary — the extracted text is written to `output_path`,
|
|
38
|
+
not returned to the client.
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"job_id": "b8b26984b3ca49199c28303cad6a144d",
|
|
43
|
+
"output_path": "/abs/path/out.txt",
|
|
44
|
+
"pages": 1,
|
|
45
|
+
"processing_time_seconds": 3.957,
|
|
46
|
+
"chars": 1234
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Install
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
uv sync # or: pip install -e '.[dev]'
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Run locally with the MCP inspector:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
uv run mcp dev src/mcp_dharmamitra/server.py
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Run tests:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
uv run pytest
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Wire into Claude Desktop / Claude Code
|
|
69
|
+
|
|
70
|
+
`~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"mcpServers": {
|
|
75
|
+
"dharmamitra": {
|
|
76
|
+
"command": "uv",
|
|
77
|
+
"args": ["--directory", "/path/mcp-dharmamitra", "run", "mcp-dharmamitra"],
|
|
78
|
+
"env": {"DHARMAMITRA_COOKIE": ""}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Docker
|
|
85
|
+
|
|
86
|
+
Build the image:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
docker build -t mcp-dharmamitra:latest .
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
MCP talks over stdio, so the container must be run with `-i` (no `-t`). Mount
|
|
93
|
+
a host directory that holds the input images — the tool arguments
|
|
94
|
+
(`image_path`, `output_path`) are paths **inside the container**.
|
|
95
|
+
|
|
96
|
+
Smoke-test the entrypoint:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
docker run --rm --entrypoint python mcp-dharmamitra:latest \
|
|
100
|
+
-c "from mcp_dharmamitra.server import mcp; print(mcp.name)"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Wire the container into Claude Desktop / Claude Code
|
|
104
|
+
|
|
105
|
+
Replace `/Users/aleksei/projects/ai/mcp-dharmamitra/tmp` with any host
|
|
106
|
+
directory you want the tool to be able to read/write. Files inside it will be
|
|
107
|
+
visible under `/data` in the container.
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"mcpServers": {
|
|
112
|
+
"dharmamitra": {
|
|
113
|
+
"command": "docker",
|
|
114
|
+
"args": [
|
|
115
|
+
"run", "-i", "--rm",
|
|
116
|
+
"-v", "/Users/aleksei/projects/ai/mcp-dharmamitra/tmp:/data",
|
|
117
|
+
"-e", "DHARMAMITRA_COOKIE",
|
|
118
|
+
"mcp-dharmamitra:latest"
|
|
119
|
+
],
|
|
120
|
+
"env": {
|
|
121
|
+
"DHARMAMITRA_COOKIE": ""
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Or the same via CLI:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
claude mcp add dharmamitra \
|
|
132
|
+
--scope project \
|
|
133
|
+
-e DHARMAMITRA_COOKIE="" \
|
|
134
|
+
-- docker run -i --rm \
|
|
135
|
+
-v /Users/aleksei/projects/ai/mcp-dharmamitra/tmp:/data \
|
|
136
|
+
-e DHARMAMITRA_COOKIE \
|
|
137
|
+
mcp-dharmamitra:latest
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
When calling the tool, pass container paths. Example — image at
|
|
141
|
+
`<host>/tmp/text.png` → `/data/text.png`:
|
|
142
|
+
|
|
143
|
+
```json
|
|
144
|
+
{
|
|
145
|
+
"image_path": "/data/text.png",
|
|
146
|
+
"output_path": "/data/text.txt"
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The resulting `text.txt` appears in the mounted host directory.
|
|
151
|
+
|
|
152
|
+
## Cloudflare / `cf_clearance`
|
|
153
|
+
|
|
154
|
+
The endpoint sits behind Cloudflare. Most requests go through without any
|
|
155
|
+
cookie. If you start getting `403`, grab a fresh `cf_clearance` value from a
|
|
156
|
+
logged-in browser session on `dharmamitra.org` and export it:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
export DHARMAMITRA_COOKIE='cf_clearance value here'
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The server will attach it as a cookie on every request. Do not hardcode it —
|
|
163
|
+
Cloudflare rotates the value.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# mcp-dharmamitra
|
|
2
|
+
|
|
3
|
+
<!-- mcp-name: io.github.alex-deus/mcp-dharmamitra -->
|
|
4
|
+
|
|
5
|
+
MCP server that runs OCR on an image via the public
|
|
6
|
+
[Dharmamitra](https://dharmamitra.org) OCR endpoint and writes the extracted
|
|
7
|
+
text to a local file. Optimised for Tibetan / Sanskrit / Devanagari input.
|
|
8
|
+
|
|
9
|
+
## Tool
|
|
10
|
+
|
|
11
|
+
### `ocr_image`
|
|
12
|
+
|
|
13
|
+
| Argument | Type | Default | Notes |
|
|
14
|
+
| --- | --- | --- | --- |
|
|
15
|
+
| `image_path` | `str` | — | Absolute path to a local image (png/jpg/…). |
|
|
16
|
+
| `output_path` | `str` | — | File to write UTF-8 text into. Parent dirs are created. |
|
|
17
|
+
| `transliterate_devanagari_to_iast` | `bool` | `false` | Passed to the API. |
|
|
18
|
+
| `transliterate_tibetan_to_wylie` | `bool` | `false` | Passed to the API. |
|
|
19
|
+
| `instruction` | `str` | `""` | Optional model instruction. |
|
|
20
|
+
| `model` | `str` | `"auto"` | OCR model id. |
|
|
21
|
+
| `poll_interval_seconds` | `float` | `2.0` | Delay between status polls. |
|
|
22
|
+
| `timeout_seconds` | `float` | `300.0` | Overall polling deadline. |
|
|
23
|
+
|
|
24
|
+
Returns a small JSON summary — the extracted text is written to `output_path`,
|
|
25
|
+
not returned to the client.
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"job_id": "b8b26984b3ca49199c28303cad6a144d",
|
|
30
|
+
"output_path": "/abs/path/out.txt",
|
|
31
|
+
"pages": 1,
|
|
32
|
+
"processing_time_seconds": 3.957,
|
|
33
|
+
"chars": 1234
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
uv sync # or: pip install -e '.[dev]'
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Run locally with the MCP inspector:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
uv run mcp dev src/mcp_dharmamitra/server.py
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Run tests:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
uv run pytest
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Wire into Claude Desktop / Claude Code
|
|
56
|
+
|
|
57
|
+
`~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"mcpServers": {
|
|
62
|
+
"dharmamitra": {
|
|
63
|
+
"command": "uv",
|
|
64
|
+
"args": ["--directory", "/path/mcp-dharmamitra", "run", "mcp-dharmamitra"],
|
|
65
|
+
"env": {"DHARMAMITRA_COOKIE": ""}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Docker
|
|
72
|
+
|
|
73
|
+
Build the image:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
docker build -t mcp-dharmamitra:latest .
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
MCP talks over stdio, so the container must be run with `-i` (no `-t`). Mount
|
|
80
|
+
a host directory that holds the input images — the tool arguments
|
|
81
|
+
(`image_path`, `output_path`) are paths **inside the container**.
|
|
82
|
+
|
|
83
|
+
Smoke-test the entrypoint:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
docker run --rm --entrypoint python mcp-dharmamitra:latest \
|
|
87
|
+
-c "from mcp_dharmamitra.server import mcp; print(mcp.name)"
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Wire the container into Claude Desktop / Claude Code
|
|
91
|
+
|
|
92
|
+
Replace `/Users/aleksei/projects/ai/mcp-dharmamitra/tmp` with any host
|
|
93
|
+
directory you want the tool to be able to read/write. Files inside it will be
|
|
94
|
+
visible under `/data` in the container.
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"mcpServers": {
|
|
99
|
+
"dharmamitra": {
|
|
100
|
+
"command": "docker",
|
|
101
|
+
"args": [
|
|
102
|
+
"run", "-i", "--rm",
|
|
103
|
+
"-v", "/Users/aleksei/projects/ai/mcp-dharmamitra/tmp:/data",
|
|
104
|
+
"-e", "DHARMAMITRA_COOKIE",
|
|
105
|
+
"mcp-dharmamitra:latest"
|
|
106
|
+
],
|
|
107
|
+
"env": {
|
|
108
|
+
"DHARMAMITRA_COOKIE": ""
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Or the same via CLI:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
claude mcp add dharmamitra \
|
|
119
|
+
--scope project \
|
|
120
|
+
-e DHARMAMITRA_COOKIE="" \
|
|
121
|
+
-- docker run -i --rm \
|
|
122
|
+
-v /Users/aleksei/projects/ai/mcp-dharmamitra/tmp:/data \
|
|
123
|
+
-e DHARMAMITRA_COOKIE \
|
|
124
|
+
mcp-dharmamitra:latest
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
When calling the tool, pass container paths. Example — image at
|
|
128
|
+
`<host>/tmp/text.png` → `/data/text.png`:
|
|
129
|
+
|
|
130
|
+
```json
|
|
131
|
+
{
|
|
132
|
+
"image_path": "/data/text.png",
|
|
133
|
+
"output_path": "/data/text.txt"
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The resulting `text.txt` appears in the mounted host directory.
|
|
138
|
+
|
|
139
|
+
## Cloudflare / `cf_clearance`
|
|
140
|
+
|
|
141
|
+
The endpoint sits behind Cloudflare. Most requests go through without any
|
|
142
|
+
cookie. If you start getting `403`, grab a fresh `cf_clearance` value from a
|
|
143
|
+
logged-in browser session on `dharmamitra.org` and export it:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
export DHARMAMITRA_COOKIE='cf_clearance value here'
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The server will attach it as a cookie on every request. Do not hardcode it —
|
|
150
|
+
Cloudflare rotates the value.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "mcp-dharmamitra"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "MCP server for Dharmamitra OCR (Tibetan/Sanskrit/Devanagari)"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"mcp[cli]>=2.0.0",
|
|
9
|
+
"httpx>=0.27.0",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[project.scripts]
|
|
13
|
+
mcp-dharmamitra = "mcp_dharmamitra.server:main"
|
|
14
|
+
|
|
15
|
+
[project.optional-dependencies]
|
|
16
|
+
dev = [
|
|
17
|
+
"pytest>=8.0",
|
|
18
|
+
"pytest-asyncio>=0.24",
|
|
19
|
+
"respx>=0.21",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[build-system]
|
|
23
|
+
requires = ["hatchling"]
|
|
24
|
+
build-backend = "hatchling.build"
|
|
25
|
+
|
|
26
|
+
[tool.hatch.build.targets.wheel]
|
|
27
|
+
packages = ["src/mcp_dharmamitra"]
|
|
28
|
+
|
|
29
|
+
[tool.pytest.ini_options]
|
|
30
|
+
asyncio_mode = "auto"
|
|
31
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.alex-deus/mcp-dharmamitra",
|
|
4
|
+
"description": "OCR for Tibetan / Sanskrit / Devanagari images via the Dharmamitra API; writes extracted text to a local file.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"url": "https://github.com/alex-deus/mcp-dharmamitra",
|
|
7
|
+
"source": "github"
|
|
8
|
+
},
|
|
9
|
+
"version": "0.1.0",
|
|
10
|
+
"packages": [
|
|
11
|
+
{
|
|
12
|
+
"registryType": "pypi",
|
|
13
|
+
"identifier": "mcp-dharmamitra",
|
|
14
|
+
"version": "0.1.0",
|
|
15
|
+
"runtimeHint": "uvx",
|
|
16
|
+
"transport": {
|
|
17
|
+
"type": "stdio"
|
|
18
|
+
},
|
|
19
|
+
"environmentVariables": [
|
|
20
|
+
{
|
|
21
|
+
"name": "DHARMAMITRA_COOKIE",
|
|
22
|
+
"description": "Optional cf_clearance cookie value. Only needed if requests start returning 403 from Cloudflare.",
|
|
23
|
+
"isRequired": false,
|
|
24
|
+
"isSecret": true,
|
|
25
|
+
"format": "string"
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import mimetypes
|
|
5
|
+
import os
|
|
6
|
+
import time
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
BASE_URL = "https://dharmamitra.org/bff/api/ocr"
|
|
13
|
+
DEFAULT_UA = (
|
|
14
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
|
|
15
|
+
"(KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36"
|
|
16
|
+
)
|
|
17
|
+
TERMINAL_STATUSES = {"completed", "failed", "error"}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DharmamitraError(RuntimeError):
|
|
21
|
+
"""Raised when the Dharmamitra OCR API returns a failure."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class DharmamitraForbiddenError(PermissionError):
|
|
25
|
+
"""Raised on HTTP 403 — likely Cloudflare requires cf_clearance cookie."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _default_headers() -> dict[str, str]:
|
|
29
|
+
return {
|
|
30
|
+
"accept": "*/*",
|
|
31
|
+
"origin": "https://dharmamitra.org",
|
|
32
|
+
"referer": "https://dharmamitra.org/translate",
|
|
33
|
+
"user-agent": DEFAULT_UA,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _cookies_from_env() -> dict[str, str] | None:
|
|
38
|
+
cookie = os.environ.get("DHARMAMITRA_COOKIE", "").strip()
|
|
39
|
+
if not cookie:
|
|
40
|
+
return None
|
|
41
|
+
return {"cf_clearance": cookie}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _build_client(client: httpx.AsyncClient | None) -> tuple[httpx.AsyncClient, bool]:
|
|
45
|
+
if client is not None:
|
|
46
|
+
return client, False
|
|
47
|
+
return (
|
|
48
|
+
httpx.AsyncClient(
|
|
49
|
+
timeout=httpx.Timeout(60.0, connect=10.0),
|
|
50
|
+
headers=_default_headers(),
|
|
51
|
+
cookies=_cookies_from_env(),
|
|
52
|
+
),
|
|
53
|
+
True,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
async def submit_ocr(
|
|
58
|
+
image_path: Path,
|
|
59
|
+
*,
|
|
60
|
+
transliterate_devanagari_to_iast: bool = False,
|
|
61
|
+
transliterate_tibetan_to_wylie: bool = False,
|
|
62
|
+
instruction: str = "",
|
|
63
|
+
model: str = "auto",
|
|
64
|
+
client: httpx.AsyncClient | None = None,
|
|
65
|
+
) -> str:
|
|
66
|
+
"""POST the image to Dharmamitra OCR. Returns job_id."""
|
|
67
|
+
image_path = Path(image_path)
|
|
68
|
+
if not image_path.is_file():
|
|
69
|
+
raise FileNotFoundError(f"image not found: {image_path}")
|
|
70
|
+
|
|
71
|
+
mime = mimetypes.guess_type(image_path.name)[0] or "application/octet-stream"
|
|
72
|
+
data = {
|
|
73
|
+
"transliterate_devanagari_to_iast": "true" if transliterate_devanagari_to_iast else "false",
|
|
74
|
+
"transliterate_tibetan_to_wylie": "true" if transliterate_tibetan_to_wylie else "false",
|
|
75
|
+
"instruction": instruction,
|
|
76
|
+
"model": model,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
ac, owns = _build_client(client)
|
|
80
|
+
try:
|
|
81
|
+
with image_path.open("rb") as fh:
|
|
82
|
+
files = {"file": (image_path.name, fh, mime)}
|
|
83
|
+
resp = await ac.post(BASE_URL, data=data, files=files)
|
|
84
|
+
_raise_for_status(resp)
|
|
85
|
+
payload = resp.json()
|
|
86
|
+
finally:
|
|
87
|
+
if owns:
|
|
88
|
+
await ac.aclose()
|
|
89
|
+
|
|
90
|
+
job_id = payload.get("job_id")
|
|
91
|
+
if not job_id:
|
|
92
|
+
raise DharmamitraError(f"submit response missing job_id: {payload!r}")
|
|
93
|
+
return job_id
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
async def poll_status(
|
|
97
|
+
job_id: str,
|
|
98
|
+
*,
|
|
99
|
+
interval: float = 2.0,
|
|
100
|
+
timeout: float = 300.0,
|
|
101
|
+
client: httpx.AsyncClient | None = None,
|
|
102
|
+
) -> dict[str, Any]:
|
|
103
|
+
"""Poll status endpoint until the job reaches a terminal state.
|
|
104
|
+
|
|
105
|
+
Returns the final status payload on success. Raises DharmamitraError on
|
|
106
|
+
failure, TimeoutError if the deadline is exceeded.
|
|
107
|
+
"""
|
|
108
|
+
url = f"{BASE_URL}/status"
|
|
109
|
+
ac, owns = _build_client(client)
|
|
110
|
+
deadline = time.monotonic() + timeout
|
|
111
|
+
last_payload: dict[str, Any] | None = None
|
|
112
|
+
try:
|
|
113
|
+
while True:
|
|
114
|
+
resp = await ac.get(url, params={"jobId": job_id})
|
|
115
|
+
_raise_for_status(resp)
|
|
116
|
+
payload = resp.json()
|
|
117
|
+
last_payload = payload
|
|
118
|
+
status = payload.get("status")
|
|
119
|
+
if status == "completed":
|
|
120
|
+
return payload
|
|
121
|
+
if status in TERMINAL_STATUSES:
|
|
122
|
+
raise DharmamitraError(
|
|
123
|
+
f"job {job_id} ended with status={status!r} "
|
|
124
|
+
f"error={payload.get('error')!r} "
|
|
125
|
+
f"error_code={payload.get('error_code')!r}"
|
|
126
|
+
)
|
|
127
|
+
if time.monotonic() >= deadline:
|
|
128
|
+
raise TimeoutError(
|
|
129
|
+
f"job {job_id} still status={status!r} after {timeout}s"
|
|
130
|
+
)
|
|
131
|
+
await asyncio.sleep(interval)
|
|
132
|
+
finally:
|
|
133
|
+
if owns:
|
|
134
|
+
await ac.aclose()
|
|
135
|
+
# unreachable
|
|
136
|
+
return last_payload # type: ignore[return-value]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _raise_for_status(resp: httpx.Response) -> None:
|
|
140
|
+
if resp.status_code == 403:
|
|
141
|
+
raise DharmamitraForbiddenError(
|
|
142
|
+
"Dharmamitra returned 403 (likely Cloudflare). "
|
|
143
|
+
"Set DHARMAMITRA_COOKIE env var to a fresh cf_clearance value."
|
|
144
|
+
)
|
|
145
|
+
if resp.status_code >= 400:
|
|
146
|
+
body = resp.text[:500]
|
|
147
|
+
raise DharmamitraError(
|
|
148
|
+
f"Dharmamitra HTTP {resp.status_code} for {resp.request.url}: {body}"
|
|
149
|
+
)
|