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).
- npm:
@shuji-bonji/pdf-verify-mcp/ current v0.31.0 / GitHub - This page is the guide — responsibilities and boundaries. For every tool's parameters and returns, see the tools reference (generated from
tools/list)
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
| Question | What is measured | Tool | Assertion strength |
|---|---|---|---|
| Is the signature cryptographically valid? | ByteRange digest recomputation, CMS messageDigest comparison, signature value verification, RFC 3161 timestamps | verify_signatures | T1 |
| 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 changed | verify_integrity | T1 |
| Does it breach the ISO 32000 body? | The constraint tables of pdf-constraints (where veraPDF does not look) | validate_clauses | T1 |
| Does it conform to PDF/UA (accessibility)? | ISO 14289, via veraPDF or 12 built-in rules | validate_conformance | T1 |
| Does it conform to PDF/A (archiving)? | ISO 19005, via veraPDF or 15 built-in rules | validate_conformance | T2 |
| Does it carry long-term-preservation structure? | PAdES B-B / B-T / B-LT / B-LTA-matching structure | detect_pades_level | T3 (observation only) |
| Does it claim to be PDF/A or PDF/UA? | The pdfaid / pdfuaid declarations in XMP | identify_conformance | Reading a declaration |
| May it be used in the business process? | The facts above applied to a fixed rule table and reduced to 4 values | evaluate_policy | Aggregation 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).
| Skill | What this server does there | Required? |
|---|---|---|
| pdf-trust | The axis of the audit. evaluate_policy decides the 4-value verdict; the Skill explains firedRules, recommends actions and cites legal grounds | Required (v0.7.0+, v0.21.0+ recommended) |
| pdf-publish | Scores the produced PDF with veraPDF and withholds delivery if it fails (quality gate). At the conformance level the pipeline aborts without it | Required 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),validmeans 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
{
"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.
| Parameter | Type | Description |
|---|---|---|
file_path required | string | Absolute path to a local PDF |
response_format | markdown / json | Output format. Default markdown |
password | string | Password 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).
| Tool | One-liner |
|---|---|
verify_signatures | Cryptographic signature verification (chain, revocation, timestamps) |
verify_integrity | Analysis of changes after signing (incremental updates, coverage, DocMDP) |
detect_pades_level | Observation of PAdES B-B / B-T / B-LT / B-LTA-matching structure |
identify_conformance | Reading XMP declarations (pdfaid / pdfuaid) — the entry point of judgment |
validate_conformance | PDF/A / PDF/UA validation. Delegates to veraPDF, or built-in rules |
validate_clauses | Constraint checks against the ISO 32000 body (where veraPDF does not look) |
evaluate_policy | Deterministic 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.
| Field | Values | What it looks at |
|---|---|---|
verdict | valid / invalid / indeterminate | whether the cryptography matched |
trust | trusted / untrusted / not_evaluated | the certificate chain |
| revocation | good / revoked / revoked_after_validation_time / unknown / not_checked | OCSP / 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.
| Step | What is checked | Data used | Network | Result field |
|---|---|---|---|---|
| ① Integrity | The hash of the bytes named by /ByteRange matches the CMS messageDigest attribute | The PDF itself | none | cms.digestMatches |
| ② Signature value | The CMS signature value verifies with the signer certificate's public key | Signer certificate in the CMS | none | cms.signatureVerified |
| ③ Certificate chain | The signer certificate chains up to a trust anchor | Certificates in the CMS and DSS, trust anchors | online only: missing issuer certificates are fetched via AIA caIssuers | trust |
| ④ Revocation | The signer certificate has not been revoked | See "Revocation modes" below | see below | revocation.status / revocation.source / revocation.origin / revocation.revocationTime |
| ⑤ Timestamp | The RFC 3161 token's messageImprint matches the signature value and the TSA signature verifies; with trust anchors, the TSA chain is evaluated too | Unsigned attributes of the CMS | none | cms.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 }):
- The
genTimeof a verified signature timestamp (source: signature_timestamp) - Otherwise, the earliest
genTimeamong verified document timestamps covering this signature (source: document_timestamp) - 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_revocation | Order of lookup | Network |
|---|---|---|
none | not 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 DSS | none |
online | if 1–2 give no answer: 3. the OCSP responder named in the certificate's AIA 4. the certificate's CRL distribution points | HTTP (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.
| Condition | revocation.status | verdict |
|---|---|---|
| The validation time comes from a timestamp and the revocation is later | revoked_after_validation_time | unchanged |
| Anything else (no timestamp, revocation at or before the timestamp, revocation time unreadable) | revoked | indeterminate |
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
unknownunderembedded, but may returngoodorrevokedunderonline onlinereflects 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 revocationreports the signer certificate only. Each intermediate CA's result is intrust.chainRevocation; a revoked one makestrustuntrusted- A smaller
revocation_freshnessgives moreunknown. An OCSP response fetched just before signing usually has athisUpdateminutes 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.
| State | Meaning |
|---|---|
pass | nothing in this constraint could be disproved |
fail | disproved, with the fact and measured value |
not_applicable | the clause does not apply to this document |
needs_external_fact | a 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.
| Level | Structure (each row adds to the one above) | Meaning |
|---|---|---|
| B-B | CAdES signature | signature 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 timestamp | the 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.
verdict | Three values | What is lost |
|---|---|---|
trust_and_use | yes | — |
use_with_caution | yes | The use restriction. Without firedRules alongside, it reads as "usable as is" |
human_review_required | unknown | The difference between "a fact is missing" and "the criteria cannot be written" |
reject | no | — |
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.