Content
# substance-painter-mcp
Bridge Claude to Substance 3D Painter via the Model Context Protocol (MCP).
`substance-painter-mcp` lets Claude create projects, bake texture maps, apply smart materials, export PBR textures, and capture viewport screenshots -- all by talking to Substance Painter's built-in remote scripting API.
---
## Quick Start (Artists)
Get Claude talking to Substance Painter in under 5 minutes.
### Prerequisites
- **Substance 3D Painter** 10.x or later installed
- **Node.js** 18+ installed ([nodejs.org](https://nodejs.org))
- **pnpm** installed (`npm install -g pnpm`)
- **Claude Code** or **Claude Desktop**
### 1. Clone and Build
```bash
git clone https://github.com/user/substance-painter-mcp.git
cd substance-painter-mcp
pnpm install
pnpm build
```
### 2. Configure Claude
Add to your Claude Code MCP config (`.claude/mcp.json` or global `~/.claude.json`):
```json
{
"mcpServers": {
"substance-painter": {
"command": "node",
"args": ["/absolute/path/to/substance-painter-mcp/packages/mcp-server/dist/index.js"]
}
}
}
```
> **Windows example path:** `"E:/coding/substance-painter-mcp/packages/mcp-server/dist/index.js"` (use forward slashes)
For **Claude Desktop**, add the same block to your `claude_desktop_config.json`.
### 3. Launch Substance Painter with Remote Scripting
Substance Painter **must** be launched with the `--enable-remote-scripting` flag. Without this flag, the MCP server cannot communicate with SP.
**Windows:**
```
"C:\Program Files\Adobe\Adobe Substance 3D Painter\Adobe Substance 3D Painter.exe" --enable-remote-scripting
```
**macOS:**
```
/Applications/Adobe\ Substance\ 3D\ Painter.app/Contents/MacOS/Adobe\ Substance\ 3D\ Painter --enable-remote-scripting
```
> **Tip:** Create a desktop shortcut with the flag so you always launch SP with remote scripting enabled.
### 4. Verify Connection
Ask Claude:
> "Check if Substance Painter is running"
Claude will call `sp_health_check` and report whether SP is reachable, whether a project is open, and the SP version.
### 5. First Use
Try these prompts:
> "Create a new SP project from C:/models/my-model.fbx"
> "Bake all texture maps for the current project"
> "Export PBR textures to C:/exports/my-model"
> "Take a screenshot of the SP viewport"
---
## Available Tools
| Tool | Description | Key Parameters |
|------|-------------|----------------|
| **sp_health_check** | Check if SP is running and accessible | -- |
| **sp_create_project** | Create a project from an FBX/OBJ mesh | `mesh_file_path`, `normal_map_format?`, `texture_resolution?` |
| **sp_close_project** | Close the current project | `save?` |
| **sp_bake_maps** | Start baking texture maps (non-blocking) | `map_types?` |
| **sp_get_bake_status** | Poll bake progress (auto-screenshot on completion) | `job_id` |
| **sp_apply_smart_material** | Search and apply a smart material by name | `material_name`, `texture_set?` |
| **sp_export_textures** | Export PBR textures to disk | `export_path`, `preset?`, `file_format?`, `bit_depth?` |
| **sp_get_viewport_screenshot** | Capture the SP viewport as a PNG image | -- |
### Tool Categories
**Health:** `sp_health_check` -- Always run first to verify SP connectivity.
**Project:** `sp_create_project`, `sp_close_project` -- Project lifecycle with safety checks (won't create if already open, won't close if nothing is open).
**Baking:** `sp_bake_maps`, `sp_get_bake_status` -- Fire-and-poll pattern. `sp_bake_maps` returns a `job_id` immediately; Claude polls `sp_get_bake_status` until complete. Completion response includes a viewport screenshot.
**Materials:** `sp_apply_smart_material` -- Searches SP's shelf by name and applies the first match to the active texture set.
**Export:** `sp_export_textures` -- Exports PBR textures using configurable preset, format (PNG/EXR/TGA), and bit depth (8/16).
**Viewport:** `sp_get_viewport_screenshot` -- Captures the SP window as a PNG image (under 1MB for MCP transport).
---
## Developer Guide
### Architecture
```
Claude (LLM)
|
v
MCP Server (Node.js / TypeScript)
| stdio transport
v
SPClient.execute(pythonCode)
| HTTP POST (base64-encoded Python)
v
Substance Painter Remote Scripting (localhost:60041)
| Executes Python inside SP environment
v
substance_painter Python API
| print(json.dumps({...}))
v
JSON response back to MCP Server
|
v
Claude receives structured result
```
The MCP server is a thin bridge. It does **not** embed SP logic. Instead, it sends Python code snippets to SP's built-in remote scripting HTTP server on `localhost:60041`. SP executes the Python inside its own environment (with full access to the `substance_painter` API) and returns JSON results.
### Monorepo Structure
```
substance-painter-mcp/
packages/
mcp-server/ # Node.js MCP server (npm package)
src/
index.ts # Entry point, registers all tools
sp-client.ts # HTTP bridge to SP remote scripting
js-snippets.ts # JS/Python code templates for each tool
types.ts # Shared response types
tools/ # Tool handlers (one file per category)
health.ts
project.ts
baking.ts
export.ts
materials.ts
viewport.ts
__tests__/ # Unit tests (vitest)
sp-client.test.ts
tools.test.ts
mocks/
sp-server.ts # Mock SP HTTP server for testing
package.json
tsconfig.json
vitest.config.ts
sp-plugin/ # Python helpers for SP
sp_mcp_bridge/
__init__.py
helpers.py # make_response(), make_error()
install.py # Installer for SP plugin directory
README.md
scripts/
integration-test.py # Full round-trip test against running SP
claude-mcp-config.example.json
package.json # Root workspace config
pnpm-workspace.yaml
LICENSE # MIT
README.md # This file
```
### How to Add a New Tool
Tools use a hybrid JS+Python pattern: JS IIFE snippets for pre-flight checks and verification, Python snippets for actions that need the `substance_painter` API.
1. **Add snippets** in `packages/mcp-server/src/js-snippets.ts`:
```typescript
// Pre-flight check (JS IIFE — runs via executeJS)
myToolPreCheck(): string {
return `(function() {
try {
if (!alg.project.isOpen()) {
return { success: false, summary: "No project open", error: "no_project_open" };
}
return { success: true, summary: "Ready" };
} catch(e) {
return { success: false, summary: "Pre-check failed", error: e.message };
}
})()`;
},
// Action (Python — runs via executePython)
myToolPython(param: string): string {
const safePath = escapePath(param);
return `
import substance_painter.some_module
substance_painter.some_module.do_thing("${safePath}")
`.trim();
},
// Verification (JS IIFE — runs via executeJS)
verifyMyTool(): string {
return `(function() {
try {
return { success: true, summary: "Tool completed", data: {} };
} catch(e) {
return { success: false, summary: "Verification failed", error: e.message };
}
})()`;
},
```
2. **Create a tool handler** in `packages/mcp-server/src/tools/my-tool.ts`:
```typescript
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import type { SPClient } from "../sp-client.js";
import { makeToolResponse } from "../types.js";
import { jsSnippets } from "../js-snippets.js";
export function registerMyTools(server: McpServer, client: SPClient): void {
server.tool(
"sp_my_new_tool",
"Description of what this tool does",
{ param: z.string().describe("What this parameter controls") },
async ({ param }) => {
// 1. Pre-flight check (JS)
const preCheck = await client.executeJS(jsSnippets.myToolPreCheck());
if (!preCheck.success) return makeToolResponse(preCheck);
// 2. Action (Python)
await client.executePython(jsSnippets.myToolPython(param));
// 3. Verify (JS)
const result = await client.executeJS(jsSnippets.verifyMyTool());
return makeToolResponse(result);
},
);
}
```
3. **Register in index.ts**:
```typescript
import { registerMyTools } from "./tools/my-tool.js";
registerMyTools(server, client);
```
4. **Add tests** in `packages/mcp-server/src/__tests__/tools.test.ts`.
5. **Run tests**: `pnpm test`
### How SPClient Works
`SPClient` is the HTTP bridge between the MCP server and Substance Painter:
1. Takes raw Python code as a string
2. Base64-encodes it (SP requires this encoding)
3. POSTs to `http://localhost:60041/run.json` with `{"python": "<base64>"}`
4. SP executes the Python code in its environment
5. The Python code must end with `print(json.dumps({...}))` to return results
6. SPClient parses the JSON response and returns it
Error handling includes:
- `ECONNREFUSED` -- SP not running or `--enable-remote-scripting` not enabled
- HTTP errors -- unexpected status codes from SP
- Invalid JSON -- Python code didn't print valid JSON
- Timeout -- operation took longer than 30 seconds (bake uses polling instead)
### Python Snippet Pattern
Every tool sends a Python code snippet to SP. Snippets follow this pattern:
```python
import substance_painter.module
import json
try:
# 1. Safety check (is project open? etc.)
# 2. Perform operation using substance_painter API
# 3. Return result as JSON
print(json.dumps({
"success": True,
"summary": "Human-readable one-liner",
"data": { "key": "structured data" }
}))
except Exception as e:
print(json.dumps({
"success": False,
"summary": "What went wrong",
"error": str(e),
"hint": "Actionable suggestion"
}))
```
All responses use the standard shape: `{success, summary, data?, error?, hint?}`.
### Contributing
1. Fork the repository
2. Create a feature branch: `git checkout -b feat/my-new-tool`
3. Write tests first (TDD encouraged): `pnpm test`
4. Follow the tool-adding pattern described above
5. Ensure all tests pass: `pnpm test`
6. Submit a pull request
---
## Troubleshooting
### "Connection refused" or ECONNREFUSED
**Cause:** Substance Painter is not running, or it was launched without the `--enable-remote-scripting` flag.
**Fix:** Launch SP with the flag:
```
"Adobe Substance 3D Painter.exe" --enable-remote-scripting
```
Then run `sp_health_check` to verify connectivity.
### "Project already open"
**Cause:** `sp_create_project` was called while a project is already open in SP. SP can only have one project open at a time.
**Fix:** Close the current project first:
- Ask Claude: "Close the current SP project"
- Or manually: File > Close in SP
### "Bake takes a long time" or "Bake timeout"
**Cause:** Baking is a GPU-intensive operation that can take 30 seconds to several minutes depending on mesh complexity and map count.
**How it works:** `sp_bake_maps` returns immediately with a `job_id`. Claude automatically polls `sp_get_bake_status` to check progress. When baking completes, the response includes a viewport screenshot.
**Fix:** Just wait. If baking seems stuck, check SP's UI to see if the bake is progressing. Very complex meshes with many texture sets will take longer.
### "Export failed"
**Cause:** Export path does not exist, is not writable, or the preset name does not match an installed preset.
**Fix:**
- Ensure the export directory exists
- Check disk space
- Use a standard preset name like "PBR Metallic Roughness"
### Version Compatibility
| SP Version | Status | Notes |
|------------|--------|-------|
| 10.0 (2024) | Supported | Baseline API, PySide2 (Qt5) |
| 10.1+ (late 2024) | Supported | PySide6 (Qt6), Python 3.11 |
| 11.x (2025) | Supported | Latest API (0.3.4+), experimental auto-cage baking |
The MCP server handles PySide6/PySide2 differences automatically via conditional imports.
---
## License
MIT -- see [LICENSE](LICENSE).
MCP Config
Below is the configuration for this MCP Server. You can copy it directly to Cursor or other MCP clients.
mcp.json
Connection Info
You Might Also Like
No recommendations available