Architecture

On this page

The Hub is one Rust program that gives a model two tools, execute_script and get_state, and reaches each application through a module on the Hub’s side and a bridge on the application’s side. This page shows how the parts connect, the three ways of reaching an application, and one script measured against separate calls.

The parts

  • MCP client: on your side. It starts libremodular-mcp serve and calls the two tools.
  • Hub: one process, libremodular-mcp. It answers the client, checks the target, reads the script before sending it and writes the audit journal.
  • Module: inside the Hub, one per application. It knows how to reach the application.
  • Bridge: in the application, beside it, or one Python process per script. It runs the script and returns what it printed, its result, or the application’s own error.
  • Application: GIMP, Krita, Blender, Inkscape or LibreOffice. It does the work.

The Hub knows no application’s interface. A script is written in the application’s own Python, and the module and the bridge are the only parts that depend on the application.

The Hub is a single executable of about 4.3 MB. Besides the loader, ldd lists two libraries: libc.so.6 and libgcc_s.so.1. The package installs a second executable, libremodular-launch, which starts LibreOffice and its bridge.

Two tools

The answer of the Hub to tools/list names exactly ["execute_script", "get_state"].

execute_script(target_app, script_content) runs a complete Python script in the application named by target_app and returns a JSON object: status, duration_ms, stdout, stderr, result, error with the application’s own traceback, truncated, and reason when the Hub refused the script. get_state(target_app) returns the state of the application: its documents, layers or objects, a path to a PNG capture when there is one, and content_origin, which says the text came from a document and not from the Hub. The target hub lists the configured applications without contacting any of them.

Three ways of reaching an application

The Hub was started with every application enabled and no bridge started. get_state on hub listed seven targets in these modes:

  • tcp: gimp, krita, blender. A bridge loaded in the application listens on a local port, and the Hub connects to it.
  • uno: libreoffice-writer, libreoffice-calc, libreoffice-impress. One stand-alone bridge connects to the UNO port LibreOffice listens on, and the three targets share it.
  • cli: inkscape. The Hub starts one Python process per script; there is no bridge to start.

Only inkscape showed connected: true: in the cli mode nothing has to be running, and the Hub had found the program. In the tcp and uno modes the Hub never starts, closes or stops an application on the model’s request; the operator starts the bridge, as the guide of each application says.

An application runs one script at a time. A script that outlasts the Hub’s delay gets the answer timeout, and the next request gets busy until the application is free; the Security page measures it.

What a bridge needs

The four bridge files the package installs are generated from one common core and one adapter per application. They import the Python standard library and, besides it, only the application’s own interface. Each line gives the file, its length and what it imports outside the standard library:

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

The list comes from parsing each file, not from running it. The Compatibility page gives, for each application, the version these bridges were tried with.

One script against separate calls

A model that drives an application one gesture at a time makes one call per gesture. A model that writes the whole job makes one call. The Hub treats both the same way, so the two can be timed on the same Hub, the same application and the same job.

The job is to add N elements to a document: N rectangles in Inkscape, N cubes in Blender. For each N, ten repetitions timed one call of execute_script that does the whole job, then N calls that do one gesture each. The document was emptied before each measure, and two warm-up calls preceded the repetitions. The time is read in the client, from writing the request to reading the answer; in Inkscape the job is this script,

python · Script written
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)}))

and each of the twenty separate calls is the same script with the loop replaced by one line, for example the third:

python · Script written
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 of ten repetitions, with the smallest and the largest in brackets. The client is the one of the Demonstration, with a clock around each call.

Inkscape 1.4, cli mode (one Python process per call):

  • N = 1: one script 141.7 ms (141.7 to 151.7); one call 141.7 ms (141.5 to 161.9); ratio ×1.0.
  • N = 5: one script 146.7 ms (141.6 to 152.0); five calls 738.7 ms (718.6 to 749.3); ratio ×5.0.
  • N = 20: one script 151.7 ms (141.6 to 151.9); twenty calls 2,933.8 ms (2,874.0 to 3,046.2); ratio ×19.3.

Blender 4.3.2, tcp mode (the application stays running):

  • N = 1: one script 1.5 ms (1.3 to 2.7); one call 1.2 ms (1.0 to 2.1); ratio ×0.8.
  • N = 5: one script 3.5 ms (3.3 to 6.1); five calls 5.9 ms (5.5 to 8.3); ratio ×1.7.
  • N = 20: one script 17.8 ms (16.7 to 20.4); twenty calls 30.5 ms (27.2 to 32.3); ratio ×1.7.

In the cli mode every call starts a Python process and loads inkex, which costs about 0.14 s whatever the job: twenty gestures in separate calls cost 19.3 times the single call. In the tcp mode the application is already running, and the 19 extra calls added 12.7 ms, about 0.7 ms each. A second run of the whole measurement gave medians that differed by at most 10 ms on Inkscape, except the twenty separate calls (87 ms), and by at most 1.3 ms on Blender; the ratio at N = 20 was ×19.9 on Inkscape and ×1.7 on Blender.

NextSecurity →