mcpField
P
PDF Writer (PDF Agent Stack)

PDF Writer (PDF Agent Stack)

本地服务
shuji-bonji.github.io生产力02 周前更新

关于 PDF Writer (PDF Agent Stack)

PDF Writer 是用于创建和编辑 PDF 的 MCP 服务,面向需要生成文档、调整页面或处理表单的用户。它支持日文等 CJK 字体嵌入,并可输出带标签的 PDF/UA-1 文档。

主要功能

  1. 01从文本、Markdown 和表格创建 PDF
  2. 02合并、拆分、提取及重排页面
  3. 03编辑元数据、书签、注释和表单
  4. 04嵌入附件并添加水印或页码
  5. 05支持日文字体及 PDF/UA-1 标记

适用场景

  1. 01将 Markdown 报告导出为 PDF
  2. 02合并或重排现有 PDF 页面
  3. 03填写表单并添加书签或批注
  4. 04为日文文档生成带标签的 PDF

MCP 相关

它作为 MCP 服务向客户端提供 PDF 创建、修改和修复工具,可通过工具调用处理文档。

配置

本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。

mcp.json · 通用配置
{
"mcpServers": {
"shuji-bonji-pdf-writer": {
"command": "npx",
"args": [
"-y",
"@shuji-bonji/pdf-writer-mcp"
]
}
}
}
启动命令npx -y @shuji-bonji/pdf-writer-mcp

本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。

安装包
npm@shuji-bonji/pdf-writer-mcp0.22.0

工具

20

收录的工具列表 · 更新于 2 周前

参数类型必填说明
langstring否文档的自然语言(BCP 47,例如 "ja" / "en-US")。省略此项且启用 tagged 时,将根据文本推断语言,并在 warnings 中报告推断结果。错误的语言声明会导致屏幕阅读器误读;如果你知道语言,请明确指定。
textstring是正文文本。\n 用于换行;空行用于分隔段落。
titlestring否PDF 标题。设置为元数据,并作为标题显示在正文顶部。
authorstring否PDF 作者(元数据)。
marginnumber否所有边的页边距(pt)。默认值为 56(约 20 mm)。范围:0-300。
taggedboolean否生成带标签的 PDF(PDF/UA-1、ISO 14289)。默认值为 false。设为 true 时,将添加结构树、PDF/UA 声明、/Lang 和 DisplayDocTitle,使文档可供屏幕阅读器读取。PDF/UA 要求设置标题,因此 title 将成为必填项。
fontPathstring否要嵌入的字体文件的绝对路径(.ttf / .otf)。日文等非拉丁文本必需。.ttc(TrueType Collection)不受支持。也可通过 PDF_WRITER_FONT 环境变量设置。
fontSizenumber否正文字体大小(pt)。默认值为 11。范围:4–96。
pageSizeenum否页面尺寸。默认值为 A4。A4 | A3 | A5 | LETTER | LEGAL
outputPathstring否目标文件的绝对路径。省略时,将返回 base64 字符串。
pdfVersionenum否要输出的 PDF 版本。默认值为 "1.7"。"2.0"(ISO 32000-2)不仅满足版本声明,还满足与之绑定的要求:会添加 trailer /ID(表 15 要求),并将 Info 字典精简为 CreationDate / ModDate,同时将标题、作者和 Producer 移至 XMP(§14.3.3)。不能与 tagged: true 同时使用(编写器只能生成基于 PDF 1.7 的 PDF/UA-1 声明)。1.7 | 2.0
returnBase64boolean否为 true 时,除保存外,还会在结果中包含 base64 字符串。
onMissingGlyphenum否字体缺少字符时的处理方式(例如 ✔ U+2714,Noto Sans JP 中缺少此字符)。error(默认值)= 失败并列出缺失字符 / replace = 替换为 〓 并发出警告 / ignore = 渲染为空白并发出警告。error | replace | ignore

详细介绍

pdf-writer-mcp

CI npm version License: MIT

日语

