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.

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

python · Script escrito
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:

Respuesta del Hub
{
  "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:

Respuesta del Hub
{
  "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.

Tres cuadrados en fila sobre una franja blanca: cobre, ámbar y verde azulado.
La captura del documento de trabajo tras draw.py, tal como la exportó el Hub.

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:

python · Script escrito
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:

Respuesta del Hub
{
  "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
}

SiguienteInstalación →