pdf-verify-mcp
[CI]
[npm version]
[License: MIT]
日本語
MCP server for PDF authenticity and conformance verification — cryptographic digital signature verification, tamper detection, PAdES baseline level detection, and PDF/A (ISO 19005) / PDF/UA (ISO 14289) validation.
Part of the PDF family alongside pdf-reader-mcp (structure analysis) and pdf-spec-mcp (specification reference). Where pdf-reader-mcp tells you what is in a PDF, pdf-verify-mcp tells you whether it is genuine.
Tools
Verdicts
DocMDP certification permissions (v0.14)
A certification signature's P value states which kinds of change are allowed
(ISO 32000-2 Table 257). verify_integrity classifies the changes made after signing at the
object level and compares them against what that P permits.
Objects that a permitted change necessarily drags along — the page whose /Annots grew, the
catalog, /Info, the XMP stream — are classified as housekeeping and are not counted as
violations. Counting them would make every certified document violate, since a lawful P=3
annotation addition moves all four.
violationAssessment is three-valued
⚠️ indeterminate is not a pass. This server disproves, and "could not be disproved" is a
different statement from "is fine" — the same discipline as validate_clauses returning
needs_external_fact rather than defaulting a check into a pass.
violatedByLaterChanges (boolean) is kept for compatibility and collapses indeterminate to
false. Read violationAssessment wherever "could not tell" must not be mistaken for
"fine". evaluate_policy raises indeterminate to human_review_required as well.
A bare stream with no /Type is treated as not determined rather than as content: the same
bytes could be a form field's appearance stream (which P=2 permits) or a page's content stream
(which no P permits).
Response size (v0.29.0)
A JSON response is never cut by length. Lists are capped instead, and every cut is reported next to the list as { returned, total }: signatures / levels at 32 (verify_signatures does not verify the fields beyond the cap; evaluate_policy verifies all and caps only facts.signatures), revisions at 32, violations / results at 200 (counts such as compliant, failedRules, violations, notDecided cover all entries). A markdown response is cut at 50,000 characters with a visible marker.
Content inventory in evaluate_policy (v0.30.0)
evaluate_policy also counts what the document carries that acts, or that is unfinished, and
reports it under facts.content — the counts of ISO 32000-2 §12.8.7 Table 264 (legal content
attestations): JavaScriptActions, LaunchActions, URIActions, GoToRemoteActions,
HideAnnotationActions, MovieActions, SoundActions, plus SubmitFormActions, the URI hosts,
whether /OpenAction or a document /AA is present, the annotation count, and the number of
/Subtype /Redact annotations still in the file. Nested (direct) action dictionaries are walked,
so an inline /A on a link counts. Two rules read it:
POL-CAUTION-REDACT-PENDING — a redaction annotation is still present (ISO 32000-2
§12.5.6.23). An applied redaction leaves no annotation behind, so the content it marks has
not been removed from the file. use_with_caution.
POL-CAUTION-ACTIVE-CONTENT — ECMAScript, Launch, remote go-to, or SubmitForm/ImportData
actions are present; the reason says whether /OpenAction / /AA makes some of it run on
open. use_with_caution. URI actions never change the verdict; their hosts go to an advisory.
The inventory is an observation, not a verdict: null (could not be taken) is reported as
"not counted", never as 0, and unreadable objects make the counts lower bounds. Keys of
Table 264 that are not counted: AlternateImages, ExternalStreams, TrueTypeFonts,
ExternalRefXobjects, ExternalOPIdicts, NonEmbeddedFonts, DevDepGS_*.
Trust & revocation (v0.2)
Pass trust_anchors (PEM/DER file paths) or set the PDF_VERIFY_TRUST_ANCHORS env var (a directory of certificates) to evaluate the signer's chain: results are trusted / untrusted / not_evaluated with the certificate path. The validation time is a verified signature timestamp, else the earliest document timestamp covering the signature, else the current time (v0.27.0); the CMS signingTime attribute is written by the signer and is not used. The chosen time is reported as validationTime.
check_revocation controls revocation checking: embedded (default — OCSP/CRL data in the DSS, CMS SignedData.crls, or the CMS signed attribute adbe-revocationInfoArchival; reported as revocation.origin), online (additionally query OCSP responders and CRL distribution points over HTTP), or none (revocation.status: not_checked). CRLs and OCSP responses whose signatures cannot be verified, whose nextUpdate is before the validation time, or whose thisUpdate is more than revocation_freshness seconds (default 86400) before it give unknown (v0.28.0). Pass trusted_ocsp_responders to trust an OCSP responder issued by another CA. Each intermediate CA's result is reported in trust.chainRevocation. A revoked signer certificate gives revoked_after_validation_time (verdict unchanged) when a timestamp proves the signature predates the revocation; otherwise revoked with verdict indeterminate. In online mode, missing issuer certificates are fetched via AIA caIssuers to complete the chain (v0.4). When anchors are provided, TSA certificate chains of RFC 3161 timestamps are evaluated too (tsaTrust).
verdict: valid means integrity and signature value passed (revoked turns it into indeterminate). online results reflect the CA's state at query time and can change later. See the site guide for each step and how to choose a mode.
Without trust anchors, trust stays not_evaluated and a valid verdict asserts cryptographic integrity, not signer identity.
Encrypted PDFs are decrypted automatically when permission-encrypted (empty user password); pass password for reader-password PDFs. Supported: RC4 (R2–R4), AES-128, AES-256 (R6). Decryption recovers string metadata (field name, /M, /Reason, /Location) and XMP — a signature's /Contents is exempt from encryption, so verification never depends on it.
Supported SubFilters: ETSI.CAdES.detached (PAdES), adbe.pkcs7.detached, ETSI.RFC3161 (document timestamps). RFC 3161 signature timestamps are fully verified (imprint + TSA signature). Legacy MD5/SHA-1 signatures are verified via node:crypto and flagged as weak.
PDF/A validation (v0.3)
validate_conformance uses a hybrid engine. With veraPDF installed (PDF_VERIFY_VERAPDF env var or on PATH) validation is delegated for authoritative results. Otherwise a built-in subset of ~15 high-value ISO 19005 rules runs natively (encryption, trailer /ID, LZW, font embedding, JavaScript/prohibited actions, OutputIntent, transparency for A-1, XFA, and more), each reported with its clause reference.
Native results are honest about their limits: violations mean definitively non-compliant; all-passed means "no violations in the checked subset" — never certification.
PDF/A-4 (pdfa-4, pdfa-4e, pdfa-4f) is accepted. Note that PDF/A-4 has no conformance level — there is no pdfa-4b; e and f are variants. The native rules were written from ISO 19005-1/-2 and have not been checked against ISO 19005-4, so a PDF/A-4 report says outright that the native verdict ranks below veraPDF. Validate part 4 with veraPDF.
XMP extension schemas and TDMRep (v0.31.0)
ISO 19005 accepts an XMP property only when its schema is predefined (XMP Specification, pdfaid) or described in the packet by pdfaExtension:schemas (19005-1 6.7.8, 19005-2/-3/-4 6.6.2.3). veraPDF reports a breach as "XMP property is either not predefined, or is not defined in any XMP extension schema" and names the namespace only in the check context. From v0.31.0 validate_conformance carries that name into the violation detail — e.g. http://www.w3.org/ns/tdmrep/ (tdm:reservation, tdm:policy) — W3C TDM Reservation Protocol (TDMRep): … — and says how the two coexist: PDF/A and TDMRep are compatible when the packet describes the TDMRep schema (reservation as Integer, policy as URL). identify_conformance lists every namespace the packet declares, marked predefined / described / known, and notes the undescribed ones when PDF/A is declared. evaluate_policy repeats the named cause in an advisory. Measured with veraPDF 1.28.2: a PDF/A-3B packet with TDMRep and no extension schema fails 6.6.2.3.1 test1 and test2 on both properties; the same packet with the description raises no XMP failure.
ISO 32000 clause constraints (v0.9)
validate_clauses covers different ground: the body of the PDF specification, not the PDF/A or PDF/UA profiles. A file can be judged COMPLIANT by veraPDF and still violate ISO 32000 — embedding a CFF font program under /FontFile2 (Table 124) is a real example that surfaced only as a viewer warning.
The mapping from clauses to structural conditions, and its evaluation, live in @shuji-bonji/pdf-constraints. Every report names the version that decided it, because the rules move as constraints are added.
Each constraint resolves to one of four states:
Two things the report distinguishes deliberately:
needs_external_fact is not a pass. Whether a font is a subset is known only to whoever made it — the PDF does not say. Supply it with given: { isSubset: true }; without it the constraint degrades rather than defaulting into silent approval.
- Some failures are traces, not violations. Where a clause addresses the PDF processor (the act of writing), a file can only show that someone broke it — §14.3.4 explicitly allows leaving an existing inconsistency alone, so the last writer is not necessarily at fault.
Because these are T1 clauses, a failure can be stated plainly and its clause ID quoted — retrieve the wording with pdf-spec-mcp's get_requirements.
PDF/UA validation (v0.6)
Pass flavour: "pdfua-1" (or "pdfua-2") to validate accessibility conformance against ISO 14289. veraPDF is delegated to with --flavour ua1 when installed; otherwise 12 native rules run: MarkInfo/Marked, StructTreeRoot, pdfuaid declaration, /Lang, DisplayDocTitle, document title, Figure /Alt, image tagging, heading hierarchy, table TH/TR, Link /Contents, and encryption barriers. Tags are resolved through /RoleMap.
PDF/UA native violations carry a severity: only error rules can prove non-conformance, while warning rules flag what needs human review. Accessibility is not fully machine-decidable — whether alt text is present is checkable, whether it is meaningful is not.
The native subset stops where pdf-lib does. Rules needing content-stream analysis — 7.1-3 (content marked as artifact or tagged), 7.2-34 (language of page content), 7.18.1-1 (annotations nested in Annot tags), 7.18.3-1 (/Tabs) — are left to veraPDF rather than approximated. Install veraPDF when accessibility matters.
Without an explicit flavour, PDF/UA is selected only when the document declares PDF/UA and not PDF/A. Use pdf-reader-mcp's inspect_tags to examine the structure tree itself; conformance judgment lives here.
Errors (v0.33.0, legacy names removed in v0.34.0)
A failed call returns isError: true with a JSON body in the PDF family shape. Branch on code, not on the sentence.
The legacy names are gone from 0.34.0. Up to 0.32.x the body was { error: true, code, message, suggestion } with the codes FILE_NOT_FOUND, PARSE_FAILED, MISSING_PATH, RELATIVE_PATH, NOT_A_FILE and INVALID_FLAVOUR. 0.33.x, the transition release, also returned detail.legacyCode, message and suggestion; 0.34.0 removed them. Read error instead of message, hint instead of suggestion, and code (with detail.reason for INVALID_ARGUMENT) instead of the old code: FILE_NOT_FOUND → DOC_NOT_FOUND, PARSE_FAILED → INVALID_PDF, MISSING_PATH / RELATIVE_PATH / NOT_A_FILE / INVALID_FLAVOUR → INVALID_ARGUMENT.
Input that the MCP SDK rejects before the handler runs (for example a key the schema does not declare) keeps the SDK's own message.
Installation
As a plugin (via the shuji-bonji/claude-plugins marketplace, recommended):
Or add directly to your MCP config:
Use @latest (or pin a version). npx -y <pkg> without a version keeps running whatever it cached the first time — -y only skips the install prompt, it does not check for updates. To clear a stale cache: rm -rf ~/.npm/_npx.
Usage examples
- "Verify the signatures in /path/to/contract.pdf — has it been altered since signing?"
- "Was this certified PDF modified after certification?"
- "Is this signature LTV-enabled (B-LT or B-LTA)?"
- "Does this document declare PDF/A-2b conformance?"
Development
Test fixtures (self-signed certificate + signed PDFs) are generated programmatically with pkijs + WebCrypto — no binary assets in the repository.
License
MIT © shuji-bonji
Dependencies: pkijs / asn1js (BSD-3-Clause), pdf-lib (MIT), normativepdf (MIT), @shuji-bonji/pdf-constraints (MIT), @modelcontextprotocol/sdk (MIT), zod (MIT).
Cross-reference sections are read by normativepdf, a clause-driven PDF library in which every behaviour is tied to an ISO 32000 clause. It is strict — an unreadable section is an error there. The recovery policy for damaged files (falling back to an older startxref, capping a cyclic /Prev chain, folding a linearised file's two sections back into one save) stays in this server, because it is a forensic judgement rather than a reading of the specification.