MCP 服务,可从文本、Markdown 或表格数据创建 PDF,并编辑现有 PDF(元数据和页面操作)。基于 normativepdf 构建——这是一个条款驱动的 PDF 库,每种行为都对应一项 ISO 32000 条款——并通过 harfbuzz 子集化嵌入 CJK 字体。

它与 pdf-reader-mcp(结构分析)和 pdf-verify-mcp(真实性验证)同属 PDF 工具系列。pdf-reader-mcp 告诉你 PDF 包含什么内容,pdf-verify-mcp 告诉你 PDF 是否真实,而 pdf-writer-mcp 则负责写入 PDF。

工具

重要

所有文件路径都必须是绝对路径(自 v0.7.0 起)。相对路径和包含 .. 的路径都会被拒绝——相对路径会根据 MCP 宿主的工作目录解析,而该目录可能不是你以为的目录。此要求适用于 inputPath、inputPaths、outputPath、outputDir、fontPath 和 attachmentPath。大于 100 MB 的输入 PDF 也会被拒绝。

创建
工具用途
create_text_pdf纯文本——保留 \n,空行用于分隔段落,长行自动换行
create_markdown_pdfMarkdown——标题、段落、项目符号列表/有序列表、代码块、引用、分隔线、表格
create_table_pdf带边框的表格——自动调整列宽、单元格换行、跨页时重复显示表头

通用选项:outputPath、returnBase64、fontPath、fontSize、pageSize(A4/A3/A5/LETTER/LEGAL)、margin、title、author、onMissingGlyph、tagged、lang、pdfVersion。

PDF 2.0 输出(v0.16.0)

传入 pdfVersion: "2.0" 以写入符合 ISO 32000-2 的文件。默认值仍为 "1.7",其字节内容保持不变。

版本不仅仅体现在文件头中。ISO 32000-2 对其规定了两项要求,且两项都已满足:

  • 尾部 /ID 变为必需项(表 15)。首次写入时,两个元素相等(R-14.4-6),且该值在 SOURCE_DATE_EPOCH 下保持确定性。
  • Info 字典仅保留 CreationDate 和 ModDate(§14.3.3);标题、作者和生成程序会移至 XMP 元数据流,PDF 2.0 规定文档元数据应存放于此。
{ "text": "Body.", "title": "Report", "pdfVersion": "2.0" }

tagged: true 不能与此选项同时使用。此服务唯一写入的无障碍声明是 PDF/UA-1(ISO 14289-1),它基于 PDF 1.7——将其写入 2.0 文件会构成无法验证的声明。PDF/UA-2 输出尚未实现,因此会拒绝这种组合,而不是静默生成文件。

带标签的 PDF / PDF/UA(v0.5.0)

传入 tagged: true,即可生成符合 PDF/UA-1 (ISO 14289) 标准的无障碍带标签 PDF。输出经 veraPDF 验证符合标准(--flavour ua1,106/106 条规则)。

{ "markdown": "# Title\n\nBody.", "title": "Report", "tagged": true, "lang": "en" }

Markdown 会映射到结构树:标题 → H1–H6,列表 → L/LI/LBody,表格 → Table/TR/TH/TD(表头会添加 /Scope),引用 → BlockQuote,代码 → Code。分隔线、边框和代码背景会成为伪影。标题级别会规范化,使其从 H1 开始且不跳级——Markdown 中的 # → ### 跳级会在结构中变为 H1 → H2,而视觉字号保持作者设定不变。

PDF/UA 要求文档标题,因此 tagged: true 必须提供 title。如果未提供,系统会根据文本推断 lang(BCP 47),并通过 warnings 报告——如果你知道语言,请显式传入,因为错误的 /Lang 会导致屏幕阅读器错误地朗读文本。

说明

标签功能需主动启用——默认输出不变。

机器验证(veraPDF)只能检查内容是否存在,无法判断阅读顺序或替代文本是否恰当——仍需人工审核。

