# libre-mcp > A stdio MCP server that controls a headless LibreOffice over UNO. Use it to > build and export real office deliverables — Writer documents, Calc > spreadsheets, and Impress presentations — then save them as PDF, DOCX, XLSX, > PPTX, and more. This file is the minimal guide an agent needs to use it. ## Model - Call `create_document` (or `open_document`) to get a `doc_id`. Every other tool takes that `doc_id`. Documents stay open across calls until `close_document`. - Workflow: **create/open → edit → `export_document`** to the final format. - Export/save paths must be **absolute**. The format is inferred from the file extension, or pass `format` explicitly. - LibreOffice starts on the first tool call. Reuse a `doc_id` to keep editing the same document. ## Tools Documents - `create_document(kind)` — kind: `writer` | `calc` | `impress` | `draw` → `{doc_id, kind}` - `open_document(path)` → `{doc_id, kind, path}` - `list_documents()` → `{documents}` · `document_info(doc_id)` → `{kind, url, modified, rev, seen}` - `save_document(doc_id, path?, format?)` — save in place, or a copy to `path` - `export_document(doc_id, path, format?)` — pdf, docx, xlsx, pptx, csv, odt/ods/odp, html, txt - `close_document(doc_id, force?)` — refuses if there are unsaved changes; pass `force` to discard Writer — basic - `get_text(doc_id)` → `{text}` - `insert_text(doc_id, text, paragraph_break?)` — append; set `paragraph_break` for a new paragraph - `find_and_replace(doc_id, search, replace, regex?)` → `{count}` Writer — rich (for branded documents: reports, letterheads, one-pagers). Sizes in points, spacing/margins in cm, colors hex. - `page_setup(doc_id, margin?, top?, color?)` — margins (cm); `top` clears a header band; `color` = page background - `add_paragraph(doc_id, text, size?, color?, bold?, italic?, align?, font?, space_before?, space_after?, style?)` — the workhorse; large + bold = a heading; `align` left|center|right|justify - `add_list(doc_id, items, ordered?, size?, color?)` — bullets, or numbered if `ordered` - `insert_table(doc_id, rows, header?, accent?, color?, size?)` — `rows` is a 2D list; `header` styles row 0 with the accent fill - `insert_image(doc_id, path, width_cm?)` — embed an image inline, aspect preserved - `add_page_box(doc_id, x, y, w, h, text?, fill?, color?, size?, bold?, align?, font?)` — a page-anchored band/callout; x/y/w/h are PERCENT of the PAGE; give it a `fill` (transparent frames aren't reliable across builds) Calc - `set_cells(doc_id, cells, sheet?)` — `cells`: `[{cell, value?|formula?}]`; a `formula` starts with `=` - `read_cells(doc_id, range, sheet?)` → `{values: [[...]]}` Impress — basic - `add_slide(doc_id, layout?)` — a new deck already has slide `0` → `{index, count}` - `set_slide_content(doc_id, index, title?, bullets?)` — `bullets`: one string per line - `list_slides(doc_id)` → `{slides, count}` · `delete_slide(doc_id, index)` - `read_slide(doc_id, index)` → `{index, title, bullets, text}` — `text` is every string on the slide (incl. graphics-built textboxes/shape labels) Impress — graphics (for designed, engaging decks). x/y/w/h are PERCENT (0-100) of the slide; colors are hex. - `set_presentation_size(doc_id, preset)` — `"16:9"` (default) or `"4:3"`; call once first - `set_slide_background(doc_id, slide, color, color2?, angle?)` — solid, or gradient color→color2 at `angle`°; call FIRST on each slide (stacks to the back) - `add_textbox(doc_id, slide, text, x, y, w, h, size?, color?, bold?, italic?, align?, valign?, font?)` — `size` in points; `align` left|center|right; `valign` top|center|bottom; `\n` for line breaks - `add_shape(doc_id, slide, x, y, w, h, shape?, fill?, fill2?, angle?, corner?, line?, line_width?, text?, text_color?, text_size?, text_bold?)` — `shape` rect|round|ellipse|line; `fill2` makes a gradient; `text` is centered inside - `add_image(doc_id, slide, path, x, y, w, h)` — absolute `path` ## Recipes Branded one-pager → PDF (header band, heading, body, list, table) ``` create_document {"kind": "writer"} page_setup {"doc_id": "doc-1", "margin": 2.3, "top": 3.8} add_page_box {"doc_id": "doc-1", "x": 0, "y": 0, "w": 100, "h": 9, "fill": "#c2410c"} add_page_box {"doc_id": "doc-1", "x": 10, "y": 2.5, "w": 55, "h": 5, "text": "Acme Report", "fill": "#c2410c", "color": "#ffffff", "size": 26, "bold": true} add_paragraph {"doc_id": "doc-1", "text": "Q3 in review", "size": 20, "bold": true, "space_after": 0.3} add_paragraph {"doc_id": "doc-1", "text": "Revenue grew 20% QoQ as the new line shipped.", "align": "justify", "space_after": 0.4} add_list {"doc_id": "doc-1", "items": ["Launched in 3 regions", "Churn down 8%", "NPS up to 61"]} insert_table {"doc_id": "doc-1", "rows": [["Metric", "Q2", "Q3"], ["Revenue", "$1.2M", "$1.44M"], ["Customers", "820", "910"]]} export_document {"doc_id": "doc-1", "path": "/abs/path/report.pdf"} ``` Note: give page boxes a `fill` (the title box uses the band's color so it blends); transparent frames aren't reliable across LibreOffice builds. Calc spreadsheet → XLSX ``` create_document {"kind": "calc"} set_cells {"doc_id": "doc-1", "cells": [ {"cell": "A1", "value": "Item"}, {"cell": "B1", "value": "Qty"}, {"cell": "B2", "value": 7}, {"cell": "B3", "formula": "=SUM(B2:B2)"}]} read_cells {"doc_id": "doc-1", "range": "A1:B3"} export_document {"doc_id": "doc-1", "path": "/abs/path/sheet.xlsx"} ``` Impress deck → PPTX ``` create_document {"kind": "impress"} set_slide_content {"doc_id": "doc-1", "index": 0, "title": "Project Kickoff", "bullets": ["Goals", "Timeline", "Owners"]} add_slide {"doc_id": "doc-1"} set_slide_content {"doc_id": "doc-1", "index": 1, "title": "Timeline", "bullets": ["Q1: design", "Q2: build", "Q3: ship"]} export_document {"doc_id": "doc-1", "path": "/abs/path/deck.pptx"} ``` ## Engaging presentations (patterns) `set_slide_content` (title + bullets) is fine for plain decks. For decks that actually look designed, skip it and compose with the graphics tools. What works: - **Pick a palette and reuse it on every slide** — one dark background, one accent, a light "ink", and a muted grey. E.g. bg `#15110f`, accent `#c2410c`, ink `#f4efe7`, muted `#9a8f84`. Consistency is what reads as "designed". - **Background-first.** Call `set_slide_background` before anything else on a slide (it stacks behind). A subtle gradient (`color2` + `angle`) adds depth. - **Position by percent**, and keep a margin (≈6-8% left). For a *square* element on a 16:9 slide, width% ≈ height% × 0.5625 (the slide is wider than tall). - **A repeated motif ties slides together** — e.g. a small rust accent bar under every title, and a `NN — SECTION` eyebrow label in the accent color. Slide archetypes (each = background + a few shapes/textboxes): - **Title:** big bold wordmark (size 60-80) + accent bar + grey subtitle. A logo can be drawn from `round` shapes (a 3×3 grid of squares). - **Statement:** a thin full-height accent bar at x≈0, an eyebrow, one large line. - **Diagram:** `round` shapes as boxes (highlight the key one in the accent fill); thin `rect`s as connectors drawn BEFORE the boxes so the boxes sit on top; small grey textboxes label the connectors. - **Cards:** a row of `round` shapes (`fill` a touch lighter than the bg, `line` subtle), each with an eyebrow + title + description textbox. - **Big stat:** a huge number (size 90+) with an accent bar and a grey caption. - **Closing / install:** a dark `round` "terminal" box with a monospace (`font: "Menlo"`) command inside. Themed deck → PPTX (one slide of each, abridged) ``` create_document {"kind": "impress"} set_presentation_size {"doc_id": "doc-1", "preset": "16:9"} set_slide_background {"doc_id": "doc-1", "slide": 0, "color": "#15110f", "color2": "#2a201a", "angle": 115} add_textbox {"doc_id": "doc-1", "slide": 0, "text": "Project Atlas", "x": 7, "y": 40, "w": 80, "h": 16, "size": 72, "color": "#f4efe7", "bold": true, "valign": "center"} add_shape {"doc_id": "doc-1", "slide": 0, "shape": "rect", "x": 7, "y": 60, "w": 13, "h": 1.3, "fill": "#c2410c"} add_textbox {"doc_id": "doc-1", "slide": 0, "text": "Q3 review", "x": 7, "y": 64, "w": 60, "h": 8, "size": 23, "color": "#9a8f84"} add_slide {"doc_id": "doc-1", "layout": 20} set_slide_background {"doc_id": "doc-1", "slide": 1, "color": "#15110f"} add_shape {"doc_id": "doc-1", "slide": 1, "shape": "round", "x": 8, "y": 40, "w": 22, "h": 16, "fill": "#221b16", "line": "#5c4f45", "text": "Input", "text_color": "#f4efe7"} add_shape {"doc_id": "doc-1", "slide": 1, "shape": "round", "x": 39, "y": 40, "w": 22, "h": 16, "fill": "#c2410c", "text": "Atlas", "text_bold": true} add_shape {"doc_id": "doc-1", "slide": 1, "shape": "rect", "x": 30, "y": 47.5, "w": 9, "h": 0.5, "fill": "#6b5d52"} export_document {"doc_id": "doc-1", "path": "/abs/path/deck.pptx"} ``` Rich Writer documents use the **Writer — rich** tools above (styled paragraphs, lists, tables, inline images, and page-anchored bands/callouts). For a free-form drawn diagram, build it on an Impress/Draw page with the slide-graphics shapes and export, or render it and `insert_image` it into the document. ## Notes - Cells: a numeric `value` is written as a number, a string `value` as text, a `formula` (starting `=`) is evaluated by Calc. `sheet` selects by name or 0-based index. - To revise an existing file, `open_document` it, edit, then `save_document`. ## Live editing & concurrency The server drives ONE LibreOffice instance in an isolated profile (its own process). This shapes how live editing and conflict-handling work — for every document kind (Writer/Calc/Impress/Draw): - **One shared window for live editing.** In live mode (`show: true`) the server opens a visible window backed by the *same* document model you're editing through tools — a human watching that window sees your edits instantly, and you see theirs. A file opened separately in another LibreOffice is a distinct in-memory copy: the two processes share nothing until one saves to disk and the other reopens (local LibreOffice has no real-time co-editing). - **Conflict guard — don't clobber human edits.** Each doc has a `rev` counter bumped on every change (yours or a human's). If you call a mutating tool on a doc that changed since you last touched it, the call is REFUSED and returns `{conflict: true, warning, rev, seen}` instead of editing. Recover by re-reading the doc (e.g. `get_text`, or `read_cells`) — that resyncs `seen` and shows you the human's change — then retry your edit. Pass `force: true` to override without re-reading (rarely needed; prefer re-reading so you merge, not clobber). - **`close_document` refuses on unsaved changes** unless `force: true`. Save first (`save_document`) so nothing is silently discarded. `document_info` reports `modified`, `rev`, and `seen` so you can inspect state at any time. ## Reference The server wraps LibreOffice's UNO API. To understand a capability or find one not yet exposed: - LibreOffice: https://www.libreoffice.org · Help: https://help.libreoffice.org - UNO API reference: https://api.libreoffice.org/docs/idl/ref/index.html (Writer `com.sun.star.text`, Calc `com.sun.star.sheet`, Impress `com.sun.star.presentation`) - Export filter names: https://help.libreoffice.org/latest/en-US/text/shared/guide/convertfilters.html - Model Context Protocol: https://modelcontextprotocol.io Source: https://github.com/krondor-corp/libre-mcp