Content
# Substance Painter MCP
An MCP server that wraps the Adobe Substance 3D Painter remote-scripting REST API as 10 well-shaped tools. Lets an LLM agent drive Painter end-to-end: create projects, import textures, create fill layers with PBR channel wiring, bake mesh maps without the dialog popping, and export textures.
Single-file `FastMCP` stdio server. Only runtime dependency is `mcp`.
## Requirements
- Adobe Substance 3D Painter (tested against 10.x / 11.x — anything with `--enable-remote-scripting`)
- Python 3.10+
- `mcp` (FastMCP)
## Install
```bash
pip install -r requirements.txt
```
## Launch Substance Painter with remote scripting
```bat
"C:\Program Files\Adobe\Adobe Substance 3D Painter\Adobe Substance 3D Painter.exe" --enable-remote-scripting
```
A convenience launcher is included: `launch_sp_remote.bat`.
The remote API listens on `localhost:60041` (HTTP, base64-encoded payloads).
## Wire into your MCP client
Add to your `mcp.json` (or equivalent):
```json
{
"mcpServers": {
"substance-painter": {
"command": "python",
"args": ["C:/path/to/substance-painter-mcp/sp-mcp.py"]
}
}
}
```
## Tools
| Tool | Purpose |
|---|---|
| `check_connection` | Verify SP is up + remote scripting is enabled. Returns API version + project-open state. |
| `create_project` | New project from a mesh file. Configurable normal-map format, default resolution, camera import. |
| `open_project` | Open an existing `.spp`. |
| `save_project` | Save in place, or Save As to a new path. |
| `import_texture` | Import a texture file as a project resource (TEXTURE / ENVIRONMENT / ALPHA). |
| `get_project_info` | List texture sets + their root layer nodes. |
| `create_fill_layer` | Insert a fill layer and wire PBR channels via a JSON `{channel: search_term}` map. Resolves resources by name search; sets projection mode. |
| `configure_and_bake` | Configure high-poly mesh + bake parameters and run the bake **via the JS API so the bake dialog does not pop**. Polls `alg.project.isBusy()` to detect completion. |
| `export_textures` | Export project textures using a named preset (`PBR Metallic Roughness`, `Unreal Engine 4 Packed`, `Unity HD Render Pipeline Lit`, ...). |
| `execute_script` | Escape hatch — run arbitrary Python or JavaScript inside Painter. Python has full `substance_painter.*` access; JS has `alg.*`. |
## Highlights
### Headless baking (no dialog)
`configure_and_bake` uses the JavaScript baking API (`alg.baking.textureSetBakingParameters` + `alg.baking.bake`) to bypass the modal bake dialog entirely. It then polls `alg.project.isBusy()` every 3s up to a 10-minute timeout so the tool returns when the bake actually finishes.
```python
configure_and_bake(
hp_mesh_path=r"D:/meshes/hp_helmet.fbx",
output_size=2048,
max_frontal_distance=0.5,
bake_normal=True,
bake_ao=True,
bake_curvature=True,
)
```
### Channel-mapped fill layers
`create_fill_layer` takes a JSON string mapping PBR channels to resource search terms. The tool searches for resources by name, then wires each match into the right channel on a new fill layer.
```python
create_fill_layer(
layer_name="Brushed Steel",
textures='{"BaseColor": "steel_albedo", "Roughness": "steel_rough", "Metallic": "steel_metal", "Normal": "steel_norm"}',
projection_mode="Triplanar",
)
```
Valid channels: `BaseColor`, `Roughness`, `Metallic`, `Normal`, `Height`, `AmbientOcclusion`, `Emissive`.
Projection modes: `UV`, `Planar`, `Triplanar`.
### Unified Python / JS execution
`execute_script` accepts both languages — useful when something isn't exposed in the Python API but is reachable via `alg.*` JS (or vice versa).
```python
execute_script(
script="alg.project.lastImportedMeshUrl()",
language="javascript",
)
```
## API notes
- The remote API base64-encodes script payloads. `lib_remote.py` handles that — you don't see it from the MCP surface.
- Python `print()` from inside Painter returns `null` to the caller. To return data, raise an exception with the JSON: `raise Exception(json.dumps(result))`. The tool unwraps it.
- Use forward slashes or raw strings in mesh / texture paths on Windows.
- `set_source(ChannelType, ResourceID)` — channel first. Easy to flip.
- `InsertPosition.from_textureset_stack(stack)` for top-of-stack. There is no `InsertPosition.on_top()`.
## Files
```
sp-mcp.py FastMCP server, 10 tools
lib_remote.py RemotePainter HTTP client (Python + JS exec)
test_connection.py Standalone connection smoke test
launch_sp_remote.bat One-click launch with --enable-remote-scripting
```
## Troubleshooting
| Symptom | Fix |
|---|---|
| `Cannot connect to Substance Painter` | Launch SP with `--enable-remote-scripting`. Wait for it to fully load. |
| `null` responses from Python scripts | Switch to the exception-return trick (see API notes). |
| `Layer Operations Fail` | Confirm a project is open. Call `texture_set.get_stack()`, pass the Stack (not the TextureSet) to `get_root_layer_nodes()`. |
| Bake fires the dialog | You're calling Painter's Python bake API. Use `configure_and_bake` (this tool uses the JS API). |
## License
MIT — see `LICENSE`.