Skip to main content
Glama
martinmangos

ZelioSoft2 MCP

by martinmangos
README.md
# ZelioSoft2 MCP

MCP servers that let an LLM agent build and test Schneider Electric **ZelioSoft2 V5.4.3** relay-ladder-logic programs (`.zm2` files) — without ZelioSoft2 exposing any official API or scripting interface of its own.

**[🇬🇧 English](#english)** · **[🇪🇸 Español](#español)**

---

<a name="english"></a>
## English

### What's in here

Three independent MCP servers, each usable on its own:

| Server | File | What it does |
|---|---|---|
| **zelio-ladder-builder** | `server.py` | Generates a valid `.zm2` ladder program **directly from a JSON spec** — no GUI, no automation, just the file format written correctly. |
| **zelio-desktop-control** | `desktop_server.py` | Drives the real, **visible** ZelioSoft2 window: open a file, run its simulation, toggle inputs, read outputs, screenshot. |
| **zelio-hidden-desktop** | `hidden_desktop_server.py` | Same capabilities as above, but the automated ZelioSoft2 instance runs on an **isolated Windows desktop** — your mouse, keyboard, and screen are never touched. |

### Why this exists

ZelioSoft2 has no scripting API, no CLI, no file format documentation. Everything here came from reverse-engineering the app (Win32/MFC binary analysis) and the `.zm2` container format, then building three different ways to act on that knowledge:

1. **Write the file directly** (fastest, most precise — no UI involved at all).
2. **Drive the real GUI** (needed to actually *run* a program and verify its logic, not just its syntax).
3. **Drive the GUI in isolation** (so an agent can debug a program while you keep using your own PC).

### How it works (short version)

- **`zm2codec.py`** — the outer container: `LZMA` compression + a keyed `BLAKE2b` MAC + a 14-byte XOR mask. Fully reversed and round-trip verified against real ZelioSoft2 sample files.
- **`zm2builder.py`** — the inner ladder format: a fixed table of "P-pins" encodes every contact/coil in the grid as a 16-bit opcode (element type, address, NA/NC flag). Validates addresses against the target hardware module's real I/O capacity, and can auto-pick the smallest module that fits a given spec.
- **`templates/`** — one `.zm2` per supported hardware module (I/O count varies by model); `zm2builder` reads each module's real symbol table dynamically, no hardcoded per-model offsets.
- **`zelio_driver.py`** — classic GUI automation (`pyautogui` + `pywinauto`) for the visible window.
- **`hidden_desktop.py`** — the isolated version: a real Win32 `CreateDesktop`, with input injected via `PostMessage`/`WM_COMMAND` directly to window handles (`SendInput` does not work on a non-visible desktop — confirmed empirically) and screenshots captured via `PrintWindow` (a desktop-wide `BitBlt` also silently fails on a hidden desktop).

Full reverse-engineering log, including every dead end: **[`HANDOFF.md`](./HANDOFF.md)**.

### Install

```bash
pip install -r requirements.txt
```

Windows only (ZelioSoft2 itself is Windows-only). Requires ZelioSoft2 V5.4.3 installed for the two desktop-control servers; `server.py` alone needs nothing but the templates included here.

### Wire it into an MCP client

Add to your client's MCP config (Claude Code's `.mcp.json`, Antigravity's `mcp_config.json`, etc.):

```json
{
  "mcpServers": {
    "zelio-ladder-builder": {
      "command": "python",
      "args": ["<path>/zelio_mcp/server.py"]
    },
    "zelio-desktop-control": {
      "command": "python",
      "args": ["<path>/zelio_mcp/desktop_server.py"]
    },
    "zelio-hidden-desktop": {
      "command": "python",
      "args": ["<path>/zelio_mcp/hidden_desktop_server.py"]
    }
  }
}
```

### Tools

**`zelio-ladder-builder`**
- `describe_addressing()` — the row/contact/coil spec DSL reference (call this first).
- `list_hardware_modules()` — every template's I/O capacity.
- `validate_program(rows, ...)` — dry-run a spec, get errors/warnings without writing a file.
- `build_ladder_program(rows, output_path, ...)` — compile and save a `.zm2` file.

**`zelio-desktop-control`** (visible window)
- `zelio_launch_and_open`, `zelio_open_file`, `zelio_is_running`
- `zelio_enter_simulation`, `zelio_press_run`, `zelio_stop_simulation`, `zelio_exit_simulation`
- `zelio_read_io`, `zelio_set_input`
- `zelio_screenshot`, `zelio_force_repaint` (the simulation panels cache stale frames — a real ZelioSoft2 bug, not this tool's)

**`zelio-hidden-desktop`** (isolated window — same shape, `_hidden` naming)
- `zelio_hidden_open`, `zelio_hidden_enter_simulation`, `zelio_hidden_press_run`, `zelio_hidden_stop_simulation`, `zelio_hidden_exit_simulation`
- `zelio_hidden_read_io`, `zelio_hidden_set_input`, `zelio_hidden_screenshot`, `zelio_hidden_close`

### ⚠️ About the hidden-desktop technique, honestly

`hidden_desktop.py` uses Win32's `CreateDesktop` API to give a launched process its own input queue and render surface, invisible to the interactive desktop. This is the exact same low-level mechanism "Hidden VNC" (HVNC) malware uses for covert remote control — worth knowing if you're reading or extending this code, since the shape (create desktop → inject input → hide a process) is identical either way.

The use here is the opposite of covert: it automates **your own** licensed copy of ZelioSoft2, at **your own** explicit direction, so an agent can debug a program without hijacking the mouse/keyboard you're actively using. Nothing is hidden from you — the whole point is that you can keep working while it runs. If you fork this for something else, make sure that framing still holds.

### Known limitations

- `branch_of` (parallel-branch / seal-in wiring in `zm2builder.py`) is implemented but was never verified by opening a branch-using file in the real app.
- I/O comments (ZelioSoft2's per-symbol "Comentario" field) aren't reachable through the P-pin table at all — they live in a still-unsolved, ID-renumbers-every-save part of the format.
- `hidden_desktop_server.py`'s MCP tool wrappers weren't exercised through an actual MCP client — the underlying `HiddenDesktopSession` methods they call were verified directly.

### Credits

- **[martinmangos](https://github.com/martinmangos)** — project owner, direction, and every hands-on test against the real app.
- **Claude** (Anthropic) — reverse engineering (IDA Pro decompilation, live binary tracing) and implementation, across several sessions.
- **[Antigravity CLI](https://antigravity.google/)** (Google) — built parts of the ladder-generation logic and exercised the MCP tools independently.

A lot of the reverse engineering (the `.zm2` container format, the P-pin ladder-matrix encoding, the Win32 internals behind the desktop-control layer) came from decompiling `Zelio2.exe` in **IDA Pro**. The full session-by-session log, including every dead end, is in [`HANDOFF.md`](./HANDOFF.md).

---

<a name="español"></a>
## Español

### Qué hay acá

Tres servidores MCP independientes, cada uno usable por separado:

| Servidor | Archivo | Qué hace |
|---|---|---|
| **zelio-ladder-builder** | `server.py` | Genera un programa ladder `.zm2` válido **directo desde un spec JSON** — sin GUI, sin automatización, solo el formato de archivo escrito correctamente. |
| **zelio-desktop-control** | `desktop_server.py` | Controla la ventana **visible** real de ZelioSoft2: abrir archivo, correr simulación, togglear entradas, leer salidas, sacar captura. |
| **zelio-hidden-desktop** | `hidden_desktop_server.py` | Mismas capacidades, pero la instancia automatizada corre en un **desktop de Windows aislado** — tu mouse, teclado y pantalla nunca se tocan. |

### Por qué existe esto

ZelioSoft2 no tiene API de scripting, ni CLI, ni el formato de archivo documentado. Todo esto salió de hacer ingeniería inversa de la app (análisis del binario Win32/MFC) y del contenedor `.zm2`, y después construir tres formas distintas de actuar con ese conocimiento:

1. **Escribir el archivo directo** (lo más rápido y preciso — sin GUI de por medio).
2. **Controlar la GUI real** (necesario para de verdad *correr* un programa y verificar la lógica, no solo que compile).
3. **Controlar la GUI de forma aislada** (para que una IA pueda debuggear un programa mientras vos seguís usando tu PC).

### Cómo funciona (versión corta)

- **`zm2codec.py`** — el contenedor externo: compresión `LZMA` + MAC `BLAKE2b` con clave + una máscara XOR de 14 bytes. Reverseado completo y verificado con archivos de ejemplo reales de ZelioSoft2.
- **`zm2builder.py`** — el formato interno del ladder: una tabla fija de "P-pins" codifica cada contacto/bobina de la grilla como un opcode de 16 bits (tipo de elemento, dirección, flag NA/NC). Valida direcciones contra la capacidad de E/S real del módulo de hardware elegido, y puede elegir automáticamente el módulo más chico que cumpla un spec dado.
- **`templates/`** — un `.zm2` por cada módulo de hardware soportado (la cantidad de E/S varía según el modelo); `zm2builder` lee la tabla de símbolos real de cada módulo de forma dinámica, sin offsets hardcodeados por modelo.
- **`zelio_driver.py`** — automatización de GUI clásica (`pyautogui` + `pywinauto`) para la ventana visible.
- **`hidden_desktop.py`** — la versión aislada: un `CreateDesktop` real de Win32, con input inyectado vía `PostMessage`/`WM_COMMAND` directo a los handles de ventana (`SendInput` no funciona en un desktop no visible — confirmado empíricamente) y capturas de pantalla vía `PrintWindow` (un `BitBlt` de todo el desktop también falla en silencio en un desktop oculto).

Historial completo de la ingeniería inversa, con cada callejón sin salida incluido: **[`HANDOFF.md`](./HANDOFF.md)**.

### Instalación

```bash
pip install -r requirements.txt
```

Solo Windows (ZelioSoft2 en sí es solo Windows). Los dos servers de control de escritorio necesitan ZelioSoft2 V5.4.3 instalado; `server.py` solo no necesita nada más que los templates incluidos acá.

### Conectarlo a un cliente MCP

Agregalo a la config MCP de tu cliente (`.mcp.json` de Claude Code, `mcp_config.json` de Antigravity, etc.):

```json
{
  "mcpServers": {
    "zelio-ladder-builder": {
      "command": "python",
      "args": ["<ruta>/zelio_mcp/server.py"]
    },
    "zelio-desktop-control": {
      "command": "python",
      "args": ["<ruta>/zelio_mcp/desktop_server.py"]
    },
    "zelio-hidden-desktop": {
      "command": "python",
      "args": ["<ruta>/zelio_mcp/hidden_desktop_server.py"]
    }
  }
}
```

### Tools

**`zelio-ladder-builder`**
- `describe_addressing()` — referencia del DSL de filas/contactos/bobinas (llamar esto primero).
- `list_hardware_modules()` — capacidad de E/S de cada template.
- `validate_program(rows, ...)` — prueba en seco de un spec, errores/warnings sin escribir archivo.
- `build_ladder_program(rows, output_path, ...)` — compila y guarda un `.zm2`.

**`zelio-desktop-control`** (ventana visible)
- `zelio_launch_and_open`, `zelio_open_file`, `zelio_is_running`
- `zelio_enter_simulation`, `zelio_press_run`, `zelio_stop_simulation`, `zelio_exit_simulation`
- `zelio_read_io`, `zelio_set_input`
- `zelio_screenshot`, `zelio_force_repaint` (los paneles de simulación cachean frames viejos — bug real de ZelioSoft2, no de esta herramienta)

**`zelio-hidden-desktop`** (ventana aislada — mismas tools, con prefijo `_hidden`)
- `zelio_hidden_open`, `zelio_hidden_enter_simulation`, `zelio_hidden_press_run`, `zelio_hidden_stop_simulation`, `zelio_hidden_exit_simulation`
- `zelio_hidden_read_io`, `zelio_hidden_set_input`, `zelio_hidden_screenshot`, `zelio_hidden_close`

### ⚠️ Sobre la técnica de hidden-desktop, con honestidad

`hidden_desktop.py` usa la API `CreateDesktop` de Win32 para darle a un proceso lanzado su propia cola de input y superficie de render, invisible para el desktop interactivo. Es exactamente el mismo mecanismo de bajo nivel que usa el malware "Hidden VNC" (HVNC) para control remoto encubierto — vale la pena saberlo si leés o extendés este código, porque la forma (crear desktop → inyectar input → ocultar un proceso) es idéntica en ambos casos.

El uso acá es lo opuesto a encubierto: automatiza **tu propia** copia licenciada de ZelioSoft2, bajo **tu propia** dirección explícita, para que una IA pueda debuggear un programa sin secuestrarte el mouse/teclado que estás usando activamente. No se te oculta nada — la gracia es justamente que puedas seguir trabajando mientras corre. Si forkeás esto para otra cosa, asegurate de que ese encuadre se mantenga.

### Limitaciones conocidas

- `branch_of` (ramas paralelas / sello propio en `zm2builder.py`) está implementado pero nunca se verificó abriendo un archivo con ramas en la app real.
- Los comentarios de E/S (el campo "Comentario" por símbolo de ZelioSoft2) no son alcanzables desde la tabla de P-pins — viven en una parte del formato aún no resuelta, que renumera IDs en cada guardado.
- Los wrappers de tools MCP de `hidden_desktop_server.py` no se probaron a través de un cliente MCP real — sí se verificaron directamente los métodos de `HiddenDesktopSession` que usan por debajo.

### Créditos

- **[martinmangos](https://github.com/martinmangos)** — dueño del proyecto, dirección, y todas las pruebas a mano contra la app real.
- **Claude** (Anthropic) — ingeniería inversa (decompilación con IDA Pro, tracing en vivo del binario) e implementación, a lo largo de varias sesiones.
- **[Antigravity CLI](https://antigravity.google/)** (Google) — construyó parte de la lógica de generación del ladder y probó las tools MCP de forma independiente.

Buena parte de la ingeniería inversa (el formato de contenedor `.zm2`, la codificación de la matriz ladder de P-pins, los detalles de Win32 detrás de la capa de control de escritorio) salió de decompilar `Zelio2.exe` en **IDA Pro**. El log sesión por sesión completo, con cada callejón sin salida incluido, está en [`HANDOFF.md`](./HANDOFF.md).