office-mcp
by jijiboo123
README.md
# office-mcp
一个 MCP 服务器:让 AI agent 能读 Word、Excel、PPT、PDF。
模型能读代码、能读网页,但**读不了你的 `.docx`**。这个项目用 MCP 协议把办公文档接给 agent——接上之后,一句"看看这份报告讲了什么"就能直接出结论。
## 为什么是 MCP
| 方案 | 问题 |
|---|---|
| 写个脚本让 agent 调 | 每个 agent 都要单独适配 |
| 把文档转成 txt 再喂 | 丢结构(表格、标题层级、备注全没了) |
| **MCP 服务器** | 一次实现,任何支持 MCP 的客户端都能用 |
MCP(Model Context Protocol)是标准协议,`stdio` 上跑 JSON-RPC 2.0。接一次,Claude Desktop / Cline / VS Code Copilot / DeepSeek Harness 都能用。
## 架构
```
┌─────────────┐ JSON-RPC over stdio ┌──────────────┐
│ AI agent │ ◀─────────────────────▶ │ office-mcp │
│ (MCP 客户端) │ initialize/tools/call │ server.py │
└─────────────┘ └──────┬───────┘
│
┌──────────┴──────────┐
▼ ▼
python-docx openpyxl
python-pptx pymupdf
```
## 四个工具
| 工具 | 输入 | 输出 |
|---|---|---|
| `doc_read` | `.docx` | Markdown:标题、段落、列表、表格(**按原文顺序**) |
| `sheet_read` | `.xlsx` | 列出所有工作表,指定表转 Markdown 表格(取公式**计算结果**) |
| `slide_read` | `.pptx` | 按页输出标题、正文要点、表格、演讲者备注 |
| `pdf_extract` | `.pdf` | 按页范围提文本;扫描件会明确说"没有文字层" |
所有输出都有截断上限,防止 300 页的文档把上下文撑爆。
## 快速开始
### 1. 装依赖
```bash
pip install -r requirements.txt
```
四个库各管一种格式,只读 Word 就只装 `python-docx`。
### 2. 自检
```bash
python server.py --selftest "D:\某处\报告.docx"
```
会列出工具清单,并真的读一次那份文档。
### 3. 接进 MCP 客户端
配置形如(各客户端字段名略有差异):
```json
{
"mcpServers": {
"office": {
"command": "python",
"args": ["D:\\path\\to\\office-mcp\\server.py"]
}
}
}
```
> 注意:`command` 要用**装了上述依赖的那个 Python**。如果系统 Python 没装库,就填虚拟环境或其它解释器的绝对路径。
### 4. 手动验证协议
```bash
python examples/test_client.py
```
它会把完整对话跑一遍:`initialize` → `tools/list` → 三次 `tools/call`(真实文档)→ 两个错误场景。
实测输出:
```
[id=1] initialize -> {'name': 'office-mcp', 'version': '1.0.0'} 协议 2024-11-05
[id=2] tools/list -> 4 个工具: ['doc_read', 'sheet_read', 'slide_read', 'pdf_extract']
[id=3] ✅ 返回 631 字符:
# 家庭用电分析报告.docx
(家庭用电分析报告.docx:53 段、3 表)
| 报告名称 | 家庭用电情况分析报告 |
```
## 协议实现说明
不用官方 SDK,手写不到 200 行,好处是**协议长什么样一目了然**:
```python
# 一条请求进来
{"jsonrpc": "2.0", "id": 3, "method": "tools/call",
"params": {"name": "doc_read", "arguments": {"path": "报告.docx"}}}
# 一条响应出去
{"jsonrpc": "2.0", "id": 3,
"result": {"content": [{"type": "text", "text": "# 报告.docx\n…"}], "isError": false}}
```
三个容易踩的点:
1. **stdout 只能出 JSON**。任何 `print` 调试都会污染协议流,日志必须走 `stderr`。
2. **通知没有 `id`,不能回复**。`notifications/initialized` 回了就会让客户端困惑。
3. **工具异常不能弄死服务器**。工具里任何异常都转成 `isError: true` 的响应,循环继续。
## 接入 DeepSeek Harness(实测记录)
我把这个服务接进 DSH 时踩了两个坑,都修好了,记录在这里省得别人再踩。
### 抓到的真实协议流
DSH 内置的 MCP 客户端比 2024-11-05 规范要新,握手时会先探测一个 `server/discover`:
```
IN <<< {"method":"server/discover","params":{"_meta":{
"io.modelcontextprotocol/protocolVersion":"2026-07-28", ...}}}
OUT >>> {"error":{"code":-32601,"message":"不支持的方法: server/discover"}}
IN <<< {"method":"initialize","params":{"protocolVersion":"2025-11-25", ...}}
OUT >>> {"protocolVersion":"2024-11-05","capabilities":{"tools":{}}, ...}
IN <<< {"jsonrpc":"2.0","method":"notifications/initialized"}
IN <<< {"method":"tools/list","jsonrpc":"2.0","id":1}
OUT >>> {"id":1,"result":{"tools":[{"name":"doc_read", ...}]}}
```
**好消息**:回 2024-11-05 它仍然接受,向下兼容。拒掉 `server/discover` 也不影响握手。
### 坑一:Windows 上中文变乱码,还会崩
接上后工具描述显示成 `��ȡ Word`。原因是 Python 连管道时默认用 **GBK**,而客户端按 UTF-8 解码。
更麻烦的是会**直接崩**——文档里有 `✅` 这种 GBK 编不出来的字符:
```
'gbk' codec can't encode character '\u2705' in position 22
```
双保险修法(`server.py` 里的 `_force_utf8` + `_dumps`):
```python
for stream in (sys.stdout, sys.stderr):
stream.reconfigure(encoding="utf-8", errors="replace", newline="\n")
# 万一 reconfigure 没生效,就退化成纯 ASCII 输出(中文变 \uXXXX 转义)
ascii_only = (sys.stdout.encoding or "").lower().replace("-", "") != "utf8"
json.dumps(payload, ensure_ascii=ascii_only)
```
JSON 客户端解析转义后会还原成正常中文,所以**任何控制台编码下都不会崩**。
### 坑二:回复"通知"会被判违规
JSON-RPC 规定没有 `id` 的消息是通知,**不能回复**。我早期对未知通知也回了一条 `id: null` 的错误,客户端直接判定协议违规断开。
```python
is_notification = "id" not in request
...
if is_notification:
return None
```
### 坑三:残留多个子进程 → 工具调用永远超时(最坑的一个)
症状:握手、`tools/list` 全正常,工具也确实注册上了,但一调用就 `Request timed out` 或 `Connection closed`,**服务端日志里根本收不到 `tools/call`**。
排查过程(值得记下来):
1. 以为编码问题 → 修了,没用
2. 以为协议不兼容 → 抓包发现握手完全正常
3. 以为要重启 DSH → 重启了,还是超时
4. 最后用任务管理器数进程,发现**同时有 2 个 `server.py` 在跑**
结论:调试时反复开关插件、手动杀进程,会留下**多个实例**;工具调用绑在最早那个已经死掉的实例上,请求发进了一个没有读者的管道——所以服务端永远收不到,客户端只能等到超时。
**修法**:把所有 `server.py` 进程全部结束(客户端会自动重连拉起一个干净的),再调用就通了。
**一句话教训**:MCP 是"一个连接一个进程"的模型,调试时**只留一个实例**,别靠反复开关插件去撞状态。
DSH 里不需要写插件代码,一个纯配置文件就能挂上(样例见 [`examples/dsh-bundle/`](examples/dsh-bundle/)):
```yaml
- insert:
- id: office-mcp
name: '@deepseek-ai/dsh-mcp-client'
config:
serverName: office
transport: stdio
command: '<装了依赖的那个 python 绝对路径>'
args:
- '<server.py 的绝对路径>'
failOnStartupError: true
```
装好后工具名是 `mcp__office__doc_read` 这种格式。
### 接入后的实测结果
四个工具在真实文档上都跑通了:
| 工具 | 实测对象 | 结果 |
|---|---|---|
| `doc_read` | 家庭用电分析报告.docx | 53 段 / 3 表,表格按原位置还原 |
| `sheet_read` | 家庭用电数据分析.xlsx | 识别 3 个工作表,正确读出月度用电表 |
| `slide_read` | 家庭用电情况分析.pptx | 7 页全部输出,含正文要点 |
| `pdf_extract` | 一份 1 页简历 PDF | 整页文字完整提取 |
一个实际暴露的体验问题:Excel 里算出来的 `13.290322580645162` 会原样进表格,模型没法用。已在 `_cell()` 里收敛到 4 位小数(`13.2903`),整数不带 `.0`,日期转 ISO。
### 调试建议
给 bundle 加一个环境变量就能看协议原文,排查编码/协议问题非常有用:
```yaml
env:
OFFICE_MCP_DEBUG: '1'
```
之后每次收发都会追加到 `%TEMP%\office-mcp-debug.log`,形如:
```
IN <<< {"method":"tools/call", ...}
OUT >>> {"jsonrpc":"2.0","id":2,"result":{"content":[...]}}
```
## 实现细节
### Word 按原顺序输出
`document.paragraphs` 和 `document.tables` 是两个独立列表,分别遍历会**丢掉表格和段落的相对位置**。所以直接遍历 XML body:
```python
for child in document.element.body.iterchildren():
tag = child.tag.split("}")[-1]
if tag == "p": ...Paragraph(child, document)
elif tag == "tbl": ...Table(child, document)
```
### Excel 取公式结果而不是公式
```python
openpyxl.load_workbook(path, data_only=True)
```
代价:如果文件是程序生成、**从未被 Excel 打开过**,公式没有缓存值,读出来是 `None`。这是 Excel 的机制,不是 bug。
### 截断要说清楚
不是静默截断,而是明确告诉模型:
```
…(已截断,原文 24311 字符,只返回前 8000 字符)
```
这样模型知道"我没看到全部",不会拿半份内容下结论。
## 已知限制
1. **只读**。写入工具还没做——写坏原文件是最糟的失败模式,要单独设计(先写副本、变更前备份)。
2. 不支持旧格式 `.doc` / `.xls`,请先另存为新格式。
3. **扫描件 PDF 提不出文字**(没有文字层),需要 OCR,本项目不做。
4. 复杂排版(艺术字、SmartArt、文本框嵌套)读不干净,只保证正文、表格、备注。
5. `slide_read` 不输出图表里的数据(图表是图片级对象)。
## Roadmap
- [ ] `doc_write`:Markdown → Word(先写副本,不覆盖原文件)
- [ ] `sheet_append`:往工作表追加行
- [ ] `office_convert`:docx ↔ md ↔ pdf
- [ ] 批量:一次读整个目录的文档并汇总
## License
MIT
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues