substance-painter-mcp
by jwang47
README.md
# substance-painter-mcp
An MCP server for driving **Adobe Substance 3D Painter** conversationally — bake mesh maps,
stack smart materials, and tune generator parameters by talking to the app instead of
guessing weights and re-baking.
Built for look-dev iteration, where weathering is authored by taste against a reference
rather than by fixed numbers.
## Requirements
* **Substance 3D Painter 12.x** (developed against 12.1.1 / Python API 0.3.5), running
* **Python 3.10+** for the MCP server
Windows, macOS and Linux. No launch flags and no `--enable-remote-scripting` — Painter can
be started normally. If you have no Python installed, Painter bundles a complete CPython
3.13 at `<install>/resources/pythonsdk/python.exe` that works fine.
## Install
### 1. Install the bridge plugin into Painter
```bash
python scripts/install_plugin.py
```
This copies `painter_plugin/substance_painter_mcp_bridge` into Painter's user plugin
folder, `<Documents>/Adobe/Adobe Substance 3D Painter/python/plugins/`. On Windows the
Documents location is read from the shell-folder registry, so OneDrive redirection and
moved Documents folders are handled; pass `--plugins-dir` to override.
Use `--link` to symlink instead of copy while developing (needs Developer Mode or an
elevated shell on Windows), and `--uninstall` to remove it.
### 2. Enable it in Painter
**Python → Reload Plugins Folder**, then tick `substance_painter_mcp_bridge` under
**Python → Plugins**.
> **This step is required.** Painter treats everything in `python/plugins/` as an
> *optional component*: modules are discovered but not started until enabled. Only
> `python/startup/` loads automatically. Painter remembers the choice, so this is
> one-time — but a fresh install that skips it will simply never open the port.
Prefer no UI step? `python scripts/install_plugin.py --startup` installs into
`startup/` instead, which always loads, at the cost of losing the on/off toggle.
Either way the log window should show:
```
[substance-painter-mcp] listening on 127.0.0.1:60043
```
### 3. Install the MCP server
```bash
python -m venv .venv
.venv/Scripts/python -m pip install -e . # .venv/bin/python on macOS/Linux
```
### 4. Register it with your MCP client
```json
{
"mcpServers": {
"substance-painter": {
"command": "C:/Users/you/code/substance-painter-mcp/.venv/Scripts/python.exe",
"args": ["-m", "substance_painter_mcp"]
}
}
}
```
Then call `painter_status` — it distinguishes "Painter isn't running" from "that
operation was invalid", so it's the right first thing to try when something fails.
### Configuration
| Variable | Default | Meaning |
| -- | -- | -- |
| `SP_MCP_BRIDGE_HOST` | `127.0.0.1` | Bind/connect address |
| `SP_MCP_BRIDGE_PORT` | `60043` | Bridge port |
| `SP_MCP_BRIDGE_TIMEOUT` | `600` | Client socket timeout, seconds |
The bridge binds to loopback only. It executes arbitrary Python inside Painter by design
(`painter_exec`), so do not expose the port beyond localhost.
## Tools
| Tool | Purpose |
| -- | -- |
| `painter_status` | Reachability and version check |
| `painter_project_info` / `painter_create_project` / `painter_open_project` / `painter_save_project` | Project lifecycle |
| `painter_search_resources` | Find smart materials, generators, masks, export presets |
| `painter_get_baking_parameters` / `painter_set_baking_parameters` | Inspect and set bake settings, including high-poly path and cage distance |
| `painter_bake` / `painter_baking_status` | Bake mesh maps (async, with optional wait) |
| `painter_layer_tree` | Read the layer stack; every node has a `uid` |
| `painter_insert_smart_material` | Instantiate a shelf smart material |
| `painter_insert_layer` / `painter_insert_generator` / `painter_add_mask` | Build stacks |
| `painter_set_fill_source` / `painter_import_resource` | Assign a bitmap or flat colour to a fill layer's channel |
| `painter_set_layer` / `painter_delete_layer` | Name, visibility, opacity, blending mode; delete |
| `painter_screenshot` | **Capture the viewport** — see the result instead of inferring it |
| `painter_get_parameters` / `painter_set_parameters` / `painter_apply_preset` | **Tune generators** — the look-dev loop |
| `painter_list_export_presets` / `painter_export_textures` | Export maps |
| `painter_list_ops` / `painter_call` / `painter_exec` | Escape hatches |
### The tuning loop
`painter_get_parameters` is the tool that makes conversational iteration work. It returns
each parameter's key, human label, widget type, current value and enum options, so the
model discovers that a rust generator exposes *Dirt Level* rather than guessing at names:
```
painter_layer_tree() → find the generator's uid
painter_get_parameters(uid=42) → see what knobs exist
painter_set_parameters(uid=42, '{"Dirt Level": 0.7}')
painter_screenshot() → look at what that did
```
`painter_screenshot` closes the loop. Without it the model is editing blind and has to
trust that a parameter did what it claims; with it, each change can actually be checked.
A smart material is a group of layers, so tuning one means walking into the group and
adjusting the generator effects inside it. `painter_layer_tree` returns the full
hierarchy with uids in a single call for that reason.
Wear is driven by the baked mesh maps via generators — `mg_metal_edge_wear` for edges and
bolts, `mg_dirt` for crevices, `mg_position` multiplied into a mask to bias wear
vertically. Mask effects blend with each other, which is how "heavier down low, lighter
up top" gets expressed.
## How it works
```
Claude ──stdio──> MCP server (this repo, any Python)
│
│ newline-delimited JSON over TCP, loopback only
▼
Bridge plugin (runs inside Painter, Qt server on the main thread)
│
▼
substance_painter Python API
```
The bridge is a **Painter plugin** rather than the `--enable-remote-scripting` HTTP
endpoint, for two reasons:
1. **No launch flag.** Painter can be started normally, including from Creative Cloud.
2. **Thread affinity.** The `substance_painter` API is only safe to call from the main/UI
thread. A Qt `QTcpServer` created inside the plugin lives on Painter's own event loop,
so every request handler already runs on the right thread — no cross-thread
marshalling and no temp-file result passing.
### Wire protocol
Newline-delimited JSON, one object per line:
```
→ {"id": 1, "op": "layers.tree", "params": {"deep": true}}
← {"id": 1, "ok": true, "result": {...}}
← {"id": 1, "ok": false, "error": {"kind": "no_project", "message": "..."}}
```
`json.dumps` escapes newlines inside strings, so a serialised message never contains a
raw newline of its own and the framing is safe.
## Development
```bash
.venv/Scripts/python -m pytest # protocol tests, no Painter needed
.venv/Scripts/python -m ruff check .
```
`tests/` runs the bridge client against a fake NDJSON server, so framing, error mapping
and reconnect-after-plugin-reload are covered without the application installed. Anything
touching the `substance_painter` API needs Painter running.
Against a running Painter:
```bash
python scripts/smoke_test.py # exercise the bridge end to end
python scripts/reload_plugin.py # hot-reload after editing the plugin
python scripts/smoke_test.py --mesh mesh.fbx --force # also create a project
```
`reload_plugin.py` avoids a Painter restart on every edit. Install the plugin with
`--link` so the files are already in place and only the reload is needed.
## Notes and limitations
* **Baking is asynchronous.** API 0.3.5 has no synchronous `bake()`, only `bake_async`.
The bridge tracks progress through Painter's `BakingProcessProgress` /
`BakingProcessEnded` events and `painter_bake` polls to completion by default.
* **Cage distance matters on meshes with overlapping shells.** Cage distances are
*relative to the bounding box* by default, and a generous cage samples interior
surfaces and produces black patches. Read the real key names with
`painter_get_baking_parameters` before setting them — they come from Painter, not from
this server.
* **One bake at a time.** The bridge rejects a second concurrent bake.
See [docs/api-findings.md](docs/api-findings.md) for what was verified against the
installed SDK, including several silent-failure modes worth knowing about.
## License
MIT — see [LICENSE](LICENSE).
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues