Demostración
En esta página
Un cliente de 25 líneas, escrito en Python con solo su biblioteca estándar, inicia el Hub, envía un script a Inkscape e imprime todas las respuestas. Es para ingenieros que quieren ver trabajar al Hub antes de conectar un modelo. Primero hay que instalar el Hub.
Lo que necesita la ejecución
La ejecución de abajo se hizo en Debian 13 con Inkscape 1.4 y el Python 3 de la distribución, ambos de sus paquetes, y sin la variable DISPLAY. El módulo de Inkscape inicia un proceso de Python por script y ejecuta inkscape una vez para exportar una captura, así que no se abre ninguna ventana. Busca /usr/bin/python3 y /usr/bin/inkscape, y las extensiones de Inkscape en /usr/share/inkscape/extensions.
Preparar el Hub
init crea la configuración y el token. En la configuración, todas las aplicaciones están desactivadas; el segundo comando activa Inkscape.
libremodular-mcp init
sed -i '/^\[apps.inkscape\]/,/^$/ s/enabled = false/enabled = true/' ~/.config/libremodular-mcp/config.toml
Ambos archivos tienen el modo 600 y están en ~/.config/libremodular-mcp/.
Escribir el cliente
Guárdelo como client.py. Inicia libremodular-mcp serve, que lee mensajes JSON-RPC, uno por línea, en su entrada estándar. El cliente envía initialize y después cuatro llamadas a herramientas en el orden que pide la guía para el modelo: get_state sobre hub, get_state sobre inkscape antes de escribir nada, execute_script y de nuevo 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()Escribir el script
Guárdelo como draw.py. Recorta la página a una franja de 210 por 70 mm, añade una capa llamada Demo y dibuja en ella tres cuadrados, con colores del logotipo del proyecto. La biblioteca inkex de Inkscape le entrega el documento; el Hub pone la ruta del documento de trabajo en LMM_DOCUMENT. La última línea devuelve un valor como result.
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)}))Ejecutarlo
python3 client.py draw.py
El cliente imprimió cuatro respuestas; la ejecución completa duró 0,65 segundos. La primera dice que inkscape está conectado, en el modo cli, con app_version 1.4. La segunda, tomada antes de cualquier script, tiene un state vacío y una advertencia: no document yet: the working document is created by the first script. La tercera es la respuesta al script:
{
"app": "inkscape",
"duration_ms": 141,
"error": null,
"result": {
"squares": 3
},
"status": "ok",
"stderr": "",
"stdout": "",
"truncated": false
}result es el valor que el script imprimió tras LMM_RESULT: , y duration_ms es el tiempo que el Hub midió para el script. La cuarta respuesta describe el documento que dejó el script. En esta página el comienzo de la ruta de screenshot se escribe $XDG_RUNTIME_DIR:
{
"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": []
}La imagen que produjo
El archivo que indica screenshot es la captura que el Hub exportó del documento de trabajo, legible solo por su usuario (modo 600). Este es ese archivo, tal como lo escribió la ejecución: 794 por 265 píxeles.

Lo que el Hub rechaza
El Hub lee un script antes de enviarlo. Guárdelo como refused.py y ejecute python3 client.py refused.py:
import os
os.remove("current.svg")La tercera respuesta es un rechazo, y su reason dice que el script se detuvo antes de llegar a Inkscape:
{
"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
}