Skip to content

v1.6.0 - Full engine coverage, PAdES LTV, print production, charts v2, MCP 2026-07-28, pdfnative 1.7

Latest

Choose a tag to compare

@Nizoka Nizoka released this 23 Aug 16:29

pdfnative-mcp v1.6.0

Released 2026-08-23

v1.6.0 aligns the server with pdfnative v1.7.0
and the MCP 2026-07-28 specification, and grows the catalogue to 28 tools.
It exposes the engine's full document model — all 13 block kinds in
generate_basic_pdf, page layout options on every document tool, build-time
encryption, image watermarks, an annotation inventory and the read-only
inspect_layout pagination preview — completes the PAdES baseline ladder
(B-B → B-T → B-LT → B-LTA) with sign_pdf timestamps, add_ltv and
timestamp_pdf, adds print production (page boxes, bleed, printer's marks,
/Trapped, /UserUnit, custom OutputIntent), charts v2 (stacked / area / scatter,
secondary axis, log and time scales), PDF/A conformance diagnostics with an honest
embedFonts switch, and update_metadata. The MCP transport moves to the SDK v2
line: 2026-07-28 clients get the stateless protocol (server/discover,
resultType, cache hints), while 2025-era hosts keep working through the automatic
legacy fallback.

No breaking changes to the tool API: every existing call keeps its inputs, error
codes and response shape, and default outputs are byte-identical except where
pdfnative 1.7.0 corrected previously-wrong output (listed under Upgrade). All new
behaviour is opt-in; a superset gate against the published 1.5.0 catalogue proves
it.

Highlights

  • Full engine coveragegenerate_basic_pdf now composes every DocumentBlock pdfnative offers (table, image, link, toc, barcode, svg, formField join heading, paragraph, list, chart, pageBreak, spacer); pageSize, margins, running headerTemplate / footerTemplate, compress and debug on the nine document tools; build-time encrypt that keeps the AcroForm; image watermarks with position; inspect_pdf annotations: true; and the 28th tool, inspect_layout, a pagination dry run with no PDF produced. See Full engine coverage below.
  • PAdES long-term validationsign_pdf gains profile: 'pades', timestamp: true (RFC 3161, B-T), RSA-SHA384/512, fieldName / allowMultiple for several signatures and certChainDerBase64; new add_ltv embeds /DSS + /VRI (online through the operator-configured revocation provider or offline from caller-supplied material, B-LT); new timestamp_pdf appends a /DocTimeStamp (B-LTA). verify_pdf ltv: true reports profile, timestamp, embedded revocation status and the achieved level. New guide: docs/guides/LTV.md.
  • Network charter, mirrored from pdfnative — the server still makes no outbound request by default. The only egress it can ever perform goes to the TSA / OCSP / CRL endpoints the operator configures (PDFNATIVE_MCP_TSA_URL, PDFNATIVE_MCP_REVOCATION, PDFNATIVE_MCP_NETWORK_ALLOWED_HOSTS); URLs never come from tool arguments, and certificate-supplied OCSP/CRL URLs pass an SSRF guard (allow-list, http(s) only, no credentials, no redirects, internal address literals rejected, size caps, timeouts).
  • MCP 2026-07-28 — SDK v2 (@modelcontextprotocol/server): stateless per-request envelope, server/discover, resultType, ttlMs / cacheScope hints (catalogue 24 h public, resources private), per-result serverInfo, Mcp-Method / Mcp-Name headers, resource-not-found -32602, deterministic tools/list. Legacy initialize clients (2025-11-25 / 06-18 / 03-26) are served by the same binary on stdio and HTTP.
  • Print productionprint (TrimBox / BleedBox / ArtBox / CropBox, bleed shorthand, crop + registration marks, /UserUnit), metadata (/Author, /Subject, /Keywords, /Trapped with XMP parity), outputIntent (custom RGB ICC) and print-dialog viewerPreferences on every document tool; boxes survive merge_pdfs / split_pdf / extract_pages and are reported by inspect_pdf. New guide: docs/guides/PRINT.md.
  • Charts v2stackedBar / stackedBarH / area / scatter, per-series xValues with linear or UTC-deterministic time axes, a secondary right axis (axis2), axis.scale: 'log', dataLabels, labelStride / labelRotation.
  • Honest PDF/A — text rendered through the viewer's base-14 Helvetica is not embedded, so a PDF/A claim on such a file is rejected by veraPDF. embedFonts: true embeds Noto Sans Latin for a valid claim; strict: true fails instead of producing a non-conformant file; includeDiagnostics: true echoes the engine's diagnostics. The PDF/A guide is corrected accordingly, and an advisory veraPDF corpus runs locally (npm run validate:pdfa) and in CI.
  • update_metadata — rewrite /Info (+ XMP) of an existing PDF as an incremental update.

Full engine coverage

Every option of pdfnative 1.7's document builder that a thin wrapper can expose honestly is now reachable. All of it is opt-in and absent by default, so default output stays byte-identical; tests/catalogue-superset.test.ts compares the live catalogue with the frozen 1.5.0 one and fails on any removal or narrowing.

  • 13 block kinds in generate_basic_pdf — new table, image, link, toc, barcode, svg, formField blocks (src/blocks.ts), each sharing its body with the dedicated tool (add_table, embed_image, add_barcode, add_form) so a standalone artefact and an inline block validate and render identically. Rules: link accepts http: / https: / mailto: only, control characters (C0, DEL and C1) rejected; image ≤ 12 M base64 characters each and a 24 MiB decoded budget per call — PNGs must be 8-bit, non-interlaced, without alpha or palette (the IHDR is read at the boundary and the unsupported variants fail with VALIDATION_ERROR and a remedy instead of an opaque GENERATION_FAILED); svg (≤ 100 000 characters) covers <path>, <rect>, <circle>, <ellipse>, <line>, <polyline>, <polygon>, <text>/<tspan> and silently ignores transform, <g>, <use>, <image>, gradients, opacity and CSS — no XML parser, no external reference ever fetched, entities limited to &amp; &lt; &gt; &quot; &apos; &nbsp; &#n;; toc prints a contents page from the heading blocks with internal /GoTo links and pairs with outline: 'auto'; formField under a PDF/A claim reports PDFA_UNEMBEDDED_FORM_FONT; barcode has no alt (engine limitation). Documents that expand to more than 50 000 engine blocks after newline splitting fail with VALIDATION_ERROR and an agent remedy (split the document) rather than the engine's GENERATION_FAILED naming an option the agent cannot set.
  • Layout options on the nine document tools (src/layout.ts) — pageSize (A4 default, Letter, Legal, A3, Tabloid; print.* boxes must fit it), margins (all four, 0–200 pt, default 45 / 36 / 35 / 36), headerTemplate / footerTemplate (left / center / right zones, fontSize, color; placeholders {page} {pages} {title} {date}) — a footerTemplate replaces the default footer so footerText is then ignored, and {date} is the build-day wall clock, not creationDate (avoid it for stable bytes; a cache hit returns the earlier date); compress (FlateDecode streams — smaller file, different bytes; the XMP packet stays plain under PDF/A); debug (margin / block / cell guide rectangles as unmarked content — not for PDF/UA output).
  • Encryption at build timeencrypt on 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 (an encrypted fillable form was unreachable before — encrypt_pdf rebuilds the page tree and drops it). Exclusive with pdfA (VALIDATION_ERROR, ISO 19005-1 §6.3.2); randomised bytes, never cached. Deliberately not offered on prepare_signature_placeholder (the output must stay signable) or add_attachment (PDF/A-3).
  • inspect_layout (28th tool, read-only) — wraps inspectDocumentLayout: totalPages, pageWidth / pageHeight, margins and each block's page / x / top / width / height (2-decimal points) for the same blocks plus every input that moves a block (title, footerText, pdfA, normalize, embedFonts, pageSize, margins, headerTemplate, footerTemplate); verbosity: 'summary' yields { pageWidth, pageHeight, totalPages, blockCount }. Known engine gap: a toc block is measured as 0 pt, so a document with a printed contents may paginate one page later than previewed (stated in the tool description, pinned by a test).
  • inspect_pdf annotations: trueannotations[] with 0-based page, subtype, rect, and when present contents (truncated to 200 characters), title, color, quadPoints, link url; annotationCount; new check: 'annotations'.
  • Image watermarkswatermark.image (JPEG / PNG through the shared decoder, ≤ 12 M base64 characters, own 8 MiB decoded cap, opacity default 0.10, width / height) on generate_basic_pdf and add_table, alone or with text (default opacity 0.15); watermark.position: 'background' | 'foreground' for both. text is no longer required. Either opacity below 1.0 is rejected under pdfa1b.
  • Forms and imagesadd_form and formField blocks gain fieldType: 'listbox' and placeholder; embed_image gains align and alt (tagged /Figure /Alt). embed_image.imageBase64 keeps its unbounded 1.5.0 contract; the 12 M-character cap applies to inline image blocks and watermark images only.
  • PDFNATIVE_MCP_MAX_INFLATE_BYTES (src/inflate-cap.ts) — operator override of the engine's 100 MiB per-stream decompression cap (integer ≥ 1024, read once at startup; an invalid value refuses to start with one clean fatal line). A capped attachment stream fails extract_attachments includeData: true with PDF_PARSE_FAILED and a remedy; extract_text degrades to empty page text for a capped content stream (engine behaviour — the per-page decode failure is swallowed upstream; candidate upstream issue).
  • New PDF/A diagnosticsPDFA_UNEMBEDDED_FORM_FONT (any form field under a PDF/A claim) and PDFA_DEVICE_CMYK_IMAGE (CMYK JPEG against the sRGB OutputIntent) alongside PDFA_NO_FONT_ENTRIES; visible with includeDiagnostics, fatal with strict.
  • Deliberately unexposed engine options (recorded in docs/KNOWLEDGE_BASE.md): the LayoutDebugOptions object form, WatermarkText.autoFit, per-block font / colour overrides, TableBlock.columns, PdfParams.columns / colors / fontSizes, maxBlocks — one tool surface, byte-identical defaults, agent-facing simplicity.
  • Corpuscomposite-blocks-pdfa2b.pdf (every block kind under PDF/A-2b) and layout-letter-templates-compress-pdfa2b.pdf (Letter, templates, compress) join the veraPDF corpus (26 files, 24 validated).

Added

  • feat(tool): inspect_layout — read-only pagination dry run (see Full engine coverage); the seventh tool with verbosity / fields.
  • feat(tool): add_ltv — PAdES B-LT Document Security Store; mode: 'online' (operator revocation provider, REVOCATION_NOT_CONFIGURED otherwise) or mode: 'offline' (parse-validated caller material); summary in the structured result.
  • feat(tool): timestamp_pdf — PAdES B-LTA document timestamp through the operator TSA (TSA_NOT_CONFIGURED otherwise); the token's status, message imprint and nonce are checked before embedding (its own CMS signature is verified by verify_pdf), auto-suffixed field names for periodic re-timestamping.
  • feat(tool): update_metadata — incremental /Info + XMP rewrite (title / author / subject / keywords / pinned modDate); encrypted sources rejected.
  • feat(sign): sign_pdfprofile, timestamp, algorithm rsa-sha384 / rsa-sha512, certChainDerBase64, fieldName, allowMultiple; errors PLACEHOLDER_AMBIGUOUS, SIGNATURE_FIELD_NOT_FOUND, TSA_REJECTED.
  • feat(sign): prepare_signature_placeholdersubFilter, reserveTimestamp.
  • feat(verify): verify_pdf/DocTimeStamp entries verified as RFC 3161 tokens, per-signature subFilter, opt-in ltv view (profile, timestamp, revocation, ltvLevel, document-level dss / ltvLevel / caveats).
  • feat(inspect): inspect_pdfsignatures: true inventory (subFilter, isDocTimestamp, isPlaceholder, byteRange, vriKey), presence-gated dss / docTimestampCount / trapped, page boxes + userUnit under pages: true, check values dss / docTimestamp / trapped.
  • feat(print): print, outputIntent, metadata, strict, includeDiagnostics, embedFonts on generate_basic_pdf, add_table (both backends), add_international_text (no embedFonts — fonts are always embedded), add_chart, add_barcode, embed_image, add_form, add_attachment, prepare_signature_placeholder; viewerPreferences gains duplex, pickTrayByPDFSize, printPageRange, numCopies. Errors PRINT_ERROR, GENERATION_FAILED; PDF_A_COMPLIANCE_VIOLATION also covers strict diagnostics and userUnit under pdfa1b.
  • feat(chart): charts v2 fields on add_chart and the chart block of generate_basic_pdf; engine cross-field rules surface as CHART_ERROR with the remedy.
  • feat(mcp): MCP 2026-07-28 on @modelcontextprotocol/server ^2.0.0 with legacy fallback; SERVER_CACHE_HINTS; dependency-free node:http bridge (src/http.ts); stdio smoke and 2026-era HTTP conformance tests.
  • feat(network): src/network.ts — operator-configured TSA / OCSP / CRL providers with the SSRF guard; env vars PDFNATIVE_MCP_TSA_URL, PDFNATIVE_MCP_TSA_AUTH, PDFNATIVE_MCP_REVOCATION, PDFNATIVE_MCP_NETWORK_ALLOWED_HOSTS, PDFNATIVE_MCP_NETWORK_TIMEOUT_MS; errors TSA_NOT_CONFIGURED, REVOCATION_NOT_CONFIGURED, NETWORK_HOST_NOT_ALLOWED, NETWORK_ERROR, LTV_NO_SIGNATURE, LTV_EMPTY, LTV_MATERIAL_INVALID, LTV_ERROR, METADATA_ERROR.
  • feat(validation): advisory veraPDF corpus — npm run corpus:pdfa / npm run validate:pdfa (pinned veraPDF 1.30.2, Windows .bat launcher supported) and the non-blocking verapdf.yml workflow. See Hardening and behaviour changes for the fail-closed mode, the negative canaries and the 26-file corpus.
  • feat(env): PDFNATIVE_MCP_MAX_INFLATE_BYTES — operator-set engine decompression cap (see Full engine coverage).
  • test: offline mock PKI (tests/_ltv-fixtures.ts), loopback RFC 3161 server (tests/_tsa-server.ts), in-memory MCP harness (tests/_mcp-harness.ts), HTTP fixture; new suites document-blocks, layout-options, inspect-layout, watermark, inflate-cap, catalogue-superset, error-codes; coverage thresholds raised to 89 / 80 / 90 / 91 (test counts under Hardening and behaviour changes).
  • docs: guides LTV.md, PRINT.md; examples pades-ltv-ladder, ltv-offline, multi-signature, update-metadata, stacked-bar-chart, dual-axis-time-chart, scatter-log-chart, print-bleed-marks, pdfa-embed-fonts, inspect-layout.

