Skip to content

Commit 9db22f0

Browse files
authored
feat(mcp): add Python MCP server for paperjam (#46)
* fix(publish): use manylinux_2_28 for aarch64 to fix aws-lc-sys C11 compile failure manylinux2014 (auto) uses GCC 4.8 from CentOS 7 which lacks stdatomic.h, causing aws-lc-sys bcm.c cross-compilation to fail on aarch64. manylinux_2_28 uses AlmaLinux 8 with GCC 8.5 which has full C11 support. * feat(mcp): add Python MCP server for paperjam document processing FastMCP-based server exposing 48+ tools for AI agents to open, extract, manipulate, convert, and analyze documents (PDF, DOCX, XLSX, PPTX, HTML, EPUB) via the Model Context Protocol. Flat src/ layout with tools organized by domain: - document: session lifecycle (open, close, save, list, info) - extraction: text, tables, structure, markdown, search, links, images - page: page-level operations and layout analysis - manipulation: split, merge, reorder, delete, insert, rotate, optimize - annotation: watermark, annotations, stamp - metadata: document metadata, bookmarks, TOC generation - comparison: text diff and visual diff - conversion: format conversion and detection - render: page rendering to PNG/JPEG/BMP - forms: AcroForm field operations - security: sanitize, redact, encrypt - signatures: digital signature operations - validation: PDF/A and PDF/UA compliance - pipeline: YAML/JSON workflow execution Includes session management with TTL, MCP resources for document browsing, prompt templates for common workflows, structured error handling with logging, and setuptools package-dir mapping for clean src/ → paperjam_mcp installation. * docs(mcp): add README with installation, config, and tools reference Installation via uvx/pip, MCP client configuration examples for tools reference table covering all 48+ tools across 14 categories. * ci(mcp): add PyPI publish workflow with trusted publishing Triggers on mcp-* tags (e.g. mcp-0.1.0). Builds pure Python wheel with uv and publishes via OIDC trusted publishing — no API tokens. * fix(mcp): use SPDX license string to fix setuptools deprecation warning * docs: update README — remove redundant title, center badges, update MCP section * fix(mcp): resolve PR review comments — unused imports and mixed import style - server.py: move side-effect imports out, use importlib in __main__ - security.py: replace mixed import styles with single from-import * fix(mcp): harden for production — path sandboxing, resource leaks, memory limits Security: - resolve_path: reject paths that escape the working directory - pipeline tools: disabled — they bypass server-side path sandboxing Resource management: - update_document: close old document before replacing (prevents native resource leak) - _cleanup_expired: wrap close() in try/except so one failure doesn't orphan remaining sessions - close_all: new method for clean shutdown, registered via atexit - TTL now based on last access time, not creation time Memory safety: - render_page/render_pages: cap DPI at 600, limit to 50 pages per call - visual_diff: cap DPI at 300 Error handling: - Add @handle_errors to all 7 resource handlers (previously raw exceptions) * docs(mcp): update docs site MCP guide for Python server Replace Rust MCP server references with the new Python package (paperjam-mcp via uvx/pip). Add Cursor config example, complete tools reference across all 14 categories, document security sandboxing and session TTL behavior. * ci(mcp): add CI workflow for MCP server — lint, typecheck, build Runs on mcp-server/** changes. Lint job: ruff check, ruff format, mypy. Build job: wheel build + CLI smoke test on Python 3.12 and 3.13. * fix(ci): let uv sync build paperjam from source, drop maturin develop step maturin develop requires a pre-existing virtualenv. uv sync handles building the paperjam path dependency (which uses maturin) automatically. * fix(mcp): add str() casts to fix mypy no-any-return errors paperjam's stubs type extract_text() and to_markdown() as returning Any, which triggers no-any-return when returned from -> str functions.
1 parent 5491a3f commit 9db22f0

29 files changed

Lines changed: 2340 additions & 35 deletions

.github/workflows/ci-mcp.yml

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
name: CI (MCP Server)
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths:
7+
- "mcp-server/**"
8+
- ".github/workflows/ci-mcp.yml"
9+
pull_request:
10+
branches: [main]
11+
paths:
12+
- "mcp-server/**"
13+
- ".github/workflows/ci-mcp.yml"
14+
15+
concurrency:
16+
group: ${{ github.workflow }}-${{ github.ref }}
17+
cancel-in-progress: true
18+
19+
defaults:
20+
run:
21+
working-directory: mcp-server
22+
23+
jobs:
24+
lint:
25+
runs-on: ubuntu-latest
26+
steps:
27+
- uses: actions/checkout@v6
28+
29+
- uses: astral-sh/setup-uv@v6
30+
31+
- name: Install Rust toolchain
32+
uses: dtolnay/rust-toolchain@stable
33+
34+
- uses: Swatinem/rust-cache@v2.9.1
35+
36+
- name: Install dependencies (builds paperjam from source)
37+
run: uv sync
38+
39+
- name: Ruff check
40+
run: uv run ruff check src/
41+
42+
- name: Ruff format
43+
run: uv run ruff format --check src/
44+
45+
- name: Mypy
46+
run: uv run mypy src/
47+
48+
build:
49+
runs-on: ubuntu-latest
50+
needs: lint
51+
strategy:
52+
matrix:
53+
python-version: ["3.12", "3.13"]
54+
steps:
55+
- uses: actions/checkout@v6
56+
57+
- uses: actions/setup-python@v6
58+
with:
59+
python-version: ${{ matrix.python-version }}
60+
61+
- uses: astral-sh/setup-uv@v6
62+
63+
- name: Build wheel
64+
run: uv build
65+
66+
- name: Verify CLI
67+
run: |
68+
uv run --no-project --with dist/*.whl paperjam-mcp --version
69+
uv run --no-project --with dist/*.whl paperjam-mcp --help

.github/workflows/publish-mcp.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: Publish paperjam-mcp to PyPI
2+
3+
on:
4+
push:
5+
tags:
6+
- "mcp-[0-9]+.[0-9]+.[0-9]+*"
7+
8+
permissions:
9+
contents: read
10+
11+
jobs:
12+
publish:
13+
runs-on: ubuntu-latest
14+
environment: pypi
15+
permissions:
16+
id-token: write
17+
steps:
18+
- uses: actions/checkout@v6
19+
20+
- uses: astral-sh/setup-uv@v6
21+
22+
- name: Build
23+
run: uv build
24+
working-directory: mcp-server
25+
26+
- name: Publish to PyPI
27+
uses: pypa/gh-action-pypi-publish@release/v1
28+
with:
29+
packages-dir: mcp-server/dist/
30+
verbose: true
31+
skip-existing: true

README.md

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
<img src="docs-site/static/img/logo.jpeg" alt="paperjam logo" width="250">
33
</p>
44

5-
# paperjam
6-
7-
[![PyPI](https://img.shields.io/pypi/v/paperjam)](https://pypi.org/project/paperjam/)
8-
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
9-
[![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
5+
<p align="center">
6+
<a href="https://pypi.org/project/paperjam/"><img src="https://img.shields.io/pypi/v/paperjam" alt="PyPI"></a>
7+
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT"></a>
8+
<a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.12+-blue.svg" alt="Python 3.12+"></a>
9+
</p>
1010

1111
Fast document processing powered by Rust. One API. Every document format.
1212

@@ -93,14 +93,18 @@ paperjam info document.pdf
9393

9494
### MCP server
9595

96-
Add to your MCP client configuration:
96+
```bash
97+
pip install paperjam-mcp
98+
```
99+
100+
Add to your MCP client configuration (Claude Code, Claude Desktop, Cursor):
97101

98102
```json
99103
{
100104
"mcpServers": {
101105
"paperjam": {
102-
"command": "paperjam",
103-
"args": ["mcp", "serve"]
106+
"command": "uvx",
107+
"args": ["paperjam-mcp", "--working-dir", "."]
104108
}
105109
}
106110
}

docs-site/docs/guides/mcp.md

Lines changed: 137 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ title: MCP Server
55

66
# MCP Server
77

8-
paperjam ships with a built-in MCP (Model Context Protocol) server, making it the first document processing library with native AI-agent support. Any MCP-compatible client -- Claude Code, Claude Desktop, or custom agents -- can open, extract from, convert, and manipulate documents through a standard tool interface.
8+
paperjam ships with an MCP (Model Context Protocol) server, making it the first document processing library with native AI-agent support. Any MCP-compatible client -- Claude Code, Claude Desktop, Cursor, or custom agents -- can open, extract from, convert, and manipulate documents through a standard tool interface.
99

1010
## What is MCP?
1111

@@ -14,11 +14,11 @@ The Model Context Protocol is an open standard for connecting AI models to exter
1414
## Installation
1515

1616
```bash
17-
# From crates.io
18-
cargo install paperjam-mcp
17+
# Run directly (no install needed)
18+
uvx paperjam-mcp
1919

20-
# From source (in the paperjam repository)
21-
cargo install --path crates/paperjam-mcp
20+
# Or install globally
21+
pip install paperjam-mcp
2222
```
2323

2424
Verify the installation:
@@ -31,14 +31,14 @@ paperjam-mcp --version
3131

3232
### Claude Code
3333

34-
Add the server to your project's `.mcp.json` or global MCP settings:
34+
Add the server to your project's `.mcp.json`:
3535

3636
```json
3737
{
3838
"mcpServers": {
3939
"paperjam": {
40-
"command": "paperjam-mcp",
41-
"args": ["--working-dir", "/path/to/documents"]
40+
"command": "uvx",
41+
"args": ["paperjam-mcp", "--working-dir", "."]
4242
}
4343
}
4444
}
@@ -52,63 +52,162 @@ Add to your `claude_desktop_config.json`:
5252
{
5353
"mcpServers": {
5454
"paperjam": {
55-
"command": "paperjam-mcp",
56-
"args": ["--working-dir", "/Users/you/Documents"]
55+
"command": "uvx",
56+
"args": ["paperjam-mcp", "--working-dir", "/Users/you/Documents"]
57+
}
58+
}
59+
}
60+
```
61+
62+
### Cursor
63+
64+
Add to `.cursor/mcp.json`:
65+
66+
```json
67+
{
68+
"mcpServers": {
69+
"paperjam": {
70+
"command": "uvx",
71+
"args": ["paperjam-mcp", "--working-dir", "."]
5772
}
5873
}
5974
}
6075
```
6176

6277
### Server options
6378

64-
| Flag | Description |
65-
|------|-------------|
66-
| `--working-dir <path>` | Base directory for resolving relative file paths |
67-
| `--max-sessions <n>` | Maximum concurrent document sessions (default: 16) |
68-
| `--log-level <level>` | Logging verbosity: `error`, `warn`, `info`, `debug` |
79+
| Flag | Default | Description |
80+
|------|---------|-------------|
81+
| `--working-dir` | `.` | Base directory for resolving relative file paths. All file access is sandboxed to this directory. |
82+
| `--transport` | `stdio` | Transport: `stdio` or `sse` |
83+
| `--port` | `8080` | Port for SSE transport |
84+
| `--max-sessions` | `50` | Maximum concurrent document sessions |
85+
| `--session-ttl` | `3600` | Session time-to-live in seconds (resets on each access) |
86+
| `--log-level` | `warning` | Logging verbosity: `debug`, `info`, `warning`, `error` |
6987

7088
## Available tools
7189

7290
Once connected, the AI model can call these tools through the MCP protocol.
7391

74-
### Document tools
92+
### Document management
7593

7694
| Tool | Description |
7795
|------|-------------|
78-
| `open_document` | Open a document by path or URL. Returns a session ID. |
96+
| `open_document` | Open a document by path. Returns a session ID. |
7997
| `get_document_info` | Get page count, metadata, format, and structural summary. |
8098
| `save_document` | Save the current document state to disk. |
8199
| `close_document` | Close a session and free resources. |
100+
| `list_sessions` | List all open document sessions. |
82101

83-
### Extraction tools
102+
### Extraction
84103

85104
| Tool | Description |
86105
|------|-------------|
87-
| `extract_text` | Extract plain text from all or specified pages. |
106+
| `extract_text` | Extract plain text from all pages. |
88107
| `extract_tables` | Extract tables as structured data (rows, headers, cells). |
89108
| `extract_structure` | Extract headings, paragraphs, and list items. |
90109
| `to_markdown` | Convert the document to Markdown. |
110+
| `search_document` | Full-text search with regex support (PDF only). |
111+
| `extract_links` | Extract all hyperlinks (PDF only). |
112+
| `extract_images` | Extract image metadata from a page (PDF only). |
113+
| `extract_bookmarks` | Extract bookmark/TOC tree. |
114+
115+
### Page operations
116+
117+
| Tool | Description |
118+
|------|-------------|
119+
| `page_get_info` | Page dimensions and rotation. |
120+
| `page_extract_text` | Text from a specific page. |
121+
| `page_extract_tables` | Tables from a specific page. |
122+
| `page_extract_structure` | Structure from a specific page. |
123+
| `page_analyze_layout` | Detect columns, headers, footers. |
124+
| `page_to_markdown` | Convert a page to Markdown. |
125+
126+
### Manipulation
127+
128+
| Tool | Description |
129+
|------|-------------|
130+
| `split_document` | Split by page ranges into multiple sessions. |
131+
| `merge_documents` | Merge multiple PDFs into one session. |
132+
| `reorder_pages` | Reorder, subset, or duplicate pages. |
133+
| `delete_pages` | Remove specific pages. |
134+
| `insert_blank_pages` | Add blank pages at positions. |
135+
| `rotate_pages` | Rotate specific pages. |
136+
| `optimize_document` | Compress and reduce file size. |
137+
138+
### Annotations & stamps
139+
140+
| Tool | Description |
141+
|------|-------------|
142+
| `add_watermark` | Apply a text watermark to pages. |
143+
| `add_annotation` | Add annotation (text, highlight, stamp, etc.). |
144+
| `remove_annotations` | Remove annotations by type or index. |
145+
| `stamp_pages` | Overlay a page from another PDF. |
146+
147+
### Metadata & TOC
91148

92-
### Conversion tools
149+
| Tool | Description |
150+
|------|-------------|
151+
| `set_metadata` | Update title, author, subject, keywords. |
152+
| `set_bookmarks` | Set/replace bookmarks. |
153+
| `generate_toc` | Auto-generate TOC from headings. |
154+
155+
### Comparison
156+
157+
| Tool | Description |
158+
|------|-------------|
159+
| `diff_documents` | Text-level diff between two PDFs. |
160+
| `visual_diff` | Pixel-level visual comparison. |
161+
162+
### Conversion
93163

94164
| Tool | Description |
95165
|------|-------------|
96166
| `convert_document` | Convert between formats (PDF, DOCX, XLSX, PPTX, HTML, EPUB, Markdown). |
167+
| `convert_file` | Direct file-to-file conversion. |
168+
| `detect_format` | Detect document format from path. |
169+
170+
### Rendering
171+
172+
| Tool | Description |
173+
|------|-------------|
174+
| `render_page` | Render a page to PNG/JPEG/BMP. Max 600 DPI. |
175+
| `render_pages` | Render multiple pages to images. Max 50 pages per call. |
176+
177+
### Forms
178+
179+
| Tool | Description |
180+
|------|-------------|
181+
| `has_form` | Check if document has a form. |
182+
| `get_form_fields` | List all form fields. |
183+
| `fill_form` | Fill form fields by name/value. |
184+
| `modify_form_field` | Modify field properties. |
185+
| `add_form_field` | Create a new form field. |
97186

98-
### Manipulation tools
187+
### Security
99188

100189
| Tool | Description |
101190
|------|-------------|
191+
| `sanitize_document` | Remove JavaScript, actions, embedded files, and links. |
102192
| `redact_text` | Find and permanently redact text by query or regex. |
103-
| `add_watermark` | Apply a text watermark to pages. |
193+
| `redact_regions` | Redact rectangular areas. |
104194
| `encrypt_document` | Password-protect a document with AES-128, AES-256, or RC4. |
105-
| `sanitize_document` | Remove JavaScript, actions, embedded files, and links. |
106195

107-
### Pipeline tools
196+
### Digital signatures
108197

109198
| Tool | Description |
110199
|------|-------------|
111-
| `run_pipeline` | Execute a multi-step processing pipeline from a YAML definition. |
200+
| `get_signatures` | Extract signature info. |
201+
| `verify_signatures` | Verify all signatures. |
202+
| `sign_document` | Digitally sign a PDF. |
203+
204+
### Validation
205+
206+
| Tool | Description |
207+
|------|-------------|
208+
| `validate_pdf_a` | Check PDF/A compliance. |
209+
| `validate_pdf_ua` | Check PDF/UA accessibility. |
210+
| `convert_to_pdf_a` | Convert to PDF/A. |
112211

113212
## Example interaction
114213

@@ -131,7 +230,7 @@ The agent uses the extracted Markdown to write a summary, while the redacted PDF
131230

132231
The MCP server maintains document sessions. When a client calls `open_document`, the server loads the file into memory and returns a session ID. Subsequent tool calls reference this ID to operate on the same document without re-reading from disk.
133232

134-
Sessions are lightweight -- the document is loaded once and shared across all operations. The server enforces a maximum session count (`--max-sessions`) to bound memory usage. Sessions are released when the client calls `close_document` or when the MCP connection closes.
233+
Sessions are lightweight -- the document is loaded once and shared across all operations. The server enforces a maximum session count (`--max-sessions`) to bound memory usage. Sessions expire after the TTL (`--session-ttl`) of inactivity and are released when the client calls `close_document` or when the server shuts down.
135234

136235
Multiple documents can be open simultaneously in separate sessions:
137236

@@ -141,11 +240,22 @@ Multiple documents can be open simultaneously in separate sessions:
141240
4. `to_markdown` on `s2`
142241
5. `close_document` on `s1` and `s2`
143242

243+
## Security
244+
245+
The MCP server enforces a working directory sandbox:
246+
247+
- All file paths are resolved relative to `--working-dir`
248+
- Paths that escape the working directory (e.g. `../../etc/passwd`) are rejected
249+
- Absolute paths outside the working directory are rejected
250+
251+
Pipeline tools (`run_pipeline`, `validate_pipeline`) are disabled because they perform file I/O with their own path resolution that bypasses the sandbox. Use individual tools instead.
252+
144253
## Error handling
145254

146-
Tool calls return structured errors when something goes wrong:
255+
Tool calls return structured JSON errors when something goes wrong:
147256

148257
- **File not found** -- the path does not exist or is outside the working directory
258+
- **Path escapes working directory** -- the resolved path is outside the sandbox
149259
- **Invalid session** -- the session ID is expired or unrecognised
150260
- **Unsupported operation** -- e.g. calling `redact_text` on an XLSX document
151261
- **Conversion error** -- the requested format conversion is not supported

0 commit comments

Comments
 (0)