Skip to content

pdf-verify-mcp ​

The server that judges whether a signature is cryptographically valid and whether the file meets the standard. It verifies electronic signatures cryptographically, detects changes made after signing, and scores conformance to PDF/A (archiving) and PDF/UA (accessibility).

What this one server gives you ​

"Is the signature on this contract valid?" "Was it changed after signing?" "Will this PDF survive as PDF/A?" — all answerable here. Verdicts come from cryptography and rule tables, so the same file yields the same result every time. Deciding whether an incoming PDF may be used in a business process (incoming audit) centres on this server.

What it can decide ​

QuestionWhat is measuredToolAssertion strength
Is the signature cryptographically valid?ByteRange digest recomputation, CMS messageDigest comparison, signature value verification, RFC 3161 timestampsverify_signaturesT1
Is the signer trustworthy?Certificate chain against trust anchors, revocation (OCSP / CRL)verify_signatures (needs trust_anchors)T1
Was it changed after signing?Incremental revisions, signed-range coverage, DocMDP permissions and violations, which objects changedverify_integrityT1
Does it breach the ISO 32000 body?The constraint tables of pdf-constraints (where veraPDF does not look)validate_clausesT1
Does it conform to PDF/UA (accessibility)?ISO 14289, via veraPDF or 12 built-in rulesvalidate_conformanceT1
Does it conform to PDF/A (archiving)?ISO 19005, via veraPDF or 15 built-in rulesvalidate_conformanceT2
Does it carry long-term-preservation structure?PAdES B-B / B-T / B-LT / B-LTA-matching structuredetect_pades_levelT3 (observation only)
Does it claim to be PDF/A or PDF/UA?The pdfaid / pdfuaid declarations in XMPidentify_conformanceReading a declaration
May it be used in the business process?The facts above applied to a fixed rule table and reduced to 4 valuesevaluate_policyAggregation of facts

What it gives you together with a Skill ​

This MCP server sits in the judgment layer of the four (judgment = pass or fail over observed facts): it decides pass or fail over the facts observed. What to measure in which order, how to explain the verdict and which legal grounds to attach are a Skill's job.

Shapes carry meaning (→ legend).

SkillWhat this server does thereRequired?
pdf-trustThe axis of the audit. evaluate_policy decides the 4-value verdict; the Skill explains firedRules, recommends actions and cites legal groundsRequired (v0.7.0+, v0.21.0+ recommended)
pdf-publishScores the produced PDF with veraPDF and withholds delivery if it fails (quality gate). At the conformance level the pipeline aborts without itRequired at conformance

The verdict comes from code; the LLM only explains it

Use the returned firedRules / advisories to explain the outcome, never to override it. Do not read an advisory as a failure. Do not read the absence of advisories as a pass.

What it cannot do ​

  • It never proves conformance. What it does is look for demonstrable errors. Finding one lets you state that the file does not meet the standard; finding none does not prove that it does
  • Without trust anchors it cannot speak to identity. Without trust_anchors (or the env var), valid means only that the cryptography checks out (trust: not_evaluated). Likewise for revocation: if it could not be checked, you may not say "not revoked"
  • PDF/UA cannot be fully decided by machine. Whether alt text exists is checkable; whether it is meaningful is not
  • PAdES never becomes a conformance verdict. ETSI EN 319 142 is not in the spec corpus and there is no third-party validator, so the result is "the structure matches B-LT", never "conforms to PAdES B-LT" (every report carries normativeBasis: "T3")

What it does not do ​

  • Judging whether the content is true (a properly signed document can still state something untrue)
  • Observation (→ pdf-reader), citing the spec (→ pdf-spec), production (→ pdf-writer)

Installation ​

jsonc
{
  "mcpServers": {
    "pdf-verify": {
      "command": "npx",
      "args": ["-y", "@shuji-bonji/pdf-verify-mcp@latest"],
      "env": {
        "PDF_VERIFY_VERAPDF": "/usr/local/bin/verapdf",
        "PDF_VERIFY_TRUST_ANCHORS": "/path/to/trust-anchors"
      }
    }
  }
}

Both environment variables are optional; the built-in rules work without veraPDF.

Common parameters ​

Every tool accepts these.

ParameterTypeDescription
file_path requiredstringAbsolute path to a local PDF
response_formatmarkdown / jsonOutput format. Default markdown
passwordstringPassword for an encrypted PDF. Permissions encryption (empty user password) is tried automatically (where supported)

Tools ​

Parameters, types and defaults are in the tools reference (generated from tools/list).

ToolOne-liner
verify_signaturesCryptographic signature verification (chain, revocation, timestamps)
verify_integrityAnalysis of changes after signing (incremental updates, coverage, DocMDP)
detect_pades_levelObservation of PAdES B-B / B-T / B-LT / B-LTA-matching structure
identify_conformanceReading XMP declarations (pdfaid / pdfuaid) — the entry point of judgment
validate_conformancePDF/A / PDF/UA validation. Delegates to veraPDF, or built-in rules
validate_clausesConstraint checks against the ISO 32000 body (where veraPDF does not look)
evaluate_policyDeterministic 4-value verdict from the facts

How to use it ​

Per-tool cautions and prompt → parameters → returned JSON are at the end of each tool on the tools reference.

Check the signatures ​

verify_signatures returns three values per signature. They are independent.

FieldValuesWhat it looks at
verdictvalid / invalid / indeterminatewhether the cryptography matched
trusttrusted / untrusted / not_evaluatedthe certificate chain
revocationgood / revoked / revoked_after_validation_time / unknown / not_checkedOCSP / CRL

Without trust anchors (trust_anchors or PDF_VERIFY_TRUST_ANCHORS), trust stays not_evaluated. That valid means the digest matched; it does not prove the signer is who they claim to be. evaluate_policy then stops at use_with_caution.

What signature verification checks ​

verify_signatures checks each signature in the order below. The only steps that reach outside the PDF are ③ and ④, and only with check_revocation: "online". Everything else is decided from data inside the PDF and the trust anchors you supply.

StepWhat is checkedData usedNetworkResult field
① IntegrityThe hash of the bytes named by /ByteRange matches the CMS messageDigest attributeThe PDF itselfnonecms.digestMatches
② Signature valueThe CMS signature value verifies with the signer certificate's public keySigner certificate in the CMSnonecms.signatureVerified
③ Certificate chainThe signer certificate chains up to a trust anchorCertificates in the CMS and DSS, trust anchorsonline only: missing issuer certificates are fetched via AIA caIssuerstrust
④ RevocationThe signer certificate has not been revokedSee "Revocation modes" belowsee belowrevocation.status / revocation.source / revocation.origin / revocation.revocationTime
⑤ TimestampThe RFC 3161 token's messageImprint matches the signature value and the TSA signature verifies; with trust anchors, the TSA chain is evaluated tooUnsigned attributes of the CMSnonecms.signatureTimestamp

verdict: valid ("cryptographically valid") means ① and ② passed: the signed bytes have not changed since signing, and they were signed with the private key matching that certificate. Who issued the certificate (③) and whether it is revoked (④) are reported in separate fields. The one exception: if ④ returns revoked, verdict becomes indeterminate (see "Signatures with a revoked certificate" below).

The time used for ③ and ④ (the validation time) is chosen in this order and reported as validationTime ({ time, source }):

  1. The genTime of a verified signature timestamp (source: signature_timestamp)
  2. Otherwise, the earliest genTime among verified document timestamps covering this signature (source: document_timestamp)
  3. Otherwise, the time the verification runs (source: current_time)

When trust anchors are given, only timestamps whose TSA chain is trusted count. The CMS signingTime attribute is written by the signer, so it is never used (ISO 32000-2 §12.8.3.4.5 b)).

Revocation modes ​

OCSP asks the CA's responder for the status of one certificate (RFC 6960). A CRL is the CA's list of revoked certificates (RFC 5280). Both are signed by the CA, so once stored in the PDF's DSS they can be verified later without any network access. That is the idea behind LTV.

check_revocationOrder of lookupNetwork
nonenot checked (revocation.status is not_checked)none
embedded (default)1. OCSP responses in the DSS and the CMS signed attribute adbe-revocationInfoArchival 2. CRLs in CMS SignedData.crls, adbe-revocationInfoArchival and the DSSnone
onlineif 1–2 give no answer: 3. the OCSP responder named in the certificate's AIA 4. the certificate's CRL distribution pointsHTTP (each fetch times out after 10 s)

revocation.source tells you where the answer came from (ocsp_embedded / crl_embedded / ocsp_online / crl_online). revocation.origin tells you where embedded data sat (dss / cms_signed_data / cms_revocation_info_archival). If no source answered, status is unknown and source is null.

Only CRLs and OCSP responses whose signatures verify are used: a CRL against the issuing CA's certificate, an OCSP response against the issuing CA itself or a delegated responder signed by that CA (with id-kp-OCSPSigning). Unverifiable data, data whose nextUpdate is before the validation time, and data whose thisUpdate is more than revocation_freshness seconds (default 86400 = 24 h) before it give unknown. The CRL issuer's certificate and a delegated OCSP responder's certificate must be valid when the data was issued. To trust an OCSP responder issued by another CA, pass its certificate in trusted_ocsp_responders.

Signatures with a revoked certificate ​

A timestamp shows that the signature already existed at that time; it does not show how much earlier it was made. A revoked signer certificate is therefore judged as follows.

Conditionrevocation.statusverdict
The validation time comes from a timestamp and the revocation is laterrevoked_after_validation_timeunchanged
Anything else (no timestamp, revocation at or before the timestamp, revocation time unreadable)revokedindeterminate

Nothing shows the signature was made after the revocation, so invalid is not claimed. evaluate_policy treats revoked_after_validation_time as use_with_caution and revoked as reject.

