MCP Server
pdfvision mcp serves the same extraction engine over the Model Context Protocol on stdio. It exists for hosts that cannot run a shell — Claude Desktop, Cursor, Cline, Zed, n8n, and similar environments where the model can only call tools.
If your agent has a shell (Claude Code, Codex, or another CLI-capable environment), prefer the CLI plus the Agent Skills. The skill loads on demand and costs nothing until it is used, while MCP tool schemas sit in the host's context for the whole session.
Setup
The server is a subcommand of the main binary, not a separate package:
{
"mcpServers": {
"pdfvision": { "command": "npx", "args": ["-y", "pdfvision", "mcp"] }
}
}pdfvision mcp takes no arguments. It speaks JSON-RPC on stdout, so anything the process would otherwise log goes to stderr.
The Three Tools
| Tool | Returns | Parameters |
|---|---|---|
read_pdf | Text as Markdown | source, pages, ocr, attachment, password |
search_pdf | One row per distinct place a hit lands, each with a short ref | source, query, pages, regex, password |
render_pdf | Page or region PNGs as image blocks | source, pages, ref, region, password |
source takes a local path or an http(s) URL — there is no separate remote parameter.
The surface is deliberately smaller than the CLI. There is no format, include, scale, or cache parameter: anything pdfvision can decide from the document itself, the server decides. read_pdf always runs layout, form fields, links, and annotations, and simply omits sections that found nothing. This keeps the permanently-resident tool schemas small and leaves the model nothing to misconfigure.
How a Session Flows
An unscoped read_pdf on a document over 20 pages returns a document map instead of the body: page count, outline, per-page native-text quality and warning codes collapsed into ranges, plus the specific calls to make next. That is the normal first move on an unknown document.
From there:
read_pdf(pages: "12-18")reads a range.search_pdf(query: "…")locates a term. Same-source occurrences whose crops resolve to the same region — typically repeats within one line or table row — collapse into a single row marked×N, while the headline count still reports every occurrence. Each row carries a shortreflikep47m1— pass it straight torender_pdf(ref: "p47m1")to see the match in place, instead of transcribing coordinates. A source's refs are whatever its lastsearch_pdf, or its last full-pagerender_pdfthat listed visual regions, filed; either of those replaces the whole previous set — a search that found nothing replaces it with an empty one. Arender_pdfthat mints no refs leaves the set alone — a region render, everyrefcall included, or a full page whose response lists no regions — so the hits of one search can be rendered one after another.refcannot be combined withpagesorregion: the ref already names both, and the call is rejected rather than quietly answering for the ref's page.read_pdf(pages: "31", ocr: "jpn+eng")re-reads scanned pages with OCR when quality reporting says the native text is unusable.read_pdf(attachment: "invoice.xml")— or a 1-based index — returns an embedded file instead of the pages. In e-invoices and regulatory filings (Factur-X, ZUGFeRD, XBRL) the attachment is the authoritative data and the pages are only its rendering. Text attachments come back inline, images as image blocks; opaque binaries are refused with a pointer to the CLI's--attachments --attachment-output.
Renders are fitted to 1568 px on the longest edge, past which vision models downsample anyway. If a render is too small to read, the fix is a smaller region, not a bigger raster.
Budgets and Honesty
Responses are budgeted: 30,000 characters per body, 12,000 per page, 100 match places, 4 rendered pages, 5 OCR pages, and 6 MB of images per call. Every truncation names what to do next, so a clipped result is recoverable rather than silently incomplete — a page call narrower than the one that produced it, even when the requested range is so wide that its per-page Overview table fills the budget on its own, or search_pdf guidance in the one case where no narrower page call exists because a single page cannot fit whole.
The 20-page threshold decides map versus body, not whether the body fits: a document under it is read whole and then truncated like any other if it exceeds the character budget. What a truncation notice counts as omitted is page bodies — the Overview row for each of those pages is still in the response, unless the same notice also says Overview clipped after page N, or Overview clipped before any page row when no complete row survived the cut. That is the one case where the rows go too: a range wide enough that its Overview alone fills the budget has the table cut as well, always on a row boundary. after page N names the last page whose row is complete, with no per-page detail past it; before any page row means none is left at all.
The same honesty applies to search: core warnings ride the response, so a regex query that exceeds the per-page time budget reports itself instead of masquerading as "0 matches", and a search over pages with no usable native text says a miss there is not evidence of absence. A dynamic XFA (LiveCycle) form gets the same treatment one step further: when the pages that were searched are only the "Please wait..." viewer placeholder, every response says so for every page the search selected — the zero-hit one included, which is the response most likely to be mistaken for absence — and points at Adobe Acrobat/Reader rather than at a render, because the render is the placeholder too. When too little was extracted to tell, the note says that instead and asks for a render or OCR, rather than guessing in either direction. A hybrid AcroForm+XFA form whose pages carry their own text, images, or drawing — an IRS return, say — extracts normally and is not flagged that way. And a form whose only static substance is its field layer sits in between: the note says field hits stand while the page text around them is not the document.
search_pdf considers page-level extraction warnings from every selected page, including no-hit pages and all-zero searches. It lists at most five diagnostic pages and reports how many additional pages were omitted, prioritizing error-bearing pages, then pages carrying hits within the same severity. These codes mean that a hit or miss may need visual checking; they do not make every part of the page invalid. The response gives one concrete render_pdf(pages: "N") call for a listed page. Pages covered by the separate XFA note stay out of this generic render guidance because a confirmed placeholder renders as the placeholder too.
Every successful result leads with an untrusted-data banner (error results do not, and can still quote the document). MCP hosts have no equivalent of the Agent Skill's guidance, so the trust boundary travels with the payload. Treat extracted content as data, not instructions — see Security and Privacy.
Remote Input Is Guarded
Unlike the CLI's --remote, the MCP server refuses URLs that resolve to private, loopback, link-local, CGNAT, NAT64, or IPv4-mapped addresses, and re-validates every redirect hop. The model chooses the URL here, which would otherwise make the server an SSRF pivot into whatever network it runs on.
For an intranet document store, set PDFVISION_MCP_ALLOW_PRIVATE_NETWORK=1. Known limitation: the validated address is not pinned for the fetch, so a DNS answer that changes between validation and connection is not covered.
Errors Name the Next Call
Tool failures come back as in-band results with a recovery instruction, not protocol errors. An out-of-range page selector, an OCR request over the page budget, an unknown ref, or a malformed region all state what to do instead — read the message; it names the next call.