armorpaint-mcp
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@armorpaint-mcp Open the robot project and export its base color texture as PNG"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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.
Copy the bridge into ArmorPaint. Put
plugin/armorpaint_mcp_bridge.cin<ArmorPaint>/data/plugins/. (That directory is besideArmorPaint.exe, and it already containsautosave.c,converter.cand friends — that is how you know you found it.)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.Install the server.
pip install -e .in this repo (or pointuvat the directory).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"}.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 toD:/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 |
| 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 |
| Liveness: app version, uptime, open project, busy flag |
| Window geometry, data path, project format version |
| Turn the bridge off (and let the app idle again) or back on |
| Write to ArmorPaint's console at info/error/log level |
| Transient status message, or a modal box |
Project (8)
Tool | Does |
| New project |
| Open an |
| Save in place; errors with |
| Set the filepath, then save. Deferred — ArmorPaint queues the write for the next frame ( |
| Filepath, basepath, version, envmap, camera FOV, BGRA flag |
| Imported texture assets — live |
| Project scripts (a save/load snapshot; labelled as such) |
| Quit ArmorPaint |
Import / export (7)
Tool | Does |
| Import a texture, mesh or |
| Import an environment map |
| Envmap strength and angle |
| 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 |
| Bake the material to a plane and export it |
| Export the mesh as |
| Export the material as |
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 |
| The workhorse read: tool, brush, layer, material, viewport mode, … (15 fields) |
| The 16 readable config fields |
| Write those same 16 ( |
| The active paint object: name, visibility, transform |
| 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 |
| Active material name and its 9 paint-channel flags |
| Recompile the material after node edits. This does not change the viewport — see "The loop that actually works" below |
| Create a material |
| Make a material active, by name |
| Delete a material, by name |
| Assign a material to an object |
| Toggle the per-material paint channels |
| Degraded. Reads a save/load snapshot: empty before the first save, and blind to materials created this session. Use |
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 |
| Select one of the 14 tools, and read back what took |
| Radius, opacity, hardness, scale, angle, blending |
| A stroke in screen space, as a point list |
| A stroke in world space |
| Fill the active layer |
| Switch the viewport display channel (one of 16) |
| 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 itap_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_pngandgpu_get_texture_pixelsare not exposed to plugins. What an agent can actually see is its work product:ap_export_textureswrites 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 |
Step-by-step Windows install | |
Why a file mailbox, why a plugin, and the constraints that forced both | |
The wire contract — either half can be reimplemented against it | |
The minic dialect and the whole plugin API, read out of the source. If you are writing a plugin, this is the document. | |
The generated binding list | |
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 toolsap_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
Scored across 58 tools
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.
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.
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.
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
Related MCP Connectors
Control Unreal Engine to browse assets, import content, and manage levels and sequences. Automate…
Nifty's MCP server — exposes tasks, projects, messages, and files as tools for AI agents.
Persistent memory, knowledge graph, task management and more for AI agents via MCP.
Project management shared by people and AI agents, with persistent project state through MCP.
Related MCP Servers
- AlicenseNot gradedqualityBmaintenanceEnables 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.2MIT
- AlicenseBqualityCmaintenanceEnables 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.411MIT
- AlicenseNot gradedqualityBmaintenanceEnables 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
- FlicenseNot gradedqualityBmaintenanceEnables 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-