docuflow
by deja111vu
README.md
# docuflow
> A unified interface for AI agents and scripts to work with office documents: `.docx`, `.xlsx`, `.pdf` via MCP tools and a Python library. Markdown is the lingua franca for reading, templating, and patching.
[](https://www.python.org)
[](LICENSE)
[](https://modelcontextprotocol.io)
[](https://github.com/deja111vu/docuflow/actions)
---
## Why
Documents in `.docx`/`.xlsx` share the same logic (headings, paragraphs, tables), but each library has its own API — and any non-trivial edit usually breaks the formatting (fonts, line spacing, alignment, table borders, headers/footers, `sectPr`).
`docuflow` solves this with three ideas:
1. **Markdown as the lingua franca.** `read → markdown + outline + tables`; `create/render → markdown → docx`. The agent works with familiar text instead of raw XML.
2. **Patches by stable IDs.** `apply_patches` with `target_id="h:0.1.0"` / `"t:0"` — `replace` / `insert_after` / `delete` / `append` without losing formatting.
3. **`.docx` templates as a Normal-style source.** Point at a reference document — the new file inherits the font, size, line spacing, page margins, and headers/footers. The "Times New Roman 14 / 1.5 / justified" template look is no longer lost.
---
## Features
- 📖 **Read** documents as markdown + structural outline + tables (`.docx`, `.xlsx`, `.pdf`)
- ✍️ **Create** from markdown (with Jinja2 templating and YAML frontmatter)
- 🔧 **In-place patches** by stable IDs — `replace`, `insert_after`, `delete`, `append`
- 🛡 **Run-aware in-place replacement** that preserves bold, italic, color — both inside and outside the match
- 🧬 **Inline styles** (Pandoc-like): `{#id .class key=value}` on headings, `{color="red" bold}…{/}` on paragraphs
- 📐 **Template inheritance** — fonts, sizes, margins, `sectPr` from a reference `.docx`
- 🔍 **Bulk search/replace** (including regex) with run-aware formatting preservation
- 🔒 **Sandbox** with a directory whitelist and path-traversal protection
- 📕 **PDF reading** via `opendataloader-pdf` (Java 11+)
- 🤖 **MCP server** with 11 tools for Claude Desktop / Cursor / Claude Code
- 🎓 **Claude skill** at [`.claude/skills/docuflow-using/SKILL.md`](.claude/skills/docuflow-using/SKILL.md) — teaching an AI agent how to pick the right tool
---
## Installation
```bash
pip install docuflow
# optional: PDF reading via opendataloader-pdf
pip install 'docuflow[pdf]'
```
Requirements:
- Python 3.11+
- Java 11+ (only for PDF reading; everything else works without it)
---
## Quick start
### Python API
```python
from pathlib import Path
from docuflow import Sandbox
from docuflow.formats import build_default_registry
sb = Sandbox(roots=[Path.home() / "Documents" / "Work"])
reg = build_default_registry()
# Read
content = reg.get_reader("docx").read(sb.resolve("report.docx"))
print(content.markdown)
for node in content.outline:
print(node.id, "—", node.title)
# Create from markdown
reg.get_writer("docx").write(
sb.resolve("new.docx"),
type(content)(format="docx", markdown="# Title\n\nBody.",
outline=[], tables=[], metadata={}),
template=sb.resolve("reference.docx"), # ← inherits fonts, margins, sectPr
)
# In-place patches (preserve fonts, alignment, table borders)
from docuflow.core import EditPatch
reg.get_editor("docx").apply(sb.resolve("report.docx"), [
EditPatch(op="replace", target_id="h:0", content="# Renamed"),
EditPatch(op="replace", target_id="t:0", content="| A | B |\n|---|---|\n| 1 | 2 |"),
EditPatch(op="insert_after", target_id="h:0.0", content="## New section"),
])
```
### MCP server
```bash
docuflow-mcp --root ~/Documents/Work --root ~/Documents/Templates
```
Or via environment variable:
```bash
export DOCUFLOW_ROOTS="~/Documents/Work:~/Documents/Templates"
docuflow-mcp --env-roots
```
Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json`):
```json
{
"mcpServers": {
"docuflow": {
"command": "docuflow-mcp",
"args": ["--root", "/Users/me/Documents/Work"]
}
}
}
```
---
## MCP tools (11)
| Group | Tool | Purpose |
| --- | --- | --- |
| Read | `read_document` | markdown + outline + tables |
| | `get_outline` | structure only (cheap for large files) |
| | `extract_tables` | tables only |
| Create | `create_document` | new docx/xlsx from markdown |
| | `render_template` | Jinja2 + YAML frontmatter → docx/xlsx |
| | `list_template_variables` | which `{{variables}}` a template needs |
| Edit | `edit_document` | rewrite from markdown (legacy) |
| | `apply_patches` | **in-place** patches by ID (recommended) |
| | `find_and_replace` | **in-place** search/replace (run-aware) |
| Meta | `get_document_info` | author, size, pages |
| | `get_sandbox_roots` | available directories |
### Edit pattern: outline → patches
```json
[
{"op": "replace", "target_id": "h:0", "content": "# Renamed"},
{"op": "insert_after", "target_id": "h:1.0", "content": "## New section\n\nBody."},
{"op": "replace", "target_id": "t:0", "content": "| A | B |\n|---|---|\n| X | Y |"},
{"op": "delete", "target_id": "h:2.1"},
{"op": "append", "content": "# Tail\n\nFinal paragraph."}
]
```
Identifiers:
- **`h:N.N.N`** — heading (N — 0-indexed sibling position; number of segments = level).
- **`t:N`** — table (N — global index in the document).
- `h:0` / `h:0.0` / `h:0.0.0` — 1st, 2nd, 3rd level respectively.
**Why not `edit_document` for patches?** A full rewrite loses styles, fonts, headers/footers, and embedded images. Use `apply_patches` for targeted changes.
---
## Templates
### Jinja2 + YAML frontmatter
`templates/contract.md`:
```markdown
---
template_version: 1
style:
default_font: Times New Roman
default_size: 12
align: justify
---
# Contract No. {{number}}
City of {{city}}, {{date}}
{% for stage in stages %}
- **Stage {{loop.index}}**: {{stage.description}} — {{stage.amount}} ₽
{% endfor %}
```
```python
render_template(
template_path="templates/contract.md",
output_path="contracts/2026-001.docx",
data={"number": "2026-001", "city": "Moscow", "date": "2026-06-17",
"stages": [{"description": "Prepayment", "amount": "100000"},
{"description": "Settlement", "amount": "900000"}]},
)
```
`render_template` is strict-undefined: a missing variable raises `TemplateRenderError`. For large templates call `list_template_variables` first and diff against `data`.
### `.docx` template as a formatting source
If you need a specific look ("Times New Roman 14, line spacing 1.5, 2 cm margins, table borders"), pass a reference `.docx` to `template=`:
```python
writer.write(out, content, template=Path("reference.docx"))
```
The writer clears the reference body and fills it with markdown, inheriting the `Normal` style, `sectPr` (page margins, page size, headers/footers), and table-style definitions. If the reference is minimal (missing `Heading 1`, `List Bullet`, `Table Grid`), those styles are auto-created.
---
## Inline styles (Pandoc-like)
### Heading attributes
```markdown
# Introduction {#intro}
## Methods {#methods color="#1f4e79" align="center" bold}
# Sheet: Data {#hdr bold color="#1f4e79"}
```
- `#id` — fixes the node ID in the outline.
- `.class` — classes (`.foo .bar`).
- `key=value` — properties (`color`, `align`, `bold`, `size`, `font`, `italic`, `underline`).
- A bare token (no value) is treated as `=true` (`bold` → `bold=true`).
Color is `#rrggbb` or `#rgb` only. For `.docx` the attributes are applied to all runs of the heading. For `.xlsx` — to the first header row of the sheet.
### Inline spans in paragraphs (`.docx` only)
```markdown
Text with {color="red" bold}important{/} part.
```
Syntax `{attrs}text{/}` — text between `{` and `{/}` receives the attributes. The closing `{/}` is required. Supported attributes: `color`, `bold`, `italic`, `underline`, `size`, `font`.
### Document style (YAML frontmatter)
```markdown
---
title: Quarterly Report
author: Analytics
style:
default_font: Arial
default_size: 11
align: justify
---
```
For `.docx`, `default_font` / `default_size` / `align` apply to the `Normal` style. For `.xlsx`, they apply to cells from the 2nd row on (the header row stays as is).
### Heading IDs
Auto-generated as **0-indexed hierarchical** (`h:0`, `h:0.0`, `h:0.1.0`). `{#custom-id}` overrides.
---
## Architecture
```text
docuflow/
├── core/ ← models, sandbox, markdown, registry
├── formats/
│ ├── docx/ ← Reader, Writer, Editor (in-place)
│ ├── xlsx/ ← Reader, Writer, Editor
│ └── pdf/ ← Reader (opendataloader-pdf)
└── mcp_server.py ← MCP tools
.claude/skills/
└── docuflow-using/ ← Claude skill for agent integration
```
**Two editor modes:**
- **markdown-roundtrip** (`apply_markdown`): rewrite the document from markdown — for "create from scratch".
- **in-place** (`apply`, `find_and_replace_in_place`): modify XML elements directly, preserving run fonts, table borders, line spacing, `sectPr`, headers/footers. **Recommended** for edits.
**In-place `find_and_replace`** runs in run-aware mode: for each match it splits the first and last run at the match boundaries, removes runs that fall entirely inside the match, and inserts a single new run with the replacement text, inheriting the first affected run's formatting. Bold/italic/color **outside** the matched fragment is preserved. Paragraph-level format (alignment, indent, line-spacing, style) is always preserved.
---
## Security
All paths pass through `Sandbox.resolve()`. Attempts to escape the allowed roots raise `PathSecurityError`. Symlinks are resolved; relative paths are resolved against the first root.
---
## Limitations
| Preserved | NOT preserved from `.docx` template |
| --- | --- |
| `Normal` style (font, size) | Page headers/footers (unless via `template=`) |
| `sectPr` (margins, page size, orientation) | Section breaks, page numbering |
| `paragraph_format` (line-spacing, alignment) | Embedded images, OLE objects |
| `Table Grid` (borders, if the style exists) | Track changes, comments |
| Headers/footers (when `template=` is used) | |
For full visual fidelity with a complex template, use `template=` + `apply_patches` — the markdown parser keeps most of the template's formatting because you're editing text rather than recreating the document.
---
## Common errors
- **Path outside sandbox** → `PathSecurityError`. Don't try to bypass via `..` — it's blocked. Ask to add a root.
- **Unknown format** → `UnsupportedFormatError`. Supported: `docx`, `xlsx`, `pdf` (read-only).
- **Missing template variable** → `TemplateRenderError`. Strict-undefined by design.
- **`edit_document` instead of `apply_patches` and lost formatting** → restore from a backup; next time use patches.
- **PDF without Java** → `PdfJavaNotFoundError`. Install JDK 11+.
---
## Claude skill
A [Claude skill](https://docs.claude.com/en/docs/agents-and-tools/agent-skills/overview) ships with the repo at [`.claude/skills/docuflow-using/SKILL.md`](.claude/skills/docuflow-using/SKILL.md). It teaches the agent:
- which MCP tool to pick for a given task (read / create / patch / template),
- the outline→patches workflow and how to read stable IDs (`h:N.N.N`, `t:N`),
- inline-styles syntax (Pandoc-like),
- the correct workflow for full template fidelity (`template=` + `apply_patches`),
- common errors and their fixes.
For Russian-speaking agents the same skill is available at [`.claude/skills/docuflow-using/SKILL.md`](.claude/skills/docuflow-using/SKILL.md) — it's authored in Russian and references [README_RU.md](README_RU.md).
---
## Development
```bash
# Clone
git clone https://github.com/deja111vu/docuflow.git
cd docuflow
# Create venv
python -m venv .venv
. .venv/bin/activate # Linux/macOS
.venv\Scripts\activate.bat # Windows
# Install editable + dev deps
pip install -e '.[dev,pdf]'
# Tests + coverage (≥85%)
pytest --cov=docuflow
# Lint
ruff check src tests
mypy src/docuflow
```
### Test stack
- pytest + pytest-cov
- hypothesis (property-based tests for sandbox and outline)
- All tests are isolated: `tmp_path` + `Sandbox(roots=[tmp_path])`
### CI
GitHub Actions runs `ruff`, `mypy --strict`, `pytest --cov` on Python 3.11 / 3.12 / 3.13.
---
## Roadmap
- **v0.2.0** (current): in-place editor, run-aware replace, template-aware writer, auto-style creation, inline styles, PDF reader.
- **v0.3.0**: pptx (read/write/edit), track changes, comments.
- **v0.4.0**: bulk operations (merge, split), benchmarks, performance budgets.
See [CHANGELOG.md](CHANGELOG.md) for the full version history.
---
## License
MIT. See [LICENSE](LICENSE).
## Acknowledgements
- [python-docx](https://github.com/python-openxml/python-docx) — docx engine core.
- [openpyxl](https://openpyxl.readthedocs.io/) — xlsx engine core.
- [opendataloader-pdf](https://github.com/opendataloader-project/opendataloader-pdf) — PDF reader.
- [markdown-it-py](https://github.com/executablebooks/markdown-it-py) — markdown parser.
- [MCP](https://modelcontextprotocol.io) — agent integration protocol.This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues