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.

La ventana de LibreOffice Writer con una carta de una página, en español, del Hub a GIMP, Krita, Blender, Inkscape y LibreOffice, con el logotipo y el título LibreModular-MCP sobre el texto.
Una carta del Hub a las cinco aplicaciones, maquetada en Writer por un script, con el logotipo sobre el título. Carta en PDF

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:

Respuesta del Hub
{
  "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

Texto dado al modelo
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.

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.

python · Script escrito
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

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:

Respuesta del Hub
{
  "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.

Una tabla titulada Sales con el encabezado Product, Q1 a Q4 y Total sobre una banda azul oscuro, tres filas de productos y una fila Total en negrita.
La parte superior de la captura de la hoja Sales, tal como la exportó el Hub.
La ventana de LibreOffice Calc: una tabla de ventas con Product, Q1 a Q4 y Total sobre una banda azul oscuro, tres productos y, debajo, un gráfico de barras de los cuatro trimestres.
La tabla de ventas de la receta y su gráfico de barras, hechos por el mismo script en Calc.

Guía de referencia en el repositorio →

SiguienteCompatibilidad →