# Demonstration

A client of 25 lines, written in Python with only its standard library, starts the Hub, sends one script to Inkscape and prints every answer. It is for engineers who want to watch the Hub work before connecting a model. The Hub must be [installed](https://libremodular.davalan.fr/installation/index.md) first.

## What the run needs

The run below was made on Debian 13 with Inkscape 1.4 and the distribution's Python 3, both from its packages, and with no `DISPLAY` variable set. The Inkscape module starts one Python process per script and runs `inkscape` once to export a capture, so no window opens. It looks for `/usr/bin/python3` and `/usr/bin/inkscape`, and for the Inkscape extensions in `/usr/share/inkscape/extensions`.

## Prepare the Hub

`init` creates the configuration and the token. In the configuration, every application is disabled; the second command enables Inkscape.

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

Both files are in mode 600, in `~/.config/libremodular-mcp/`.

## Write the client

Save this as `client.py`. It starts `libremodular-mcp serve`, which reads JSON-RPC messages, one per line, on its standard input. The client sends `initialize`, then four tool calls in the order the [guide for the model](https://github.com/RonanDavalan/LibreModular-MCP/blob/main/docs/GUIDE_LLM.md) asks for: `get_state` on `hub`, `get_state` on `inkscape` before writing anything, `execute_script`, and `get_state` again.

**Script written**

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


## Write the script

Save this as `draw.py`. It crops the page to a 210 by 70 mm strip, adds a layer named `Demo` and draws three squares in it, in colours of the project's logo. Inkscape's `inkex` library gives it the document; the Hub puts the path of the working document in `LMM_DOCUMENT`. The last line hands a value back as `result`.

**Script written**

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


## Run it

```sh
python3 client.py draw.py
```

The client printed four answers; the whole run took 0.65 seconds. The first says that `inkscape` is connected, in the `cli` mode, with `app_version` `1.4`. The second, taken before any script, has an empty `state` and one warning: `no document yet: the working document is created by the first script`. The third is the answer to the script:

**Hub response**

```json
{
  "app": "inkscape",
  "duration_ms": 141,
  "error": null,
  "result": {
    "squares": 3
  },
  "status": "ok",
  "stderr": "",
  "stdout": "",
  "truncated": false
}
```


`result` is the value the script printed after `LMM_RESULT: `, and `duration_ms` is the time the Hub measured for the script. The fourth answer describes the document the script left. On this page the start of the `screenshot` path is written `$XDG_RUNTIME_DIR`:

**Hub response**

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


## The image it produced

The file named by `screenshot` is the capture the Hub exported from the working document, readable by your user only (mode 600). This is that file, as the run wrote it: 794 by 265 pixels.

![Three squares in a row on a white strip: copper, amber and teal.](https://libremodular.davalan.fr/images/demonstration/inkscape-three-squares.png)

*The capture of the working document after draw.py, as exported by the Hub.*


## What the Hub refuses

The Hub reads a script before it sends it. Save this as `refused.py` and run `python3 client.py refused.py`:

**Script written**

```python
import os

os.remove("current.svg")
```


The third answer is a refusal, and its `reason` says that the script was stopped before it reached Inkscape:

**Hub response**

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


> **Limit**
>
> The Hub bounds the size, the time and the output of a script and reads its text before sending it. A script runs with the rights of your user, in the Python of the application. The guard stops a clumsy script, not a determined one: according to the repository's [README](https://github.com/RonanDavalan/LibreModular-MCP#limits), it does not see `runpy`, `pickle`, `http.server`, the save functions of the applications, or a path passed to a function as a parameter.

