Real-time global intelligence dashboard. AI-powered news aggregation, geopolitical monitoring, and infrastructure tracking in a unified situational awareness interface
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.
Builders who want an MCP agent to draft presentations, documents, spreadsheets, and email files.
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
- 1Run
curl -L -o docker-compose.yml https://raw.githubusercontent.com/dvejsada/mcp-ms-office-docs/main/docker-compose.yml
- 2Run
cp .env.example .env
- 3Run
docker-compose up -d
README
π MCP Office Documents Server
Let your AI assistant create professional Office documents β PowerPoint, Word, Excel, emails & XML β with a single prompt.
π Table of Contents
- What is this?
- Features at a Glance
- Quick Start
- Configuration
- Markdown Reference
- Custom Templates
- Connecting Your AI Client
- Contributing
π‘ 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 Type | Tool | Highlights |
|---|---|---|
| π PowerPoint | create_powerpoint_presentation | Typed 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 |
| π Word | create_word_from_markdown | Write 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 |
| π Excel | create_excel_from_markdown | Markdown tables β .xlsx Β· Multiple sheets Β· Formulas with table-relative & cross-sheet references Β· Column data types Β· Freeze panes & auto-filter Β· Column alignment |
| π§ Email | create_email_draft | HTML email drafts (.eml) Β· Subject, recipients, priority, language |
| ποΈ XML | create_xml_file | Well-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
.docxfiles 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.ymlis 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
| Variable | Description | Default |
|---|---|---|
DEBUG | Enable debug logging (1, true, yes) | (off) |
API_KEY | Protect the server with an API key (see Authentication below) | (disabled) |
UPLOAD_STRATEGY | Where to save files: LOCAL, S3, GCS, AZURE, MINIO | LOCAL |
SIGNED_URL_EXPIRES_IN | How long cloud download links stay valid (seconds) | 3600 |
RUN_BLOCKING_BY_ASYNCIO_THREAD_ENABLED | Offload blocking tool work to a thread pool, keeping the event loop free for health probes & concurrent requests | true |
RUN_BLOCKING_MAX_WORKERS | Maximum concurrent worker threads for blocking tool calls | 4 |
π 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:
| Header | Format |
|---|---|
Authorization | Bearer your-secret-key |
Authorization | your-secret-key |
x-api-key | your-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:
| Variable | Description | Required |
|---|---|---|
S3_BUCKET | S3 bucket name | β Always |
AWS_ACCESS_KEY | AWS access key ID | β οΈ See below |
AWS_SECRET_ACCESS_KEY | AWS secret access key | β οΈ See below |
AWS_REGION | AWS region (e.g., us-east-1) | β οΈ See below |
Credential modes:
-
Explicit credentials β Set all three of
AWS_ACCESS_KEY,AWS_SECRET_ACCESS_KEY, andAWS_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_KEYenvironment 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_BUCKETis required; region is resolved automatically.
βοΈ Google Cloud Storage
Set UPLOAD_STRATEGY=GCS and provide:
| Variable | Description |
|---|---|
GCS_BUCKET | GCS bucket name |
GCS_CREDENTIALS_PATH | Path 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:
| Variable | Description |
|---|---|
AZURE_STORAGE_ACCOUNT_NAME | Storage account name |
AZURE_STORAGE_ACCOUNT_KEY | Storage account key |
AZURE_CONTAINER | Blob container name |
AZURE_BLOB_ENDPOINT | (Optional) Custom endpoint for sovereign clouds |
βοΈ MinIO / S3-Compatible Storage
Set UPLOAD_STRATEGY=MINIO and provide:
| Variable | Description | Default |
|---|---|---|
MINIO_ENDPOINT | MinIO server URL (e.g., https://minio.example.com) | (required) |
MINIO_ACCESS_KEY | Access key | (required) |
MINIO_SECRET_KEY | Secret key | (required) |
MINIO_BUCKET | Bucket name | (required) |
MINIO_REGION | Region | us-east-1 |
MINIO_VERIFY_SSL | Verify SSL certificates | true |
MINIO_PATH_STYLE | Use 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.
| Variable | Description | Required |
|---|---|---|
LIBRECHAT_SERVICE_URL | Full URL to LibreChat's service files endpoint | β |
LIBRECHAT_SERVICE_TOKEN | Service 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:
| Endpoint | Probe | Body |
|---|---|---|
GET /healthz | startupProbe β the pod has started | ok |
GET /readyz | readinessProbe β the pod can receive traffic | ready |
GET /livez | livenessProbe β the event loop is responsive; restart on failure | alive |
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):
| Parameter | Description |
|---|---|
slides | Ordered list of slide objects (below). Required. |
format | 16:9 (default) or 4:3. |
author | Stored in document properties. |
footer_text | Shown on every slide whose layout has a footer placeholder. |
show_slide_numbers | Slide numbers on every slide. |
language | BCP-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_name | Output filename without extension. |
Every slide takes type plus optional title, notes (speaker notes) and layout.
type | Fields |
|---|---|
title | subtitle? |
section | β |
content | body |
two_column | left, right β each {heading?, body} |
table | rows, align?, header_color?, zebra?, font_size? |
chart | chart_type, categories, series, legend?, data_labels?, number_format?, chart_title?, x_title?, y_title? |
scatter | series ({name, points: [[x, y], β¦]}), legend?, chart_title?, x_title?, y_title? |
image | source, caption?, body? |
quote | text, attribution? |
kpi | items ({value, label, delta?}), 2β4 read best |
timeline | steps ({label, detail?}), style? (chevron or box) |
agenda | items? β omit to build it from the deck's own section slides |
closing | subtitle?, contact? |
blank | elements β 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):
| Parameter | Description |
|---|---|
markdown_content | The document body (see syntax below) |
title / author / subject | Document properties (file metadata) |
header_text / footer_text | Text for the top/bottom of every page. Use {page} for the current page number and {pages} for the total |
include_toc | Insert an auto-updating Table of Contents at the start |
file_name | Output filename without extension |
Block elements (each on its own line, separated by blank lines):
| Syntax | Result |
|---|---|
# H1 β¦ ###### H6 | Headings 1β6 |
- item / * item / + item | Bullet list (nest by indenting children β 2-4 spaces or a tab β List Bullet 2/3) |
1. item / 2. item | Numbered 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. |
> quote | Block 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 |
 | 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 like23. Δervna 2026renders 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Γ‘ΕΓ 2026right after item2.) β write them as1\. Δervna 2026/3\. zΓ‘ΕΓ 2026.
