docs-mcp
# docs-mcp
> 一个 MCP Server,让 AI 代理真正读懂和改写 **Word(.docx)** 与 **PDF**——
> 而不是把二进制倒成一堆字节,也不是对着扫描件干瞪眼。
[](#测试)
[](LICENSE)
---
## 它解决什么问题
AI 代理处理文档时通常走两条烂路:要么把文件当纯文本读(丢掉段落结构、样式、表格、排版),
要么用通用库重写整个文件(**改一个字,页眉、图片、批注、嵌入字体全乱**)。
碰到扫描版 PDF 更糟——文本抽取拿到 0 个字符,模型却不知道自己拿到的是一片空白。
`docs-mcp` 走第三条路,两种格式各用各的正确解法:
| | 做法 | 结果 |
|---|---|---|
| **`.docx`** | 在 OOXML 包内部做**最小改写**:只重写真正被改到的 `<w:t>` 节点,ZIP 里其余分片按**原始压缩字节透传** | 「改完用 Word / WPS 打开格式不乱」是**可测的字节级性质**,不是承诺 |
| **PDF** | 先探测有没有真实文本层:有就按页抽取并重建段落;**没有就是扫描件**,明确拒绝文本读取,改用逐页渲染成图交给视觉模型 | 不再对着 405 页扫描件空跑一遍才发现取不到字 |
---
## 安装与接入
任何 MCP 客户端都可以直接拉起:
```jsonc
// Claude Code / Cursor / 其它 MCP 客户端
{
"mcpServers": {
"docs": {
"command": "npx",
"args": ["-y", "docs-mcp", "--root", "${workspaceFolder}"]
}
}
}
```
从源码跑:
```bash
git clone <repo> && cd docs-mcp
npm install
node bin/docs-mcp.js --root /path/to/workspace
```
### 命令行选项
| 选项 | 说明 |
|---|---|
| `--root <dir>` | 相对路径的解析根(默认进程 cwd) |
| `--allow-write <dir>` | 把写入限制在该目录内,可重复。**省略则不做额外限制** |
| `-h, --help` / `-v, --version` | 用法 / 版本 |
---
## 七个工具
### Word:`docx_read` / `docx_edit` / `docx_create`
```
docx_read { path, mode?: "text" | "outline" | "tables", from?, to? }
docx_edit { path, output_path?, dry_run?, operations: [...] }
docx_create { path, title?, paragraphs: [...] }
```
- 读取按 **1 起下标**返回段落,带样式 id 与「是否在表格内」,模型可以直接寻址;大文档自动分页
- 编辑支持 `replace_text`(**跨格式 run 匹配,保留匹配起点的格式**)/ `replace_paragraph` /
`insert_paragraph_after` / `delete_paragraph` / `set_metadata`
- 所有 `index` 指向 **`docx_read` 报告的那份原始文档**——多个操作一次性应用,前面的操作不会让后面的下标移位
- `dry_run: true` 只报告会改什么,**一个字节都不写**
- 原地修改首次自动留一份 `<名字>.orig.docx` 一次性备份
- 写入走「同目录临时文件 + rename」原子落盘
### PDF:`pdf_info` / `pdf_outline` / `pdf_read` / `pdf_render_page`
```
pdf_info { path, toc_limit? }
pdf_outline { path, from?, to?, max_depth? }
pdf_read { path, from?, to?, mode?: "paragraphs" | "lines", max_chars? }
pdf_render_page { path, page, scale?, region?, out_path? }
```
**先 `pdf_info`**——它会告诉你这份 PDF 该用哪种读法,并给出目录页码:
```
3 page(s) — "Quarterly Report"
Text layer: yes
Table of contents (top level):
1 Overview
2 Key findings
```
- **扫描件会被明确拒绝**:`pdf_read` 直接报错并指出改用 `pdf_render_page`,
而不是返回一堆空白让模型猜
- `pdf_render_page` 把整页或**指定区域**(按页面比例)渲染成 PNG,
以 **MCP 一等公民的图片内容块**直接进模型上下文
- **空白页会被检测出来**:渲染管线最典型的静默失败是 wasm/字体路径没配对时
pdf.js 不报错、只是画出一张白纸——非白像素低于 0.05% 就明确报错
---
## 设计取舍
**核心与宿主分离。** `src/core/` 是纯文档库(ZIP、XML、OOXML 文档模型、PDF 解析与渲染),
不认识 MCP、也不认识任何宿主框架;`src/tools/` 是宿主无关的工具实现;`src/server.js`
才把它们接到 MCP 协议上。同一份核心逻辑因此可以同时服务 MCP 客户端和别的宿主——**不重复实现**。
**ZIP 容器与 XML 解析全部自研,零依赖。** 用 `node:zlib` 的 `crc32` / `deflateRawSync` /
`inflateRawSync` 手写。这不是炫技:通用 ZIP 库会重新压缩所有分片,而保真要求
**未被修改的分片逐字节透传**。PDF 侧则老实依赖 `pdfjs-dist` + `@napi-rs/canvas`——
解析 PDF 和光栅化是两件不该手写的事。
**PDF 渲染用 MCP 原生图片块,不绕附件总线。** 插件形态要把图经宿主的附件服务送进上下文,
还得先校验当前模型是否声明了 image 输入模态,并准备「附件不可用时降级落盘」的兜底。
MCP 协议本身就有一等公民的图片内容块,**少一整层适配**。
**错误走 `isError` 而不是抛异常。** 工具级失败返回 `isError: true` 加一句人能读懂的话,
让模型看到「为什么失败」并自行纠正;协议连接保持完好——
[`test/mcp-e2e.test.mjs`](test/mcp-e2e.test.mjs) 专门验证了「连续报错之后连接仍可用」。
**`--allow-write` 是可选的,不是默认强制的。** 它像普通本地 CLI 工具一样默认不设限,
但保留一个开关,让「只想让它改某个目录」成为一条命令行就能表达的事。
---
## 测试
```bash
npm test
```
**265 项,五个套件分工:**
| 套件 | 项数 | 覆盖 |
|---|---|---|
| `test/core.test.mjs` | 45 | OOXML 核心;含 `unzip -t` 与 **Python `zipfile` + `ElementTree` 独立解析器**交叉验证 |
| `test/tools.test.mjs` | 61 | 三个 docx 工具、错误路径、**字节级保真**、写入围栏、路径解析、`tables` 模式 |
| `test/pdf.test.mjs` | 73 | 四个 pdf 工具:文本层、分页与字符预算、**大纲树提取**(层级/页码/max_depth/分页)、**扫描件识别与拒绝**、渲染成图(PNG 魔数校验)、区域裁剪、空白页检测、围栏 |
| `test/mcp-e2e.test.mjs` | 59 | **spawn 真实 stdio 子进程**,用官方 SDK 的 `Client` 连接:握手、`tools/list` schema、七种工具的 `tools/call` 往返(含图片内容块)、错误恢复、围栏、CLI |
| `test/realworld.mjs` | 27 | **真实文档验收**:179KB 带图带表的 Word(改一个字后除 `document.xml` 外逐字节不变)、**405 页扫描教材**(正确识别并拒绝文本读取、渲染出 890KB 的 PNG)、1MB 真实文本 PDF。文件不存在则整段跳过 |
**夹具测试完全自包含**:PDF 夹具是手写的最小 PDF 1.4(`test/fixtures.mjs`),不依赖任何
外部样例文件或写库。夹具证明不了的那部分交给 `realworld.mjs`——**手造的文档段落干净、
没有嵌入字体和图片,真实文件不是这样**。协议测试**不使用 in-memory transport 走捷径**——
它要证明的正是「任何标准 MCP 客户端都能挂上」,那就必须真的过一遍 stdio。
---
## 来源
`src/core/` 的模块来自作者另外两个项目,逐字节复用:
- `zip.js` / `xml.js` / `document.js` —— `dsh-docx-tool` 的 OOXML 核心
- `pdf.js` —— `dsh-pdf-tool` 的 PDF 解析与渲染核心
那两份代码在同一套夹具上已有各自的回归测试。本仓库把它们从
「某个特定宿主的插件」提升为「任何 MCP 客户端可用的独立服务」,
并补上了协议层、围栏层与端到端测试。
## License
MIT
TDQS
Scored across 7 tools
The four PDF tools (info, outline, read, render_page) and three DOCX tools (read, edit, create) target clearly distinct actions with clear format prefixes. The only overlap is that pdf_info also returns a table of contents while pdf_outline is dedicated to it, which the descriptions partially reconcile but could cause slight hesitation.
A strong, predictable format_action convention is used throughout (pdf_read, pdf_render_page, docx_read, docx_edit, docx_create). Minor deviation: pdf_info and pdf_outline are noun-style rather than verb-style, slightly breaking the verb_noun pattern.
Seven tools is well-scoped for a document-handling server, with each one earning its place (info, outline, read, render for PDF; read, edit, create for DOCX). No redundancy or bloat.
DOCX has full lifecycle coverage (create, read, edit/update) and PDF reading is thorough across text, scanned, and outline modes. Gaps: no PDF creation or PDF editing, though that may be out of scope for the server's apparent read-PDF/write-DOCX focus.