Before choosing a mode

  • The same PDF can give different results in different modes. A PDF without revocation data in its DSS returns unknown under embedded, but may return good or revoked under online
  • online reflects the CA's state at the moment of the query. Verifying the same PDF on another day can give a different result. If you keep the result as a record, keep the time of the run with it
  • Under online, the OCSP responder and CRL host learn which certificate you are checking
  • revocation reports the signer certificate only. Each intermediate CA's result is in trust.chainRevocation; a revoked one makes trust untrusted
  • A smaller revocation_freshness gives more unknown. An OCSP response fetched just before signing usually has a thisUpdate minutes to an hour before the signature timestamp

For the general background — public-key cryptography, certificates, PKI — see the author's notes, Notes about Digital Signatures and Timestamps (Japanese).

When the file grew after signing ​

What verify_integrity returns is what to look at — not an automatic finding of tampering. Adding a signature, DSS or a document timestamp is a permitted way to write PDF. Per ISO 32000-2 §12.8.2.2, DSS / document-timestamp increments after a P=1 certification are not reported as violations (flagged laterChangesAppearLtvOnly).

Since v0.10.0 it also returns which objects were written after signing (revisions / objectChangesAfterLastSignature), walking the xref chain over raw bytes (table / xref stream / hybrid). That diff does not move the signature verdict.

Not being able to walk the xref chain is not the same as nothing having changed

When the chain cannot be walked, the result is null, not an empty array. Three things go wrong in a naive implementation:

  • A linearized file has two xref sections for one save. Without merging them, every object looks "added"
  • A full save reports an inflated change count (224,065 in one measurement)
  • Reading "could not walk" as "unchanged" states as settled a fact that was never established

All three were fixed against real measurements.

A claim in the file is not a measurement ​

identify_conformance only reads whether the XMP wrote "I am PDF/A". Writing it is not evidence. Rule checking is validate_conformance.

validate_conformance delegates to veraPDF when it is present (authoritative), and otherwise runs the built-in rules. flavour accepts "pdfa-1b"–"pdfa-3b" / "pdfa-4" / "pdfa-4e" / "pdfa-4f" (v0.11.0+) / "pdfua-1" / "pdfua-2". When omitted it follows the XMP declaration (PDF/A wins when both are declared; otherwise pdfa-2b). PDF/A-4 has no conformance level, so "pdfa-4b" does not exist — e / f are variants, not levels.

Read compliant as follows. Under veraPDF it is true / false. Under the built-in rules, false means a decisive violation, and null means "no violation in the subset that was checked" (not a certificate). Native PDF/UA violations carry a severity: only error can prove non-conformance; warning needs a person to review.

Check against ISO 32000 clauses ​

validate_clauses looks at constraints mapped from the ISO 32000-1/-2 body. A file can pass PDF/A or PDF/UA and still violate ISO 32000 (example: embedding a CFF font program under /FontFile2, which Table 124 forbids). The mapping and its evaluator live in pdf-constraints, and the output names the version that decided.

StateMeaning
passnothing in this constraint could be disproved
faildisproved, with the fact and measured value
not_applicablethe clause does not apply to this document
needs_external_facta fact outside the file was missing, so no decision was made (never defaulted to pass)

Failures marked trace mean the clause binds the PDF processor — evidence that someone broke it, not necessarily the last writer.

Observe PAdES structure ​

detect_pades_level observes structure. Do not write "conforms to PAdES". Levels are added in this order.

LevelStructure (each row adds to the one above)Meaning
B-BCAdES signaturesignature only
B-T+ signature timestamp (RFC 3161)a third party attests the signing time
B-LT+ DSS with validation data (certificates, OCSP/CRL)revocation-checking material is self-contained in the document
B-LTA+ document timestampthe validation material itself is sealed for long-term preservation

B-LT and B-LTA also require that the DSS revocation data actually covers the signer certificate. Otherwise the level stops at B-T. Legacy adbe.pkcs7.detached is reported as non-PAdES.

Ask whether it may be used in the process ​

evaluate_policy runs verify_signatures, verify_integrity and detect_pades_level internally (plus validate_conformance for long-term-preservation profiles). It then applies those facts to a fixed rule table and reduces them to one of four values. The same facts and the same profile always yield the same verdict.

Choose profile from general / contract (signature required, identity-focused) / financial and government (long-term-preservation checks) / legal / medical (most conservative — caution escalates to review).

It returns verdict, firedRules (fired rule IDs and reasons), advisories (recommendations that do not move the verdict) and a facts summary. The Skill that builds a whole audit around this verdict is pdf-trust.

Mapping onto a three-value sheet ​

External checklists are often yes / no / unknown. Two things are lost in the mapping.

verdictThree valuesWhat is lost
trust_and_useyes—
use_with_cautionyesThe use restriction. Without firedRules alongside, it reads as "usable as is"
human_review_requiredunknownThe difference between "a fact is missing" and "the criteria cannot be written"
rejectno—

Four values cannot be recovered from three. If you must map, keep the original verdict and firedRules next to it.

Assertion strength ​

How strongly a verdict may be stated depends on whether the normative text is at hand → the T1/T2/T3 rule. ETSI PAdES (T3) stays a structural observation, PDF/A (T2) goes as far as "veraPDF judged it", ISO 32000 / PDF/UA (T1) can quote the clause and state it plainly.

MIT Licensed