fastocr-mcp 0.1.0__py3-none-any.whl
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.
- fastocr_mcp/__init__.py +6 -0
- fastocr_mcp/__main__.py +3 -0
- fastocr_mcp/_version.py +1 -0
- fastocr_mcp/py.typed +0 -0
- fastocr_mcp/server.py +329 -0
- fastocr_mcp-0.1.0.dist-info/METADATA +172 -0
- fastocr_mcp-0.1.0.dist-info/RECORD +10 -0
- fastocr_mcp-0.1.0.dist-info/WHEEL +4 -0
- fastocr_mcp-0.1.0.dist-info/entry_points.txt +2 -0
- fastocr_mcp-0.1.0.dist-info/licenses/LICENSE +21 -0
fastocr_mcp/__init__.py
ADDED
fastocr_mcp/__main__.py
ADDED
fastocr_mcp/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
fastocr_mcp/py.typed
ADDED
|
File without changes
|
fastocr_mcp/server.py
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""MCP server exposing the FastOCR Document OCR API as tools."""
|
|
2
|
+
import json
|
|
3
|
+
import tempfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Annotated, Any, Callable, Dict, Literal, Optional
|
|
6
|
+
|
|
7
|
+
from fastocr_sdk import (
|
|
8
|
+
AsyncFastOCR,
|
|
9
|
+
AuthenticationError,
|
|
10
|
+
DocumentFailedError,
|
|
11
|
+
DocumentJob,
|
|
12
|
+
FastOCRError,
|
|
13
|
+
NotFoundError,
|
|
14
|
+
PollingTimeoutError,
|
|
15
|
+
RateLimitError,
|
|
16
|
+
)
|
|
17
|
+
from mcp.server.mcpserver import MCPServer
|
|
18
|
+
from mcp.server.mcpserver.exceptions import ToolError
|
|
19
|
+
from mcp.types import ToolAnnotations
|
|
20
|
+
from pydantic import Field
|
|
21
|
+
|
|
22
|
+
from ._version import __version__
|
|
23
|
+
|
|
24
|
+
DOCS_URL = "https://fastocr.org/docs"
|
|
25
|
+
KEYS_URL = "https://fastocr.org/developers/keys"
|
|
26
|
+
BUY_PAGES_URL = f"{KEYS_URL}#buy-api-pages"
|
|
27
|
+
MAX_INLINE_CHARS = 20_000
|
|
28
|
+
# Stay under the 60 s request timeout many MCP clients use; longer documents are resumed with get_document.
|
|
29
|
+
WAIT_SECONDS = 45.0
|
|
30
|
+
|
|
31
|
+
ClientFactory = Callable[[], AsyncFastOCR]
|
|
32
|
+
USER_AGENT = f"fastocr-mcp/{__version__}"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def default_client() -> AsyncFastOCR:
|
|
36
|
+
"""An SDK client whose User-Agent names this server, so API access logs can count MCP traffic."""
|
|
37
|
+
client = AsyncFastOCR()
|
|
38
|
+
sdk_agent = client._http.headers.get("User-Agent", "")
|
|
39
|
+
client._http.headers["User-Agent"] = f"{USER_AGENT} {sdk_agent}".strip()
|
|
40
|
+
return client
|
|
41
|
+
|
|
42
|
+
INSTRUCTIONS = f"""\
|
|
43
|
+
FastOCR extracts text from scanned PDFs and images and makes searchable PDFs, in 100+ languages.
|
|
44
|
+
Use ocr_file for a local PDF, PNG, JPG, GIF, WebP or BMP file; pass an absolute path. Every processed
|
|
45
|
+
page (an image is one page) uses one page of the account's FastOCR API allowance, so do not OCR the
|
|
46
|
+
same file twice. The server reads the API key
|
|
47
|
+
from the FASTOCR_API_KEY environment variable. Docs: {DOCS_URL}"""
|
|
48
|
+
|
|
49
|
+
DocumentId = Annotated[str, Field(description="The document id returned by ocr_file or list_documents.")]
|
|
50
|
+
InputPath = Annotated[str, Field(description="Absolute path to a local PDF, PNG, JPG, JPEG, GIF, WebP or BMP file.")]
|
|
51
|
+
OcrOutput = Annotated[
|
|
52
|
+
Literal["text", "pdf"],
|
|
53
|
+
Field(description="'text' returns the extracted text; 'pdf' saves a searchable PDF with a text layer."),
|
|
54
|
+
]
|
|
55
|
+
OutputPath = Annotated[
|
|
56
|
+
Optional[str],
|
|
57
|
+
Field(description="Optional absolute path for the result file. An existing file there is replaced."),
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def explain(error: FastOCRError) -> str:
|
|
62
|
+
"""Turn an SDK error into a message an AI agent or a developer can act on."""
|
|
63
|
+
if isinstance(error, AuthenticationError):
|
|
64
|
+
return (
|
|
65
|
+
"FastOCR did not accept the API key, or none is set. Set FASTOCR_API_KEY in this MCP server's "
|
|
66
|
+
f"environment to a key from {KEYS_URL} (any signed-in account can create one "
|
|
67
|
+
"after accepting the API terms)."
|
|
68
|
+
)
|
|
69
|
+
if error.code == "buy_pages":
|
|
70
|
+
document = f" (document {error.document_id})" if isinstance(error, DocumentFailedError) else ""
|
|
71
|
+
return (
|
|
72
|
+
f"The FastOCR account is out of API pages{document}. Buy pages at {BUY_PAGES_URL}, "
|
|
73
|
+
"then run ocr_file again."
|
|
74
|
+
)
|
|
75
|
+
if error.code == "concurrency_limit":
|
|
76
|
+
return (
|
|
77
|
+
"Too many documents are already processing for this FastOCR account. Wait until one "
|
|
78
|
+
"finishes (check list_documents), then try again. Accounts with purchased API pages can "
|
|
79
|
+
f"process more documents at once: {KEYS_URL}"
|
|
80
|
+
)
|
|
81
|
+
if isinstance(error, RateLimitError):
|
|
82
|
+
wait = int(error.retry_after) if error.retry_after else 60
|
|
83
|
+
return f"FastOCR is rate limiting requests. Try again in {wait} seconds."
|
|
84
|
+
if isinstance(error, NotFoundError):
|
|
85
|
+
return "No document with that id exists in this FastOCR account."
|
|
86
|
+
if error.code == "document_processing":
|
|
87
|
+
return "The document is still processing and cannot be deleted yet. Try again when it has finished."
|
|
88
|
+
if isinstance(error, DocumentFailedError):
|
|
89
|
+
advice = "Trying again may work." if error.retryable else "Trying the same file again will not help."
|
|
90
|
+
return f"OCR failed for document {error.document_id} (code: {error.code}): {error.message} {advice}"
|
|
91
|
+
return f"FastOCR request failed: {error}. See {DOCS_URL}"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _summary(job: DocumentJob) -> Dict[str, Any]:
|
|
95
|
+
summary: Dict[str, Any] = {
|
|
96
|
+
"document_id": job.id,
|
|
97
|
+
"status": job.status,
|
|
98
|
+
"pages_total": job.pages_total,
|
|
99
|
+
"pages_billed": job.pages_billed,
|
|
100
|
+
"text_available": job.outputs.text_available,
|
|
101
|
+
"pdf_available": job.outputs.pdf_available,
|
|
102
|
+
}
|
|
103
|
+
if job.external_id:
|
|
104
|
+
summary["external_id"] = job.external_id
|
|
105
|
+
if job.error:
|
|
106
|
+
summary["error"] = {"code": job.error.code, "message": job.error.message, "retryable": job.error.retryable}
|
|
107
|
+
if job.is_failed:
|
|
108
|
+
summary["next_step"] = explain(_failure(job))
|
|
109
|
+
return summary
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _partial_note(job: DocumentJob) -> str:
|
|
113
|
+
if not job.is_partial:
|
|
114
|
+
return ""
|
|
115
|
+
return (
|
|
116
|
+
f"\nNote: only {job.pages_billed} of {job.pages_total} pages were processed because the account "
|
|
117
|
+
f"ran out of API pages. Buy pages at {BUY_PAGES_URL} and run ocr_file again to process the whole file."
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _beside(stem: Path, suffix: str) -> Path:
|
|
122
|
+
return stem.with_name(stem.name + suffix)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _write_text(path: Path, text: str) -> Path:
|
|
126
|
+
path = path.expanduser()
|
|
127
|
+
path.write_text(text, encoding="utf-8")
|
|
128
|
+
return path
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
async def _deliver(
|
|
132
|
+
client: AsyncFastOCR,
|
|
133
|
+
job: DocumentJob,
|
|
134
|
+
output: str,
|
|
135
|
+
output_path: Optional[str],
|
|
136
|
+
default_stem: Path,
|
|
137
|
+
) -> str:
|
|
138
|
+
header = f"Document {job.id}: {job.status}, {job.pages_billed} page(s) billed."
|
|
139
|
+
if output == "pdf":
|
|
140
|
+
if not job.outputs.pdf_available:
|
|
141
|
+
raise ToolError(f"Document {job.id} has no searchable PDF. Use output='text' instead.")
|
|
142
|
+
destination = Path(output_path).expanduser() if output_path else _beside(default_stem, ".searchable.pdf")
|
|
143
|
+
await client.documents.download_searchable_pdf(job.id, destination)
|
|
144
|
+
return f"{header}\nSearchable PDF saved to {destination}{_partial_note(job)}"
|
|
145
|
+
|
|
146
|
+
text = await client.documents.get_text(job.id)
|
|
147
|
+
saved = None
|
|
148
|
+
if output_path:
|
|
149
|
+
saved = _write_text(Path(output_path), text)
|
|
150
|
+
elif len(text) > MAX_INLINE_CHARS:
|
|
151
|
+
saved = _write_text(_beside(default_stem, ".ocr.txt"), text)
|
|
152
|
+
if len(text) > MAX_INLINE_CHARS:
|
|
153
|
+
return (
|
|
154
|
+
f"{header}{_partial_note(job)}\nThe text has {len(text)} characters; the first "
|
|
155
|
+
f"{MAX_INLINE_CHARS} are below. The full text is in {saved}.\n\n{text[:MAX_INLINE_CHARS]}"
|
|
156
|
+
)
|
|
157
|
+
where = f"\nFull text saved to {saved}." if saved else ""
|
|
158
|
+
return f"{header}{where}{_partial_note(job)}\n\n{text}"
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _output_file(output_path: Optional[str], source: Optional[Path] = None) -> None:
|
|
162
|
+
if not output_path:
|
|
163
|
+
return
|
|
164
|
+
target = Path(output_path).expanduser()
|
|
165
|
+
if not target.is_absolute():
|
|
166
|
+
raise ToolError(f"output_path must be an absolute path, not '{output_path}'.")
|
|
167
|
+
if source and target.resolve() == source.resolve():
|
|
168
|
+
raise ToolError("output_path must differ from the input file.")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
SUPPORTED_SUFFIXES = (".pdf", ".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _source(path: str) -> Path:
|
|
175
|
+
source = Path(path).expanduser()
|
|
176
|
+
if not source.is_absolute():
|
|
177
|
+
raise ToolError(f"Pass an absolute path to the file, not '{path}'.")
|
|
178
|
+
if not source.is_file():
|
|
179
|
+
raise ToolError(f"No file at {source}.")
|
|
180
|
+
if source.suffix.lower() not in SUPPORTED_SUFFIXES:
|
|
181
|
+
raise ToolError("FastOCR's API accepts PDF, PNG, JPG, JPEG, GIF, WebP and BMP files.")
|
|
182
|
+
return source
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def create_server(client_factory: ClientFactory = default_client) -> MCPServer:
|
|
186
|
+
server = MCPServer(
|
|
187
|
+
"fastocr",
|
|
188
|
+
title="FastOCR",
|
|
189
|
+
instructions=INSTRUCTIONS,
|
|
190
|
+
website_url=DOCS_URL,
|
|
191
|
+
version=__version__,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
async def run(action: Callable[[AsyncFastOCR], Any]) -> Any:
|
|
195
|
+
try:
|
|
196
|
+
async with client_factory() as client:
|
|
197
|
+
return await action(client)
|
|
198
|
+
except FastOCRError as error:
|
|
199
|
+
raise ToolError(explain(error)) from error
|
|
200
|
+
except OSError as error:
|
|
201
|
+
raise ToolError(f"Could not read or write a local file: {error}") from error
|
|
202
|
+
|
|
203
|
+
async def ocr(path: str, output: str, output_path: Optional[str]) -> str:
|
|
204
|
+
source = _source(path)
|
|
205
|
+
_output_file(output_path, source)
|
|
206
|
+
|
|
207
|
+
async def action(client: AsyncFastOCR) -> str:
|
|
208
|
+
try:
|
|
209
|
+
job = await client.documents.process(source, timeout=WAIT_SECONDS)
|
|
210
|
+
except PollingTimeoutError as pending:
|
|
211
|
+
return (
|
|
212
|
+
f"Document {pending.document_id} is still processing. Call get_document with "
|
|
213
|
+
f"document_id='{pending.document_id}' and output='{output}' to wait for and fetch the result. "
|
|
214
|
+
"Do not run ocr_file on the same file again."
|
|
215
|
+
)
|
|
216
|
+
return await _deliver(client, job, output, output_path, source.with_suffix(""))
|
|
217
|
+
|
|
218
|
+
result: str = await run(action)
|
|
219
|
+
return result
|
|
220
|
+
|
|
221
|
+
@server.tool(annotations=ToolAnnotations(read_only_hint=False, destructive_hint=False, open_world_hint=True))
|
|
222
|
+
async def ocr_file(
|
|
223
|
+
path: InputPath,
|
|
224
|
+
output: OcrOutput = "text",
|
|
225
|
+
output_path: OutputPath = None,
|
|
226
|
+
) -> str:
|
|
227
|
+
"""OCR a local PDF or image with FastOCR and return its text or save a searchable PDF.
|
|
228
|
+
|
|
229
|
+
Uploads the file, runs OCR and waits up to 45 seconds. With output='text' the
|
|
230
|
+
text is returned; text longer than 20,000 characters is cut, and the full text is
|
|
231
|
+
written to output_path or to <file>.ocr.txt next to the input file. With
|
|
232
|
+
output='pdf' the searchable PDF is saved to output_path or to <file>.searchable.pdf
|
|
233
|
+
next to the input file. Either default file replaces an earlier result with the
|
|
234
|
+
same name. Each processed page uses one page of the account's API allowance; an
|
|
235
|
+
image is one page. Large documents take longer: then this returns a document id,
|
|
236
|
+
and get_document with the same output fetches the result. Never call ocr_file
|
|
237
|
+
again for that file, which bills it again.
|
|
238
|
+
"""
|
|
239
|
+
return await ocr(path, output, output_path)
|
|
240
|
+
|
|
241
|
+
@server.tool(annotations=ToolAnnotations(read_only_hint=False, destructive_hint=False, open_world_hint=True))
|
|
242
|
+
async def ocr_pdf(
|
|
243
|
+
path: InputPath,
|
|
244
|
+
output: OcrOutput = "text",
|
|
245
|
+
output_path: OutputPath = None,
|
|
246
|
+
) -> str:
|
|
247
|
+
"""The same tool as ocr_file, kept under its earlier name. Prefer ocr_file."""
|
|
248
|
+
return await ocr(path, output, output_path)
|
|
249
|
+
|
|
250
|
+
@server.tool(annotations=ToolAnnotations(read_only_hint=False, destructive_hint=False, idempotent_hint=True, open_world_hint=True))
|
|
251
|
+
async def get_document(
|
|
252
|
+
document_id: DocumentId,
|
|
253
|
+
output: Annotated[
|
|
254
|
+
Literal["status", "text", "pdf"],
|
|
255
|
+
Field(description="'status' (default) returns the status at once; 'text' or 'pdf' waits for the document to finish and fetches the result."),
|
|
256
|
+
] = "status",
|
|
257
|
+
output_path: OutputPath = None,
|
|
258
|
+
) -> str:
|
|
259
|
+
"""Get a FastOCR document's status, and optionally its text or searchable PDF.
|
|
260
|
+
|
|
261
|
+
Statuses: awaiting_upload, processing, completed, partial (the account ran out of
|
|
262
|
+
pages partway), failed. With output='text' or 'pdf' this waits up to 45 seconds;
|
|
263
|
+
if the document is still processing, call it again. Fetching a result again costs
|
|
264
|
+
no pages. Without output_path, long text and PDFs are saved in the system temp
|
|
265
|
+
directory.
|
|
266
|
+
"""
|
|
267
|
+
_output_file(output_path)
|
|
268
|
+
|
|
269
|
+
async def action(client: AsyncFastOCR) -> str:
|
|
270
|
+
if output == "status":
|
|
271
|
+
return _format(_summary(await client.documents.get(document_id)))
|
|
272
|
+
try:
|
|
273
|
+
job = await client.documents.wait_for_completion(document_id, timeout=WAIT_SECONDS)
|
|
274
|
+
except PollingTimeoutError:
|
|
275
|
+
return f"Document {document_id} is still processing. Call get_document again with the same arguments."
|
|
276
|
+
default_stem = Path(tempfile.gettempdir()) / f"fastocr-{job.id}"
|
|
277
|
+
return await _deliver(client, job, output, output_path, default_stem)
|
|
278
|
+
|
|
279
|
+
result: str = await run(action)
|
|
280
|
+
return result
|
|
281
|
+
|
|
282
|
+
@server.tool(annotations=ToolAnnotations(read_only_hint=True, open_world_hint=True))
|
|
283
|
+
async def list_documents(
|
|
284
|
+
limit: Annotated[int, Field(ge=1, le=100, description="How many documents to return, newest first (1 to 100).")] = 20,
|
|
285
|
+
) -> str:
|
|
286
|
+
"""List the most recent documents in the FastOCR account with their status and page counts."""
|
|
287
|
+
|
|
288
|
+
async def action(client: AsyncFastOCR) -> str:
|
|
289
|
+
page = await client.documents.list(limit=limit)
|
|
290
|
+
if not page.documents:
|
|
291
|
+
return "No documents in this FastOCR account yet."
|
|
292
|
+
return "\n".join(_format(_summary(job)) for job in page.documents)
|
|
293
|
+
|
|
294
|
+
result: str = await run(action)
|
|
295
|
+
return result
|
|
296
|
+
|
|
297
|
+
@server.tool(annotations=ToolAnnotations(read_only_hint=False, destructive_hint=True, idempotent_hint=True, open_world_hint=True))
|
|
298
|
+
async def delete_document(document_id: DocumentId) -> str:
|
|
299
|
+
"""Permanently delete a FastOCR document and its stored files (original, text, searchable PDF).
|
|
300
|
+
|
|
301
|
+
A document that is still processing cannot be deleted. Local files are not touched.
|
|
302
|
+
"""
|
|
303
|
+
|
|
304
|
+
async def action(client: AsyncFastOCR) -> str:
|
|
305
|
+
await client.documents.delete(document_id)
|
|
306
|
+
return f"Document {document_id} is being deleted."
|
|
307
|
+
|
|
308
|
+
result: str = await run(action)
|
|
309
|
+
return result
|
|
310
|
+
|
|
311
|
+
return server
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _failure(job: DocumentJob) -> DocumentFailedError:
|
|
315
|
+
error = job.error
|
|
316
|
+
return DocumentFailedError(
|
|
317
|
+
(error.message if error else "") or "Document processing failed",
|
|
318
|
+
document_id=job.id,
|
|
319
|
+
code=error.code if error else "processing_failed",
|
|
320
|
+
retryable=error.retryable if error else True,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _format(summary: Dict[str, Any]) -> str:
|
|
325
|
+
return json.dumps({key: value for key, value in summary.items() if value is not None})
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def main() -> None:
|
|
329
|
+
create_server().run()
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: fastocr-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for the FastOCR Document OCR API: OCR scanned PDFs into text or searchable PDFs from Claude, Cursor and other MCP clients.
|
|
5
|
+
Project-URL: Homepage, https://fastocr.org
|
|
6
|
+
Project-URL: Documentation, https://fastocr.org/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/fastocr-org/fastocr-mcp
|
|
8
|
+
Project-URL: Changelog, https://github.com/fastocr-org/fastocr-mcp/blob/main/CHANGELOG.md
|
|
9
|
+
Project-URL: Issues, https://github.com/fastocr-org/fastocr-mcp/issues
|
|
10
|
+
Author-email: FastOCR <support@fastocr.org>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: ai-agents,fastocr,mcp,model-context-protocol,ocr,pdf,searchable-pdf
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
+
Classifier: Topic :: Scientific/Engineering :: Image Recognition
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.10
|
|
27
|
+
Requires-Dist: fastocr-sdk<0.3,>=0.2.0
|
|
28
|
+
Requires-Dist: mcp<3,>=2.3.0
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: jsonschema>=4.20.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest>=7.4.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: respx>=0.21.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
<a href="https://fastocr.org"><img src="https://raw.githubusercontent.com/fastocr-org/fastocr-mcp/main/docs/images/logo.png" alt="FastOCR" width="56" height="56"></a>
|
|
39
|
+
|
|
40
|
+
# FastOCR MCP server
|
|
41
|
+
|
|
42
|
+
OCR any scanned PDF or image from Claude, Cursor and other MCP clients, in 100+ languages. **[fastocr.org](https://fastocr.org)** · [Docs](https://fastocr.org/docs) · [Get an API key](https://fastocr.org/developers/keys)
|
|
43
|
+
|
|
44
|
+
<!-- mcp-name: io.github.fastocr-org/fastocr-mcp -->
|
|
45
|
+
|
|
46
|
+
[](https://pypi.org/project/fastocr-mcp/)
|
|
47
|
+
[](LICENSE)
|
|
48
|
+
|
|
49
|
+
An [MCP](https://modelcontextprotocol.io) server for the
|
|
50
|
+
[FastOCR Document OCR API](https://fastocr.org/docs). It lets Claude Desktop,
|
|
51
|
+
Claude Code, Cursor and other MCP clients OCR a local scanned PDF or image and get
|
|
52
|
+
back its text or a searchable PDF.
|
|
53
|
+
|
|
54
|
+
- PDF files up to 1 GiB, and PNG, JPG, JPEG, GIF, WebP and BMP images up to 20 MiB; 100+ languages.
|
|
55
|
+
- Built on the official [`fastocr-sdk`](https://pypi.org/project/fastocr-sdk/) and the
|
|
56
|
+
official MCP Python SDK. Runs locally over stdio; your files go only to the FastOCR API.
|
|
57
|
+
|
|
58
|
+

|
|
59
|
+
|
|
60
|
+
A real scanned page and the first lines of the text FastOCR returned for it, unedited.
|
|
61
|
+
|
|
62
|
+
API reference and guides: **https://fastocr.org/docs**
|
|
63
|
+
|
|
64
|
+
## Get an API key
|
|
65
|
+
|
|
66
|
+
Any signed-in FastOCR account can create an API key and start right away.
|
|
67
|
+
|
|
68
|
+
1. Sign in and create an API key in the
|
|
69
|
+
[developer console](https://fastocr.org/developers/keys). You accept the
|
|
70
|
+
[API terms](https://fastocr.org/api-terms) with your first key.
|
|
71
|
+
2. Set the key as `FASTOCR_API_KEY` in your MCP client config (see below).
|
|
72
|
+
3. When you need more than the free pages, buy a prepaid API page pack by card in the
|
|
73
|
+
[developer console](https://fastocr.org/developers/keys#buy-api-pages).
|
|
74
|
+
|
|
75
|
+
Each account gets 50 free document pages per month.
|
|
76
|
+
|
|
77
|
+
## Install
|
|
78
|
+
|
|
79
|
+
With [uv](https://docs.astral.sh/uv/) there is nothing to install; MCP clients run it with
|
|
80
|
+
`uvx fastocr-mcp`. Or install it with pip and run `fastocr-mcp`:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pip install fastocr-mcp
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Python 3.10 or newer. The server reads the key from `FASTOCR_API_KEY`.
|
|
87
|
+
|
|
88
|
+
## Configure your MCP client
|
|
89
|
+
|
|
90
|
+
### Claude Code
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
claude mcp add fastocr -e FASTOCR_API_KEY=your_api_key -- uvx fastocr-mcp
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Claude Desktop
|
|
97
|
+
|
|
98
|
+
Add this to `claude_desktop_config.json` (Settings > Developer > Edit Config), then restart
|
|
99
|
+
Claude Desktop:
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
{
|
|
103
|
+
"mcpServers": {
|
|
104
|
+
"fastocr": {
|
|
105
|
+
"command": "uvx",
|
|
106
|
+
"args": ["fastocr-mcp"],
|
|
107
|
+
"env": { "FASTOCR_API_KEY": "your_api_key" }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Cursor
|
|
114
|
+
|
|
115
|
+
Add the same block to `~/.cursor/mcp.json` (all projects) or `.cursor/mcp.json` (one project):
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"mcpServers": {
|
|
120
|
+
"fastocr": {
|
|
121
|
+
"command": "uvx",
|
|
122
|
+
"args": ["fastocr-mcp"],
|
|
123
|
+
"env": { "FASTOCR_API_KEY": "your_api_key" }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
If you installed with pip, use `"command": "fastocr-mcp"` and `"args": []`.
|
|
130
|
+
|
|
131
|
+
## Tools
|
|
132
|
+
|
|
133
|
+
| Tool | What it does |
|
|
134
|
+
|---|---|
|
|
135
|
+
| `ocr_file(path, output="text", output_path=None)` | Uploads a local PDF or image, runs OCR and waits up to 45 seconds; a larger document returns its id for `get_document`. `output="text"` returns the text (text over 20,000 characters is cut, and the full text is saved to `output_path` or `<file>.ocr.txt` next to the input file). `output="pdf"` saves a searchable PDF to `output_path` or `<file>.searchable.pdf`. |
|
|
136
|
+
| `ocr_pdf(path, output="text", output_path=None)` | The same tool as `ocr_file` under its earlier name, kept so existing setups keep working. |
|
|
137
|
+
| `get_document(document_id, output="status", output_path=None)` | Status and page counts of a document; with `output="text"` or `"pdf"`, waits up to 45 seconds and fetches the finished result. Fetching a result again costs no pages. |
|
|
138
|
+
| `list_documents(limit=20)` | The most recent documents in the account, newest first. |
|
|
139
|
+
| `delete_document(document_id)` | Deletes a document and its stored files on FastOCR. Local files are not touched. |
|
|
140
|
+
|
|
141
|
+
Try asking: *"OCR ~/Downloads/scanned-contract.pdf and summarize the termination clause"*,
|
|
142
|
+
*"Read the text in /Users/me/Desktop/receipt.jpg"* or
|
|
143
|
+
*"Make a searchable copy of /Users/me/archive/1998-letters.pdf"*.
|
|
144
|
+
|
|
145
|
+
Every processed page uses one page of the account's API allowance; an image is one page. A document that runs out
|
|
146
|
+
of pages fails with `buy_pages`; the tool says so and links to the
|
|
147
|
+
[developer console](https://fastocr.org/developers/keys#buy-api-pages).
|
|
148
|
+
|
|
149
|
+
## Errors
|
|
150
|
+
|
|
151
|
+
Tool errors are written for the agent to act on: a rejected key, an account out of pages
|
|
152
|
+
(`buy_pages`), too many documents processing at once (`concurrency_limit`), a missing file, or an
|
|
153
|
+
unsupported file type. Each message says what to do next. Error codes are listed in the
|
|
154
|
+
[API docs](https://fastocr.org/docs/documents).
|
|
155
|
+
|
|
156
|
+
## Configuration
|
|
157
|
+
|
|
158
|
+
| Variable | Purpose |
|
|
159
|
+
|---|---|
|
|
160
|
+
| `FASTOCR_API_KEY` | Required. Your API key. |
|
|
161
|
+
| `FASTOCR_BASE_URL` | Optional. Overrides `https://api.fastocr.org`. |
|
|
162
|
+
|
|
163
|
+
## Links
|
|
164
|
+
|
|
165
|
+
- Docs: https://fastocr.org/docs
|
|
166
|
+
- API keys and page packs: https://fastocr.org/developers/keys
|
|
167
|
+
- Python SDK: https://github.com/fastocr-org/fastocr-python
|
|
168
|
+
- Support: support@fastocr.org
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
MIT
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
fastocr_mcp/__init__.py,sha256=RqiKHUtiXjI4HV8v1qFXj7G9bGgcr4uyLZwKq02UnEw,189
|
|
2
|
+
fastocr_mcp/__main__.py,sha256=3dYKHfmWsrdExFlTFlcR5a_icR9fAkn06Yh14TQkEd8,33
|
|
3
|
+
fastocr_mcp/_version.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
|
|
4
|
+
fastocr_mcp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
fastocr_mcp/server.py,sha256=1XSaCtJfreLiR2aXgzDCRmtAWlSxxPteO1xAVnSMdDs,14169
|
|
6
|
+
fastocr_mcp-0.1.0.dist-info/METADATA,sha256=OwGOXhqrQ1tk03KfuaA8SWb9sHNzpCpxVSk92z1_eVs,7062
|
|
7
|
+
fastocr_mcp-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
8
|
+
fastocr_mcp-0.1.0.dist-info/entry_points.txt,sha256=IzpVLUD-ltIL-SFZpkECQfOuVErkkqpIufZDxTcy4hE,56
|
|
9
|
+
fastocr_mcp-0.1.0.dist-info/licenses/LICENSE,sha256=iAEJvhOAbbRmyZP6iiDxfCJrifPj_5COWm4eVHmbOQ8,1064
|
|
10
|
+
fastocr_mcp-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 FastOCR
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|