Content
# pdf-mcp
A first-class **command-line tool** and **Model Context Protocol (MCP)
server** for everything PDF: form filling, page operations, OCR,
metadata, signatures, redaction, image and table extraction, batch
processing, and LLM-backed document understanding.
Built with Python, `pypdf` (BSD), `fillpdf` (MIT), and `pymupdf` (AGPL-3.0).
**Goal**: Extract 99% of information from any PDF file, including
scanned / image-based documents, and fill any PDF form. Works equally
well as a daily CLI for humans, as a CI / automation script, and as an
MCP server inside Cursor or Claude Desktop.
## Status
[](https://github.com/nfsarch33/pdf-mcp-server/releases/latest)
[](https://github.com/nfsarch33/pdf-mcp-server/actions/workflows/ci.yml)
[](https://github.com/nfsarch33/pdf-mcp-server/actions/workflows/codeql.yml)
[](https://www.python.org/downloads/)
[](https://opensource.org/licenses/Apache-2.0)
## Quick start
```bash
# 1. Install (project root)
git clone https://github.com/nfsarch33/pdf-mcp-server.git
cd pdf-mcp-server
make install # uv-based install of runtime requirements
# 2. Try the CLI
pdf-mcp --help # top-level help
pdf-mcp form --help # 9 form-handling tools
pdf-mcp ai get-llm-backend-info --pretty # check LLM backends
# 3. Run as an MCP server (Cursor, Claude Desktop, etc.)
pdf-mcp serve # stdio transport
# 4. Run the tests
make test
```
Run the CLI on a real PDF:
```bash
# Read form fields
pdf-mcp form get-pdf-form-fields \
--json '{"pdf_path":"sample.pdf"}' --pretty
# Fill a form (writes filled.pdf)
pdf-mcp form fill-pdf-form --json '{
"input_path": "sample.pdf",
"output_path": "filled.pdf",
"data": {"Name": "Jane Doe", "Email": "jane@example.com"},
"flatten": false
}'
# OCR a scanned PDF
pdf-mcp extract extract-text --json '{
"pdf_path": "scanned.pdf",
"engine": "ocr",
"language": "eng"
}' --pretty
```
## CLI surface (v1.3.0+)
`pdf-mcp` ships **all 57 underlying tools** as CLI subcommands grouped
by verb. The taxonomy comes directly from `pdf_mcp.registry`, so the
CLI and MCP surfaces never drift.
| Verb | Tools | Examples |
|------|-------|----------|
| `pdf-mcp form` | 9 | `fill-pdf-form`, `get-pdf-form-fields`, `flatten-pdf` |
| `pdf-mcp pages` | 8 | `merge-pdfs`, `split-pdf`, `extract-pages`, `rotate-pages` |
| `pdf-mcp text` | 13 | `redact-text-regex`, `add-text-watermark`, `add-bates-numbering` |
| `pdf-mcp extract` | 7 | `extract-text`, `extract-tables`, `extract-images`, `extract-links` |
| `pdf-mcp metadata` | 4 | `get-pdf-metadata`, `set-pdf-metadata`, `sanitize-pdf-metadata` |
| `pdf-mcp sign` | 6 | `sign-pdf`, `verify-digital-signatures`, `add-signature-image` |
| `pdf-mcp ocr` | 2 | `get-ocr-languages`, `get-image-info` |
| `pdf-mcp ai` | 4 | `auto-fill-pdf-form`, `extract-structured-data`, `analyze-pdf-content` |
| `pdf-mcp batch` | 2 | `batch-process`, `compare-pdfs` |
| `pdf-mcp security` | 2 | `encrypt-pdf`, `detect-pii-patterns` |
| `pdf-mcp serve` | — | Run as an MCP server over stdio (drop-in for `python -m pdf_mcp.server`). |
Every command accepts:
```
pdf-mcp <verb> <tool> [--json '{...}'] [--json-file PATH]
[--pretty] [--output PATH]
```
* `--json` and `--json-file` are mutually exclusive (`--json` wins).
* `--pretty` indents the JSON output for human reading.
* `--output PATH` writes the JSON result to a file instead of stdout.
* Tool exceptions exit non-zero with `error: <tool> failed: <msg>` on
stderr, so the CLI is safe to use in pipelines.
The full per-tool reference is in [`USAGE.md`](USAGE.md), generated
directly from the registry by `scripts/generate_usage_doc.py`.
### Why this layout?
* **Verb-first** matches Unix muscle memory (`git commit`, `kubectl apply`).
* **Same code as the MCP surface** — there is one source of truth
(`pdf_mcp.registry`) so a CLI invocation and an MCP tool call hit
identical implementations.
* **Lazy imports** — `pdf-mcp --help` and `pdf-mcp <verb> --help`
do **not** load `pymupdf` or `pypdf`. The heavy dependency tree only
loads when a tool is actually run, so help is sub-second cold.
* **Backwards compatible** — `pdf-mcp serve` is a drop-in replacement
for `python -m pdf_mcp.server`. Existing Cursor / Claude Desktop
configs need no changes.
## CI notes
- Dependency Review requires GitHub Dependency Graph to be enabled in the repository settings.
- AI Review is optional and only runs if you add the `OPENAI_API_KEY` repository secret.
## Setup (uv)
1) Install `uv` if not present:
```bash
curl -Ls https://astral.sh/uv/install.sh | sh
```
2) Install dependencies (project root is this folder):
```bash
cd /path/to/pdf-mcp-server
uv pip install -r requirements.txt
```
Or use the Makefile:
```bash
cd /path/to/pdf-mcp-server
make install
```
For best flatten support, install Poppler:
```bash
sudo apt-get install poppler-utils
```
### OCR Support (Optional)
For OCR capabilities on scanned/image-based PDFs, install Tesseract:
**macOS:**
```bash
brew install tesseract
pip install pytesseract pillow
```
**Linux (Ubuntu/Debian):**
```bash
sudo apt-get install tesseract-ocr
pip install pytesseract pillow
```
**Additional language packs (optional):**
```bash
sudo apt-get install tesseract-ocr-chi-tra tesseract-ocr-jpn tesseract-ocr-kor
```
Or install with the `ocr` extra:
```bash
pip install -e ".[ocr]"
```
## Run the MCP server
```bash
python -m pdf_mcp.server
```
(It runs over stdio by default.)
## Register with Cursor
Edit `~/.cursor/mcp.json`:
```json
{
"mcpServers": {
"pdf-mcp": {
"command": "/path/to/pdf-mcp-server/.venv/bin/python",
"args": ["-m", "pdf_mcp.server"],
"description": "Local PDF form filling and editing (stdio)"
}
}
}
```
Restart Cursor after saving.
## Features overview (57 tools across 10 verbs)
| Verb group | Tools | What it does |
|------------|-------|--------------|
| `form` | 9 | Read, fill, clear, flatten, and create PDF forms (AcroForm + label-detection fallback). |
| `pages` | 8 | Merge, split, extract, rotate, reorder, insert, remove, optimise. |
| `text` | 13 | Annotations, redaction, watermarks, comments, page numbers, Bates stamps. |
| `extract` | 7 | Text blocks, tables, images, links, structured data, barcodes/QR, format export. |
| `metadata` | 4 | Read, write, sanitise metadata + classify PDF type (searchable / image / hybrid). |
| `sign` | 6 | PKCS#12 / PEM digital signatures + signature image stamps + verification. |
| `ocr` | 2 | OCR language inventory + image inspection (Tesseract + optional packs). |
| `ai` | 4 | LLM-backed form auto-fill, structured data extraction, document analysis. |
| `batch` | 2 | Multi-file processing and PDF-vs-PDF comparison. |
| `security` | 2 | Password encryption + PII pattern detection. |
**See [`USAGE.md`](USAGE.md) for the full per-tool command reference.**
The same 57 tools are exposed as MCP tools with their original
`snake_case` names (e.g. `fill_pdf_form`, `extract_text`,
`detect_pii_patterns`).
## Available tools (Python API reference)
The Python API on `pdf_mcp.pdf_tools` matches the CLI / MCP surface
1:1. `pdf-mcp form fill-pdf-form ...` calls the same
`pdf_tools.fill_pdf_form(...)` function as an MCP `fill_pdf_form` tool
call. Pick the surface that matches your workflow.
### Form Handling
- `get_pdf_form_fields(pdf_path)`: list fields and count.
- `fill_pdf_form(input_path, output_path, data, flatten=False)`: fill fields; optional flatten.
- `fill_pdf_form_any(input_path, output_path, data, flatten=False)`: fill standard or non-standard forms using label detection when needed.
- `clear_pdf_form_fields(input_path, output_path, fields=None)`: clear values while keeping fields fillable.
- `flatten_pdf(input_path, output_path)`: flatten forms/annotations.
- `create_pdf_form(output_path, fields, page_size=None, pages=1)`: create a new PDF with AcroForm fields.
- `get_form_templates()`: list built-in templates for common workflows.
- `create_pdf_form_from_template(output_path, template_name)`: create a form from a built-in template.
Note: XFA/LiveCycle forms are not supported; convert to AcroForm or flatten first.
### Page Operations
- `merge_pdfs(pdf_list, output_path)`: merge multiple PDFs.
- `extract_pages(input_path, pages, output_path)`: 1-based pages, supports negatives (e.g., -1 = last).
- `rotate_pages(input_path, pages, degrees, output_path)`: degrees must be multiple of 90.
- `reorder_pages(input_path, pages, output_path)`: reorder pages using a 1-based page list.
- `insert_pages(input_path, insert_from_path, at_page, output_path)`: insert pages from another PDF.
- `remove_pages(input_path, pages, output_path)`: remove specific pages.
### Annotations & Text
- `add_text_annotation(input_path, page, text, output_path, rect=None, annotation_id=None)`: add FreeText annotation.
- `update_text_annotation(input_path, output_path, annotation_id, text, pages=None)`: update annotation by id.
- `remove_text_annotation(input_path, output_path, annotation_id, pages=None)`: remove annotation by id.
- `remove_annotations(input_path, output_path, pages, subtype=None)`: remove annotations, optionally by subtype.
- `redact_text_regex(input_path, output_path, pattern, ...)`: redact text using a regex pattern.
- `add_text_watermark(input_path, output_path, text, ...)`: add text watermark/stamp.
- `add_highlight(input_path, output_path, page, text=None, rect=None)`: add highlight annotations.
- `add_date_stamp(input_path, output_path, ...)`: add a date stamp.
- `add_page_numbers(input_path, output_path, ...)`: add page numbers as annotations.
- `add_bates_numbering(input_path, output_path, ...)`: add Bates numbering as annotations.
- `add_comment` / `update_comment` / `remove_comment`: PDF comments (sticky notes).
### Signatures & Security
- `add_signature_image(input_path, output_path, page, image_path, rect)`: add signature image.
- `update_signature_image(...)`: update or resize signature.
- `remove_signature_image(...)`: remove signature image.
- `sign_pdf(input_path, output_path, pfx_path, ...)`: sign with PKCS#12/PFX, optional `timestamp_url`, `embed_validation_info`, `allow_fetching`, `docmdp_permissions`.
- `sign_pdf_pem(input_path, output_path, key_path, cert_path, ...)`: sign with PEM key + cert chain, supports timestamping, revocation checks, DocMDP.
- `encrypt_pdf(input_path, output_path, user_password, ...)`: password-protect PDF.
- `verify_digital_signatures(pdf_path)`: verify digital signatures.
### Metadata
- `get_pdf_metadata(pdf_path, full=False)`: return document metadata; set `full=True` for extended info.
- `set_pdf_metadata(input_path, output_path, title=None, author=None, ...)`: set metadata fields.
- `sanitize_pdf_metadata(input_path, output_path, ...)`: remove metadata keys.
### Export
- `export_pdf(pdf_path, output_path, format="markdown", ...)`: export text to Markdown or JSON. Formats: "markdown", "json".
### OCR and Text Extraction
- `detect_pdf_type(pdf_path)`: analyze PDF to classify as "searchable", "image_based", or "hybrid"; returns page-by-page metrics and OCR recommendation.
- `extract_text(pdf_path, engine="auto", pages=None, include_confidence=False, ...)`: unified text extraction. Engines: "native", "auto", "smart", "ocr", "force_ocr". Set `include_confidence=True` for word-level confidence scores.
- `get_pdf_text_blocks(pdf_path, pages=None)`: extract text blocks with bounding box positions (useful for form field detection).
- `get_ocr_languages()`: get available Tesseract languages and installation status.
Optional language packs: `chi_tra`, `jpn`, `kor` improve OCR for low-quality scans.
### Table Extraction
- `extract_tables(pdf_path, pages=None, output_format="list")`: extract tables as structured data; format "list" or "dict" (with headers).
### Image Extraction
- `extract_images(pdf_path, output_dir, pages=None, min_width=50, min_height=50, image_format="png")`: extract embedded images to files.
- `get_image_info(pdf_path, pages=None)`: get image metadata (dimensions, format, positions) without extracting.
### Form Auto-Detection
- `detect_form_fields(pdf_path, pages=None)`: detect potential form fields in non-AcroForm PDFs using text pattern analysis (labels, checkboxes, underlines).
### PII Detection
- `detect_pii_patterns(pdf_path, pages=None)`: detect common PII patterns (email, phone, SSN, credit cards).
### Link Extraction
- `extract_links(pdf_path, pages=None)`: extract URLs, hyperlinks, and internal references with type categorization.
### PDF Optimization
- `optimize_pdf(pdf_path, output_path, quality="medium")`: compress PDF; quality: "low" (max compression), "medium", "high".
### Barcode/QR Detection
- `detect_barcodes(pdf_path, pages=None, dpi=200)`: detect and decode QR codes, Code128, EAN13, etc. Requires optional `pyzbar`.
### Page Splitting
- `split_pdf(pdf_path, output_dir, mode="pages", pages_per_split=1)`: split PDF by pages or bookmarks. Modes: "pages", "bookmarks".
### PDF Comparison
- `compare_pdfs(pdf1_path, pdf2_path, compare_text=True, compare_images=False)`: diff two PDFs, find text and page differences.
### Batch Processing
- `batch_process(pdf_paths, operation, output_dir=None)`: process multiple PDFs; operations: "get_info", "extract_text", "extract_links", "optimize".
### API Reference
**Unified Text Extraction:**
```python
extract_text(pdf_path, engine="auto", pages=None, include_confidence=False,
native_threshold=100, dpi=300, language="eng", min_confidence=0)
```
- Engines: "native" (fast), "auto" (native→OCR), "smart" (per-page), "ocr"/"tesseract", "force_ocr"
- Set `include_confidence=True` for word-level confidence scores
**Unified Page Splitting:**
```python
split_pdf(pdf_path, output_dir, mode="pages", pages_per_split=1)
```
- Modes: "pages" (by count), "bookmarks" (by table of contents)
**Unified Export:**
```python
export_pdf(pdf_path, output_path, format="markdown", engine="auto", ...)
```
- Formats: "markdown", "json"
**Extended Metadata:**
```python
get_pdf_metadata(pdf_path, full=False) # Set full=True for document info
```
### Agentic AI (v0.8.0+) with Local VLM Support (v0.9.0+)
LLM-powered tools for intelligent PDF processing. **Uses local VLM by default (free, no API costs!)** Falls back to Ollama or OpenAI.
- `get_llm_backend_info()`: Check which LLM backends are available (local, ollama, openai).
- `auto_fill_pdf_form(pdf_path, output_path, source_data, backend=None)`: Intelligently fill form fields with LLM-powered field mapping.
- `extract_structured_data(pdf_path, data_type=None, schema=None, ocr_engine="auto", ocr_language="eng", backend=None)`: Extract structured data using pattern matching or LLM (includes non-LLM `passport` extraction via MRZ + labels). Use `ocr_language` for non-English OCR.
- `analyze_pdf_content(pdf_path, include_summary=True, detect_entities=True, backend=None)`: Analyze PDF for document type classification, entity extraction, and summarization.
**Backend Priority (v0.9.0+):**
1. **local** (free): Local model server at `localhost:8100` using Qwen3-VL
2. **ollama** (free): Local Ollama models
3. **openai** (paid): OpenAI API (requires `OPENAI_API_KEY`)
**Start local model server (recommended):**
```bash
# Self-contained setup (one-time)
./scripts/setup_environment.sh
# Start local VLM (auto-detects best GPU)
./scripts/run_local_vlm.sh
```
**Or use Ollama:**
```bash
# Install Ollama
curl -fsSL https://ollama.ai/install.sh | sh
# Install model (skips download if already present)
make install-llm-models
```
Optional override:
```bash
export PDF_MCP_OLLAMA_MODEL="qwen3-vl:8b"
```
**Or use OpenAI (paid):**
```bash
pip install -e ".[llm]"
export OPENAI_API_KEY="your-api-key"
export PDF_MCP_LLM_BACKEND="openai" # Force OpenAI
```
**Example:**
```python
# Check available backends
info = get_llm_backend_info()
print(info["current_backend"]) # "local", "ollama", or "openai"
# Intelligent form filling (uses local VLM by default)
result = auto_fill_pdf_form("form.pdf", "filled.pdf", {
"name": "John Smith", # Maps to "Full Name" field
"email_address": "john@example.com" # Maps to "Email" field
})
print(result["backend"]) # Which backend was used
# Extract invoice data
result = extract_structured_data("invoice.pdf", data_type="invoice")
print(result["data"]["total"]) # "$162.00"
# Analyze document
result = analyze_pdf_content("document.pdf")
print(result["document_type"]) # "invoice"
print(result["summary"]) # "Invoice #12345 for $162.00..."
```
## Conventions
- Paths should be absolute; outputs are created with parent directories if missing.
- Inputs must exist and be files; errors return `{ "error": "..." }`.
- Form flattening prefers fillpdf+poppler; falls back to a pypdf-only flatten (removes form structures).
- Text insert/edit/remove is implemented via managed FreeText annotations, not by editing PDF content streams.
## Smoke tests (manual)
```bash
python - <<'PY'
from pdf_mcp import pdf_tools
sample = "/path/to/sample.pdf"
out = "/tmp/out.pdf"
print(pdf_tools.get_pdf_form_fields(sample))
print(pdf_tools.fill_pdf_form(sample, out, {"Name": "Test"}, flatten=True))
PY
```
## Automated Tests
```bash
cd /path/to/pdf-mcp-server
# Run all tests
make test
# Run OCR-specific tests (requires Tesseract)
make test-ocr
# Quick test run
make test-quick
# Check Tesseract installation status
make check-tesseract
# Install OCR dependencies
make install-ocr
# Pre-push checks (lint + format + test + smoke)
make prepush
```
### LLM/Agentic AI Tests (v0.9.2+)
```bash
# Run all LLM-related tests (mocked)
make test-llm
# Run E2E tests with real LLM backends
# Requires: local model server at localhost:8100, or Ollama, or OPENAI_API_KEY
make test-e2e
# Check LLM backend status
make check-llm
# Install LLM dependencies
make install-llm
# Ensure Ollama model is present (no duplicate installs)
make install-llm-models
```
**E2E Test Requirements:**
- **Local VLM**: Run `./scripts/run_local_vlm.sh` (auto-detects best GPU)
- **Ollama**: Install with `curl -fsSL https://ollama.ai/install.sh | sh`, then `make install-llm-models`
- **OpenAI**: Optional remote fallback. Set `OPENAI_API_KEY` plus
`PDF_MCP_ENABLE_REMOTE_LLM=1` to opt in. Sensitive flows such as
passport extraction still require
`PDF_MCP_ALLOW_REMOTE_LLM_FOR_SENSITIVE=1`.
**Full environment setup (one-time):**
```bash
./scripts/setup_environment.sh
```
This handles Python venv, system packages, Ollama, GPU detection, and VLM runner generation for both macOS and WSL/Linux.
### Test coverage
- **526 collected tests** (496 passing, 30 optional-backend skips on the
current macOS dev lane) covering Tier 1/2, agentic features,
multi-backend behavior, e2e paths, v1.3.0 CLI surface, and verb-group
mounts.
- 75% line coverage gate enforced in CI via `pytest --cov-fail-under=75` (see `pyproject.toml`).
- `pytest --cov` ships in dev extras (`make install-dev`) and writes
`coverage.xml` for upload to coverage tools.
- All tests pass with Tesseract installed; a small number skip
depending on which optional dependencies / backends are available.
## Privacy and remote LLMs
`pdf-mcp` is local-first. PDF text is processed on your machine by
default, and LLM-backed tools prefer local model servers or Ollama.
OpenAI support is optional and explicit:
```bash
export OPENAI_API_KEY=...
export PDF_MCP_ENABLE_REMOTE_LLM=1
```
Sensitive workflows, including passport extraction and form-field
mapping, are blocked from remote LLM backends by default. To use a
remote model for those flows, set:
```bash
export PDF_MCP_ALLOW_REMOTE_LLM_FOR_SENSITIVE=1
```
This fail-closed behaviour avoids accidental provider policy blocks and
prevents identity-document text from leaving the host unless the user
deliberately opts in.
## Development Workflow
- Use feature branches off `main` and open a PR for review.
- Keep each PR focused on a single tool or capability with tests.
- For larger features, split into small PRs (tool surface, core implementation, tests, docs).
- After merging a PR, delete the feature branch and run `git fetch --prune` locally to keep branch state clean.
## License
`pdf-mcp` itself is **Apache-2.0** (since v1.3.0; v1.2.x and earlier were AGPL-3.0).
See [`LICENSE`](LICENSE) for full text and [`NOTICE`](NOTICE) for the relicense
notice and attribution.
> **Important:** `pdf-mcp` depends on [`pymupdf`](https://github.com/pymupdf/PyMuPDF),
> which is **AGPL-3.0** (with an optional commercial license from Artifex). When
> you distribute `pdf-mcp` together with `pymupdf`, the combined distribution is
> subject to AGPL-3.0 terms because `pymupdf` is the copyleft component. The
> Apache-2.0 license on `pdf-mcp`'s own code remains valid for use, modification,
> and redistribution of `pdf-mcp` code in isolation or with permissive-licensed
> dependencies. Closed-source/proprietary distributors should contact Artifex
> for a commercial `pymupdf` license, or wait for a future release that
> replaces `pymupdf` with permissive-licensed alternatives. See [`NOTICE`](NOTICE)
> for the full third-party dependency breakdown.
## Changelog
See `CHANGELOG.md`.
MCP Config
Below is the configuration for this MCP Server. You can copy it directly to Cursor or other MCP clients.
mcp.json
Connection Info
You Might Also Like
everything-claude-code
Complete Claude Code configuration collection - agents, skills, hooks,...
markitdown
Python tool for converting files and office documents to Markdown.
awesome-claude-skills
A curated list of awesome Claude Skills, resources, and tools for...
antigravity-awesome-skills
The Ultimate Collection of 130+ Agentic Skills for Claude...
claude-context-mode
claude-context-mode plugin reduces MCP context bloat, saving up to 99% of tokens.
context-mode
MCP is the protocol for tool access. We're the virtualization layer for context.