# Architektur

Der Hub ist ein einziges Rust-Programm, das einem Modell zwei Werkzeuge gibt, `execute_script` und `get_state`, und jede Anwendung über ein Modul auf der Seite des Hubs und eine Brücke auf der Seite der Anwendung erreicht. Die Seite zeigt, wie die Teile zusammenhängen, die drei Wege zu einer Anwendung und ein Skript im Vergleich mit getrennten Aufrufen.

## Die Teile

- **MCP-Client**: auf Ihrer Seite. Er startet `libremodular-mcp serve` und ruft die beiden Werkzeuge auf.
- **Hub**: ein Prozess, `libremodular-mcp`. Er antwortet dem Client, prüft das Ziel, liest das Skript vor dem Senden und schreibt das Audit-Journal.
- **Modul**: im Hub, eines pro Anwendung. Es weiß, wie die Anwendung zu erreichen ist.
- **Brücke**: in der Anwendung, neben ihr oder als ein Python-Prozess pro Skript. Sie führt das Skript aus und gibt zurück, was es ausgegeben hat, sein `result`, oder den anwendungseigenen Fehler.
- **Anwendung**: GIMP, Krita, Blender, Inkscape oder LibreOffice. Sie leistet die Arbeit.

Der Hub kennt die Schnittstelle keiner Anwendung. Ein Skript ist im Python der jeweiligen Anwendung geschrieben, und nur das Modul und die Brücke hängen von der Anwendung ab.

Der Hub ist eine einzelne ausführbare Datei von etwa 4,3 MB. Neben dem Lader listet `ldd` zwei Bibliotheken auf: `libc.so.6` und `libgcc_s.so.1`. Das Paket installiert eine zweite ausführbare Datei, `libremodular-launch`, die LibreOffice und dessen Brücke startet.

## Zwei Werkzeuge

Die Antwort des Hubs auf `tools/list` nennt genau `["execute_script", "get_state"]`.

`execute_script(target_app, script_content)` führt ein vollständiges Python-Skript in der Anwendung aus, die `target_app` nennt, und gibt ein JSON-Objekt zurück: `status`, `duration_ms`, `stdout`, `stderr`, `result`, `error` mit der Fehlerspur der Anwendung, `truncated` und `reason`, wenn der Hub das Skript abgelehnt hat. `get_state(target_app)` gibt den Zustand der Anwendung zurück: ihre Dokumente, Ebenen oder Objekte, einen Pfad zu einer PNG-Aufnahme, falls es eine gibt, und `content_origin`, das angibt, dass der Text aus einem Dokument stammt und nicht vom Hub. Das Ziel `hub` listet die konfigurierten Anwendungen auf, ohne eine davon zu kontaktieren.

## Drei Wege zu einer Anwendung

Der Hub wurde mit jeder aktivierten Anwendung und ohne gestartete Brücke gestartet. `get_state` für `hub` listete sieben Ziele in diesen Modi auf:

- `tcp`: `gimp`, `krita`, `blender`. Eine in die Anwendung geladene Brücke lauscht an einem lokalen Port, und der Hub verbindet sich mit ihr.
- `uno`: `libreoffice-writer`, `libreoffice-calc`, `libreoffice-impress`. Eine eigenständige Brücke verbindet sich mit dem UNO-Port, an dem LibreOffice lauscht, und die drei Ziele teilen sie.
- `cli`: `inkscape`. Der Hub startet pro Skript einen Python-Prozess; es gibt keine Brücke zu starten.

Nur `inkscape` zeigte `connected: true`: Im Modus `cli` muss nichts laufen, und der Hub hatte das Programm gefunden. In den Modi `tcp` und `uno` startet, schließt oder beendet der Hub nie eine Anwendung auf Wunsch des Modells; der Bediener startet die Brücke, wie es der Leitfaden jeder Anwendung beschreibt.

