Skip to main content
Glama
armorpaint-mcp
by baktubak

armorpaint-mcp

Drive ArmorPaint 1.0 from an MCP client.

An MCP server (Python) plus a small in-app bridge (an ArmorPaint plugin, written in the app's embedded C dialect) let an agent open projects, inspect and edit materials and node graphs, paint, and export textures — against a stock, unmodified ArmorPaint, including the official paid binary.

Not affiliated with, sponsored by, or endorsed by Armory3D or the ArmorPaint project. This is an independent third-party tool. Please do not file ArmorPaint bugs for it, and do not file its bugs upstream.

Status: works, lightly travelled. All 58 tools are implemented and were exercised against a live ArmorPaint 1.0: a 66-call sweep covering 42 tools returned 60 OK, 6 structured errors (all deliberate bad-input probes), 0 crashes or timeouts, at 15–513 ms per call. That includes the destructive surface — project_new, project_open, project_save_as, material_delete, export_* and the paint ops — run against a scratch project.

What that does not cover: Windows is the only platform tested. The Linux and macOS paths are written but unexercised. Long painting sessions, huge meshes and 4K exports are untested, and the optional viewport patch has only been built against the pinned commit named in docs/UPSTREAM_CHANGES.md. Expect rough edges outside the tested path, and please report them.


Architecture

One diagram, and it explains most of the design:

   ┌───────────────┐    MCP over stdio (JSON-RPC)    ┌──────────────────────┐
   │  MCP client   │ ──────────────────────────────► │   armorpaint-mcp     │
   │ (Claude Code, │ ◄────────────────────────────── │   server (Python)    │
   │  Claude, …)   │                                 └──────────┬───────────┘
   └───────────────┘                                            │
                                     write  req/<id>.json  (via os.replace — atomic)
                                     poll   res/<id>.done  (5 ms → 25 ms → 100 ms)
                                                                │
                                                     ┌──────────▼────────────┐
                                                     │     file mailbox      │
                                                     │  <spool>/req/  res/   │
                                                     │  heartbeat.json       │
                                                     │  bridge.lock          │
                                                     └──────────┬────────────┘
                                                                │
                                     read request, delete it, run it, write
                                     res/<id>.json then res/<id>.done
                                     — at most ONE per frame, inside on_update
                                                                │
 ┌──────────────────────────────────────────────────────────────▼───────────────────────┐
 │ ArmorPaint 1.0  (stock binary — nothing patched, nothing replaced)                   │
 │                                                                                      │
 │   data/plugins/armorpaint_mcp_bridge.c   the bridge: one minic plugin, one dispatcher           │
 │          │ calls                                                                     │
 │          ▼                                                                           │
 │   529 minic bindings ─► project · assets · objects · materials · nodes ·             │
 │                         brush & paint · viewport · export · filesystem               │
 └──────────────────────────────────────────────────────────────────────────────────────┘

Two things about this shape are worth knowing up front, because they are consequences of what ArmorPaint's plugin API actually offers, not preferences:

  • The transport is files, not a socket. ArmorPaint's plugin API exposes no inbound socket binding at all; its only network calls are hardcoded HTTPS GETs. A localhost listener is unreachable from inside a plugin. File I/O is unrestricted.

  • The bridge is a plugin, not a fork. It runs on the binary you already bought, survives app updates, and needs no compiler.

Both are argued in full, with the source citations, in docs/ARCHITECTURE.md. The byte-level contract is docs/PROTOCOL.md.

Related MCP server: ArmorPaint MCP

Requirements

ArmorPaint

1.0 (the current C/minic generation). The plugin system in the pre-2025 Haxe/Kha builds is a different thing entirely and is not supported.

OS

Windows 10/11 verified. Linux/macOS should work — the protocol is plain files — but are untested.

Python

3.11+

MCP client

Anything that can launch a stdio MCP server (Claude Code, Claude Desktop, …)

Compiler

None. Not for the core toolkit. Only the optional viewport patch needs a self-built ArmorPaint.

Install

Short version; the careful one is docs/INSTALL.md.

  1. Copy the bridge into ArmorPaint. Put plugin/armorpaint_mcp_bridge.c in <ArmorPaint>/data/plugins/. (That directory is beside ArmorPaint.exe, and it already contains autosave.c, converter.c and friends — that is how you know you found it.)

  2. Enable it. In ArmorPaint: Plugins tab → Preferences button → the Plugins list → tick armorpaint_mcp_bridge (the list shows the filename without its extension). It starts immediately; no restart. The choice is remembered, and the plugin auto-starts on subsequent launches.

  3. Install the server. pip install -e . in this repo (or point uv at the directory).

  4. Register it with your client. In .mcp.json:

    {
      "mcpServers": {
        "armorpaint": {
          "command": "python",
          "args": ["-m", "armorpaint_mcp"]
        }
      }
    }

    Usually that is all: the server finds the spool by locating your ArmorPaint install (it only accepts a candidate directory that really contains data\plugins). If your install is somewhere unusual — the itch.io app, for instance — add "env": {"ARMORPAINT_DIR": "C:\\ArmorPaint"}.

  5. Check the handshake. Ask the agent to call ap_ping. A healthy answer names the app version and the open project. If it reports the bridge as absent, see Troubleshooting.

Quickstart

With the bridge enabled and a mesh already imported, a prompt like this exercises the whole path — read state, mutate the node graph, write files to disk:

Using ArmorPaint: open D:/work/goblin.arm, tell me which material is active and which paint channels it has enabled. Then add a Noise node, wire its output into the base-color input of the output node, set its scale to 4.0, fill the layer with it, and export the textures to D:/work/out. List what landed there.

That is ap_project_open → ap_material_get_active → ap_node_add → ap_node_connect → ap_node_set_value → ap_material_update → ap_fill_layer → ap_export_textures → ap_fs_list. Each step is one request, one frame.

Do not drop the ap_fill_layer. Without it every call still returns success and the model, the viewport and the export are all unchanged — the graph is the paint source, not the render. See The loop that actually works.

Because the bridge answers at most one request per frame, a chain like this completes in well under a second — but a bake or a large export takes as long as ArmorPaint takes.

Tool index

58 tools. Each is backed by a named minic binding or a registered struct field; the implementation of each is tabulated in docs/MINIC_DIALECT_AND_API.md §2.13, and what was deliberately left out, with the reason, is §2.14 — read that before assuming a missing capability is an oversight.

Bridge & session (6)

Tool

Does

ap_bridge_status

Call this first when anything fails. Diagnoses the connection with no round trip: resolved spool path, heartbeat presence and whether its clock is advancing, plus a plain-language diagnosis and next step

ap_ping

Liveness: app version, uptime, open project, busy flag

ap_get_app_info

Window geometry, data path, project format version

ap_bridge_set_enabled

Turn the bridge off (and let the app idle again) or back on

ap_console_write

Write to ArmorPaint's console at info/error/log level

ap_show_message

Transient status message, or a modal box

Project (8)

Tool

Does

ap_project_new

New project

ap_project_open

Open an .arm (existence-checked first)

ap_project_save

Save in place; errors with no_project if never saved. Deferred — see below

ap_project_save_as

Set the filepath, then save. Deferred — ArmorPaint queues the write for the next frame (sys_notify_on_next_frame), so a success reply means queued, not written. Confirm with ap_fs_stat if it matters

ap_project_get_info

Filepath, basepath, version, envmap, camera FOV, BGRA flag

ap_project_list_texture_assets

Imported texture assets — live

ap_project_list_scripts

Project scripts (a save/load snapshot; labelled as such)

ap_quit

Quit ArmorPaint

Import / export (7)

Tool

Does

ap_import_asset

Import a texture, mesh or .arm by path

ap_import_envmap

Import an environment map

ap_set_envmap_params

Envmap strength and angle

ap_export_textures

Export the texture set to a directory, and report the filenames. 8-bit PNG; the format is not settable, and the base name comes from ArmorPaint's own state (the last export dialog, else untitled) — so read the returned filenames rather than predicting them

ap_export_material_bake

Bake the material to a plane and export it

ap_export_mesh

Export the mesh as .obj

ap_export_material

Export the material as .arm

Filesystem (4) — ap_fs_list, ap_fs_stat, ap_fs_mkdir run inside ArmorPaint; ap_read_image_file is answered by the server itself and hands back an image, so you can look at what an export produced (and it still works while ArmorPaint is closed). Enough for an agent to find its inputs and confirm its outputs, without a second tool server.

Introspection (5)

Tool

Does

ap_get_context

The workhorse read: tool, brush, layer, material, viewport mode, … (15 fields)

ap_get_config

The 16 readable config fields

ap_set_config

Write those same 16 (layer_res, camera_fov, workspace, workflow, …)

ap_get_main_object

The active paint object: name, visibility, transform

ap_get_object

Any object by name

Objects & meshes (6) — ap_shape_list, ap_shape_add, ap_object_duplicate, ap_object_set_transform, ap_object_set_visible, ap_append_mesh.

Materials (8)

Tool

Does

ap_material_get_active

Active material name and its 9 paint-channel flags

ap_material_update ⚠

Recompile the material after node edits. This does not change the viewport — see "The loop that actually works" below

ap_material_create

Create a material

ap_material_select

Make a material active, by name

ap_material_delete

Delete a material, by name

ap_material_assign

Assign a material to an object

ap_material_set_channels

Toggle the per-material paint channels

ap_material_list ⚠

Degraded. Reads a save/load snapshot: empty before the first save, and blind to materials created this session. Use ap_material_get_active for ground truth.

Material nodes (6) — ap_node_list (nodes and link topology), ap_node_add, ap_node_remove, ap_node_connect, ap_node_disconnect, ap_node_set_value (float / color / vector / button).

Node types are Blender-style uppercase identifiers — TEX_NOISE, TEX_BRICK, RGB, MIX_RGB — and ap_node_add validates against the 76 legal names rather than passing an unknown string into the app. The list is in docs/MINIC_DIALECT_AND_API.md §2.6.

Painting & viewport (7)

Tool

Does

ap_select_tool

Select one of the 14 tools, and read back what took

ap_set_brush

Radius, opacity, hardness, scale, angle, blending

ap_paint_stroke

A stroke in screen space, as a point list

ap_paint_stroke_world

A stroke in world space

ap_fill_layer

Fill the active layer

ap_set_display_channel

Switch the viewport display channel (one of 16)

ap_capture_to_project

Capture the viewport into the project as a texture asset — see Limitations; this does not produce a file you can read

Optional, patched builds only (1) — ap_capture_viewport writes the 3D viewport to a real PNG and returns it as an image, which is what closes the see → adjust → see loop. It requires the opt-in native patch in patch/ and therefore a self-built ArmorPaint; see docs/UPSTREAM_CHANGES.md. On a stock binary the tool reports unsupported and says why. Measured on a patched 1.0 build: 11–15 ms in-app, ~140 ms round trip for an 800×600 PNG.

The loop that actually works

This one is worth stating plainly, because every step reports success and the obvious ordering still shows you nothing:

ap_node_add / ap_node_set_value / ap_node_connect   edit the graph
ap_material_update                                  recompile it
ap_fill_layer   (or ap_paint_stroke / _world)       APPLY it   <-- the step people miss
ap_capture_viewport                                 look at it

ap_material_update does not render. ArmorPaint's viewport shows the layer stack; the node graph is only the paint source. Measured: viewport captures taken before and after a colour change plus ap_material_update are byte-identical — the pixels change only once you fill or paint. There is no scriptable layer CRUD (see Limitations), so ap_fill_layer applies to whichever layer the user has selected.

Limitations

These are properties of ArmorPaint's plugin API, verified by reading its source. They are not temporary gaps, and no amount of work on this repo removes them.

  • No 3D viewport capture on a stock binary. A plugin can capture the viewport to a GPU texture, but the only save path (viewport_save_texture) encodes it into the project's in-memory asset list — persisted inside the .arm, unreachable from another process. iron_encode_png and gpu_get_texture_pixels are not exposed to plugins. What an agent can actually see is its work product: ap_export_textures writes real PNGs, which the server reads and returns as images. For the shaded viewport itself, the optional patch (12 added lines, one new binding) closes the gap on a self-built ArmorPaint.

  • ArmorPaint renders at full rate while the bridge is enabled. The app normally sleeps after ~120 idle frames, and a sleeping app does not dispatch plugin callbacks — so a polling bridge must keep it awake, and pays for it in GPU and power. ap_bridge_set_enabled (and a toggle in the Plugins tab) turns it off when no agent is working. This is a real cost, not a rounding error.

  • One request per frame. minic has no threads, so every handler runs inline on the render thread. Batching a backlog into one frame is not just slow, it risks the interpreter's 8 MB per-frame arena. Throughput is therefore bounded by frame rate; a hundred-op plan is a hundred frames.

  • A slow handler is a visible hitch in the user's painting, for the same reason. Long operations (bake, large export) return immediately with a pending token and are polled.

  • No layer control. Not a single layer binding exists beyond "fill the active layer" — no add, delete, reorder, rename, opacity, blend mode, or mask. Layer state is not readable either.

  • No undo/redo, no bake-parameter control, no export format/bit-depth control, no tone mapping or LUT, no shelf/resource search, no project metadata, no UI automation. Each of these is a missing binding, itemised with its evidence in docs/MINIC_DIALECT_AND_API.md §2.14.

  • Windows is the tested platform. The protocol is plain files and should port, but nothing else has been measured.

Troubleshooting

The agent says the bridge is not detected. Check, in order: (1) armorpaint_mcp_bridge.c is in <ArmorPaint>/data/plugins/ — the directory beside the executable, not the config directory; (2) its checkbox is ticked in Preferences → Plugins; (3) bridge.lock and heartbeat.json exist in the spool directory; (4) the server and the plugin agree on where the spool is.

That last one is the common case and the confusing one, because both halves are individually healthy. The server reports the spool path it resolved and how it decided, in every transport error — read that path and compare it to where the plugin is actually writing. If the server fell back to its per-user default (%LOCALAPPDATA%\armorpaint-mcp\spool) it means it could not find an ArmorPaint install at all; set ARMORPAINT_DIR.

If heartbeat.json exists but its t value is not advancing between two reads, the plugin loaded but is not being ticked — the app is idle, or the bridge is disabled.

Does the ArmorPaint window have to be in the foreground? No. ArmorPaint has two sleep gates — a Windows-background gate (3 frames) and an idle gate (120 frames) — and it turns out both increment the same counter, which the bridge resets on every frame. Neither ever trips. This was traced in iron.h rather than assumed; the trace is in docs/MINIC_DIALECT_AND_API.md §0.3. The tolerance is narrow (at most 3 consecutive missed frames), so if you modify the plugin, keep iron_delay_idle_sleep() as the first statement of on_update. A minimized window has not been separately measured.

The plugin is not listed in Preferences → Plugins. The list shows only files ending in .c, read directly from <ArmorPaint>/data/plugins. A .txt extension added by a browser download, or the file sitting one directory too high, both produce an empty row. The list is also cached until the panel is reopened, so close and reopen Preferences after copying the file. Alternatively use the Import button in that same panel, which copies a .c or .zip into the right place for you.

It is listed, ticks on, and nothing happens. Look at ArmorPaint's Console. minic reports compile and run errors as armorpaint_mcp_bridge.c:<line>: error: <message> and does not pop a dialog — a plugin with a syntax error fails quietly. Note also that some minic failures are entirely silent by design (the catalogue is §1.12), which is why the bridge logs a version banner on load: no banner in the console means the script did not reach the end of main.

ArmorPaint is installed under C:\Program Files. Then data\plugins\ needs elevation to write, and ArmorPaint itself redirects its config.json to %USERPROFILE%\Saved Games\ArmorPaint\ — while still loading plugins from the install directory. Use the in-app Import button rather than copying by hand, and point the spool somewhere writable with ARMORPAINT_SPOOL. Installing ArmorPaint outside Program Files avoids the whole class of problem. Details in docs/INSTALL.md.

Documentation

File

Contents

docs/INSTALL.md

Step-by-step Windows install

docs/ARCHITECTURE.md

Why a file mailbox, why a plugin, and the constraints that forced both

docs/PROTOCOL.md

The wire contract — either half can be reimplemented against it

docs/MINIC_DIALECT_AND_API.md

The minic dialect and the whole plugin API, read out of the source. If you are writing a plugin, this is the document.

docs/API_REFERENCE.md

The generated binding list

docs/UPSTREAM_CHANGES.md

The optional viewport patch, exactly

Everything in docs/ was derived by reading ArmorPaint 1.0 at commit 906418acc600132fa927876d208eb452dc5a0967. Public web documentation for ArmorPaint's scripting describes the pre-2025 Haxe/Kha version and is wrong for this generation — a trap worth naming.

Credits and licensing

armorpaint-mcp is MIT licensed — see LICENSE.

ArmorPaint is by the Armory3D project and is licensed zlib/libpng: https://armorpaint.org · https://github.com/armory3d/armorpaint · manual

This repository contains no ArmorPaint source and is not a fork. The optional patch in patch/ edits your own checkout in place, marks every line it inserts, and is reversible.

ArmorPaint's development is funded by sales of its binaries. They are paid — from https://armorpaint.org/download and the itch.io store at https://armorpaint.itch.io/armorpaint. Building from source is explicitly supported by upstream and is free, but if this tool is useful to you, the project it drives is worth paying for.

Available Tools

58 tools
ap_append_meshA

Append geometry to the current project, either from a mesh file on disk or from inline Wavefront OBJ text. Give exactly one of 'path' or 'obj_data'. Pass inline OBJ with ordinary newlines — they are re-encoded for the wire and restored inside ArmorPaint — but it must contain no double quotes, backslashes or '|' (the bridge's JSON parser does not decode escapes and '|' is the line separator), and the whole request is capped at 8 KB. Prefer 'path' for anything non-trivial.

Inputs: path, obj_data.

ap_bridge_set_enabledA

Turn the bridge's per-frame polling on or off. This has a real cost: while enabled the plugin calls iron_delay_idle_sleep() every frame, which keeps ArmorPaint rendering at full rate even when unfocused (that is also what makes remote control of a background window possible). Disable it when no agent is working. WARNING: disabling it stops the bridge from reading requests, so this is the last tool that will work until someone re-enables it from the Plugins tab.

Inputs: enabled (required).

ap_bridge_statusA

Diagnose the connection to ArmorPaint. Answered entirely by this server, with no round trip, so it works when everything else is failing: it reports the resolved spool directory and how it was resolved, whether the bridge heartbeat exists, whether the heartbeat's clock is advancing (the only valid liveness test), the bridge/app versions, the open project, queue depth, and a plain-language diagnosis plus next step. Call this FIRST whenever any other tool reports a transport error.

Inputs: probe.

ap_capture_to_projectA

Capture the 3D viewport into the project as a packed texture asset. Works on a stock ArmorPaint, but the pixels land INSIDE the project (persisted only when the .arm is saved) — nothing outside ArmorPaint can read them, so you cannot look at the result. To actually see the viewport, use ap_capture_viewport (needs the optional viewport patch). NOTE: the bridge handler runs inline in one frame and cannot wait for a re-render, so the capture is of the frame ALREADY drawn and may include the UI overlay; ArmorPaint's own two-frame settle is not reproducible from a plugin.

Inputs: width, height.

ap_capture_viewportA

Capture the shaded 3D viewport to a PNG file and return the image so you can look at your own work. REQUIRES the optional viewport patch (docs/UPSTREAM_CHANGES.md) which adds the viewport_save_texture_to_file binding — on a stock build this returns code 'unsupported', and the fallbacks are ap_capture_to_project (in-project only) or ap_export_textures (writes real files, but flat textures rather than the shaded view).

Inputs: path (required), width, height, include_image.

ap_console_writeA

Write a line to ArmorPaint's own console. Write-only: no binding can read the console back, so this is for leaving a trail for the human, not for logging you intend to read.

Inputs: text (required), level.

ap_export_materialB

Export the active material as a reusable .arm material file.

Inputs: path (required).

ap_export_material_bakeA

Bake the ACTIVE MATERIAL onto a plane and export the result as images (export_texture_run with bake_material=1). This is not mesh map baking — there is no bake-run binding for normal/AO/curvature maps.

Inputs: directory (required).

ap_export_meshA

Export the paint geometry as Wavefront OBJ. The binding appends '.obj' to the path you give, so pass a path WITHOUT the extension.

Inputs: path (required).

ap_export_texturesA

Export the project's texture channels to real image files on disk — the only binding in ArmorPaint's plugin API that writes images to disk. Returns the files found in the target directory afterwards; feed one to ap_read_image_file to look at it. IMPORTANT LIMITS, none of which are settable from a plugin: 'directory' is a DIRECTORY, not a filename; the base filename comes from the last name used in ArmorPaint's own export dialog, falling back to 'untitled'; the channel suffixes come from the active export preset ('generic' is auto-selected on first use); and the format/bit depth is whatever the UI is set to (8-bit PNG by default).

Inputs: directory (required).

ap_fill_layerA

Fill the selected layer with the active material and push an undo step. THIS IS THE STEP THAT MAKES A NODE-GRAPH EDIT VISIBLE — ap_material_update only recompiles the material; nothing appears in the viewport until you fill or paint. The working loop is: ap_node_* edits -> ap_material_update -> ap_fill_layer -> ap_capture_viewport. Fails with 'no_project'/'bad_args' if no layer is selected. This is also the ONLY layer operation in ArmorPaint's plugin API — there is no create/delete/rename/mask/opacity/blend binding, so layer management has to be done by hand in the UI.

Inputs: no named parameters.

ap_fs_listA

List a directory as seen by the ArmorPaint process (which may be a different machine/container than this server). Useful for finding what an export actually wrote.

Inputs: path (required).

ap_fs_mkdirA

Create a directory (as the ArmorPaint process), e.g. before an export.

Inputs: path (required).

ap_fs_statA

Report whether a path exists, whether it is a directory, and whether ArmorPaint considers it absolute.

Inputs: path (required).

ap_get_app_infoA

Application-level facts: window title, window size and position, ArmorPaint's data directory, and the project format version.

Inputs: no named parameters.

ap_get_configA

Application preferences that are readable from a plugin: window size/scale, supersampling, keymap, theme, undo steps, camera FOV, default layer resolution, live-brush/live-material/node-preview toggles, workspace and workflow, plus the recent-project and plugin lists. Post-processing settings (SSAO, bloom, LUT, gamma...) are not exposed by the API.

Inputs: no named parameters.

ap_get_contextA

The workhorse read: the live painting context — active tool, brush radius/opacity/hardness/scale/angle/blending, viewport display mode, x-ray flag, whether a layer and a material are selected, and the active material's name. Note that only whether a layer is selected can be reported: the layer object itself is an opaque pointer with no readable fields.

Inputs: no named parameters.

ap_get_main_objectA

Describe the main paint object: name, visibility and transform (location, rotation quaternion, scale).

Inputs: no named parameters.

ap_get_objectA

Describe a scene object by name: visibility and transform. Names are matched against the paint objects. NOTE: there is no binding that ENUMERATES objects — you can only look one up by name, or use ap_get_main_object.

Inputs: name (required).

ap_import_assetA

Import a file into the project, dispatched by extension: an image becomes a texture asset, a mesh replaces/adds paint geometry, a .arm becomes a material.

Inputs: path (required).

ap_import_envmapA

Import an HDR image as the environment map (script_import_asset with hdr_as_envmap=1) and report the resulting envmap name.

Inputs: path (required).

ap_material_assignA

Assign an existing material to a scene object. Both are looked up by name, and a material's name is its node-canvas name (what ap_material_get_active reports).

Inputs: object (required), material (required).

ap_material_createA

Create a new material slot and make it active.

Inputs: name (required).

ap_material_deleteA

Delete a material slot by name. Irreversible from here — ArmorPaint's plugin API has no undo/redo binding, so this cannot be taken back except by the user pressing Ctrl+Z in the app.

Inputs: name (required).

ap_material_get_activeA

Report the active material: its name (a material's name is its node-canvas name) and its nine per-channel paint flags.

Inputs: no named parameters.

ap_material_listA

List material names. DEGRADED — read the caveat before trusting it: this reads project_t.material_nodes, which is a SNAPSHOT written only at save and load. It is null in a project that has never been saved, and it misses materials created during this session. There is no live material enumeration binding. For the material you are actually working on, use ap_material_get_active.

Inputs: no named parameters.

ap_material_selectA

Make an existing material active, by name.

Inputs: name (required).

ap_material_set_channelsA

Enable or disable which channels the active material paints into. These are the material's own paint_* flags, not texture-set channels (ArmorPaint's plugin API has no channel add/remove). Omitted channels are left alone.

Inputs: base, height, normal, opacity, +5 more.

ap_material_updateA

Recompile the active material after node edits (script_material_update). THIS DOES NOT CHANGE THE VIEWPORT. ArmorPaint renders the layer stack, and the node graph is only the paint SOURCE — measured: viewport captures before and after a colour change plus ap_material_update are byte-identical. To make a graph edit visible you must apply it: ap_fill_layer (whole layer) or ap_paint_stroke / ap_paint_stroke_world (where you paint). Call this once after a batch of ap_node_* edits, then apply.

Inputs: no named parameters.

ap_node_addA

Add a node to the active material's graph at a canvas position, and return its new id. Valid types: ATTRIBUTE BAKE_CURVATURE BLUR BOOL BRIGHTCONTRAST BUMP CLAMP COLMASK COMBINE_COLOR COMBXYZ CURVE_RGB CURVE_VEC CUSTOM DIRECT_WARP ENUM FLOAT_CURVE GAMMA GROUP GROUP_INPUT GROUP_OUTPUT HUE_SAT INVERT_COLOR LAYER LAYER_MASK MAPPING MAPRANGE MATERIAL MATH MIX_NORMAL_MAP MIX_RGB NEURAL_EDIT_IMAGE NEURAL_IMAGE_TO_3D_MESH NEURAL_IMAGE_TO_PBR NEURAL_REPEAT NEURAL_SAVE_IMAGE NEURAL_TEXT_TO_IMAGE NEURAL_UPSCALE_IMAGE NEW_GEOMETRY NORMAL NORMAL_MAP OBJECT_INFO OUTPUT_MATERIAL_PBR PICKER QUANTIZE REPLACECOL RGB RGBA RGBTOBW SCRIPT_CPU SEPARATE_COLOR SEPXYZ SHADER_GPU STRING TEX_BAKE TEX_BRICK TEX_CAMERA TEX_CHECKER TEX_COORD TEX_GABOR TEX_GRADIENT TEX_IMAGE TEX_MAGIC TEX_NOISE TEX_TEXT TEX_VORONOI TEX_WAVE TILESHEET TILESHEET_ANIM UVMAP VALTORGB VALUE VECTOR VECT_MATH VECT_ROTATE VECT_TRANSFORM WIREFRAME.

Inputs: type (required), x, y.

ap_node_connectA

Link one node's output socket to another node's input socket. Socket numbers are 0-based positions, as reported by ap_node_list; the output node's material socket order is Base Color, Opacity, Occlusion, Roughness, Metallic, Normal Map, Emission, Height, Subsurface.

Inputs: to_id (required), from_id (required), to_socket, from_socket.

ap_node_disconnectA

Remove whatever is linked into one input socket of a node.

Inputs: to_id (required), to_socket (required).

ap_node_listA

Read the active material's node graph: every node's id, name, type and canvas position, its input/output socket names, and every link (from_id/from_socket -> to_id/to_socket). This is the richest and best-tested part of the API — node work is where an agent has the most real leverage in ArmorPaint.

Inputs: no named parameters.

ap_node_removeA

Remove a node from the active material's graph by id (from ap_node_list).

Inputs: id (required).

ap_node_set_valueA

Set a value on a node. Four kinds: 'float' (one number on a socket), 'color' (r,g,b,a on a socket), 'vector' (x,y,z on a socket), and 'button' (a node's own widget — dropdown index, checkbox 0/1, or a slider value; see the node reference for each type's button list). Socket-based kinds default to the node's INPUT sockets. Follow a batch of edits with ap_material_update.

Inputs: id (required), kind (required), color, value, +4 more.

ap_object_duplicateA

Duplicate a scene object by name and return the new object's name.

Inputs: name (required).

ap_object_set_transformA

Set an object's location, rotation and/or scale, then rebuild its matrix. Rotation is given as XYZ Euler angles in DEGREES and converted to the quaternion the engine wants. Omitted components are left alone. ArmorPaint's world is Z-up.

Inputs: name (required), scale, location, rotation_euler_degrees.

ap_object_set_visibleA

Show or hide a scene object. This is the object's visible flag: a hidden object stays in the project with its materials intact, it just stops rendering.

Inputs: name (required), visible (required).

ap_paint_strokeA

Paint a stroke in SCREEN space and close it. Coordinates are normalised (0..1) across the viewport, so 0.5,0.5 is the centre and what gets painted depends on the current camera. Paints into the selected layer with the active tool/brush; silently does nothing if no project is open, no layer is selected, or the selected layer is a group. At most 48 points — the whole stroke runs inside one frame.

Inputs: points (required).

ap_paint_stroke_worldA

Paint a stroke in WORLD space and close it — camera-independent, which makes it the reliable choice for scripted painting. ArmorPaint's world is Z-up; use ap_get_main_object for the object's bounds.

Inputs: points (required).

ap_pingA

Round-trip health check: sends a real request through the file mailbox and returns the bridge's app time, window title and current project path. Confirms the whole path works end to end, unlike ap_bridge_status which only reads the heartbeat.

Inputs: no named parameters.

ap_project_get_infoA

Project-level state: file path, base path, format version, BGRA flag, environment map name/strength/angle, and camera FOV.

Inputs: no named parameters.

ap_project_list_scriptsA

List script assets attached to the project. DEGRADED: project_t.script_datas is a snapshot written at save/load, so it is empty in a fresh project and stale after changes made this session.

Inputs: no named parameters.

ap_project_list_texture_assetsA

List the names of texture assets imported into the project. This one is LIVE (it reads project_t.assets), unlike the material/mesh lists which are save-time snapshots.

Inputs: no named parameters.

ap_project_newA

Start a new, empty project. Discards the current project WITHOUT prompting and without saving — call ap_project_save first if the work matters.

Inputs: no named parameters.

ap_project_openA

Open an existing .arm project file. Discards the current project without prompting.

Inputs: path (required).

ap_project_saveA

Save the current project to its existing path. Fails with code 'no_project' if the project has never been saved — use ap_project_save_as to give it a path first. THE SAVE IS DEFERRED: ArmorPaint queues the write for the next frame, so a successful reply means 'queued', not 'written'. Call ap_fs_stat on the path if you need proof it landed.

Inputs: no named parameters.

ap_project_save_asA

Set the project file path and save to it (project_filepath_set + project_save). Like ap_project_save, the write is DEFERRED to the next frame, so the reply confirms the path was set and the save queued — verify with ap_fs_stat.

Inputs: path (required).

ap_quitA

Quit ArmorPaint. Unsaved work is LOST — save first. Requires confirm=true. The app usually exits before it can commit a reply, so a 'no reply' result here is normal and the response says whether the request was picked up.

Inputs: confirm (required).

ap_read_image_fileA

Read an image file from THIS server's filesystem and return it as an image, so you can look at what ArmorPaint exported. Answered locally — no round trip, and it works while ArmorPaint is closed. Pair it with ap_export_textures: export, then read one of the reported files. PNG/JPEG/GIF/WEBP/BMP only (EXR cannot be displayed), up to 6 MB.

Inputs: path (required), max_bytes.

ap_select_toolA

Select the active tool: bake, blur, brush, clone, colorid, cursor, decal, eraser, fill, material, particle, picker, select, text. The selection is read back from the context so the result confirms it took effect.

Inputs: tool (required).

ap_set_brushA

Set brush parameters on the painting context. Omitted values are left alone. These are direct context writes, so they take effect immediately for the next stroke.

Inputs: angle, scale, radius, opacity, +2 more.

ap_set_configA

Change application preferences. Only the listed fields are writable; anything else in ArmorPaint's preferences is not exposed to plugins. layer_res is the DEFAULT resolution for new layers — changing it does not resize existing ones.

Inputs: theme, keymap, window_h, window_w, +10 more.

ap_set_display_channelA

Set what the 3D viewport displays: base_color, emission, height, lit, mask, material_id, metallic, none, normal_map, object_id, object_normal, occlusion, opacity, path_trace, roughness, subsurface, texcoord. 'lit' is the normal shaded view; the others isolate a channel or a debug output.

Inputs: mode (required).

ap_set_envmap_paramsA

Set environment map strength and/or rotation angle (direct writes to project_t).

Inputs: angle, strength.

ap_shape_addA

Add a built-in primitive to the scene (cube, sphere, plane, cylinder, cone, torus, the bevelled/shared-UV cube variants, the 2048-poly plane/sphere, or 'empty'). Use ap_shape_list for this build's exact names.

Inputs: name (required).

ap_shape_listA

List the built-in primitive shapes that ap_shape_add accepts, from the running build. Typically: cone cube cube_bevel cube_bevel_shared_uvs cube_shared_uvs cylinder empty torus plane plane_2048 sphere sphere_2048.

Inputs: no named parameters.

ap_show_messageA

Show a message to the user in ArmorPaint: a transient toast by default, or a modal box. Prefer the toast — a modal blocks ArmorPaint's UI thread, which also stalls the bridge until the user dismisses it.

Inputs: text (required), modal, title, seconds.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A3.6/5.0

    Scored across 58 tools

    Disambiguation4/5

    Most tools map to a distinct resource/action, and the descriptions aggressively disambiguate near-pairs like ap_capture_to_project vs ap_capture_viewport and ap_paint_stroke vs ap_paint_stroke_world. A handful of similarly named tools (ap_export_material vs ap_export_material_bake, ap_bridge_status vs ap_ping) could still be misselected without reading the details.

    Naming Consistency3/5

    All names are lowercase snake_case with an ap_ prefix, but the verb position is inconsistent: action-first names (ap_get_config, ap_export_textures, ap_set_brush) sit alongside resource-first names (ap_material_create, ap_project_open, ap_object_set_transform). The names remain readable, but the ordering convention is not uniform.

    Tool Count2/5

    At 58 tools, this is a very large surface and far beyond the 3-15 tool sweet spot. The ArmorPaint domain is broad and each tool corresponds to a real plugin binding, but many related operations could be consolidated into higher-level tools without losing capability.

    Completeness3/5

    The tool set covers project lifecycle, materials, node graphs, objects, painting, import/export, and bridge health. However, there are notable documented gaps that matter for real workflows: no layer create/delete/mask/opacity/blend, no object enumeration, no undo/redo, and degraded material/project listings.

    Maintenance

    ActivityStale
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      B
      maintenance
      Enables agents to observe, batch, script, recover, bake, and export Adobe Substance 3D Painter scenes through a Model Context Protocol server with specialist schemas and persistent tooling.
      2
      MIT
    • A
      license
      B
      quality
      C
      maintenance
      Enables AI agents to control ArmorPaint for 3D painting workflows, including project and mesh management, material and node editing, layers, masks, painting, baking, cameras, and exports, with viewport captures returned as images.
      41
      1
      MIT
    • A
      license
      Not graded
      quality
      B
      maintenance
      Enables MCP-compatible AI clients to control a running Krita instance, allowing them to create canvases, paint strokes, draw shapes, export images, and perform other painting tasks.
      MIT
    • F
      license
      Not graded
      quality
      B
      maintenance
      Enables MCP clients to drive Blockbench Desktop locally for Minecraft model, UV, texture, animation, and export workflows. It provides typed operations, high-level task workflows, native controls, and inspection/verification tools over an authenticated loopback connection.
      3
      -