Demonstration
Auf dieser Seite
Ein Client mit 25 Zeilen, in Python geschrieben und nur auf dessen Standardbibliothek gestützt, startet den Hub, sendet ein Skript an Inkscape und gibt jede Antwort aus. Er ist für Ingenieure gedacht, die den Hub arbeiten sehen wollen, bevor sie ein Modell anbinden. Der Hub muss zuvor installiert sein.
Was der Lauf voraussetzt
Der folgende Lauf wurde unter Debian 13 mit Inkscape 1.4 und dem Python 3 der Distribution gemacht, beide aus deren Paketen, und ohne gesetzte Variable DISPLAY. Das Inkscape-Modul startet pro Skript einen Python-Prozess und führt inkscape einmal aus, um eine Aufnahme zu exportieren; es öffnet sich also kein Fenster. Es sucht /usr/bin/python3 und /usr/bin/inkscape sowie die Inkscape-Erweiterungen in /usr/share/inkscape/extensions.
Den Hub vorbereiten
init legt die Konfiguration und das Token an. In der Konfiguration ist jede Anwendung deaktiviert; der zweite Befehl aktiviert Inkscape.
libremodular-mcp init
sed -i '/^\[apps.inkscape\]/,/^$/ s/enabled = false/enabled = true/' ~/.config/libremodular-mcp/config.toml
Beide Dateien haben den Modus 600 und liegen in ~/.config/libremodular-mcp/.
Den Client schreiben
Speichern Sie den Code unter dem Namen client.py. Er startet libremodular-mcp serve, das an der Standardeingabe JSON-RPC-Nachrichten liest, eine pro Zeile. Der Client sendet initialize und danach vier Werkzeugaufrufe in der Reihenfolge, die der Leitfaden für das Modell verlangt: get_state für hub, get_state für inkscape, bevor irgendetwas geschrieben wird, execute_script und noch einmal 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()Das Skript schreiben
Speichern Sie den Code unter dem Namen draw.py. Er beschneidet die Seite auf einen Streifen von 210 × 70 mm, fügt eine Ebene namens Demo hinzu und zeichnet darin drei Quadrate in den Farben des Projektlogos. Die Bibliothek inkex von Inkscape liefert ihm das Dokument; der Hub legt den Pfad des Arbeitsdokuments in LMM_DOCUMENT ab. Die letzte Zeile gibt einen Wert als result zurück.
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)}))Ausführen
python3 client.py draw.py
Der Client hat vier Antworten ausgegeben; der gesamte Lauf dauerte 0,65 Sekunden. Die erste meldet, dass inkscape im Modus cli verbunden ist, mit app_version 1.4. Die zweite, vor jedem Skript aufgenommen, hat einen leeren state und eine Warnung: no document yet: the working document is created by the first script. Die dritte ist die Antwort auf das Skript:
{
"app": "inkscape",
"duration_ms": 141,
"error": null,
"result": {
"squares": 3
},
"status": "ok",
"stderr": "",
"stdout": "",
"truncated": false
}result ist der Wert, den das Skript nach LMM_RESULT: ausgegeben hat, und duration_ms ist die Zeit, die der Hub für das Skript gemessen hat. Die vierte Antwort beschreibt das Dokument, das das Skript hinterlassen hat. Auf dieser Seite ist der Anfang des Pfads in screenshot als $XDG_RUNTIME_DIR geschrieben:
{
"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": []
}Das erzeugte Bild
Die Datei, die screenshot nennt, ist die Aufnahme, die der Hub aus dem Arbeitsdokument exportiert hat; lesen kann sie nur Ihr Benutzer (Modus 600). Das ist diese Datei, so wie der Lauf sie geschrieben hat: 794 × 265 Pixel.

Was der Hub ablehnt
Der Hub liest ein Skript, bevor er es sendet. Speichern Sie den Code unter dem Namen refused.py und führen Sie python3 client.py refused.py aus:
import os
os.remove("current.svg")Die dritte Antwort ist eine Ablehnung, und ihr reason besagt, dass das Skript angehalten wurde, bevor es Inkscape erreichte:
{
"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
}