# Demostración

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

```sh
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](https://github.com/RonanDavalan/LibreModular-MCP/blob/main/docs/GUIDE_LLM.md): `get_state` sobre `hub`, `get_state` sobre `inkscape` antes de escribir nada, `execute_script` y de nuevo `get_state`.

**Script escrito**

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


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

**Script escrito**

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


## Ejecutarlo

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

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

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


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

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

**Script escrito**

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

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


> **Límite**
>
> El Hub limita el tamaño, el tiempo y la salida de un script, y lee su texto antes de enviarlo. Un script se ejecuta con los derechos de su usuario, en el Python de la aplicación. La compuerta detiene un script torpe, no uno decidido: según el [README](https://github.com/RonanDavalan/LibreModular-MCP#limits) del repositorio, no ve `runpy`, `pickle`, `http.server`, las funciones de guardado de las aplicaciones ni una ruta pasada a una función como parámetro.

