mcp-docgen
A Markdown-driven Model Context Protocol (MCP) server
to create, read, edit and convert Word (.docx), Excel (.xlsx),
PowerPoint (.pptx), PDF and HTML documents.
Built entirely on mature, permissively-licensed Python libraries
(python-docx,
python-pptx,
openpyxl,
reportlab,
pypdf,
markdown-it-py,
markdownify,
beautifulsoup4) — no proprietary
dependencies. MIT licensed.
Part of the Touka project: giving AI agents the ability to produce, read and edit real Office documents using only open-source building blocks.
Why
LLMs are great at producing Markdown. mcp-docgen converts Markdown to polished Office
documents — and reads them back to Markdown — so an MCP-capable assistant (Claude Desktop,
Touka, …) can run a full read → edit → write loop on real .docx / .xlsx / .pptx
/ .pdf / .html files, and convert between any of them.
Install & run
uvx mcp-docgen # once published to PyPI
# or, from a local checkout:
uv sync && uv run mcp-docgen
The server speaks MCP over stdio.
MCP client configuration
{
"mcpServers": {
"docgen": {
"command": "uvx",
"args": ["mcp-docgen"],
"env": { "MCP_DOCGEN_OUTPUT_DIR": "/absolute/path/to/workdir" }
}
}
}
From a local checkout, swap the command for:
{ "command": "uv", "args": ["run", "--directory", "/path/to/mcp-docgen", "mcp-docgen"] }
Tools
Create (Markdown / structured data → file)
| Tool | Input → Output |
|---|---|
create_docx(markdown, output_path, title?) |
Markdown → Word |
create_pptx(markdown, output_path, title?) |
Markdown → PowerPoint |
create_pdf(markdown, output_path, title?) |
Markdown → PDF |
create_html(markdown, output_path, title?) |
Markdown → HTML |
create_xlsx(sheets, output_path) |
structured rows → Excel |
Markdown features: headings, bold / italic / inline code, bullet & numbered lists
(nested), tables, block quotes, fenced code blocks, horizontal rules.
PowerPoint slide convention: # Heading starts a new slide (its title); content below
becomes bullet points; --- forces a slide break; title adds a leading title slide.
Excel sheets: [{ "name": str, "rows": [[cell, …], …], "header"?: bool }]. Cells may
be strings / numbers / booleans / null, or { "formula": "=SUM(A1:A2)" } for a live formula
(a plain =… string is kept as inert text); the first row is a bold, frozen header unless
"header": false.
Read (file → Markdown / structured data)
| Tool | Returns |
|---|---|
read_docx(input_path, base_dir?) |
{ "markdown": … } |
read_pptx(input_path, base_dir?) |
{ "markdown": … } |
read_xlsx(input_path, base_dir?) |
{ "sheets": [{ "name", "rows" }] } (round-trips with create_xlsx) |
read_pdf(input_path, base_dir?) |
{ "num_pages", "pages": […], "text" } |
read_html(input_path, base_dir?) |
{ "markdown": … } |
Reading docx/pptx to Markdown enables editing without in-place tools: read → edit the
Markdown → create_* to regenerate.
Edit (in-place, preserving the rest)
| Tool | Effect |
|---|---|
edit_xlsx(input_path, output_path, edits) |
set cells / append rows / add sheets, keeping other sheets, formulas & formatting |
append_docx(input_path, output_path, markdown) |
append Markdown content to the end |
append_pptx(input_path, output_path, markdown) |
append Markdown-derived slides to the end |
edits = { "set_cells": [{"sheet","cell","value"}], "append_rows": [{"sheet","rows"}],
"add_sheet": [{"name","rows"}] }. Any cell value may be { "formula": "=…" } for a live formula.
PDF page operations
| Tool | Effect |
|---|---|
pdf_merge(input_paths, output_path) |
concatenate PDFs in order |
pdf_split(input_path, output_dir?) |
one file per page |
pdf_extract(input_path, pages, output_path) |
extract a page subset (e.g. "1-3,5") |
Note on PDF "editing": clean open-source PDF editing means page operations (merge / split / extract), not reflowing or replacing body text — PDFs are not designed for in-place text editing. To revise PDF content, regenerate with
create_pdf.
Create/edit tools return {"path": <absolute path>}; pdf_split returns {"paths": […]}.
Convert (any format → any format)
| Tool | Effect |
|---|---|
convert_document(input_path, output_path) |
convert between formats — both inferred from the file extensions |
Conversions among html / docx / pptx read straight into a rich intermediate model, so
colour, font, size, alignment, images and table cell spans survive — no lossy Markdown
round-trip. xlsx → xlsx is lossless (styles, formulas, merges). Other pairings degrade
gracefully. The result reports an honest fidelity plus a warnings list of what that
particular pairing drops:
| Fidelity | When | What it means |
|---|---|---|
rich |
html · docx · pptx among themselves, and xlsx → xlsx |
styling preserved (colour / font / align / images / cell spans; formulas for xlsx) |
clean |
md → a flow format |
structure preserved (Markdown carries no styling to begin with) |
lossy |
any → pptx; xlsx ↔ documents |
slides split on # headings; only tables map to / from worksheets |
text-only |
pdf as a source |
pypdf extracts plain text only; layout, styling and tables are lost |
Honest limits: a PDF source is plain text (the open-source ceiling); Markdown can't carry styling, so any
.mdstep is structure-only. A few write-side gaps remain (docx cell shading), and only inline-styleHTML is read.
convert_document returns { "path", "source_format", "target_format", "fidelity", "warnings" }.
Directories & safety
- Output files are written inside
MCP_DOCGEN_OUTPUT_DIR(default./out). - Input files (read / edit) are read from
MCP_DOCGEN_INPUT_DIR(default = the output dir), so a read → edit → write loop shares one working directory. - Every tool accepts an optional
base_dirto use as the root for that single call (it roots both reads and writes), e.g. a host that jails each call to one user's directory. WhenMCP_DOCGEN_INPUT_DIRis configured, a per-callbase_dirmay only narrow within it — one that escapes the configured boundary is rejected. - Every path is interpreted relative to its base; any path escaping it (via
..or an absolute path) is rejected, missing inputs and wrong suffixes raise errors. - When a per-call
base_diris supplied withoutMCP_DOCGEN_INPUT_DIRset, it is trusted as given — the host is responsible for validating it (the trust boundary is the host). A host that would rather fail closed can setMCP_DOCGEN_STRICT_BASE_DIR=1to cap anybase_dirat the output root even when no input root is configured. - The server makes no network calls and spawns no subprocesses.
- Generated output is hardened against injection from untrusted source content: HTML text,
inline styles and URLs are escaped / scheme-allowlisted, and spreadsheet text that looks like
a formula (
=…) is neutralized so it can't execute in Excel. - Process only trusted input files. Documents are parsed by third-party libraries
(
python-docx,openpyxl,python-pptx,pypdf); a hostile archive could attempt a decompression / XML-expansion bomb. Keep the pinned dependency versions current.
Examples
uv run python examples/generate_samples.py # create report.docx / review.pptx / sales.xlsx
uv run python examples/roundtrip_demo.py # create → read → edit → PDF → convert round-trip
Development
uv sync
uv run pytest
uv run ruff check .
License
MIT © 2026 Touka Project — see LICENSE.
Powered by python-docx, python-pptx, openpyxl, reportlab and pypdf; Markdown parsing by markdown-it-py; HTML reading by markdownify and BeautifulSoup. All MIT/BSD licensed.
Metadata
Release files for mcp-docgen 0.8.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| mcp_docgen-0.8.0.tar.gz | 37.7 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| mcp_docgen-0.8.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 84.6 kB
Release files / mcp_docgen-0.8.0.tar.gz
| Download URL | mcp_docgen-0.8.0.tar.gz |
|---|---|
| Size | 37.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ae1ad44904a9a7426a8603830ad1fe09603fba827a2480b3128e29350a05e55f
|
|
BLAKE2b-256 checksum How to use checksums |
fc4a245d9d5e7d7e26a3287d3eb372202e8820bfbecd1859d41aace298152075
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.10.5 {"installer":{"name":"uv","version":"0.10.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / mcp_docgen-0.8.0-py3-none-any.whl
| Download URL | mcp_docgen-0.8.0-py3-none-any.whl |
|---|---|
| Size | 47.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d450c65f812cd681a7b0f9a687321b6fb1acf0ebb060b5165a017e9f746bacc0
|
|
BLAKE2b-256 checksum How to use checksums |
f559463eef5059325c3f3912f2b6779bf71bb747fa6be43e08e73a6269799cf6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.10.5 {"installer":{"name":"uv","version":"0.10.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Security for mcp-docgen 0.8.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.