Changed

  • chore(deps): pdfnative ^1.6.0^1.7.0 (additive, 0 removed exports). Free wins: colour-emoji flag and ZWJ sequences render as single glyphs; Arabic ALEF joining and Persian forms, RTL digit order and paired-delimiter mirroring are now UAX #9 conformant; incremental writer / xref reader hardening.
  • chore(deps): MCP SDK @modelcontextprotocol/sdk ^1.29 → @modelcontextprotocol/server ^2.0.0 (+ @modelcontextprotocol/core); zod ^4.2.0. Still three runtime dependencies; the transitive tree no longer contains hono / express / jose.
  • chore(api): _meta.apiVersion 1.5.01.6.0; package / server versions → 1.6.0.
  • chore(governance): the charter keeps "no GitHub write path" and "no telemetry" absolute and now states the single permitted egress class: operator-configured TSA / OCSP / CRL endpoints, never a URL from a tool argument (ai-governance.json, AGENT_RULES.md, MCP governance prompts).
  • chore(http): GET / DELETE on /mcp answer 405 (stateless serving, no SSE resumability — unchanged for the stateless mode v1.5 already used); Host / Origin loopback guard kept.

Fixed

  • fix(sign): signerName / reason / location / contactInfo / signingTime never reached the /Sig dictionary — pdfnative < 1.7 dropped the values passed at placeholder time and wrote /M = now. They are now baked into the placeholder (sign_pdf when it injects one, prepare_signature_placeholder always), so verify_pdf reports them and a pinned signingTime lands in /M (a pre-built placeholder keeps its own /M).
  • fix(verify): a /DocTimeStamp field was parsed as a CMS signature and flipped allValid to false on every B-LTA document; document timestamps are now verified as RFC 3161 tokens.
  • fix(form): add_form fieldType: 'textarea' (and the formField block) now maps to the engine's multilineText. 1.5.0 passed the string through unmapped, so the widget rendered as a plain single-line text field; the output now carries /Ff 4096. This changes bytes for that one input (recorded under docs/API_STABILITY.md §5 Bug fixes that change bytes).
  • fix(docs): the PDF/A guide claimed every font was embedded; base-14 Helvetica text is not. See embedFonts.

Hardening and behaviour changes

Independent read-only reviews (MCP 2026-07-28 conformance and backward compatibility, agent-consumption quality and engine coverage, factuality / tests / veraPDF) ran on the finished branch. Everything below is additive or touches error paths only; default outputs stay byte-identical and _meta.apiVersion stays 1.6.0.

Hardening

  • Strict input schemas — every Zod schema is .strict() at every nesting level: an unknown or misspelt key is a VALIDATION_ERROR ("Unrecognized key"), exactly as the published additionalProperties: false always declared (previously stripped silently).
  • Base64 / DER boundary (src/base64.ts) — a data:…;base64, prefix is tolerated; PEM armour where DER base64 is expected → VALIDATION_ERROR with the exact openssl … -outform DER remedy; PEM text, a nested data: URI or double-encoded base64 passed as a PDF → PDF_PARSE_FAILED with a hint; an empty payload → VALIDATION_ERROR. sign_pdf certificate / chain / key parse failures now carry VALIDATION_ERROR + remedy (were uncoded engine text).
  • Output-schema conformancestructuredContent always validates against outputSchema (a 2026-07-28 MUST), including verbosity: 'summary' and fields projections: the seven read tools declare projectable schemas (every property optional, additionalProperties: false kept, summary-only scalars declared); asserted with the SDK's Ajv 2020-12 validator (tests/schema-conformance.test.ts).
  • HTTP bearer token (src/auth.ts) — opt-in PDFNATIVE_MCP_HTTP_TOKEN (≥ 16 characters, no whitespace; a weaker value aborts startup). When set, /mcp requires Authorization: Bearer <token>, otherwise 401 + WWW-Authenticate: Bearer realm="pdfnative-mcp" (with error="invalid_token" only when credentials were sent, RFC 6750 §3.1) and a JSON-RPC -32600 body; constant-time comparison, never logged. Without it the loopback endpoint has no authentication (SECURITY.md now says so and recommends the token); the startup log states the active mode.
  • HTTP Origin port — the SDK's loopback Origin check is port-agnostic; the server additionally requires the Origin port to equal its own port (1.5.0 pinned http://127.0.0.1:<port> the same way).
  • HTTP keep-alive — client-disconnect detection moved to the per-request response; long-lived connections no longer accumulate socket listeners (MaxListenersExceededWarning).
  • Response cachesign_pdf is never cached (every call, not only timestamp: true), nor is any document call carrying encrypt; the key is namespaced by TOOL_API_VERSION/package version so an engine upgrade never serves old bytes; a hit carries _meta.cached: true (and the earlier {date} placeholder, if any).
  • Compatibility gatetests/_fixtures/tool-shape.v1.5.0.json freezes the published 1.5.0 catalogue and tests/catalogue-superset.test.ts fails on any removal or narrowing (tool, property, enum value, default, new required, tighter bound); the accepted deltas (lang oneOfanyOf, watermark.text no longer required, sign_pdf openWorldHint) are enumerated and must each still occur. It caught a bound that had crept onto embed_image.imageBase64; that input is unbounded again.
  • Configuration errors — a weak token or an invalid PDFNATIVE_MCP_MAX_INFLATE_BYTES prints one clean fatal: line (no stack) and exits 1.
  • Lintnpm run lint is eslint src --max-warnings 0; the non-null assertions in src/ were replaced by explicit narrowing (a CMS structure shorter than the parser expects now raises a coded CMS_PARSE_FAILED).
  • Protocol errorstools/call with an unknown tool name is a JSON-RPC -32602 error ([UNKNOWN_TOOL] Unknown tool: …) instead of an isError result, as the specification classifies it (callToolDirect keeps isError). serverInfo carries websiteUrl; the resource template is pdfnative://output/{+path} (RFC 6570 reserved expansion); server.json description trimmed to the registry's 100-character limit.
  • Honest wording — three runtime dependencies (pdfnative, the MCP SDK, zod); "constant-time node:crypto" applies to signing DER keys only (raw P-256 scalars and all verification are pure JS); a timestamp token is checked for status, imprint and nonce before embedding and its own signature verified by verify_pdf; a /DocTimeStamp counts in allValid like any signature (the earlier "never flips allValid" wording was wrong).

New opt-in inputs (byte-identical when absent)

  • creationDate (ISO-8601) on all nine document tools — generate_basic_pdf, add_table, add_form, add_international_text, embed_image, add_barcode, add_attachment, add_chart, prepare_signature_placeholder — pins /Info /CreationDate, the XMP dates and therefore the trailer /ID.
  • signingTime on prepare_signature_placeholder (/Sig /M frozen at placeholder time); sign_pdf.signingTime now accepts time-zone offsets (2026-01-15T10:00:00+01:00).
  • Pinned dates give byte-identical output on the same host time zone (the engine serialises local time, e.g. D:20260115100000+01'00'); TSA tokens, online add_ltv, encrypt_pdf and ECDSA signatures are never reproducible. rsaKeyPkcs1DerBase64 accepts PKCS#8 DER as well as PKCS#1.

Behaviour changes (error and opt-in paths only)

  • validate_pdf on unparsable input → PDF_PARSE_FAILED (was a valid: false result carrying Unparseable PDF: …).
  • inspect_pdf checks contains only the requested keys (was every key, false when unrequested); check: 'signed' is true when at least one signature field carries signed content — an extra unsigned placeholder no longer negates it.
  • verbosity: 'summary' keeps docTimestampCount / trapped / checksPassed (inspect_pdf) and ltvLevel with ltv: true (verify_pdf); fields paths that match nothing are reported in _meta.unmatchedFields + _meta.availableFields.
  • Page-index / range errors from merge_pdfs / split_pdf / extract_pagesVALIDATION_ERROR with a 0-based hint (was PDF_PARSE_FAILED).
  • FORM_FIELD_NOT_FOUND names read_form_fields / onUnknownField: 'ignore'; ENCRYPTED_SOURCE remedies are tool-specific (annotate_pdf / update_metadata: decrypt_pdf → edit → encrypt_pdf; add_ltv / timestamp_pdf: decrypting would destroy the signatures — sign before encrypting). The page-tree tools never raise ENCRYPTED_SOURCE (they take password); their descriptions said otherwise.
  • add_international_text.lang uses anyOf (was oneOf, same accepted values).

Catalogue size and compaction

  • tools/list is ≈ 245 kB (1.5.0: ≈ 108 kB). The descriptions and examples were compacted (≈ 206 kB → ≈ 174 kB at that point), but advertising every block kind inline — the 13-member blocks union appears in both generate_basic_pdf and inspect_layout — plus the layout / encrypt fragments on ten tools outweighs it. There is no $ref / $defs by policy (hosts that forward inputSchema to function-calling APIs), so this is the honest number; SERVER_INSTRUCTIONS is ≈ 6.7 kB (from 12.9 kB); at most two _meta.examples per tool, all executable against their inputSchema (three were not); the decision tree resolves add_attachment vs generate_basic_pdf, inspect_pdf check:'signed' (structural) vs verify_pdf (cryptographic), sign_pdf vs prepare_signature_placeholder, and points to inspect_layout for "will it fit".
  • New recipe prompts pades_ladder, print_ready, reproducible_output, pdfa_valid (with the existing governance_contract, draft_issue_workflow).
  • Parity gate: scripts/tool-shape.mjs (+ --write) fingerprints the catalogue with description strings stripped; tests/catalogue-parity.test.ts compares it with tests/_fixtures/tool-shape.json. Wording may change freely; a structural change is a deliberate fixture refresh reviewed under docs/API_STABILITY.md §5 (which now states that tools/list wording, examples and instructions are outside the byte-identical charter).