Inline formatting (works in paragraphs, headings, list items, table cells, quotes):
| Syntax | Result |
|---|---|
**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 / syntax | Effect |
|---|---|
|:---|:---:|---:| separator | Column 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 cell | New 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):
| Parameter | Description |
|---|---|
markdown_content | Markdown containing one or more tables |
auto_filter | Apply Excel auto-filter (dropdown filters) to each table |
file_name | Output filename without extension |
Sheets & tables:
| Syntax | Effect |
|---|---|
| A | B | + |---|---| | A table becomes a block of cells |
## Sheet: Name | Start a new worksheet named Name |
# Heading above a table | Used as a title row above the table |
Formulas & references (put a formul
Files in the repo
- .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 itSign in to join the discussion.
No comments yet. Be the first to say what this is good for.
More connectors

Universal provider proxy for OpenAI Codex & Claude Code β use any LLM (Claude, Gemini, Grok, DeepSeek, Ollamaβ¦) with Codex CLI, App, SDK, and Claude Code
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.

Context7 Platform -- Up-to-date code documentation for LLMs and AI code editors
Open-source auth gateway connecting 1400+ SaaS providers to AI agents through SDK, CLI, MCP, HTTP, and OpenAPI.
Open-source 3D architectural editor with a local CLI, MCP tools, and practical workflows for humans and AI agents.