bambu-printer-mcp 1.1.28 → 1.1.30

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/CONTRIBUTORS.md CHANGED
@@ -6,12 +6,14 @@ Thank you to everyone who builds, tests, reports problems, and shares real print
6
6
 
7
7
  | Contributor | Contribution |
8
8
  | --- | --- |
9
- | [mygrexit](https://github.com/mygrexit) | Contributes pinned x86-64 Linux X2D native setup, emergency routing and one-shot cleanup in [#56](https://github.com/DMontgomery40/bambu-printer-mcp/pull/56), checked Orca/time-lapse G-code forms in [#57](https://github.com/DMontgomery40/bambu-printer-mcp/pull/57), and model-specific hidden-HMS notices in [#58](https://github.com/DMontgomery40/bambu-printer-mcp/pull/58). Supplies regressions, review corrections, and contributor-reported Debian/X2D read-only, heater-off and print-start evidence in [#55](https://github.com/DMontgomery40/bambu-printer-mcp/issues/55); maintainer validation is offline and mocked. |
9
+ | [mygrexit](https://github.com/mygrexit) | Contributes pinned x86-64 Linux X2D native setup, emergency routing and one-shot cleanup in [#56](https://github.com/DMontgomery40/bambu-printer-mcp/pull/56), checked Orca/time-lapse G-code forms in [#57](https://github.com/DMontgomery40/bambu-printer-mcp/pull/57), and model-specific hidden-HMS notices in [#58](https://github.com/DMontgomery40/bambu-printer-mcp/pull/58). Supplies regressions, review corrections, and contributor-reported Debian/X2D read-only, heater-off and print-start evidence in [#55](https://github.com/DMontgomery40/bambu-printer-mcp/issues/55); maintainer validation is offline and mocked. Also reports and fixes hanging Linux X2D stop/heater-off calls in [#61](https://github.com/DMontgomery40/bambu-printer-mcp/pull/61), with never-connecting transport and helper-exit regressions; the release adds protection against expired commands dispatching after reconnection. |
10
10
  | [patrikmadoran-cyber](https://github.com/patrikmadoran-cyber) | Adds explicit Standard/High Flow nozzle selection across slicing and print auto-slicing in [#54](https://github.com/DMontgomery40/bambu-printer-mcp/pull/54), with unsupported-variant regressions and real Windows BambuStudio 02.06.00.51 H2S 0.6 slicing evidence. No physical print is claimed. |
11
11
  | [Patrick Juchli (@patrickjuchli)](https://github.com/patrickjuchli) | Maintains basic-ftp and supplies the 6.2.3 fixes for rejected-command recovery, asynchronous credential-error crashes, and password redaction used by direct FTPS transfers. |
12
12
  | [Aleksei Popovich (alexpapay)](https://github.com/alexpapay) | Fixes cross-platform AMS profile resolution and packed P2S chamber status in [#43](https://github.com/DMontgomery40/bambu-printer-mcp/pull/43), contributes regressions, and supplies read-only Windows P2S/AMS 2 Pro status and local slicing evidence. No physical print is claimed for this contribution. |
13
13
  | [Adam (@adamkaplan)](https://github.com/adamkaplan) | Identifies the reversed H2C/H2D Pro serial prefixes in [#47](https://github.com/DMontgomery40/bambu-printer-mcp/issues/47), verifies corrected H2C status locally, and isolates the official H2C startup commands and config-block marker failure in [#48](https://github.com/DMontgomery40/bambu-printer-mcp/issues/48). Provides exact Bambu Studio version/template hash and unsafe-file reproductions; maintainer validation is offline and mocked. |
14
- | [travismcashan](https://github.com/travismcashan) | Reports stock X2D material-switch and runout-purge rejection, misleading unchecked confirmation, and stdout diagnostics in [#44](https://github.com/DMontgomery40/bambu-printer-mcp/issues/44). Supplies exact preset syntax and firmware evidence, then reports a clean physical X2D cube print with a local workaround; maintainer validation remains isolated and mocked. Reports the accepted native job and subsequent helper teardown crash in [#46](https://github.com/DMontgomery40/bambu-printer-mcp/issues/46), with completed-print and macOS crash evidence informing result preservation and duplicate-print warnings. |
14
+ | [Pavel Bukhatkin (welderpb)](https://github.com/welderpb) | Requests remote API slicing for containerized deployments and specifies authentication, timeout and async needs in [#50](https://github.com/DMontgomery40/bambu-printer-mcp/issues/50). This informs the optional single-slot Orca API backend; validation uses HTTP fixtures and the genuine API application with a fake CLI, without a physical print. |
15
+ | [AFKFelix](https://github.com/AFKFelix) and [orca-slicer-api contributors](https://github.com/AFKFelix/orca-slicer-api) | Provide the upstream HTTP slicing application and published multipart/job contracts used by the optional remote backend. |
16
+ | [travismcashan](https://github.com/travismcashan) | Reports stock X2D material-switch and runout-purge rejection, misleading unchecked confirmation, and stdout diagnostics in [#44](https://github.com/DMontgomery40/bambu-printer-mcp/issues/44). Supplies exact preset syntax and firmware evidence, then reports a clean physical X2D cube print with a local workaround; maintainer validation remains isolated and mocked. Reports the accepted native job and subsequent helper teardown crash in [#46](https://github.com/DMontgomery40/bambu-printer-mcp/issues/46), with completed-print and macOS crash evidence informing result preservation and duplicate-print warnings. Contributes the X2D dual-nozzle slicer-output and dual-external-spool fix in [#59](https://github.com/DMontgomery40/bambu-printer-mcp/pull/59), with a real five-filament X2D job, live `vir_slot` evidence and the Bambu Studio virtual-tray conventions; the change is validated offline and mocked, and no physical print is claimed for it. |
15
17
  | [nitpreet22](https://github.com/nitpreet22) | Fixes rejection of the stock A1/A1 mini `M109 H` startup waits in [#41](https://github.com/DMontgomery40/bambu-printer-mcp/pull/41), contributes temperature and model regressions, and supplies Windows Bambu Studio profile and real sliced A1 job inspection evidence. Their physical print used Bambu Studio, not this MCP change. |
16
18
  | [Steavie (steavie)](https://github.com/steavie) | Reports the reversed A1/A1 mini serial prefixes in [#39](https://github.com/DMontgomery40/bambu-printer-mcp/issues/39), with A1 status and configuration evidence that identifies the status and fresh-identity regression. |
17
19
  | [JaviOFC](https://github.com/JaviOFC) | Reports the X2D identity rejection in [#36](https://github.com/DMontgomery40/bambu-printer-mcp/issues/36) and supplies current firmware, connection-mode, status-field, serial-prefix, and cloud-job evidence showing that `model_id` can contain a print-job identifier while `model` correctly identifies the printer. |
package/README.md CHANGED
@@ -210,7 +210,7 @@ The optional FULU **BambuNetwork bridge** exposes `bambu_network_bridge_status`,
210
210
  - Start G-code files already stored on the printer
211
211
  - **Collar charm print wrapper** (`print_collar_charm`) — specialized two-color workflow with fixed tray policy for inner (black, AMS 1 slot 1) and outer (white, AMS 2 slot 1) charm parts
212
212
  - STL manipulation: scale, rotate, extend base, merge vertices, center at origin, lay flat, and inspect model info
213
- - Slice STL or 3MF files using an external CLI. BambuStudio, FULU, and Orca require the exact machine preset and resolve BBL dependencies before slicing; see [FULU/Orca CLI setup and validation limits](https://github.com/DMontgomery40/bambu-printer-mcp/blob/main/docs/FULU.md#fulu-and-orca-cli-safety-limit).
213
+ - Slice STL or 3MF files using an external CLI, or use the optional [`orca-slicer-api` backend](https://github.com/DMontgomery40/bambu-printer-mcp/blob/main/docs/SLICING.md) in a separate container without a local slicer executable. Remote slicing requires a matching read-only Orca profile tree and currently supports one filament slot and one selected plate. BambuStudio, FULU, and Orca require the exact machine preset and resolve BBL dependencies before slicing; see [FULU/Orca CLI setup and validation limits](https://github.com/DMontgomery40/bambu-printer-mcp/blob/main/docs/FULU.md#fulu-and-orca-cli-safety-limit).
214
214
  - Inspect slicer settings from a saved 3MF template or extracted profile via `get_slice_settings`
215
215
  - Enumerate saved slicing templates from the local registry via `list_templates`
216
216
  - Save templates into the local registry via `save_template`
@@ -279,6 +279,8 @@ If you are using the direct-feed spool holder (no AMS attached) or want to bypas
279
279
 
280
280
  For H2 projects with declared filaments, also provide the required mapping; `use_ams: false` alone does not remove the firmware's mapping requirement. See [`print_3mf`](#print_3mf).
281
281
 
282
+ Dual-nozzle printers (H2D, H2D Pro, H2C, X2D) have two external spool holders, and the printer reports one virtual slot per nozzle. Use Bambu Studio's ids in `ams_slots`/`ams_mapping`: `254` is the left/deputy nozzle's external spool and `255` the right/main nozzle's. The server composes the selected plate's `filament_map` with `physical_extruder_map` to verify each filament's physical nozzle (`0` = right, `1` = left), and requires the matching fresh `vir_slot` report. An explicit external-spool mapping is preserved with `use_ams: false`; a missing mapping is derived only from these inspected headers. Single-nozzle printers keep `254` as their only external tray.
283
+
282
284
  ### Auto-match AMS by RFID
283
285
 
284
286
  For pre-sliced 3MFs that declare filament types, the `auto_match_ams` flag on `print_3mf` (or the standalone `resolve_3mf_ams_slots` dry-run tool) automatically resolves the required filaments against your live AMS inventory. The matcher works as follows:
@@ -948,7 +950,7 @@ Install Bambu Studio and its networking plug-in, plus the Apple command-line dev
948
950
 
949
951
  The npm package and desktop bundle include `native/bambu-native-print.cpp`, `scripts/build-bambu-native.sh` and the Linux setup script, never a developer's compiled binary. The server finds the built helper relative to its installed package, regardless of the launch directory. A trusted server setting `BAMBU_NATIVE_HELPER` can select an alternate executable.
950
952
 
951
- For X2D, `print_3mf` defaults to `connection_mode: "bambu_native"`; legacy `lan_mqtt_ftps` requests are redirected to it. Supply `ams_slots`, a complete project-level `ams_mapping`, or `auto_match_ams: true`. An external-spool job requires explicit `use_ams: false`. Raw `ams_mapping2` and nozzle/extra-option overrides are rejected before dispatch; the server derives both AMS representations from the checked structured mapping. Model/nozzle/material inspection, fresh printer-state checks, and human preflight apply before the helper receives a private snapshot. The native helper requests fresh shared printer-state authorization immediately before each print submission, including a certificate retry, and before checked heating, resume, or error-clearing commands; custom helpers must support this handshake. Stop and heater-off remain available without preflight. Native upload-only requests use the same all-plate inspection as FTPS uploads. They accept `project_name`, `preset_name`, `bed_type`, and an existing `plate_index`; AMS, nozzle, and calibration print options are rejected. Use `print_3mf` for checked print settings.
953
+ For X2D, `print_3mf` defaults to `connection_mode: "bambu_native"`; legacy `lan_mqtt_ftps` requests are redirected to it. Supply `ams_slots`, a complete project-level `ams_mapping`, or `auto_match_ams: true`. A job that uses only one external spool requires explicit `use_ams: false`; a job mixing AMS trays with the external spools maps them as `254` (left/deputy nozzle) and `255` (right/main nozzle). Raw `ams_mapping2` and nozzle/extra-option overrides are rejected before dispatch; the server derives both AMS representations from the checked structured mapping. Model/nozzle/material inspection, fresh printer-state checks, and human preflight apply before the helper receives a private snapshot. The native helper requests fresh shared printer-state authorization immediately before each print submission, including a certificate retry, and before checked heating, resume, or error-clearing commands; custom helpers must support this handshake. Stop and heater-off remain available without preflight. Native upload-only requests use the same all-plate inspection as FTPS uploads. They accept `project_name`, `preset_name`, `bed_type`, and an existing `plate_index`; AMS, nozzle, and calibration print options are rejected. Use `print_3mf` for checked print settings.
952
954
 
953
955
  The helper waits for connection/certificate exchange and retries only the initial `-4030` send once. Rebuild the helper after upgrading to receive the teardown fix. A successful dispatch receipt ends the helper before plug-in cleanup can turn an accepted job into an error. If an older/custom helper crashes after dispatch, the server reports uncertainty: check the printer state and job name before retrying, because resending can duplicate a print. Previous hardware testing reached `RUNNING` on X2D; this revision is validated with mocked transport regressions and clean installs, not a new physical print. `bambu_connect` is an optional macOS handoff for user review in Bambu Connect and does not start a print. Native task, heater, and error controls retain the common safety checks; raw controls cannot bypass them. Request cancellation, stop, and heater-off interrupt pending native helpers; the server waits for process exit before releasing a checked file. A command already sent to the printer cannot be recalled by cancelling the request, so verify printer state before retrying.
954
956
 
@@ -964,7 +966,7 @@ Use the same package version as the server, for example `bambu-printer-mcp@1.2.3
964
966
 
965
967
  `--check-update` asks Bambu's plug-in service for the newest Linux plug-in and compares the `PrintParams` layout in the matching Bambu Studio header with the helper. It reports only: the setup never installs an unpinned plug-in, because a changed layout would send wrong print parameters. A pin is raised by a package release after the check passes and a hardware test.
966
968
 
967
- Once the helper, plug-in and certificates are installed, X2D printing, upload and controls use the same routing, safety checks and authorization handshake described for macOS above. On Linux, stop and heater-off go out over the native route and MQTT at the same time, so a stuck helper cannot delay them. MQTT is sent again once every older print or heating helper for that printer has exited, so none can dispatch after it; either the native route or that later MQTT send succeeding counts, and the result lists all sends under `routes`. A repeated stop or heater-off is harmless. macOS keeps its native-only route. The server accepts only the pinned plug-in checksums; `BAMBU_NATIVE_ALLOW_UNPINNED_PLUGIN=1` is a trusted override for testing another build at your own risk. Without a complete installation, Linux keeps its previous behavior: X2D status and slicing work, stop, pause and heater-off use MQTT, and native print requests name the setup command before contacting the printer. Bambu publishes the Linux plug-in for x86-64 only; other architectures keep the MQTT behavior.
969
+ Once the helper, plug-in and certificates are installed, X2D printing, upload and controls use the same routing, safety checks and authorization handshake described for macOS above. On Linux, stop and heater-off go out over the native route and MQTT at the same time, so a stuck helper cannot delay them. MQTT is sent again once every older print or heating helper for that printer has exited and the immediate MQTT attempt has settled, so none can dispatch after it; either the native route or that later MQTT send succeeding counts, and the result lists all sends under `routes`. Each MQTT send gives up after 15 seconds (`BAMBU_EMERGENCY_MQTT_TIMEOUT_MS`), so an unreachable MQTT connection cannot hold back a stop the native route already sent. A connection completing after that deadline cannot dispatch its expired command; an already queued MQTT publish is discarded by force-closing and invalidating that client. A repeated stop or heater-off is harmless. macOS keeps its native-only route. The server accepts only the pinned plug-in checksums; `BAMBU_NATIVE_ALLOW_UNPINNED_PLUGIN=1` is a trusted override for testing another build at your own risk. Without a complete installation, Linux keeps its previous behavior: X2D status and slicing work, stop, pause and heater-off use MQTT, and native print requests name the setup command before contacting the printer. Bambu publishes the Linux plug-in for x86-64 only; other architectures keep the MQTT behavior.
968
970
 
969
971
  #### x2d_native_control
970
972
 
@@ -1,3 +1,19 @@
1
- export declare function normalizeBridgeAmsTrayValue(value: unknown, label: string): number;
2
- export declare function normalizeAmsMappingObject(mapping: Record<string, unknown>, label?: string): number[];
1
+ /** Tray-value options. `dualNozzle` admits 255, the right/main nozzle external spool that
2
+ * dual-nozzle printers report beside the left/deputy external spool 254 (Bambu Studio
3
+ * VIRTUAL_TRAY_MAIN_ID / VIRTUAL_TRAY_DEPUTY_ID). Single-nozzle printers keep 254 only. */
4
+ export interface AmsTrayValueOptions {
5
+ dualNozzle?: boolean;
6
+ }
7
+ export declare function describeAmsTrayRange(options?: AmsTrayValueOptions): string;
8
+ export declare function normalizeBridgeAmsTrayValue(value: unknown, label: string, options?: AmsTrayValueOptions): number;
9
+ export declare function normalizeAmsMappingObject(mapping: Record<string, unknown>, label?: string, options?: AmsTrayValueOptions): number[];
3
10
  export declare function hasAmsMappingInput(value: unknown): boolean;
11
+ /** Flat wire entries use -1 for dual-nozzle virtual trays; ams_mapping2 identifies their spool. */
12
+ export declare function bridgeAmsMappingValue(value: number, dualNozzle?: boolean): number;
13
+ /** Select an external spool only from a verified physical nozzle on dual-nozzle printers. */
14
+ export declare function externalTrayForNozzle(nozzle: number | undefined, dualNozzle?: boolean): number;
15
+ /** Virtual trays use slot zero on dual-nozzle printers; retain the historical single-nozzle form. */
16
+ export declare function bridgeAmsMapping2Entry(value: number, dualNozzle?: boolean): {
17
+ ams_id: number;
18
+ slot_id: number;
19
+ };
@@ -1,4 +1,9 @@
1
- export function normalizeBridgeAmsTrayValue(value, label) {
1
+ export function describeAmsTrayRange(options = {}) {
2
+ return options.dualNozzle
3
+ ? "[-1, 15] (absolute tray), 128-253 (AMS-HT), 254 (left/deputy external) or 255 (right/main external)"
4
+ : "[-1, 15] (absolute tray) or 128-254 (HT/external)";
5
+ }
6
+ export function normalizeBridgeAmsTrayValue(value, label, options = {}) {
2
7
  let numeric;
3
8
  if (typeof value === "number") {
4
9
  numeric = value;
@@ -12,8 +17,8 @@ export function normalizeBridgeAmsTrayValue(value, label) {
12
17
  if (!Number.isInteger(numeric) ||
13
18
  numeric < -1 ||
14
19
  (numeric > 15 && numeric < 128) ||
15
- numeric > 254) {
16
- throw new Error(`${label} must be an integer in [-1, 15] (absolute tray) or 128-254 (HT/external); got ${JSON.stringify(value)}`);
20
+ numeric > (options.dualNozzle ? 255 : 254)) {
21
+ throw new Error(`${label} must be an integer in ${describeAmsTrayRange(options)}; got ${JSON.stringify(value)}`);
17
22
  }
18
23
  return numeric;
19
24
  }
@@ -21,7 +26,7 @@ function isNumericMappingKey(key) {
21
26
  const numeric = Number(key);
22
27
  return Number.isInteger(numeric) && numeric >= 0;
23
28
  }
24
- export function normalizeAmsMappingObject(mapping, label = "ams_mapping") {
29
+ export function normalizeAmsMappingObject(mapping, label = "ams_mapping", options = {}) {
25
30
  return Object.entries(mapping)
26
31
  .sort(([leftKey], [rightKey]) => {
27
32
  const leftNumeric = isNumericMappingKey(leftKey);
@@ -34,7 +39,7 @@ export function normalizeAmsMappingObject(mapping, label = "ams_mapping") {
34
39
  return 1;
35
40
  return 0;
36
41
  })
37
- .map(([key, value]) => normalizeBridgeAmsTrayValue(value, `${label}[${key}]`));
42
+ .map(([key, value]) => normalizeBridgeAmsTrayValue(value, `${label}[${key}]`, options));
38
43
  }
39
44
  export function hasAmsMappingInput(value) {
40
45
  if (Array.isArray(value))
@@ -43,3 +48,29 @@ export function hasAmsMappingInput(value) {
43
48
  return Object.keys(value).length > 0;
44
49
  return value !== undefined && value !== null;
45
50
  }
51
+ /** Flat wire entries use -1 for dual-nozzle virtual trays; ams_mapping2 identifies their spool. */
52
+ export function bridgeAmsMappingValue(value, dualNozzle = false) {
53
+ return dualNozzle && (value === 254 || value === 255) ? -1 : value;
54
+ }
55
+ /** Select an external spool only from a verified physical nozzle on dual-nozzle printers. */
56
+ export function externalTrayForNozzle(nozzle, dualNozzle = false) {
57
+ if (!dualNozzle)
58
+ return 254;
59
+ if (nozzle === 0)
60
+ return 255;
61
+ if (nozzle === 1)
62
+ return 254;
63
+ throw new Error("External-spool printing requires inspected filament_map and physical_extruder_map nozzle bindings.");
64
+ }
65
+ /** Virtual trays use slot zero on dual-nozzle printers; retain the historical single-nozzle form. */
66
+ export function bridgeAmsMapping2Entry(value, dualNozzle = false) {
67
+ if (value < 0)
68
+ return { ams_id: 255, slot_id: 255 };
69
+ if (value === 255)
70
+ return dualNozzle ? { ams_id: 255, slot_id: 0 } : { ams_id: 255, slot_id: 255 };
71
+ if (value === 254)
72
+ return { ams_id: 254, slot_id: dualNozzle ? 0 : 254 };
73
+ if (value >= 128)
74
+ return { ams_id: 128, slot_id: value - 128 };
75
+ return { ams_id: Math.floor(value / 4), slot_id: value % 4 };
76
+ }
package/dist/index.js CHANGED
@@ -14,11 +14,12 @@ import { BambuNetworkBridge } from "./bambu-network-bridge.js";
14
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
- import { hasAmsMappingInput, normalizeAmsMappingObject, normalizeBridgeAmsTrayValue } from "./ams-mapping.js";
17
+ import { bridgeAmsMapping2Entry, bridgeAmsMappingValue, externalTrayForNozzle, hasAmsMappingInput, normalizeAmsMappingObject, normalizeBridgeAmsTrayValue } from "./ams-mapping.js";
18
18
  import { analyze3MFAmsRequirements, analyze3MFPlateObjects, analyzeCollarCharm3MF, extractBambuTemplateSettings, getCollarCharmRolePolicy, parse3MF } from './3mf_parser.js';
19
19
  import { BambuImplementation, assertDirectPrintSupported } from "./printers/bambu.js";
20
20
  import { inspectPrintFile } from "./safety/print-file.js";
21
21
  import { validatePrinterState } from "./safety/printer-state.js";
22
+ import { isDualNozzleModel } from "./safety/limits.js";
22
23
  import { withPrinterOperation, withPrintSnapshot, uniquePrintName } from "./safety/artifact.js";
23
24
  import { detectProfilesRoot } from "./slicer/profile-flatten.js";
24
25
  dotenv.config();
@@ -51,6 +52,21 @@ const DEFAULT_BAMBU_MODEL = process.env.BAMBU_PRINTER_MODEL?.trim().toLowerCase(
51
52
  const DEFAULT_BAMBU_CONNECTION_MODE = process.env.BAMBU_DEFAULT_CONNECTION_MODE?.trim().toLowerCase() || "";
52
53
  const DEFAULT_BED_TYPE = process.env.BED_TYPE?.trim().toLowerCase() || "textured_plate";
53
54
  const DEFAULT_NOZZLE_DIAMETER = process.env.NOZZLE_DIAMETER?.trim() || "0.4";
55
+ /** Bound for each Linux X2D emergency MQTT send (BAMBU_EMERGENCY_MQTT_TIMEOUT_MS, 1–60000 ms). */
56
+ function withEmergencyMqttTimeout(send) {
57
+ const raw = Number(process.env.BAMBU_EMERGENCY_MQTT_TIMEOUT_MS?.trim() || 15000);
58
+ const ms = Number.isInteger(raw) && raw >= 1 && raw <= 60000 ? raw : 15000;
59
+ const controller = new AbortController();
60
+ let timer;
61
+ const timeout = new Promise((_, reject) => {
62
+ timer = setTimeout(() => {
63
+ const error = new Error(`no MQTT confirmation within ${ms / 1000} s`);
64
+ controller.abort(error);
65
+ reject(error);
66
+ }, ms);
67
+ });
68
+ return Promise.race([Promise.resolve().then(() => send(controller.signal)), timeout]).finally(() => clearTimeout(timer));
69
+ }
54
70
  function requestedPrintNozzles(args) {
55
71
  if (args.nozzle_diameters !== undefined && args.nozzle_diameter !== undefined) {
56
72
  throw new Error("Supply nozzle_diameters or nozzle_diameter, not both.");
@@ -523,7 +539,7 @@ function defaultSlicerPathFor(slicerType) {
523
539
  }
524
540
  function resolveSlicerConfig(args) {
525
541
  const slicerType = normalizeSlicerType(String(args?.slicer_type || DEFAULT_SLICER_TYPE));
526
- const slicerPath = String(args?.slicer_path || process.env.SLICER_PATH || defaultSlicerPathFor(slicerType));
542
+ const slicerPath = slicerType === "orca-slicer-api" ? "" : String(args?.slicer_path || process.env.SLICER_PATH || defaultSlicerPathFor(slicerType));
527
543
  const slicerProfile = String(args?.slicer_profile || DEFAULT_SLICER_PROFILE);
528
544
  return { slicerType, slicerPath, slicerProfile };
529
545
  }
@@ -735,18 +751,10 @@ function stringifyBridgeJson(value) {
735
751
  return value;
736
752
  return JSON.stringify(value);
737
753
  }
738
- function nativeAmsMapping2(mapping) {
754
+ function nativeAmsMapping2(mapping, dualNozzle = false) {
739
755
  if (!mapping || mapping.length === 0)
740
756
  return undefined;
741
- return JSON.stringify(mapping.map((value) => {
742
- if (value < 0 || value === 255)
743
- return { ams_id: 255, slot_id: 255 };
744
- if (value === 254)
745
- return { ams_id: 254, slot_id: 254 };
746
- if (value >= 128)
747
- return { ams_id: 128, slot_id: value - 128 };
748
- return { ams_id: Math.floor(value / 4), slot_id: value % 4 };
749
- }));
757
+ return JSON.stringify(mapping.map((value) => bridgeAmsMapping2Entry(value, dualNozzle)));
750
758
  }
751
759
  function redactPrintParams(params) {
752
760
  return {
@@ -1025,6 +1033,9 @@ class BambuPrinterMCPServer {
1025
1033
  resolveSlicerConfigFromArgs(args, toolName) {
1026
1034
  const config = resolveSlicerConfig(args);
1027
1035
  if (args?.slicer_path !== undefined) {
1036
+ if (config.slicerType === "orca-slicer-api") {
1037
+ throw new Error(`${toolName}: remote slicing uses trusted SLICER_API_URL and BAMBU_PROFILES_ROOT configuration; slicer_path is unsupported.`);
1038
+ }
1028
1039
  const resolvedSlicerPath = this.resolveExecutableSelectorArg(args.slicer_path, toolName, "slicer_path");
1029
1040
  if (resolvedSlicerPath !== undefined) {
1030
1041
  config.slicerPath = resolvedSlicerPath;
@@ -1048,10 +1059,10 @@ class BambuPrinterMCPServer {
1048
1059
  }
1049
1060
  console.error(`3MF has no gcode - auto-slicing with ${slicerType} for ${printModel}`);
1050
1061
  const autoSliceOptions = {
1051
- uptodate: true,
1052
- ensureOnBed: true,
1053
- minSave: true,
1054
- skipModifiedGcodes: true,
1062
+ ...(slicerType === "orca-slicer-api" ? {} : {
1063
+ uptodate: true, ensureOnBed: true, minSave: true, skipModifiedGcodes: true,
1064
+ }),
1065
+ ...(slicerType === "orca-slicer-api" ? { slicePlate: Number(args.plate_index ?? 0) + 1 } : {}),
1055
1066
  bedType: bedType ? resolveBambuStudioCliBedType(bedType) : undefined,
1056
1067
  nozzleType: resolveNozzleType(args?.nozzle_type),
1057
1068
  nozzleVolumeType: resolveNozzleVolumeType(args?.nozzle_volume_type),
@@ -1063,8 +1074,9 @@ class BambuPrinterMCPServer {
1063
1074
  async resolveAmsPrintSettings(threeMFPath, args, host, bambuSerial, bambuToken, printModel, printNozzle) {
1064
1075
  const parsed3MFData = await parse3MF(threeMFPath);
1065
1076
  let parsedAmsMapping;
1077
+ const trayOptions = { dualNozzle: isDualNozzleModel(printModel) };
1066
1078
  if (parsed3MFData.slicerConfig?.ams_mapping) {
1067
- const slots = normalizeAmsMappingObject(parsed3MFData.slicerConfig.ams_mapping);
1079
+ const slots = normalizeAmsMappingObject(parsed3MFData.slicerConfig.ams_mapping, "ams_mapping", trayOptions);
1068
1080
  if (slots.length > 0) {
1069
1081
  parsedAmsMapping = slots;
1070
1082
  }
@@ -1077,10 +1089,10 @@ class BambuPrinterMCPServer {
1077
1089
  if (hasUserAmsMapping) {
1078
1090
  let userMappingOverride;
1079
1091
  if (Array.isArray(args.ams_mapping)) {
1080
- userMappingOverride = args.ams_mapping.map((v, i) => normalizeBridgeAmsTrayValue(v, `ams_mapping[${i}]`));
1092
+ userMappingOverride = args.ams_mapping.map((v, i) => normalizeBridgeAmsTrayValue(v, `ams_mapping[${i}]`, trayOptions));
1081
1093
  }
1082
1094
  else if (args.ams_mapping && typeof args.ams_mapping === 'object') {
1083
- userMappingOverride = normalizeAmsMappingObject(args.ams_mapping);
1095
+ userMappingOverride = normalizeAmsMappingObject(args.ams_mapping, "ams_mapping", trayOptions);
1084
1096
  }
1085
1097
  if (userMappingOverride && userMappingOverride.length > 0) {
1086
1098
  finalAmsMapping = userMappingOverride;
@@ -1089,7 +1101,7 @@ class BambuPrinterMCPServer {
1089
1101
  }
1090
1102
  }
1091
1103
  if (!hasUserAmsMapping && hasUserAmsSlots) {
1092
- const userSlots = args.ams_slots.map((slot, i) => normalizeBridgeAmsTrayValue(slot, `ams_slots[${i}]`));
1104
+ const userSlots = args.ams_slots.map((slot, i) => normalizeBridgeAmsTrayValue(slot, `ams_slots[${i}]`, trayOptions));
1093
1105
  finalAmsSlots = userSlots;
1094
1106
  finalAmsMapping = undefined;
1095
1107
  useAMS = userSlots.length > 0;
@@ -1106,8 +1118,10 @@ class BambuPrinterMCPServer {
1106
1118
  useAMS = true;
1107
1119
  }
1108
1120
  if (args?.use_ams === false) {
1109
- finalAmsMapping = undefined;
1110
- finalAmsSlots = undefined;
1121
+ if (!hasUserAmsMapping)
1122
+ finalAmsMapping = undefined;
1123
+ if (!hasUserAmsSlots)
1124
+ finalAmsSlots = undefined;
1111
1125
  useAMS = false;
1112
1126
  }
1113
1127
  if ((!finalAmsMapping || finalAmsMapping.length === 0) && (!finalAmsSlots || finalAmsSlots.length === 0)) {
@@ -1177,8 +1191,16 @@ class BambuPrinterMCPServer {
1177
1191
  inspection.usedFilamentPositions.forEach((position, index) => { mapping[position] = finalAmsSlots[index]; });
1178
1192
  }
1179
1193
  if (!useAMS) {
1180
- mapping = Array(inspection.materials.length).fill(-1);
1181
- inspection.usedFilamentPositions.forEach(position => { mapping[position] = 254; });
1194
+ mapping ?? (mapping = Array(inspection.materials.length).fill(-1));
1195
+ const dualNozzle = isDualNozzleModel(printModel);
1196
+ for (const position of inspection.usedFilamentPositions) {
1197
+ if (mapping[position] === undefined || mapping[position] < 0) {
1198
+ mapping[position] = externalTrayForNozzle(inspection.filamentNozzleIndices?.[position], dualNozzle);
1199
+ }
1200
+ if (mapping[position] !== 254 && !(dualNozzle && mapping[position] === 255)) {
1201
+ throw new Error("use_ams:false requires an external-spool mapping for every used filament.");
1202
+ }
1203
+ }
1182
1204
  }
1183
1205
  const requirements = { ...inspection, amsMapping: mapping, useAMS };
1184
1206
  const initialStatus = await this.bambu.getSafetyStatus(devIp, devId, password || bambuToken);
@@ -1187,7 +1209,7 @@ class BambuPrinterMCPServer {
1187
1209
  const confirmedStatus = await this.bambu.getSafetyStatus(devIp, devId, password || bambuToken);
1188
1210
  validatePrinterState(confirmedStatus, requirements);
1189
1211
  this.bambu.assertBedClearance(confirmedStatus, bedClearance);
1190
- const amsMapping = stringifyBridgeJson(mapping);
1212
+ const amsMapping = stringifyBridgeJson(mapping?.map(value => bridgeAmsMappingValue(value, isDualNozzleModel(printModel))));
1191
1213
  const remoteFileName = uniquePrintName(threeMFPath);
1192
1214
  const params = {
1193
1215
  dev_id: devId,
@@ -1204,7 +1226,7 @@ class BambuPrinterMCPServer {
1204
1226
  ftp_file_md5: String(args?.ftp_file_md5 || ""),
1205
1227
  nozzle_mapping: stringifyBridgeJson(args?.nozzle_mapping) || "",
1206
1228
  ams_mapping: amsMapping || "",
1207
- ams_mapping2: stringifyBridgeJson(args?.ams_mapping2) || "",
1229
+ ams_mapping2: nativeAmsMapping2(mapping, isDualNozzleModel(printModel)) || "",
1208
1230
  ams_mapping_info: stringifyBridgeJson(args?.ams_mapping_info) || "",
1209
1231
  nozzles_info: stringifyBridgeJson(args?.nozzles_info) || "",
1210
1232
  connection_type: connectionType,
@@ -1244,7 +1266,7 @@ class BambuPrinterMCPServer {
1244
1266
  host: devIp, serial: devId, token: password || bambuToken, filePath: snapshot,
1245
1267
  projectName: String(params.task_name), presetName, remoteName: remoteFileName,
1246
1268
  plateIndex, bedType: printBedType, useAMS,
1247
- amsMapping, amsMapping2: nativeAmsMapping2(mapping),
1269
+ amsMapping, amsMapping2: nativeAmsMapping2(mapping, isDualNozzleModel(printModel)),
1248
1270
  bedLeveling: params.task_bed_leveling,
1249
1271
  flowCalibration: params.task_flow_cali,
1250
1272
  vibrationCalibration: params.task_vibration_cali,
@@ -1447,10 +1469,9 @@ class BambuPrinterMCPServer {
1447
1469
  throw new Error('BambuStudio CLI SuperTack bed type is not verified; use a pre-sliced 3MF for SuperTack or choose textured_plate, cool_plate, engineering_plate, or hot_plate.');
1448
1470
  }
1449
1471
  const autoSliceOptions = {
1450
- uptodate: true,
1451
- ensureOnBed: true,
1452
- minSave: true,
1453
- skipModifiedGcodes: true,
1472
+ ...(slicerType === "orca-slicer-api" ? {} : {
1473
+ uptodate: true, ensureOnBed: true, minSave: true, skipModifiedGcodes: true,
1474
+ }),
1454
1475
  bedType,
1455
1476
  };
1456
1477
  try {
@@ -1943,7 +1964,7 @@ class BambuPrinterMCPServer {
1943
1964
  rotate_y: { type: "number", description: "Rotate the model around the Y-axis by this many degrees before slicing. Useful for reorienting prints for better layer adhesion." },
1944
1965
  min_save: { type: "boolean", description: "Write a smaller output 3MF by omitting non-essential metadata. Reduces file size for faster FTP upload to the printer." },
1945
1966
  skip_modified_gcodes: { type: "boolean", description: "Strip custom start/end gcodes embedded in the 3MF. Recommended for downloaded 3MFs since custom gcodes from other users' profiles may be unsafe for your printer." },
1946
- slice_plate: { type: "number", description: "Which plate index to slice. 0 = all plates (default). Use 1, 2, etc. to slice only a specific plate in multi-plate 3MF projects." }
1967
+ slice_plate: { type: "number", description: "Which plate index to slice. 0 = all plates for local slicers; remote slicing defaults to plate 1 and rejects 0. Use 1, 2, etc. to slice only a specific plate in multi-plate 3MF projects." }
1947
1968
  },
1948
1969
  required: ["stl_path", "bambu_model"]
1949
1970
  }
@@ -2046,8 +2067,8 @@ class BambuPrinterMCPServer {
2046
2067
  nozzle_diameter: { type: "string", description: "Nozzle diameter in mm for auto-slicing (default: 0.4)." },
2047
2068
  nozzle_diameters: { type: "array", minItems: 1, maxItems: 2, items: { type: "number", enum: [0.2, 0.4, 0.6, 0.8] }, description: "Complete per-nozzle diameters for pre-sliced jobs, e.g. [0.4, 0.6]. Omit to verify file metadata against every reported nozzle; cannot combine with nozzle_diameter." },
2048
2069
  use_ams: { type: "boolean", description: "Whether to use the AMS; defaults to auto-detect from the 3MF mapping." },
2049
- ams_mapping: { type: "array", description: "AMS slot mapping array used by both local MCP printing and FULU PrintParams.", items: { type: "number" } },
2050
- ams_slots: { type: "array", description: "Per-used-filament AMS slot list, matching the local LAN print path.", items: { type: "number" } },
2070
+ ams_mapping: { type: "array", description: "AMS slot mapping array used by both local MCP printing and FULU PrintParams. Same tray values as print_3mf: 254 = external spool; on dual-nozzle printers 254 = left/deputy nozzle external and 255 = right/main nozzle external.", items: { type: "number" } },
2071
+ ams_slots: { type: "array", description: "Per-used-filament AMS slot list, matching the local LAN print path (254/255 external spool rules as in print_3mf).", items: { type: "number" } },
2051
2072
  ams_mapping_bridge: { type: "string", description: "Raw JSON string override for FULU PrintParams ams_mapping when the automatic array is not enough." },
2052
2073
  ams_mapping2: { type: "string", description: "Raw JSON string for FULU PrintParams ams_mapping2, matching OrcaSlicer-bambulab's v1 AMS mapping field." },
2053
2074
  ams_mapping_info: { type: "string", description: "Raw JSON string for FULU PrintParams ams_mapping_info, matching OrcaSlicer-bambulab's detailed AMS mapping field." },
@@ -2475,12 +2496,12 @@ class BambuPrinterMCPServer {
2475
2496
  use_ams: { type: "boolean", description: "Whether to use the AMS (default: auto-detect from 3MF)" },
2476
2497
  ams_mapping: {
2477
2498
  type: "array",
2478
- description: "Project-level AMS mapping array. Position = project filament index, value = absolute AMS tray (0-3=AMS 0, 4-7=AMS 1, 8-11=AMS 2, 128+=AMS-HT, 254=external, -1=unused). Prefer ams_slots unless you know the project-level layout.",
2499
+ description: "Project-level AMS mapping array. Position = project filament index, value = absolute AMS tray (0-3=AMS 0, 4-7=AMS 1, 8-11=AMS 2, 128+=AMS-HT, 254=external, -1=unused). On dual-nozzle printers (H2D, H2D Pro, H2C, X2D) 254 is the LEFT/deputy nozzle external spool and 255 the RIGHT/main nozzle external spool, matching Bambu Studio's virtual tray ids; the gcode header's filament_map (1=left, 2=right) says which nozzle each filament was sliced for. Prefer ams_slots unless you know the project-level layout.",
2479
2500
  items: { type: "number" }
2480
2501
  },
2481
2502
  ams_slots: {
2482
2503
  type: "array",
2483
- description: "Preferred AMS input: one absolute tray index per USED filament in plate order, e.g. [1] for a single-filament print pulling from AMS 0 slot 1. Expanded to project-level ams_mapping automatically from the 3MF's plate_N.json and gcode header.",
2504
+ description: "Preferred AMS input: one absolute tray index per USED filament in plate order, e.g. [1] for a single-filament print pulling from AMS 0 slot 1. Use 254 for the external spool (on dual-nozzle printers: 254 = left/deputy nozzle external, 255 = right/main nozzle external). Expanded to project-level ams_mapping automatically from the 3MF's plate_N.json and gcode header.",
2484
2505
  items: { type: "number" }
2485
2506
  },
2486
2507
  auto_match_ams: {
@@ -2670,10 +2691,19 @@ class BambuPrinterMCPServer {
2670
2691
  const linuxEmergency = (native, mqtt) => async () => {
2671
2692
  if (process.platform !== "linux")
2672
2693
  return native();
2694
+ // An unreachable printer makes the MQTT connect retry forever; bound each
2695
+ // MQTT send so it cannot hold back a stop the native route already sent.
2696
+ const immediate = withEmergencyMqttTimeout(mqtt);
2673
2697
  const [nativeResult, immediateResult, mqttResult] = await Promise.allSettled([
2674
2698
  native(),
2675
- mqtt(),
2676
- interruptNativeOperations(bambuSerial).then(() => mqtt()),
2699
+ immediate,
2700
+ // Finish the first attempt before reusing its cached publisher: its
2701
+ // expiry must never close the later, counted send's transport.
2702
+ Promise.allSettled([interruptNativeOperations(bambuSerial), immediate]).then(results => {
2703
+ if (results[0].status === "rejected")
2704
+ throw results[0].reason;
2705
+ return withEmergencyMqttTimeout(mqtt);
2706
+ }),
2677
2707
  ]);
2678
2708
  const reason = (result) => result.status === "rejected" ? (result.reason instanceof Error ? result.reason.message : String(result.reason)) : undefined;
2679
2709
  if (nativeResult.status === "rejected" && mqttResult.status === "rejected") {
@@ -2890,7 +2920,7 @@ class BambuPrinterMCPServer {
2890
2920
  case "cancel_print":
2891
2921
  result = await this.bambu.cancelJob(host, bambuSerial, bambuToken, nativeControls()
2892
2922
  ? 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))
2923
+ messageJson: JSON.stringify({ print: { command: "stop", param: "", sequence_id: String(Date.now()) } }), qos: 1 }), signal => this.bambu.sendStopOverMqtt(host, bambuSerial, bambuToken, signal))
2894
2924
  : undefined);
2895
2925
  break;
2896
2926
  case "pause_print":
@@ -3030,7 +3060,7 @@ class BambuPrinterMCPServer {
3030
3060
  const native = () => sendNativeCommand({ host, serial: bambuSerial, token: bambuToken,
3031
3061
  messageJson: buildBambuNativeTemperatureCommand(heater, target).messageJson }, { signal: extra.signal, assertActive, beforeDispatch });
3032
3062
  return target === 0
3033
- ? linuxEmergency(native, () => this.bambu.sendHeaterOffOverMqtt(host, bambuSerial, bambuToken, heater))()
3063
+ ? linuxEmergency(native, signal => this.bambu.sendHeaterOffOverMqtt(host, bambuSerial, bambuToken, heater, signal))()
3034
3064
  : native();
3035
3065
  }
3036
3066
  : undefined);
@@ -3415,10 +3445,9 @@ class BambuPrinterMCPServer {
3415
3445
  }
3416
3446
  console.error(`3MF has no gcode — auto-slicing with ${slicerType} for ${printModel}`);
3417
3447
  const autoSliceOptions = {
3418
- uptodate: true,
3419
- ensureOnBed: true,
3420
- minSave: true,
3421
- skipModifiedGcodes: true,
3448
+ ...(slicerType === "orca-slicer-api" ? { slicePlate: plateIndex + 1 } : {
3449
+ uptodate: true, ensureOnBed: true, minSave: true, skipModifiedGcodes: true,
3450
+ }),
3422
3451
  bedType: printBedType,
3423
3452
  nozzleType: resolveNozzleType(args?.nozzle_type),
3424
3453
  nozzleVolumeType: resolveNozzleVolumeType(args?.nozzle_volume_type),
@@ -3440,9 +3469,10 @@ class BambuPrinterMCPServer {
3440
3469
  }
3441
3470
  const parsed3MFData = await parse3MF(threeMFPath);
3442
3471
  const isH2Print = H2_BAMBU_MODELS.has(printModel);
3472
+ const trayOptions = { dualNozzle: isDualNozzleModel(printModel) };
3443
3473
  let parsedAmsMapping;
3444
3474
  if (!isH2Print && parsed3MFData.slicerConfig?.ams_mapping) {
3445
- const slots = normalizeAmsMappingObject(parsed3MFData.slicerConfig.ams_mapping);
3475
+ const slots = normalizeAmsMappingObject(parsed3MFData.slicerConfig.ams_mapping, "ams_mapping", trayOptions);
3446
3476
  if (slots.length > 0) {
3447
3477
  parsedAmsMapping = slots;
3448
3478
  }
@@ -3455,10 +3485,10 @@ class BambuPrinterMCPServer {
3455
3485
  if (hasUserAmsMapping) {
3456
3486
  let userMappingOverride;
3457
3487
  if (Array.isArray(args.ams_mapping)) {
3458
- userMappingOverride = args.ams_mapping.map((v, i) => normalizeBridgeAmsTrayValue(v, `ams_mapping[${i}]`));
3488
+ userMappingOverride = args.ams_mapping.map((v, i) => normalizeBridgeAmsTrayValue(v, `ams_mapping[${i}]`, trayOptions));
3459
3489
  }
3460
3490
  else if (args.ams_mapping && typeof args.ams_mapping === 'object') {
3461
- userMappingOverride = normalizeAmsMappingObject(args.ams_mapping);
3491
+ userMappingOverride = normalizeAmsMappingObject(args.ams_mapping, "ams_mapping", trayOptions);
3462
3492
  }
3463
3493
  if (userMappingOverride && userMappingOverride.length > 0) {
3464
3494
  finalAmsMapping = userMappingOverride;
@@ -3499,8 +3529,10 @@ class BambuPrinterMCPServer {
3499
3529
  }
3500
3530
  }
3501
3531
  if (args?.use_ams === false) {
3502
- finalAmsMapping = undefined;
3503
- finalAmsSlots = undefined;
3532
+ if (!hasUserAmsMapping)
3533
+ finalAmsMapping = undefined;
3534
+ if (!hasUserAmsSlots)
3535
+ finalAmsSlots = undefined;
3504
3536
  useAMS = false;
3505
3537
  }
3506
3538
  if ((!finalAmsMapping || finalAmsMapping.length === 0) && (!finalAmsSlots || finalAmsSlots.length === 0)) {
@@ -60,10 +60,11 @@ export declare class BambuImplementation {
60
60
  print3mf(host: string, serial: string, token: string, options: BambuPrintOptionsInternal): Promise<any>;
61
61
  private print3mfPrepared;
62
62
  cancelJob(host: string, serial: string, token: string, dispatch?: () => Promise<any>): Promise<any>;
63
+ private invokeEmergencyCommand;
63
64
  /** Raw MQTT stop for routes that already ran the shared cancellation steps. */
64
- sendStopOverMqtt(host: string, serial: string, token: string): Promise<any>;
65
+ sendStopOverMqtt(host: string, serial: string, token: string, signal?: AbortSignal): Promise<any>;
65
66
  /** 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>;
67
+ sendHeaterOffOverMqtt(host: string, serial: string, token: string, heater: "bed" | "nozzle", signal?: AbortSignal): Promise<any>;
67
68
  pauseJob(host: string, serial: string, token: string, dispatch?: () => Promise<any>): Promise<any>;
68
69
  resumeJob(host: string, serial: string, token: string, dispatch?: (assertActive: () => void, beforeDispatch: () => Promise<void>) => Promise<any>): Promise<any>;
69
70
  clearHmsErrors(host: string, serial: string, token: string, dispatch?: (assertActive: () => void, beforeDispatch: () => Promise<void>) => Promise<any>): Promise<any>;