pdf-triage-mcp

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 (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
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:
> [!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:
- Classify before extracting.
pdf_classify costs ~20ms and tells you whether extraction is worth attempting at all.
- Bound every output. A 300-page PDF is easily 500k tokens. Everything truncates by default and tells you how to page through instead.
- 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:
npx -y pdf-triage-mcp --version
Requires Node 20+. Available on npm as pdf-triage-mcp and in the MCP Registry as io.github.vishalmeena2211/pdf-triage-mcp.
From source instead (for development)
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.
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.
1. Claude Desktop
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 |
{
"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
2. Claude Code
CLI โ the easiest route. The -- separator is mandatory; everything after it is the server command.
claude mcp add pdf-triage -- npx -y pdf-triage-mcp --root /Users/me/Documents
claude mcp add --scope user pdf-triage -- npx -y pdf-triage-mcp --root /Users/me/Documents
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:
{
"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
3. Cursor
Config file: .cursor/mcp.json (project) or ~/.cursor/mcp.json (global). Project wins on conflict.
{
"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
4. Windsurf
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.
{
"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
5. VS Code + GitHub Copilot
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).
{
"servers": {
"pdf-triage": {
"type": "stdio",
"command": "npx",
"args": [
"-y", "pdf-triage-mcp",
"--root", "/Users/me/Documents"
]
}
}
}
CLI alternative:
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
6. Zed
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.
{
"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
7. Cline (VS Code extension)
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 |
{
"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
8. Continue.dev
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.
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
9. Google Gemini CLI
CLI:
gemini mcp add pdf-triage npx -y pdf-triage-mcp --root /Users/me/Documents
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):
{
"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
10. OpenAI Codex CLI
Config file: ~/.codex/config.toml (global) or .codex/config.toml (project).
TOML, and the key is mcp_servers โ snake_case, never mcpServers.
[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, #26810). That is a Codex runtime bug, not a config error.
Docs
11. JetBrains AI Assistant / Junie
AI Assistant โ configured through the IDE, no file to edit:
- Settings โ Tools โ AI Assistant โ Model Context Protocol (MCP)
- Add โ transport STDIO
- Paste:
{
"mcpServers": {
"pdf-triage": {
"command": "npx",
"args": [
"-y", "pdf-triage-mcp",
"--root", "/Users/me/Documents"
]
}
}
}
- 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
12. LM Studio
Config file: ~/.lmstudio/mcp.json (macOS/Linux), %USERPROFILE%\.lmstudio\mcp.json (Windows).
Easier via the app: right sidebar โ Program tab โ Install โ Edit mcp.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
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.
| 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.
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) โ 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). 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).
- Japanese CIDFontType0 (CFF) subset fonts can decode to unrelated glyphs (#208).
- Multi-column reading order may emit in raster order on some layouts despite columns being detected (#219).
- Images are not PDFs. A scanned JPEG has no text layer; the server rejects non-PDF input rather than pretending otherwise.
Development
npm run typecheck
npm test
npm run test:coverage
npm run build
npm run dev
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 for why.
Debug a client connection:
node dist/index.js --root ~/Documents --log-level debug
Troubleshooting: docs/TROUBLESHOOTING.md.
Roadmap
License
MIT