# PDF Triage [Health: Active]

**Category:** 💻 Developer Tools  
**Repository:** https://github.com/vishalmeena2211/pdf-triage-mcp  
**GitHub Stars:** 0  
**Views:** 0  
**Installs:** 0  
**Upvotes:** 0  
**Directory Page:** https://allmcps.com/mcp/pdf-triage

## Description
Read local PDFs without uploading. Classifies first, flags untrustworthy text, bounds output.

## Tools
Capabilities this server exposes over MCP:

- **pdf_classify** — Type, confidence, page count, exact pages needing OCR. **Call this first.
- **pdf_extract** — PDF → Markdown. Truncates by default; slice with `pages`.
- **pdf_search** — Locate text, return page-attributed snippets. Cheapest way into a long document.
- **pdf_tables** — Tables only, as Markdown pipe tables.

## Claude Desktop Quick Installation
Install path detected from listing signals. Uses `npx` (confidence: high):

```json
"mcpServers": {
  "pdf-triage": {
    "command": "npx",
    "args": ["-y","pdf-triage-mcp"]
  }
}
```

## Documentation & README

# pdf-triage-mcp

[![npm](https://img.shields.io/npm/v/pdf-triage-mcp.svg)](https://www.npmjs.com/package/pdf-triage-mcp)
[![CI](https://github.com/vishalmeena2211/pdf-triage-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/vishalmeena2211/pdf-triage-mcp/actions/workflows/ci.yml)
[![MCP Registry](https://img.shields.io/badge/MCP%20Registry-io.github.vishalmeena2211%2Fpdf--triage--mcp-blue)](https://registry.modelcontextprotocol.io)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](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](https://github.com/vishalmeena2211/pdf-triage-mcp/blob/HEAD/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](https://github.com/vishalmeena2211/pdf-triage-mcp/blob/HEAD/docs/ARCHITECTURE.md) for why.

Debug a client connection:

```bash
node dist/index.js --root ~/Documents --log-level debug
```

Troubleshooting: [docs/TROUBLESHOOTING.md](https://github.com/vishalmeena2211/pdf-triage-mcp/blob/HEAD/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

