bambu-printer-mcp 1.1.27 → 1.1.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ import { createServer as createHttpServer } from "node:http";
11
11
  import { randomUUID } from "node:crypto";
12
12
  import { STLManipulator, SLICER_TYPES, normalizeSlicerType, } from "./stl/stl-manipulator.js";
13
13
  import { BambuNetworkBridge } from "./bambu-network-bridge.js";
14
- import { assertBambuNativeAvailable, validateBambuNativeMetadata, buildBambuNativeFanCommand, buildBambuNativeTemperatureCommand, printWithBambuNative, sendCommandWithBambuNative, uploadWithBambuNative, } from "./bambu-native.js";
14
+ import { assertBambuNativeAvailable, bambuNativeControlsAvailable, bambuNativeUnavailableReason, interruptNativeOperations, validateBambuNativeMetadata, buildBambuNativeFanCommand, buildBambuNativeTemperatureCommand, printWithBambuNative, sendCommandWithBambuNative, uploadWithBambuNative, } from "./bambu-native.js";
15
15
  import { importFileViaBambuConnect } from "./bambu-connect.js";
16
16
  import { BlenderMcpBridge } from "./blender-mcp-bridge.js";
17
17
  import { hasAmsMappingInput, normalizeAmsMappingObject, normalizeBridgeAmsTrayValue } from "./ams-mapping.js";
@@ -90,6 +90,20 @@ function resolveNozzleType(value) {
90
90
  }
91
91
  return normalized;
92
92
  }
93
+ /** BambuStudio's nozzle_volume_type value for each tool argument value. */
94
+ const NOZZLE_VOLUME_TYPES = { standard: "Standard", high_flow: "High Flow" };
95
+ const VALID_NOZZLE_VOLUME_TYPES = Object.keys(NOZZLE_VOLUME_TYPES);
96
+ /** The installed nozzle's flow type. Bambu machine presets assume Standard; the print gate compares this with the printer. */
97
+ function resolveNozzleVolumeType(value) {
98
+ const raw = value === undefined || value === null || value === "" ? process.env.BAMBU_NOZZLE_VOLUME_TYPE?.trim() : value;
99
+ if (raw === undefined || raw === "")
100
+ return undefined;
101
+ const normalized = String(raw).trim().toLowerCase().replace(/[\s-]+/g, "_");
102
+ if (!VALID_NOZZLE_VOLUME_TYPES.includes(normalized)) {
103
+ throw new Error(`Invalid nozzle volume type "${raw}". Valid types: ${VALID_NOZZLE_VOLUME_TYPES.join(", ")}`);
104
+ }
105
+ return NOZZLE_VOLUME_TYPES[normalized];
106
+ }
93
107
  const VALID_BAMBUSTUDIO_CLI_BED_TYPES = ["textured_plate", "cool_plate", "engineering_plate", "hot_plate"];
94
108
  // Map model IDs to BambuStudio --load-machine preset names