编辑 — 页面操作
工具用途
merge_pdfs按顺序合并 2–50 个 PDF;元数据继承自第一个文件
split_pdf每个页面范围生成一个输出文件
extract_pages按请求顺序提取页面(也可用于重新排序)
delete_pages删除页面(不允许删除所有页面)
reorder_pages按明确指定的所有页面排列顺序重新排序
rotate_pages顺时针旋转(90/180/270 度),并累加现有旋转角度。直接修改文件,因此不适用下方警告

警告

除 rotate_pages 外的五个工具都会根据页面重建文档。

  • 会保留:附件(/Names /EmbeddedFiles、/AF)、/Lang、/ViewerPreferences、/OutputIntents
  • 不会保留:带标签的结构树、XMP,以及任何与页码或页面引用关联的内容(书签、页面标签、命名目标)
  • 所有丢失的内容都会通过 warnings 报告——不会有内容在无提示的情况下消失

页面规格使用 "1,3-5,8-"(从 1 开始;-3 表示直到第 3 页,8- 表示从第 8 页到末尾)。保留顺序并移除重复项。

编辑 — 添加和修复文档
工具用途
set_metadata更新 Info 字典字段(title / author / subject / keywords / creator),并保留其余内容。对于带有 XMP 的文档,dc:title 等字段会保持同步(保留 PDF/UA 和 PDF/A 声明)。tdmReservation(0 | 1)和 tdmPolicy 会将 W3C TDMRep 权利声明写入 XMP(tdm:reservation / tdm:policy,命名空间 http://www.w3.org/ns/tdmrep/);没有 XMP 的文档会自动添加 XMP,而 PDF/A 文档还会添加 pdfaExtension:schemas 中描述的架构,避免 veraPDF 因其违反 ISO 19005-2 6.6.2.3 而报错(v0.22.0)
add_bookmarks设置大纲(书签);可通过 children 嵌套,并会替换任何现有大纲
add_annotation添加便笺(text)、highlight 或 square 注释。对于带标签的 PDF,注释会嵌套在 Annot 元素中,并保持符合 PDF/UA;传入 alt 可为其添加描述
attach_file嵌入文件(/Names /EmbeddedFiles + catalog /AF + /AFRelationship)——采用 PDF/A-3 结构
stamp_page_numbers添加页码({n} / {total}、六种位置、pages、startAt)。在带标签的 PDF 中会成为构件,因此仍符合规范
fill_form填写 AcroForm 字段。日文值使用嵌入字体;可在同一次处理中扁平化
flatten_form将表单扁平化为静态内容。默认拒绝处理带标签的 PDF(会破坏 PDF/UA)
tag_form_fields修复带标签 PDF 中的表单,使其符合 PDF/UA-1(将控件嵌套在 Form 中,设置 /Tabs S,添加 /TU 替代名称;传入 labels 可设置便于理解的名称)。支持幂等操作
ensure_tagged将现有 PDF 放入 PDF/UA-1 容器 → 为未加标签的 PDF 搭建基础结构
ensure_pdfa将现有 PDF 放入 PDF/A-3b / PDF/A-4 / PDF/A-4f 容器(flavour)→ 归档容器
add_watermark添加对角水印(“社外秘” / “DRAFT”)。默认位于正文内容后方;在带标签的 PDF 中会成为构件

共享选项:outputPath、returnBase64、allowBreakingSignatures。

重要

编辑已签名的 PDF:普通保存会重写整个文件,因此编辑通常会使现有签名失效。默认情况下会拒绝包含 /ByteRange 的 PDF。

  • preserveSignatures: true — 追加符合 ISO 32000 的增量更新,保留所有签名有效(原始字节不会更改)。所有会向文档添加内容的编辑工具均支持:add_annotation、set_metadata、add_bookmarks、tag_form_fields、ensure_tagged、attach_file、stamp_page_numbers、add_watermark(对于带标签的 PDF,结构树更改也会通过同一次增量更新写入)
  • allowBreakingSignatures: true — 继续执行破坏性修改,使签名失效
  • 当更改类型不在认证级别允许范围内时,会拒绝处理经过认证的文档(DocMDP)(§12.8.2.2)

测试结果:对真正签名的 PDF 叠加多次增量更新后,pdf-verify-mcp 仍报告 VALID;带标签 PDF 的增量结构更新仍通过 veraPDF 检查,结果为 COMPLIANT (106/106)。

为未标记的 PDF 搭建脚手架(ensure_tagged)

ensure_tagged 会将现有 PDF 放入 PDF/UA-1 容器。对于带标签的输入,结构树保持不变,只修复缺失的文档级要求(MarkInfo、/Lang、DisplayDocTitle、XMP)。对于从未包含结构树的文档,则会创建一个最小脚手架——将每个页面的内容包裹在单个 P 元素中,使辅助技术能够访问文本,并通过 veraPDF 验证(实测:106/106)。

警告

这是脚手架,不代表具备无障碍性。 机器无法推断含义,因此不会生成标题、列表、表格、阅读顺序和图形替代文本。工具会在其 warnings 中说明这一点。(将内容包裹在 Artifact 中也能通过 veraPDF 验证,但会让屏幕阅读器无法读取正文——这种合规表演是刻意不实现的。)如果你能控制源文件,请使用带 tagged: true 的 create_* 来生成真正的结构;ensure_tagged 适用于他人交给你的文档。

归档容器(ensure_pdfa)

ensure_pdfa 是 ensure_tagged 的归档版(PDF/A-3b),只会添加文档级要求:

  • 尾部 /ID(ISO 32000-1 §14.4)
  • sRGB 输出意图(GTS_PDFA1;生成并嵌入 ICC 配置文件)
  • XMP pdfaid 声明(创建日期继承自 Info /CreationDate)

警告

内容流、字体和结构树都不会更改——因此这并不会让 PDF 符合规范。写入声明后,还要进行测量:使用 pdf-verify-mcp 的 validate_conformance(flavour: "pdfa-3b") 验证。对电子记账样本的实测结果:veraPDF 146/146 COMPLIANT,PDF/UA-1 仍为 106/106,附件也得到保留。

从 v0.17.0 开始,如果已知声明不成立,结果还会附带 declarationRisks。目前唯一报告的风险是 FONT_NOT_EMBEDDED:PDF/A 要求嵌入所有字体,而 ensure_pdfa 不会嵌入字体,因此使用标准 14 种字体绘制的文档无论其文档级结构多么正确,都会验证失败。声明仍会写入——判定权属于 veraPDF——但现在调用方可以根据风险进行分支处理,而无需解析警告文本。

PDF/A-4(v0.16.0)

flavour: "pdfa-4" 改为面向 ISO 19005-4。它基于 PDF 2.0 构建,因此除了上述三项,还会将文件头重写为 2.0,并移除 Info 字典——除非目录包含 /PieceInfo,否则 PDF/A-4 不允许使用 Info 字典;这比 ISO 32000-2 §14.3.3 的规定更严格。会写入 pdfaid:rev,但不会写入 pdfaid:conformance,因为 PDF/A-4 没有符合级别。

如果文档带有附件,请使用 "pdfa-4f"。 普通 PDF/A-4 要求每个嵌入文件本身都符合 PDF/A,因此附加 CSV 或 JSON 文件(电子记账场景)会使其不符合规范。"pdfa-4f" 正是为此类情况提供的变体。

使用 veraPDF 1.30.0 实测:pdfa-4 为 109/109 COMPLIANT;同一文档附加 CSV 后,作为 pdfa-4 时为 108/109,作为 pdfa-4f 时则为 109/109。

除非输入文件已经是 PDF 2.0,否则 PDF/A-4 的 flavour 不允许使用 preserveSignatures:增量更新无法重写文件的第 0 字节,而强行这样做会破坏原本要保留的签名。

安装

{
  "mcpServers": {
    "pdf-writer": {
      "command": "npx",
      "args": ["-y", "@shuji-bonji/pdf-writer-mcp@latest"],
      "env": {
        "PDF_WRITER_FONT": "/absolute/path/to/NotoSansJP-Regular.otf"
      }
    }
  }
}

PDF_WRITER_FONT 让所有工具都可以省略 fontPath,仍能渲染 CJK 文本。

提示

使用 @latest(或固定版本)。 不带版本的 npx -y <pkg> 会继续运行首次缓存的版本——-y 只会跳过安装提示,不会检查更新。不带版本的包说明符可能会运行数月前的旧版本。@latest 会让 npx 在每次启动时检查注册表;如果需要可复现性,则改用 @0.5.0 固定版本。清除过期缓存:rm -rf ~/.npm/_npx。

字体

标准 PDF 字体(Helvetica)仅支持 ASCII。要渲染日语或任何非拉丁文本,请将 fontPath 或 PDF_WRITER_FONT 指向可嵌入的单字形字体(.ttf / .otf)。

  • 推荐来源:Noto Sans JP (SubsetOTF/JP) — 静态单字形字体,采用 SIL OFL 许可。

  • 不支持 .ttc(TrueType 字体集合)——检测到该文件时会将其拒绝。请先提取单个字形:

    python3 -c "from fontTools.ttLib import TTCollection; \
      TTCollection('NotoSansCJK-Regular.ttc').fonts[0].save('NotoSansCJKjp-Regular.otf')"
    
缺失字形

字体中不存在的字符(例如 Noto Sans JP 不包含的 ✔ U+2714)原本会以 .notdef 嵌入,并渲染为空白。onMissingGlyph 用于控制此行为:

值行为
error(默认)失败,并列出有问题的字符,例如 "✔" (U+2714)
replace替换为 〓,并通过 warnings 报告
ignore渲染为空白,并通过 warnings 报告

结果

{
  "path": "/abs/out.pdf",     // when outputPath is given
  "base64": "JVBERi0xLj...",  // when returnBase64, or outputPath is omitted
  "pageCount": 3,
  "bytes": 91788,
  "font": "NotoSansJP-Regular.otf",
  "warnings": ["Replaced 1 unsupported character(s) with \"〓\": \"✔\" (U+2714)"]
}

编辑工具返回相同结构,但不包含 font;split_pdf 返回 { files: [...], count }。

错误(v0.7.0)

错误采用结构化形式,遵循与 pdf-reader-mcp 相同的约定:为程序提供稳定的 code,并提供 LLM Agent 可执行的 next_actions。写入器专属的保护机制均表示为可重试,并带有明确标志:

{
  "error": "\"/in/signed.pdf\" appears to be digitally signed (/ByteRange found). …",
  "code": "SIGNED_PDF",
  "retryable": true,
  "next_actions": [
    {
      "action": "retry_with_allowBreakingSignatures",
      "reason": "Only if invalidating the signature is acceptable…",
      "example": { "allowBreakingSignatures": true }
    }
  ]
}

代码:INVALID_ARGUMENT、DOC_NOT_FOUND、FONT_NOT_FOUND、INVALID_PDF、ENCRYPTED_PDF、UNSUPPORTED_PDF_FEATURE(XFA)、FILE_TOO_LARGE、INTERNAL_ERROR,以及写入器保护代码 SIGNED_PDF(allowBreakingSignatures)、TAGGED_PDF(allowBreakingTags)、FONT_REQUIRED(fontPath)、MISSING_GLYPH(onMissingGlyph)。

确定性输出(v0.7.0)

设置 SOURCE_DATE_EPOCH 环境变量(UNIX 秒数,遵循 reproducible-builds.org 约定),即可固定 CreationDate、ModificationDate 和 XMP 时间戳。这样,同一输入会生成逐字节完全一致的输出,便于比较差异、缓存和进行可复现测试。无效值会引发错误,而不会被忽略。

文本提取

生成的 PDF 支持选择、复制和搜索文本,也方便屏幕阅读器读取:即使使用嵌入的子集字体,也会写入 ToUnicode CMap(§9.10.3)。此功能由回归测试(extract.test.ts、render.test.ts)覆盖。

说明

v0.13.x 及更早版本的输出可能导致基于 poppler 的查看器打印 Mismatch between font type and embedded font file。这是一个真实符合性缺陷的表现(W-2:通过 FontFile2 嵌入 CFF 字体),已在 v0.14.0 修复——当前输出不会产生此类警告。

开发

npm install
npm run build      # emits dist/
npm test           # vitest
npm run typecheck  # tsc --noEmit

当 TEST_FONT_PATH 指向 CJK 字体时,会启用依赖字体的测试:

TEST_FONT_PATH=/path/to/NotoSansJP-Regular.otf npm test

已知限制

  • 行内样式:粗体/斜体只影响字号和字形文本,不影响字体系列——每份文档只嵌入一种字体。
  • .ttc 字体需要先提取单个字形(见上文)。
  • title 和正文的首个标题都会变成 H1(B-19)。这符合 PDF/UA 7.4.2(从 H1 开始,不跳过级别)的要求,也能通过 veraPDF 检查,但如果读取结构树后重新生成,标题会重复。
  • 不会输出列表 /Lbl(B-18)。项目符号和编号会直接写入正文文本。ISO 32000-2 §14.8.4.8.2 将 Lbl 列为备注(often include),而非要求,因此这种做法符合规范——但从读取结果重新生成时,标记会重复。

已解决:

  • 从 v0.14.0 起,子集字体名称前缀(ABCDEF+)已应用(W-3)。此前关于此行为的限制已不再适用。
  • snake_case 中的下划线被删除(B-17)的问题已于 2026-07-21 修复(将在下一个版本中发布)。现在 _ 强调格式仅在非单词内部的位置生效,并且会优先保护代码片段,因此 日本語_変数名 等非 ASCII 标识符也能保留。

路线图

  • 编辑 Tier A 第 1 波 — 元数据和页面操作(v0.2.0)
  • 编辑 Tier A 第 2 波 — 书签和注释(v0.4.0)
  • Tagged PDF / PDF/UA-1 — 经 veraPDF 验证(v0.5.0)
  • 在 Tagged PDF 输出中将注释放入 Annot 标签内(v0.5.1)
  • 编辑 Tier B — 文件附件、表单填写/扁平化、水印、页码盖章(v0.6.0)
  • 代码规范 / 系列对齐 — McpServer + Zod、结构化错误、绝对路径强制校验、stdout 防护、工具注解、确定性输出(v0.7.0)
  • tag_form_fields — 修复 Tagged PDF 中表单的 PDF/UA 问题,经 veraPDF 验证为 COMPLIANT(v0.8.0)
  • Tier C 首个里程碑 — 为 add_annotation 实现保留签名的增量更新,经 pdf-verify-mcp 使用真实 CMS 签名验证(v0.9.0)
  • 将增量更新扩展到 set_metadata / add_bookmarks,完整保留 trailer(§7.5.6),使 XMP 与 Info 保持同步(v0.10.0)
  • 对 Tagged PDF 执行增量更新 — 将脏跟踪推广到整个结构树;为 tag_form_fields 添加 preserveSignatures(v0.11.0)
  • 在所有编辑工具中支持增量更新,并新增 ensure_tagged — PDF/UA 框架与修复(v0.12.0)
  • 页面操作会报告并保留文档级信息;根据 ISO 32000-2 重新审计后发现三处 shall 违规(v0.13.0)
  • 在页面操作中保留结构树(同时支持 MarkInfo、声明符合性的 XMP、/AcroForm,以及合并多个输入文件中的附件)
  • Tier C 剩余工作 — edit_text(正文文本编辑/重排)
  • 发布流程技能(写入 → 使用 pdf-reader 读回 → 使用 pdf-verify 验收)
  • 带替代文本的图像(Figure + /Alt)
  • 从 .ttc 中自动提取字形
  • 标题和正文使用不同字形(嵌入粗体字形)
  • 图像嵌入、页眉/页脚
  • Tier C — 保留签名的增量更新、正文文本编辑、标签树维护
  • PDF/A 转换

许可证

MIT © shuji-bonji

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。