qidi-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., "@qidi-mcp what's my QIDI printer doing right now, and what are the temps?"
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.
qidi-mcp — MCP server for QIDI Max4 (Klipper / Moonraker)
An MCP server that lets AI agents observe and operate a QIDI Max4 3D printer through Moonraker's API, with a safety layer designed for the fact that the thing on the other end is a 400 °C hotend on a 800 mm/s gantry in a heated chamber.
Created at Exowatt.
Status: qualified end to end against one physical QIDI Max 4 running the firmware documented below — read-only discovery, a full confirmation round trip, and a complete 27-minute PLA print through the MCP tools. It has not been qualified against any other printer, any other firmware version, or a machine fitted with a QIDI Box filament switcher (those tools are built from QIDI's published macros and a mock only — see the Box guide). Read docs/guides/going-live.md before pointing this at hardware it has not been run against.
"QIDI" and "QIDI Max 4" are trademarks of their respective owner, Qidi Technology. This project is independent, community-maintained tooling and is not affiliated with, endorsed by, or sponsored by Qidi Technology, nor by the Klipper or Moonraker projects.
Try it with no hardware
The whole stack — the MCP tools, the safety gates, the dashboard — runs against a bundled fake Moonraker, so you can see what an agent sees before any printer is involved:
uv sync --extra dev
# terminal 1: a fake printer on loopback, mid-print by default
uv run python scripts/run_mock_printer.py
# terminal 2: point the server at it and talk to it like an agent would
QIDI_PRINTER_URL=http://127.0.0.1:7125 QIDI_TEST_MODE=1 \
uv run python scripts/validate.pyscripts/validate.py drives real tool calls end to end (read-only mode,
confirmation tickets, the safety envelope, capability discovery) and prints what
happened. The same mock backs the automated test suite — see
Tests — and qidi-dashboard (below) can watch it live.
Related MCP server: prusactl
Install
uv sync --extra dev(Drop --extra dev for a plain runtime install once you are past trying it out —
dev additionally pulls in pytest, aiohttp for the mock, and ruff.)
Configure in your MCP client
Add a qidi entry to your MCP client's server list. Rather than retyping the
paths by hand, have them resolved for your checkout:
.venv/bin/python scripts/mcp_entry.py # real printer, read-only
.venv/bin/python scripts/mcp_entry.py --mock # the bundled mock, no hardwaremcp-entry.json is the same block as a template with placeholder paths. This is
a generic MCP stdio server, so it works the same way in Claude Code, Claude
Desktop, or any other MCP client — only the surrounding config file's shape
differs:
{
"mcpServers": {
"qidi": {
"type": "stdio",
"command": "/absolute/path/to/qidi-max4-mcp/.venv/bin/python",
"args": ["-m", "qidi_mcp.server"],
"env": {
"QIDI_PRINTER_URL": "http://192.0.2.10:7125",
"QIDI_READ_ONLY": "1",
"QIDI_AUDIT_LOG": "/absolute/path/to/qidi-max4-mcp/audit.jsonl"
}
}
}
}Environment variables
Variable | Default | Purpose |
| (required) | Printer address; |
| — | Only needed if Moonraker auth is enabled |
|
|
|
|
|
|
|
|
|
|
| Operator switch: |
|
| Hotend ceiling (hard-capped at the firmware's 375) |
|
| Bed ceiling |
|
| Chamber ceiling |
|
| Above this, extra confirmation |
|
| Per-call jog cap, mm |
|
| Per-call extrude cap, mm |
|
| Z floor for jogs |
|
| Ceiling for |
|
| Ceiling for |
|
| Ceiling for |
|
| crowsnest/nginx snapshot base, derived from the printer host |
| — | JSONL audit path |
|
| Refuse mutating calls while another agent holds the printer lease |
|
| Refuses any non-loopback host (tests and the mock quickstart) |
The full list, including identity/role options for anyone fronting this with their own auth proxy, is in docs/reference/configuration.md.
Why a safety layer at all
Moonraker's API is unauthenticated on the LAN by default (QIDI's stock
moonraker.conf trusts every RFC-1918 subnet) and it will happily accept
M104 S370, SAVE_CONFIG, or a file delete from anything that can reach port
7125. An LLM with that endpoint and no guardrails is one hallucinated tool call
away from a scrapped print, a crashed toolhead, or an overwritten config.
This server therefore treats printer control as a privileged operation. Every
check below is enforced in qidi_mcp/limits.py and qidi_mcp/policy.py, and has
a regression test in tests/test_safety.py:
Tier | Meaning | Gate |
| pure query | none |
| reversible (lights, beeper, fan, messages) | none |
| moves an axis or extruder |
|
| changes a heater target |
|
| starts/stops/alters a job |
|
| deletes files, writes config, restarts, reboots, calibrates | two-step token |
| E-stop | never gated |
Layered on top of the tiers:
Numeric envelope. Temperatures, coordinates and runtime velocity/accel limits are range-checked before a byte leaves the process. The extruder default (300 °C) is below both the hardware's own
[extruder] max_tempceiling (375 °C) and QIDI's ownheater_temp_limitmacro, an independent clamp that caps extruder 370 °C / bed 120 °C / chamber 65 °C; the bed and chamber defaults match that macro's clamp exactly. All three are intersected at runtime with the limits the printer itself reports, so the stricter of the two wins, and an override can only lower a ceiling, never raise one past the hardware maximum.M201/M203/M204/M205are held to theprinter.cfgvalues (800 mm/s, 30000 mm/s², square corner 8 mm/s).Interlocks read live state: no jogging while a job is printing, no extrusion below
min_extrude_temp, no motion on unhomed axes, nothing at all while Klipper is shut down.G-code screening.
run_gcodeandrun_macroshare one screen. Scripts are split on\nonly, as Klipper splits them, and any other control character or Unicode line separator is refused (a tab, and a\rending a line, are allowed). Every extended-command parameter name must be letters, digits and underscores, unpadded — Klipper keeps a quoted key like" HOTEND"verbatim, and QIDI's print-start macros re-render their parameters, so that used to be read back asHOTEND; such a line (orrun_macrocall) is refused outright instead. A name given more than once is accepted byrun_gcodeand checked at its last value, the one Klipper uses;run_macrorefuses a repeated name. On a letter-and-number command (G27,M600,M104, …) the value itself is read exactly as Klipper keeps it, separators included: it may not contain=, a quote or a space, and an empty value on a checked parameter is refused —G27 P1 Z=20keepsZ="=20"; Klipper's own handlers reject that, butG27/M600forward it toPARK, whose\|floatreads it as 0, so the toolhead went to Z=0 while the screen had approved Z=20 (M117/M118are exempt — their text is a message). Write the value straight after the letter (G27 P1 Z20, notZ=20);run_macrodoes the same (M600 Z20). Heater targets (including QIDI'sSET_HEATER_TEMPERATURE_SCALED, the decimal spellings of built-ins QIDI renames such asM104.6245197, and the stock macro parameters that reach a heater —PRINT_START/PRINT_START_SET,CLEAR_NOZZLE,M600 R,EXTRUSION_AND_FLUSH,M4029 S,M603/M604 S), kinematic limits, absolute coordinates (G0–G3, tracked perG90/G91line so the envelope — including the Z floor — holds until the nextG90;PARK/G27/M600/PAUSEalways, since they switch toG90themselves) and G-code offsets (SET_GCODE_OFFSET/M290, capped at ±5 mm) are range-checked. The Box's firmware commands (E_LOAD,MULTI_COLOR_*,EXTRUDER_LOAD, …) have unpublished parameters, so any whose name could hold a temperature is held to the extruder envelope on the load/unload family and refused outright on the rest. These value checks run before the deny-list, so a deny-listed macro's planted temperature or coordinate (PRINT_START_SET,_PRINT_START_PHASE_INIT,M4029,M603/M604,PAUSE/_PARK_INNER) stays bounded even with the operator switch on; a handful of commands that would defeat the value checks themselves (SET_GCODE_VARIABLE, the heater-scaling macros, the deferred-command macros, the wait-wrapper internals) are inALWAYS_DENIED_GCODE_COMMANDS, which the operator switch can never unlock.SET_FILAMENT_SENSOR ... ENABLE=0(the primitive behindDISABLE_ALL_SENSOR) is denied;ENABLE=1is fine. Raw print starts (SDCARD_PRINT_FILE,M23,M24,M32) are denied in favour ofstart_print/resume_print, which claim the lease and bind the checked file. A deny-list refusesSAVE_CONFIG,FORCE_MOVE,SET_KINEMATIC_POSITION, TMC current writes,PID_CALIBRATE, everyrename_existingtarget (including its decimal alias), the stock vendor macros that reach those primitives, and friends. Overriding the deny-list needs the operator'sQIDI_ALLOW_DANGEROUS_GCODE=1and the agent'sallow_dangerous=trueand token;run_macronever overrides it. Macros are screened by name and parameters, not expanded, so a user-defined macro is only as safe as its name and listed parameters — see known limitations.Read-only mode (
QIDI_READ_ONLY=1) refuses ordinary mutating tools (emergency stop is exempt) — the safest way to let an agent watch a print it must not touch.A printer lease so two agents cannot drive one machine. Each agent runs its own server process, so the claim is stored on the printer (a namespace in Moonraker's database) — the only state they all share. Holding it refuses another agent's mutating calls by name, and
start_printrequires claiming the machine first. Cooperative, not a lock: the touchscreen and Fluidd ignore it, and prints started there do not take the lease in this release (the during-print interlock still refuses motion/thermal changes while any job runs, from any agent, regardless of who holds the lease). See sharing the printer.Audit log: every non-read action appended as JSONL with tier, args and outcome.
A tier is not always fixed. Ten tools choose one per call, so the count above is
the set that always needs a token, not the set that ever can. set_temperature
is ungated at 0 (turning a heater off is safety-positive), ordinary at a normal
target, and escalates to a token past the confirm threshold; run_gcode spans
three tiers by what the line does; upload_gcode, move_file and copy_file
escalate when they would overwrite something.
Tools
101 tools in eleven modules (verified by querying a mock-backed server:
38 read-only, 24 carrying the destructive annotation, none unannotated). R =
read query, W = mutation whose confirmation depends on its tier, D = two-step
confirm_token. Low-tier actions need no confirmation by default; strict mode
escalates the guarded tiers.
Of those 24, 22 always require a token, and the two exceptions are both
deliberate. emergency_stop is annotated destructive because it truly is — it kills
the print and shuts Klipper down — but it sits in its own never-gated tier, exempt
from confirmation, read-only mode and interlocks alike; a safety mechanism you have
to argue with is not one. abort_macro is the gentle stop for a runaway macro, so it
takes a single confirm=true rather than a round trip. These counts are asserted by
tests/test_docs.py and by the safety-invariants CI job, because they have
drifted before.
Discovery & status (13, all R) — get_printer_status, get_capabilities,
get_temperatures, get_position, get_job_status, get_printer_config,
query_objects, list_gcode_commands, get_gcode_log,
get_temperature_history, get_system_info, get_audit_log,
get_safety_policy
Thermal (3) — set_temperature W, wait_for_temperature R, cooldown W
Motion (8, all W) — home_axes, move_absolute, move_relative,
set_z_offset, extrude_filament, set_speed_factor, set_flow_factor,
motors_off
Print jobs (11) — start_print D, pause_print W, resume_print W,
cancel_print D, get_print_history R, get_history_totals R,
get_job_queue R, check_print_fit R, queue_jobs W, remove_queued_jobs D,
control_job_queue W
Files (11) — list_gcode_files R, get_file_metadata R,
list_file_roots R, list_directory R, download_file R, upload_gcode W,
move_file W, copy_file W, create_directory W, delete_file D,
delete_directory D
Devices, fans & lights (10) — list_fans R, list_devices R,
query_filament_sensor R, set_fan_speed W, set_case_light W,
set_output_pin W, set_led W, beep W, display_message W,
set_filament_sensor W
Calibration & probing (10) — get_bed_mesh R, query_probe R,
query_endstops R, probe_accuracy W, screws_tilt_calculate W,
z_tilt_adjust W, set_input_shaper W, bed_mesh_calibrate D,
bed_mesh_profile D, input_shaper_calibrate D
Camera (2, both R) — get_webcam_snapshot (returns an actual image for a
vision model to inspect), list_webcams
Sharing the printer (5) — get_printer_lease R, acquire_printer_lease W,
renew_printer_lease W, release_printer_lease W, break_printer_lease D
(force-releasing someone else's claim is audited with your name)
System & escape hatches (12) — get_klipper_log R, abort_heating W,
abort_macro W, emergency_stop emergency (never gated), restart_firmware D,
restart_klipper_host D, restart_service D, reboot_printer D,
shutdown_printer D, run_gcode D (line-screened), run_macro D,
resume_after_power_loss D
Three of these exist only because QIDI's Moonraker fork adds them, verified
against QIDI_MAX4's own klippy_apis.py. abort_heating (breakheater) and
abort_macro (breakmacro) are soft aborts: they escape a stuck heater wait
or a runaway macro while leaving the printer alive, where emergency_stop
shuts Klipper down and needs a firmware restart to come back. An agent should
reach for these first. get_capabilities reports features.soft_abort so it
knows whether they are available; on stock Klipper they degrade to
"not supported" rather than pretending to work.
QIDI-specific (Max4) (16) — chamber_heater W, set_polar_cooler W,
clean_nozzle W, get_box_status R, list_box_filament_options R,
set_box_slot_filaments W, load_box_slot D, set_box_option W,
get_saved_variables R,
get_filament_presets R, get_ai_detection_status R, set_ai_detection W,
load_filament D (staged heat/push/purge),
unload_filament D, qidi_bed_level_calibrate D (M4029), save_z_offset D
The QIDI Box (four-spool filament switcher) tools — get_box_status,
load_box_slot, set_box_option, and cooldown(also_box_heater=true) — were built
from QIDI's published macros and a mock. On the one physical qualification run (see
Status above), the read-only tools were run against a real Max4 with a Box and
worked (status, slot names, filament table, queue previews); nothing has been
written to or moved on a real Box. That run also found that this firmware has none
of E_LOAD, E_UNLOAD or DISABLE_BOX_HEATER — it has MULTI_COLOR_* commands with
unpublished parameters — so load_box_slot refuses on it, and the Box was present but
disabled (enable_box = 0). The tools discover the Box at runtime (it must exist as
both box_count > 0 and a live box_extras object, as QIDI's own macros require) and
say which half is missing. Read
The QIDI Box: first contact before writing to a
real machine.
Saying what is in each slot. get_box_status returns one readable line per slot
(slot 0 (unit 1, position 1): PLA Matte, black (QIDI)). To change them, call
set_box_slot_filaments with plain names:
assignments=[{"slot": 0, "filament": "PETG Basic", "color": "dark blue"}]
(vendor is optional; only the fields you name change). Names are resolved against
the printer's own officiall_filas_list.cfg, so the ids written are ones the firmware
already knows; list_box_filament_options searches that table. An ambiguous name
(ABS) is an error that lists the candidates, a bare material (PETG) resolves to
its "Basic" profile and says so, and a colour snaps to the nearest of the printer's 24
swatches and says so. The first call previews exactly what it would write; repeat it
with confirm=true to apply, and it reads the result back. It is refused during a
print and cannot clear a slot.
Queueing prints. queue_jobs accepts loose names (["benchy", "bracket abs"]),
fails the whole call on an unknown or ambiguous one, and its preview shows what each
resolved to, the print time, and whether the Box's filament suits the file.
check_print_fit does that comparison on its own, and start_print includes it in
its confirmation ticket. The comparison is advisory — which slot the printer feeds for
which slicer tool is not published, so it checks materials and colours and never
blocks. get_job_queue lists the queue in plain lines with total time, and reports
whether the queue advances by itself (automatic_transition).
AI failure detection (noodle = spaghetti, foreign = debris on the plate)
ships disabled on a stock Max4. Enabling a detector is free; disabling one
removes a safety net and needs confirm=true. Klipper does not run the vision
model — QIDI's camera service does and pushes verdicts in via
SET_AI_DETECT_RESULT, so a count of 0 from a disabled detector means "nothing
ever reported", not "the print is fine".
Live dashboard
Everything else here is for an agent to call. This is for you to watch:
QIDI_PRINTER_URL=http://127.0.0.1:7125 uv run qidi-dashboard
# or against the bundled mock from "Try it with no hardware" above, same URLQIDI_PRINTER_URL is required — the dashboard refuses to start without it
(same validation as the server). It opens a browser on 127.0.0.1:8770 and polls every 2s. Two things worth
knowing:
It cannot move the printer. The dashboard builds the server with
read_onlyforced on regardless of your environment, so it is the same code path an agent uses with the write half bolted shut. Launching it withQIDI_READ_ONLY=0already exported does not change that.It shows the envelope, not just the readings. Each heater meter draws the temperature, its target, and the ceiling this server enforces — e.g. the nozzle track is captioned "firmware allows 375 °C; this server stops at 300 °C, and asks above 260 °C". The gap is the point of the project, so it is on screen rather than buried in a config file.
It also reports job progress and layer, motion and Z-offset, filament sensor
state, and whether the AI failure detectors are armed. Follows light/dark
automatically; ?theme=light or ?theme=dark pins it for a screenshot or a
shared link. QIDI_DASHBOARD_HOST / QIDI_DASHBOARD_PORT override the bind
(loopback by default on purpose — this exposes printer state), and
QIDI_DASHBOARD_NO_BROWSER=1 stops it opening a tab.

Two routes, both GET:
Route | Returns |
| The page. Single file, no external requests, no web fonts. |
| Aggregated JSON — the same shape the page consumes, so |
The screenshots above are generated from the bundled mock, not captured by hand against a real machine, and the same script asserts the layout does not scroll sideways at three viewports:
.venv/bin/python scripts/screenshots.py --checkTests
The suite runs entirely against an in-process mock Moonraker (aiohttp) that
mimics the Max4's object model, and a conftest guard that blocks any socket
to a non-loopback address, so a test run can never touch a real printer.
uv run pytest -q # 777 tests (901 with the optional upstream sources fetched), a couple of minutes on an idle machineCoverage is deliberately weighted toward refusals rather than happy paths:
every temperature ceiling, every interlock, token replay and cross-tool token
reuse, G-code screen bypass attempts, path traversal, read-only mode, and the
stdio transport itself. The mock's object model, macro names and limits are
transcribed from QIDI's published printer.cfg, so a passing test exercises
the shapes the real machine returns. scripts/run_mock_printer.py (above) is
the same mock, exposed as a standalone process rather than a test fixture.
Going live
Read-only discovery has been qualified against the physical Max 4; no write has been commanded beyond the single qualification print described under Status. When you're ready:
Start read-only:
QIDI_READ_ONLY=1, thenget_capabilitiesandget_printer_statusto confirm discovery matches your firmware.Drop read-only, try
low-tier actions (set_case_light,beep).Then thermal/motion with the printer watched, then a real print.
emergency_stop works in every mode, including read-only. See
docs/guides/going-live.md for the full walk-through.
What adversarial review found
The safety layer was audited by independent agents told to break it and to prove every claim by running code. Twelve real bugs came out of it, each fixed with a regression test that fails if the fix is reverted. Every exploit was re-run after the fix to confirm it is actually closed, rather than trusting the diff.
The full findings, the evidence, and the fix for each live in
docs/security/ — including the proof scripts,
which can be re-run. The most serious nine:
Severity | Bug |
HIGH |
|
HIGH |
|
HIGH | Three guards were written |
HIGH | The deny-list only knew upstream's names, missing QIDI's |
HIGH | Interlocks were judged on a 0.75 s cache, so a refused call's retry was authorised against pre-refusal state. |
HIGH | Any HTTP 503 was read as "nothing is printing" — but Moonraker returns the same 503 while a print continues. |
HIGH | A failed capability discovery was cached 300 s as "no chamber heater", so |
HIGH | A |
HIGH | A negative |
A later review, during the open-source export, reproduced five more bypasses with
the token redeemed — run_macro never range-checked its parameters, QIDI's
SET_HEATER_TEMPERATURE_SCALED and M201/M203/M205 were unchecked,
allow_dangerous needed no operator, and fifteen stock vendor macros reached
restricted primitives undenied. All are closed and regression-tested; see
the security review.
A second review of that same fix found five more ways past it, all closed and
regression-tested in tests/test_envelope_bypasses.py (sections 7–9): the raw
built-ins QIDI's wrapper macros hide behind rename_existing (M104.6245197 S374) were reachable unchecked; a line separator other than \n (\r,
\x0b, \u2028, …) hid a second value on what the screen saw as two harmless
lines; several stock macro parameters — not just names — carried a temperature
into a heater (M600 R374, EXTRUSION_AND_FLUSH HOTEND=374); M109/M190
forwarded every parameter into an internal wrapper, so M109 S200 R1 M140 set
the bed to 200 °C through a parameter named M; and several deferred or
unbounded commands (GCODE_ON_PRINT_STATUS, M0/M25/M601, M1002, M900,
the closed-loop stepper-current commands, and an unbounded SET_GCODE_OFFSET/
M290) were not covered. L5 section (e) now traces every parameter of every
stock macro to the primitive it reaches (78 flows, 0 uncovered); section (f)
checks every rename_existing target is denied. See
the second review
for the full table and fixes.
A third review, with QIDI_ALLOW_DANGEROUS_GCODE both unset and set, found six
more ways past the same gates, all closed and regression-tested in
tests/test_envelope_bypasses.py (section 10): padded or quoted parameter
names (PRINT_START HOTEND=200 " HOTEND"=374 BED=60) that Klipper keeps
verbatim and a forwarding macro re-renders as the plain name, reaching
M109 S374; E_LOAD SLOT=0 S=374 and the Box's other firmware commands
reaching the wire through unpublished parameters; PRINT_START_SET,
_PRINT_START_PHASE_INIT, M4029 S, M603/M604 S and PAUSE/_PARK_INNER
X/Y/Z staying open to a planted value with the operator switch on, because
only the deny-list (which the switch reopens) covered them; SET_FILAMENT_SENSOR ... ENABLE=0 reaching the sensor it disables; raw print starts
(SDCARD_PRINT_FILE, M23, M24, M32) skipping start_print's lease and
file binding; and the Z floor lapsing for every line after a G91 anywhere in a
script, rather than only until the next G90. The fixes: parameter names are
now validated against [A-Z0-9_]+; value checks run before the deny-list,
and the handful of commands that would defeat them outright
(SET_GCODE_VARIABLE, the heater-scaling macros, …) are in
ALWAYS_DENIED_GCODE_COMMANDS, which the operator switch can never unlock; and
G90/G91 are tracked line by line. See
the third review
for the full table.
One reviewer drove Klipper's real GCodeDispatch as an oracle across 154,371
generated command spellings and found zero divergences in the resolved command
name, which is the kind of evidence worth having on a safety boundary. That
oracle was Klipper's klippy/gcode.py (GPLv3). It is not bundled in this
Apache-2.0 export. To rerun the standalone differential proofs, first run
bash scripts/fetch_upstream_sources.sh, which retrieves a pinned v0.13.0 parser
into Git-ignored research/upstream/; see the provenance inventory.
The internal record of this result did not pin the precise parser revision used
for the historical 154,371-case run. Rerunning L4_fuzz.py against the pinned
v0.13.0 fetch checked 320,424 single-line inputs (current deny-list, up from
271,344 before the safety-envelope-bypass fixes) with zero name divergences
and zero safety bypasses. The installed package never imports the upstream parser.
The recurring theme is worth stating: each bug was a place where the code guessed instead of knowing. It guessed how Klipper parses a line, guessed that cached state was still true, guessed what a 503 meant, guessed that undiscovered hardware was absent, guessed that a filename identified a file. The fixes all replace a guess with either a real query or an honest "unknown" that fails safe.
Two rules came out of this and are worth keeping:
A screen that parses its input differently from the executor is not a screen. All G-code classification goes through
klipper_command_name().A guarantee that holds only on the paths you remembered to guard is not a guarantee. The raw escape hatch (
run_gcode,run_macro) now applies the same temperature, velocity/accel and axis envelope as the dedicated tools to every heater, limit and motion command it can parse, to the decimal spellings of renamed built-ins, and to the stock QIDI macro parameters that reach one — value-checked before the deny-list, so the operator switch cannot reopen a planted value. It does not coverG0–G3coordinates after aG91(until the nextG90; a script is assumed to start in absolute mode, so a previous script'sG91makes the check stricter, not looser), Box parameters whose names do not look like a temperature, arc centres,G92, print files (the envelope governs this server's raw path, not what a slicer wrote), or macros the stock config does not define; see the residual limits.
Historical firmware research (not a current compatibility survey)
This was the original question, so it is worth stating plainly. The research
checkout under research/ was collected in September 2026 and found no
open-source OS/firmware replacement supporting the Max4. Firmware projects
move, so re-check current releases before acting on the table:
Project | Targets | Max4? |
X-Max3, X-Plus3, X-Smart3, Q1 Pro, Plus4 | No | |
FreeQIDI | X-3 series | No |
OpenQ1 | Q1 Pro only | No |
Max4 | Macros/config only — not firmware | |
Kalico ports | Plus4, Q2 | No verified Max4 port |
The one Max4-specific community project is a Klipper macro and slicer-profile
overlay installed over SSH; it does not replace the OS, Klipper, Moonraker, or
MCU firmware. So this server targets stock firmware — which is the right
call anyway, since it is what the printer runs. The platform facts below come
from QIDI's published config at commit 47fc14c
(https://github.com/QIDITECH/QIDI_MAX4/tree/47fc14cefe064dbd30b90582429fe650883ae900),
not from querying your machine.
Stock platform, for reference: Debian 11 Bullseye, kernel 5.10.160, aarch64,
4× ARM Cortex-A35 on a Rockchip SoC, separate mainboard and toolhead MCUs (the
latter on /dev/ttyS4 at 500000 baud). Moonraker reports API version 1.4.0 and
its Klipper is closest to upstream v0.13.0 with QIDI patches — the exact strings
the bundled mock returns are MOONRAKER_VERSION = "v0.8.0-266-gd5b0e7f" and
KLIPPER_VERSION = "v0.13.0-qidi-284-g5d1b0f3f" (tests/mock_moonraker.py);
the physical qualification run matched these.
A security note about your printer
Stock Moonraker binds 0.0.0.0:7125 and lists the private LAN ranges as
trusted_clients, with no force_logins. Anything on your LAN can control
the printer with no authentication — no API key needed, which is why this
server does not require one. That is a property of the printer, not of this
server, but it is worth knowing: the safety envelope here constrains this
client, and does nothing about any other. QIDI's algo_app also exposes an AI
detection API on port 9010. crowsnest.conf advertises the camera on 8080, but
on the reference Max4 that port is closed — nginx proxies the stream at
/webcam/ on :80 instead, which is what QIDI_WEBCAM_URL defaults to
(see qidi_mcp/config.py).
Hardware reference (Max4)
Transcribed from QIDITECH/QIDI_MAX4
config/printer.cfg: CoreXY, 385×405×342 mm build volume (position_max
392/410/342), extruder max_temp 375 °C with min_extrude_temp 135,
bed max_temp 125, [heater_generic chamber] max_temp 68, 9×9 bed mesh over
15–375 mm, dual Z with z_tilt, closed-loop X/Y, QIDI's probe_air inductive
probe, filament switch sensor, and a multi_color_controller (declared on every
Max4, with or without a Box; a Box additionally needs box_extras and box_count)
for up to four Box units. The firmware's own heater_temp_limit macro caps extruder/bed/chamber at
370/120/65 °C.
Moonraker on stock firmware: port 7125, trusted_clients covering all private
subnets, [octoprint_compat], [history], [timelapse] enabled and
[update_manager] absent; camera via crowsnest on 8080.
Prerequisites
No proprietary SDK is required or bundled. You need:
A QIDI Max 4 (or the bundled mock — see "Try it with no hardware" above) on its stock firmware, reachable over HTTP from wherever this server runs.
Python ≥3.11 and uv.
Optionally, Google Chrome or Chromium to regenerate the dashboard screenshots (
scripts/screenshots.py).
License
Original code is licensed under Apache-2.0. No third-party GPLv3 firmware/configuration, docs or parser is included. NOTICE and the research provenance index explain how to fetch the pinned source material separately for local verification.
Contributing
See CONTRIBUTING. Contributions require DCO sign-off (git commit -s); no CLA is required.
Security
Please report vulnerabilities privately using GitHub private vulnerability reporting. See the security policy; do not open a public issue.
Available Tools
101 toolsabort_heatingAIdempotent
Abort a heater WAIT that is blocking the queue (QIDI's breakheater). If the printer is stuck in M109/M190/M191 waiting to reach a temperature it will never reach, this releases the wait without an emergency stop — so no firmware restart is needed afterwards. Heaters keep their targets; use cooldown to actually turn them off. QIDI-specific: not available on stock Klipper.
Inputs: no named parameters. Returns: object
abort_macroADestructive
Abort a running macro (QIDI's breakmacro). Use it when a macro is stuck or is doing something you want stopped — a levelling routine, a filament change, a purge — without the emergency stop's shutdown and firmware restart. The macro stops wherever it is, so the printer may be left mid-sequence (parked oddly, heaters on, filament part-loaded): check get_printer_status afterwards. QIDI-specific: not available on stock Klipper.
Inputs: confirm. Returns: object
acquire_printer_leaseA
Claim this printer for yourself, so other agents are refused mutating actions while you work. Give a short human reason ('bracket v3, ~40 min') — the point is that whoever gets blocked can see who to talk to. Refused if somebody else holds it. Expires on its own so a forgotten lease cannot strand the machine; renew it with renew_printer_lease for long jobs. This is cooperative: it does not stop the printer's own touchscreen or Fluidd.
Inputs: reason, confirm, minutes. Returns: object
bed_mesh_calibrateADestructive
Probe the bed and build a new mesh. Takes several minutes on the Max4's 9x9 grid and moves the toolhead across the whole bed; the bed should be at printing temperature for a meaningful result. The mesh is active immediately but NOT saved to disk — use bed_mesh_profile to save it. Two-step confirmation.
Inputs: confirm, profile, adaptive, confirm_token. Returns: object
bed_mesh_profileADestructive
Manage bed mesh profiles: action='load' to apply a saved profile, 'save' to persist the current mesh (this writes printer.cfg and RESTARTS Klipper), 'remove' to delete one, or 'clear' to drop the active mesh. Save/remove are two-step because they rewrite the config.
Inputs: action (required), name, confirm, confirm_token. Returns: object
beepAIdempotent
Sound the printer's beeper. duration_ms is capped at 5000. Useful to get a human's attention at the machine.
Inputs: repeat, duration_ms. Returns: object
break_printer_leaseADestructive
Force-release SOMEBODY ELSE's lease, for when its holder has clearly gone away and the machine is stranded. Two-step confirmation, and the break is recorded in the audit log with your name. Check get_printer_lease and look at the machine first: if a print is actually running, breaking the lease does not stop it, and cancel_print or emergency_stop is what you want. On a manual-print claim (a print started at the touchscreen, shown with manual=true) this takes the print over: the claim becomes yours so you can then pause or cancel it.
Inputs: why, confirm_token. Returns: object
cancel_printADestructive
Cancel the running or paused print. This ruins the part — the job cannot be resumed afterwards. Two-step confirmation, and it reports how far along the print was so you can see what you are throwing away.
Inputs: confirm, confirm_token. Returns: object
chamber_heaterA
Control the Max4's actively heated chamber: set a target in °C (max 65 by policy), or 0 to turn it off. The chamber is what makes ABS/ASA/PA printing work on this machine. Also reports the chamber circulation fan and the independent thermal-protection sensor. Requires confirm=true to heat.
Inputs: target, confirm, confirm_token. Returns: object
check_print_fitARead-onlyIdempotent
Before printing or queueing: does what the QIDI Box records in its slots suit these files? Compares each file's filament type and colours (from its slicer metadata) with the material and colour recorded per slot, and says which slots could serve each one. Names may be loose ('benchy'); an ambiguous or unknown name is an error that lists candidates. Advisory only: it never blocks, and it does not claim to know which slot the printer feeds for which tool. Returns nothing to compare on a printer with no Box.
Inputs: filenames (required). Returns: object
clean_nozzleA
Run QIDI's nozzle-cleaning routine (CLEAR_NOZZLE): heats up, moves over the purge chute and wipes the nozzle. Worth doing before a critical first layer. Requires confirm=true; refused during a print.
Inputs: confirm, hotend_temp, confirm_token. Returns: object
control_job_queueA
Start or pause the shared job queue. action='start' releases it so queued jobs begin printing as soon as the printer is idle — that needs the printer lease, since it commits the machine. action='pause' holds them, which is always allowed: preventing surprise starts is the safe direction. Requires confirm=true. Neither affects a job that is already printing.
Inputs: action (required), confirm, confirm_token. Returns: object
cooldownAIdempotent
Turn off every heater (extruder, bed, chamber) in one call. Always permitted — this is the safe direction — and the right thing to call when you are unsure of the machine's state or finishing a session. The QIDI Box's own heater (filament drying) is left alone unless also_box_heater=true, so a drying run is not ended by accident.
Inputs: also_stop_fans, also_box_heater. Returns: object
copy_fileA
Copy a file/directory on the printer. Both paths include the root, e.g. source='gcodes/a.gcode', dest='gcodes/backup/a.gcode'. Requires confirm=true.
Inputs: dest (required), source (required), confirm, confirm_token. Returns: object
create_directoryA
Create a directory on the printer, path including the root, e.g. 'gcodes/projects'. Requires confirm=true.
Inputs: path (required), confirm, confirm_token. Returns: object
delete_directoryADestructive
Delete a directory on the printer. Two-step confirmation, and the ticket lists what is inside first. force=true is required to delete a non-empty directory.
Inputs: path (required), force, confirm, confirm_token. Returns: object
delete_fileADestructive
Delete a file from the printer. Two-step confirmation. Refuses to delete the file that is currently printing.
Inputs: path (required), root, confirm, confirm_token. Returns: object
display_messageAIdempotent
Show a short message on the printer's screen (M117) and/or write one to the G-code console (M118), which is where a human watching Fluidd will see it.
Inputs: message (required), to_console. Returns: object
download_fileARead-onlyIdempotent
Read a file off the printer as text (truncated to a byte budget so it cannot flood context). Works on any root: 'gcodes' for prints, 'config' for printer.cfg and friends, 'logs' for klippy.log. Use tail=true to read the end.
Inputs: path (required), root, tail, max_bytes. Returns: object
emergency_stopADestructiveIdempotent
EMERGENCY STOP. Immediately halts the printer: all heaters off, all motion stopped, Klipper enters a shutdown state (this is M112). Any running print is lost. Never blocked — no confirmation, and it works even in read-only mode. Use it the moment something looks wrong. Recovery afterwards needs restart_firmware.
Inputs: reason. Returns: object
extrude_filamentA
Manually extrude (positive) or retract (negative) filament, in mm. The hotend must already be at or above Klipper's min_extrude_temp (135 °C on the Max4) — heat it with set_temperature first. Capped per call by policy. Requires confirm=true. Blocked during a print.
Inputs: distance (required), confirm, feedrate, confirm_token. Returns: object
get_ai_detection_statusARead-onlyIdempotent
Report the Max4's AI failure detection: whether spaghetti ('noodle') and foreign-object detection are enabled, their sensitivity, and the last verdict the camera service reported. Read-only. Note a zero count from a DISABLED detector means 'nothing ever reported', not 'the bed is clear' -- check the enabled flags before trusting it as evidence of anything.
Inputs: no named parameters. Returns: object
get_audit_logARead-onlyIdempotent
Every non-read action this MCP server has taken on the printer this session, with risk tier, arguments, and outcome (including refusals). Use it to answer 'what did the agent actually do to my printer?'
Inputs: count. Returns: object
get_bed_meshBRead-onlyIdempotent
The active bed mesh and saved profiles: the probed Z deviation grid, its range, and the mesh area. Reveals whether the bed is warped and by how much.
Inputs: include_matrix. Returns: object
get_box_statusARead-onlyIdempotent
State of QIDI's Box (the multi-filament switcher): whether the firmware will actually use it, each unit's four slots with what the printer believes is in them (filament, colour and vendor decoded to names where QIDI's tables are readable), which slot is synced to the extruder, and the live box_extras object verbatim. If the Box is not working, problems says which of the conditions QIDI's macros test is false. Compare slots with the file's filament_type / filament_colors / referenced_tools from get_file_metadata before printing a multi-colour job. Returns box_count=0 on a machine with no Box.
Inputs: no named parameters. Returns: object
get_capabilitiesARead-onlyIdempotent
What this specific printer supports, discovered live: heaters and their real temperature limits, controllable fans, sensors, LEDs, output pins, filament sensors, axis travel, calibration features, QIDI-specific Klipper modules, Moonraker components present, and macro count. Call this before assuming a feature exists — QIDI ship a trimmed Moonraker and an extended Klipper.
Inputs: refresh. Returns: object
get_filament_presetsARead-onlyIdempotent
QIDI's built-in filament profiles as configured on this printer, including the recommended drying temperature and time per material from the machine's own drying table. Useful for answering 'how should I dry this spool?'.
Inputs: no named parameters. Returns: object
get_file_metadataARead-onlyIdempotent
Full slicer metadata for one G-code file: estimated time, filament length and weight, layer heights, per-object height, nozzle diameter, filament type, first-layer and chamber temperatures, thumbnails present, and the slicer used. Read this before start_print to confirm the file matches the loaded filament.
Inputs: filename (required). Returns: object
get_gcode_logARead-onlyIdempotent
Recent G-code console history — the commands sent and Klipper's replies, exactly what the Fluidd console shows. The fastest way to see why something failed or what a macro actually did.
Inputs: count. Returns: object
get_history_totalsARead-onlyIdempotent
Lifetime totals for the machine: jobs completed, total print time, longest job, and total filament used.
Inputs: no named parameters. Returns: object
get_job_queueARead-onlyIdempotent
The pending job queue: what is lined up to print next and whether the queue is started or paused.
Inputs: no named parameters. Returns: object
get_job_statusARead-onlyIdempotent
Detail on the current print job: state, filename, progress by both layer and file position, durations, filament used, estimated remaining time, and the slicer metadata for the running file.
Inputs: no named parameters. Returns: object
get_klipper_logARead-onlyIdempotent
Read the tail of klippy.log — the definitive place to find why Klipper shut down, what an MCU error was, or what a failed print reported. Truncated to a byte budget.
Inputs: which, max_bytes. Returns: object
get_positionBRead-onlyIdempotent
Toolhead position: raw kinematic position, the G-code position (offsets applied), which axes are homed, the active Z offset, and speed/flow factors.
Inputs: no named parameters. Returns: object
get_printer_configARead-onlyIdempotent
The printer's live Klipper configuration as Moonraker reports it: either the resolved settings tree (section -> key -> value) or the list of section names. Pass a section name to get just that section — the full tree is large.
Inputs: section, list_sections. Returns: object
get_printer_leaseARead-onlyIdempotent
Who is currently driving this printer, if anyone. Returns the holder's name, their stated reason, and how many minutes are left, plus whether it is you. A print started at the touchscreen or from Fluidd shows as a manual-print claim (manual=true) held by nobody. Call this first when a write is refused with 'lease_held', and before starting any long job. Reports coordination as unavailable on a printer whose Moonraker has no database component.
Inputs: no named parameters. Returns: object
get_printer_statusARead-onlyIdempotent
Overall printer state: Klipper readiness, print job state and progress, temperatures, position, homed axes, and any active error message. Start here — this is the cheapest complete picture of the machine.
Inputs: no named parameters. Returns: object
get_print_historyARead-onlyIdempotent
Past print jobs with status, duration, filament used and timestamps. Useful for 'did the last print succeed?' and for spotting a pattern of failures.
Inputs: limit, start. Returns: object
get_safety_policyARead-onlyIdempotent
The safety envelope this MCP server is enforcing: temperature ceilings, confirmation thresholds, jog/extrude caps, read-only and during-print flags, and which risk tiers require confirmation. Read this to understand why a call was refused.
Inputs: no named parameters. Returns: object
get_saved_variablesARead-onlyIdempotent
The printer's persistent saved variables (save_variables.cfg): Z offset, power-loss-recovery state, Box slot assignments, feature toggles, and the last printed file. Read-only view of what the firmware remembers across restarts.
Inputs: no named parameters. Returns: object
get_system_infoARead-onlyIdempotent
Host and service health: Moonraker/Klipper versions, the printer's OS and CPU, load, memory, uptime, throttle flags (undervoltage), MCU load and communication error counters. Use this when prints fail oddly — an undervoltage flag or MCU bandwidth saturation explains a lot.
Inputs: no named parameters. Returns: object
get_temperature_historyARead-onlyIdempotent
Moonraker's cached temperature history (roughly the last 20 minutes, 1 Hz) for every heater and sensor: temperatures, targets, and power. Use this to judge whether a heater is tracking its target or a thermal runaway is developing.
Inputs: last_n, sensor, include_monitors. Returns: object
get_temperaturesARead-onlyIdempotent
Current temperature, target, and heater power for every heater, plus all read-only temperature sensors (including the Max4's chamber thermal-protection sensor) and temperature-controlled fans.
Inputs: no named parameters. Returns: object
get_webcam_snapshotARead-only
Grab a still image from the printer's camera and return it for inspection. Use this to check on a print: whether the first layer stuck, whether there is spaghetti, whether the part came loose. This printer switches its case light off when the touchscreen sleeps, so an idle machine usually returns a black frame: rather than hand you an image with nothing in it, the tool refuses with error 'frame_too_dark' and tells you to call set_case_light first. Pass allow_dark=true only if you want the raw bytes regardless — never describe the contents of a frame returned that way.
Inputs: allow_dark, camera_url.
home_axesA
Home the printer. Omit axes to home all of them (G28), or pass e.g. 'xy' to home a subset. On the Max4, homing Z runs the inductive probe against the bed. Requires confirm=true. Blocked during a print.
Inputs: axes, confirm, confirm_token. Returns: object
input_shaper_calibrateADestructive
Run resonance testing with the toolhead accelerometer and compute optimal input shaper settings (SHAPER_CALIBRATE). The gantry is shaken hard through a frequency sweep — loud, and it must not be interrupted. Results are applied in memory; saving them rewrites printer.cfg. Two-step confirmation. Note: the Max4 also ships QIDI's own M901 macro for this.
Inputs: axis, confirm, confirm_token. Returns: object
list_box_filament_optionsARead-onlyIdempotent
Search the filament profiles, colour swatches and vendors the printer itself knows (its officiall_filas_list.cfg). Use it to find the exact name to give set_box_slot_filaments. query filters profiles by words in their name or type ('petg', 'matte', 'cf'); leave it empty for all of them. Colours are the printer's fixed swatches, each with a plain-English name.
Inputs: query. Returns: object
list_devicesARead-onlyIdempotent
Everything controllable or readable that is not a heater or an axis: fans, output pins, LEDs, filament sensors, and their current values. A quick inventory before poking at hardware.
Inputs: no named parameters. Returns: object
list_directoryARead-onlyIdempotent
List one directory's immediate contents (subdirectories and files) under a root. Use this instead of list_gcode_files when you care about folder structure.
Inputs: path, extended. Returns: object
list_fansARead-onlyIdempotent
Every fan on the printer with its current speed and type: the part-cooling fan, the Max4's two auxiliary fans, the chamber circulation fan, and the firmware-managed hotend/board fans (which report but cannot be set).
Inputs: no named parameters. Returns: object
list_file_rootsARead-onlyIdempotent
The file roots this printer exposes (gcodes, config, logs, timelapse) with their permissions. Tells you where you may read and write.
Inputs: no named parameters. Returns: object
list_gcode_commandsBRead-onlyIdempotent
Every G-code command and macro this printer accepts, with descriptions where available. Includes QIDI's custom macros (PRINT_START, M4029 levelling, M603/M604 filament handling, CLEAR_NOZZLE, and so on). Filter with contains.
Inputs: limit, contains. Returns: object
list_gcode_filesARead-onlyIdempotent
List G-code files on the printer with size, modified time, and (when scanned) estimated print time and filament use. Filter with contains, sort by name or modified time. This is where start_print's filenames come from.
Inputs: root, limit, sort_by, contains. Returns: object
list_webcamsARead-onlyIdempotent
The cameras this printer exposes, with their stream and snapshot URLs. On stock QIDI firmware the camera is a crowsnest ustreamer instance on port 8080.
Inputs: no named parameters. Returns: object
load_box_slotADestructive
Feed one Box slot toward the extruder with QIDI's E_LOAD SLOT= S=. Refused unless the printer lists E_LOAD: a real Max4 Box seen so far did not (it has MULTI_COLOR_LOAD, parameters unpublished), so on it this tool says so and sends nothing. slot is 0-based across the whole Box (unit 1 = slots 0-3, unit 2 = 4-7, ...). Refused unless the firmware will actually act on the Box (see get_box_status problems), the slot exists, and no tool change is running; empty-slot and already-loaded conditions are shown on the ticket for you to weigh. Two-step confirmation; refused during a print. Not yet verified against a physical Box: run docs/guides/box-first-contact.md first. To unload, use unload_filament -- QIDI's M603 already calls E_UNLOAD for the extruder.
Inputs: slot (required), confirm, hotend_temp, confirm_token. Returns: object
load_filamentADestructive
Run one stage of QIDI's filament load routine (M604). stage='heat' warms up and parks over the purge chute, stage='push' feeds filament in, stage='purge' extrudes until the old colour is flushed. Run them in that order, checking between stages — this mirrors what the printer's own screen does. Two-step confirmation; refused during a print.
Inputs: stage, confirm, hotend_temp, confirm_token. Returns: object
motors_offA
Disable the stepper motors (M84). The toolhead can then be moved by hand, but the printer loses its homed position and must be re-homed. Refused while a print is running — it would abandon the job mid-air.
Inputs: confirm, confirm_token. Returns: object
move_absoluteA
Move the toolhead to absolute coordinates in mm. Supply any of x/y/z; omitted axes hold position. feedrate is mm/min (capped by policy). Axes must be homed and each coordinate must be inside the machine's travel. Requires confirm=true. Blocked during a print.
Inputs: x, y, z, confirm, +2 more. Returns: object
move_fileA
Move or rename a file/directory on the printer. Both paths include the root, e.g. source='gcodes/old.gcode', dest='gcodes/archive/new.gcode'. Requires confirm=true.
Inputs: dest (required), source (required), confirm, confirm_token. Returns: object
move_relativeA
Jog the toolhead by a relative offset in mm (any of dx/dy/dz). The destination is computed and bounds-checked before moving, and a single jog is capped by policy. Requires confirm=true. Blocked during a print.
Inputs: dx, dy, dz, confirm, +2 more. Returns: object
pause_printA
Pause the running print. On the Max4 this runs QIDI's PAUSE macro, which parks the toolhead and retracts. Requires confirm=true and an active print.
Inputs: confirm, confirm_token. Returns: object
probe_accuracyA
Probe the same point repeatedly and report min/max/average/median/stddev. This is the diagnostic for 'is my probe repeatable?' — a stddev above ~0.01 mm means the probe or gantry has a mechanical problem. Moves Z only. Requires confirm=true.
Inputs: confirm, samples, probe_speed, confirm_token. Returns: object
qidi_bed_level_calibrateADestructive
Run QIDI's own bed-levelling calibration (M4029) — the routine the printer's touchscreen calls, which heats the bed, probes, and stores the result. This is QIDI's equivalent of bed_mesh_calibrate plus Z-tilt, tuned for this machine. Takes 10+ minutes and normally restarts Klipper at the end. Two-step confirmation.
Inputs: confirm, bed_temp, confirm_token, restart_after. Returns: object
query_endstopsARead-onlyIdempotent
Read every endstop's triggered/open state without moving. Use this to diagnose a homing failure — a switch already reading 'TRIGGERED' at rest is the usual culprit.
Inputs: no named parameters. Returns: object
query_filament_sensorCRead-onlyIdempotent
Whether filament is currently detected, and whether the sensor is enabled.
Inputs: sensor. Returns: object
query_objectsARead-onlyIdempotent
Escape hatch for arbitrary Klipper status objects. Pass a list of object names (e.g. ['bed_mesh','probe','mcu']); omit to get the full object list instead. Use get_capabilities first to see what exists.
Inputs: objects. Returns: object
query_probeARead-onlyIdempotent
Read the probe's current state (triggered or open) without moving anything. On the Max4 this is QIDI's inductive 'probe_air' sensor.
Inputs: no named parameters. Returns: object
queue_jobsA
Add one or more G-code files to the shared print queue. Names may be loose ('benchy' finds benchy.gcode); an unknown or ambiguous name fails the whole call with the candidates. The first call (no confirm) shows what each name resolved to, its print time, and whether the Box's filament suits it; re-call with confirm=true to queue. This is the cooperative way to share the machine: when somebody else is printing, queue your file so it runs after theirs instead of competing for the printer. Requires confirm=true. If the queue is already 'ready' it auto-starts when the printer goes idle, so in that state this counts as starting a print and also needs the printer lease.
Inputs: filenames (required), reset, confirm, confirm_token. Returns: object
reboot_printerADestructive
Reboot the printer's host computer (the whole OS, not just Klipper). Everything goes down for a minute or two. Two-step confirmation; refused during a print.
Inputs: confirm, confirm_token. Returns: object
release_printer_leaseA
Give the printer back when you are finished. Do this rather than letting the lease expire — the next person should not have to wait out your timeout.
Inputs: no named parameters. Returns: object
remove_queued_jobsADestructive
Remove jobs from the print queue by job_id, or clear the whole queue with all=true. Requires confirm=true.
Inputs: all, confirm, job_ids, confirm_token. Returns: object
renew_printer_leaseA
Extend your own lease, for a job that is taking longer than planned. Refused if you do not hold it, or if it already lapsed and somebody else took it.
Inputs: minutes. Returns: object
restart_firmwareADestructive
FIRMWARE_RESTART: reset the microcontrollers and reload Klipper's config. This is how you recover from a shutdown or an e-stop, and how config changes take effect. Loses homing and any in-memory tuning. Requires confirm=true; refuses while a print is running.
Inputs: confirm, confirm_token. Returns: object
restart_klipper_hostADestructive
RESTART: restart the Klipper host process (not the MCUs, not the OS). Reloads config without resetting the microcontrollers — a lighter action than restart_firmware, but it still ends any print. Two-step confirmation.
Inputs: confirm, confirm_token. Returns: object
restart_serviceADestructive
Restart, stop, or start a systemd service on the printer (klipper, moonraker, crowsnest, …). Two-step confirmation. Restarting klipper or moonraker will drop this connection briefly and ends any print.
Inputs: service (required), action, confirm, confirm_token. Returns: object
resume_after_power_lossADestructive
Resume the print that was interrupted by a power loss (QIDI's power-loss recovery, 断电续打). Moonraker rebuilds recovery G-code from its saved state and STARTS PRINTING from where it stopped. Only meaningful when the printer's saved state says a print was interrupted — check get_saved_variables for 'was_interrupted' first. Two-step confirmation, because it begins a real print. QIDI-specific: not available on stock Klipper.
Inputs: confirm, confirm_token. Returns: object
resume_printB
Resume a paused print. Requires confirm=true and a paused job.
Inputs: confirm, confirm_token. Returns: object
run_gcodeADestructive
Run raw G-code on the printer — the escape hatch for anything the dedicated tools do not cover. Every line is screened: pure queries (M105, QUERY_ENDSTOPS, DUMP_TMC…) run with no confirmation, anything that acts needs confirm=true or the two-step token, and commands that bypass safety systems or rewrite config (SAVE_CONFIG, FORCE_MOVE, SET_KINEMATIC_POSITION, SET_TMC_CURRENT, PID_CALIBRATE…) are refused unless allow_dangerous=true AND the operator started the server with QIDI_ALLOW_DANGEROUS_GCODE=1. Heater targets, velocity/accel limits and coordinates are range-checked against the safety envelope. Prefer the dedicated tools — they add interlocks this cannot.
Inputs: script (required), confirm, confirm_token, allow_dangerous. Returns: object
run_macroADestructive
Run one of the printer's defined macros by name, with optional parameters. This is how you reach QIDI's own routines (e.g. M4029 bed levelling, CLEAR_NOZZLE, PRINT_START). Use list_gcode_commands to see what exists. Two-step confirmation, because a macro can do anything. The assembled command is screened exactly like run_gcode (deny-list, temperature and velocity/accel envelope); deny-listed macros are always refused here.
Inputs: macro (required), params, confirm, confirm_token. Returns: object
save_z_offsetADestructive
Persist the current live Z offset using QIDI's M1002 routine, so it survives a restart. Use set_z_offset to dial it in first, then this to keep it. Two-step confirmation, because it writes persistent state.
Inputs: confirm, confirm_token. Returns: object
screws_tilt_calculateA
Probe the bed's screw positions and report how far, and which way, to turn each levelling knob. The Max4 has four screws. Manual bed levelling aid — the printer only measures; you turn the knobs. Requires confirm=true.
Inputs: confirm, direction, confirm_token. Returns: object
set_ai_detectionA
Enable or disable one of the Max4's AI failure detectors: type 'noodle' (spaghetti) or 'foreign' (debris on the plate), optionally setting sensitivity to LOW/MEDIUM/HIGH. Worth enabling before a long unattended print. Turning a detector ON is free; turning one OFF removes a safety net and needs confirm=true. This only flips the firmware flag -- it cannot start QIDI's camera service, so verify with get_ai_detection_status afterwards.
Inputs: enabled (required), detector (required), confirm, sensitivity. Returns: object
set_box_optionA
Change one Box setting QIDI stores in saved variables: 'enable_box' (the firmware's master switch for Box handling), 'auto_read_rfid' (read the spool tag when a spool is inserted) or 'auto_reload_detect' (on a filament runout, run QIDI's AUTO_RELOAD_FILAMENT -- the Box feeds filament by itself). Needs a detected Box, refused during a print, and always needs confirm=true because there is no free direction: switching auto-reload on makes the machine feed filament unattended, and switching enable_box off makes the firmware ignore the Box. Reads the value back and reports whether it took. Slot contents and box_count cannot be set here.
Inputs: option (required), enabled (required), confirm. Returns: object
set_box_slot_filamentsA
Tell the printer what filament is in each Box slot, in plain words. assignments is a list like [{"slot": 0, "filament": "PLA Matte", "color": "black"}, {"slot": 1, "filament": "PETG", "color": "#1353AB", "vendor": "Generic"}]. slot is 0-based across the Box (unit 1 = slots 0-3). Filament and vendor are names or ids from list_box_filament_options; color is a colour word, #RRGGBB or swatch id, and lands on the nearest of the printer's own swatches. Only the fields you name change; everything else is kept. The first call (no confirm) shows exactly what each name resolved to and what would be written; re-call with confirm=true to write it, and the values are read back. This labels spools for the printer; it moves no filament and does not change whether a slot holds filament. Refused during a print. Not yet verified on a physical Box.
Inputs: assignments (required), confirm. Returns: object
set_case_lightAIdempotent
Turn the chamber/case light on or off. Handy before taking a webcam snapshot.
Inputs: on (required). Returns: object
set_fan_speedAIdempotent
Set a fan's speed as a fraction from 0.0 to 1.0. Use fan='part' (or omit) for the part-cooling fan; other names come from list_fans (e.g. 'auxiliary_cooling_fan', 'chamber_circulation_fan'). Firmware-managed fans (hotend, board) cannot be set.
Inputs: speed (required), fan. Returns: object
set_filament_sensorA
Enable or disable filament runout detection. Disabling it removes the printer's protection against printing into thin air after the spool runs out — QIDI's own macros disable it briefly during filament changes. Requires confirm=true to disable; enabling is free.
Inputs: enable (required), sensor, confirm, confirm_token. Returns: object
set_flow_factorA
Set the extrusion flow override (M221) as a percentage, 50–150. Applies live. Use sparingly — flow changes mid-print show up as a visible band.
Inputs: percent (required), confirm, confirm_token. Returns: object
set_input_shaperA
Set input shaper frequency/type directly, without running a calibration sweep. Useful to apply values you already know. Applies in memory only.
Inputs: freq_x, freq_y, confirm, confirm_token, +2 more. Returns: object
set_ledAIdempotent
Set an RGB(W) LED strip's colour. Components are 0.0–1.0. On the Max4 the strip is 'RGB' (25 LEDs); omit index to set the whole chain.
Inputs: led, red, blue, green, +2 more. Returns: object
set_output_pinAIdempotent
Set one of the printer's output pins by name and value (0–1). On the Max4 these are 'caselight', 'beeper', and 'Polar_cooler' (the chamber air conditioner). Use list_devices to see what exists.
Inputs: pin (required), value (required). Returns: object
set_polar_coolerB
Turn the Max4's chamber cooler ('Polar_cooler' — the active chamber air-conditioning) on or off. Used for PLA in a warm chamber, where you want the chamber cooler rather than hotter.
Inputs: on (required). Returns: object
set_speed_factorB
Set the print speed override (M220) as a percentage, 10–200. Applies live to a running print. Slowing a print that is having quality trouble is a common fix.
Inputs: percent (required), confirm, confirm_token. Returns: object
set_temperatureAIdempotent
Set a heater's target temperature. heater is 'extruder', 'bed', or 'chamber'; target is °C (0 turns the heater off). Returns immediately — the printer heats in the background; use wait_for_temperature to block until it arrives. Requires confirm=true. Refused if the target is outside the safety envelope, and while a print is running unless the server allows it.
Inputs: heater (required), target (required), confirm, confirm_token. Returns: object
set_z_offsetA
Adjust the live Z offset (baby-stepping), in mm — positive raises the nozzle. Use mode='adjust' to nudge relative to the current offset, or mode='set' for an absolute value. Applies immediately, including mid-print, which is the usual way to fix first-layer squish. Not persistent across restarts.
Inputs: offset (required), mode, move, confirm, +1 more. Returns: object
shutdown_printerADestructive
Shut down the printer's host computer. It will NOT come back until someone power-cycles the machine physically. Two-step confirmation; refused during a print.
Inputs: confirm, confirm_token. Returns: object
start_printADestructive
Start printing a G-code file already on the printer (path relative to the gcodes root, e.g. 'benchy.gcode' or 'projects/bracket.gcode'). Two-step confirmation: the first call returns the file's slicer metadata plus a token so you can check you are printing the right thing, and the second call with that token starts it. Refused if a job is already running.
Inputs: filename (required), confirm, confirm_token. Returns: object
unload_filamentADestructive
Run QIDI's filament unload routine (M603). Heats the hotend to the given temperature, then retracts and ejects the filament. Give a temperature appropriate to the filament currently loaded (e.g. 250 for PLA, 280 for PETG). Two-step confirmation; refused during a print.
Inputs: confirm, hotend_temp, confirm_token. Returns: object
upload_gcodeA
Upload a G-code file to the printer. Provide the content either as content (plain text) or content_base64 (for binary-safe transfer). Does NOT start a print — call start_print separately, deliberately. Requires confirm=true; set overwrite=true to replace an existing file.
Inputs: filename (required), confirm, content, overwrite, +3 more. Returns: object
wait_for_temperatureARead-onlyIdempotent
Poll until a heater reaches its target (within tolerance) or the timeout expires. Does not change any target — set_temperature does that. Safe to call repeatedly; returns the temperature trend so you can tell whether heating is actually progressing.
Inputs: heater (required), target, timeout_s, tolerance. Returns: object
z_tilt_adjustB
Level the Max4's two independent Z steppers against each other (Z_TILT_ADJUST). Probes the configured points and adjusts each Z motor until the gantry is square. Run this if first layers are thicker on one side. Requires confirm=true.
Inputs: confirm, retries, confirm_token, retry_tolerance. Returns: object
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 101 tools
Most tools have a clear distinct purpose, but several clusters overlap heavily: restart_firmware vs restart_klipper_host vs restart_service vs reboot_printer, bed_mesh_calibrate vs qidi_bed_level_calibrate vs z_tilt_adjust, set_temperature vs chamber_heater vs set_polar_cooler, list_fans vs list_devices, run_gcode vs run_macro. The long descriptions do resolve most of these, but the agent must read carefully and an overlap like cooldown/set_temperature(target=0) invites misselection.
Nearly everything is snake_case with a verb_noun pattern (get_*, set_*, list_*, start_print, cancel_print), which is highly predictable. Deviations are minor: bare noun/verb names (beep, cooldown, chamber_heater, emergency_stop) and domain-prefixed forms (qidi_bed_level_calibrate, bed_mesh_profile, box_*), but the convention stays readable.
101 tools for a single 3D printer is an extreme surface; many operations are splintered into near-duplicate tools (four restart variants, three bed-leveling paths, separate list_fans/list_devices/list_directory/list_gcode_files). This far exceeds what an agent can scan and select from reliably, and a large fraction could be consolidated into parameterised tools.
Coverage of the printer domain is essentially exhaustive: motion, heaters, files/CRUD, job queue, box/multi-filament, calibration, diagnostics, recovery, leases, audit and safety policy all have dedicated tools, with no obvious dead ends. Minor omissions (e.g. no explicit tool-change or PID tuning exposed) are covered by run_gcode/run_macro escape hatches.
Maintenance
Related MCP Connectors
3D print farm management for AI. Monitor, queue, and control prints on your SimplyPrint account.
Parametric 3D CAD for AI agents: build print-ready parts, check them, export STL, 3MF or STEP.
Connect your AI to your own devices, 3D printers, factory tools and a shared agent task pool.
Operate Linux, macOS and Windows from your LLM. Every action runs through an auditable allowlist.
Related MCP Servers
- AlicenseNot gradedqualityCmaintenanceEnables AI agents to manage multiple 3D printers via MCP, including monitoring temperatures and progress, uploading and slicing models, controlling prints, and viewing cameras.3 npmMIT
- AlicenseNot gradedqualityBmaintenanceEnables AI agents to monitor and control Prusa 3D printers over PrusaLink or Prusa Connect, including status and camera checks, file upload and print control, G-code execution, queue and history access, and responding to printer dialogs.1MIT
- AlicenseNot gradedqualityBmaintenanceEnables an agent to inspect, slice, upload, start, and monitor jobs on a Moonraker/Klipper 3D printer end to end, using one JSON profile for machine-specific configuration. Built-in safety checks block unsafe starts such as incomplete G-code, nozzle mismatches, hot beds, or running jobs.MIT
- AlicenseAqualityCmaintenanceEnables AI assistants to monitor and control Bambu Lab 3D printers over a local network via MQTT, including real-time status, AMS filament info, print control, and maintenance tracking. It supports guided setup, safety confirmations, read-only mode, and a built-in printer simulator.280 npmMIT