Skip to main content
Glama

word-mcp

An MCP server that measures and fixes the page layout of .docx documents using a real word processor rather than a converter. Converters paginate differently from Word, and over a few hundred pages the drift adds up, so the numbers here come from the application itself.

Platform

Driven through

Whose pagination

macOS

Microsoft Word over Apple Events

Word

Windows

Microsoft Word over COM

Word

Linux

LibreOffice over UNO

LibreOffice

The engine follows the platform; WORD_MCP_BACKEND=mac|win|uno overrides it. Every answer carries a fidelity field saying whose numbers these are.

Example

Asking a document where its parts begin and how long they are:

>>> word_status()
{'engine': 'libreoffice', 'fidelity': 'libreoffice', 'available': True,
 'version': '/usr/bin/soffice', 'document': None, 'detail': None}

>>> word_open('/tmp/report.docx')
'/tmp/report.docx'

>>> word_outline()
{
  "total_pages": 13,
  "fidelity": "libreoffice",
  "sections": [
    { "name": "Introduction", "page":  1, "pages": 2 },
    { "name": "Background",   "page":  3, "pages": 3 },
    { "name": "Method",       "page":  6, "pages": 3 },
    { "name": "Results",      "page":  9, "pages": 2 },
    { "name": "Conclusions",  "page": 11, "pages": 2 },
    { "name": "Appendix A",   "page": 13, "pages": 1 }
  ]
}

>>> word_find_page('Complexity')
{'text': 'Complexity', 'pages': [7, 7, 7, 7, 8, 8]}

Keeping a caption attached to the figure above it, so a page break never separates them:

>>> word_format_paragraphs(contains='Figure 1.', keep_with_next=True, widow_control=True)
1
>>> word_save()
'saved'

Sections can also be defined by text instead of by heading level, which is useful when a document has no outline levels set:

>>> word_outline(markers=['Introduction', 'Method', 'Conclusions'])

The last occurrence of each marker is taken, so mentions inside a table of contents are skipped.

Related MCP server: docx-mcp

Layout

wordmcp/
├── models.py          immutable result and parameter types
├── engine.py          DocumentEngine: the contract an engine fulfils
├── engines/
│   ├── __init__.py    engine selection; dependencies are passed in, not read from the module
│   ├── applescript.py Word on macOS
│   ├── com.py         Word on Windows
│   └── uno.py         LibreOffice
├── service.py         document operations, expressed once
├── tools/
│   ├── session.py     open, close, save, status
│   ├── measure.py     page count, search, headings, outline, page text, self-test
│   ├── modify.py      formatting, field updates, PDF export
│   └── escape.py      engine-native code, registered only when allowed
├── server.py          assembles a server from the tool groups
└── __main__.py        composition root: pick an engine, serve over stdio

Each tool module exposes register(mcp, service) and keeps nothing at module scope, so the groups stay independent and no global server instance is needed. Tool bodies are one line: they delegate to the service, which holds the logic and is what the tests exercise.

Dependencies are injected through constructors: an engine goes into DocumentService, the service goes into create_server. There is no global state, so the logic runs against a stub:

server = create_server(DocumentService(StubEngine()), allow_raw=False)

Tools

Tool

What it does

word_status

which engine is active, whether it answers, what is open

word_open / word_close / word_save

open, close, save

word_page_count

number of pages

word_find_page

pages on which a string occurs

word_headings

headings with outline level and page

word_outline

start page and length of every top-level part

word_page_text

text placed on a given page

word_update_fields

refresh fields and tables of contents

word_format_paragraphs

keep with next, keep together, widow control, spacing

word_export_pdf

export to PDF with the application's own engine

word_selftest

run every operation and report what this machine supports

word_run_raw

engine-native code; registered only when WORD_MCP_ALLOW_RAW=1

Installing

git clone https://github.com/<user>/word-mcp.git
cd word-mcp
uv sync

macOS. Needs Microsoft Word and automation permission: System Settings, Privacy and Security, Automation, the application running this server, Microsoft Word. The first call raises the system prompt.

Windows. Needs Microsoft Word: uv sync --extra windows, which adds pywin32.

Linux. Needs LibreOffice and the system Python-UNO package: sudo apt install libreoffice python3-uno. The server starts soffice headless by itself; WORD_MCP_UNO_PORT sets the socket. Run it with the interpreter that can see uno, which is usually the system python3.

Connecting

{
  "mcpServers": {
    "word": {
      "command": "uv",
      "args": ["run", "--directory", "/path/to/word-mcp", "word-mcp"],
      "env": { "WORD_MCP_ALLOW_RAW": "1" }
    }
  }
}

On Linux the system interpreter is usually required:

{
  "mcpServers": {
    "word": {
      "command": "python3",
      "args": ["-m", "wordmcp"],
      "env": { "PYTHONPATH": "/path/to/word-mcp" }
    }
  }
}

WORD_MCP_ALLOW_RAW=1 registers word_run_raw, which runs code in the engine's own language: AppleScript on macOS, a Python expression over the live application objects on Windows and Linux. It exists so a failing call can be repaired without editing the server, but it is arbitrary code execution; drop the variable once things settle.

Restart the client after changing its configuration.

Development

uv run pytest        # service logic against a stub engine, no word processor needed
uv run ruff check .

State of testing

The LibreOffice engine has been exercised on a real 161-page document: page count, text search, heading map, section lengths, page text, paragraph formatting, field updates and the escape hatch.

The Word engines for macOS and Windows follow documented interfaces but have not been run on a live machine. Start with word_selftest. The likely places to adjust on macOS are compute statistics and get information ... information type page number; the corresponding COM calls on Windows are more stable.

Licence

MIT

Available Tools

13 tools
word_closeWord CloseA

Close the current document, saving it or discarding changes.

Inputs: save. Returns: result

word_export_pdfWord Export PdfC

Export the document to PDF with the word processor's own engine.

Inputs: path, pdf_path. Returns: Where the exported PDF landed.

word_find_pageWord Find PageC

List the pages on which a piece of text occurs, in document order.

Inputs: text (required), path, limit. Returns: Pages on which a search string occurs, in document order.

word_format_paragraphsWord Format ParagraphsA

Set layout properties on every paragraph containing a string.

keep_with_next ties a heading or a caption to what follows, keep_together forbids splitting a paragraph across pages, widow_control switches widow and orphan control on. Spacing is in points. Returns how many paragraphs changed.

Inputs: contains (required), path, space_after, space_before, +3 more. Returns: result

word_headingsWord HeadingsC

List every heading with its outline level and the page it starts on.

Inputs: path, max_level. Returns: result

word_openWord OpenC

Open a document and make it the current one.

Inputs: path (required). Returns: result

word_outlineWord OutlineB

Give the start page and length of each top-level part of the document.

Pass markers to define the parts by text instead of by heading level.

Inputs: path, markers. Returns: Section map of a document.

word_page_countWord Page CountC

Count the pages of a document as the word processor lays it out.

Inputs: path. Returns: result

word_page_textWord Page TextC

Read the text that the word processor placed on a given page.

Inputs: page (required), path. Returns: Text the engine placed on one page.

word_saveWord SaveB

Save the current document in place.

Inputs: no named parameters. Returns: result

word_selftestWord SelftestC

Try every operation on a document and report which ones this machine supports.

Inputs: path (required). Returns: result

word_statusWord StatusB

Report which word processor drives documents here and whether it answers.

Inputs: no named parameters. Returns: What is driving documents on this machine and whether it answers.

word_update_fieldsWord Update FieldsC

Refresh fields and tables of contents so their page numbers become current.

Inputs: toc, path. Returns: Outcome of refreshing fields and tables of contents.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    B3.2/5.0

    Scored across 13 tools

    Disambiguation4/5

    Most tools target clearly distinct operations: lifecycle (open/close/save), page inspection, text search, outline extraction, formatting, field refresh, and PDF export. The main possible overlap is between word_headings and word_outline, but their descriptions distinguish all headings from customizable top-level parts.

    Naming Consistency4/5

    All tools use a consistent word_ prefix and snake_case naming, making the family easy to recognize. The only minor deviation is that some names are noun-like (word_page_count, word_headings) while others are verb-like (word_open, word_find_page).

    Tool Count5/5

    With 13 tools, the server sits comfortably in the well-scoped range. Each tool appears to cover a specific document operation or inspection task without excessive fragmentation.

    Completeness3/5

    The surface covers opening, saving, closing, layout inspection, page text reading, paragraph formatting, field updates, and PDF export. However, it lacks document creation and direct content editing operations such as inserting or replacing text, which are notable gaps for a Word automation server.

    Related MCP Connectors

    Related MCP Servers