Skip to main content
Glama

doc-agent-mcp

CI PyPI Python License: MIT

A Model Context Protocol server that gives AI agents stable, semantic operations on documents — instead of making them shuffle raw text.

Human ─────┐
           ↓
        Document          ← Markdown (.md/.markdown) and DOCX today,
           ↑                 Tiptap / SuperDoc / Shimo / Google Docs tomorrow
AI Agent ──┘

LLM agents editing documents as one big string break things: they mangle formatting they cannot see, lose images and comments, and cannot express "insert a paragraph after section 3". doc-agent-mcp exposes the document as a normalized, addressable structure (headings, paragraphs, list items, tables with stable IDs) and lets agents work in a safe loop:

read  →  propose change  →  inspect diff  →  apply  →  export

Nothing touches your file until apply_changes is called. Every read accepts a doc_hash, so if the file changes underneath the agent mid-task, further edits fail loudly (stale_document) instead of corrupting the file.


The problem this solves

Raw-text editing (typical today)

doc-agent-mcp

Agent rewrites the whole file to change one word

Agent replaces an exact character range in one block

DOCX round-trips through text converters destroy styles/comments

Edits are applied inside the original OOXML package; untouched content passes through

No way to review what will change before it changes

Every edit is staged with a unified diff; apply is explicit

Silent conflicts when humans edit concurrently

Content-hash optimistic locking; stale edits are rejected

Format-specific hacks hardcoded into prompts

One tool surface, any backend

Related MCP server: docx-mcp-server

Architecture

MCP interface (13 tools)
        ↓
Document operation layer      ← staging, diffs, hashes, search, sessions
        ↓                          (doc_agent_mcp/service.py)
Normalized document model     ← Block(h-0, p-1, li-2, tbl-0), Comment,
        ↓                          ProposedChange   (core/model.py)
Backend adapters              ← parse() + serialize() per format
        ↓                          (adapters/*_adapter.py)
Markdown · DOCX · future editors (Tiptap, SuperDoc, Shimo, Google Docs)

Key property: the MCP tools never know which backend is underneath. Adding a new editor backend means implementing two methods — see ADAPTER_GUIDE.md.

Installation

From PyPI (recommended for users):

pip install doc-agent-mcp

Requires Python 3.10+.

From source (development):

git clone https://github.com/xyyyang97/doc-agent-mcp.git
cd doc-agent-mcp

python3 -m venv .venv
.venv/bin/pip install -e ".[dev]"

Verify:

doc-agent-mcp --version
# doc-agent-mcp 0.1.0

MCP configuration

The server speaks standard MCP over stdio.

Claude Desktop

claude_desktop_config.json:

{
  "mcpServers": {
    "doc-agent": {
      "command": "/absolute/path/to/doc-agent-mcp/.venv/bin/doc-agent-mcp",
      "args": ["--roots", "/Users/you/Documents"]
    }
  }
}

Claude Code / Codex CLI

claude mcp add doc-agent -- /absolute/path/to/doc-agent-mcp/.venv/bin/doc-agent-mcp --roots ~/Documents

Generic MCP client (JSON)

{
  "mcpServers": {
    "doc-agent": {
      "command": "/absolute/path/to/doc-agent-mcp/.venv/bin/doc-agent-mcp",
      "args": [],
      "env": {}
    }
  }
}

--roots DIR [DIR ...] optionally restricts all reads/writes to those directories (recommended). Without it the server can touch any path its process can reach — treat server configuration like filesystem credentials.

Available tools

Read operations (never mutate)

Tool

Purpose

read_document(path, section_id?, include_spans?, doc_hash?)

Structured blocks with IDs; optional single-section view; reports unmodeled_features

get_outline(path, doc_hash?)

Headings flat + nested tree with paths

find_text(path, query, scope_element_id?, is_regex?, case_sensitive?, doc_hash?)

Exact occurrences with (element_id, start, end) offsets ready for propose_replace_text; table hits flagged editable: false

get_comments(path, doc_hash?)

Native comments (author, body, anchor element, quoted range)

Propose operations (stage a change; nothing written yet)

Tool

Purpose

propose_replace_text(path, element_id, start, end, text)

Replace character range inside one block; returns diff preview

propose_insert_block(path, anchor_id, position, kind, text, level?)

Insert paragraph/heading/list item before or after any element (covers insert-before/after/append)

propose_delete_block(path, element_id)

Delete one whole block

propose_add_comment(path, anchor_id, body, quote?, author?)

Native Word comment (DOCX); session-only for Markdown (see limitations)

Commit & review

Tool

Purpose

get_changes(path)

All staged changes with unified diffs

discard_changes(path, change_ids?)

Drop staged changes (all or selected)

apply_changes(path, change_ids?, doc_hash?)

Write to disk atomically; returns new doc_hash + warnings

export_document(path, target_format, output_path?, title?)

Convert via the model: md↔docx both directions

list_backends()

Registered backends and supported conversions

Every mutating/read call accepts the doc_hash you got from the previous call. If the file changed since (including by another process), you get {"code": "stale_document", ...} and your staged changes are dropped — re-read first.

Example workflow

This is the exact loop examples/demo_workflow.py runs (against real files):

from doc_agent_mcp.service import DocumentService

svc = DocumentService()                      # same facade the MCP tools wrap

# 1. Understand the document
outline = svc.get_outline("brief.md")
summary = next(h for h in outline["headings"] if h["title"] == "Executive Summary")
section = svc.read_document("brief.md", section_id=summary["id"])

# 2. Locate exact text
hit = svc.find_text("brief.md", "30 percent")["matches"][0]

# 3. Stage a change (file is untouched)
proposal = svc.propose_replace_text(
    "brief.md", hit["element_id"], hit["start"], hit["end"],
    "at least 30 percent (validated with finance)",
)

# 4. Review the diff
changes = svc.get_changes("brief.md")
print(changes["changes"][0]["diff"])

# 5. Commit, then export
svc.apply_changes("brief.md", doc_hash=proposal["doc_hash"])
svc.export_document("brief.md", "docx", output_path="brief.docx")

Over MCP the same steps are one tool call each — see the tool table above.

Run the full demo (Markdown + DOCX + export + stale-guard, all verified):

.venv/bin/python examples/demo_workflow.py

Sample documents live in examples/documents/: sample.md and sample.docx (the latter with two native Word comments, regenerable via scripts/make_sample_docx.py).

Error handling

All errors are structured JSON — no tracebacks across the wire:

{
  "code": "element_not_found",
  "message": "Element 'p-99' not found. Call get_outline ...",
  "details": {"element_id": "p-99"}
}

Code

Meaning

document_not_found

Path does not exist

unsupported_format

No backend for this extension

element_not_found

Stale/unknown element ID

match_not_found / ambiguous_match

Search found nothing / reserved for disambiguation

validation_error

Bad range, bad quote anchor, table-cell replace, path outside roots...

stale_document

File changed since your snapshot; staged changes were dropped

change_not_found

Unknown or already-discarded change_id

export_error

Unsupported conversion pair

Testing

.venv/bin/pip install -e ".[dev]"
.venv/bin/pytest                 # unit + integration + MCP protocol tests
.venv/bin/ruff check src tests   # lint
.venv/bin/ruff format --check .  # formatting
.venv/bin/mypy                   # strict type checking

The suite includes DOCX round-trip tests (edits verified by re-opening the saved file with python-docx and at raw OOXML level) and an end-to-end MCP test that spawns the server over stdio and speaks real protocol messages.

Limitations (by design, not by accident)

The normalized model covers what Markdown and DOCX can both represent reliably. Everything else is explicitly surfaced as unmodeled_features on every read — never silently destroyed:

  • DOCX: images/drawings, headers & footers, footnotes/endnotes, content controls, tracked changes present in source are preserved untouched but invisible to the model. Tables are plain-text cells (cell formatting not modeled). replace_text refuses paragraphs containing hyperlinks (the rewrite would destroy them).

  • Markdown: serialization is model-faithful, not byte-faithful — content survives round-trips, original line wrapping/marker style may not. Blockquotes are flattened to their paragraphs (flagged). Reference-style link definitions are resolved and inlined. Comments have no native home: propose_add_comment stores them session-only and says so.

  • Tables: searchable (flagged editable: false) but cell-level editing is not implemented yet — delete/re-insert instead.

  • Concurrent agents: last-writer-wins per file, guarded by hash checks; there is no merge engine.

Roadmap ideas

  • Table cell operations (update_table_cell)

  • Tiptap/SuperDoc adapters over their JSON models

  • Google Docs adapter via Drive API (comments map natively)

  • Anchored suggestions mode for Markdown (<!-- suggestion --> blocks)

  • Multi-file workspaces and rename-safe sessions

License

MIT

A
license - permissive license
A
quality
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

View all related MCP servers

Related MCP Connectors

  • Persistent docs and memory for AI agents — read, write, organize & search a shared workspace.

  • MCP-native collaborative markdown editor with real-time AI document editing

  • AI document editing for agents: draft, edit, export .docx/PDF. 37 MCP tools; agent self-signup.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/xyyyang97/doc-agent-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server