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.
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.
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 :
{
"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 :
{
"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.

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 :
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 :
{
"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
}