{
  "markdown": "# pdf-triage-mcp\n\n[![npm](https://img.shields.io/npm/v/pdf-triage-mcp.svg)](https://www.npmjs.com/package/pdf-triage-mcp)\n[![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)\n[![MCP Registry](https://img.shields.io/badge/MCP%20Registry-io.github.vishalmeena2211%2Fpdf--triage--mcp-blue)](https://registry.modelcontextprotocol.io)\n[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)\n\nAn MCP server that lets any AI tool read local PDFs — **without uploading them, without an OCR bill, and without silently handing back garbage.**\n\nBuilt on [`@firecrawl/pdf-inspector`](https://github.com/firecrawl/pdf-inspector) (Rust, no ML models, no external services).\n\n```\n┌─ pdf_classify ──→  text_based · 0.98 · 12 pages · 0 need OCR   ~20ms\n├─ pdf_search  ──→  \"invoice total\" found on p4, p9              ~150ms\n├─ pdf_extract ──→  clean Markdown, truncated to your budget     ~150ms\n└─ pdf_tables  ──→  just the pipe tables, no prose               ~150ms\n```\n\n---\n\n## Table of contents\n\n- [Why this exists](#why-this-exists)\n- [Install](#install)\n- [Connect it to your AI tool](#connect-it-to-your-ai-tool) — 12 clients\n- [Tools](#tools)\n- [Configuration](#configuration)\n- [Engine fallback](#engine-fallback)\n- [Known limitations](#known-limitations)\n- [Development](#development)\n\n---\n\n## Why this exists\n\nMost 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.\n\n`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.\n\n**This server acts on them.** Every response carries the trust signals, above the content, where the model reads them first:\n\n```markdown\n> [!WARNING] ENCODING ISSUES DETECTED. The text layer decoded to suspicious\n> output — typically a garbled CID font or a substitution-cipher encoding\n> where letter frequencies match natural language but the letters themselves\n> are wrong. Treat all extracted text here as unreliable and prefer OCR.\n\n---\n\n# Quarterly Report\n...\n```\n\nThree design rules follow:\n\n1. **Classify before extracting.** `pdf_classify` costs ~20ms and tells you whether extraction is worth attempting at all.\n2. **Bound every output.** A 300-page PDF is easily 500k tokens. Everything truncates by default and tells you how to page through instead.\n3. **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.\n\n---\n\n## Install\n\n**Nothing to install.** Every config below runs the published package straight from npm:\n\n```\nnpx -y pdf-triage-mcp --root /path/to/your/documents\n```\n\nYour MCP client runs that for you — you only need to paste the config. Confirm it works first:\n\n```bash\nnpx -y pdf-triage-mcp --version\n```\n\n**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`.\n\n<details>\n<summary>From source instead (for development)</summary>\n\n```bash\ngit clone https://github.com/vishalmeena2211/pdf-triage-mcp.git\ncd pdf-triage-mcp\nnpm install\nnpm run build\nnode dist/index.js --root ~/Documents\n```\n\nThen substitute `\"command\": \"node\", \"args\": [\"/absolute/path/to/dist/index.js\", ...]` for the `npx` invocation in any config below.\n\n</details>\n\n---\n\n## Connect it to your AI tool\n\nEvery config below is complete as written except for one value:\n\n- **`/Users/me/Documents`** — replace with the directory the server may read. This is the only thing you must change.\n\nUse an absolute path; `~` is not expanded by most clients. Repeat `--root` for multiple directories.\n\n> **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).\n\n> **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.\n\n<details open>\n<summary><b>1. Claude Desktop</b></summary>\n\n**Config file**\n\n| OS | Path |\n|---|---|\n| macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` |\n| Windows | `%APPDATA%\\Claude\\claude_desktop_config.json` |\n| Linux | `~/.config/Claude/claude_desktop_config.json` |\n\n```json\n{\n  \"mcpServers\": {\n    \"pdf-triage\": {\n      \"command\": \"npx\",\n      \"args\": [\n        \"-y\", \"pdf-triage-mcp\",\n        \"--root\", \"/Users/me/Documents\"\n      ]\n    }\n  }\n}\n```\n\n**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.\n\nLogs: `~/Library/Logs/Claude/mcp*.log` (macOS), `%APPDATA%\\Claude\\logs\\mcp*.log` (Windows).\n\n[Docs](https://modelcontextprotocol.io/quickstart/user)\n\n</details>\n\n<details>\n<summary><b>2. Claude Code</b></summary>\n\n**CLI — the easiest route.** The `--` separator is mandatory; everything after it is the server command.\n\n```bash\n# Just you, this project (default)\nclaude mcp add pdf-triage -- npx -y pdf-triage-mcp --root /Users/me/Documents\n\n# Just you, every project\nclaude mcp add --scope user pdf-triage -- npx -y pdf-triage-mcp --root /Users/me/Documents\n\n# Shared with your team, writes .mcp.json to the repo\nclaude mcp add --scope project pdf-triage -- npx -y pdf-triage-mcp --root /Users/me/Documents\n```\n\n**Or edit `.mcp.json` at the project root directly:**\n\n```json\n{\n  \"mcpServers\": {\n    \"pdf-triage\": {\n      \"type\": \"stdio\",\n      \"command\": \"npx\",\n      \"args\": [\n        \"-y\", \"pdf-triage-mcp\",\n        \"--root\", \"/Users/me/Documents\"\n      ]\n    }\n  }\n}\n```\n\n| Scope | Stored in | Shared |\n|---|---|---|\n| `local` (default) | `~/.claude.json`, under this project | No |\n| `user` | `~/.claude.json`, top level | No |\n| `project` | `.mcp.json` in repo root | Yes, via git |\n\n**Verify:** `claude mcp list` → look for `✔ Connected`. Project-scoped servers need approval on first use — run `/mcp` inside a session.\n\n[Docs](https://code.claude.com/docs/en/mcp)\n\n</details>\n\n<details>\n<summary><b>3. Cursor</b></summary>\n\n**Config file:** `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global). Project wins on conflict.\n\n```json\n{\n  \"mcpServers\": {\n    \"pdf-triage\": {\n      \"command\": \"npx\",\n      \"args\": [\n        \"-y\", \"pdf-triage-mcp\",\n        \"--root\", \"/Users/me/Documents\"\n      ]\n    }\n  }\n}\n```\n\n**Verify:** Cursor hot-reloads — no restart. Open **Cursor Settings → Tools & MCP** and look for a green dot next to `pdf-triage`.\n\n[Docs](https://docs.cursor.com/context/model-context-protocol)\n\n</details>\n\n<details>\n<summary><b>4. Windsurf</b></summary>\n\n**Config file:** `~/.codeium/windsurf/mcp_config.json` (macOS/Linux), `%USERPROFILE%\\.codeium\\windsurf\\mcp_config.json` (Windows).\n\nNot created on first launch — create it yourself if missing.\n\n```json\n{\n  \"mcpServers\": {\n    \"pdf-triage\": {\n      \"command\": \"npx\",\n      \"args\": [\n        \"-y\", \"pdf-triage-mcp\",\n        \"--root\", \"/Users/me/Documents\"\n      ]\n    }\n  }\n}\n```\n\n**Verify:** Windsurf watches the file and hot-reloads on save. Tools appear in Cascade on the next chat session.\n\n[Docs](https://docs.windsurf.com/windsurf/mcp)\n\n</details>\n\n<details>\n<summary><b>5. VS Code + GitHub Copilot</b></summary>\n\n> **The key is `servers`, not `mcpServers`.** This is the most common mistake when copying a config from Claude Desktop.\n\n**Config file:** `.vscode/mcp.json` (workspace), or Command Palette → **MCP: Open User Configuration** (global).\n\n```json\n{\n  \"servers\": {\n    \"pdf-triage\": {\n      \"type\": \"stdio\",\n      \"command\": \"npx\",\n      \"args\": [\n        \"-y\", \"pdf-triage-mcp\",\n        \"--root\", \"/Users/me/Documents\"\n      ]\n    }\n  }\n}\n```\n\n**CLI alternative:**\n\n```bash\ncode --add-mcp '{\"name\":\"pdf-triage\",\"command\":\"npx\",\"args\":[\"-y\",\"pdf-triage-mcp\",\"--root\",\"/Users/me/Documents\"]}'\n```\n\n**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.\n\n[Docs](https://code.visualstudio.com/docs/agent-customization/mcp-servers)\n\n</details>\n\n<details>\n<summary><b>6. Zed</b></summary>\n\n> **The key is `context_servers`, not `mcpServers`,** and `command` is a nested object rather than a string.\n\n**Config file:** `~/.config/zed/settings.json` (macOS/Linux), `%APPDATA%\\Zed\\settings.json` (Windows). Command Palette → **zed: open settings**.\n\n```json\n{\n  \"context_servers\": {\n    \"pdf-triage\": {\n      \"source\": \"custom\",\n      \"command\": {\n        \"path\": \"npx\",\n        \"args\": [\n          \"-y\", \"pdf-triage-mcp\",\n          \"--root\", \"/Users/me/Documents\"\n        ],\n        \"env\": {}\n      }\n    }\n  }\n}\n```\n\nIf 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.\n\n**Verify:** Agent Panel (`Cmd+Shift+A`) → gear icon → **MCP Servers**. Green dot means connected.\n\n[Docs](https://zed.dev/docs/ai/mcp)\n\n</details>\n\n<details>\n<summary><b>7. Cline (VS Code extension)</b></summary>\n\n**Config file** — separate from VS Code's own:\n\n| OS | Path |\n|---|---|\n| macOS | `~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json` |\n| Windows | `%APPDATA%\\Code\\User\\globalStorage\\saoudrizwan.claude-dev\\settings\\cline_mcp_settings.json` |\n| Linux | `~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json` |\n\n```json\n{\n  \"mcpServers\": {\n    \"pdf-triage\": {\n      \"command\": \"npx\",\n      \"args\": [\n        \"-y\", \"pdf-triage-mcp\",\n        \"--root\", \"/Users/me/Documents\"\n      ],\n      \"disabled\": false,\n      \"autoApprove\": [\"pdf_classify\", \"pdf_search\"]\n    }\n  }\n}\n```\n\n`autoApprove` runs the listed read-only tools without a confirmation prompt.\n\n**Easier route:** Cline panel → MCP servers icon → **Edit MCP Settings** opens this file directly.\n\n**Verify:** Panel refreshes automatically; green dot next to the server.\n\n[Docs](https://docs.cline.bot/mcp/mcp-overview)\n\n</details>\n\n<details>\n<summary><b>8. Continue.dev</b></summary>\n\n**Config file:** `~/.continue/config.yaml` (global) or `.continue/config.yaml` (project). YAML is current; `config.json` is deprecated.\n\n> `mcpServers` here is a **list**, not an object — and YAML needs spaces, never tabs.\n\n```yaml\nmcpServers:\n  - name: pdf-triage\n    command: npx\n    args:\n      - -y\n      - pdf-triage-mcp\n      - --root\n      - /Users/me/Documents\n```\n\n**Verify:** Reloads automatically on save. Switch Continue to **Agent mode** — MCP tools are unavailable in other modes.\n\n[Docs](https://docs.continue.dev/customize/deep-dives/mcp)\n\n</details>\n\n<details>\n<summary><b>9. Google Gemini CLI</b></summary>\n\n**CLI:**\n\n```bash\ngemini mcp add pdf-triage npx -y pdf-triage-mcp --root /Users/me/Documents\n\n# global instead of project-scoped\ngemini mcp add --scope user pdf-triage npx -y pdf-triage-mcp --root /Users/me/Documents\n```\n\n**Or edit `~/.gemini/settings.json` (global) / `.gemini/settings.json` (project):**\n\n```json\n{\n  \"mcpServers\": {\n    \"pdf-triage\": {\n      \"command\": \"npx\",\n      \"args\": [\n        \"-y\", \"pdf-triage-mcp\",\n        \"--root\", \"/Users/me/Documents\"\n      ],\n      \"timeout\": 30000,\n      \"trust\": false\n    }\n  }\n}\n```\n\n**Verify:** Run `/mcp` inside a `gemini` session — servers show `CONNECTED` with their tool list. Or `gemini mcp list` from the shell.\n\n[Docs](https://google-gemini.github.io/gemini-cli/docs/tools/mcp-server.html)\n\n</details>\n\n<details>\n<summary><b>10. OpenAI Codex CLI</b></summary>\n\n**Config file:** `~/.codex/config.toml` (global) or `.codex/config.toml` (project).\n\n> TOML, and the key is `mcp_servers` — snake_case, never `mcpServers`.\n\n```toml\n[mcp_servers.pdf-triage]\ncommand = \"npx\"\nargs = [\n  \"-y\", \"pdf-triage-mcp\",\n  \"--root\", \"/Users/me/Documents\"\n]\nstartup_timeout_sec = 20\ntool_timeout_sec = 60\n```\n\n**Verify:** `codex doctor --json` validates the config syntax. Note that it validates syntax only — it does not confirm the server actually spawned.\n\n> **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.\n\n[Docs](https://learn.chatgpt.com/docs/extend/mcp?surface=cli)\n\n</details>\n\n<details>\n<summary><b>11. JetBrains AI Assistant / Junie</b></summary>\n\n**AI Assistant** — configured through the IDE, no file to edit:\n\n1. **Settings → Tools → AI Assistant → Model Context Protocol (MCP)**\n2. **Add** → transport **STDIO**\n3. Paste:\n\n```json\n{\n  \"mcpServers\": {\n    \"pdf-triage\": {\n      \"command\": \"npx\",\n      \"args\": [\n        \"-y\", \"pdf-triage-mcp\",\n        \"--root\", \"/Users/me/Documents\"\n      ]\n    }\n  }\n}\n```\n\n4. **OK → Apply**\n\n**Junie** uses a file instead — `~/.junie/mcp/mcp.json` (global) or `.junie/mcp/mcp.json` (project), same JSON shape.\n\n**Verify:** Check the **Status** column in the MCP settings panel; click it to list the server's tools.\n\n[Docs](https://www.jetbrains.com/help/ai-assistant/mcp.html)\n\n</details>\n\n<details>\n<summary><b>12. LM Studio</b></summary>\n\n**Config file:** `~/.lmstudio/mcp.json` (macOS/Linux), `%USERPROFILE%\\.lmstudio\\mcp.json` (Windows).\n\nEasier via the app: right sidebar → **Program** tab → **Install → Edit mcp.json**.\n\n```json\n{\n  \"mcpServers\": {\n    \"pdf-triage\": {\n      \"command\": \"npx\",\n      \"args\": [\n        \"-y\", \"pdf-triage-mcp\",\n        \"--root\", \"/Users/me/Documents\"\n      ]\n    }\n  }\n}\n```\n\n**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.\n\n[Docs](https://lmstudio.ai/docs/app/mcp)\n\n</details>\n\n### Cheat sheet\n\n| Client | File | Top-level key | Restart? |\n|---|---|---|---|\n| Claude Desktop | `claude_desktop_config.json` | `mcpServers` | Full quit |\n| Claude Code | `.mcp.json` / CLI | `mcpServers` | No |\n| Cursor | `.cursor/mcp.json` | `mcpServers` | No |\n| Windsurf | `~/.codeium/windsurf/mcp_config.json` | `mcpServers` | No |\n| VS Code Copilot | `.vscode/mcp.json` | **`servers`** | First time |\n| Zed | `~/.config/zed/settings.json` | **`context_servers`** | No |\n| Cline | `cline_mcp_settings.json` | `mcpServers` | No |\n| Continue.dev | `~/.continue/config.yaml` | `mcpServers` (**list**) | No |\n| Gemini CLI | `~/.gemini/settings.json` | `mcpServers` | No |\n| Codex CLI | `~/.codex/config.toml` | **`[mcp_servers.*]`** | N/A |\n| JetBrains | IDE settings UI | `mcpServers` | No |\n| LM Studio | `~/.lmstudio/mcp.json` | `mcpServers` | No |\n\n**The three that differ:** VS Code (`servers`), Zed (`context_servers` + nested `command`), Codex (TOML `mcp_servers`). Everything else takes the Claude Desktop format verbatim.\n\n---\n\n## Tools\n\n| Tool | Cost | Purpose |\n|---|---|---|\n| **`pdf_classify`** | ~20ms | Type, confidence, page count, exact pages needing OCR. **Call this first.** |\n| **`pdf_extract`** | ~150ms | PDF → Markdown. Truncates by default; slice with `pages`. |\n| **`pdf_search`** | ~150ms | Locate text, return page-attributed snippets. Cheapest way into a long document. |\n| **`pdf_tables`** | ~150ms | Tables only, as Markdown pipe tables. |\n\nFull parameter reference: [docs/TOOLS.md](docs/TOOLS.md).\n\n**The intended flow** on an unfamiliar document:\n\n```\npdf_classify → is it text_based with no warnings?\n  ├─ yes → pdf_search to locate → pdf_extract with `pages`\n  └─ no  → stop; route to OCR\n```\n\n---\n\n## Configuration\n\n```\npdf-triage-mcp [options]\n\n  -r, --root <dir>          Directory the server may read. Repeatable. Default: cwd.\n      --max-chars <n>       Default truncation ceiling. Default: 40000. Max: 200000.\n      --max-file-bytes <n>  Largest PDF to read. Default: 104857600 (100 MB).\n      --log-level <level>   debug | info | warn | error | silent. Default: info.\n  -h, --help                Show usage.\n  -v, --version             Print version.\n```\n\nEnvironment 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.\n\n**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.\n\n---\n\n## Engine fallback\n\nUpstream 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.\n\nThis server prefers native and falls back to WASM, which runs anywhere. Capability differences are **surfaced, never faked**:\n\n| | Native | WASM |\n|---|---|---|\n| Classify / extract | Yes | Yes |\n| Per-page extraction | Yes | No — throws, and `pdf_search` reports its matches are unattributed |\n| `pages` selection | Yes | No — ignored, and the response says so |\n\nCheck which engine you got: `pdf_classify` reports it, and the server logs `engine selected` at startup.\n\n---\n\n## Known limitations\n\nInherited from upstream. Worth reading before you trust output:\n\n- **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.\n- **No xref recovery.** Malformed PDFs that `pypdf` and `pdfium` silently repair will throw ([#228](https://github.com/firecrawl/pdf-inspector/issues/228)).\n- **Japanese CIDFontType0 (CFF)** subset fonts can decode to unrelated glyphs ([#208](https://github.com/firecrawl/pdf-inspector/issues/208)).\n- **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)).\n- **Images are not PDFs.** A scanned JPEG has no text layer; the server rejects non-PDF input rather than pretending otherwise.\n\n---\n\n## Development\n\n```bash\nnpm run typecheck    # tsc --noEmit, maximum strictness\nnpm test             # vitest, 108 tests\nnpm run test:coverage\nnpm run build\nnpm run dev          # tsx, no build step\n```\n\nThe 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.\n\nDebug a client connection:\n\n```bash\nnode dist/index.js --root ~/Documents --log-level debug\n```\n\nTroubleshooting: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md).\n\n---\n\n## Roadmap\n\n- [ ] `pdf_regions` — bbox-scoped extraction for hybrid model pipelines\n- [ ] Positioned-item tool exposing `{page, bbox}` for visual citation UX\n- [ ] Integration tests asserting native and WASM produce identical normalized shapes\n- [ ] Optional OCR adapter interface, closing the routing loop end to end\n\n## License\n\nMIT\n",
  "bytes": 20102,
  "sha": "3f789d9262006fbc539d57da4c746477e4d687940424b93658d90a7ad3de25c6",
  "repo_slug": "vishalmeena2211/pdf-triage-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_vishalmeena2211_pdf_triage_mcp_bccc0cc2/readme"
}