CI / veraPDF

  • scripts/validate-pdfa.mjs reports PASS / FAIL / XFAIL / XPASS / INFRA / SKIP per file; VERAPDF_REQUIRED=1 fails closed (missing veraPDF / Java, crash or unparseable report → exit 3) and the workflow sets it; infrastructure failures are no longer reported as conformance failures.
  • Corpus grown to 26 files (24 claiming PDF/A incl. 3 negative canaries with expectCompliant: false, 2 page-tree outputs skipped) — 1b watermark, custom outputIntent, emoji + math under 2u, add_form, unsigned and PAdES-signed placeholders, PDF attachment under 3b, update_metadata on a claiming file, merge / extract, a composite document with every block kind, a Letter document with templates and compress. Last run: 21 PASS / 3 XFAIL / 2 SKIP.
  • The veraPDF installer zip is SHA-256-pinned in verapdf.yml; the job stays non-blocking in 1.6.0. ci.yml adds a Windows job (Node 22) next to the Linux 22 / 24 matrix.
  • Known engine limitations (documented in AGENTS.md, docs/guides/PDFA.md, docs/KNOWLEDGE_BASE.md): add_form / formField blocks with pdfA + embedFonts: true still fail PDF/A-2b under veraPDF — the AcroForm /DR /Helv is an unembedded Type1 font (ISO 19005-2 6.2.11.4.1; candidate upstream issue via draft_governance_issue, draft generated); an unsigned prepare_signature_placeholder output with pdfA is conformant only once signed (ISO 19005-2 6.4.3); inspectDocumentLayout measures a toc block as 0 pt; extractText swallows per-page decode failures under the inflate cap.
  • Docs corrected across README / AGENTS / guides / llms.txt: full 25-code lang list, seven (not four) read tools take verbosity / fields, examples/run.mjs never existed, EXTRACTION_UNSUPPORTED is never raised, @modelcontextprotocol/server (not /sdk), error reference lists every emitted code (45) plus the [UNKNOWN_TOOL] protocol error.
  • Build no longer ships .map files (tsconfig.build.json): the npm package is 134 files / 285.9 kB packed. package.json carries a plain-text description and 66 curated keywords. Measured on the final branch: 69 test files, 937 tests passed + 1 skipped; coverage 93.06 % statements / 84.68 % branches / 98.59 % functions / 95.09 % lines.

Deferred by design

  • redact_pdf stays deferred (overlay / flatten ≠ content removal; pdfnative 1.7.0 exports no content-removal API). verify_pdf keeps its local P-256 ECDSA verifier (ecdsaVerifyHash still not exported). Per-tool HTTP page streaming remains blocked: MCP 2026-07-28 still has no partial structuredContent envelope.
  • The opt-in telemetry hook stays a long-term item and is intentionally not part of this release.

Install

npm install pdfnative-mcp@1.6.0

Upgrade

