LibreOffice
Un modèle pilote LibreOffice 25.2.3.2 par le Hub pour construire dans Calc une feuille de ventes, avec des formules et un en-tête, sans aucun document ouvert, et l’enregistrer en fichier .ods. Le premier script a fonctionné. Cette page donne le texte, le script, les réponses du Hub et la capture.

Sur cette page
Démarrer LibreOffice et son pont
LibreOffice et son pont ne démarrent jamais sur la parole du modèle : l’opérateur démarre les deux. Un seul lanceur le fait, d’après le guide, et rend la main aussitôt :
libremodular-launch libreoffice --bridge-file /usr/share/libremodular-mcp/bridges/uno_bridge.py
L’exécution n’avait pas d’écran, un serveur X virtuel en a tenu lieu, et elle a réglé dans config.toml les ports 2102 et 9314, et non les valeurs par défaut 2002 et 9304. Le lanceur a imprimé :
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 s’est mis à l’écoute d’UNO 1,3 s après le lancement, et le Hub a été démarré ensuite. get_state sur hub a listé les trois cibles qui partagent le pont, libreoffice-writer, libreoffice-calc et libreoffice-impress, connectées, avec la version que LibreOffice déclarait :
{
"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": []
}Texte donné au modèle
Avant d’écrire, le modèle a appelé get_state sur libreoffice-calc. active valait null et documents était vide, avec un avertissement disant que LibreOffice n’avait aucun document Calc courant. Ce n’est pas une erreur : un script s’exécute alors avec doc à None et peut créer le premier document.
Script écrit
Un modèle, Claude Sonnet 5.5, a écrit ce script à partir du guide et de cet état. Ce n’est pas un script tiré des tests du projet. Il crée le document quand doc vaut None, et il enregistre avec storeToURL dans le répertoire de travail.
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)}La réponse du 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
}Le total général, calculé par la formule, est 1 450 : 670 pour la tente, 480 pour le réchaud et 300 pour la lanterne. Le fichier .ods que le script a enregistré fait 13 472 octets. get_state a ensuite décrit la feuille et renvoyé une capture :
{
"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": []
}L’image produite
La capture est une page exportée par le filtre PNG de LibreOffice, de 828 par 1 170 pixels, avec le tableau en haut et le reste blanc. Cette image garde ses 230 pixels du haut. L’en-tête est en blanc gras sur #1c385c, et la dernière ligne et la dernière colonne contiennent les totaux.

