Krita
A model drives Krita 5.2.9 through the Hub to paint a postcard on two layers and save it as a .kra file. The first script worked. This page gives the text, the script, the Hub’s answers and the capture, and says how the bridge was started: not from Scripter, which was not tried.
On this page
Start Krita with its bridge
The guide has the operator run the bridge file from Scripter. This run did not use that gesture. It started Krita on a virtual X server, in a new profile, with a startup extension whose whole __init__.py runs the bridge file:
xvfb-run -a krita --nosplash
exec(open('/usr/share/libremodular-mcp/bridges/krita_bridge.py', encoding='utf-8').read())
The encoding is not optional. Krita’s embedded Python reads files in the encoding of the locale it was started under, and the bridge file holds accented characters: under the C locale a command without encoding='utf-8' stopped with UnicodeDecodeError, as the compatibility page records. The run used port 9312 in config.toml and a new token. The port listened 10.4 s after the start, and get_state on hub listed Krita as connected:
{
"app": "hub",
"app_version": "0.1.0",
"bridge_version": null,
"captured_at": "2026-10-21T12:47:48.951Z",
"connected": true,
"content_origin": "hub",
"screenshot": null,
"state": {
"modules": [
{
"app_version": "5.2.9",
"bridge_version": "1",
"connected": true,
"mode": "tcp",
"name": "krita"
}
]
},
"warnings": []
}Text given to the model
Before writing, the model called get_state on krita: "document": null. The document does not exist yet, so the script has to create it and open it in a window before it can paint.
Script written
A model, Claude Sonnet 5.5, wrote this script from the guide and from that state. It is not a script taken from the project’s tests. The script writes pixel bytes in the order blue, green, red, alpha; the colours of the capture below show that this is the order Krita expects for this document. The script sets batch mode before saveAs; what happens without it was not tried.
import os
krita = Krita.instance()
document = krita.createDocument(600, 400, "Postcard", "RGBA", "U8", "", 72.0)
krita.activeWindow().addView(document)
root = document.rootNode()
def lerp(a, b, t):
return round(a + (b - a) * t)
top, bottom = (0x1c, 0x38, 0x5c), (0x29, 0xa1, 0x96)
sky_pixels = bytearray()
for y in range(400):
t = y / 399
r, g, b = (lerp(top[i], bottom[i], t) for i in range(3))
sky_pixels += bytes((b, g, r, 255)) * 600
sky = document.createNode("Sky", "paintlayer")
root.addChildNode(sky, None)
sky.setPixelData(bytes(sky_pixels), 0, 0, 600, 400)
hill_pixels = bytearray()
for y in range(400):
for x in range(600):
inside = ((x - 300) / 260) ** 2 + ((y - 400) / 120) ** 2 <= 1
hill_pixels += bytes((0x38, 0x54, 0xC2, 255)) if inside else bytes((0, 0, 0, 0))
hill = document.createNode("Hill", "paintlayer")
root.addChildNode(hill, None)
hill.setPixelData(bytes(hill_pixels), 0, 0, 600, 400)
document.refreshProjection()
document.setBatchmode(True)
path = os.path.join(LMM_WORK_DIR, "postcard.kra")
saved = document.saveAs(path)
result = {"saved": saved, "kra_bytes": os.path.getsize(path), "layers": [node.name() for node in document.topLevelNodes()]}The Hub’s answer
{
"app": "krita",
"duration_ms": 1060,
"error": null,
"result": {
"kra_bytes": 110725,
"layers": [
"Background",
"Sky",
"Hill"
],
"saved": true
},
"status": "ok",
"stderr": "",
"stdout": "",
"truncated": false
}The script ran in 1.06 s. The document has three layers, because a new Krita document starts with a Background layer that the script left alone, under Sky and Hill. get_state then described the document and returned a capture of its projection:
{
"app": "krita",
"app_version": "5.2.9",
"bridge_version": "1",
"captured_at": "2026-10-21T12:47:50.526Z",
"connected": true,
"content_origin": "document",
"screenshot": "$XDG_RUNTIME_DIR/libremodular-mcp/captures/krita-20261021T124750502Z.png",
"state": {
"document": {
"active_layer": "{727e9cd9-e6e1-423c-ade7-1bf29d6d5289}",
"color_depth": "U8",
"color_model": "RGBA",
"color_profile": "sRGB-elle-V2-srgbtrc.icc",
"height": 400,
"layers": [
{
"id": "{69f5540d-2916-4771-8df7-1fcad46b677f}",
"name": "Background",
"opacity": 255,
"type": "paintlayer",
"visible": true
},
{
"id": "{ed307be8-5a93-4b16-8739-7508916ea687}",
"name": "Sky",
"opacity": 255,
"type": "paintlayer",
"visible": true
},
{
"id": "{727e9cd9-e6e1-423c-ade7-1bf29d6d5289}",
"name": "Hill",
"opacity": 255,
"type": "paintlayer",
"visible": true
}
],
"name": "Postcard",
"width": 600
}
},
"warnings": []
}The image produced
The capture is the merged image Krita displays, 600 by 400 pixels. The pixels read in it are #1C385C at the top, #29A196 at the bottom and #C25438 inside the hill, the three colours the text asked for. The .kra file the script saved is 110 725 bytes.
