Démonstration

Sur cette page

Un client de 25 lignes, écrit en Python avec sa seule bibliothèque standard, lance le Hub, envoie un script à Inkscape et affiche chaque réponse. Il s’adresse aux ingénieurs qui veulent voir le Hub travailler avant d’y brancher un modèle. Le Hub doit d’abord être installé.

Ce dont l’exécution a besoin

L’exécution ci-dessous a eu lieu sous Debian 13 avec Inkscape 1.4 et le Python 3 de la distribution, tous deux issus de ses paquets, et sans variable DISPLAY. Le module Inkscape lance un processus Python par script et exécute inkscape une fois pour exporter une capture : aucune fenêtre ne s’ouvre. Il cherche /usr/bin/python3 et /usr/bin/inkscape, ainsi que les extensions d’Inkscape dans /usr/share/inkscape/extensions.

Préparer le Hub

init crée la configuration et le jeton. Dans la configuration, toutes les applications sont désactivées ; la deuxième commande active Inkscape.

libremodular-mcp init
sed -i '/^\[apps.inkscape\]/,/^$/ s/enabled = false/enabled = true/' ~/.config/libremodular-mcp/config.toml

Les deux fichiers ont les droits 600 et se trouvent dans ~/.config/libremodular-mcp/.

Écrire le client

Enregistrez ce code sous le nom client.py. Il lance libremodular-mcp serve, qui lit sur son entrée standard des messages JSON-RPC, un par ligne. Le client envoie initialize, puis quatre appels d’outil dans l’ordre que demande le guide du modèle : get_state sur hub, get_state sur inkscape avant d’écrire quoi que ce soit, execute_script, puis de nouveau get_state.

python · Script écrit
import json
import subprocess
import sys

hub = subprocess.Popen(["libremodular-mcp", "serve"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True)

def send(**message):
    hub.stdin.write(json.dumps({"jsonrpc": "2.0", **message}) + "\n")
    hub.stdin.flush()

def tool(id, name, **arguments):
    send(id=id, method="tools/call", params={"name": name, "arguments": arguments})
    while (reply := json.loads(hub.stdout.readline())).get("id") != id:
        pass
    print(json.dumps(json.loads(reply["result"]["content"][0]["text"]), indent=2))

send(id=1, method="initialize", params={"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "demo", "version": "1"}})
hub.stdout.readline()
send(method="notifications/initialized")
tool(2, "get_state", target_app="hub")
tool(3, "get_state", target_app="inkscape")
tool(4, "execute_script", target_app="inkscape", script_content=open(sys.argv[1]).read())
tool(5, "get_state", target_app="inkscape")
hub.stdin.close()
hub.wait()

Écrire le script

Enregistrez ce code sous le nom draw.py. Il rogne la page en une bande de 210 par 70 mm, ajoute un calque nommé Demo et y dessine trois carrés, dans les couleurs du logo du projet. La bibliothèque inkex d’Inkscape lui fournit le document ; le Hub place le chemin du document de travail dans LMM_DOCUMENT. La dernière ligne renvoie une valeur sous le nom result.

python · Script écrit
import json
import os

import inkex

document = inkex.load_svg(os.environ["LMM_DOCUMENT"])
svg = document.getroot()
svg.set("height", "70mm")
svg.set("viewBox", "0 0 210 70")
layer = svg.add(inkex.Layer.new("Demo"))
for index, colour in enumerate(["#c25438", "#f2b344", "#29a196"]):
    square = inkex.Rectangle.new(15 + index * 65, 7.5, 55, 55)
    square.style = {"fill": colour}
    layer.add(square)
document.write(os.environ["LMM_DOCUMENT"])
print("LMM_RESULT: " + json.dumps({"squares": len(layer)}))

L’exécuter

python3 client.py draw.py

Le client a affiché quatre réponses ; l’exécution entière a duré 0,65 seconde. La première indique que inkscape est connecté, en mode cli, avec app_version valant 1.4. La deuxième, prise avant tout script, a un state vide et un avertissement : no document yet: the working document is created by the first script. La troisième est la réponse au script :

Réponse du Hub
{
  "app": "inkscape",
  "duration_ms": 141,
  "error": null,
  "result": {
    "squares": 3
  },
  "status": "ok",
  "stderr": "",
  "stdout": "",
  "truncated": false
}

result est la valeur que le script a imprimée après LMM_RESULT: , et duration_ms est la durée que le Hub a mesurée pour le script. La quatrième réponse décrit le document que le script a laissé. Sur cette page, le début du chemin de screenshot est écrit $XDG_RUNTIME_DIR :

Réponse du Hub
{
  "app": "inkscape",
  "app_version": "1.4",
  "bridge_version": null,
  "captured_at": "2026-10-21T07:57:38.500Z",
  "connected": true,
  "content_origin": "document",
  "screenshot": "$XDG_RUNTIME_DIR/libremodular-mcp/captures/inkscape-20261021T075738167Z.png",
  "state": {
    "element_count": 6,
    "height": "70mm",
    "layers": [
      {
        "id": "layer1",
        "label": "Layer 1"
      },
      {
        "id": null,
        "label": "Demo"
      }
    ],
    "width": "210mm"
  },
  "warnings": []
}

L’image produite

Le fichier désigné par screenshot est la capture que le Hub a exportée depuis le document de travail, lisible par votre seul utilisateur (droits 600). Le voici tel que l’exécution l’a écrit : 794 par 265 pixels.

Trois carrés alignés sur une bande blanche : cuivre, ambre et turquoise.
La capture du document de travail après draw.py, telle que le Hub l'a exportée.

Ce que le Hub refuse

Le Hub lit un script avant de l’envoyer. Enregistrez ce code sous le nom refused.py et lancez python3 client.py refused.py :

python · Script écrit
import os

os.remove("current.svg")

La troisième réponse est un refus, et son champ reason dit que le script a été arrêté avant d’atteindre Inkscape :

Réponse du Hub
{
  "app": "inkscape",
  "duration_ms": 0,
  "error": null,
  "reason": "Script refused by the safety guard before reaching the application:\n  G-1 (file deletion) at line 3: os.remove\nThe guard is a safety net, not a sandbox. Write files under LMM_WORK_DIR or a directory listed in extra_write_roots. To allow these constructs the operator sets allow_dangerous_scripts = true in the [security] section of config.toml; the audit log then records guard: bypassed.",
  "result": null,
  "status": "refused",
  "stderr": "",
  "stdout": "",
  "truncated": false
}

SuiteInstallation →