# Démonstration

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

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

```sh
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](https://github.com/RonanDavalan/LibreModular-MCP/blob/main/docs/GUIDE_LLM.md) : `get_state` sur `hub`, `get_state` sur `inkscape` avant d'écrire quoi que ce soit, `execute_script`, puis de nouveau `get_state`.

**Script écrit**

```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()
```


## É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`.

**Script écrit**

```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)}))
```


## L'exécuter

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

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

```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": []
}
```


## 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.](https://libremodular.davalan.fr/images/demonstration/inkscape-three-squares.png)

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

**Script écrit**

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

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


> **Limite**
>
> Le Hub borne la taille, la durée et la sortie d'un script, et lit son texte avant de l'envoyer. Un script s'exécute avec les droits de votre utilisateur, dans le Python de l'application. Le garde-fou arrête un script maladroit, pas un script déterminé : selon le [README](https://github.com/RonanDavalan/LibreModular-MCP#limits) du dépôt, il ne voit ni `runpy`, ni `pickle`, ni `http.server`, ni les fonctions d'enregistrement des applications, ni un chemin passé en paramètre à une fonction.

