LibreOffice
Un modelo maneja LibreOffice 25.2.3.2 a través del Hub para construir en Calc una hoja de ventas, con fórmulas y una fila de encabezado, sin ningún documento abierto, y guardarla como archivo .ods. El primer script funcionó. Esta página da el texto, el script, las respuestas del Hub y la captura.

En esta página
Iniciar LibreOffice y su puente
LibreOffice y su puente nunca se inician por la palabra del modelo: los inicia el operador. Un lanzador lo hace todo, según la guía, y vuelve al instante:
libremodular-launch libreoffice --bridge-file /usr/share/libremodular-mcp/bridges/uno_bridge.py
La ejecución no tenía pantalla, así que una pantalla X virtual hizo sus veces, y fijó los puertos en 2102 y 9314 en config.toml, no los predeterminados 2002 y 9304. El lanzador imprimió:
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 empezó a escuchar UNO 1,3 s después del lanzamiento, y el Hub se inició después. get_state sobre hub enumeró los tres destinos que comparten el puente, libreoffice-writer, libreoffice-calc y libreoffice-impress, conectados, con la versión que informó LibreOffice:
{
"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": []
}Texto dado al modelo
Antes de escribir, el modelo llamó a get_state sobre libreoffice-calc. active era null y documents estaba vacío, con una advertencia de que LibreOffice no tenía ningún documento de Calc actual. Eso no es un error: un script se ejecuta entonces con doc en None y puede crear el primer documento.
Script escrito
Un modelo, Claude Sonnet 5.5, escribió este script a partir de la guía y de ese estado. No es un script tomado de las pruebas del proyecto. Crea el documento cuando doc es None y guarda con storeToURL dentro del directorio de trabajo.
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)}Respuesta del Hub
{
"app": "libreoffice-calc",
"duration_ms": 555,
"error": null,
"result": {
"grand_total": 1450.0,
"ods_bytes": 13472,
"sheet": "Sales"
},
"status": "ok",
"stderr": "",
"stdout": "",
"truncated": false
}El total general, calculado por la fórmula, es 1450: 670 de la tienda de campaña, 480 del hornillo y 300 de la linterna. El archivo .ods que guardó el script pesa 13 472 bytes. get_state describió entonces la hoja y devolvió una captura:
{
"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": []
}La imagen producida
La captura es una página exportada por el filtro PNG de LibreOffice, de 828 por 1170 píxeles, con la tabla arriba y el resto en blanco. Esta imagen conserva sus 230 píxeles superiores. El encabezado está en negrita blanca sobre #1c385c, y la última fila y la última columna contienen los totales.

