pdfnative-mcp
A zero-dependency MCP server (pdfnative-mcp) exposing 28 tools and 7 prompts for creating, transforming, signing, and verifying PDFs in any Model Context Protocol host (Claude Desktop, Cursor, ChatGPT, etc.).
Generate documents — multi-page PDFs from 13 block kinds (heading, paragraph, list, table, image, link, TOC, barcode, SVG, form field, chart, page break, spacer) with PDF/A, PDF/X-4, headers/footers, watermarks, margins, page sizes, outlines, page labels and print production boxes.
Advanced typography & colour — orphans/widows, justification, kerning, OpenType features, unit binding, French spacing, spot colours and CMYK everywhere a colour is accepted.
International text — 32 Unicode scripts (Arabic, Hebrew, Indic, CJK, Thai, etc.), BiDi/RTL reordering, colour emoji, math symbols and operator-supplied fonts.
Native vector charts & barcodes — bar/stacked/line/area/scatter/pie/donut charts with axes, legends and data labels; QR, Code 128, EAN-13, Data Matrix and PDF417.
Forms — create AcroForms, read field trees, and fill/flatten existing forms non-destructively.
Signatures & LTV — PAdES B-B/B-T/B-LT/B-LTA CMS signing (RSA/ECDSA), signature placeholders, DSS/revocation material, RFC 3161 timestamps and full signature verification.
Document lifecycle — merge, split, extract pages, annotate (markup/link overlays), update metadata, encrypt/decrypt (AES-128/256) and embed/extract attachments (Factur-X/ZUGFeRD).
Analysis & validation — inspect structure, metadata, signatures and annotations; dry-run pagination; extract text with positioned runs; structurally validate PDF/UA and PDF/X-4.
Governance — draft human-in-the-loop GitHub issues locally (never submitted, no network writes).
Safety — strict schemas, sandboxed file output, no outbound calls by default (only operator-configured TSA/OCSP/CRL), loopback-only HTTP with optional bearer token.
pdfnative-mcp
MCP server for PDF generation, PDF/A archival, PDF/X-4 print exchange, spot colours, right-to-left and 32-script text, JPEG 2000, fine typography, PAdES signing with long-term validation, AcroForms, merge/split, encryption and layout preview — 28 tools and 7 prompts on the pdfnative engine (zero-dependency, ISO 32000-1 compliant), for Claude Desktop, Cursor, ChatGPT and any Model Context Protocol client.
✨ Features
pdfnative-mcp exposes 28 production-grade tools to any MCP host:
Tool | Purpose |
| Multi-page documents from 13 block kinds — |
| Read-only pagination dry run of the same |
| QR Code, Code 128, EAN-13, Data Matrix, PDF417 — embedded in a single-page PDF. |
| 32 Unicode scripts — Gujarati, Gurmukhi, Kannada, Malayalam and Odia (new in v1.8.0), Lao, Tai Tham, New Tai Lue, Tai Le and Cham (v1.7.0) — plus Latin, math and COLRv1 colour emoji (flag / ZWJ sequences, skin-tone modifiers), with BiDi & OpenType shaping; multi-lang per document. |
| Tabular reports with smart fields (wrap, repeatHeader, zebra, caption, minRowHeight, cellPadding). |
| Create a new interactive AcroForm PDF with text fields, text areas, checkboxes, radio buttons, dropdowns, list boxes (+ |
| Read-only enumeration of an existing AcroForm's field tree (names, types, values, widgets). |
| Fill and/or flatten an existing AcroForm (non-destructive incremental update). |
| Native vector charts v2 — bar / barH / stackedBar / stackedBarH / line / area / scatter / pie / donut, secondary axis, log & time scales, data labels, per-value bar colours and optional series labels (new in v1.8.0) (pure PDF path operators, PDF/A-safe). |
| Embed a JPEG, PNG or JPEG 2000 image ( |
| Optional step 1 of the sign workflow — create a PDF with a |
| PAdES B-B / B-T CMS signature (RSA-SHA256/384/512, ECDSA-SHA256 P-256; |
| PAdES B-LT — embed a |
| PAdES B-LTA — append an RFC 3161 |
| Verify every PAdES signature and document timestamp (integrity + signature value + optional chain trust; a |
| Validate a Tagged PDF for PDF/UA (ISO 14289-1) structural conformance, or with |
| Generate a PDF/A-3 document with embedded files (Factur-X / ZUGFeRD invoices). |
| Read-only extraction of embedded files (Factur-X / ZUGFeRD XML round-trip) with byte-for-byte payloads. |
| Unicode text extraction (resolves |
| Read-only inspection: PDF version, page count, encryption (+ precise |
| Rewrite |
| Re-secure a PDF with AES-128 / AES-256 (owner/user passwords, permissions, password rotation). |
| Emit an unencrypted copy of an RC4 / AES-128 / AES-256 document. |
| Concatenate 2–50 PDFs into one via pdfnative's page-tree API (page boxes preserved). |
| Split one PDF into one document per page range (multi-output). |
| Pull an arbitrary page subset into a single PDF. |
| Add markup annotations (highlight, note, square/circle, line, freetext) and |
| Draft a governance-compliant GitHub issue locally for human review; never submits, no network. |
New in v1.8.0:
🎨 Spot colours —
print.spotColoursdeclares inks with a process alternate;{ spot, tint }(a /Separation plate) and{ inks: { … } }(a /DeviceN mix) are accepted wherever a colour is — table borders, templates, watermarks, charts, SVG, annotations. An undeclared ink is refused by name.↔️ Right-to-left documents —
direction: 'auto' | 'ltr' | 'rtl'on the nine document tools,inspect_layoutand each paragraph; untagged output carries/ActualTexton right-to-left and reordered shaped runs (actualText: falseto opt out), soextract_textreturns reading order with or without a PDF/A claim.🌏 32 Unicode scripts —
add_international_textgainsgu(Gujarati),pa(Gurmukhi),kn(Kannada),ml(Malayalam) andor(Odia); every shaper names its glyphs inToUnicode, so Tai Tham passes PDF/A-2u too.🖼️ JPEG 2000 —
mimeType: 'image/jp2'(a JP2 file or a raw codestream) onembed_image, theimageblock and image watermarks, embedded as/JPXDecodewithout re-encoding;PDFA1_JPX_IMAGEunder PDF/A-1,PDFA_JPX_IMAGEfor an opacity channel.🖨️ PDF/X-4 + PDF/A-2 dual claim —
pdfx: 'pdfx4'combines withpdfA: 'pdfa2b' | 'pdfa2u'in one file;pdfa1bandpdfa3bare refused with the engine's own message.🔗 Link annotations —
annotate_pdf type: 'link'adds a/URIlink to an existing page (http:/https:/mailto:).🔤 Operator fonts —
PDFNATIVE_MCP_FONT_DIRregisters the operator's.ttf/.otffiles at boot (parsed by the engine, ≤ 64 files);font: '<slug>'ongenerate_basic_pdf,inspect_layoutandadd_international_textdraws that face first —FONT_NOT_FOUNDlists the slugs. See docs/guides/FONTS.md.🌐 File mode behind a reverse proxy —
PDFNATIVE_MCP_OUTPUT_BASE_URLmaps the sandbox to a public base URL: file-mode results carryurland name it instead of the host's path (issue #66); pure string formatting, never requested.📊 Charts —
series[].colors(one colour per bar) and an optional serieslabel("Series N" otherwise).🔏 LTV, ECDSA —
add_ltv mode: 'offline'composes/DSS+/VRIthrough the engine (match: 'all' | 'chain');verify_pdfverifies ECDSA through the engine'secdsaVerifyHash(the local P-256 code is gone).📖 extract_text — a content stream over the decompression cap is now
PDF_PARSE_FAILEDnaming the remedy (onStreamError: 'skip'restores the 1.7.0 empty page);inspect_pdfdecodes/Infotext strings (UTF-16BE titles read back as text).🤝 Governance as resources —
pdfnative://governance/policyandpdfnative://governance/rulesserve the AI-governance contract byte-identical to the repository (always listed); 13 engine diagnostics (4 new), 48 error codes (FONT_NOT_FOUND),prepare_signature_placeholder.blockstakes 5000 items likegenerate_basic_pdf(llama.cpp grammar, issue #65); calls carryingpasswordare never cached.🧪 Hardening — the pdfnative 1.9 release pipeline (guard → build → publish → attest → registry: Trusted Publishing, SHA-512-pinned npm client, registry bytes attested with three SBOMs,
server.jsonpublished to the MCP registry with the workflow's OIDC identity), ClusterFuzzLite over five boundary targets,workflow-lint(zizmor + actionlint) as a required check, block-mode egress on the Linux jobs whose hosts are baselined (audit mode with a dated reason elsewhere), CodeQL on the workflows, Node 26 advisory, apack-manifestgate step, every Dependabot and code-scanning alert of the 1.7.0 line closed. Catalogue:tools/list≈ 357 kB under a 384 KiB budget.⬆ Engine upgrade — pdfnative v1.9.0. No breaking change to the tool API; the bytes that change (lean compact subsets,
/ActualTextspans, one text object per line) are declared in the release note and the sample baseline is re-anchored once.
New in v1.7.0:
🔤 Fine typography — an opt-in
typographyobject on the nine document tools and oninspect_layout:splitParagraphswithorphans/widows,keepHeadingsWithNext,unitBinding,bindShortWords,punctuationSpacing('fr','fr-CA'or explicit rules),opticalMargins,metrics: 'exact',fontFeatures(11 OpenType tags),kerning,hyphenationLanguage. Paragraph blocks gainalign(left/right/center/justify),keepWithNextandsplittable; heading blocks gainkeepWithNext. Honest limits:kerning,fontFeaturesand the'fr'narrow no-break space needembedFonts: true(base-14 Helvetica degrades'fr'to'fr-CA');tnum/lnumchange nothing on the bundled Noto Sans (diagnosticTYPOGRAPHY_FEATURE_INEFFECTIVE); no hyphenation dictionary is installed, sohyphenationLanguagehas no effect here — soft hyphens (U+00AD) are honoured. Seedocs/guides/TYPOGRAPHY.md.🎨 CMYK everywhere a colour is accepted —
'c m y k'operand strings (0–1) and[c, m, y, k]percent tuples (0–100) beside the existing hex / RGB forms: watermarks, header / footer templates, table cell borders, outline entries, charts,linkandsvgblocks,annotate_pdf. Every 1.6.0 colour form still validates.🖨️ PDF/X-4 —
pdfx: 'pdfx4'on six generation tools (generate_basic_pdf,add_table,add_chart,add_barcode,embed_image,add_international_text), CMYK or GrayoutputIntentprofiles beside RGB,print.marks.colourBars, andvalidate_pdf { standard: 'pdf-x-4' }to check the result;inspect_pdfreports the claim (pdfX, check'pdfx'). It requires the printer's ICC profile (device classprtr— none is bundled), needsembedFonts: truefor a conformant file (PDFX_NO_FONT_ENTRIESotherwise;strict: truerefuses), and combines withpdfA: 'pdfa2b' | 'pdfa2u'since v1.8.0 (pdfa1b/pdfa3bandencryptrefused). The validation is structural — not a certified preflight; the result says so itself incaveats[]. Seedocs/guides/PRINT.md.🌏 27 Unicode scripts —
add_international_textacceptslo(Lao),nod(Tai Tham),khb(New Tai Lue),tdd(Tai Le) andcjm(Cham);ha,yo,ig,sware aliases oflatin(tone marks attach); emoji skin-tone modifiers render. Tai Tham under PDF/A must usepdfa2b, notpdfa2u(one glyph lacks aToUnicodeentry upstream).🔁 Reproducible on every host — every date is written in UTC,
{date}in a header or footer follows the pinned instant, and the operator can pin the whole process withPDFNATIVE_MCP_CREATION_DATEorSOURCE_DATE_EPOCH(see Environment variables). Not covered, by design:signingTime,modDate, RFC 3161 tokens and revocation data, encryption, ECDSA signatures. Seedocs/guides/REPRODUCIBLE.md.🚦
strictescalates by diagnostic code —PDFA_*→PDF_A_COMPLIANCE_VIOLATION,PDFX_*→PDF_X_COMPLIANCE_VIOLATION(new), anything else →DIAGNOSTIC_ESCALATED(new). Both new codes can only be returned by a call that setsstrict: true.🧩 A seventh MCP prompt,
typography— andprint_ready,reproducible_output,pdfa_validrewritten for CMYK, PDF/X-4, colour bars and the UTC / operator pin.🐛 Fixes — a 0–1 RGB triple (
watermark.color, theannotate_pdfcolours) now renders the colour it names ([1, 0, 0]used to render almost black); any unexpected failure of a tool that takes PDF input is classifiedPDF_PARSE_FAILEDinstead of surfacing uncoded.✅ Closed upstream — a PDF/A form built with
embedFonts: truenow validates under veraPDF (the AcroForm font is embedded), andinspect_layoutmeasures atocblock exactly as the build lays it out.🧪 One gate, hardened CI, verified docs —
npm run gateis the single definition of green (the built server is driven over stdio and stdout must carry JSON-RPC frames only); veraPDF is blocking over a 41-file conformance corpus; a 96-sample byte baseline guards the output;npm run verify:docsholds every count, version, tool, error code and operator variable quoted in the docs todocs/assets/ecosystem.jsonand the source tree.🧾 Catalogue —
tools/listgrows to ≈ 306 kB in 1.7.0 (the typography fragment and the widened colour schemas are inlined in every tool that carries them;npx tsx scripts/tool-shape.ts --checkheld it under 320 KiB and the instructions under 8 KiB);_meta.apiVersionis1.7.0.⬆ Engine upgrade — pdfnative v1.8.0. No breaking change to the tool API; the bytes that do change (embedded TrueType subsets,
print.marks, shaped text with mark positioning, UTC dates) are listed under Upgrade inrelease-notes/v1.7.0.md.
New in v1.6.0:
🧱 Full engine coverage — 13 block kinds —
generate_basic_pdfaccepts everyDocumentBlockpdfnative offers: the newtable,image,link,toc,barcode,svgandformFieldblocks share their body with the dedicated tools (add_table,embed_image,add_barcode,add_form) so a standalone artefact and an inline block validate and render identically. Rules:linkacceptshttp:/https:/mailto:only (control characters rejected);imageblocks are bounded (12 M base64 characters each, 24 MiB decoded per call; PNG must be 8-bit, non-interlaced, without alpha or palette — rejected with a remedy);svgcovers paths, basic shapes and<text>(notransform,<g>, gradients or CSS — silently ignored; nothing is ever fetched);tocpairs withoutline: 'auto';formFieldunder a PDF/A claim reportsPDFA_UNEMBEDDED_FORM_FONT;barcodehas noalt(engine limitation).📐 Layout options on the nine document tools —
pageSize(A4default,Letter,Legal,A3,Tabloid),margins(all four, 0–200 pt),headerTemplate/footerTemplatewith{page}{pages}{title}{date}(afooterTemplatereplaces the default footer, sofooterTextis then ignored;{date}was the build-day wall clock in 1.6.0 — since v1.7.0 it follows the pinned instant),compress(FlateDecode streams — smaller file, different bytes; XMP stays plain under PDF/A) anddebug(guide rectangles, unmarked content — not for PDF/UA). Absent by default, so default output stays byte-identical.🔐 Encryption at build time —
encrypton seven document tools (generate_basic_pdf,add_table,add_form,add_international_text,embed_image,add_barcode,add_chart): Standard Security Handler, AES-128 default / AES-256, keeps the AcroForm (unlikeencrypt_pdf, which rebuilds the page tree). Exclusive withpdfA(VALIDATION_ERROR), never cached; not offered onprepare_signature_placeholder(must stay signable) oradd_attachment(PDF/A-3).📏
inspect_layout— the 28th tool: a read-only pagination dry run over the sameblocksand layout inputs, reportingtotalPagesand each block's page / x / top / width / height without rendering a PDF. Known engine gap in 1.6.0 (closed in v1.7.0): atocblock was measured as 0 pt, so documents with a printed contents could paginate one page later than previewed.🔎
inspect_pdf annotations: true— lists every page annotation (subtype, 0-based page, rect, contents truncated to 200 chars, title, colour, quadPoints, link URL) plusannotationCount; newcheck: 'annotations'.🖼️ Image watermarks —
watermark.image(JPEG/PNG, default opacity 0.10, own 8 MiB cap) ongenerate_basic_pdfandadd_table, alone or combined withtext(default opacity 0.15);position: 'background' | 'foreground'for both. Either opacity below 1.0 is rejected underpdfa1b.🧯
PDFNATIVE_MCP_MAX_INFLATE_BYTES— operator override of the engine's 100 MiB per-stream decompression cap (integer ≥ 1024; an invalid value refuses to start). A capped attachment stream failsextract_attachments includeData: truewithPDF_PARSE_FAILED;extract_textdegrades to empty page text (the engine swallows per-page decode failures).📝 Forms —
add_formandformFieldblocks gainlistboxandplaceholder;fieldType: 'textarea'now reaches the engine asmultilineText(it was passed through unmapped before and rendered as a single-line field — a bug fix that changes bytes for that input).embed_imagegainsalignandalt.🔏 PAdES long-term validation ladder —
sign_pdfgainsprofile: 'pades'(ETSI EN 319 142-1 baseline, ESS signing-certificate-v2,ETSI.CAdES.detached),timestamp: true(B-T, RFC 3161), RSA-SHA384/512,certChainDerBase64,fieldName/allowMultiplefor several signatures; newadd_ltvembeds a/DSS(B-LT,mode: 'online'through the operator provider ormode: 'offline'with caller-supplied DER material); newtimestamp_pdfappends a/DocTimeStamp(B-LTA).verify_pdf ltv: truereports profile, timestamp, revocation status andltvLevel. Seedocs/guides/LTV.md.🌐 Network charter — no outbound request by default. The only egress the server can ever perform goes to the RFC 3161 / OCSP / CRL endpoints the operator configured (
PDFNATIVE_MCP_TSA_URL,PDFNATIVE_MCP_REVOCATION,PDFNATIVE_MCP_NETWORK_ALLOWED_HOSTS), behind an SSRF guard; tool arguments can never supply a URL.🖨️ Print production — every document tool accepts
print(TrimBox / BleedBox / ArtBox / CropBox or thebleedshorthand, crop + registrationmarks,/UserUnit),metadata(/Author,/Subject,/Keywords,/Trapped) andoutputIntent(custom RGB ICC for PDF/A);viewerPreferencesgainsduplex,pickTrayByPDFSize,printPageRange,numCopies.inspect_pdf pages: truereports the boxes; merge / split / extract preserve them. Seedocs/guides/PRINT.md.✍️
update_metadata— rewrite/Info+ XMP of an existing PDF as an incremental update (earlier revisions and signatures preserved verbatim).📊 Charts v2 —
stackedBar/stackedBarH/area/scatter, secondary right axis (axis2),axis.scale: 'log',xAxis.type: 'linear' | 'time',dataLabels,labelStride/labelRotation; overlapping category labels are thinned automatically.📜 Honest PDF/A —
embedFonts: trueembeds Noto Sans Latin (base-14 Helvetica is not embedded, so a PDF/A claim on plain Latin text is rejected by veraPDF),strict: truefails instead of producing a non-conformant file,includeDiagnostics: trueechoes engine diagnostics. Local veraPDF script (npm run validate:pdfa) over a 26-file corpus (24 validated, 3 of them negative canaries; 2 page-tree outputs skipped) and a fail-closedVERAPDF_REQUIRED=1mode; the CI workflow pins the installer by SHA-256 and stays non-blocking in 1.6.0 (blocking since v1.7.0, where--require-allon the gate replacesVERAPDF_REQUIRED=1). Known engine gaps in 1.6.0:add_formoutput fails PDF/A-2b even withembedFonts(unembedded/DR /Helv— closed in v1.7.0), and aprepare_signature_placeholderoutput is conformant only once signed.🧰
inspect_pdf—signatures: trueinventory,dss/docTimestampCount/trapped(presence-gated), newcheckvaluesdss,docTimestamp,trapped;checkslists only the keys you requested, andsignedis structural (a signed field exists — validity isverify_pdf's job).🔁 Reproducible output — opt-in
creationDateon all nine document tools pins/CreationDate, the XMP dates and the trailer/ID;signingTimeonprepare_signature_placeholder(and onsign_pdf, now with time-zone offsets) pins/Sig /M. Identical bytes on the same host time zone in 1.6.0 (on every host since v1.7.0: dates are written in UTC). Backed by thereproducible_outputprompt.🛡️ Hardened boundary — strict input schemas (unknown or misspelt keys →
VALIDATION_ERRORinstead of being silently ignored);data:…;base64,prefixes tolerated, PEM-where-DER and double-encoded payloads rejected with the exact remedy; page-index mistakes on the page-tree tools areVALIDATION_ERRORwith a 0-based hint; an unknown tool name is a JSON-RPC protocol error (-32602,[UNKNOWN_TOOL]).🔑 HTTP bearer token — opt-in
PDFNATIVE_MCP_HTTP_TOKENgates the Streamable HTTP endpoint (401+WWW-Authenticateotherwise). Without it the loopback endpoint has no authentication — seeSECURITY.md.🧾 Catalogue —
tools/listis ≈ 245 kB (1.5.0: ≈ 108 kB) because every block kind, layout option andencryptfragment is now advertised inline — no$ref/$defsby policy, so hosts that forwardinputSchemato function-calling APIs never meet a reference; the server instructions are ≈ 6.7 kB (from 12.9 kB). Structure is guarded byscripts/tool-shape.mjs(scripts/tool-shape.tssince v1.7.0) +tests/catalogue-parity.test.ts, andtests/catalogue-superset.test.tsproves the live catalogue is a superset of the published 1.5.0 one; at most two executable_meta.examplesper tool, the rest underexamples/. Four new recipe prompts:pades_ladder,print_ready,reproducible_output,pdfa_valid.🐛 Fixes — signer metadata (
signerName/reason/location/contactInfo) never reached the/Sigdictionary on pdfnative < 1.7; it is now baked at placeholder time.verify_pdfno longer reportsallValid: falseon B-LTA documents (a/DocTimeStampwas parsed as a CMS signature).🔌 MCP 2026-07-28 on the MCP TypeScript SDK v2 (
@modelcontextprotocol/server) with automatic fallback to the 2025-erainitializehandshake — existing hosts keep working unchanged. See MCP protocol compliance.⬆ Engine upgrade — pdfnative v1.7.0 (LTV, print production, charts v2, digest agility, flag / ZWJ emoji sequences, UAX #9 fixes).
New in v1.5.0:
📊 Native vector charts —
add_chartrenders bar / horizontal-bar / line / pie / donut charts as pure PDF path operators (zero rasterisation, PDF/A-safe with auto alt text).generate_basic_pdfalso accepts achartblock for composition with text and tables.📝 Fill & flatten forms —
read_form_fieldslists an existing AcroForm's fields;fill_formfills and/or flattens it via a non-destructive incremental update (the counterpart toadd_form).🔐 Encryption round-trip —
encrypt_pdfre-secures with AES-128 / AES-256 (RC4 never emitted),decrypt_pdfrecovers an unencrypted copy, apasswordinput opens encrypted sources on the read-only tools, andmerge_pdfs/split_pdf/extract_pagesgainpassword+encrypt.🔤 Real text extraction —
extract_textnow resolves each font's/ToUnicodeCMap (no more glyph-index output) and can return positionedruns.🔗 Native MCP resources — sandboxed generated PDFs become
pdfnative://output/…resources (resources/list+resources/read), with aresource_linkin file-mode results for cross-call re-reference.🏷️ Tool annotations — every tool advertises
readOnlyHint/destructiveHint/idempotentHint/openWorldHint.⬆ Engine upgrade — pdfnative v1.6.0 (decrypt/re-encrypt,
extractText, fill/flatten, charts; colour-emoji subset 221 → 1167 glyphs).
New in v1.4.0:
🤝 AI governance + human-in-the-loop —
draft_governance_issuelets an agent draft a fully compliant GitHub issue locally (draft.md+ machine-readable compliance report). The agent is a draftsman, never an autonomous submitter: a human is the only gate, and the server makes zero GitHub writes (and, since v1.6.0, no outbound call other than to operator-configured TSA / OCSP / CRL endpoints). Backed by thegovernance_contractanddraft_issue_workflowMCP prompts.✏️ Markup annotations —
annotate_pdfoverlays highlight, sticky-note, underline, strikeout, squiggly, square, circle, line, and freetext annotations on an existing PDF via incremental update. It is a visual review layer, not a redaction — underlying bytes remain.🔢 Page labels in
inspect_pdf— read-only surfacing of/PageLabelsranges (roman, decimal, prefixed).∑ Math / scientific script —
add_international_textacceptslang: 'math'(explicit, likeemoji) to embed the Noto Sans Math face on demand.🧩 MCP prompts — the server now advertises the
promptscapability withgovernance_contractanddraft_issue_workflow.⬆ Engine upgrade — pdfnative v1.5.0.
New in v1.3.0:
🆕 Three page-tree tools —
merge_pdfs,split_pdf,extract_pages(built on pdfnative v1.4.0's page-tree API; encrypted sources were rejected until v1.5.0 addedpassword).🔖 Bookmarks, page labels & nested lists —
generate_basic_pdfgainsoutline('auto'or explicit tree),pageLabels, multi-levellistitems, andviewerPreferences.📐 Table cell borders & alignment —
add_tablegainscellBorders,cellVAlign, andviewerPreferences;add_international_textgainsviewerPreferences.🔐 Constant-time signing —
sign_pdfsigns RSA and EC-DER keys through anode:cryptoprovider with a transparent pure-JS fallback (raw P-256 scalars stay pure JS, and verification is pure JS); signatures stay interoperable.⬆ Engine upgrade — pdfnative v1.4.0.
🆕 Tool
extract_attachments— read embedded files back out of a PDF (completes the Factur-X / ZUGFeRD round-trip) with byte-for-byte payloads, afilenamefilter, and anincludeData: falsemetadata-only probe.💧 Watermarks —
generate_basic_pdfandadd_tableaccept an optionalwatermark(text, opacity, angle, colour, position;imagesince v1.6.0) rendered on every page.🌐 Unicode
normalize— opt-inNFC/NFD/NFKC/NFKDongenerate_basic_pdfandadd_international_text.🪙 Token-frugal reads — the read-only tools (
inspect_pdf,verify_pdf,validate_pdf,extract_text,extract_attachments;read_form_fieldssince v1.5.0) accept optionalverbosity: 'summary'andfields: […]inputs for ~90% smaller responses on large results, with no loss of the fields agents branch on. Defaults are unchanged.🪙 No base64 duplication — generated PDFs (base64 mode) are returned once as an embedded
resourcecontent block instead of also being copied intostructuredContent.🔧 MCP registry publish fix —
mcpNamenow uses the canonical GitHub login casing (io.github.Nizoka/pdfnative-mcp) so the registry's case-sensitive validation accepts the npm package.⬆ Dependency — upgraded to zod 4.
New in v1.1.0:
🆕 Tool
validate_pdf— read-only PDF/UA (ISO 14289-1) structural conformance check.🆕 Six new scripts — Telugu, Sinhala, Tibetan, Khmer, Myanmar, Ethiopic (24 scripts total).
🆕 COLRv1 colour emoji — native colour emoji with monochrome fallback.
🆕 Newline sanitizer — embedded
\nin paragraphs auto-splits into separate paragraphs (Safe PDF/A).🆕 Automatic NFC normalisation for
add_international_text.🛠 Engine upgrade — pdfnative v1.3.0: the Euro sign / CP-1252 symbols now extract correctly, and wrapped table cells get unique per-line MCIDs (PDF/UA-safe).
New in v1.0.0:
🆕 Three new tools:
verify_pdf,add_attachment(Factur-X / ZUGFeRD),extract_text.🆕 Smart-table fields:
wrap,repeatHeader,zebra,caption,minRowHeight,cellPadding.🆕
inspect_pdfnow reportshasSignaturePlaceholderand per-attachment summary; newcheckvalues'placeholder'and'attachments'.🆕 Signing ergonomics:
sign_pdfaccepts ECDSA SEC1 / PKCS#8 DER keys and auto-injects a/Sigplaceholder when missing (one-call signing of any PDF).🆕 Opt-in cache (
PDFNATIVE_MCP_CACHE_DIR): SHA-256 keyed, 1 h TTL, 256 MiB LRU.🆕
_meta.apiVersionand per-tool_meta.examplesfor AI-agent discovery — seedocs/API_STABILITY.md.🆕 AI agent guide:
docs/AI_GUIDE.md— decision tree + common pitfalls. See also the agent contract,docs/AGENT_CONTRACT.md(catalogue, decision tree, recipes, error table); contributors and coding agents start atAGENTS.md.🆕 PDF/A authoring guide:
docs/guides/PDFA.md.🛠 Env-var rename:
PDFNATIVE_MCP_OUTPUT_DIR(wasPDFNATIVE_MPC_OUTPUT_DIR; old name still works with a one-shot deprecation warning).✅ Now shipped:
merge_pdfs,split_pdf,extract_pages(v1.3.0),annotate_pdf(v1.4.0), theadd_chart/read_form_fields/fill_form/encrypt_pdf/decrypt_pdftools plus the encrypted round-trip and native MCP resources (v1.5.0), andadd_ltv/timestamp_pdf/update_metadataplus print production and charts v2 (v1.6.0).redact_pdfstays deferred — pdfnative can overlay/flatten but not remove page content, and an overlay-only "redaction" would create false security, so it is intentionally not shipped (tracked as an upstream content-removal request).
All tools support two output modes:
base64(default) — the generated PDF is returned once as an embeddedresourcecontent block (adata:application/pdf;base64,…URI);structuredContentcarries only{ mode, sizeBytes }(plusdiagnostics[]whenincludeDiagnostics: true, and asummaryforadd_ltv).file— the PDF is written to a sandboxed directory configured viaPDFNATIVE_MCP_OUTPUT_DIR. File output is disabled unless this variable is set; absolute paths, path traversal, non-.pdfextensions, and NUL bytes are all rejected.
Upgrading from v1.1.0: the only behaviour change is that base64-mode bytes are no longer duplicated into
structuredContent.base64. Read them from the embeddedresourceblock instead:- const base64 = response.structuredContent.base64; // v1.1.0 + const block = response.content.find((c) => c.type === 'resource'); + const base64 = block.resource.blob; // v1.2.0
Token-frugal reads (v1.2.0). The seven read-only tools (inspect_pdf, verify_pdf, validate_pdf, extract_text, extract_attachments, read_form_fields, inspect_layout) accept two optional inputs:
verbosity: 'summary'— returns a compact scalar-only verdict (drops the heavy arrays / full text). E.g.verify_pdf→{ signatureCount, allValid, invalid, summary }(+ltvLevelwithltv: true);inspect_pdfkeepsdocTimestampCount/trapped/checksPassedwhen present.fields: ['a', 'b.c']— projects the structured result to named dot-paths; composes afterverbosity. Unmatched paths are omitted and reported in_meta.unmatchedFields(with_meta.availableFields).
Smallest “is this PDF signed and valid?” probe: { "pdfBase64": "…", "verbosity": "summary", "fields": ["allValid"] }.
Why pdfnative?
pdfnative-mcp inherits every guarantee of the underlying engine:
Zero runtime dependencies in the engine — pure JavaScript, no native bindings (this server adds only the MCP SDK and zod: three runtime dependencies in total).
ISO 32000-1 (PDF 1.7) compliant output.
PDF/A-1b/2b/2u/3b, AES-128/256 encryption, AcroForm, digital signatures.
32 Unicode scripts (39
langcodes incl.latin,emoji,mathand the fourlatinaliases) with built-in BiDi reordering, Arabic positional shaping, Indic / Thai OpenType shaping and, since v1.8.0, a base textdirection(auto/ltr/rtl) for right-to-left documents.PDF/X-4 print exchange, DeviceCMYK colour and fine typography (orphan / widow control, justification, French punctuation spacing, OpenType features).
Tree-shakeable ESM build.
Related MCP server: pdf-agent-toolkit-mcp
🚀 Installation
# Run directly with npx (recommended for MCP clients)
npx -y pdfnative-mcp
# Or install globally
npm install -g pdfnative-mcp
pdfnative-mcpRequirements: Node.js ≥ 22.
⚙️ Configuration
Claude Desktop
Edit ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows):
{
"mcpServers": {
"pdfnative": {
"command": "npx",
"args": ["-y", "pdfnative-mcp"],
"env": {
"PDFNATIVE_MCP_OUTPUT_DIR": "/Users/you/Documents/mcp-pdfs"
}
}
}
}Cursor / Continue / Zed / Windsurf / Cline / Roo Code
Any MCP-compatible client that supports stdio servers will work. Use the same command + args + env triple. Example for Cursor (~/.cursor/mcp.json):
{
"mcpServers": {
"pdfnative": {
"command": "npx",
"args": ["-y", "pdfnative-mcp"],
"env": { "PDFNATIVE_MCP_OUTPUT_DIR": "/Users/you/Documents/mcp-pdfs" }
}
}
}Windsurf / Cline / Roo Code use the same shape inside their respective MCP config files.
🌐 Supported AI Ecosystem & Clients
pdfnative-mcp is designed for MCP-native environments and works with clients that support MCP over stdio or Streamable HTTP.
Community-verified compatibility includes:
Ontheia — a self-hosted, open-source AI agent platform (privacy-first). Reported as working out of the box in issue #41 and listed on Ontheia's compatible MCP servers page.
🔌 MCP protocol compliance
Since v1.6.0 the server is built on the MCP TypeScript SDK v2 (@modelcontextprotocol/server) and speaks MCP 2026-07-28:
Stateless serving —
server/discoverreplaces the session handshake; every result carriesresultTypeand the_metaserverInfoenvelope. Over HTTP, 2026-07-28 clients sendMcp-Method/Mcp-Nameheaders with eachPOST /mcp.Cache hints —
tools/listandprompts/listarepublicwith a 24 httlMs,server/discoverispublicfor 1 h, andresources/list/resources/templates/list/resources/readareprivatewithttlMs: 0(generated PDFs are per-host user data).Resource errors — an unknown resource URI is reported as JSON-RPC
-32602(Invalid params), as the 2026-07-28 specification requires.Automatic legacy fallback — a client that opens with
initialize(2025-11-25, 2025-06-18 or 2025-03-26) is served through the SDK's legacy path on both stdio and HTTP. Nothing changes for existing hosts.HTTP —
GET/DELETE /mcpanswer 405 (no SSE resumability; the server is stateless). The loopback bind and theHost/Originguard are unchanged, and theOriginport must now equal the server port (the SDK check alone is port-agnostic);PDFNATIVE_MCP_HTTP_TOKENadds an opt-in bearer-token gate (401+WWW-Authenticatewithout it). JSON-RPC batch arrays (2025-03-26) are accepted over HTTP. Keep-alive connections no longer accumulate socket listeners.stdio — as in every SDK release to date, a request sent before
initializeis dropped without a reply and JSON-RPC batch arrays are not accepted on stdio (unchanged from 1.5.0; no major host batches).Protocol errors —
tools/callwith an unknown tool name is a JSON-RPC error (-32602,[UNKNOWN_TOOL] Unknown tool: …) rather than anisErrorresult, as the specification classifies it;isError: trueis reserved for execution failures.Output schemas — every
structuredContentvalidates against the tool'soutputSchema(a 2026-07-28 MUST), includingverbosity: 'summary'andfieldsprojections: the seven read tools declare projectable schemas (all properties optional,additionalProperties: falsekept). Input schemas carry no$schemakeyword by policy (MCP ≥ 2025-11-25 defaults to JSON Schema 2020-12; some hosts forwardinputSchemato function-calling APIs that reject unknown keywords).serverInfocarrieswebsiteUrl; the resource template ispdfnative://output/{+path}.
The tools/call payload (content, structuredContent, isError) is identical between the 2026-07-28 path and the legacy path; tests/http-modern.test.ts asserts it, and tests/schema-conformance.test.ts validates structuredContent with the SDK's JSON Schema 2020-12 validator.
Client | Transport | Protocol negotiated |
Claude Desktop, Cursor, Continue, Zed, Windsurf, Cline | stdio | legacy |
ChatGPT and other Streamable HTTP hosts | HTTP | legacy stateless streamable HTTP — unchanged |
MCP 2026-07-28 clients (SDK v2 | stdio / HTTP |
|
Ontheia | stdio | legacy |
Environment variables
Variable | Purpose |
| Absolute path to the sandbox directory. Required to enable |
| (v1.8.0) Public base URL the operator serves the sandbox under (e.g. |
| (v1.8.0) Absolute directory of |
| Absolute path to enable the persistent SHA-256-keyed result cache (1 h TTL, 256 MiB LRU; key namespaced by tool API + package version + the pinned creation instant). When unset, the cache is disabled. Never caches |
| When set to a valid port (1–65535), starts an HTTP server on |
| (v1.6.0, secret) Opt-in bearer token for the HTTP transport (≥ 16 characters, no whitespace — a weaker value aborts startup). When set, every |
| (v1.6.0) Overrides the engine's 100 MiB per-stream decompression cap (zip-bomb guard): a positive integer number of bytes ≥ 1024, read once at startup — an invalid value refuses to start. Lower it on a shared host, raise it for trusted archives of large scans. A capped attachment stream fails |
| (v1.6.0) Absolute |
| (v1.6.0, secret) Optional |
| (v1.6.0) |
| (v1.6.0) Comma-separated allow-list ( |
| (v1.6.0) Per-request timeout for TSA / OCSP / CRL calls, 1000–120000 ms (default 10000). |
| (v1.7.0) ISO 8601 instant with a time zone (e.g. |
| (v1.7.0) The reproducible-builds.org convention: integer seconds since the Unix epoch, used when |
Creation-date precedence (highest first): the per-call creationDate → PDFNATIVE_MCP_CREATION_DATE → SOURCE_DATE_EPOCH → the wall clock. Dates are always written in UTC, so pinned output is byte-identical on every host and in every time zone. The pin does not cover, by design: signingTime (sign_pdf, prepare_signature_placeholder), modDate (update_metadata), the regenerated second /ID of incremental writers (annotate_pdf, fill_form), RFC 3161 timestamp tokens and online revocation data, encryption (fresh file key, salts and IVs) and ECDSA signatures (randomised by design). See docs/guides/REPRODUCIBLE.md.
🛠 Tool reference
generate_basic_pdf
{
"title": "Q1 2026 Report",
"blocks": [
{ "type": "heading", "text": "Executive summary", "level": 1 },
{ "type": "paragraph", "text": "Revenue grew 24% year over year." },
{ "type": "list", "style": "bullet", "items": ["Strong APAC", "Stable EU", "Soft NA"] },
{ "type": "pageBreak" },
{ "type": "heading", "text": "Details", "level": 2 }
],
"footerText": "Confidential — Internal use only",
"outputMode": "base64"
}The 13 block kinds: heading, paragraph, list, table, image, link, toc, barcode, svg, formField, chart, pageBreak, spacer. A composite report:
{
"title": "Quarterly report",
"blocks": [
{ "type": "toc" },
{ "type": "heading", "text": "Sales", "level": 1 },
{ "type": "table", "headers": ["Region", "Revenue"], "rows": [["EMEA", "1.2 M"], ["APAC", "0.9 M"]], "zebra": true },
{ "type": "image", "imageBase64": "<base64 JPEG>", "mimeType": "image/jpeg", "width": 300, "alt": "Revenue chart" },
{ "type": "svg", "data": "M10 10 H 90 V 90 H 10 Z", "viewBox": [0, 0, 100, 100], "fill": "#0a7e8c" },
{ "type": "barcode", "format": "qr", "data": "https://example.com/q1", "align": "center" },
{ "type": "link", "text": "Full dataset", "url": "https://example.com/data" },
{ "type": "formField", "fieldType": "text", "name": "reviewer", "label": "Reviewed by" }
],
"outline": "auto",
"pageSize": "Letter",
"headerTemplate": { "right": "{title} — page {page}/{pages}" },
"embedFonts": true
}Block rules: table, barcode, formField and chart take the same body as add_table / add_barcode / add_form / add_chart; link URLs must be http:, https: or mailto:; image blocks are capped at 12 M base64 characters each and 24 MiB decoded per call (PNG: 8-bit greyscale/RGB, non-interlaced, no alpha, no palette — otherwise VALIDATION_ERROR with a remedy); svg supports <path>, <rect>, <circle>, <ellipse>, <line>, <polyline>, <polygon>, <text>/<tspan> and silently ignores transform, <g>, <use>, <image>, gradients, opacity and CSS (no external reference is ever fetched); toc is built from the heading blocks and pairs with outline: 'auto'; formField under pdfA needs embedFonts: true so the field font is embedded too (PDFA_UNEMBEDDED_FORM_FONT otherwise; strict: true then fails); barcode has no alt. Use inspect_layout with the same inputs to preview the pagination before rendering.
Typography (v1.7.0). typography is one opt-in object (12 keys, all off by default — omitted means unchanged bytes) on the nine document tools and on inspect_layout:
{
"title": "Annual report",
"embedFonts": true,
"typography": {
"splitParagraphs": true, "orphans": 3, "widows": 3,
"keepHeadingsWithNext": { "minLines": 3 },
"opticalMargins": true, "kerning": true, "fontFeatures": ["onum", "smcp"],
"unitBinding": true, "bindShortWords": true, "punctuationSpacing": "fr"
},
"blocks": [
{ "type": "heading", "text": "Outlook", "level": 2, "keepWithNext": true },
{ "type": "paragraph", "text": "A long justified paragraph…", "align": "justify" },
{ "type": "paragraph", "text": "Figures by region:", "keepWithNext": true, "splittable": false }
]
}Keys:
splitParagraphs,orphans/widows(1–10, default 2, needsplitParagraphs),keepHeadingsWithNext(trueor{ minLines }),unitBinding(trueor{ units }),bindShortWords(trueor{ maxLength, words }),punctuationSpacing('fr','fr-CA'or an array of{ char, side, space }rules),opticalMargins,metrics('approximate'default /'exact'),fontFeatures(tnum,pnum,lnum,onum,zero,ordn,sups,subs,smcp,c2sc,case),kerning,hyphenationLanguage.Block inputs: a
paragraphtakesalign(leftdefault /right/center/justify),keepWithNextandsplittable(overridestypography.splitParagraphsfor that block); aheadingtakeskeepWithNext(overridestypography.keepHeadingsWithNextfor that block).Limits:
kerning,fontFeaturesand the'fr'narrow no-break space need an embedded font (embedFonts: true; on base-14 Helvetica'fr'degrades to'fr-CA');metrics: 'exact'acts on base-14 text only;tnum/lnumchange nothing on the bundled Noto Sans (diagnosticTYPOGRAPHY_FEATURE_INEFFECTIVE); no hyphenation dictionary is installed, sohyphenationLanguagehas no effect on this server — soft hyphens (U+00AD) in the text are honoured. Seedocs/guides/TYPOGRAPHY.mdand thetypographyprompt.
CMYK colours (v1.7.0). Every colour input keeps the form it has always accepted (hex on charts, templates, link and svg blocks; a 0–1 RGB triple on watermarks and annotate_pdf; a free string on outline entries and table cell borders) and gains DeviceCMYK beside it: an operand string 'c m y k' with components 0–1 ("1 0.6 0 0.1") and a percent tuple [c, m, y, k] with components 0–100 ([100, 60, 0, 10]). Under a PDF/A claim against the default sRGB intent a CMYK colour reports PDFA_DEVICE_CMYK_CONTENT — keep colours RGB there, or supply a CMYK outputIntent.
PDF/X-4 (v1.7.0). pdfx: 'pdfx4' on generate_basic_pdf, add_table, add_chart, add_barcode, embed_image and add_international_text writes a PDF/X-4 (ISO 15930-7) file: %PDF-1.6 header, the PDF/X-4 XMP identification, a /GTS_PDFX output intent, a TrimBox on every page and /Trapped.
{
"title": "Spring catalogue",
"pdfx": "pdfx4",
"embedFonts": true,
"outputIntent": { "iccProfileBase64": "<the printer's CMYK ICC profile, base64>", "outputConditionIdentifier": "FOGRA39" },
"metadata": { "trapped": "False" },
"print": { "bleed": 14.17, "marks": { "colourBars": true } },
"blocks": [{ "type": "paragraph", "text": "Four-colour job exchanged as PDF/X-4." }]
}It requires
outputIntentwith the ICC profile of the printing condition (device classprtr, CMYK or Gray — no press profile is bundled; ask your printer); it needsembedFonts: truefor a conformant file (PDFX_NO_FONT_ENTRIESotherwise;strict: truerefuses); it combines withpdfA: 'pdfa2b' | 'pdfa2u'since v1.8.0 (one file, both claims;pdfa1b/pdfa3brefused) and is exclusive withencrypt;metadata.trappedmust be'True'or'False'; a page carries a TrimBox or an ArtBox, not both. Incoherent requests are refused withVALIDATION_ERRORbefore any work is done. Links and form fields are reported (PDFX_ANNOTATIONS); withstrict: trueaPDFX_*diagnostic fails the call withPDF_X_COMPLIANCE_VIOLATION.outputIntentaccepts RGB, CMYK and Gray profiles (≤ 8 MiB, underpdfAorpdfx). The profile must be a real ICC file (acspsignature, consistent size field) — a hand-made stub is rejected.print.marksacceptstrueor an object;marks.colourBars(trueor{ tints, size },size4–72 pt, default 12) adds the C M Y K solids and their 50 % tints in the bleed. Off by default; it needs a bleed of about 5 mm (14.17 pt) and is skipped when the strip would not fit.Check the result with
validate_pdf { standard: 'pdf-x-4' }— a structural check, not a certified preflight. Seedocs/guides/PRINT.mdand theprint_readyprompt.
add_barcode
{
"format": "qr",
"data": "https://pdfnative.dev",
"caption": "Scan to learn more",
"ecLevel": "H",
"outputMode": "file",
"outputPath": "tickets/event-42.pdf"
}Supported formats: qr, code128, ean13, datamatrix, pdf417.
add_international_text
{
"title": "مرحبا بالعالم",
"lang": "ar",
"paragraphs": [
"هذا اختبار للنص العربي مع تشكيل OpenType ومحارف ثنائية الاتجاه.",
"Mixed content: العربية + English ✓"
]
}Supported lang codes: the 32 Unicode scripts — ar, he, th, ja, zh, ko, el, hi, bn, ta, ru, ka, hy, tr, pl, vi, te, si, bo, km, my, am, and since v1.7.0 lo (Lao), nod (Tai Tham), khb (New Tai Lue), tdd (Tai Le), cjm (Cham), and since v1.8.0 gu (Gujarati), pa (Gurmukhi), kn (Kannada), ml (Malayalam), or (Odia) — plus the utility faces latin, emoji and math. Since v1.7.0 ha (Hausa), yo (Yoruba), ig (Igbo) and sw (Swahili) are aliases of latin: the bundled Noto Sans anchors their tone marks, no extra font is embedded. Fonts are always embedded (no embedFonts input); pin creationDate for byte-identical output. The tool also accepts typography and pdfx (see generate_basic_pdf).
Tai Tham under PDF/A:
lang: 'nod'validates underpdfa2band, since 1.8.0, underpdfa2utoo — pdfnative 1.9.0 names every shaped glyph inToUnicode(the 1.7.0 "usepdfa2b" rule is lifted).
Multi-script documents — pass an array or comma-separated list:
{
"title": "Mixed Script",
"lang": ["ar", "emoji"],
"paragraphs": ["العربية مع رموز 🎉🚀"],
"pdfA": "pdfa2u"
}sign_pdf
As of v1.0.0, sign_pdf auto-injects a /Sig placeholder when missing — you can sign any PDF in one call:
{
"pdfBase64": "<any base64 PDF>",
"algorithm": "rsa-sha256",
"certDerBase64": "<base64 X.509 cert in DER>",
"rsaKeyPkcs1DerBase64": "<base64 PKCS#1 RSAPrivateKey DER>",
"signerName": "Alice",
"reason": "Approval",
"location": "Paris, FR",
"signingTime": "2026-01-15T10:30:00Z"
}For ECDSA P-256: use algorithm: "ecdsa-sha256" and supply either ecPrivateKeyDerBase64 (SEC1 or PKCS#8 DER) or ecPrivateScalarHex (64 hex chars).
PEM → DER conversion:
openssl x509 -in cert.pem -outform DER | openssl base64 -A # cert
openssl rsa -in key.pem -outform DER -traditional | openssl base64 -A # RSA PKCS#1
openssl pkey -in key.pem -outform DER | openssl base64 -A # ECDSAUse
prepare_signature_placeholderonly when you need to customize the placeholder (e.g. largerplaceholderBytesfor >4096-bit RSA keys,subFilter: 'ETSI.CAdES.detached',reserveTimestamp: true). Otherwise callsign_pdfdirectly.
PAdES ladder (v1.6.0). sign_pdf with profile: "pades" produces a B-B signature; add timestamp: true for B-T (needs PDFNATIVE_MCP_TSA_URL), then add_ltv (B-LT) and timestamp_pdf (B-LTA):
// 1. sign_pdf { ..., "profile": "pades", "timestamp": true, "certChainDerBase64": ["<intermediate DER>"] }
// 2. add_ltv { "pdfBase64": "<signed>", "mode": "online" } // or "offline" + certificatesDerBase64 / ocspResponsesDerBase64 / crlsDerBase64
// 3. timestamp_pdf { "pdfBase64": "<ltv>" } // re-run before the TSA certificate expires
// 4. verify_pdf { "pdfBase64": "<final>", "ltv": true } // -> ltvLevel: "B-LTA"Signer metadata (signerName, reason, location, contactInfo) is baked into the placeholder; fieldName selects one of several unsigned placeholders (PLACEHOLDER_AMBIGUOUS otherwise) and allowMultiple: true adds a further signature. See docs/guides/LTV.md.
add_table
{
"title": "Monthly Sales",
"headers": ["Region", "Units", "Revenue"],
"rows": [
["APAC", "1200", "$240,000"],
["EMEA", "800", "$160,000"]
],
"infoItems": [{ "label": "Period", "value": "January 2025" }],
"footerText": "Internal use only",
"outputMode": "base64"
}add_form
{
"title": "Employee Onboarding",
"fields": [
{ "fieldType": "text", "name": "fullName", "label": "Full Name", "required": true },
{ "fieldType": "dropdown", "name": "dept", "label": "Department", "options": ["Engineering", "Sales", "HR"] },
{ "fieldType": "checkbox", "name": "agree", "label": "I agree to the terms", "checked": false },
{ "fieldType": "listbox", "name": "skills", "label": "Skills", "options": ["TypeScript", "PDF", "MCP"] },
{ "fieldType": "textarea", "name": "notes", "label": "Notes", "placeholder": "Anything we should know?" }
],
"outputMode": "base64"
}Field types: text, textarea (multi-line, /Ff 4096), checkbox, radio, dropdown, listbox; placeholder shows hint text while a field is empty. Add encrypt to produce a password-protected form that keeps its AcroForm. Under a PDF/A claim pass embedFonts: true so the field font is embedded too (since v1.7.0 such a form validates under veraPDF); without it the call reports PDFA_UNEMBEDDED_FORM_FONT (strict: true then fails).
embed_image
{
"title": "Product Photo",
"imageBase64": "<base64-encoded JPEG bytes>",
"mimeType": "image/jpeg",
"caption": "Front view of Model X",
"width": 400,
"align": "center",
"alt": "Front view of the Model X chassis",
"outputMode": "base64"
}Note: the engine's PNG decoder accepts 8-bit, non-interlaced greyscale / RGB images only. Alpha-channel (colour type 4 / 6), palette (type 3), 16-bit and interlaced PNGs are rejected at the boundary with
VALIDATION_ERRORand a remedy (flatten or re-export) — the same rule applies toimageblocks and image watermarks.embed_image.imageBase64keeps its 1.5.0 contract with no length bound; the 12 M-character cap applies to inlineimageblocks and watermark images only.
prepare_signature_placeholder
{
"title": "Service Agreement",
"signerName": "Alice Dupont",
"reason": "Approved",
"location": "Paris, FR",
"blocks": [
{ "type": "paragraph", "text": "By signing below, I accept the terms and conditions." }
],
"outputMode": "base64"
}Pass the returned PDF bytes to sign_pdf to complete the signing workflow.
inspect_pdf
Read-only structural and security inspection — useful for downstream verification, CI assertions, and AI agents that need to reason about a PDF before acting on it.
{
"pdfBase64": "<base64 PDF>",
"pages": true,
"check": ["pdfa", "signed", "attachments"]
}Returns:
{
"version": "1.7",
"pageCount": 3,
"encryption": "none", // 'none' | 'aes-128' | 'aes-256' | 'rc4' | 'unknown'
"pdfA": "3B", // null when no PDF/A claim is present
"signatureCount": 1,
"hasSignaturePlaceholder": false,
"attachments": [{ "filename": "factur-x.xml", "mimeType": "application/xml", "sizeBytes": 1234, "relationship": "Source" }],
"info": { "Producer": "pdfnative", "Title": "Invoice INV-2025-001" },
"perPage": [{ "index": 0, "width": 595, "height": 842 }],
"checks": { "pdfa": true, "signed": true, "attachments": true },
"checksPassed": true
}check[] accepts any of 'pdfa', 'signed', 'encrypted', 'placeholder', 'attachments', 'dss', 'docTimestamp', 'trapped', 'annotations' (the last four since v1.6.0) and 'pdfx' (since v1.7.0: the XMP claims PDF/X — the claim, not its validity; that is validate_pdf standard: 'pdf-x-4'). pdfX appears in the result only when the XMP claims PDF/X (kept by verbosity: 'summary'). checksPassed is the AND of all requested checks. signatures: true adds a per-field inventory (subFilter, isDocTimestamp, isPlaceholder, byteRange, vriKey); annotations: true adds annotations[] (every /Annots entry: 0-based page, subtype, rect, and when present contents truncated to 200 chars, title, color, quadPoints, link url) plus annotationCount; dss, docTimestampCount and trapped appear only when present; with pages: true each perPage entry also carries trimBox / bleedBox / artBox / cropBox / userUnit when set.
inspect_layout
Read-only pagination dry run — the same blocks as generate_basic_pdf plus every input that moves a block (title, footerText, pdfA, normalize, embedFonts, pageSize, margins, headerTemplate, footerTemplate, and since v1.7.0 typography). No PDF is produced; pass exactly what you will give generate_basic_pdf and totalPages matches.
{ "title": "Memo", "blocks": [{ "type": "paragraph", "text": "Short note." }], "pageSize": "Letter", "verbosity": "summary", "fields": ["totalPages"] }The full result carries pageWidth, pageHeight, margins, totalPages and pages[].blocks[] (type, page, x, top, width, height in points, rounded to two decimals). Since v1.7.0 the dry run and the build share one pagination planner, so every block kind — toc included — is measured exactly as it is laid out (in 1.6.0 a toc block was measured as 0 pt).
validate_pdf
Read-only structural conformance check. standard picks the rule set: 'pdf-ua-1' (default — PDF/UA, ISO 14289-1, for a Tagged PDF) or, since v1.7.0, 'pdf-x-4' (PDF/X-4, ISO 15930-7). Generate an accessible document with any tool using pdfA (e.g. pdfA: 'pdfa2u'), then validate the result:
{ "pdfBase64": "<tagged-pdf-base64>" }Returns:
{
"standard": "pdf-ua-1",
"valid": true,
"errors": [], // blocking structural violations (empty when valid)
"warnings": [], // non-blocking best-practice recommendations
"summary": "PDF/UA structural prerequisites hold."
}It verifies catalog /MarkInfo /Marked true, /StructTreeRoot (+ /ParentTree), /Metadata (XMP), /Lang, and per-page MCID uniqueness. This is a fast developer-time gate — not a substitute for a full reference validator (veraPDF), which additionally checks fonts, colour, and rendering.
PDF/X-4 (v1.7.0). { "pdfBase64": "<pdf>", "standard": "pdf-x-4" } checks the structural prerequisites of ISO 15930-7: PDF 1.6 header, no encryption, trailer /ID, the PDF/X-4 XMP identification, a /GTS_PDFX output intent with an embedded prtr ICC profile, a TrimBox or ArtBox per page nested in the BleedBox and MediaBox, every font embedded, no annotation on the printed area, no JavaScript, no embedded file, device colour consistent with the output intent. The result has the same shape plus caveats[], which states what a valid: true verdict does not establish: this is not a certified preflight, and veraPDF does not cover PDF/X — confirm a press job with callas pdfToolbox or Acrobat Preflight. The default (pdf-ua-1) response is unchanged and carries no caveats.
annotate_pdf
Overlay markup annotations on an existing PDF via incremental update. This is a visual review layer, not a redaction — the underlying content is untouched.
{
"pdfBase64": "<base64 PDF>",
"annotations": [
{ "type": "highlight", "page": 0, "rect": [72, 700, 520, 715], "color": [1, 1, 0], "contents": "Check this figure" },
{ "type": "text", "page": 0, "rect": [540, 700, 560, 720], "contents": "Reviewer note" }
]
}Types: text, highlight, underline, strikeout, squiggly, square, circle, line, freetext. Page indices are 0-based. color / interiorColor take a hex or operand string (including a CMYK operand string), a 0–1 RGB triple ([1, 1, 0]) or, since v1.7.0, a CMYK percent tuple ([0, 0, 100, 0]); v1.7.0 also fixes the 0–1 triple, which used to render almost black. Encrypted sources are rejected (ENCRYPTED_SOURCE) — run decrypt_pdf first (drops signatures / AcroForm), annotate, then encrypt_pdf again.
draft_governance_issue
Draft a governance-compliant GitHub issue locally for a human to review and submit. The server never contacts GitHub (its only possible egress is the operator-configured TSA / OCSP / CRL endpoints — see Network & egress); it returns the draft Markdown plus a machine-readable compliance report.
{
"title": "add_table drops the caption on the second page",
"issueType": "bug",
"summary": "The table caption is only rendered on page 1 when repeatHeader is true.",
"reproduction": { "command": "add_table with caption + repeatHeader over 2 pages (examples/bordered-table.json, then inspect_pdf)", "result": "Page 2 has no caption row." },
"expectedBehavior": "The caption repeats with the header on every page.",
"duplicateSearchPerformed": true
}A draft that proposes a runtime dependency, omits a reproduction, or sets duplicateSearchPerformed: false is rejected with GOVERNANCE_VIOLATION. See docs/guides/AI_GOVERNANCE.md for the full human-in-the-loop contract.
verify_pdf, add_attachment, extract_text
See the dedicated sections in docs/AI_GUIDE.md and the reference in docs/KNOWLEDGE_BASE.md. Ready-to-run examples live under examples/.
🔐 Security model
pdfnative-mcp runs inside the host process and exposes a stdio MCP server (or a loopback-only HTTP endpoint). It does not perform any I/O outside the configured sandbox.
File writes are gated by
PDFNATIVE_MCP_OUTPUT_DIR. When unset, thefileoutput mode is rejected with aSecurityError.Path resolution rejects absolute paths, traversal sequences (
..), NUL bytes, and any extension other than.pdf.Output size is capped at 50 MB per call.
Inputs are validated against strict JSON Schemas + Zod runtime checks at the boundary of every tool — unknown or misspelt keys (top-level or nested) are rejected with
VALIDATION_ERROR, and base64 / DER payloads are sanity-checked (data:prefix tolerated, PEM or double-encoded input rejected with the remedy) before any parser runs.HTTP transport (
PDFNATIVE_MCP_PORT) binds loopback only; it has no authentication unlessPDFNATIVE_MCP_HTTP_TOKENis set (then401without a valid bearer token).Operator fonts (
PDFNATIVE_MCP_FONT_DIR) are parsed by the engine before they are registered, bounded in number and size, and named by slug only — a tool argument never carries a path. The output base URL (PDFNATIVE_MCP_OUTPUT_BASE_URL) is pure string formatting: the server never requests it. Password calls are never cached.Fuzzed boundary — five ClusterFuzzLite targets (
fuzz/:tools/callarguments, base64, images, the output sandbox, the HTTP guards) run on every pull request and weekly; the same targets run againstsrc/at every gate (tests/fuzzing/).
Network & egress
The server makes no outbound network call by default. The only egress it can ever perform goes to the RFC 3161 / OCSP / CRL endpoints the operator configured in the environment for PAdES long-term validation (PDFNATIVE_MCP_TSA_URL, PDFNATIVE_MCP_REVOCATION, PDFNATIVE_MCP_NETWORK_ALLOWED_HOSTS) — never to a URL supplied by a tool argument, never to GitHub, never for telemetry. Without that configuration sign_pdf timestamp: true, timestamp_pdf and add_ltv mode: 'online' fail fast with TSA_NOT_CONFIGURED / REVOCATION_NOT_CONFIGURED before touching the document; add_ltv mode: 'offline' embeds caller-supplied material with zero network access.
OCSP / CRL URLs come from the AIA / CRL-distribution-point extensions of untrusted certificates inside the PDF, so every fetch passes an SSRF guard:
host must match the operator allow-list (
host,host:portor*.suffix; bare wildcards are rejected). Entries are hostnames, not URLs: ahost:portentry only matches URLs carrying an explicit port (the URL parser drops default:80/:443— list the bare host for those); wildcard entries cannot carry a port; IDN hostnames must be listed in punycode (xn--…); IPv6 literals in brackets ([2001:db8::1]);http:/https:only, no embedded credentials, redirects are never followed;loopback, link-local, private, unique-local, CGNAT, unspecified and multicast address literals (including decimal / octal / hex spellings and IPv4-mapped IPv6) are rejected unless that literal is allow-listed verbatim. The guard checks literals only — a listed hostname that resolves to an internal address (DNS rebinding) is not detected, since there is no resolver without adding a dependency; allow-list only hosts you control;
per-request timeout (
PDFNATIVE_MCP_NETWORK_TIMEOUT_MS) and response caps (256 KiB TSA, 1 MiB OCSP, 16 MiB CRL) enforced while streaming, so an oversized response is cut off rather than buffered;OCSP responses and CRLs returned by responders are parse-validated before
add_ltvembeds them;the TSA URL is operator-trusted (scheme + credential checks only); the
PDFNATIVE_MCP_TSA_AUTHsecret is never logged or echoed in error messages.
Providers are built per call and passed through pdfnative's per-call options — the process-wide provider setters are never used, so concurrent requests share nothing. The server/discover instructions report the current egress policy (endpoint kinds only, never secrets).
See SECURITY.md for the responsible disclosure process and docs/guides/LTV.md for the operator setup.
🧪 Local development
git clone https://github.com/Nizoka/pdfnative-mcp.git
cd pdfnative-mcp
npm ci # .npmrc sets ignore-scripts=true: nothing builds on install
npm run gate:fast # before every commit: typecheck:all, lint, test, server-json, verify:docs
npm run gate # the CI profile: build, dist checks, stdio smoke test, tool shape, pack manifest, samples, coverage, docs, corpus, validate:pdfx
npx tsx scripts/gate.ts --publish --require-all # release branches: everything incl. validate:pdfa (veraPDF); a skipped step failsnpm run gate (scripts/gate.ts) is the single definition of green — 1804 tests, the coverage thresholds of vitest.config.ts, and the built server driven over stdio, where stdout must carry JSON-RPC frames only. It prints one line per step and writes the logs to test-output/.gate/<step>.log; --only <step> runs one step and --json gives machine output (call the script directly to pass flags: npx tsx scripts/gate.ts --fast). The individual steps are ordinary npm scripts:
npm run build && npm run test:generate # drive the built server under TZ=UTC, operator variables scrubbed -> test-output/samples/
npm run verify:samples # hold the 112 samples to tests/_fixtures/samples.sha256.json (109 by bytes, 3 by a semantic projection)
npm run corpus:pdfa # write the 45-file conformance corpus (36 claim PDF/A, 6 claim PDF/X-4, 1 claims both — the PDF/X-4 + PDF/A-2 dual claim —, 2 page-tree outputs claim nothing)
npm run validate:pdfx # in-process structural PDF/X-4 check of the corpus; never skips
npm run validate:pdfa # veraPDF over the PDF/A files (VERAPDF_HOME, JAVACMD); exit 0 ok / 1 conformance / 2 infrastructure
npm run verify:docs # every count, version, tool, error code, operator variable, link and anchor in the docs vs docs/assets/ecosystem.json and src/
npx tsx scripts/tool-shape.ts --write # only after a deliberate tools/list schema change (npm run verify:tool-shape checks the fixture)
npx vitest run tests/fuzzing/targets.test.ts # the five ClusterFuzzLite targets against src/ on their seeded corpus (also part of npm test)An intended output change is rebaselined with npx tsx scripts/verify-samples.ts --update and declared in the release note — never to silence a surprise. With veraPDF 1.30.2 the corpus gives 27 PASS and 6 expected failures (negative canaries that must stay rejected). Every script, its flags and its exit codes are listed in scripts/README.md.
Smoke-test the server over stdio:
node dist/cli.js
# In another terminal, send a JSON-RPC initialize request via stdin (e.g. with mcp-inspector).Contributors: start at
AGENTS.md(the repository rules shared by every coding agent and contributor) and see docs/guides/LOCAL_TESTING.md for the full local-verification workflow — the quality gate, examples-as-tests, validating that generated PDFs are structurally correct (assertValidPdf,inspect_pdf,validate_pdf,verify_pdf), opening output in a viewer, external PDF/A checking with veraPDF, and the MCP Inspector.
📣 Release process
pdfnative-mcp follows the same release formalism as pdfnative:
One release note file per tag in
release-notes/vX.Y.Z.mdCHANGELOG.mdmirrors each release bullet listGitHub Release body is copied from
release-notes/vX.Y.Z.mdnpx tsx scripts/release-prepare.ts --version X.Y.Zapplies the mechanical part of a version bump (it never commits, tags or publishes); the version moves in lock-step acrosspackage.json,src/version.ts,server.jsonanddocs/assets/ecosystem.jsonA release branch must pass
npx tsx scripts/gate.ts --publish --require-all— every step, veraPDF included, with no skipnpm publication is handled by GitHub Actions Trusted Publishing (OIDC), without
NPM_TOKEN, from a protected environment, through the five-job pipeline of pdfnative 1.9 (publish.yml: guard refuses anything but av*tag equal to the package version before any approval → build runs the publish gate and packs once, digests recorded → publish installs nothing, fetches the npm client by SHA-512, re-verifies the tarball and publishes with--provenance→ attest fetches the bytes the registry serves, checks them against the build digests, runsnpm audit signatures, attests them with three SBOMs (CycloneDX + SPDX runtime, CycloneDX toolchain) and attaches everything plus the Sigstore bundle to the GitHub release → registry publishesserver.jsonto the MCP registry with the workflow's OIDC identity through a SHA-256- and cosign-verifiedmcp-publisher)Every Linux and Windows job starts with
step-security/harden-runner— egress blocked with an allow-list on the Linux jobs, audited with a dated reason elsewhere (Windows supports audit only; the action does not support macOS, the one documented exemption) —, checks out withpersist-credentials: false, installs withnpm ci --ignore-scripts, pins every action to a commit SHA, scopes every write permission to its job and passes event values throughenv:;workflow-lint(zizmor + checksummed actionlint) is a required check beside the gate on three operating systems and the sample baseline; CodeQL (actions+javascript-typescript, security-extended), Scorecard, Dependency Review, a weeklynpm auditand ClusterFuzzLite complete the pictureHuman-in-the-loop: coding agents prepare and verify; the maintainer pushes, opens the pull request, tags and publishes (
.github/AGENT_RULES.md)
See release-notes/TEMPLATE.md for the canonical structure and publication checklist, and CONTRIBUTING.md for the release procedure.
📚 Project structure
src/
├── cli.ts # entrypoint: stdio (default) or Streamable HTTP (PDFNATIVE_MCP_PORT)
├── http.ts # Node http <-> Web Request/Response bridge + Host/Origin loopback guard
├── auth.ts # opt-in HTTP bearer token (PDFNATIVE_MCP_HTTP_TOKEN)
├── base64.ts # base64 / DER boundary decoding with agent-facing diagnostics
├── index.ts # public library exports
├── server.ts # Server factory, tool registry, cache hints, SERVER_INSTRUCTIONS
├── network.ts # operator-configured TSA / OCSP / CRL egress + SSRF guard
├── print.ts # print-production schema (boxes, bleed, marks + colour bars, userUnit, RGB / CMYK / Gray outputIntent, metadata, creationDate)
├── pdfx.ts # pdfx: 'pdfx4' conformance target + the static conflicts refused before the build
├── color.ts # shared colour fragment: CMYK operand string / percent tuple beside each historical form, toEngineColor()
├── typography.ts # the typography fragment (12 opt-in keys) + TypographyOptions mapper
├── reproducible.ts # operator-pinned creation instant (PDFNATIVE_MCP_CREATION_DATE, SOURCE_DATE_EPOCH)
├── diagnostics.ts # engine diagnostics sink (PDFA_* / PDFX_* / TYPOGRAPHY_*), strict escalation by code, includeDiagnostics, embedFonts
├── chart.ts # charts v2 schema + ChartBlock mapper
├── blocks.ts # the 7 extended document blocks (table, image, link, toc, barcode, svg, formField)
├── layout.ts # pageSize / margins / header & footer templates / compress / debug / encrypt / typography (PdfLayoutOptions)
├── table.ts, barcode.ts, form.ts, image.ts # bodies shared by a dedicated tool and its inline block
├── watermark.ts # text and/or image watermark + position, PDF/A-1b transparency guard
├── encryption.ts # password + encrypt schema (Standard Security Handler), decrypt error mapping
├── inflate-cap.ts # PDFNATIVE_MCP_MAX_INFLATE_BYTES (engine decompression cap) + PDF_PARSE_FAILED mapping
├── output.ts # sandboxed file writer / base64 emitter (single + multi)
├── text.ts # newline sanitizer (Safe PDF/A)
├── doc-features.ts # nested lists, outline, page labels, viewer prefs (+ print-dialog defaults)
├── pagetree.ts # page-tree error mapping (merge/split/extract)
├── crypto-provider.ts # node:crypto signing provider for DER keys (SHA-256/384/512); verification stays pure JS
├── projection.ts # verbosity / fields projection for the seven read tools
├── errors.ts # ToolError, SecurityError, GovernanceError
└── tools/
├── generate-basic-pdf.ts
├── inspect-layout.ts
├── add-barcode.ts
├── sign-pdf.ts
├── add-ltv.ts
├── timestamp-pdf.ts
├── update-metadata.ts
├── add-international-text.ts
├── add-table.ts
├── add-form.ts
├── read-form-fields.ts
├── fill-form.ts
├── add-chart.ts
├── embed-image.ts
├── inspect-pdf.ts
├── verify-pdf.ts
├── validate-pdf.ts
├── add-attachment.ts
├── extract-attachments.ts
├── extract-text.ts
├── merge-pdfs.ts
├── split-pdf.ts
├── extract-pages.ts
├── annotate-pdf.ts
├── encrypt-pdf.ts
├── decrypt-pdf.ts
├── draft-governance-issue.ts
└── prepare-signature-placeholder.ts
scripts/ # TypeScript run by tsx — the full table is in scripts/README.md
├── gate.ts # THE quality gate (npm run gate / gate:fast; --publish --require-all on release branches)
├── generate-samples.ts # npm run test:generate — the built server under TZ=UTC -> test-output/samples/
├── verify-samples.ts # npm run verify:samples — the byte baseline (tests/_fixtures/samples.sha256.json)
├── generate-pdfa-corpus.ts # npm run corpus:pdfa — the 41-file PDF/A + PDF/X-4 conformance corpus
├── validate-pdfa.ts # npm run validate:pdfa — veraPDF (PASS/FAIL/XFAIL/XPASS; exit 0 / 1 / 2)
├── validate-pdfx.ts # npm run validate:pdfx — in-process structural PDF/X-4 check, never skips
├── tool-shape.ts # structural tools/list fingerprint (--write refreshes tests/_fixtures/tool-shape.json)
├── verify-docs.ts # npm run verify:docs — the docs held to docs/assets/ecosystem.json and the source tree
├── release-prepare.ts # mechanical version bump (never commits, tags or publishes)
├── build-claude-rules.ts # npm run agents:rules — .github/instructions/ -> .claude/rules/
└── verify-issue.mjs # governance draft checker (npm run verify:issue)
docs/
├── AGENT_CONTRACT.md # the consumer contract for agents that USE the server (catalogue, decision tree, recipes, error codes)
├── AI_GUIDE.md, KNOWLEDGE_BASE.md, API_STABILITY.md
├── guides/ # PDFA, PRINT, TYPOGRAPHY, REPRODUCIBLE, CHARTS, FORMS, ENCRYPTION, LTV, AI_GOVERNANCE, LOCAL_TESTING
└── assets/ecosystem.json # the single source of every count and version quoted in the docs
examples/ # 43 executable tools/call sequences (npm run examples:check)
AGENTS.md, CLAUDE.md # repository rules for coding agents; .github/instructions/ holds the per-area rules
.claude/ # shared agent settings, the fail-closed guard hook, generated rules, the release-audit skill
.github/workflows/ # ci (Node 22 / 24 on Linux, plus Windows and macOS, all running the gate, all required), publish, sample-regression, verapdf (blocking),
# docs, codeql, scorecard, dependency-review, audit
.github/rulesets/ # branch and tag rulesets to import
tests/ # vitest suites (one per tool / module), _fixtures/ (tool shape, sample baseline, engine-surface matrix,
# the frozen 1.5.0 catalogue), tools/ (repository tooling)🗺 Roadmap
v1.8.0 is shipped (spot colours, right-to-left documents, 32 Unicode scripts, JPEG 2000, the PDF/X-4 + PDF/A-2 dual claim, link annotations, operator fonts, the output base URL, the pdfnative 1.9 release pipeline and fuzzing), on top of v1.7.0 (fine typography, CMYK colours, PDF/X-4, reproducible output on every host) and v1.6.0 (full engine coverage — 13 block kinds, layout options, inspect_layout — PAdES LTV ladder, print production, charts v2, update_metadata, MCP 2026-07-28). The full plan — released milestones, in-progress work, and long-term direction — lives in ROADMAP.md.
Still deferred:
redact_pdf— pdfnative has no content-removal API; an overlay-only "redaction" would create false security.HTTP page streaming — MCP 2026-07-28 still has no partial
structuredContent, so large results stay single-shot.A Thai above vowel + tone mark pair extracts in geometric order on untagged output (every other right-to-left or reordered run round-trips through
/ActualTextsince pdfnative 1.9; tagged output always does).Non-Latin-1 form values: the engine writes only the low byte of each UTF-16 code unit of a
fill_formvalue undernonWinAnsi: 'needAppearances', soΩor漢do not round-trip throughread_form_fields— pinned upstream.Fuzz corpora survive only as long as the GitHub Actions cache; a dedicated storage repository is the next step. The OpenSSF Best Practices badge needs the maintainer's registration (docs/OPENSSF_BEST_PRACTICES.md is the prepared self-assessment).
Have a feature idea? Open an issue or PR.
🧭 Client compatibility
Every tool schema is JSON Schema 2020-12 without $ref / $defs, so hosts that forward inputSchema to a function-calling API get self-contained schemas. Since v1.8.0 every array bound is 1000 or below, or exactly 5000 (the blocks, rows and pages arrays): llama.cpp's llama-server compiles the tool schemas of a request into one GBNF grammar and refused the former 2000-item bound of prepare_signature_placeholder (#65); tests/client-compat.test.ts holds the rule. The 28 schemas weigh about 83 k tokens with a Qwen tokenizer — a client that forwards every tool on every request can disable the ones it does not need (sign_pdf injects its own placeholder, so prepare_signature_placeholder is optional; inspect_layout is a dry run of generate_basic_pdf).
🌱 Ecosystem
pdfnative — the zero-dependency engine every feature here comes from.
pdfnative-cli — the terminal wrapper of the same engine, for pipelines and shell agents.
pdfnative-react — React renderer (JSX → pdfnative blocks).
zipnative and pkinative — the sibling zero-dependency engines (archives, PKI); the native family guide explains how the four fit together.
⭐ Star the project
If pdfnative-mcp is useful to you, please ⭐ this repository — and consider also starring the underlying engine Nizoka/pdfnative. Stars help others discover the project and motivate continued development.
🤝 Contributing
Contributions are very welcome. Please read CONTRIBUTING.md, check the open issues, and follow the code of conduct.
📄 License
MIT © 2026 Nizoka. Third-party material: THIRD-PARTY-NOTICES.md.
pdfnative-mcp is built on top of pdfnative and the Model Context Protocol TypeScript SDK.
Available Tools
28 toolsadd_attachmentAdd embedded file attachment (PDF/A-3, Factur-X)AIdempotent
PDF/A-3b document with embedded files — the tool for Factur-X / ZUGFeRD e-invoices (one XML attachment, relationship 'Source', mimeType application/xml) or any PDF carrying machine-readable side-files (≤ 8 MiB each). Body via blocks (same schema as generate_basic_pdf). Pair with embedFonts:true for a valid PDF/A-3 claim (strict:true fails otherwise). Read them back with extract_attachments. Print, metadata and creationDate options as on every document tool.
Inputs: title (required), attachments (required), debug, print, +18 more. Returns: Result of a PDF-producing tool. base64 mode: the PDF arrives as an embedded `re…
add_barcodeAdd barcode / QR codeAIdempotent
Single-page PDF with a barcode: qr (URLs, vCards, UTF-8 ≤ 4296 chars; ecLevel 'H' for print, 'M' default), code128 (ASCII SKUs), ean13 (12–13 digits, checksum auto), datamatrix (dense industrial marks), pdf417 (ID cards, boarding passes). data is the raw payload — never pre-encode. Typical: { format:'qr', data:'https://example.com', caption:'Scan me' }. PDF/A, print, metadata and creationDate options as on every document tool.
Inputs: data (required), format (required), pdfA, pdfx, +24 more. Returns: Result of a PDF-producing tool. base64 mode: the PDF arrives as an embedded `re…
add_chartAdd native vector chartAIdempotent
Single-page PDF with a native vector chart (pure path operators, no raster): bar, barH, stackedBar, stackedBarH, line (markers), area, scatter, pie, donut. Multi-series, legends, 'nice' ticks, gridlines, negatives; per-series xValues with xAxis.type 'linear' | 'time' (ISO-8601), secondary right axis (axis2 + yAxis:'right'), axis.scale 'log', dataLabels, labelStride / labelRotation. Pie/donut take exactly one series. Colours: hex ('#3366cc'), 'C M Y K' / [c, m, y, k] under a print intent, { spot, tint } / { inks } declared in print.spotColours; series[].colors gives one colour per value on the bar family (its length must equal values.length on every chart type — CHART_ERROR otherwise). Tagged /Figure + /Alt (auto when altText is omitted). Engine cross-field rules surface as CHART_ERROR with the remedy. Inside a longer document use a 'chart' block in generate_basic_pdf. PDF/A (pdfA + embedFonts:true), print, metadata and creationDate as on every document tool.
Inputs: series (required), chartType (required), axis, pdfA, +35 more. Returns: Result of a PDF-producing tool. base64 mode: the PDF arrives as an embedded `re…
add_formAdd interactive formAIdempotent
New PDF with an interactive AcroForm: text fields, text areas, checkboxes, radio buttons, dropdowns (data capture, surveys, fillable templates). To fill or flatten an EXISTING form use read_form_fields + fill_form. PDF/A, print, metadata and creationDate options as on every document tool.
Inputs: title (required), fields (required), pdfA, debug, +19 more. Returns: Result of a PDF-producing tool. base64 mode: the PDF arrives as an embedded `re…
add_international_textAdd international textAIdempotent
PDF rendering text in 32 scripts (Arabic, Hebrew, Thai, Lao, CJK, Devanagari, Bengali, Gujarati, Gurmukhi, Kannada, Malayalam, Odia, Tamil, Telugu, Sinhala, Tibetan, Khmer, Myanmar, Tai Tham, New Tai Lue, Tai Le, Cham, Ethiopic, Cyrillic, Greek, Georgian, Armenian, Vietnamese, …; ha / yo / ig / sw are aliases of latin with anchored combining marks), COLRv1 colour emoji and mathematical symbols ('math': ∀ ∃ √ ∑ ∫ ∞). BiDi (UAX #9), Arabic joining and complex-script shaping are automatic; input is NFC-normalised; newlines split paragraphs. lang is a code or an array for mixed runs (['ar','emoji'], ['latin','math']). Fonts are always embedded, so embedFonts does not exist here. PDF/A, print, metadata and creationDate options as on every document tool.
Inputs: lang (required), title (required), paragraphs (required), font, +22 more. Returns: Result of a PDF-producing tool. base64 mode: the PDF arrives as an embedded `re…
add_ltvEmbed LTV validation material (PAdES B-LT)A
PAdES B-LT: embed a Document Security Store (/DSS + per-signature /VRI) with the certificates and OCSP/CRL material future verifiers need ('LTV enabled' in Adobe Reader once the root is trusted). Ladder step 3: sign_pdf (profile:'pades', timestamp:true) → add_ltv → timestamp_pdf. mode 'online' (default) fetches through the OPERATOR revocation provider (PDFNATIVE_MCP_REVOCATION + PDFNATIVE_MCP_NETWORK_ALLOWED_HOSTS; REVOCATION_NOT_CONFIGURED otherwise — no network without it); mode 'offline' embeds caller-supplied DER certificates / OCSP responses / CRLs with zero network. Incremental (existing /DSS merged). Needs ≥ 1 signed signature; unencrypted PDFs only.
Inputs: pdfBase64 (required), mode, match, outputMode, +6 more. Returns: Result of a PDF-producing tool. base64 mode: the PDF arrives as an embedded `re…
add_tableAdd table / reportAIdempotent
Tabular PDF report from headers + rows (every row the same length). Smart-table options: wrap, repeatHeader (header on every page), zebra, caption (tagged for PDF/A), minRowHeight, cellPadding, cellBorders, cellVAlign, autoFitColumns, clipCells. Inside a longer document use a 'table' block of generate_basic_pdf instead. PDF/A (pdfA + embedFonts:true), print, metadata, watermark and creationDate options as on every document tool.
Inputs: rows (required), title (required), headers (required), pdfA, +34 more. Returns: Result of a PDF-producing tool. base64 mode: the PDF arrives as an embedded `re…
annotate_pdfAnnotate PDF (markup / drawing)AIdempotent
Append markup annotations (ISO 32000-1 §12.5) to an existing PDF as a non-destructive incremental update (original bytes preserved). Types: text (sticky note), highlight | underline | strikeout | squiggly, square | circle, line (needs start/end), freetext, link (needs url — http:, https: or mailto:; the rect is the clickable area). Each needs a 0-based page and rect [x1,y1,x2,y2]; optional contents, color, opacity, title and type-specific fields. VISUAL OVERLAY ONLY — nothing is removed or redacted (covered text stays extractable). Encrypted sources → ENCRYPTED_SOURCE (decrypt_pdf first).
Inputs: pdfBase64 (required), annotations (required), outputMode, outputPath. Returns: Result of a PDF-producing tool. base64 mode: the PDF arrives as an embedded `re…
decrypt_pdfDecrypt a PDFAIdempotent
Emit an UNENCRYPTED copy of an encrypted PDF (RC4 V1–V4, AES-128, AES-256) given password (user or owner; empty user password needs none). CAVEAT: the page tree is rebuilt — signatures and AcroForm are DROPPED. To merely READ an encrypted PDF pass password to inspect_pdf / extract_text / extract_attachments / read_form_fields instead. Never cached.
Inputs: pdfBase64 (required), password, outputMode, outputPath. Returns: Result of a PDF-producing tool. base64 mode: the PDF arrives as an embedded `re…
draft_governance_issueDraft a governance-compliant GitHub issue (HITL)AIdempotent
Produce a LOCAL GitHub issue draft + compliance report for pdfnative / pdfnative-mcp and NEVER submit it — the agent drafts, a human reviews and files it under their own identity (Human-In-The-Loop contract, .github/AGENT_RULES.md). No GitHub write path exists. The draft is checked against the zero-dependency + reproduction policy; a violation (new runtime dependency, missing reproduction, duplicateSearchPerformed:false) throws GOVERNANCE_VIOLATION. Returns markdown inline (outputMode:'file' also writes a .md). Present the draft AND the report to the user, then STOP.
Inputs: title (required), summary (required), issueType (required), reproduction (required), +7 more. Returns: A local, unsubmitted issue draft plus its compliance report. The server NEVER o…
embed_imageEmbed image in PDFAIdempotent
PDF with one embedded JPEG, PNG or JPEG 2000 image (base64; PNG without alpha channel; JPEG 2000 passed through as /JPXDecode) plus optional caption and render width/height. Inside a longer document use an 'image' block of generate_basic_pdf instead. PDF/A, print, metadata and creationDate options as on every document tool.
Inputs: title (required), mimeType (required), imageBase64 (required), alt, +25 more. Returns: Result of a PDF-producing tool. base64 mode: the PDF arrives as an embedded `re…
encrypt_pdfEncrypt / re-secure a PDFA
Encrypt an existing PDF with the Standard Security Handler: AES-128 (default) or AES-256; RC4 is never emitted. ownerPassword required; optional userPassword (open password), permissions { print, copy, modify, extractText }. Rotate the password of an already-encrypted source by passing its current password. CAVEAT: the page tree is rebuilt — signatures and AcroForm are DROPPED, only URI links kept; encrypt BEFORE signing. Never cached.
Inputs: pdfBase64 (required), ownerPassword (required), password, algorithm, +4 more. Returns: Result of a PDF-producing tool. base64 mode: the PDF arrives as an embedded `re…
extract_attachmentsExtract embedded files from PDFARead-onlyIdempotent
Read-only extraction of embedded files (PDF/A-3, Factur-X, ZUGFeRD): name, mimeType, AFRelationship, description, sizeBytes and (by default) the payload as dataBase64. filename selects one file; includeData:false probes metadata only. Encrypted sources: pass password. Token-frugal: verbosity:'summary'.
Inputs: pdfBase64 (required), fields, filename, password, +2 more. Returns: Structured attachment-extraction result. Every property is optional because the…
extract_pagesExtract pages into one PDFAIdempotent
Keep an arbitrary pages[] subset (0-based, in the given order) in ONE fresh PDF. Same page-tree caveats as merge_pdfs (signatures/AcroForm/XMP dropped; boxes kept; URI links unless dropAnnotations). Encrypted sources: password; output unencrypted unless encrypt. Need several documents? Use split_pdf.
Inputs: pages (required), pdfBase64 (required), encrypt, password, +4 more. Returns: Result of a PDF-producing tool. base64 mode: the PDF arrives as an embedded `re…
extract_textExtract plain text from PDFARead-onlyIdempotent
Plain-text extraction: decodes Tj/TJ operands through each font's /ToUnicode CMap, /Encoding /Differences or base encoding and returns pages[] + fullText (positioned runs with includeRuns:true; pages selects 0-based pages). extractable is false only when a page decoded ENTIRELY to U+FFFD (a font with no usable mapping — expected for some subset fonts, not an error; extractableReason explains). Tagged content carrying /ActualText (every pdfA document this server writes) returns that SOURCE text, in logical order; untagged right-to-left and reordered shaped runs (Arabic, Hebrew, Indic, Khmer, Myanmar, …) carry /ActualText since pdfnative 1.9 and come back in logical order too — only a Thai above-vowel + tone-mark pair still reads in drawn order untagged. onStreamError: 'limit' (default) fails only on a stream over the decompression cap (PDF_PARSE_FAILED), 'throw' fails on any undecodable stream, 'skip' returns the page without that stream's text. Encrypted sources: pass password (PASSWORD_REQUIRED / PASSWORD_INVALID otherwise). Token-frugal: verbosity:'summary', fields:[…].
Inputs: pdfBase64 (required), pages, fields, password, +4 more. Returns: Structured text-extraction result. Every property is optional because the resul…
fill_formFill / flatten an existing AcroFormAIdempotent
Fill and/or flatten the AcroForm of an EXISTING PDF (add_form creates one) as an incremental update — prior signatures stay valid for their revision. values: fully-qualified name → string (array for multi-select), boolean or export state for checkbox/radio. flatten:true stamps appearances and drops the interactive layer (with no values = pure flatten). Unknown names → FORM_FIELD_NOT_FOUND unless onUnknownField:'ignore'; signature fields cannot be filled (FORM_UNSUPPORTED). Encrypted sources: password.
Inputs: pdfBase64 (required), values, flatten, password, +4 more. Returns: Result of a PDF-producing tool. base64 mode: the PDF arrives as an embedded `re…
generate_basic_pdfGenerate basic PDFAIdempotent
Multi-page PDF composed from ordered blocks: heading, paragraph, list, table, image (JPEG/PNG/JPEG 2000), link, toc (printed table of contents), barcode, svg, formField, chart, pageBreak, spacer — every block kind the engine offers, each sharing its body with the dedicated tool (add_table, embed_image, add_barcode, add_form, add_chart). DEFAULT for any document; use the dedicated tools only for a standalone artefact, add_attachment for embedded files and add_international_text for non-Latin scripts. pdfA + embedFonts:true for a valid PDF/A-1b/2b/2u/3b claim; outline / pageLabels / viewerPreferences / watermark for navigation and presentation; print, metadata, outputIntent, creationDate as on every document tool. The 'chart' block takes the same body as add_chart.
Inputs: title (required), blocks (required), font, pdfA, +26 more. Returns: Result of a PDF-producing tool. base64 mode: the PDF arrives as an embedded `re…
inspect_layoutInspect document layout (dry run)ARead-onlyIdempotent
Pagination preview WITHOUT generating a PDF: page count and where every block lands (page, x, top, width, height in points). Same blocks as generate_basic_pdf plus every input that moves a block (title, footerText, pdfA, normalize, embedFonts, pageSize, margins, headerTemplate, footerTemplate): pass what you will give generate_basic_pdf and the page count matches — the dry run shares the pagination planner of the builder, toc blocks included. Read-only, deterministic. Token-frugal: verbosity:'summary', fields:['totalPages'].
Inputs: title (required), blocks (required), font, pdfA, +12 more. Returns: Structured layout preview. Every property is optional because the result can be…
inspect_pdfInspect PDF metadataARead-onlyIdempotent
Read-only facts about a PDF: version, pageCount, encryption, pdfA claim (the claim, not its validity — use veraPDF for that), signatureCount, hasSignaturePlaceholder, attachments[], info; presence-gated dss / docTimestampCount / trapped; signatures:true lists every field (subFilter, isDocTimestamp, isPlaceholder, byteRange, vriKey); pages:true adds per-page sizes and boxes. check:[…] turns it into a CI assertion ('pdfa','signed','encrypted','placeholder','attachments','dss','docTimestamp','trapped') → checks (requested keys only) + checksPassed. Encrypted sources: pass password. Token-frugal: verbosity:'summary', fields:[…].
Inputs: pdfBase64 (required), check, pages, fields, +4 more. Returns: Structured inspection result. Every property is optional because the result can…
merge_pdfsMerge PDFsAIdempotent
Concatenate 2–50 PDFs (pdfsBase64[]) into one fresh, self-contained document. Page-tree rebuild: signatures and AcroForm are dropped, XMP (and thus a PDF/A claim) does not survive — re-declare PDF/A on the generating tools; page boxes and /UserUnit do survive; URI links kept unless dropAnnotations:true. Encrypted sources open with one password (PASSWORD_REQUIRED / PASSWORD_INVALID); output unencrypted unless encrypt. Guards: 256 MiB assembly (maxOutputSizeBytes), 50 MiB output (OUTPUT_TOO_LARGE).
Inputs: pdfsBase64 (required), encrypt, password, outputMode, +3 more. Returns: Result of a PDF-producing tool. base64 mode: the PDF arrives as an embedded `re…
prepare_signature_placeholderPrepare signature placeholderAIdempotent
New PDF carrying an unsigned /Sig placeholder for a LATER sign_pdf call. OPTIONAL — sign_pdf auto-injects one. Use it to size the placeholder (placeholderBytes for > 4096-bit keys, reserveTimestamp for an RFC 3161 token), to pin the widget page (pageIndex), to choose subFilter 'ETSI.CAdES.detached' (PAdES), or to ship the placeholder separately. signerName/reason/location/contactInfo/signingTime are frozen into /Sig here — sign_pdf cannot rewrite them later. NOTE: the unsigned file is not yet a conformant PDF/A (empty /Contents); it becomes one once signed. PDF/A, print, metadata and creationDate options as on every document tool.
Inputs: title (required), pdfA, debug, print, +27 more. Returns: Result of a PDF-producing tool. base64 mode: the PDF arrives as an embedded `re…
read_form_fieldsRead AcroForm fieldsARead-onlyIdempotent
Read-only list of an existing PDF's AcroForm fields: fully-qualified name, type (text | checkbox | radio | dropdown | listbox | button | signature | unknown), value, flags (readOnly / required / multiline), options, widget placements. Call it BEFORE fill_form to learn the names. Encrypted sources: password. Token-frugal: verbosity:'summary' → { fieldCount }.
Inputs: pdfBase64 (required), fields, password, verbosity. Returns: Structured AcroForm field listing. Every property is optional because the resul…
sign_pdfSign PDF (RSA / ECDSA, PAdES)A
CMS / PAdES signature in ONE call: a missing /Sig placeholder is auto-injected (prepare_signature_placeholder is optional). Inputs: pdfBase64, algorithm (rsa-sha256/384/512, ecdsa-sha256 P-256), certDerBase64 (+ certChainDerBase64 intermediates) and the DER key (rsaKeyPkcs1DerBase64 for rsa-*, ecPrivateKeyDerBase64 or ecPrivateScalarHex for ECDSA; PEM is rejected with the openssl remedy). profile:'pades' (ETSI EN 319 142-1 baseline) is the right choice when add_ltv / timestamp_pdf follow; timestamp:true = PAdES B-T through the operator TSA (TSA_NOT_CONFIGURED otherwise, no network without it). signerName/reason/location/contactInfo/signingTime are baked into the placeholder THIS call injects (a pre-built placeholder keeps its own). Several unsigned placeholders → pass fieldName (PLACEHOLDER_AMBIGUOUS); allowMultiple:true + a new fieldName adds a further signature. Verify with verify_pdf.
Inputs: algorithm (required), pdfBase64 (required), certDerBase64 (required), reason, +15 more. Returns: Result of a PDF-producing tool. base64 mode: the PDF arrives as an embedded `re…
split_pdfSplit PDF into rangesAIdempotent
Split one PDF into several documents, one per ranges[] entry ({ start, end? }, 0-based inclusive; end defaults to start). Multi-output result: base64 mode returns one embedded resource per part; file mode writes indexed siblings ('out.pdf' → 'out-1.pdf', 'out-2.pdf', …). Same page-tree caveats as merge_pdfs (signatures/AcroForm/XMP dropped; boxes kept). Encrypted sources: password. Need ONE document from a page subset? Use extract_pages.
Inputs: ranges (required), pdfBase64 (required), encrypt, password, +4 more. Returns: Structured result of a tool producing several PDFs. In base64 mode each PDF is…
timestamp_pdfAdd a document timestamp (PAdES B-LTA)A
PAdES B-LTA: append an RFC 3161 document timestamp (/DocTimeStamp, ETSI.RFC3161) over the whole document. Ladder step 4 after add_ltv; re-run before the TSA certificate expires. fieldName omitted → DocTimeStamp1, 2, …; a fieldName colliding with a signed field fails. Uses the OPERATOR TSA (PDFNATIVE_MCP_TSA_URL; TSA_NOT_CONFIGURED otherwise, no network without it). The token's status, imprint and nonce are checked before embedding (its own signature is verified by verify_pdf). Unencrypted PDFs only.
Inputs: pdfBase64 (required), fieldName, outputMode, outputPath, +1 more. Returns: Result of a PDF-producing tool. base64 mode: the PDF arrives as an embedded `re…
update_metadataUpdate document metadataA
Rewrite /Info (title, author, subject, keywords) of an EXISTING PDF as an incremental update; XMP stays in sync on PDF/A documents; /ModDate is refreshed (pin modDate for reproducible bytes). Earlier revisions and signatures stay byte-identical — the new revision is unsigned (sign_pdf / timestamp_pdf again if needed). Encrypted sources → ENCRYPTED_SOURCE. For metadata at generation time use the metadata option of the document tools.
Inputs: pdfBase64 (required), title, author, modDate, +4 more. Returns: Result of a PDF-producing tool. base64 mode: the PDF arrives as an embedded `re…
validate_pdfValidate PDF structure (PDF/UA, PDF/X-4)ARead-onlyIdempotent
Read-only structural gate; standard picks the rule set. 'pdf-ua-1' (default, ISO 14289-1) for Tagged PDF: /MarkInfo /Marked, /StructTreeRoot (+ /ParentTree), XMP /Metadata, /Lang, per-page MCID uniqueness — generate tagged input with pdfA (e.g. 'pdfa2u') first. 'pdf-x-4' (ISO 15930-7) for print exchange: /GTS_PDFX OutputIntent with a prtr ICC profile, TrimBox or ArtBox, every font embedded, no annotation / JavaScript / embedded file / transfer function — generate it with pdfx:'pdfx4'. Result { standard, valid, errors[], warnings[], summary } (+ caveats[] for PDF/X). Fast and structural only — NOT a reference validator or a certified preflight: fonts, colour and rendering are not checked (veraPDF for PDF/A and PDF/UA; callas pdfToolbox or Acrobat Preflight for PDF/X). Unparsable input → PDF_PARSE_FAILED.
Inputs: pdfBase64 (required), fields, standard, verbosity. Returns: Structured PDF/UA or PDF/X-4 validation result. Every property is optional beca…
verify_pdfVerify PDF signaturesARead-onlyIdempotent
Read-only verification of every signature: ByteRange digest vs CMS messageDigest (integrity), CMS signatureValue vs the embedded signer certificate (RSA-SHA256/384/512, ECDSA P-256), chain trust when trustedRootsDerBase64 is given ('self-signed' | 'unverified' | 'trusted' otherwise). /DocTimeStamp entries are verified as RFC 3161 tokens and count in allValid like any signature. Result: { allValid, signatureCount, summary, signatures:[{ valid, integrity, signerSubject, signingTime, chainTrust, errors[] }] }. ltv:true adds the PAdES view: per-signature profile, timestamp, revocation (read from embedded /DSS only — responder signatures are not re-verified) and ltvLevel B-B / B-T / B-LT / B-LTA with explicit caveats. Token-frugal: verbosity:'summary', fields:['allValid'].
Inputs: pdfBase64 (required), ltv, fields, password, +2 more. Returns: Structured signature verification result. Every property is optional because th…
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 28 tools
Most tools have sharply distinct purposes, and the descriptions actively resolve the obvious overlaps: generate_basic_pdf is declared the default while add_table/add_chart/embed_image/add_barcode are scoped to standalone artefacts, split_pdf vs extract_pages is cross-referenced, and add_form vs read_form_fields vs fill_form are clearly separated. The only soft spot is the cluster of read-only inspectors (inspect_pdf, inspect_layout, validate_pdf, verify_pdf), whose names alone would be confusable, though their descriptions disambiguate well.
All names are snake_case with a leading verb (generate_basic_pdf, extract_text, merge_pdfs, sign_pdf, validate_pdf, verify_pdf), which is predictable. The main deviation is the create-family split between generate_/embed_ and the add_* prefix (add_table, add_form, add_chart, add_barcode, add_attachment, add_ltv), plus a few oddities like timestamp_pdf and prepare_signature_placeholder.
At 28 tools this is on the heavy side, well beyond the comfortable 3–15 band. The PDF domain is genuinely broad (generation, extraction, page ops, forms, signatures, LTV, validation), so each tool does have a distinct job, but the surface is large enough that agents must scan a long list to find the right entry point.
Coverage is unusually thorough: creation from blocks, standalone artefact builders, extraction, merge/split/subset, annotation, encrypt/decrypt, form read/fill, signing with placeholder, DSS/LTV, document timestamps, structural validation and signature verification form a coherent lifecycle. Remaining gaps are true redaction (explicitly called out as out of scope for annotate_pdf) and page-level editing like rotation/reordering in place.
Maintenance
Related MCP Connectors
PDF and image tools for AI agents: compress, merge, split, OCR, sign. Runs locally, never uploads.
PDF tools for AI agents: validate, extract, merge, split, compress, OCR, sign and redact.
PDF tools for AI agents: inspect, fill, merge, split, rotate, watermark, protect and invoice PDFs.
Generate and read PDFs for AI agents: a generate_pdf and a read_pdf tool, priced per document.
Related MCP Servers
- AlicenseAqualityDmaintenanceGenerate professional PDFs from Claude, Cursor, and other AI tools. Create invoices, contracts, reports, and certificates from templates or inline HTML markup.759 npm1MIT
- FlicenseNot gradedqualityBmaintenanceEnables AI agents to perform comprehensive PDF operations locally, including compression, text extraction, PII redaction, page organization, splitting, merging, watermarking, creation, and form filling, all without cloud uploads.21 npm-
- AlicenseNot gradedqualityAmaintenanceEnables AI agents to create and process office documents locally, including generating branded PDF, Word, Excel, and editable PowerPoint files from templates and themes, plus reading, OCR, form-filling, manipulation, and conversion of PDFs.1MIT
- AlicenseBqualityBmaintenanceEnables AI agents to perform local-first PDF processing entirely on-device, including OCR write-back to make scans searchable, unlocking and re-encrypting PDFs, splitting/merging/rotating pages, and compressing files, with no cloud calls or file uploads.251MIT