LibreOffice

A model drives LibreOffice 25.2.3.2 through the Hub to build a sales sheet in Calc, with formulas and a header, from no open document, and save it as an .ods file. The first script worked. This page gives the text, the script, the Hub’s answers and the capture.

The LibreOffice Writer window with a one-page letter in English from the Hub to GIMP, Krita, Blender, Inkscape and LibreOffice, with the logo and the title LibreModular-MCP above the text.
A letter from the Hub to the five applications, laid out in Writer by a script, with the logo above the title. Letter as PDF

Start LibreOffice and its bridge

LibreOffice and its bridge never start on the model’s word: the operator starts both. One launcher does it, from the guide, and returns at once:

libremodular-launch libreoffice --bridge-file /usr/share/libremodular-mcp/bridges/uno_bridge.py

The run had no screen, so a virtual X server stood in for it, and it set the ports to 2102 and 9314 in config.toml, not the defaults 2002 and 9304. The launcher printed:

LibreOffice started (process 612876), listening for UNO on 127.0.0.1:2102
bridge started (process 612877), listening on 127.0.0.1:9314
a script runs on the current document of its target's family, or with doc set to None when none is open
stop the bridge with 'libremodular-launch libreoffice --stop'; LibreOffice is yours to close

LibreOffice listened for UNO 1.3 s after the launch, and the Hub was started after that. get_state on hub listed the three targets that share the bridge, libreoffice-writer, libreoffice-calc and libreoffice-impress, connected, with the version LibreOffice reported:

Hub response
{
  "app": "hub",
  "app_version": "0.1.0",
  "bridge_version": null,
  "captured_at": "2026-10-21T12:48:38.198Z",
  "connected": true,
  "content_origin": "hub",
  "screenshot": null,
  "state": {
    "modules": [
      {
        "app_version": "25.2.3.2",
        "bridge_version": "1",
        "connected": true,
        "mode": "uno",
        "name": "libreoffice-writer"
      },
      {
        "app_version": "25.2.3.2",
        "bridge_version": "1",
        "connected": true,
        "mode": "uno",
        "name": "libreoffice-calc"
      },
      {
        "app_version": "25.2.3.2",
        "bridge_version": "1",
        "connected": true,
        "mode": "uno",
        "name": "libreoffice-impress"
      }
    ]
  },
  "warnings": []
}

Text given to the model

Text given to the model
In LibreOffice Calc, build a quarterly sales sheet named “Sales”. Rows: Tent (120, 150, 310, 90), Stove (80, 60, 140, 200), Lantern (45, 70, 65, 120), for the columns Q1 to Q4. Add a Total column and a Total row, computed with formulas. Header row in bold white on dark blue (#1c385c). Save it as sales.ods in your working directory.

Before writing, the model called get_state on libreoffice-calc. active was null and documents was empty, with a warning that LibreOffice had no current Calc document. That is not an error: a script then runs with doc set to None and can create the first document.

Script written

A model, Claude Sonnet 5.5, wrote this script from the guide and from that state. It is not a script taken from the project’s tests. It creates the document when doc is None, and it saves with storeToURL inside the working directory.

python · Script written
import os

import uno

if doc is None:
    doc = desktop.loadComponentFromURL("private:factory/scalc", "_blank", 0, ())
sheet = doc.Sheets.getByIndex(0)
sheet.Name = "Sales"

header = ["Product", "Q1", "Q2", "Q3", "Q4", "Total"]
data = [("Tent", 120, 150, 310, 90), ("Stove", 80, 60, 140, 200), ("Lantern", 45, 70, 65, 120)]
for column, title in enumerate(header):
    cell = sheet.getCellByPosition(column, 0)
    cell.setString(title)
    cell.CharWeight = 150.0
    cell.CharColor = 0xFFFFFF
    cell.CellBackColor = 0x1C385C
    if column > 0:
        cell.HoriJustify = uno.Enum("com.sun.star.table.CellHoriJustify", "RIGHT")
for row, (name, *values) in enumerate(data, start=1):
    sheet.getCellByPosition(0, row).setString(name)
    for column, value in enumerate(values, start=1):
        sheet.getCellByPosition(column, row).setValue(value)
    sheet.getCellByPosition(5, row).setFormula(f"=SUM(B{row + 1}:E{row + 1})")
last = len(data) + 1
sheet.getCellByPosition(0, last).setString("Total")
for column in range(1, 6):
    letter = "ABCDEF"[column]
    sheet.getCellByPosition(column, last).setFormula(f"=SUM({letter}2:{letter}{last})")
for column in range(0, 6):
    sheet.getCellByPosition(column, last).CharWeight = 150.0
    sheet.Columns.getByIndex(column).Width = 2600

path = os.path.join(LMM_WORK_DIR, "sales.ods")
properties = (uno.createUnoStruct("com.sun.star.beans.PropertyValue"),)
properties[0].Name = "FilterName"
properties[0].Value = "calc8"
doc.storeToURL(uno.systemPathToFileUrl(path), properties)
result = {"sheet": sheet.Name, "grand_total": sheet.getCellByPosition(5, last).getValue(), "ods_bytes": os.path.getsize(path)}

The Hub’s answer

Hub response
{
  "app": "libreoffice-calc",
  "duration_ms": 555,
  "error": null,
  "result": {
    "grand_total": 1450.0,
    "ods_bytes": 13472,
    "sheet": "Sales"
  },
  "status": "ok",
  "stderr": "",
  "stdout": "",
  "truncated": false
}

The grand total, computed by the formula, is 1 450: 670 for the tent, 480 for the stove and 300 for the lantern. The .ods file the script saved is 13 472 bytes. get_state then described the sheet and returned a capture:

Hub response
{
  "app": "libreoffice-calc",
  "app_version": "25.2.3.2",
  "bridge_version": "1",
  "captured_at": "2026-10-21T12:48:39.480Z",
  "connected": true,
  "content_origin": "document",
  "screenshot": "$XDG_RUNTIME_DIR/libremodular-mcp/captures/libreoffice-calc-20261021T124838664Z.png",
  "state": {
    "active": {
      "sheets": [
        {
          "last_column": 5,
          "last_row": 4,
          "name": "Sales"
        }
      ]
    },
    "documents": [
      {
        "family": "calc",
        "title": "Untitled 1"
      }
    ]
  },
  "warnings": []
}

The image produced

The capture is a page exported by LibreOffice’s PNG filter, 828 by 1 170 pixels, with the table at the top and the rest white. This image keeps its top 230 pixels. The header is bold white on #1c385c, and the last row and the last column hold the totals.

A table titled Sales with the header Product, Q1 to Q4 and Total on a dark blue band, three product rows and a bold Total row.
The top of the capture of the Sales sheet, as exported by the Hub.
The LibreOffice Calc window: a sales table with Product, Q1 to Q4 and Total on a dark blue band, three products, and under it a bar chart of the four quarters.
The sales table of the recipe and its bar chart, both made by the same script in Calc.

Reference guide in the repository →

NextCompatibility →