Eine Anwendung führt jeweils ein Skript aus. Ein Skript, das die Frist des Hubs überschreitet, erhält die Antwort `timeout`, und die nächste Anfrage erhält `busy`, bis die Anwendung wieder frei ist; die Seite [Sicherheit](https://libremodular.davalan.fr/de/security/index.md) misst das.

## Was eine Brücke braucht

Die vier Brückendateien, die das Paket installiert, werden aus einem gemeinsamen Kern und einem Adapter pro Anwendung erzeugt. Sie importieren die Python-Standardbibliothek und darüber hinaus nur die eigene Schnittstelle der Anwendung. Jede Zeile nennt die Datei, ihre Länge und das, was sie außerhalb der Standardbibliothek importiert:

- `blender_bridge.py`, 1 091 Zeilen: `bpy`.
- `gimp_bridge.py`, 1 130 Zeilen: `gi`.
- `krita_bridge.py`, 1 094 Zeilen: `krita`, `PyQt5`, `PyQt6`.
- `uno_bridge.py`, 1 203 Zeilen: `uno`, `com`.

Die Liste stammt aus dem Parsen jeder Datei, nicht aus ihrer Ausführung. Die Seite [Kompatibilität](https://libremodular.davalan.fr/de/compatibility/index.md) nennt für jede Anwendung die Version, mit der diese Brücken erprobt wurden.

## Ein Skript gegen getrennte Aufrufe

Ein Modell, das eine Anwendung Arbeitsschritt für Arbeitsschritt steuert, macht pro Arbeitsschritt einen Aufruf. Ein Modell, das die ganze Arbeit schreibt, macht einen Aufruf. Der Hub behandelt beide gleich, daher lassen sich beide mit demselben Hub, derselben Anwendung und derselben Arbeit zeitlich vergleichen.

Die Arbeit besteht darin, einem Dokument N Elemente hinzuzufügen: N Rechtecke in Inkscape, N Würfel in Blender. Für jedes N maßen zehn Wiederholungen einen Aufruf von `execute_script`, der die ganze Arbeit erledigt, und dann N Aufrufe, die je einen Arbeitsschritt erledigen. Das Dokument wurde vor jeder Messung geleert, und zwei Aufwärmaufrufe gingen den Wiederholungen voraus. Die Zeit wird im Client gelesen, vom Schreiben der Anfrage bis zum Lesen der Antwort; in Inkscape ist die Arbeit dieses Skript,

**Geschriebenes Skript**

```python
import json, os
import inkex
document = inkex.load_svg(os.environ['LMM_DOCUMENT'])
root = document.getroot()
for i in range(20):
    root.add(inkex.Rectangle.new(i * 12, 10, 10, 10))
document.write(os.environ['LMM_DOCUMENT'])
print('LMM_RESULT: ' + json.dumps({'count': len(root)}))
```


und jeder der zwanzig getrennten Aufrufe ist dasselbe Skript, wobei die Schleife durch eine Zeile ersetzt ist, zum Beispiel der dritte:

**Geschriebenes Skript**

```python
import json, os
import inkex
document = inkex.load_svg(os.environ['LMM_DOCUMENT'])
root = document.getroot()
root.add(inkex.Rectangle.new(36, 10, 10, 10))
document.write(os.environ['LMM_DOCUMENT'])
print('LMM_RESULT: ' + json.dumps({'count': len(root)}))
```


Median aus zehn Wiederholungen, der kleinste und der größte Wert in Klammern. Der Client ist der der [Demonstration](https://libremodular.davalan.fr/de/demonstration/index.md), mit einer Uhr um jeden Aufruf.

**Inkscape 1.4, Modus `cli`** (ein Python-Prozess pro Aufruf):

- N = 1: ein Skript 141,7 ms (141,7 bis 151,7); ein Aufruf 141,7 ms (141,5 bis 161,9); Faktor ×1,0.
- N = 5: ein Skript 146,7 ms (141,6 bis 152,0); fünf Aufrufe 738,7 ms (718,6 bis 749,3); Faktor ×5,0.
- N = 20: ein Skript 151,7 ms (141,6 bis 151,9); zwanzig Aufrufe 2 933,8 ms (2 874,0 bis 3 046,2); Faktor ×19,3.

**Blender 4.3.2, Modus `tcp`** (die Anwendung bleibt in Betrieb):

- N = 1: ein Skript 1,5 ms (1,3 bis 2,7); ein Aufruf 1,2 ms (1,0 bis 2,1); Faktor ×0,8.
- N = 5: ein Skript 3,5 ms (3,3 bis 6,1); fünf Aufrufe 5,9 ms (5,5 bis 8,3); Faktor ×1,7.
- N = 20: ein Skript 17,8 ms (16,7 bis 20,4); zwanzig Aufrufe 30,5 ms (27,2 bis 32,3); Faktor ×1,7.

Im Modus `cli` startet jeder Aufruf einen Python-Prozess und lädt `inkex`, was unabhängig von der Arbeit etwa 0,14 s kostet: Zwanzig Arbeitsschritte in getrennten Aufrufen kosten das 19,3-Fache des einen Aufrufs. Im Modus `tcp` läuft die Anwendung bereits, und die 19 zusätzlichen Aufrufe fügten 12,7 ms hinzu, etwa 0,7 ms je Aufruf. Ein zweiter Lauf der gesamten Messung ergab Mediane, die sich bei Inkscape um höchstens 10 ms unterschieden, außer bei den zwanzig getrennten Aufrufen (87 ms), und bei Blender um höchstens 1,3 ms; der Faktor bei N = 20 war ×19,9 bei Inkscape und ×1,7 bei Blender.

> **Grenze**
>
> Diese Zahlen betreffen nur den Hub und die Anwendung. Die Zeit, die ein Modell braucht, um jeden der N Aufrufe zu schreiben und zu lesen, ist nicht gemessen und nicht enthalten. Sie stammen von einem Rechner (Debian 13, 32 Prozessoren), einer Arbeit pro Anwendung, N bis 20 und zwei der fünf Anwendungen: GIMP, Krita und LibreOffice wurden nicht gemessen.

