docuflow

docuflow

Provides a unified interface for AI agents and scripts to read, create, and edit office documents (.docx, .xlsx, .pdf) via MCP tools, using Markdown as the intermediary format. Supports in-place patching with stable IDs, Jinja2 templating, and formatting preservation.

Category
Visit Server

README

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.

Python License: MIT MCP Coverage


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 — teaching an AI agent how to pick the right tool

Installation

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

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

docuflow-mcp --root ~/Documents/Work --root ~/Documents/Templates

Or via environment variable:

export DOCUFLOW_ROOTS="~/Documents/Work:~/Documents/Templates"
docuflow-mcp --env-roots

Claude Desktop config (~/Library/Application Support/Claude/claude_desktop_config.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

[
  {"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:

---
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 %}
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=:

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

# 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)

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)

---
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

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 ships with the repo at .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 — it's authored in Russian and references README_RU.md.


Development

# 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 for the full version history.


License

MIT. See LICENSE.

Acknowledgements

Recommended Servers

playwright-mcp

playwright-mcp

A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.

Official
Featured
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.

Official
Featured
Local
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
graphlit-mcp-server

graphlit-mcp-server

The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.

Official
Featured
TypeScript
Kagi MCP Server

Kagi MCP Server

An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.

Official
Featured
Python
E2B

E2B

Using MCP to run code via e2b.

Official
Featured
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
Exa Search

Exa Search

A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured