Sandbox
@ForLegalAI/mcp-ms-office-documents

MCP server for Office document creation

This server plugs an MCP client into document generation for PowerPoint, Word, Excel, email, and XML. It uses templates, Markdown parsing, and upload backends so an agent can produce files and return them locally or through cloud storage.

38 starsβ€’28 forksβ€’Pythonβ€’Updated 7d ago
Who it's for

Builders who want an MCP agent to draft presentations, documents, spreadsheets, and email files.

What it delivers

You can ask your agent for a finished Office file and get a usable download instead of raw text.

What it does

PowerPoint generation

Creates .pptx decks with typed slide layouts, sections, charts, tables, images, speaker notes, and template support.

Word from Markdown

Turns Markdown into .docx with headings, lists, tables, images, page breaks, headers, footers, and a table of contents.

Excel from Markdown

Builds .xlsx files from Markdown tables with multiple sheets, formulas, freeze panes, and auto-filter.

Email drafts

Creates .eml email drafts with subject, recipients, priority, and language fields.

XML file creation

Creates well-formed XML files and adds an XML declaration when needed.

Dynamic templates

Loads reusable YAML email templates and `.docx` templates with placeholders as separate tools.

Storage backends

Saves files locally or uploads them to S3, GCS, Azure Blob, MinIO, or LibreChat.

How to get it

  1. 1Run
    curl -L -o docker-compose.yml https://raw.githubusercontent.com/dvejsada/mcp-ms-office-docs/main/docker-compose.yml
  2. 2Run
    cp .env.example .env
  3. 3Run
    docker-compose up -d

README

πŸ“„ MCP Office Documents Server

MCP Toplist

Let your AI assistant create professional Office documents β€” PowerPoint, Word, Excel, emails & XML β€” with a single prompt.

Docker MCP License


πŸ“‹ Table of Contents


πŸ’‘ What is this?

This is an MCP (Model Context Protocol) server that runs in Docker and gives AI assistants (like Claude, Cursor, or any MCP-compatible client) the ability to generate real Office files on demand.

Just ask your AI to "create a sales presentation" or "draft a welcome email" β€” and it will produce a ready-to-use file for you.

No coding required. Install, connect, and start creating.


✨ Features at a Glance

Document TypeToolHighlights
πŸ“Š PowerPointcreate_powerpoint_presentationTyped slide schema (14 slide types incl. KPI, timeline, agenda, free-form blank) Β· Outline-pane sections Β· Markdown bullet bodies Β· Tables, category charts & XY scatter Β· Inline & data-URI images Β· Theme colours Β· Autofit with overflow warnings Β· Proofing language Β· 4:3 or 16:9 Β· Custom templates
πŸ“ Wordcreate_word_from_markdownWrite in Markdown, get a .docx Β· Headings, lists (with auto-restart), tables, links, images, block quotes, page breaks & text alignment Β· Superscript, subscript, underline & highlighted text Β· Table column alignment, borderless tables, proportional widths & multi-paragraph cells Β· Headers/footers with page numbers Β· Table of Contents Β· Custom style mapping & per-block style tags
πŸ“ˆ Excelcreate_excel_from_markdownMarkdown tables β†’ .xlsx Β· Multiple sheets Β· Formulas with table-relative & cross-sheet references Β· Column data types Β· Freeze panes & auto-filter Β· Column alignment
πŸ“§ Emailcreate_email_draftHTML email drafts (.eml) Β· Subject, recipients, priority, language
πŸ—‚οΈ XMLcreate_xml_fileWell-formed XML files Β· Auto-validates & adds XML declaration if missing

All tools accept an optional file_name parameter. When provided, the output file will use that name (without extension) instead of a randomly generated identifier.

All tools also accept an optional add_unique_prefix parameter. Left unset, it follows the storage backend: true for LOCAL/S3/GCS/AZURE/MINIO, where an 8-character UUID prefix prevents collisions in shared storage (e.g., ff8ae81d_My_Report.docx), and false for LibreChat, which adds its own UUID prefix during file storage. Set it explicitly to override β€” false gives clean filenames (e.g., My_Report.docx).

Bonus β€” Dynamic Templates:

  • πŸ“§ Reusable Email Templates β€” Define parameterized email layouts in YAML. Each becomes its own tool with typed arguments (e.g., first_name, promo_code).
  • πŸ“ Reusable Word Templates β€” Create .docx files with {{placeholders}}. Each template becomes an AI tool. Placeholders support full Markdown.

Output options:

  • Local β€” Files saved to the output/ folder
  • Cloud β€” Upload to S3, Google Cloud Storage, Azure Blob, or MinIO and get a time-limited download link

πŸš€ Quick Start

Get up and running in 3 steps:

1. Download the compose file

curl -L -o docker-compose.yml https://raw.githubusercontent.com/dvejsada/mcp-ms-office-docs/main/docker-compose.yml

Already cloned the repo? Skip this step β€” docker-compose.yml is already there.

2. Set up your environment

cp .env.example .env

The defaults work out of the box β€” files will be saved locally to output/.

3. Start the server

docker-compose up -d

βœ… Done! Your MCP endpoint is ready at: http://localhost:8958/mcp


βš™οΈ Configuration

The server is configured through environment variables in your .env file.

Basic Settings

VariableDescriptionDefault
DEBUGEnable debug logging (1, true, yes)(off)
API_KEYProtect the server with an API key (see Authentication below)(disabled)
UPLOAD_STRATEGYWhere to save files: LOCAL, S3, GCS, AZURE, MINIOLOCAL
SIGNED_URL_EXPIRES_INHow long cloud download links stay valid (seconds)3600
RUN_BLOCKING_BY_ASYNCIO_THREAD_ENABLEDOffload blocking tool work to a thread pool, keeping the event loop free for health probes & concurrent requeststrue
RUN_BLOCKING_MAX_WORKERSMaximum concurrent worker threads for blocking tool calls4
πŸ” Authentication

Set API_KEY in your .env to require an API key for all requests:

API_KEY=your-secret-key

Clients can send the key in any of these headers:

HeaderFormat
AuthorizationBearer your-secret-key
Authorizationyour-secret-key
x-api-keyyour-secret-key

Leave API_KEY empty or unset to allow all requests without authentication.

The health-probe routes (/healthz, /readyz, /livez) are the one exception β€” they stay reachable without a key so orchestrators can poll them. See πŸ₯ Performance & Health Probes below.

☁️ AWS S3 Storage

Set UPLOAD_STRATEGY=S3 and provide:

VariableDescriptionRequired
S3_BUCKETS3 bucket nameβœ… Always
AWS_ACCESS_KEYAWS access key ID⚠️ See below
AWS_SECRET_ACCESS_KEYAWS secret access key⚠️ See below
AWS_REGIONAWS region (e.g., us-east-1)⚠️ See below

Credential modes:

  • Explicit credentials β€” Set all three of AWS_ACCESS_KEY, AWS_SECRET_ACCESS_KEY, and AWS_REGION. Recommended for simple setups.

  • AWS default credential chain β€” Leave the credential variables unset and boto3 will automatically discover credentials from the standard chain:

    • AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY environment variables
    • Shared credential / config files (~/.aws/credentials)
    • AWS SSO sessions (aws sso login) β€” useful for local development
    • IRSA (IAM Roles for Service Accounts) β€” for AWS EKS deployments
    • ECS container credentials / EC2 instance metadata (IMDSv2)

    In this mode only S3_BUCKET is required; region is resolved automatically.

☁️ Google Cloud Storage

Set UPLOAD_STRATEGY=GCS and provide:

VariableDescription
GCS_BUCKETGCS bucket name
GCS_CREDENTIALS_PATHPath to service account JSON (default: /app/config/gcs-credentials.json)

Mount the credentials file via docker-compose.yml volumes.

☁️ Azure Blob Storage

Set UPLOAD_STRATEGY=AZURE and provide:

VariableDescription
AZURE_STORAGE_ACCOUNT_NAMEStorage account name
AZURE_STORAGE_ACCOUNT_KEYStorage account key
AZURE_CONTAINERBlob container name
AZURE_BLOB_ENDPOINT(Optional) Custom endpoint for sovereign clouds
☁️ MinIO / S3-Compatible Storage

Set UPLOAD_STRATEGY=MINIO and provide:

VariableDescriptionDefault
MINIO_ENDPOINTMinIO server URL (e.g., https://minio.example.com)(required)
MINIO_ACCESS_KEYAccess key(required)
MINIO_SECRET_KEYSecret key(required)
MINIO_BUCKETBucket name(required)
MINIO_REGIONRegionus-east-1
MINIO_VERIFY_SSLVerify SSL certificatestrue
MINIO_PATH_STYLEUse path-style URLs (recommended for MinIO)true

Make sure the bucket exists and your credentials have PutObject/GetObject permissions.

πŸ’¬ LibreChat Integration (Experimental)

⚠️ Note: This feature is not yet implemented in the official LibreChat repository. It requires a custom LibreChat build with MCP file artifact support. A test version is available at:

πŸ‘‰ https://github.com/geodanchev/LibreChat/tree/feature/mcp-ms-office-docs-integration

Set UPLOAD_STRATEGY=LIBRECHAT to enable seamless document generation within LibreChat conversations. Documents are uploaded to LibreChat's service endpoint and returned as MCP file artifacts that appear as attachments in the chat UI.

VariableDescriptionRequired
LIBRECHAT_SERVICE_URLFull URL to LibreChat's service files endpointβœ…
LIBRECHAT_SERVICE_TOKENService token for authentication (must match MCP_SERVICE_TOKEN in LibreChat)βœ…

Typical URLs:

  • Inside Docker network: http://api:3080/api/service/files
  • Local development: http://localhost:3080/api/service/files

Generate a service token:

openssl rand -hex 32

LibreChat librechat.yaml configuration:

mcpServers:
  Office-documents:
    type: streamable-http
    url: http://mcp-office-docs:8958/mcp
    headers:
      X-User-Id: "{{LIBRECHAT_USER_ID}}"
      X-User-Email: "{{LIBRECHAT_USER_EMAIL}}"

The X-User-Id and X-User-Email headers are automatically populated by LibreChat and used to associate uploaded files with the correct user.

πŸ₯ Performance & Health Probes

The server exposes health-check endpoints that Kubernetes (or any orchestrator) can use for startup/readiness/liveness probes. Each returns 200 with a short plain-text body:

EndpointProbeBody
GET /healthzstartupProbe β€” the pod has startedok
GET /readyzreadinessProbe β€” the pod can receive trafficready
GET /livezlivenessProbe β€” the event loop is responsive; restart on failurealive

These three routes are not protected by API_KEY. They are registered at the Starlette layer (via FastMCP's @custom_route), so they sit outside the MCP middleware stack and deliberately bypass the API-key check described under πŸ” Authentication above β€” this lets kubelet poll them without credentials. They expose no data beyond the static strings above.

Use HTTP probes rather than TCP ones: a TCP probe only proves the socket still accepts connections, so a wedged Python process with a bound listener would never be restarted. An HTTP probe forces the application itself to answer.

Thread-pool offloading: By default (RUN_BLOCKING_BY_ASYNCIO_THREAD_ENABLED=true), all blocking document-generation work is dispatched to a bounded thread pool (RUN_BLOCKING_MAX_WORKERS threads, default 4). This keeps the asyncio event loop free to respond to health probes and handle concurrent requests β€” critical for Kubernetes deployments where blocked probes lead to pod restarts.

Set RUN_BLOCKING_BY_ASYNCIO_THREAD_ENABLED=false only for local debugging or to rule out threading-related issues.


πŸ“ Markdown Reference

The Word and Excel tools accept Markdown documents; the PowerPoint tool takes structured slides whose text fields accept Markdown. These references cover everything the parsers understand β€” including features that are easy to miss.

Golden rule: separate every block element (heading, list, table, quote…) with a blank line.

πŸ“Š PowerPoint slides β€” full schema

Tool parameters (create_powerpoint_presentation):

ParameterDescription
slidesOrdered list of slide objects (below). Required.
format16:9 (default) or 4:3.
authorStored in document properties.
footer_textShown on every slide whose layout has a footer placeholder.
show_slide_numbersSlide numbers on every slide.
languageBCP-47 proofing tag, e.g. cs-CZ. Set it when the deck is not in the template's language, or Word/PowerPoint flags every word as misspelled.
file_nameOutput filename without extension.

Every slide takes type plus optional title, notes (speaker notes) and layout.

typeFields
titlesubtitle?
sectionβ€”
contentbody
two_columnleft, right β€” each {heading?, body}
tablerows, align?, header_color?, zebra?, font_size?
chartchart_type, categories, series, legend?, data_labels?, number_format?, chart_title?, x_title?, y_title?
scatterseries ({name, points: [[x, y], …]}), legend?, chart_title?, x_title?, y_title?
imagesource, caption?, body?
quotetext, attribution?
kpiitems ({value, label, delta?}), 2–4 read best
timelinesteps ({label, detail?}), style? (chevron or box)
agendaitems? β€” omit to build it from the deck's own section slides
closingsubtitle?, contact?
blankelements β€” positioned items, each {kind: text|image|shape, x, y, w, h?} with lengths in inches (1.5, "1.5in") or as a share of the slide ("40%")

Sections. Every section slide also starts a section in PowerPoint's outline pane and slide sorter, so the presenter sees the deck's structure rather than a flat list. Slides before the first section slide go in a "Default Section", as PowerPoint would name it.

Blank slides are the escape hatch for the one layout no typed slide fits. Elements draw in order, so a later one sits on top; a text element takes the same inline markdown as a bullet, a shape is one of rectangle, rounded_rectangle, ellipse, chevron, arrow with an optional fill and centred text, and an image keeps its aspect ratio within its box. Anything that would run past the slide edge is shrunk to fit and reported in warnings; anything starting off the slide is skipped and reported.

Body text. body takes either a Markdown bullet string or explicit bullet objects. Prefer the string:

{"type": "content", "title": "Q3 results",
 "body": "- Revenue **up 12%**\n  - EMEA +18%\n  - APAC +4%\n- Churn flat"}

Indent child items with any consistent unit β€” two spaces, four spaces or a tab. A line without a - marker becomes a top-level bullet. The explicit form is [{"text": "…", "level": 2}], where level is 1 (outermost) to 5.

Inline formatting works in every text field, table cells included: **bold**, *italic*, ***bold italic***, ~~strikethrough~~, __underline__, `code`, ^superscript^, ~subscript~, and [links](https://example.com). The same grammar drives the Word tool, so text formats identically in both. A marker only formats when it hugs its text (**bold**, not ** bold **), so prose like 5 * 3 * 2 = 30 is left alone. Escape a literal marker with \*, or wrap it in backticks.

Tables take raw values β€” numbers and null are fine, not just strings:

{"type": "kpi", "title": "At a glance",
 "items": [{"value": "€4.2M", "label": "ARR", "delta": "+12% vs Q2"},
           {"value": "18%", "label": "Churn"}]}
{"type": "table", "title": "Pricing",
 "rows": [["Plan", "Users", "Price"], ["Basic", 5, 9.0], ["Pro", 25, 29.0]],
 "align": ["left", "right", "right"], "header_color": "accent1"}

Colours accept 6-digit hex with or without #, or a theme name (accent1…accent6, dark1, dark2, light1, light2). Prefer a theme name so the deck follows your template's palette.

Images take an https URL or an inline data URI, so an image you already hold can be placed without publishing it first:

{"type": "image", "source": "data:image/png;base64,iVBORw0KGgo…", "caption": "Fig 1"}

Warnings. When the deck is produced but not exactly as asked β€” an image that would not load, body text shrunk to fit, a footer dropped because the layout has no placeholder β€” the result carries a warnings list alongside the file instead of leaving it in the server log:

{"file": "https://…/deck.pptx", "slide_count": 12,
 "warnings": ["slide 4: body text is about 1.9x the space available and will be shrunk to fit; consider splitting it across slides."]}

Compatibility. The previous key names (slide_type, slide_title, slide_text, indentation_level, speaker_notes, table_data, alternate_rows, image_url, image_caption, quote_text, quote_author, left_column, right_column, chart_data, has_legend, legend_position) are still accepted and mapped onto the current ones, with a note in the log. They will be removed in a future release.

Clients that cannot send objects. slides is published as one flat object schema β€” every field of every slide type in a single properties map, each saying which types accept it β€” rather than as a oneOf union of the fourteen types. A union is the better model but not a portable declaration: clients that bridge MCP to a provider without oneOf/$ref drop what they cannot express, show the model slides: array of string, and then reject the call against the union they kept, so every call fails with a schema error no matter what the model sends. Validation is unchanged β€” it runs on this server, against the union, and names the slide and field it rejected.

As a last resort a slide sent as a string is read rather than refused: a JSON object is the slide it encodes, and anything else is markdown for one slide (# Heading plus text is a title slide, otherwise a content slide with the rest as its body). The whole deck arriving as one JSON string is unpacked the same way. A string that starts like JSON but does not parse is reported instead β€” a deck truncated in transit must not quietly arrive as one slide titled with the raw JSON. This fallback covers titles and bullets only β€” send slide objects for everything else.

πŸ“ Word Markdown β€” full syntax

Tool parameters (create_word_from_markdown):

ParameterDescription
markdown_contentThe document body (see syntax below)
title / author / subjectDocument properties (file metadata)
header_text / footer_textText for the top/bottom of every page. Use {page} for the current page number and {pages} for the total
include_tocInsert an auto-updating Table of Contents at the start
file_nameOutput filename without extension

Block elements (each on its own line, separated by blank lines):

SyntaxResult
# H1 … ###### H6Headings 1–6
- item / * item / + itemBullet list (nest by indenting children β€” 2-4 spaces or a tab β†’ List Bullet 2/3)
1. item / 2. itemNumbered list (nest by indenting children). The count continues across anything written between the items (a section title, a paragraph, a bullet list, a table) as long as the numbers run consecutively; numbering restarts only where a list begins again with 1.
> quoteBlock quote (Quote style)
| A | B | + |---|---|Table (see table features below)
``` … ``` (or ~~~)Fenced code block β€” content is rendered verbatim in a monospace font and not parsed as markdown
![alt](url)Image
--- (3+ dashes)Page break (starts a new page)
*** (3+ asterisks)Horizontal line (visual separator)

⚠️ Don't confuse --- (page break) with *** (horizontal line).

πŸ’‘ Numbered paragraphs of a filing keep counting across the section titles, evidence notes and exhibit lists between them β€” write 3., 4., … and they render as one numbered list, not as paragraphs with typed-in numbers.

πŸ’‘ A single numbered line is only treated as a list when it starts at 1., continues the count of an earlier list, or is followed by another item. This means a standalone date like 23. června 2026 renders as plain text, not a list. Two cases stay ambiguous and need the dot escaped to render as text: a day-1 date (1. června 2026), which is indistinguishable from a one-item list, and a date whose day happens to be the next number in a running count (3. zΓ‘Ε™Γ­ 2026 right after item 2.) β€” write them as 1\. června 2026 / 3\. zΓ‘Ε™Γ­ 2026.

Inline formatting (works in paragraphs, headings, list items, table cells, quotes):

SyntaxResult
**bold** Β· *italic* Β· ***bold italic***Bold / italic / both
~~strikethrough~~Strikethrough
__underline__Underline (double underscore β€” not bold)
==highlight==Yellow highlight
`code`Monospace (Courier New)
^super^ Β· ~sub~Superscript (x^2^) / subscript (H~2~O)
[text](url)Hyperlink
\* \** \` \.Escaped literals (render the marker as text β€” e.g. 1\. keeps a day-1 date from becoming a list)

Nesting and combinations work, e.g. **bold with *italic* inside**, **~~bold strikethrough~~**.

Table features β€” place the directive on the line directly above the table:

Directive / syntaxEffect
|:---|:---:|---:| separatorColumn alignment: left / center / right
<!-- borderless -->Remove all borders (great for bilingual/parallel layouts)
<!-- widths: 30 70 -->Proportional column widths (any number of columns)
<br> inside a cellNew paragraph within the cell

Text alignment (HTML tags, single- or multi-line):

<center>centered text</center>
<div align="right">right-aligned</div>
<div align="justify">justified paragraph…</div>

Soft line break: end a line with two trailing spaces to break within the same paragraph.

Custom styles (issue #66) β€” remap built-in styles or apply an ad-hoc one:

<!-- style: Callout -->
This paragraph uses the "Callout" style from your template.

The <!-- style: Name --> directive applies a style to the next block only β€” one paragraph, one heading, the whole table, or the top-level items of a list (nested items keep the List Bullet 2/3 / List Number 2/3 styles). On a numbered list the style's own numbering is used: the list restarts at 1. with the style's numeral format and indents. The directive must be alone on its line, and the name must match the Word style name exactly. Unknown styles fall back to the default with a warning. To remap styles globally or per template, see Custom Templates.

πŸ“ˆ Excel Markdown β€” full syntax

Tool parameters (create_excel_from_markdown):

ParameterDescription
markdown_contentMarkdown containing one or more tables
auto_filterApply Excel auto-filter (dropdown filters) to each table
file_nameOutput filename without extension

Sheets & tables:

SyntaxEffect
| A | B | + |---|---|A table becomes a block of cells
## Sheet: NameStart a new worksheet named Name
# Heading above a tableUsed as a title row above the table

Formulas & references (put a formul

Files in the repo

Repository payloadβ€’34 top-level entries
  • .github
  • admin
  • config
  • custom_templates
  • default_templates
  • docx_tools
  • email_tools
  • pptx_tools
  • tests
  • upload_tools
  • xlsx_tools
  • xml_tools
  • .dockerignore
  • .env.example
  • .gitignore
  • AGENTS.md
  • async_runner.py
  • config.py
  • docker-compose.yml
  • Dockerfile
  • image_utils.py
  • inline_markdown.py
  • librechat_integration.py
  • LICENSE
  • main.py
  • metrics.py
  • middleware.py
  • pytest.ini
  • Readme.md
  • requirements-dev.txt
  • requirements.txt
  • ruff.toml
  • template_registry.py
  • template_utils.py

Discussion (0)

Ask about usage, or say what you built with it

Sign in to join the discussion.

No comments yet. Be the first to say what this is good for.

More connectors

Real-time global intelligence dashboard. AI-powered news aggregation, geopolitical monitoring, and infrastructure tracking in a unified situational awareness interface

86k

Universal provider proxy for OpenAI Codex & Claude Code β€” use any LLM (Claude, Gemini, Grok, DeepSeek, Ollama…) with Codex CLI, App, SDK, and Claude Code

14k
okf-memory/
okf-agent-memory

Git-native persistent memory for AI coding agents. Implements Google OKF v0.2 with sub-300Β΅s in-memory BM25 search, embedded MCP server, and progressive disclosure. Slashes token bloat by 80% with zero external databases or dependencies. Built in pure Go.

547

Context7 Platform -- Up-to-date code documentation for LLMs and AI code editors

62k
1 add

Open-source auth gateway connecting 1400+ SaaS providers to AI agents through SDK, CLI, MCP, HTTP, and OpenAPI.

5.7k

Open-source 3D architectural editor with a local CLI, MCP tools, and practical workflows for humans and AI agents.

23k