Demonstration

On this page

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

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 asks for: get_state on hub, get_state on inkscape before writing anything, execute_script, and get_state again.

python · Script written
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.

python · Script written
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

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

python · Script written
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
{
  "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
}

NextInstallation →