PDF Triage
Read local PDFs without uploading. Classifies first, flags untrustworthy text, bounds output.
Open source Open in the app JSON README (API)
About
Read local PDFs without uploading. Classifies first, flags untrustworthy text, bounds output.
Details
- Kind
- MCP servers
- Topic
- Files & documents
- Publisher
- vishalmeena2211
- Origin
- official
- Category
- ferramentas
- Transport
- local
- Version
- 0.1.2
- Last push
- 2026-08-03T13:13:17Z
- Repository state
- ativo
- Language
- TypeScript
- License
- MIT
- Added
- 2026-08-29 04:01:39
- Updated
- 2026-08-29 04:01:39
- Origin id
io.github.vishalmeena2211/pdf-triage-mcp
README
# pdf-triage-mcp
[](https://www.npmjs.com/package/pdf-triage-mcp)
[](https://github.com/vishalmeena2211/pdf-triage-mcp/actions/workflows/ci.yml)
[](https://registry.modelcontextprotocol.io)
[](LICENSE)
An MCP server that lets any AI tool read local PDFs — **without uploading them, without an OCR bill, and without silently handing back garbage.**
Built on [`@firecrawl/pdf-inspector`](https://github.com/firecrawl/pdf-inspector) (Rust, no ML models, no external services).
```
┌─ pdf_classify ──→ text_based · 0.98 · 12 pages · 0 need OCR ~20ms
├─ pdf_search ──→ "invoice total" found on p4, p9 ~150ms
├─ pdf_extract ──→ clean Markdown, truncated to your budget ~150ms
└─ pdf_tables ──→ just the pipe tables, no prose ~150ms
```
---
## Table of contents
- [Why this exists](#why-this-exists)
- [Install](#install)
- [Connect it to your AI tool](#connect-it-to-your-ai-tool) — 12 clients
- [Tools](#tools)
- [Configuration](#configuration)
- [Engine fallback](#engine-fallback)
- [Known limitations](#known-limitations)
- [Development](#development)
---
## Why this exists
Most PDF tooling has the same failure mode: **it returns confident text regardless of whether extraction actually worked.** Broken CID fonts, substitution-cipher encodings, scanned pages with no text layer — you get plausible-looking output and find out downstream, if at all.
`pdf-inspector` is unusually good at *knowing* when it failed. It emits `U+FFFD` rather than guessing at an unmapped CID, runs substitution-cipher detection over its own output, and reclassifies a document as scanned when extracted text drops below 50% alphanumeric. But it stops at reporting those findings on a result object — and most wrappers throw them away.
**This server acts on them.** Every response carries the trust signals, above the content, where the model reads them first:
```markdown
> [!WARNING] ENCODING ISSUES DETECTED. The text layer decoded to suspicious
> output — typically a garbled CID font or a substitution-cipher encoding
> where letter frequencies match natural language but the letters themselves
> are wrong. Treat all extracted text here as unreliable and prefer OCR.
---
# Quarterly Report
...
```
Three design rules follow:
1. **Classify before extracting.** `pdf_classify` costs ~20ms and tells you whether extraction is worth attempting at all.
2. **Bound every output.** A 300-page PDF is easily 500k tokens. Everything truncates by default and tells you how to page through instead.
3. **Confine every path.** A model that has just read an untrusted document must not be talkable into reading `~/.ssh/id_rsa`. Enforced in code, not left to the model's judgement.
---
## Install
**Nothing to install.** Every config below runs the published package straight from npm:
```
npx -y pdf-triage-mcp --root /path/to/your/documents
```
Your MCP client runs that for you — you only need to paste the config. Confirm it works first:
```bash
npx -y pdf-triage-mcp --version
```
**Requires Node 20+.** Available on npm as [`pdf-triage-mcp`](https://www.npmjs.com/package/pdf-triage-mcp) and in the [MCP Registry](https://registry.modelcontextprotocol.io) as `io.github.vishalmeena2211/pdf-triage-mcp`.
<details>
<summary>From source instead (for development)</summary>
```bash
git clone https://github.com/vishalmeena2211/pdf-triage-mcp.git
cd pdf-triage-mcp
npm install
npm run build
node dist/index.js --root ~/Documents
```
Then substitute `"command": "node", "args": ["/absolute/path/to/dist/index.js", ...]` for the `npx` invocation in any config below.
</details>
---
## Connect it to your AI tool
Every config below is complete as written except for one value:
- **`/Users/me/Documents`** — replace with the directory the server may read. This is the only thing you must change.
Use an absolute path; `~` is not expanded by most clients. Repeat `--root` for multiple directories.
> **PATH gotcha, applies to every GUI client below.** Desktop apps launch servers with a minimal environment, so bare `npx` often fails to resolve even though it works in your terminal. If the server won't start, substitute the absolute path — find it with `which npx` (commonly `/opt/homebrew/bin/npx` on Apple Silicon, `/usr/local/bin/npx` on Intel macOS).
> **Why `-y`?** It skips npx's install confirmation prompt. Without it, a first run can hang waiting for input that an MCP client cannot provide — the server appears to start and then silently times out.
<details open>
<summary><b>1. Claude Desktop</b></summary>
**Config file**
| OS | Path |
|---|---|
| macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` |
| Windows | `%APPDATA%\Claude\claude_desktop_config.json` |
| Linux | `~/.config/Claude/claude_desktop_config.json` |
```json
{
"mcpServers": {
"pdf-triage": {
"command": "npx",
"args": [
"-y", "pdf-triage-mcp",
"--root", "/Users/me/Documents"
]
}
}
}
```
**Verify:** Fully quit and relaunch Claude Desktop (not just close the window). A tools icon appears near the chat input — click it and confirm the four `pdf_*` tools are listed.
Logs: `~/Library/Logs/Claude/mcp*.log` (macOS), `%APPDATA%\Claude\logs\mcp*.log` (Windows).
[Docs](https://modelcontextprotocol.io/quickstart/user)
</details>
<details>
<summary><b>2. Claude Code</b></summary>
**CLI — the easiest route.** The `--` separator is mandatory; everything after it is the server command.
```bash
# Just you, this project (default)
claude mcp add pdf-triage -- npx -y pdf-triage-mcp --root /Users/me/Documents
# Just you, every project
claude mcp add --scope user pdf-triage -- npx -y pdf-triage-mcp --root /Users/me/Documents
# Shared with your team, writes .mcp.json to the repo
claude mcp add --scope project pdf-triage -- npx -y pdf-triage-mcp --root /Users/me/Documents
```
**Or edit `.mcp.json` at the project root directly:**
```json
{
"mcpServers": {
"pdf-triage": {
"type": "stdio",
"command": "npx",
"args": [
"-y", "pdf-triage-mcp",
"--root", "/Users/me/Documents"
]
}
}
}
```
| Scope | Stored in | Shared |
|---|---|---|
| `local` (default) | `~/.claude.json`, under this project | No |
| `user` | `~/.claude.json`, top level | No |
| `project` | `.mcp.json` in repo root | Yes, via git |
**Verify:** `claude mcp list` → look for `✔ Connected`. Project-scoped servers need approval on first use — run `/mcp` inside a session.
[Docs](https://code.claude.com/docs/en/mcp)
</details>
<details>
<summary><b>3. Cursor</b></summary>
**Config file:** `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global). Project wins on conflict.
```json
{
"mcpServers": {
"pdf-triage": {
"command": "npx",
"args": [
"-y", "pdf-triage-mcp",
"--root", "/Users/me/Documents"
]
}
}
}
```
**Verify:** Cursor hot-reloads — no restart. Open **Cursor Settings → Tools & MCP** and look for a green dot next to `pdf-triage`.
[Docs](https://docs.cursor.com/context/model-context-protocol)
</details>
<details>
<summary><b>4. Windsurf</b></summary>
**Config file:** `~/.codeium/windsurf/mcp_config.json` (macOS/Linux), `%USERPROFILE%\.codeium\windsurf\mcp_config.json` (Windows).
Not created on first launch — create it yourself if missing.
```json
{
"mcpServers": {
"pdf-triage": {
"command": "npx",
"args": [
"-y", "pdf-triage-mcp",
"--root", "/Users/me/Documents"
]
}
}
}
```
**Verify:** Windsurf watches the file and hot-reloads on save. Tools appear in Cascade on the next chat session.
[Docs](https://docs.windsurf.com/windsurf/mcp)
</details>
<details>
<summary><b>5. VS Code + GitHub Copilot</b></summary>
> **The key is `servers`, not `mcpServers`.** This is the most common mistake when copying a config from Claude Desktop.
**Config file:** `.vscode/mcp.json` (workspace), or Command Palette → **MCP: Open User Configuration** (global).
```json
{
"servers": {
"pdf-triage": {
"type": "stdio",
"command": "npx",
"args": [
"-y", "pdf-triage-mcp",
"--root", "/Users/me/Documents"
]
}
}
}
```
**CLI alternative:**
```bash
code --add-mcp '{"name":"pdf-triage","command":"npx","args":["-y","pdf-triage-mcp","--root","/Users/me/Documents"]}'
```
**Verify:** MCP tools only work in **Agent mode** — switch from Ask/Edit to Agent in Copilot Chat, then click **Configure Tools** and confirm the `pdf_*` tools appear. Restart VS Code after first adding the file.
[Docs](https://code.visualstudio.com/docs/agent-customization/mcp-servers)
</details>
<details>
<summary><b>6. Zed</b></summary>
> **The key is `context_servers`, not `mcpServers`,** and `command` is a nested object rather than a string.
**Config file:** `~/.config/zed/settings.json` (macOS/Linux), `%APPDATA%\Zed\settings.json` (Windows). Command Palette → **zed: open settings**.
```json
{
"context_servers": {
"pdf-triage": {
"source": "custom",
"command": {
"path": "npx",
"args": [
"-y", "pdf-triage-mcp",
"--root", "/Users/me/Documents"
],
"env": {}
}
}
}
}
```
If your Zed version rejects that, it predates the nested form — try `command`, `args` and `env` flat at the top level of the server object instead.
**Verify:** Agent Panel (`Cmd+Shift+A`) → gear icon → **MCP Servers**. Green dot means connected.
[Docs](https://zed.dev/docs/ai/mcp)
</details>
<details>
<summary><b>7. Cline (VS Code extension)</b></summary>
**Config file** — separate from VS Code's own:
| OS | Path |
|---|---|
| macOS | `~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json` |
| Windows | `%APPDATA%\Code\User\globalStorage\saoudrizwan.claude-dev\settings\cline_mcp_settings.json` |
| Linux | `~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json` |
```json
{
"mcpServers": {
"pdf-triage": {
"command": "npx",
"args": [
"-y", "pdf-triage-mcp",
"--root", "/Users/me/Documents"
],
"disabled": false,
"autoApprove": ["pdf_classify", "pdf_search"]
}
}
}
```
`autoApprove` runs the listed read-only tools without a confirmation prompt.
**Easier route:** Cline panel → MCP servers icon → **Edit MCP Settings** opens this file directly.
**Verify:** Panel refreshes automatically; green dot next to the server.
[Docs](https://docs.cline.bot/mcp/mcp-overview)
</details>
<details>
<summary><b>8. Continue.dev</b></summary>
**Config file:** `~/.continue/config.yaml` (global) or `.continue/config.yaml` (project). YAML is current; `config.json` is deprecated.
> `mcpServers` here is a **list**, not an object — and YAML needs spaces, never tabs.
```yaml
mcpServers:
- name: pdf-triage
command: npx
args:
- -y
- pdf-triage-mcp
- --root
- /Users/me/Documents
```
**Verify:** Reloads automatically on save. Switch Continue to **Agent mode** — MCP tools are unavailable in other modes.
[Docs](https://docs.continue.dev/customize/deep-dives/mcp)
</details>
<details>
<summary><b>9. Google Gemini CLI</b></summary>
**CLI:**
```bash
gemini mcp add pdf-triage npx -y pdf-triage-mcp --root /Users/me/Documents
# global instead of project-scoped
gemini mcp add --scope user pdf-triage npx -y pdf-triage-mcp --root /Users/me/Documents
```
**Or edit `~/.gemini/settings.json` (global) / `.gemini/settings.json` (project):**
```json
{
"mcpServers": {
"pdf-triage": {
"command": "npx",
"args": [
"-y", "pdf-triage-mcp",
"--root", "/Users/me/Documents"
],
"timeout": 30000,
"trust": false
}
}
}
```
**Verify:** Run `/mcp` inside a `gemini` session — servers show `CONNECTED` with their tool list. Or `gemini mcp list` from the shell.
[Docs](https://google-gemini.github.io/gemini-cli/docs/tools/mcp-server.html)
</details>
<details>
<summary><b>10. OpenAI Codex CLI</b></summary>
**Config file:** `~/.codex/config.toml` (global) or `.codex/config.toml` (project).
> TOML, and the key is `mcp_servers` — snake_case, never `mcpServers`.
```toml
[mcp_servers.pdf-triage]
command = "npx"
args = [
"-y", "pdf-triage-mcp",
"--root", "/Users/me/Documents"
]
startup_timeout_sec = 20
tool_timeout_sec = 60
```
**Verify:** `codex doctor --json` validates the config syntax. Note that it validates syntax only — it does not confirm the server actually spawned.
> **Known upstream issue:** several Codex CLI versions have a bug where stdio servers validate cleanly but silently fail to start, showing `Tools: none` in the TUI ([#3441](https://github.com/openai/codex/issues/3441), [#26810](https://github.com/openai/codex/issues/26810)). That is a Codex runtime bug, not a config error.
[Docs](https://learn.chatgpt.com/docs/extend/mcp?surface=cli)
</details>
<details>
<summary><b>11. JetBrains AI Assistant / Junie</b></summary>
**AI Assistant** — configured through the IDE, no file to edit:
1. **Settings → Tools → AI Assistant → Model Context Protocol (MCP)**
2. **Add** → transport **STDIO**
3. Paste:
```json
{
"mcpServers": {
"pdf-triage": {
"command": "npx",
"args": [
"-y", "pdf-triage-mcp",
"--root", "/Users/me/Documents"
]
}
}
}
```
4. **OK → Apply**
**Junie** uses a file instead — `~/.junie/mcp/mcp.json` (global) or `.junie/mcp/mcp.json` (project), same JSON shape.
**Verify:** Check the **Status** column in the MCP settings panel; click it to list the server's tools.
[Docs](https://www.jetbrains.com/help/ai-assistant/mcp.html)
</details>
<details>
<summary><b>12. LM Studio</b></summary>
**Config file:** `~/.lmstudio/mcp.json` (macOS/Linux), `%USERPROFILE%\.lmstudio\mcp.json` (Windows).
Easier via the app: right sidebar → **Program** tab → **Install → Edit mcp.json**.
```json
{
"mcpServers": {
"pdf-triage": {
"command": "npx",
"args": [
"-y", "pdf-triage-mcp",
"--root", "/Users/me/Documents"
]
}
}
}
```
**Verify:** Auto-reloads on save; tools appear in the Program panel. LM Studio shows a confirmation dialog the first time a model calls a tool.
[Docs](https://lmstudio.ai/docs/app/mcp)
</details>
### Cheat sheet
| Client | File | Top-level key | Restart? |
|---|---|---|---|
| Claude Desktop | `claude_desktop_config.json` | `mcpServers` | Full quit |
| Claude Code | `.mcp.json` / CLI | `mcpServers` | No |
| Cursor | `.cursor/mcp.json` | `mcpServers` | No |
| Windsurf | `~/.codeium/windsurf/mcp_config.json` | `mcpServers` | No |
| VS Code Copilot | `.vscode/mcp.json` | **`servers`** | First time |
| Zed | `~/.config/zed/settings.json` | **`context_servers`** | No |
| Cline | `cline_mcp_settings.json` | `mcpServers` | No |
| Continue.dev | `~/.continue/config.yaml` | `mcpServers` (**list**) | No |
| Gemini CLI | `~/.gemini/settings.json` | `mcpServers` | No |
| Codex CLI | `~/.codex/config.toml` | **`[mcp_servers.*]`** | N/A |
| JetBrains | IDE settings UI | `mcpServers` | No |
| LM Studio | `~/.lmstudio/mcp.json` | `mcpServers` | No |
**The three that differ:** VS Code (`servers`), Zed (`context_servers` + nested `command`), Codex (TOML `mcp_servers`). Everything else takes the Claude Desktop format verbatim.
---
## Tools
| Tool | Cost | Purpose |
|---|---|---|
| **`pdf_classify`** | ~20ms | Type, confidence, page count, exact pages needing OCR. **Call this first.** |
| **`pdf_extract`** | ~150ms | PDF → Markdown. Truncates by default; slice with `pages`. |
| **`pdf_search`** | ~150ms | Locate text, return page-attributed snippets. Cheapest way into a long document. |
| **`pdf_tables`** | ~150ms | Tables only, as Markdown pipe tables. |
Full parameter reference: [docs/TOOLS.md](docs/TOOLS.md).
**The intended flow** on an unfamiliar document:
```
pdf_classify → is it text_based with no warnings?
├─ yes → pdf_search to locate → pdf_extract with `pages`
└─ no → stop; route to OCR
```
---
## Configuration
```
pdf-triage-mcp [options]
-r, --root <dir> Directory the server may read. Repeatable. Default: cwd.
--max-chars <n> Default truncation ceiling. Default: 40000. Max: 200000.
--max-file-bytes <n> Largest PDF to read. Default: 104857600 (100 MB).
--log-level <level> debug | info | warn | error | silent. Default: info.
-h, --help Show usage.
-v, --version Print version.
```
Environment equivalents: `PDF_TRIAGE_ROOTS` (separated by the platform PATH delimiter — `:` on macOS/Linux, `;` on Windows), `PDF_TRIAGE_MAX_CHARS`, `PDF_TRIAGE_MAX_FILE_BYTES`, `PDF_TRIAGE_LOG_LEVEL`. Flags win over environment.
**Roots are a security boundary, not a convenience.** Grant the narrowest directory that works. Paths are resolved through symlinks before checking, so a link inside a root pointing outside it is rejected rather than followed.
---
## Engine fallback
Upstream ships prebuilt native binaries for exactly three targets: `linux-x64-gnu`, `darwin-arm64`, `win32-x64-msvc`. **No musl build, no Linux ARM64 build** ([upstream #216](https://github.com/firecrawl/pdf-inspector/issues/216)) — so it fails to load on Alpine containers, Graviton instances, and most edge runtimes.
This server prefers native and falls back to WASM, which runs anywhere. Capability differences are **surfaced, never faked**:
| | Native | WASM |
|---|---|---|
| Classify / extract | Yes | Yes |
| Per-page extraction | Yes | No — throws, and `pdf_search` reports its matches are unattributed |
| `pages` selection | Yes | No — ignored, and the response says so |
Check which engine you got: `pdf_classify` reports it, and the server logs `engine selected` at startup.
---
## Known limitations
Inherited from upstream. Worth reading before you trust output:
- **RTL scripts are broken.** Arabic and Hebrew return in *visual* order, reversed and unusable, while being reported as `text_based` with high confidence ([#212](https://github.com/firecrawl/pdf-inspector/issues/212)). **This server detects and escalates it** — the one upstream failure mode we actively guard.
- **No xref recovery.** Malformed PDFs that `pypdf` and `pdfium` silently repair will throw ([#228](https://github.com/firecrawl/pdf-inspector/issues/228)).
- **Japanese CIDFontType0 (CFF)** subset fonts can decode to unrelated glyphs ([#208](https://github.com/firecrawl/pdf-inspector/issues/208)).
- **Multi-column reading order** may emit in raster order on some layouts despite columns being detected ([#219](https://github.com/firecrawl/pdf-inspector/issues/219)).
- **Images are not PDFs.** A scanned JPEG has no text layer; the server rejects non-PDF input rather than pretending otherwise.
---
## Development
```bash
npm run typecheck # tsc --noEmit, maximum strictness
npm test # vitest, 108 tests
npm run test:coverage
npm run build
npm run dev # tsx, no build step
```
The TypeScript config runs every strictness flag including `exactOptionalPropertyTypes` and `noUncheckedIndexedAccess`. Upstream responses are validated with Zod at the boundary rather than cast — see [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for why.
Debug a client connection:
```bash
node dist/index.js --root ~/Documents --log-level debug
```
Troubleshooting: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md).
---
## Roadmap
- [ ] `pdf_regions` — bbox-scoped extraction for hybrid model pipelines
- [ ] Positioned-item tool exposing `{page, bbox}` for visual citation UX
- [ ] Integration tests asserting native and WASM produce identical normalized shapes
- [ ] Optional OCR adapter interface, closing the routing loop end to end
## License
MIT