95
109
  const BAMBU_MODEL_PRESETS = {
@@ -1040,6 +1054,7 @@ class BambuPrinterMCPServer {
1040
1054
  skipModifiedGcodes: true,
1041
1055
  bedType: bedType ? resolveBambuStudioCliBedType(bedType) : undefined,
1042
1056
  nozzleType: resolveNozzleType(args?.nozzle_type),
1057
+ nozzleVolumeType: resolveNozzleVolumeType(args?.nozzle_volume_type),
1043
1058
  };
1044
1059
  threeMFPath = await this.stlManipulator.sliceSTL(threeMFPath, slicerType, slicerPath, slicerProfile || undefined, undefined, printPreset, autoSliceOptions);
1045
1060
  console.error("Auto-sliced to: " + threeMFPath);
@@ -1326,12 +1341,14 @@ class BambuPrinterMCPServer {
1326
1341
  return this.print3mfViaBambuNetwork({ ...args, bambu_model: model, connection_type: "lan", bambu_network_method: "start_local_print" }, host, bambuSerial, bambuToken, { onUpdate, signal });
1327
1342
  }
1328
1343
  async uploadFileViaBambuNative(args, host, bambuSerial, bambuToken, onUpdate, signal) {
1329
- assertBambuNativeAvailable();
1330
1344
  if (args.print)
1331
1345
  throw new Error("Native upload never starts printing; use print_3mf.");
1346
+ // Resolve and enforce the model first, as the print path does, so a
1347
+ // non-X2D request is never told to install the native helper.
1332
1348
  const model = await this.resolveBambuModel(args.bambu_model);
1333
1349
  if (model !== "x2d")
1334
1350
  throw new Error("connection_mode=bambu_native is restricted to X2D.");
1351
+ assertBambuNativeAvailable();
1335
1352
  const unsupported = ["use_ams", "ams_mapping", "ams_mapping2", "ams_mapping_info", "nozzle_mapping", "nozzles_info",
1336
1353
  "bed_leveling", "flow_calibration", "vibration_calibration", "layer_inspect", "timelapse"];
1337
1354
  for (const key of unsupported) {
@@ -1841,6 +1858,11 @@ class BambuPrinterMCPServer {
1841
1858
  enum: [...VALID_NOZZLE_TYPES],
1842
1859
  description: "Bambu-compatible slicing: the hotend nozzle material installed on the printer (default: BAMBU_NOZZLE_TYPE, else the model preset's stock nozzle, usually stainless_steel). Printing compares it with the printer's reported nozzle."
1843
1860
  },
1861
+ nozzle_volume_type: {
1862
+ type: "string",
1863
+ enum: [...VALID_NOZZLE_VOLUME_TYPES],
1864
+ description: "Bambu-compatible slicing: the flow type of the nozzle installed on the printer (default: BAMBU_NOZZLE_VOLUME_TYPE, else standard). high_flow uses the presets' High Flow values, such as the filament's High Flow max volumetric speed, and is refused if the machine preset has no High Flow variant. Printing compares it with the printer's reported nozzle flow."
1865
+ },
1844
1866
  use_printer_filaments: { type: "boolean", description: "When true, and no explicit slicer profile or load_filaments override is provided, use the printer's current or first loaded AMS filament as the slicer filament profile." },
1845
1867
  host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
1846
1868
  bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
@@ -1896,6 +1918,11 @@ class BambuPrinterMCPServer {
1896
1918
  enum: [...VALID_NOZZLE_TYPES],
1897
1919
  description: "Bambu-compatible slicing: the hotend nozzle material installed on the printer (default: BAMBU_NOZZLE_TYPE, else the model preset's stock nozzle, usually stainless_steel). Printing compares it with the printer's reported nozzle."
1898
1920
  },
1921
+ nozzle_volume_type: {
1922
+ type: "string",
1923
+ enum: [...VALID_NOZZLE_VOLUME_TYPES],
1924
+ description: "Bambu-compatible slicing: the flow type of the nozzle installed on the printer (default: BAMBU_NOZZLE_VOLUME_TYPE, else standard). high_flow uses the presets' High Flow values, such as the filament's High Flow max volumetric speed, and is refused if the machine preset has no High Flow variant. Printing compares it with the printer's reported nozzle flow."
1925
+ },
1899
1926
  use_printer_filaments: { type: "boolean", description: "When true, and no explicit slicer profile or load_filaments override is provided, use the printer's current or first loaded AMS filament as the slicer filament profile. Template 3MF process settings can still be used at the same time." },
1900
1927
  uptodate: { type: "boolean", description: "Refresh 3MF preset configs to match the latest BambuStudio version. Use when slicing downloaded or older 3MF files to prevent stale-config failures." },
1901
1928
  repetitions: { type: "number", description: "Print N identical copies of the model. Each copy gets its own plate placement. Example: 3 prints three copies." },
@@ -2002,6 +2029,7 @@ class BambuPrinterMCPServer {
2002
2029
  password: { type: "string", description: "Printer password/access code override for LAN/local bridge methods." },
2003
2030
  bed_type: { type: "string", enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate", "supertack_plate"], description: "Bed plate type currently installed (default: textured_plate)." },
2004
2031
  nozzle_type: { type: "string", enum: [...VALID_NOZZLE_TYPES], description: "Installed nozzle material, used when the 3MF must be auto-sliced (default: BAMBU_NOZZLE_TYPE, else the preset's stock nozzle)." },
2032
+ nozzle_volume_type: { type: "string", enum: [...VALID_NOZZLE_VOLUME_TYPES], description: "Installed nozzle flow type, used when the 3MF must be auto-sliced (default: BAMBU_NOZZLE_VOLUME_TYPE, else standard)." },
2005
2033
  plate_index: { type: "number", description: "Zero-based plate index to print from the sliced 3MF; converted to FULU's one-based PrintParams plate_index." },
2006
2034
  project_name: { type: "string", description: "Optional project name sent in FULU PrintParams; defaults to the 3MF filename without extension." },
2007
2035
  preset_name: { type: "string", description: "Optional preset name sent in FULU PrintParams; defaults to project plus one-based plate index." },
@@ -2265,7 +2293,7 @@ class BambuPrinterMCPServer {
2265
2293
  },
2266
2294
  {
2267
2295
  name: "reread_ams_rfid",
2268
- description: "Trigger a Bambu AMS RFID re-read for one AMS slot. X2D on macOS uses the native official plug-in route. This can move AMS filament; use only when the printer is idle and unloaded.",
2296
+ description: "Trigger a Bambu AMS RFID re-read for one AMS slot. X2D on macOS, or Linux with the native helper installed, uses the native official plug-in route. This can move AMS filament; use only when the printer is idle and unloaded.",
2269
2297
  inputSchema: {
2270
2298
  type: "object",
2271
2299
  properties: {
@@ -2298,7 +2326,7 @@ class BambuPrinterMCPServer {
2298
2326
  },
2299
2327
  {
2300
2328
  name: "set_fan_speed",
2301
- description: "Set a Bambu printer fan speed percentage. X2D on macOS uses the installed Bambu networking plug-in without UI automation; other models use MQTT.",
2329
+ description: "Set a Bambu printer fan speed percentage. X2D on macOS, or Linux with the native helper installed, uses Bambu's networking plug-in without UI automation; other models use MQTT.",
2302
2330
  inputSchema: {
2303
2331
  type: "object",
2304
2332
  properties: {
@@ -2357,7 +2385,7 @@ class BambuPrinterMCPServer {
2357
2385
  },
2358
2386
  {
2359
2387
  name: "set_ams_drying",
2360
- description: "Start or stop the AMS filament drying cycle. X2D on macOS uses the native official plug-in route and supports AMS-HT id 128.",
2388
+ description: "Start or stop the AMS filament drying cycle. X2D on macOS, or Linux with the native helper installed, uses the native official plug-in route and supports AMS-HT id 128.",
2361
2389
  inputSchema: {
2362
2390
  type: "object",
2363
2391
  properties: {
@@ -2429,6 +2457,11 @@ class BambuPrinterMCPServer {
2429
2457
  enum: [...VALID_NOZZLE_TYPES],
2430
2458
  description: "Installed nozzle material, used when the 3MF must be auto-sliced (default: BAMBU_NOZZLE_TYPE, else the preset's stock nozzle)."
2431
2459
  },
2460
+ nozzle_volume_type: {
2461
+ type: "string",
2462
+ enum: [...VALID_NOZZLE_VOLUME_TYPES],
2463
+ description: "Installed nozzle flow type, used when the 3MF must be auto-sliced (default: BAMBU_NOZZLE_VOLUME_TYPE, else standard)."
2464
+ },
2432
2465
  host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
2433
2466
  bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
2434
2467
  bambu_token: { type: "string", description: "Access token (default: value from env)" },
@@ -2618,9 +2651,42 @@ class BambuPrinterMCPServer {
2618
2651
  const host = String(args?.host || DEFAULT_HOST);
2619
2652
  const bambuSerial = String(args?.bambu_serial || DEFAULT_BAMBU_SERIAL);
2620
2653
  const bambuToken = String(args?.bambu_token || DEFAULT_BAMBU_TOKEN);
2621
- const nativeControls = process.platform === "darwin" &&
2622
- (bambuSerial.trim().toUpperCase().startsWith("20P") ||
2623
- (DEFAULT_BAMBU_MODEL === "x2d" && bambuSerial === DEFAULT_BAMBU_SERIAL));
2654
+ const x2dTarget = bambuSerial.trim().toUpperCase().startsWith("20P") ||
2655
+ (DEFAULT_BAMBU_MODEL === "x2d" && bambuSerial === DEFAULT_BAMBU_SERIAL);
2656
+ // Resolved lazily: the Linux availability check reads files, and most
2657
+ // tools (status, slicing, STL) never need it.
2658
+ // One resolution per request; null means native X2D controls are usable.
2659
+ let nativeUnavailable;
2660
+ const nativeUnavailableReason = () => (nativeUnavailable ?? (nativeUnavailable = x2dTarget ? bambuNativeUnavailableReason() ?? null : "The selected printer is not an X2D."));
2661
+ const nativeControls = () => nativeUnavailableReason() === null;
2662
+ // Linux sent X2D stop and heater-off over MQTT before the native route
2663
+ // existed. Send them over both routes there: a stuck helper must not
2664
+ // delay an emergency control, and a repeated stop or heater-off is
2665
+ // harmless. The immediate MQTT send can precede the exit of an older
2666
+ // print or heating helper that then still dispatches, so MQTT is sent
2667
+ // again once every older helper for this printer has exited, and only
2668
+ // that send counts. Either counted route succeeding is success. macOS
2669
+ // stays native-only.
2670
+ const linuxEmergency = (native, mqtt) => async () => {
2671
+ if (process.platform !== "linux")
2672
+ return native();
2673
+ const [nativeResult, immediateResult, mqttResult] = await Promise.allSettled([
2674
+ native(),
2675
+ mqtt(),
2676
+ interruptNativeOperations(bambuSerial).then(() => mqtt()),
2677
+ ]);
2678
+ const reason = (result) => result.status === "rejected" ? (result.reason instanceof Error ? result.reason.message : String(result.reason)) : undefined;
2679
+ if (nativeResult.status === "rejected" && mqttResult.status === "rejected") {
2680
+ throw new Error(`native route: ${reason(nativeResult)}; MQTT route: ${reason(mqttResult)}`);
2681
+ }
2682
+ const state = (result) => result.status === "fulfilled" ? "sent" : "failed";
2683
+ return {
2684
+ status: "success",
2685
+ routes: { native: state(nativeResult), mqtt: state(mqttResult), mqtt_immediate: state(immediateResult) },
2686
+ ...(reason(nativeResult) ? { native_error: reason(nativeResult) } : {}),
2687
+ ...(reason(mqttResult) ? { mqtt_error: reason(mqttResult) } : {}),
2688
+ };
2689
+ };
2624
2690
  const requestedTemplateDir = typeof args?.template_dir === "string" && args.template_dir.trim().length > 0
2625
2691
  ? String(args.template_dir)
2626
2692
  : undefined;
@@ -2822,19 +2888,19 @@ class BambuPrinterMCPServer {
2822
2888
  break;
2823
2889
  }
2824
2890
  case "cancel_print":
2825
- result = await this.bambu.cancelJob(host, bambuSerial, bambuToken, nativeControls
2826
- ? () => sendNativeCommand({ host, serial: bambuSerial, token: bambuToken,
2827
- messageJson: JSON.stringify({ print: { command: "stop", param: "", sequence_id: String(Date.now()) } }), qos: 1 })
2891
+ result = await this.bambu.cancelJob(host, bambuSerial, bambuToken, nativeControls()
2892
+ ? linuxEmergency(() => sendNativeCommand({ host, serial: bambuSerial, token: bambuToken,
2893
+ messageJson: JSON.stringify({ print: { command: "stop", param: "", sequence_id: String(Date.now()) } }), qos: 1 }), () => this.bambu.sendStopOverMqtt(host, bambuSerial, bambuToken))
2828
2894
  : undefined);
2829
2895
  break;
2830
2896
  case "pause_print":
2831
- result = await this.bambu.pauseJob(host, bambuSerial, bambuToken, nativeControls
2897
+ result = await this.bambu.pauseJob(host, bambuSerial, bambuToken, nativeControls()
2832
2898
  ? () => sendNativeCommand({ host, serial: bambuSerial, token: bambuToken,
2833
2899
  messageJson: JSON.stringify({ print: { command: "pause", param: "", sequence_id: String(Date.now()) } }), qos: 1 })
2834
2900
  : undefined);
2835
2901
  break;
2836
2902
  case "resume_print":
2837
- result = await this.bambu.resumeJob(host, bambuSerial, bambuToken, nativeControls
2903
+ result = await this.bambu.resumeJob(host, bambuSerial, bambuToken, nativeControls()
2838
2904
  ? (assertActive, beforeDispatch) => sendNativeCommand({ host, serial: bambuSerial, token: bambuToken,
2839
2905
  messageJson: JSON.stringify({ print: { command: "resume", param: "", sequence_id: String(Date.now()) } }), qos: 1 }, { signal: extra.signal, assertActive, beforeDispatch })
2840
2906
  : undefined);
@@ -2843,8 +2909,9 @@ class BambuPrinterMCPServer {
2843
2909
  if (!args?.message_json) {
2844
2910
  throw new Error("Missing required parameter: message_json");
2845
2911
  }
2846
- if (!nativeControls) {
2847
- throw new Error("x2d_native_control is restricted to a selected X2D printer on macOS.");
2912
+ if (!nativeControls()) {
2913
+ throw new Error("x2d_native_control is restricted to a selected X2D printer on macOS, or on Linux with the native helper installed. " +
2914
+ nativeUnavailableReason());
2848
2915
  }
2849
2916
  const messageJson = validateBambuNativeMetadata(String(args.message_json));
2850
2917
  result = await sendNativeCommand({
@@ -2858,7 +2925,7 @@ class BambuPrinterMCPServer {
2858
2925
  break;
2859
2926
  }
2860
2927
  case "clear_hms_errors":
2861
- result = await this.bambu.clearHmsErrors(host, bambuSerial, bambuToken, nativeControls
2928
+ result = await this.bambu.clearHmsErrors(host, bambuSerial, bambuToken, nativeControls()
2862
2929
  ? (assertActive, beforeDispatch) => sendNativeCommand({ host, serial: bambuSerial, token: bambuToken,
2863
2930
  messageJson: JSON.stringify({ print: { command: "clean_print_error", param: "", sequence_id: String(Date.now()) } }), qos: 1 }, { signal: extra.signal, assertActive, beforeDispatch })
2864
2931
  : undefined);
@@ -2867,7 +2934,7 @@ class BambuPrinterMCPServer {
2867
2934
  if (!args?.mode) {
2868
2935
  throw new Error("Missing required parameter: mode");
2869
2936
  }
2870
- if (nativeControls) {
2937
+ if (nativeControls()) {
2871
2938
  const normalized = String(args.mode).trim().toLowerCase();
2872
2939
  const mode = normalized === "silent" ? 1
2873
2940
  : normalized === "standard" ? 2
@@ -2894,7 +2961,7 @@ class BambuPrinterMCPServer {
2894
2961
  if (!args?.mode) {
2895
2962
  throw new Error("Missing required parameter: mode");
2896
2963
  }
2897
- if (nativeControls) {
2964
+ if (nativeControls()) {
2898
2965
  const mode = String(args.mode).trim().toLowerCase();
2899
2966
  if (mode !== "cooling" && mode !== "heating") {
2900
2967
  throw new Error("Airduct mode must be one of: cooling, heating.");
@@ -2921,7 +2988,7 @@ class BambuPrinterMCPServer {
2921
2988
  if (args?.ams_id === undefined || args?.slot_id === undefined) {
2922
2989
  throw new Error("Missing required parameters: ams_id and slot_id");
2923
2990
  }
2924
- if (nativeControls) {
2991
+ if (nativeControls()) {
2925
2992
  const amsId = Number(args.ams_id);
2926
2993
  const slotId = Number(args.slot_id);
2927
2994
  if ((!Number.isInteger(amsId) || (amsId < 0 || amsId > 3)) && amsId !== 128) {
@@ -2958,9 +3025,14 @@ class BambuPrinterMCPServer {
2958
3025
  const heaterModel = args.temperature === 0
2959
3026
  ? String(args.bambu_model || DEFAULT_BAMBU_MODEL).trim().toLowerCase()
2960
3027
  : await this.resolveBambuModel(args.bambu_model);
2961
- result = await this.bambu.setTemperature(host, bambuSerial, bambuToken, String(args.component), args.temperature, heaterModel || undefined, args?.material !== undefined ? String(args.material) : undefined, Number(args?.nozzle_diameter ?? DEFAULT_NOZZLE_DIAMETER), (args.temperature === 0 ? nativeControls : heaterModel === "x2d" && process.platform === "darwin")
2962
- ? (heater, target, assertActive, beforeDispatch) => sendNativeCommand({ host, serial: bambuSerial, token: bambuToken,
2963
- messageJson: buildBambuNativeTemperatureCommand(heater, target).messageJson }, { signal: extra.signal, assertActive, beforeDispatch })
3028
+ result = await this.bambu.setTemperature(host, bambuSerial, bambuToken, String(args.component), args.temperature, heaterModel || undefined, args?.material !== undefined ? String(args.material) : undefined, Number(args?.nozzle_diameter ?? DEFAULT_NOZZLE_DIAMETER), (args.temperature === 0 ? nativeControls() : heaterModel === "x2d" && bambuNativeControlsAvailable())
3029
+ ? (heater, target, assertActive, beforeDispatch) => {
3030
+ const native = () => sendNativeCommand({ host, serial: bambuSerial, token: bambuToken,
3031
+ messageJson: buildBambuNativeTemperatureCommand(heater, target).messageJson }, { signal: extra.signal, assertActive, beforeDispatch });
3032
+ return target === 0
3033
+ ? linuxEmergency(native, () => this.bambu.sendHeaterOffOverMqtt(host, bambuSerial, bambuToken, heater))()
3034
+ : native();
3035
+ }
2964
3036
  : undefined);
2965
3037
  break;
2966
3038
  }
@@ -2968,7 +3040,7 @@ class BambuPrinterMCPServer {
2968
3040
  if (!args?.fan || args?.speed === undefined) {
2969
3041
  throw new Error("Missing required parameters: fan and speed");
2970
3042
  }
2971
- if (nativeControls) {
3043
+ if (nativeControls()) {
2972
3044
  const status = await this.bambu.getStatus(host, bambuSerial, bambuToken);
2973
3045
  const printerState = String(status?.raw?.gcode_state ?? status?.raw?.print?.gcode_state ?? status?.gcode_state ?? status?.status ?? "").toUpperCase();
2974
3046
  if (printerState === "RUNNING" && args?.confirm_during_print !== true) {
@@ -2996,7 +3068,7 @@ class BambuPrinterMCPServer {
2996
3068
  if (!args?.light || !args?.mode) {
2997
3069
  throw new Error("Missing required parameters: light and mode");
2998
3070
  }
2999
- if (nativeControls) {
3071
+ if (nativeControls()) {
3000
3072
  const light = String(args.light).trim();
3001
3073
  const mode = String(args.mode).trim().toLowerCase();
3002
3074
  if (light !== "chamber_light" && light !== "chamber_light2") {
@@ -3031,7 +3103,7 @@ class BambuPrinterMCPServer {
3031
3103
  if (!Array.isArray(args?.object_ids)) {
3032
3104
  throw new Error("Missing required parameter: object_ids");
3033
3105
  }
3034
- if (nativeControls) {
3106
+ if (nativeControls()) {
3035
3107
  const objectIds = args.object_ids.map((id) => Number(id));
3036
3108
  if (objectIds.length === 0 || objectIds.some((id) => !Number.isInteger(id) || id < 0)) {
3037
3109
  throw new Error("object_ids must contain one or more non-negative integers.");
@@ -3054,7 +3126,7 @@ class BambuPrinterMCPServer {
3054
3126
  if (!args?.action || args?.ams_id === undefined) {
3055
3127
  throw new Error("Missing required parameters: action and ams_id");
3056
3128
  }
3057
- if (nativeControls) {
3129
+ if (nativeControls()) {
3058
3130
  const action = String(args.action).trim().toLowerCase();
3059
3131
  const amsId = Number(args.ams_id);
3060
3132
  if (action !== "start" && action !== "stop") {
@@ -3164,6 +3236,7 @@ class BambuPrinterMCPServer {
3164
3236
  sliceBambuOptions.filamentColours = parseFilamentColours(String(args.filament_colours));
3165
3237
  sliceBambuOptions.bedType = resolveBambuStudioCliBedType(args?.bed_type);
3166
3238
  sliceBambuOptions.nozzleType = resolveNozzleType(args?.nozzle_type);
3239
+ sliceBambuOptions.nozzleVolumeType = resolveNozzleVolumeType(args?.nozzle_volume_type);
3167
3240
  if (args?.enable_timelapse !== undefined)
3168
3241
  sliceBambuOptions.enableTimelapse = Boolean(args.enable_timelapse);
3169
3242
  if (args?.allow_mix_temp !== undefined)
@@ -3244,6 +3317,7 @@ class BambuPrinterMCPServer {
3244
3317
  sliceBambuOptions.filamentColours = parseFilamentColours(String(args.filament_colours));
3245
3318
  sliceBambuOptions.bedType = resolveBambuStudioCliBedType(args?.bed_type);
3246
3319
  sliceBambuOptions.nozzleType = resolveNozzleType(args?.nozzle_type);
3320
+ sliceBambuOptions.nozzleVolumeType = resolveNozzleVolumeType(args?.nozzle_volume_type);
3247
3321
  if (args?.enable_timelapse !== undefined)
3248
3322
  sliceBambuOptions.enableTimelapse = Boolean(args.enable_timelapse);
3249
3323
  if (args?.allow_mix_temp !== undefined)
@@ -3347,6 +3421,7 @@ class BambuPrinterMCPServer {
3347
3421
  skipModifiedGcodes: true,
3348
3422
  bedType: printBedType,
3349
3423
  nozzleType: resolveNozzleType(args?.nozzle_type),
3424
+ nozzleVolumeType: resolveNozzleVolumeType(args?.nozzle_volume_type),
3350
3425
  };
3351
3426
  if (!explicitSlicerProfile) {
3352
3427
  try {
@@ -26,6 +26,13 @@ interface BambuPrintOptionsInternal {
26
26
  amsSlots?: number[];
27
27
  md5?: string;
28
28
  }
29
+ /** What the human confirmed in confirmPrintPreflight, checked again before dispatch. */
30
+ export type PrintPreflight = {
31
+ finishedJob?: string;
32
+ serial: string;
33
+ model: string;
34
+ notices: string[];
35
+ };
29
36
  export declare class BambuImplementation {
30
37
  private readonly confirm?;
31
38
  private printerStore;
@@ -34,8 +41,14 @@ export declare class BambuImplementation {
34
41
  constructor(confirm?: ((message: string) => Promise<boolean>) | undefined);
35
42
  confirmHardwareAction(message: string, physicalCheck?: boolean): Promise<void>;
36
43
  private finishedJobIdentity;
37
- confirmPrintPreflight(serial: string, status: any, inspection: Awaited<ReturnType<typeof inspectPrintFile>>): Promise<string | undefined>;
38
- assertBedClearance(status: any, confirmedFinishedJob: string | undefined): void;
44
+ /**
45
+ * The human print confirmation. Its result goes to assertBedClearance at
46
+ * every later state check of the same print, which stops the print if the
47
+ * situation the human confirmed has changed.
48
+ */
49
+ confirmPrintPreflight(serial: string, status: any, inspection: Awaited<ReturnType<typeof inspectPrintFile>>): Promise<PrintPreflight>;
50
+ /** Stop a print whose confirmed situation changed: a newly finished job, or a hidden HMS notice the preflight did not name. */
51
+ assertBedClearance(status: any, preflight: PrintPreflight): void;
39
52
  private getPrinter;
40
53
  /** Internal handoff from a successful inspected transport; never exposed as an MCP tool. */
41
54
  recordCheckedJob(host: string, serial: string, remotePath: string, requirements: PrinterStateRequirements): void;
@@ -47,6 +60,10 @@ export declare class BambuImplementation {
47
60
  print3mf(host: string, serial: string, token: string, options: BambuPrintOptionsInternal): Promise<any>;
48
61
  private print3mfPrepared;
49
62
  cancelJob(host: string, serial: string, token: string, dispatch?: () => Promise<any>): Promise<any>;
63
+ /** Raw MQTT stop for routes that already ran the shared cancellation steps. */
64
+ sendStopOverMqtt(host: string, serial: string, token: string): Promise<any>;
65
+ /** Raw MQTT heater-off; switching off needs no material or model declaration. */
66
+ sendHeaterOffOverMqtt(host: string, serial: string, token: string, heater: "bed" | "nozzle"): Promise<any>;
50
67
  pauseJob(host: string, serial: string, token: string, dispatch?: () => Promise<any>): Promise<any>;
51
68
  resumeJob(host: string, serial: string, token: string, dispatch?: (assertActive: () => void, beforeDispatch: () => Promise<void>) => Promise<any>): Promise<any>;
52
69
  clearHmsErrors(host: string, serial: string, token: string, dispatch?: (assertActive: () => void, beforeDispatch: () => Promise<void>) => Promise<any>): Promise<any>;
@@ -54,6 +71,16 @@ export declare class BambuImplementation {
54
71
  setAirductMode(host: string, serial: string, token: string, mode: string): Promise<any>;
55
72
  rereadAmsRfid(host: string, serial: string, token: string, amsId: number, slotId: number): Promise<any>;
56
73
  setTemperature(host: string, serial: string, token: string, component: string, temperature: unknown, bambuModel?: string, material?: string, nozzleDiameter?: number, dispatch?: (heater: "bed" | "nozzle", target: number, assertActive: () => void, beforeDispatch?: () => Promise<void>) => Promise<any>): Promise<{
74
+ routes: any;
75
+ native_error: any;
76
+ mqtt_error: any;
77
+ status: string;
78
+ message: string;
79
+ command: string;
80
+ } | {
81
+ routes?: undefined;
82
+ native_error?: undefined;
83
+ mqtt_error?: undefined;
57
84
  status: string;
58
85
  message: string;
59
86
  command: string;
@@ -8,7 +8,7 @@ import { Readable } from "node:stream";
8
8
  import { readSafe3mfArchive } from "../safety/archive.js";
9
9
  import { inspectPrintFile } from "../safety/print-file.js";
10
10
  import { normalizeModel, validateTemperature } from "../safety/limits.js";
11
- import { validatePrinterState, manualHeatingRequirements } from "../safety/printer-state.js";
11
+ import { describeHmsKey, hiddenHmsKeys, hiddenHmsNotices, reportedHmsKey, validatePrinterState, manualHeatingRequirements } from "../safety/printer-state.js";
12
12
  import { withPrinterOperation, withPrintSnapshot, uniquePrintName, normalizedRemotePath, cancelPendingPrinterOperations } from "../safety/artifact.js";
13
13
  import { Client as FTPClient } from "basic-ftp";
14
14
  import { BambuPrinter } from "bambu-js";
@@ -69,7 +69,7 @@ const MODEL_ID_TO_NAME = {
69
69
  const H2_MODEL_NAMES = new Set(["h2", "h2c", "h2d", "h2dpro", "h2d pro", "h2s", "x2d"]);
70
70
  export function assertDirectPrintSupported(model, serial) {
71
71
  if (model?.trim().toLowerCase() === "x2d" || serial?.trim().toUpperCase().startsWith("20P")) {
72
- throw new Error("X2D direct printing is not supported through legacy MQTT/FTPS. Use the macOS native eMMC route via print_3mf. " +
72
+ throw new Error("X2D direct printing is not supported through legacy MQTT/FTPS. Use the native eMMC route via print_3mf (macOS, or Linux with the native helper installed). " +
73
73
  "X2D status and slicing remain available on all supported platforms.");
74
74
  }
75
75
  }
@@ -392,21 +392,37 @@ export class BambuImplementation {
392
392
  finishedJobIdentity(status) {
393
393
  return status.raw.gcode_state === "FINISH" ? JSON.stringify([status.raw.gcode_file, status.raw.subtask_name, status.raw.task_id, status.raw.subtask_id]) : undefined;
394
394
  }
395
+ /**
396
+ * The human print confirmation. Its result goes to assertBedClearance at
397
+ * every later state check of the same print, which stops the print if the
398
+ * situation the human confirmed has changed.
399
+ */
395
400
  async confirmPrintPreflight(serial, status, inspection) {
396
401
  const finishedJob = this.finishedJobIdentity(status);
397
402
  const cleared = this.clearedErrors.get(serial);
403
+ const notices = hiddenHmsNotices(status?.raw, serial, inspection.model);
398
404
  await this.confirmHardwareAction(`Start a checked print on ${inspection.model.toUpperCase()} (${serial})? Nozzles: ${inspection.nozzleDiameters.join(", ")} mm. ` +
399
405
  `Materials: ${inspection.materials.join(", ")}. Peak targets: nozzle ${inspection.maxNozzleTemperature}°C, bed ${inspection.maxBedTemperature}°C, chamber ${inspection.maxChamberTemperature}°C. ` +
400
406
  `File SHA-256: ${inspection.sha256}. Confirm the physical spool labels and that the build plate is clear.` +
401
407
  (finishedJob !== undefined ? " The printer reports FINISH: remove the previous part and debris before confirming." : "") +
402
- (cleared ? ` Previously cleared hardware codes: ${cleared.join(", ")}. Confirm their physical causes have been resolved.` : ""), finishedJob !== undefined || !!cleared);
403
- return finishedJob;
408
+ (cleared ? ` Previously cleared hardware codes: ${cleared.map(describeHmsKey).join(", ")}. Confirm their physical causes have been resolved.` : "") +
409
+ (notices.length ? ` The printer also reports ${notices.join(", ")}, which Bambu lists for this model without text and never shows as a fault; ` +
410
+ `${notices.length === 1 ? "it does" : "they do"} not block printing.` : ""), finishedJob !== undefined || !!cleared);
411
+ return { finishedJob, serial, model: inspection.model, notices };
404
412
  }
405
- assertBedClearance(status, confirmedFinishedJob) {
413
+ /** Stop a print whose confirmed situation changed: a newly finished job, or a hidden HMS notice the preflight did not name. */
414
+ assertBedClearance(status, preflight) {
406
415
  const current = this.finishedJobIdentity(status);
407
- if (current !== undefined && current !== confirmedFinishedJob) {
416
+ if (current !== undefined && current !== preflight.finishedJob) {
408
417
  throw new Error("Printer reports a newly finished job. Confirm that its part and debris have been removed before retrying the print.");
409
418
  }
419
+ // An X2D raises its hidden camera notice about 15 seconds after boot, so it
420
+ // can appear after the human confirmed. Never pass a notice nobody was told about.
421
+ const unnamed = hiddenHmsNotices(status?.raw, preflight.serial, preflight.model).filter(notice => !preflight.notices.includes(notice));
422
+ if (unnamed.length) {
423
+ throw new Error(`Printer reported ${unnamed.join(", ")} after the print confirmation. ` +
424
+ "Bambu hides it and it does not block printing, but it was not named when you confirmed; start the print again to review it.");
425
+ }
410
426
  }
411
427
  async getPrinter(host, serial, token) {
412
428
  return this.printerStore.getPrinter(host, serial, token);
@@ -822,6 +838,19 @@ export class BambuImplementation {
822
838
  throw new Error(`Failed to cancel print: ${error.message}`);
823
839
  }
824
840
  }
841
+ /** Raw MQTT stop for routes that already ran the shared cancellation steps. */
842
+ async sendStopOverMqtt(host, serial, token) {
843
+ const printer = await this.getPrinter(host, serial, token);
844
+ await invokeWithoutAck(printer, new UpdateStateCommand({ state: "stop" }));
845
+ return { status: "success", message: "Cancel command sent over MQTT." };
846
+ }
847
+ /** Raw MQTT heater-off; switching off needs no material or model declaration. */
848
+ async sendHeaterOffOverMqtt(host, serial, token, heater) {
849
+ const printer = await this.getPrinter(host, serial, token);
850
+ const gcode = `${heater === "bed" ? "M140" : "M104"} S0`;
851
+ await invokeWithoutAck(printer, new GCodeLineCommand({ gcodes: [gcode] }));
852
+ return { status: "success", message: `Heater-off sent over MQTT for ${heater}.`, command: gcode };
853
+ }
825
854
  async pauseJob(host, serial, token, dispatch) {
826
855
  const printer = dispatch ? undefined : await this.getPrinter(host, serial, token);
827
856
  try {
@@ -867,10 +896,12 @@ export class BambuImplementation {
867
896
  const codesFor = (status) => {
868
897
  if (!Array.isArray(status.raw?.hms) || status.raw?.print_error === undefined)
869
898
  throw new Error("Fresh error codes are required before clearing hardware errors.");
870
- return [`print_error:${status.raw.print_error}`, ...status.raw.hms.map((entry) => `hms:${entry.attr}:${entry.code}`)].sort();
899
+ // Hidden notices never block and survive clean_print_error, so they are not errors to clear or re-acknowledge.
900
+ const hidden = new Set(hiddenHmsKeys(status.raw, serial, String(status.model ?? "")));
901
+ return [`print_error:${status.raw.print_error}`, ...status.raw.hms.map(reportedHmsKey).filter((key) => !hidden.has(key))].sort();
871
902
  };
872
903
  const codes = codesFor(await this.getSafetyStatus(host, serial, token));
873
- await this.confirmHardwareAction(`Clear reported hardware errors on ${serial}: ${codes.join(", ")}? Inspect the printer and resolve the physical cause first. Confirm only after removing obstructions and correcting the fault.`, true);
904
+ await this.confirmHardwareAction(`Clear reported hardware errors on ${serial}: ${codes.map(describeHmsKey).join(", ")}? Inspect the printer and resolve the physical cause first. Confirm only after removing obstructions and correcting the fault.`, true);
874
905
  const beforeDispatch = async () => {
875
906
  assertActive();
876
907
  const current = codesFor(await this.getSafetyStatus(host, serial, token));
@@ -1001,11 +1032,13 @@ export class BambuImplementation {
1001
1032
  }
1002
1033
  const printer = dispatch ? undefined : await this.getPrinter(host, serial, token);
1003
1034
  assertActive();
1004
- if (dispatch)
1005
- await dispatch(heater, targetTemperature, assertActive, beforeDispatch);
1006
- else
1035
+ const dispatched = dispatch ? await dispatch(heater, targetTemperature, assertActive, beforeDispatch) : undefined;
1036
+ if (!dispatch)
1007
1037
  await invokeWithoutAck(printer, new GCodeLineCommand({ gcodes: [gcode] }));
1008
- return { status: "success", message: `Temperature command sent for ${normalizedComponent}.`, command: gcode };
1038
+ // Keep route details from a dual-route Linux heater-off.
1039
+ const routeDetails = dispatched && typeof dispatched === "object" && "routes" in dispatched
1040
+ ? { routes: dispatched.routes, native_error: dispatched.native_error, mqtt_error: dispatched.mqtt_error } : {};
1041
+ return { status: "success", message: `Temperature command sent for ${normalizedComponent}.`, command: gcode, ...routeDetails };
1009
1042
  };
1010
1043
  if (targetTemperature === 0) {
1011
1044
  cancelPendingPrinterOperations(host, serial);
@@ -0,0 +1,32 @@
1
+ export declare const HIDDEN_HMS_SOURCE: {
2
+ readonly studioTag: "v02.08.02.61";
3
+ readonly studioCommit: "926a7192574bcb9b3a732e1ec59a46d79cb45466";
4
+ /** Per table: Bambu's device_hms version and the SHA-256 of the English file used. */
5
+ readonly tables: {
6
+ readonly "093": {
7
+ readonly version: 202508191204;
8
+ readonly sha256: "7801fd6c355854b62ecb62ee32727b4e5dcca908841dbcfd5e5de45b3dfd1c25";
9
+ };
10
+ readonly "094": {
11
+ readonly version: 202503231214;
12
+ readonly sha256: "047c15e2140738ec08258b74536e6cb996cddf0c586c5c39e2318e54125a9e76";
13
+ };
14
+ readonly "20P": {
15
+ readonly version: 202511162200;
16
+ readonly sha256: "e4f067d027b9bcbb9bbf6cb90f41f11130cf4d83dffd3dee101db7826dc808c3";
17
+ };
18
+ readonly "22E": {
19
+ readonly version: 202510142200;
20
+ readonly sha256: "87426021aea2bd4e8d721e46e1c86808fd7c6fc06f52baf6d952ead9e8d14d1e";
21
+ };
22
+ readonly "239": {
23
+ readonly version: 202506171403;
24
+ readonly sha256: "c1d23d5302756d57df5cb9bc795d95fc54ef1db7565dca1f04b69a024886426a";
25
+ };
26
+ readonly "31B": {
27
+ readonly version: 202511162200;
28
+ readonly sha256: "dcf63657bff5451f0715ea3c032efe482279bc8cbc28cb9b8ae51c7354826574";
29
+ };
30
+ };
31
+ };
32
+ export declare const HIDDEN_HMS: Readonly<Record<string, readonly string[]>>;