No breaking changes. Drop-in replacement for v1.5.0. Deliberate behaviour changes inherited from pdfnative 1.7.0 — in each case the previous output was wrong or non-conformant:

  • RTL text (add_international_text): digit runs keep logical order, paired delimiters mirror, ALEF joins correctly and Persian letters take positional forms — every Arabic-script document renders differently, and correctly.
  • Forms (add_form, fill_form): the AcroForm /Helv font carries /ToUnicode in every mode (form text becomes searchable); all form outputs change bytes.
  • Tagged / PDF/A documents on base-14 fonts (pdfA on the document tools): the shared WinAnsi /ToUnicode CMap is emitted; bytes change. The same configuration now raises the PDFA_NO_FONT_ENTRIES diagnostic — silent by default, visible with includeDiagnostics, fatal with strict; fix it with embedFonts: true.
  • Incremental outputs (prepare_signature_placeholder, sign_pdf, annotate_pdf, fill_form, the new LTV tools): /ID[1] is regenerated per revision and an EOL is inserted before the appended revision; earlier revisions stay a byte-exact prefix.
  • CMS signatures: signed attributes are encoded in canonical DER order; signatures remain valid.
  • Charts whose x labels previously overlapped now draw every Nth label; labelStride: 1 restores the old draw-everything behaviour.
  • Colour emoji (add_international_text with emoji): COLRv1 PaintComposite layers are now rendered, so flag sequences draw as flat flags instead of tofu — output bytes change for every document containing emoji input.
  • draft_governance_issue: the HUMAN_GATE charter sentence was reworded to state the single permitted egress class (operator-configured TSA / OCSP / CRL, never GitHub), so the draft markdown and complianceReport.humanGate text differ from v1.5.0. Deliberate charter update; the report shape is unchanged.
  • sign_pdf placeholder size: the default reservation is now max(16384, estimateContentsSize(cert, algorithm)) instead of a flat 16384 bytes, plus 8192 bytes when timestamp: true — identical for signer certificates up to roughly 10 KB, larger (different bytes) beyond, where the old size risked an overflow. To pin it, build the placeholder with prepare_signature_placeholder and its placeholderBytes input (sign_pdf has no such input), then sign that document.
  • add_form text areas: fieldType: 'textarea' now produces a real multi-line field (/Ff 4096); bytes change for that input only.
  • MCP transport: hosts that speak MCP 2026-07-28 now receive resultType, cache hints and _meta.serverInfo; 2025-era hosts see no difference. Programmatic consumers of createServer() now receive an @modelcontextprotocol/server Server instance.

Migrating from 1.5.0

Every 1.5.0 input is still accepted and every 1.5.0 tool, property, enum value and bound is still there (tests/catalogue-superset.test.ts). Six things a 1.5.0 client may have relied on behave differently — all on error paths or on inputs the published schema already declared invalid:

  1. Stray keys fail. A call carrying an unknown or misspelt key, top-level or nested (validate_pdf { profile: 'pdfa2b' }, a blocks[] entry with an extra property), was silently stripped in 1.5.0 and is now VALIDATION_ERROR ("Unrecognized key"). The schemas always said additionalProperties: false.
  2. Page-index mistakes are VALIDATION_ERROR. Out-of-range pages / ranges on merge_pdfs, split_pdf, extract_pages were PDF_PARSE_FAILED; a client catching that code for this case must catch VALIDATION_ERROR (the message carries a 0-based hint).
  3. validate_pdf on unparsable bytes is an error, not a verdict. 1.5.0 returned { valid: false, errors: ['Unparseable PDF: …'] } as a successful result; 1.6.0 returns isError: true [PDF_PARSE_FAILED] with no structuredContent. A parse failure is not a PDF/UA verdict — branch on isError first.
  4. Unknown tool or prompt names are protocol errors. tools/call / prompts/get with a name that does not exist → JSON-RPC -32602 ([UNKNOWN_TOOL] / [UNKNOWN_PROMPT]), no isError result.
  5. inspect_pdf.checks holds only the keys you asked for. 1.5.0 echoed every check name with false; read checksPassed or the requested key, never an absent one.
  6. add_form text areas change bytes. fieldType: 'textarea' now sets the multiline flag (see Fixed).

Unchanged and worth knowing: a stdio request sent before initialize is dropped and JSON-RPC batch arrays are not accepted on stdio (SDK behaviour in 1.5.0 too; HTTP batches work); embed_image.imageBase64 has no length bound; encrypt_pdf / decrypt_pdf still drop signatures and the AcroForm — use the new build-time encrypt when the form must survive.

Links