orca-slicer-mcp
# orca-slicer-mcp
An MCP server that drives **OrcaSlicer** headlessly on a private `Xvfb`
display, so an agent can import a model, slice it, and export G-code without a
physical screen.
It exists because OrcaSlicer has no scripting API for slicing profiles the way
its GUI does. Rather than click buttons by screen coordinate (fragile across
versions and resolutions), this server drives OrcaSlicer through its
**single-instance IPC channel**: a short-lived `orca-slicer <arg>` process
forwards its argument over DBus to the already-running GUI. Model paths load
via `Plater::load_files`; a small **source patch** adds an
`orca-cmd:export:<path>` command that slices the current plate and writes the
G-code straight to a file. No coordinate clicking, no file-chooser scripting.
## Requirements
System binaries on `PATH`:
- `orca-slicer` **2.4.2, patched** (see "OrcaSlicer patch" below)
- `Xvfb`
- `openbox`
- `xdotool`
- `scrot`
Python deps are declared in `pyproject.toml` (`mcp`, `Pillow`).
```sh
uv venv .venv
uv pip install --python .venv/bin/python -e .
```
## OrcaSlicer patch
The DBus-command tools require a patched OrcaSlicer: `patches/orca-mcp.patch`
adds one IPC command, `orca-cmd:export:<path>`, that slices the current plate
and exports the G-code to `<path>` with no file dialog. It touches four files
(`InstanceCheck.{hpp,cpp}`, `Plater.{hpp,cpp}`): a new
`EVT_EXPORT_GCODE_OTHER_INSTANCE` event, its parsing in
`OtherInstanceMessageHandler::handle_message`, and `Plater::export_gcode_to()`,
which reuses the existing slice+export path (`priv::export_gcode` with
`FORCE_EXPORT`).
Apply it in the AUR `PKGBUILD` (portable, survives OrcaSlicer updates): add
`orca-mcp.patch` to `source=()`/`sha256sums=()` and
```sh
prepare() {
cd "$srcdir/OrcaSlicer-${pkgver}"
patch -p1 < "$srcdir/orca-mcp.patch"
}
```
then rebuild with `makepkg`. The patch applies cleanly with `patch -p1` against
the v2.4.2 source tree.
Single-instance IPC is only active when `app.single_instance` is `true` in
`~/.config/OrcaSlicer/OrcaSlicer.conf`; `start_session` sets this automatically
before launching. (The `--single-instance` CLI flag is **not** used - it is not
a valid OrcaSlicer 2.4.x option and is rejected by `read_cli()` before the IPC
code runs.)
## Running
```sh
.venv/bin/orca-slicer-mcp # stdio MCP server
```
Register it with your MCP client, e.g. for Claude Code:
```sh
claude mcp add orca-slicer -- /home/USER/forge/orca-slicer-mcp/.venv/bin/orca-slicer-mcp
```
## Tools
| Tool | Purpose |
|------|---------|
| `start_session(display, width, height)` | Launch Xvfb + openbox + OrcaSlicer; enable single-instance IPC; dismiss first-run/crash dialogs. Call first. |
| `stop_session()` | Terminate OrcaSlicer, openbox and Xvfb. |
| `session_status()` | Report running state and current windows. |
| `screenshot()` | Return the current screen as a PNG image. |
| `import_model(path)` | Load STL/OBJ/3MF/STEP/AMF/SVG into the running instance via single-instance IPC (`Plater::load_files`). |
| `export_gcode(path)` | Slice the current plate and export G-code to `path` via `orca-cmd:export:` (one IPC command); returns a header summary. |
| `process_model(stl, gcode)` | Convenience: import + export. |
| `print_gcode(gcode, start=True)` | Ship a local G-code to the Creality printer and start it (see Printing). |
| `screenshot` / `click` / `type_text` / `press_key` / `zoom` | Low-level primitives for dialogs not yet covered by the high-level tools. |
## Printing (Creality K1 Max)
`print_gcode(gcode_path, start=True)` closes the loop: it takes a G-code file
this server just exported and gets it printing without you walking to the
machine.
The printer's own web UI *does* expose starting a print, but only as a
right-click/tap context-menu item per file — easy to miss and awkward on a
phone. This tool drives the exact same two web actions programmatically:
1. **upload** — `POST /upload/<name>` (multipart, field `file`), and
2. **start** — a WebSocket `set` on `/wsapi` with
`opGcodeFile: "printprt:<gcode_dir>/<name>"`.
It reaches the Creality web UI **through the docker container that serves it,
bypassing the public oauth2-proxy gate** — the local side only runs `scp` and
`ssh <host>`; the HTTP upload and WebSocket start execute on the docker host
(see `creality_print_remote.py`), the only machine on the printer's LAN.
Configuration (environment variables):
| Variable | Default | Meaning |
|----------|---------|---------|
| `CREALITY_SSH_HOST` | `t580` | SSH alias of the host running the Creality docker stack. |
| `CREALITY_PROXY_CONTAINER` | `creality-proxy` | Docker container serving the web UI; talked to directly, bypassing oauth2-proxy. |
| `CREALITY_GCODE_DIR` | `/usr/data/printer_data/gcodes` | Upload directory on the printer, used to build the `printprt` path. |
Requires on the docker host: `docker`, `curl`, and Python with
`websocket-client`. Pass `start=False` to stage a file without printing.
## Why each moving part exists (hard-won lessons)
These are baked into `session.py`; do not "simplify" them away:
- **A window manager is mandatory.** Without one there is no
`_NET_ACTIVE_WINDOW`; focus and window management silently misbehave.
`openbox` is launched with a bundled `openbox-rc.xml` that makes every window
**undecorated and maximized**.
- **Commands travel over single-instance IPC, not the CLI action API.** A
second `orca-slicer <arg>` process passes CLI validation (the arg is a plain
positional, not an option), reaches `instance_check`, and — because
`app.single_instance` is enabled — forwards the whole command line over DBus
to the running instance, then exits. `--single-instance` must **not** be
passed: it is not a valid 2.4.x option and `read_cli()` rejects it *before*
`instance_check` runs, so the sender would error out or spawn a second GUI.
- **`app.single_instance` must be `true` before launch.** It lives nested under
the `"app"` object in `OrcaSlicer.conf` (not at the top level); `start()`
sets it and OrcaSlicer preserves it across runs.
- **Export completion is detected by the G-code file on disk**, not by any GUI
signal: `export_gcode` polls until the file stops growing across several
samples (so a mid-write stall cannot yield a truncated read).
- **Dialogs block the main window.** The first-run SSL-certificate dialog, the
update-check dialog, and — after any unclean exit — the crash "Restore"
dialog all prevent the main window from being created. `start()` interleaves
dismissing them with waiting for the main window, in one loop.
- **The main window's title varies** ("Unnamed Window", "*Untitled", a project
name), so it is detected by size (>=90% of screen width), not by title.
- **Every `xdotool`/`scrot` call has a timeout.** A single blocked call (e.g.
if the X server dies) would otherwise freeze every polling loop.
- **Long-lived processes are owned by the server.** They are spawned with
`start_new_session=True` and held by the `OrcaSession` object; launching them
from an ephemeral shell that then exits would kill them.
## Testing
```sh
.venv/bin/python test_e2e.py
```
Runs the full import -> export against a sample STL on a fresh Xvfb and asserts
a non-trivial G-code is produced.
TDQS
Scored across 12 tools
Each tool has a distinct purpose: UI actions (click, press_key, type_text), file operations (export_gcode, import_model, process_model), session management (start/stop_session, session_status), printer control (print_gcode), view manipulation (zoom), and capture (screenshot). No overlapping functionality.
Most tools follow a verb_noun pattern (e.g., import_model, start_session). However, 'screenshot' is a noun, 'zoom' is a single verb, and 'session_status' uses noun_noun, introducing a few inconsistencies.
12 tools cover the core workflow (session lifecycle, model import, slicing, printing, UI interaction) without being excessive. Each tool justifies its existence for automating OrcaSlicer.
The set covers major workflows but lacks direct tools for model editing (e.g., scale, rotate) and printer status. However, UI tools like click and press_key can compensate, and the core slice-and-print path is complete.