关于 PDF Writer (PDF Agent Stack)
PDF Writer 是用于创建和编辑 PDF 的 MCP 服务,面向需要生成文档、调整页面或处理表单的用户。它支持日文等 CJK 字体嵌入,并可输出带标签的 PDF/UA-1 文档。
主要功能
- 01从文本、Markdown 和表格创建 PDF
- 02合并、拆分、提取及重排页面
- 03编辑元数据、书签、注释和表单
- 04嵌入附件并添加水印或页码
- 05支持日文字体及 PDF/UA-1 标记
适用场景
- 01将 Markdown 报告导出为 PDF
- 02合并或重排现有 PDF 页面
- 03填写表单并添加书签或批注
- 04为日文文档生成带标签的 PDF
MCP 相关
它作为 MCP 服务向客户端提供 PDF 创建、修改和修复工具,可通过工具调用处理文档。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
工具
20收录的工具列表 · 更新于 2 周前
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| lang | string | 否 | 文档的自然语言(BCP 47,例如 "ja" / "en-US")。省略此项且启用 tagged 时,将根据文本推断语言,并在 warnings 中报告推断结果。错误的语言声明会导致屏幕阅读器误读;如果你知道语言,请明确指定。 |
| text | string | 是 | 正文文本。\n 用于换行;空行用于分隔段落。 |
| title | string | 否 | PDF 标题。设置为元数据,并作为标题显示在正文顶部。 |
| author | string | 否 | PDF 作者(元数据)。 |
| margin | number | 否 | 所有边的页边距(pt)。默认值为 56(约 20 mm)。范围:0-300。 |
| tagged | boolean | 否 | 生成带标签的 PDF(PDF/UA-1、ISO 14289)。默认值为 false。设为 true 时,将添加结构树、PDF/UA 声明、/Lang 和 DisplayDocTitle,使文档可供屏幕阅读器读取。PDF/UA 要求设置标题,因此 title 将成为必填项。 |
| fontPath | string | 否 | 要嵌入的字体文件的绝对路径(.ttf / .otf)。日文等非拉丁文本必需。.ttc(TrueType Collection)不受支持。也可通过 PDF_WRITER_FONT 环境变量设置。 |
| fontSize | number | 否 | 正文字体大小(pt)。默认值为 11。范围:4–96。 |
| pageSize | enum | 否 | 页面尺寸。默认值为 A4。A4 | A3 | A5 | LETTER | LEGAL |
| outputPath | string | 否 | 目标文件的绝对路径。省略时,将返回 base64 字符串。 |
| pdfVersion | enum | 否 | 要输出的 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 |
| returnBase64 | boolean | 否 | 为 true 时,除保存外,还会在结果中包含 base64 字符串。 |
| onMissingGlyph | enum | 否 | 字体缺少字符时的处理方式(例如 ✔ U+2714,Noto Sans JP 中缺少此字符)。error(默认值)= 失败并列出缺失字符 / replace = 替换为 〓 并发出警告 / ignore = 渲染为空白并发出警告。error | replace | ignore |
详细介绍
pdf-writer-mcp
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_pdf | Markdown——标题、段落、项目符号列表/有序列表、代码块、引用、分隔线、表格 |
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
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。