# Demonstration

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](https://libremodular.davalan.fr/de/installation/index.md) 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.

```sh
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](https://github.com/RonanDavalan/LibreModular-MCP/blob/main/docs/GUIDE_LLM.md) verlangt: `get_state` für `hub`, `get_state` für `inkscape`, bevor irgendetwas geschrieben wird, `execute_script` und noch einmal `get_state`.

**Geschriebenes Skript**

```python
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.

**Geschriebenes Skript**

```python
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

```sh
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:

**Antwort des Hubs**

```json
{
  "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:

**Antwort des Hubs**

```json
{
  "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.

![Drei Quadrate nebeneinander auf einem weißen Streifen: kupferfarben, bernsteinfarben und türkis.](https://libremodular.davalan.fr/images/demonstration/inkscape-three-squares.png)

*Die Aufnahme des Arbeitsdokuments nach draw.py, wie der Hub sie exportiert hat.*


## 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:

**Geschriebenes Skript**

```python
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:

**Antwort des Hubs**

```json
{
  "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
}
```


> **Grenze**
>
> Der Hub begrenzt Größe, Dauer und Ausgabe eines Skripts und liest seinen Text, bevor er ihn sendet. Ein Skript läuft mit den Rechten Ihres Benutzers, im Python der Anwendung. Die Skriptsperre hält ein ungeschicktes Skript auf, kein entschlossenes: Laut der [README](https://github.com/RonanDavalan/LibreModular-MCP#limits) des Repositorys sieht sie `runpy`, `pickle`, `http.server`, die Speicherfunktionen der Anwendungen und einen Pfad, der einer Funktion als Parameter übergeben wird, nicht.

