
PDF Reader (PDF Agent Stack)
io.github.shuji-bonjiPublished by GitHub account shuji-bonjiv0.22.0Updated Nov 7, 2026
Read PDF internals: text, structure, fonts, signatures and tags, with what was not read declared.
Overview
PDF Reader (PDF Agent Stack)'s local MCP server; no sign-in required; STDIO.
No AI overview yet: these facts come from the registry entry and the server itself.
Installation
In SourceWeft
- Open PDF Reader (PDF Agent Stack) in the dashboard and add it to a workspace.
- Enable the server for the chats that should use its tools.
Desktop only via STDIO. STDIO servers start a local process, so they need the SourceWeft desktop host.
Other MCP clients
Follow the launch instructions in the repository.
README
PDF Reader MCP Server
[npm version] [License: MIT] [Built with Claude Code]
English | 日本語
An MCP (Model Context Protocol) server specialized in deciphering PDF internal structures.
While typical PDF MCP servers are thin wrappers for text extraction, this project focuses on reading and analyzing the internal structure of PDF documents. Pair it with pdf-spec-mcp for specification-aware structural analysis and validation.
PDF family
pdf-reader-mcp inspects signature structure (inspect_signatures); for cryptographic signature verification, trust/revocation evaluation, and PDF/A conformance validation, use pdf-verify-mcp.
Features
18 tools organized into three tiers:
Tier 1: Basic Operations
Tier 2: Structure Inspection
Tier 3: Comparison
validate_tagged and validate_metadata were removed (deprecated since 0.7.0). This server
reports what a PDF contains and does not return pass/fail against a standard. For PDF/UA or
PDF/A conformance, use pdf-verify-mcp
validate_conformance (flavour: "pdfua-1", "pdfa-*", …). For the facts those tools used
to look at, use inspect_tags (structure tree) and get_metadata (metadata fields) here.
read_url and the read-only boundary (#25)
read_url returns text, and only text. This is a decision, now stated rather than implied:
the fetched bytes are discarded after extraction, because saving them would make a reader
tool write to the file system, and every tool of this server is read-only
(readOnlyHint: true — all 18 of them).
To run search_text, inspect_structure, extract_tables, render_page or anything else
against a PDF that lives at a URL, download the file first — with whatever fetch capability
the calling environment has — and pass the local path. Fetching is the caller's
responsibility, deliberately: an agent environment always has a way to download a file, and a
reader that also writes files has stopped being a pure observer.
read_url remains the right tool for the one-shot question: what does the document at this
URL say?
Pages can be rendered when text cannot be read (#23)
summarize reporting hasText: false used to be a dead end: nothing in this server could
read the document any further. render_page closes that — it rasterises pages to PNG or JPEG
and returns them as MCP image content blocks, so a vision model can read a scan, a diagram, a
filled form, or handwriting.
pages is required: rendering is the most expensive operation here, and "all pages" of a
500-page scan should be a decision, not a default. The same 4 MB response budget as
read_images applies, with omissions named.
Rendering runs on PDFium compiled to WebAssembly (@hyzyla/pdfium, an optional
dependency). A WASM binary is the same bytes on every platform, so the published package still
behaves identically wherever npx runs it — the reason native addons are not used here.
Without the dependency installed, render_page reports what to install and every other tool
works normally. PDFium (BSD-3-Clause) is a different engine from the pdf.js this server reads
text with; the tool description says so, because a rendering difference between engines must
not be attributed to the file.
Measured before choosing this: pdf.js +
@napi-rs/canvas(1.0.7 and 0.1.80) segfaults the whole process on pages that draw images — exactly the pages this tool exists for — and renders blank pages whenstandardFontDataUrlis not configured.
Images come back as image files (#22)
read_images used to base64 imgData.data — pdfjs's decoded pixels. An 8×8 RGB image was
192 bytes with no PNG or JPEG signature anywhere in it, so the result could not be opened by
any viewer and could not be read by a vision model, which is the reason to extract an image in
the first place.
Images are now encoded (PNG by default, lossless; format: "jpeg" with quality when smaller
matters) and returned as MCP image content blocks, with the metadata alongside in a text
block. Both encoders are written out here — no native addon, no per-platform binary.
The response is bounded at 4 MB of encoded image data. A 200 dpi A4 scan is ~11.6 MB of pixels on its own, so images past the budget are named with the reason rather than dropped:
read_images returns the image XObjects a page draws. It is not a picture of the page — vector
drawings and text are not covered by it.
Text extractability — three states, not two (#21)
read_text used to answer with text or with nothing, and nothing meant three different things.
ISO 32000-2 §9.10.1 separates them, so this server does too. Every text-returning tool —
read_text, read_url, search_text, extract_structured_text, summarize — reports, per
page:
not_extractable is reported per font, so a page that mixes a readable font with an
unreadable one is a partial loss and says so, rather than passing as complete.
The observation is made from the file, not from pdf.js's output: pdf.js synthesises a
toUnicode map for every font it loads, so asking it whether a font has a /ToUnicode CMap
answers yes for fonts whose dictionary has none.
Installation
npx (recommended)
Claude Desktop
Add to your claude_desktop_config.json:
Use
@latest. The-yflag innpx -y <pkg>only skips the install prompt — it does not check for updates. Without@latest, npx keeps running whichever version it cached the first time, so new releases never reach you. If you suspect you are on a stale version, runrm -rf ~/.npm/_npxand restart your client.
Claude Code
From Source
Usage Examples
Get Page Count
declaredCount is the /Count the page tree declares (ISO 32000-2 §7.7.3.2) and pagesReached
is how many pages walking the tree actually reached. They differ only when the writer got
/Count wrong; both are reported and neither is judged.
Metadata from both sources
get_metadata reads the eight document-level fields (title, author, subject, keywords, creator,
producer, creation and modification dates) from the document information dictionary (§14.3.3)
and the XMP metadata stream (§14.3.2). When both hold a value, PDF 2.0 and later use XMP
(§14.3.3 deprecates the dictionary for this in PDF 2.0) and earlier versions use the dictionary.
The two dates are the exception: they use the dictionary in every version, because §14.3.3 does
not deprecate CreationDate and ModDate, and §14.5 compares PieceInfo's LastModified with
the dictionary's ModDate.
metadataSources names the source of each field, and metadataConflicts lists the fields whose
two values differ (dates compared as instants). Values are returned as written — a dictionary
date looks like D:20260915100000+09'00', an XMP date like 2026-09-15T10:00:00+09:00.
Search Text
Summarize
Compare Structure
Extract Structured Text (Tagged PDF, logical order)
This answers "what is the text of the H1?" — which read_text (flat,
coordinate order) cannot. Order is a depth-first traversal of the structure
tree (ISO 32000-2 §14.8.2.5). /ActualText replaces the glyphs (§14.9.4),
/Alt stays out of the body text (§14.9.3), list labels are reported
separately, a list nested in an item comes back as its own L / LI
elements after the item (not folded into the item's text), and an element
spanning a page break stays ONE element. Use
roles: ["H1", "H2"] to pull an outline. Untagged PDFs return
isTagged: false with a reason — nothing is guessed from coordinates.
include_bbox: where the element is drawn
Rectangles are in PDF default user space (origin bottom-left, pt, normalised) —
exactly what pdf-writer-mcp
add_annotation takes, so "annotate this paragraph" needs no coordinate
conversion in between. /Rotate and a shifted /CropBox do not move them.
basis says how strong the claim is, and the two are different in kind:
An element spanning pages gets ONE RECTANGLE PER PAGE — merging them would put
content on a page it is not on. An element with no rectangle says why in
boxNote rather than returning a zero-sized one.
A declaration is reported as-is, and cross-checked. Files state nonsense:
the cover Figure of both Well-Tagged PDF 1.0 and the Tagged PDF Best Practice
Guide declares /BBox [-32768 -32768 32767 32767] — int16 sentinels where a
rectangle should be — and PDF32000_2008 has 131 of its 545 declarations reaching
past the page edge. Since this output is meant to go straight into
add_annotation, a declaration is checked against the page box (§7.7.3.3) and
against the element's own text; either contradiction is reported in boxNote,
with the rectangle still returned unaltered.
Measured against independent ground truth: on Well-Tagged PDF (WTPDF) 1.0, the
166 Link structure elements were compared with the 173 Link annotation
/Rect values the producer placed for the same links — median IoU 0.972,
none disjoint.
Extract Tables (Tagged PDF)
A table that continues across a page break is reported as ONE table —
pages is an array (e.g. ## Table 3 — Pages 5–7), and a table touching
the requested pages range is returned whole. Cell text honours
/ActualText replacements (as do read_text and search_text since #18).
Untagged PDFs return an empty result with a
note recommending the column-aware fallback below.
Read Untagged Multi-Column PDF
Use split_columns: 2 | 3 for untagged multi-column PDFs. For Tagged
PDFs with proper <Table> markup, extract_tables (above) is preferred.
Compact Whitespace (Japanese Forms)
compact_whitespace is orthogonal to split_columns — both can be combined.
Tech Stack
- TypeScript + MCP TypeScript SDK
- pdfjs-dist (Mozilla) — text/image extraction, tag tree, annotations
- normativepdf + @normativepdf/recover — COS object access, read from files whose cross-reference table does not follow ISO 32000-2 §7.5
- Vitest — unit + E2E testing (483 tests)
- Biome — linting + formatting
- Zod — input validation
Testing
Architecture
Error Contract (houki-hub family)
Since v0.6.0, this MCP returns structured errors that follow the houki-hub family error contract, sharing a unified code vocabulary across the family. Combined with houki-egov-mcp / houki-nta-mcp, an LLM or Skill layer can interpret errors with consistent logic.
docs/ERROR-CODES.md— error code vocabulary (houki-research-skill)docs/ERROR-HANDLING.md— handling policy / next_actions templates
Implementation is independent — no dependency on houki-abbreviations or other family packages. The reference implementation is houki-egov-mcp/src/errors.ts; pdf-reader-mcp's local definition is in src/errors.ts.
On error, every tool returns isError: true and the JSON-stringified LawServiceError in content[0].text:
Every error carries error (an English sentence), code and retryable; hint, next_actions and detail are optional, and nothing else appears at the top level (PDF family contract, pdf-agent-stack docs/specs/06 §2.3, since v0.19.0). retryable is fixed per code: it says whether changing the arguments, flags or environment could make the same reading of the same document succeed. The values of next_actions[].action are stable — a Skill may branch on them.
Codes used by pdf-reader-mcp
Migration note (v0.5.x → v0.6.0)
旧 v0.5.x までは content[0].text に Error: ...\n\nSuggestion: ... という人間可読文字列を入れていました。v0.6.0 では同じ場所に JSON 文字列 が入ります。LLM 側でテキスト解釈に依存していた場合は、JSON.parse(content[0].text) での解釈に切り替えてください。isError: true フラグで構造化エラーかどうかを判定できます。
Pairing with pdf-spec-mcp
pdf-spec-mcp provides PDF specification knowledge (ISO 32000-2, etc.). With both servers enabled, an LLM can perform specification-aware workflows:
summarize— get a PDF overviewinspect_tags— examine the tag structure- pdf-spec-mcp
get_requirements— fetch PDF/UA requirements - pdf-verify-mcp
validate_conformance— check conformance (flavour: "pdfua-1") compare_structure— diff before/after fixes
License
MIT
Source: README.md at commit ebffee0
Tools
0Version history
9- v0.22.0LatestNov 7, 2026
- v0.21.0Nov 6, 2026
- v0.20.1Nov 5, 2026
- v0.19.0Nov 5, 2026
- v0.18.1Nov 3, 2026
- v0.17.0Oct 15, 2026
- v0.16.1Oct 15, 2026
- v0.16.0Oct 12, 2026
- v0.15.5Sep 19, 2026
Score
50% of the applicable checks met
How complete this listing is, computed from the registry entry, the endpoint and public sources. Not a rating of the server and not a statement that it is safe or working today: reachability is a dated check, and no installs or reviews go into the score.
- Reached in the last 30 days25 pts
Keep the endpoint answering the MCP handshake; SourceWeft probes it every 30 days.
- Auth declared10 pts
Declare the credential headers in the registry entry, or keep the endpoint answering the probe so its auth can be classified.
- Tools listed15 pts
Answer tools/list without a credential, or ship a package that starts in a sandbox without one.
- DescriptionRequired15 pts
- Logo10 pts
- Documentation15 pts
- License10 pts