bambu-printer-mcp 1.1.11 → 1.1.13

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
@@ -15,6 +15,9 @@ import { BlenderMcpBridge } from "./blender-mcp-bridge.js";
15
15
  import { hasAmsMappingInput, normalizeAmsMappingObject, normalizeBridgeAmsTrayValue } from "./ams-mapping.js";
16
16
  import { analyze3MFAmsRequirements, analyze3MFPlateObjects, analyzeCollarCharm3MF, extractBambuTemplateSettings, getCollarCharmRolePolicy, parse3MF } from './3mf_parser.js';
17
17
  import { BambuImplementation, assertDirectPrintSupported } from "./printers/bambu.js";
18
+ import { inspectPrintFile } from "./safety/print-file.js";
19
+ import { validatePrinterState } from "./safety/printer-state.js";
20
+ import { withPrinterOperation, withPrintSnapshot, uniquePrintName } from "./safety/artifact.js";
18
21
  dotenv.config();
19
22
  const DEFAULT_HOST = process.env.BAMBU_PRINTER_HOST || process.env.PRINTER_HOST || "localhost";
20
23
  const DEFAULT_BAMBU_SERIAL = process.env.BAMBU_PRINTER_SERIAL || process.env.BAMBU_SERIAL || "";
@@ -44,6 +47,27 @@ const DEFAULT_BAMBU_MODEL = process.env.BAMBU_PRINTER_MODEL?.trim().toLowerCase(
44
47
  "";
45
48
  const DEFAULT_BED_TYPE = process.env.BED_TYPE?.trim().toLowerCase() || "textured_plate";
46
49
  const DEFAULT_NOZZLE_DIAMETER = process.env.NOZZLE_DIAMETER?.trim() || "0.4";
50
+ function requestedPrintNozzles(args) {
51
+ if (args.nozzle_diameters !== undefined && args.nozzle_diameter !== undefined) {
52
+ throw new Error("Supply nozzle_diameters or nozzle_diameter, not both.");
53
+ }
54
+ const raw = args.nozzle_diameters !== undefined ? args.nozzle_diameters :
55
+ args.nozzle_diameter !== undefined ? [args.nozzle_diameter] : undefined;
56
+ // A pre-sliced job already declares every nozzle; compare those with the
57
+ // printer rather than broadcasting the CLI's default diameter over them.
58
+ if (raw === undefined)
59
+ return undefined;
60
+ if (!Array.isArray(raw) || !raw.length || raw.length > 2 || raw.some(value => (typeof value !== "number" && typeof value !== "string") || ![0.2, 0.4, 0.6, 0.8].includes(Number(value)))) {
61
+ throw new Error("Nozzle diameters must contain one or two supported sizes: 0.2, 0.4, 0.6, 0.8 mm.");
62
+ }
63
+ return raw.map(Number);
64
+ }
65
+ function assertUniformCliNozzles(args) {
66
+ const nozzles = requestedPrintNozzles(args);
67
+ if (nozzles && new Set(nozzles).size > 1) {
68
+ throw new Error("Mixed nozzle diameters require a pre-sliced 3MF with complete per-nozzle metadata. Export it from the slicer GUI before printing.");
69
+ }
70
+ }
47
71
  const VALID_BAMBU_MODELS = ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s", "h2c", "x2d"];
48
72
  const H2_BAMBU_MODELS = new Set(["h2d", "h2s", "h2c"]);
49
73
  const VALID_BED_TYPES = ["textured_plate", "cool_plate", "engineering_plate", "hot_plate", "supertack_plate"];
@@ -789,7 +813,23 @@ class BambuPrinterMCPServer {
789
813
  tools: {}
790
814
  }
791
815
  });
792
- this.bambu = new BambuImplementation();
816
+ this.bambu = new BambuImplementation(async (message) => {
817
+ try {
818
+ const response = await this.server.elicitInput({
819
+ mode: "form",
820
+ message,
821
+ requestedSchema: {
822
+ type: "object",
823
+ properties: { confirmed: { type: "boolean", title: "I checked the printer and confirm this operation", default: false } },
824
+ required: ["confirmed"],
825
+ },
826
+ });
827
+ return response.action === "accept" && response.content?.confirmed === true;
828
+ }
829
+ catch {
830
+ throw new Error("Human hardware confirmation requires an MCP client with elicitation support. For deliberately headless operation, BAMBU_REQUIRE_CONFIRMATION=0 disables ordinary print/heat prompts; finished-bed clearance and hardware-error confirmation still require elicitation.");
831
+ }
832
+ });
793
833
  this.bambuNetwork = new BambuNetworkBridge();
794
834
  this.stlManipulator = new STLManipulator(TEMP_DIR);
795
835
  this.setupHandlers();
@@ -903,10 +943,11 @@ class BambuPrinterMCPServer {
903
943
  const JSZip = (await import('jszip')).default;
904
944
  const zipData = fs.readFileSync(threeMFPath);
905
945
  const zip = await JSZip.loadAsync(zipData);
906
- const hasGcode = Object.keys(zip.files).some(f => f.match(/Metadata\/plate_\d+\.gcode/i) || f.endsWith('.gcode'));
946
+ const hasGcode = Object.values(zip.files).some(entry => !entry.dir && /\.gcode$/i.test(entry.name));
907
947
  if (hasGcode) {
908
948
  return { threeMFPath, autoSliced: false };
909
949
  }
950
+ assertUniformCliNozzles(args);
910
951
  if (bedType === "supertack_plate") {
911
952
  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.');
912
953
  }
@@ -983,7 +1024,8 @@ class BambuPrinterMCPServer {
983
1024
  }
984
1025
  const printModel = await this.resolveBambuModel(args?.bambu_model);
985
1026
  const printBedType = resolveBedType(args?.bed_type);
986
- const printNozzle = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
1027
+ const printNozzles = requestedPrintNozzles(args);
1028
+ const printNozzle = String(printNozzles?.[0] ?? DEFAULT_NOZZLE_DIAMETER);
987
1029
  const printPreset = BAMBU_MODEL_PRESETS[printModel]?.(printNozzle);
988
1030
  const plateIndex = args?.plate_index !== undefined ? Number(args.plate_index) : 0;
989
1031
  if (!Number.isInteger(plateIndex) || plateIndex < 0) {
@@ -1009,82 +1051,130 @@ class BambuPrinterMCPServer {
1009
1051
  if (isLocalBridgePrint && !password) {
1010
1052
  throw new Error("bambu_token/access code is required for FULU BambuNetwork LAN/local print methods.");
1011
1053
  }
1054
+ if (!bambuSerial || devId !== bambuSerial || !devIp || devIp === "localhost" || !(password || bambuToken)) {
1055
+ throw new Error("Bridge printing requires a matching bambu_serial/dev_id, printer host, and LAN access token for fresh MQTT safety verification, including cloud dispatch.");
1056
+ }
1057
+ for (const field of ["config_filename", "ftp_file", "ftp_file_md5", "ams_mapping_bridge", "ams_mapping2", "ams_mapping_info", "nozzle_mapping", "nozzles_info", "extra_options"]) {
1058
+ if (args[field] !== undefined && args[field] !== "")
1059
+ throw new Error(`${field} cannot override a verified print. Use the structured plate, nozzle, and AMS arguments.`);
1060
+ }
1012
1061
  const { threeMFPath, autoSliced } = await this.ensurePrintableThreeMFPath(args, printModel, printPreset, printBedType);
1013
1062
  const { useAMS, finalAmsMapping, finalAmsSlots } = await this.resolveAmsPrintSettings(threeMFPath, args, host, bambuSerial, bambuToken, printModel, printNozzle);
1014
1063
  const threeMfFilename = path.basename(threeMFPath);
1015
- const projectName = String(args?.project_name || threeMfFilename.replace(/\.3mf$/i, ''));
1064
+ const projectName = String(args?.project_name || args?.task_name || threeMfFilename.replace(/\.3mf$/i, ''));
1016
1065
  const presetName = String(args?.preset_name || `${projectName}_plate_${plateIndex + 1}`);
1017
1066
  const clientJobId = args?.client_job_id !== undefined ? Number(args.client_job_id) : Date.now();
1018
- const amsMapping = stringifyBridgeJson(args?.ams_mapping_bridge ?? finalAmsMapping ?? finalAmsSlots);
1019
- const params = {
1020
- dev_id: devId,
1021
- task_name: String(args?.task_name || projectName),
1022
- project_name: projectName,
1023
- preset_name: presetName,
1024
- filename: threeMFPath,
1025
- config_filename: String(args?.config_filename || threeMFPath),
1026
- plate_index: plateIndex + 1,
1027
- ftp_folder: String(args?.ftp_folder || ""),
1028
- ftp_file: String(args?.ftp_file || ""),
1029
- ftp_file_md5: String(args?.ftp_file_md5 || ""),
1030
- nozzle_mapping: stringifyBridgeJson(args?.nozzle_mapping) || "",
1031
- ams_mapping: amsMapping || "",
1032
- ams_mapping2: stringifyBridgeJson(args?.ams_mapping2) || "",
1033
- ams_mapping_info: stringifyBridgeJson(args?.ams_mapping_info) || "",
1034
- nozzles_info: stringifyBridgeJson(args?.nozzles_info) || "",
1035
- connection_type: connectionType,
1036
- comments: String(args?.comments || ""),
1037
- origin_profile_id: args?.origin_profile_id !== undefined ? Number(args.origin_profile_id) : 0,
1038
- stl_design_id: args?.stl_design_id !== undefined ? Number(args.stl_design_id) : 0,
1039
- origin_model_id: String(args?.origin_model_id || ""),
1040
- print_type: String(args?.print_type || "from_normal"),
1041
- dst_file: String(args?.dst_file || ""),
1042
- dev_name: String(args?.dev_name || ""),
1043
- dev_ip: devIp,
1044
- use_ssl_for_ftp: args?.use_ssl_for_ftp !== undefined ? Boolean(args.use_ssl_for_ftp) : true,
1045
- use_ssl_for_mqtt: args?.use_ssl_for_mqtt !== undefined ? Boolean(args.use_ssl_for_mqtt) : true,
1046
- username: String(args?.username || "bblp"),
1047
- password,
1048
- task_bed_leveling: args?.bed_leveling !== undefined ? Boolean(args.bed_leveling) : true,
1049
- task_flow_cali: args?.flow_calibration !== undefined ? Boolean(args.flow_calibration) : true,
1050
- task_vibration_cali: args?.vibration_calibration !== undefined ? Boolean(args.vibration_calibration) : true,
1051
- task_layer_inspect: args?.layer_inspect !== undefined ? Boolean(args.layer_inspect) : false,
1052
- task_record_timelapse: args?.timelapse !== undefined ? Boolean(args.timelapse) : false,
1053
- task_use_ams: useAMS,
1054
- task_bed_type: printBedType,
1055
- extra_options: stringifyBridgeJson(args?.extra_options) || "",
1056
- auto_bed_leveling: args?.auto_bed_leveling !== undefined ? Number(args.auto_bed_leveling) : 0,
1057
- auto_flow_cali: args?.auto_flow_cali !== undefined ? Number(args.auto_flow_cali) : 0,
1058
- auto_offset_cali: args?.auto_offset_cali !== undefined ? Number(args.auto_offset_cali) : 0,
1059
- extruder_cali_manual_mode: args?.extruder_cali_manual_mode !== undefined ? Number(args.extruder_cali_manual_mode) : -1,
1060
- task_ext_change_assist: args?.external_change_assist !== undefined ? Boolean(args.external_change_assist) : false,
1061
- try_emmc_print: args?.try_emmc_print !== undefined ? Boolean(args.try_emmc_print) : false,
1062
- };
1063
- const bridgeResult = await this.bambuNetwork.callWithAgent(bridgeMethod, { client_job_id: clientJobId, params }, this.bridgeOptionsFromArgs(args, "print_3mf_bambu_network"));
1064
- if (typeof bridgeResult === "object" && bridgeResult !== null && bridgeResult.ok === false) {
1065
- throw new Error(`FULU BambuNetwork bridge method ${bridgeMethod} failed: ${String(bridgeResult.error || "unknown bridge error")}`);
1066
- }
1067
- if (typeof bridgeResult === "object" &&
1068
- bridgeResult !== null &&
1069
- typeof bridgeResult.value === "number" &&
1070
- bridgeResult.value !== 0) {
1071
- const value = bridgeResult.value;
1072
- throw new Error(`FULU BambuNetwork bridge method ${bridgeMethod} returned non-zero result ${value}.`);
1073
- }
1074
- return {
1075
- status: "success",
1076
- message: `FULU BambuNetwork ${bridgePrintMethod} command for ${threeMfFilename} sent successfully.`,
1077
- bridgeMethod,
1078
- bridgeResult,
1079
- clientJobId,
1080
- autoSliced,
1081
- projectName,
1082
- plateIndex,
1083
- bridgePlateIndex: plateIndex + 1,
1084
- useAMS,
1085
- amsMapping: finalAmsMapping ?? finalAmsSlots,
1086
- params: redactPrintParams(params),
1087
- };
1067
+ return withPrinterOperation(devIp, devId, assertActive => withPrintSnapshot(threeMFPath, async (snapshot, retainForDispatchedUpload) => {
1068
+ const inspection = await inspectPrintFile(snapshot, { model: printModel, nozzleDiameters: printNozzles, plateIndex, bedType: printBedType });
1069
+ let mapping = finalAmsMapping?.slice();
1070
+ if (finalAmsSlots) {
1071
+ if (finalAmsSlots.length !== inspection.usedFilamentPositions.length)
1072
+ throw new Error("ams_slots must supply exactly one physical tray per used filament.");
1073
+ mapping = Array(inspection.materials.length).fill(-1);
1074
+ inspection.usedFilamentPositions.forEach((position, index) => { mapping[position] = finalAmsSlots[index]; });
1075
+ }
1076
+ if (!useAMS) {
1077
+ mapping = Array(inspection.materials.length).fill(-1);
1078
+ inspection.usedFilamentPositions.forEach(position => { mapping[position] = 254; });
1079
+ }
1080
+ const requirements = { ...inspection, amsMapping: mapping, useAMS };
1081
+ const initialStatus = await this.bambu.getSafetyStatus(devIp, devId, password || bambuToken);
1082
+ validatePrinterState(initialStatus, requirements);
1083
+ const bedClearance = await this.bambu.confirmPrintPreflight(devId, initialStatus, inspection);
1084
+ const confirmedStatus = await this.bambu.getSafetyStatus(devIp, devId, password || bambuToken);
1085
+ validatePrinterState(confirmedStatus, requirements);
1086
+ this.bambu.assertBedClearance(confirmedStatus, bedClearance);
1087
+ const amsMapping = stringifyBridgeJson(mapping);
1088
+ const remoteFileName = uniquePrintName(threeMFPath);
1089
+ const params = {
1090
+ dev_id: devId,
1091
+ // Firmware can report the task name instead of the uploaded filename.
1092
+ // Bind either report to this inspected artifact for a later safe resume.
1093
+ task_name: remoteFileName.replace(/(?:\.gcode)?\.3mf$/i, ""),
1094
+ project_name: projectName,
1095
+ preset_name: presetName,
1096
+ filename: snapshot,
1097
+ config_filename: snapshot,
1098
+ plate_index: plateIndex + 1,
1099
+ ftp_folder: String(args?.ftp_folder || ""),
1100
+ ftp_file: String(args?.ftp_file || ""),
1101
+ ftp_file_md5: String(args?.ftp_file_md5 || ""),
1102
+ nozzle_mapping: stringifyBridgeJson(args?.nozzle_mapping) || "",
1103
+ ams_mapping: amsMapping || "",
1104
+ ams_mapping2: stringifyBridgeJson(args?.ams_mapping2) || "",
1105
+ ams_mapping_info: stringifyBridgeJson(args?.ams_mapping_info) || "",
1106
+ nozzles_info: stringifyBridgeJson(args?.nozzles_info) || "",
1107
+ connection_type: connectionType,
1108
+ comments: String(args?.comments || ""),
1109
+ origin_profile_id: args?.origin_profile_id !== undefined ? Number(args.origin_profile_id) : 0,
1110
+ stl_design_id: args?.stl_design_id !== undefined ? Number(args.stl_design_id) : 0,
1111
+ origin_model_id: String(args?.origin_model_id || ""),
1112
+ print_type: String(args?.print_type || "from_normal"),
1113
+ dst_file: remoteFileName,
1114
+ dev_name: String(args?.dev_name || ""),
1115
+ dev_ip: devIp,
1116
+ use_ssl_for_ftp: args?.use_ssl_for_ftp !== undefined ? Boolean(args.use_ssl_for_ftp) : true,
1117
+ use_ssl_for_mqtt: args?.use_ssl_for_mqtt !== undefined ? Boolean(args.use_ssl_for_mqtt) : true,
1118
+ username: String(args?.username || "bblp"),
1119
+ password,
1120
+ task_bed_leveling: args?.bed_leveling !== undefined ? Boolean(args.bed_leveling) : true,
1121
+ task_flow_cali: args?.flow_calibration !== undefined ? Boolean(args.flow_calibration) : true,
1122
+ task_vibration_cali: args?.vibration_calibration !== undefined ? Boolean(args.vibration_calibration) : true,
1123
+ task_layer_inspect: args?.layer_inspect !== undefined ? Boolean(args.layer_inspect) : false,
1124
+ task_record_timelapse: args?.timelapse !== undefined ? Boolean(args.timelapse) : false,
1125
+ task_use_ams: useAMS,
1126
+ task_bed_type: printBedType,
1127
+ extra_options: stringifyBridgeJson(args?.extra_options) || "",
1128
+ auto_bed_leveling: args?.auto_bed_leveling !== undefined ? Number(args.auto_bed_leveling) : 0,
1129
+ auto_flow_cali: args?.auto_flow_cali !== undefined ? Number(args.auto_flow_cali) : 0,
1130
+ auto_offset_cali: args?.auto_offset_cali !== undefined ? Number(args.auto_offset_cali) : 0,
1131
+ extruder_cali_manual_mode: args?.extruder_cali_manual_mode !== undefined ? Number(args.extruder_cali_manual_mode) : -1,
1132
+ task_ext_change_assist: args?.external_change_assist !== undefined ? Boolean(args.external_change_assist) : false,
1133
+ try_emmc_print: args?.try_emmc_print !== undefined ? Boolean(args.try_emmc_print) : false,
1134
+ };
1135
+ assertActive();
1136
+ const bridgeResult = await this.bambuNetwork.callWithAgent(bridgeMethod, { client_job_id: clientJobId, params }, {
1137
+ ...this.bridgeOptionsFromArgs(args, "print_3mf_bambu_network"),
1138
+ beforeDispatch: async (method) => {
1139
+ if (method !== bridgeMethod)
1140
+ return;
1141
+ assertActive();
1142
+ const dispatchStatus = await this.bambu.getSafetyStatus(devIp, devId, password || bambuToken);
1143
+ validatePrinterState(dispatchStatus, requirements);
1144
+ this.bambu.assertBedClearance(dispatchStatus, bedClearance);
1145
+ },
1146
+ assertDispatchAllowed: method => { if (method === bridgeMethod)
1147
+ assertActive(); },
1148
+ onDispatched: method => { if (method === bridgeMethod)
1149
+ retainForDispatchedUpload(); },
1150
+ });
1151
+ if (typeof bridgeResult === "object" && bridgeResult !== null && bridgeResult.ok === false) {
1152
+ throw new Error(`FULU BambuNetwork bridge method ${bridgeMethod} failed: ${String(bridgeResult.error || "unknown bridge error")}`);
1153
+ }
1154
+ if (typeof bridgeResult === "object" &&
1155
+ bridgeResult !== null &&
1156
+ typeof bridgeResult.value === "number" &&
1157
+ bridgeResult.value !== 0) {
1158
+ const value = bridgeResult.value;
1159
+ throw new Error(`FULU BambuNetwork bridge method ${bridgeMethod} returned non-zero result ${value}.`);
1160
+ }
1161
+ assertActive();
1162
+ this.bambu.recordCheckedJob(devIp, devId, remoteFileName, requirements);
1163
+ return {
1164
+ status: "success",
1165
+ message: `FULU BambuNetwork ${bridgePrintMethod} command for ${threeMfFilename} sent successfully.`,
1166
+ bridgeMethod,
1167
+ bridgeResult,
1168
+ clientJobId,
1169
+ autoSliced,
1170
+ projectName,
1171
+ plateIndex,
1172
+ bridgePlateIndex: plateIndex + 1,
1173
+ useAMS,
1174
+ amsMapping: mapping,
1175
+ params: redactPrintParams(params),
1176
+ };
1177
+ }));
1088
1178
  }
1089
1179
  async getResolvedPrinterFilamentInventory(host, bambuSerial, bambuToken, bambuModel, nozzleDiameter) {
1090
1180
  const status = await this.bambu.getStatus(host, bambuSerial, bambuToken);
@@ -1704,7 +1794,7 @@ class BambuPrinterMCPServer {
1704
1794
  plate_index: { type: "number", description: "Zero-based plate index to print from the sliced 3MF; converted to FULU's one-based PrintParams plate_index." },
1705
1795
  project_name: { type: "string", description: "Optional project name sent in FULU PrintParams; defaults to the 3MF filename without extension." },
1706
1796
  preset_name: { type: "string", description: "Optional preset name sent in FULU PrintParams; defaults to project plus one-based plate index." },
1707
- task_name: { type: "string", description: "Optional BambuNetwork task name; defaults to the project name." },
1797
+ task_name: { type: "string", description: "Optional project label when project_name is omitted. The submitted task name uses a unique inspected-job identity for safe resume." },
1708
1798
  config_filename: { type: "string", description: "Optional config 3MF path for cloud print; defaults to the same 3MF path." },
1709
1799
  bridge_command: { type: "string", description: "Override command for the FULU bridge host or macOS/WSL wrapper; defaults to BAMBU_NETWORK_BRIDGE_COMMAND. Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1." },
1710
1800
  bambu_network_config_dir: { type: "string", description: "Config/log directory used by the BambuNetwork agent; defaults to BAMBU_NETWORK_CONFIG_DIR or a user config directory." },
@@ -1715,6 +1805,7 @@ class BambuPrinterMCPServer {
1715
1805
  slicer_path: { type: "string", description: "Path to the slicer executable for auto-slicing; defaults to value from env or a platform default. Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1." },
1716
1806
  slicer_profile: { type: "string", description: "Path to an optional slicer profile/config file for auto-slicing." },
1717
1807
  nozzle_diameter: { type: "string", description: "Nozzle diameter in mm for auto-slicing (default: 0.4)." },
1808
+ 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." },
1718
1809
  use_ams: { type: "boolean", description: "Whether to use the AMS; defaults to auto-detect from the 3MF mapping." },
1719
1810
  ams_mapping: { type: "array", description: "AMS slot mapping array used by both local MCP printing and FULU PrintParams.", items: { type: "number" } },
1720
1811
  ams_slots: { type: "array", description: "Per-used-filament AMS slot list, matching the local LAN print path.", items: { type: "number" } },
@@ -1778,13 +1869,14 @@ class BambuPrinterMCPServer {
1778
1869
  },
1779
1870
  {
1780
1871
  name: "upload_gcode",
1781
- description: "Upload a G-code file to the Bambu Lab printer",
1872
+ description: "Inspect a G-code file and upload a uniquely named copy without overwriting existing printer files.",
1782
1873
  inputSchema: {
1783
1874
  type: "object",
1784
1875
  properties: {
1785
1876
  filename: { type: "string", description: "Name for the file on the printer" },
1786
1877
  gcode: { type: "string", description: "G-code content to upload, or a readable local .gcode path. Required unless gcode_path is provided. For large files, prefer gcode_path." },
1787
1878
  gcode_path: { type: "string", description: "Local path to a .gcode file to upload. Required unless gcode is provided. This avoids sending large G-code bodies through the MCP request." },
1879
+ bambu_model: { type: "string", enum: [...VALID_BAMBU_MODELS], description: "Required printer model for inspecting the uploaded G-code, even when not starting it." },
1788
1880
  host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
1789
1881
  bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
1790
1882
  bambu_token: { type: "string", description: "Access token (default: value from env)" }
@@ -1804,7 +1896,7 @@ class BambuPrinterMCPServer {
1804
1896
  bambu_model: {
1805
1897
  type: "string",
1806
1898
  enum: [...VALID_BAMBU_MODELS],
1807
- description: "Required when print is true. Bambu Lab printer model used as a safety confirmation before starting the uploaded file."
1899
+ description: "Required for every printable .gcode or .3mf upload, including upload-only operations."
1808
1900
  },
1809
1901
  host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
1810
1902
  bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
@@ -1952,12 +2044,15 @@ class BambuPrinterMCPServer {
1952
2044
  },
1953
2045
  {
1954
2046
  name: "set_temperature",
1955
- description: "Set the temperature of a printer component (bed, nozzle)",
2047
+ description: "Set a checked bed or nozzle temperature. Positive heating requires matching fresh printer telemetry; nozzle heating also requires declared material. Zero turns the heater off.",
1956
2048
  inputSchema: {
1957
2049
  type: "object",
1958
2050
  properties: {
1959
2051
  component: { type: "string", description: "Component to heat: bed, nozzle, or extruder" },
1960
- temperature: { type: "number", description: "Target temperature in °C" },
2052
+ temperature: { type: "number", minimum: 0, description: "Finite target temperature in °C, checked against model/component and declared material limits" },
2053
+ bambu_model: { type: "string", enum: [...VALID_BAMBU_MODELS], description: "Printer model, required for positive heating unless configured in the environment" },
2054
+ material: { type: "string", description: "Loaded material, for example PLA or PETG. Required for positive nozzle heating, including non-RFID spools." },
2055
+ nozzle_diameter: { type: "number", enum: [0.2, 0.4, 0.6, 0.8], description: "Installed nozzle diameter to compare with printer-reported configuration (default 0.4)" },
1961
2056
  host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
1962
2057
  bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
1963
2058
  bambu_token: { type: "string", description: "Access token (default: value from env)" }
@@ -2108,7 +2203,8 @@ class BambuPrinterMCPServer {
2108
2203
  template_3mf_path: { type: "string", description: "Optional template 3MF whose embedded Bambu slicer settings should be reused when auto-slicing this print job." },
2109
2204
  template_name: { type: "string", description: "Optional named template from the local registry. Resolves to template_3mf_path automatically." },
2110
2205
  template_dir: { type: "string", description: "Optional template directory override when resolving template_name." },
2111
- nozzle_diameter: { type: "string", description: "Nozzle diameter in mm for auto-slicing (default: 0.4)" }
2206
+ nozzle_diameter: { type: "string", description: "Nozzle diameter in mm for auto-slicing (default: 0.4)" },
2207
+ 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 a pre-sliced job, e.g. [0.4, 0.6]. Omit to verify its declared diameters against live telemetry; cannot combine with nozzle_diameter." }
2112
2208
  },
2113
2209
  required: ["three_mf_path", "bambu_model"]
2114
2210
  }
@@ -2316,6 +2412,10 @@ class BambuPrinterMCPServer {
2316
2412
  throw new Error("Missing required parameter: method");
2317
2413
  }
2318
2414
  const bridgeArgs = args;
2415
+ const readMethods = new Set(["bridge.handshake", "net.is_user_login", "net.get_user_id", "net.get_user_name", "net.get_user_avatar", "net.get_user_nickanme"]);
2416
+ if (!readMethods.has(String(bridgeArgs.method))) {
2417
+ throw new Error("Raw bridge mutations and unknown methods are disabled. Use print_3mf_bambu_network for verified printing or a dedicated printer tool.");
2418
+ }
2319
2419
  const payload = bridgeArgs.payload && typeof bridgeArgs.payload === "object"
2320
2420
  ? bridgeArgs.payload
2321
2421
  : {};
@@ -2352,7 +2452,8 @@ class BambuPrinterMCPServer {
2352
2452
  }
2353
2453
  const uploadSource = resolveUploadGcodeSource(args);
2354
2454
  try {
2355
- result = await this.bambu.uploadFile(host, bambuSerial, bambuToken, uploadSource.filePath, String(args.filename), false);
2455
+ const uploadModel = await this.resolveBambuModel(args?.bambu_model);
2456
+ result = await this.bambu.uploadFile(host, bambuSerial, bambuToken, uploadSource.filePath, String(args.filename), false, uploadModel);
2356
2457
  }
2357
2458
  finally {
2358
2459
  if (uploadSource.cleanupDir) {
@@ -2366,7 +2467,8 @@ class BambuPrinterMCPServer {
2366
2467
  throw new Error("Missing required parameters: file_path and filename");
2367
2468
  }
2368
2469
  const print = Boolean(args.print ?? false);
2369
- const uploadModel = print ? await this.resolveBambuModel(args?.bambu_model) : undefined;
2470
+ const printableUpload = /\.(?:gcode|3mf)$/i.test(String(args.file_path)) || /\.(?:gcode|3mf)$/i.test(String(args.filename));
2471
+ const uploadModel = print || printableUpload ? await this.resolveBambuModel(args?.bambu_model) : undefined;
2370
2472
  if (print)
2371
2473
  assertDirectPrintSupported(uploadModel, bambuSerial);
2372
2474
  result = await this.bambu.uploadFile(host, bambuSerial, bambuToken, String(args.file_path), String(args.filename), print, uploadModel);
@@ -2412,12 +2514,16 @@ class BambuPrinterMCPServer {
2412
2514
  }
2413
2515
  result = await this.bambu.rereadAmsRfid(host, bambuSerial, bambuToken, Number(args.ams_id), Number(args.slot_id));
2414
2516
  break;
2415
- case "set_temperature":
2517
+ case "set_temperature": {
2416
2518
  if (!args?.component || args?.temperature === undefined) {
2417
2519
  throw new Error("Missing required parameters: component and temperature");
2418
2520
  }
2419
- result = await this.bambu.setTemperature(host, bambuSerial, bambuToken, String(args.component), Number(args.temperature));
2521
+ if (typeof args.temperature !== "number" || !Number.isFinite(args.temperature) || args.temperature < 0) {
2522
+ throw new Error("Temperature must be a finite, non-negative number in °C.");
2523
+ }
2524
+ result = await this.bambu.setTemperature(host, bambuSerial, bambuToken, String(args.component), args.temperature, args.temperature === 0 ? undefined : await this.resolveBambuModel(args?.bambu_model), args?.material !== undefined ? String(args.material) : undefined, Number(args?.nozzle_diameter ?? DEFAULT_NOZZLE_DIAMETER));
2420
2525
  break;
2526
+ }
2421
2527
  case "set_fan_speed":
2422
2528
  if (!args?.fan || args?.speed === undefined) {
2423
2529
  throw new Error("Missing required parameters: fan and speed");
@@ -2656,7 +2762,8 @@ class BambuPrinterMCPServer {
2656
2762
  const printModel = await this.resolveBambuModel(args?.bambu_model);
2657
2763
  assertDirectPrintSupported(printModel, bambuSerial);
2658
2764
  const printBedType = resolveBedType(args?.bed_type);
2659
- const printNozzle = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
2765
+ const printNozzles = requestedPrintNozzles(args);
2766
+ const printNozzle = String(printNozzles?.[0] ?? DEFAULT_NOZZLE_DIAMETER);
2660
2767
  const activeSlicerProfile = await resolveTemplateFirstSlicerProfilePath(args, slicerProfile || undefined, resolveTemplatePathFromName(["json", "config", "3mf"]) || explicitTemplatePath || undefined, TEMP_DIR);
2661
2768
  const explicitSlicerProfile = hasExplicitSlicerProfile(args);
2662
2769
  const printPreset = BAMBU_MODEL_PRESETS[printModel]?.(printNozzle);
@@ -2671,6 +2778,7 @@ class BambuPrinterMCPServer {
2671
2778
  const zip = await JSZip.loadAsync(zipData);
2672
2779
  const hasGcode = Object.values(zip.files).some(entry => !entry.dir && /\.gcode$/i.test(entry.name));
2673
2780
  if (!hasGcode) {
2781
+ assertUniformCliNozzles(args);
2674
2782
  if (printBedType === "supertack_plate") {
2675
2783
  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.');
2676
2784
  }
@@ -2746,6 +2854,7 @@ class BambuPrinterMCPServer {
2746
2854
  useAMS = finalAmsSlots.length > 0;
2747
2855
  }
2748
2856
  if (isH2Print &&
2857
+ args?.use_ams !== false &&
2749
2858
  !hasUserAmsMapping &&
2750
2859
  !hasUserAmsSlots &&
2751
2860
  args?.auto_match_ams !== true &&
@@ -2770,6 +2879,7 @@ class BambuPrinterMCPServer {
2770
2879
  projectName,
2771
2880
  filePath: threeMFPath,
2772
2881
  bambuModel: printModel,
2882
+ nozzleDiameters: printNozzles,
2773
2883
  plateIndex,
2774
2884
  useAMS: useAMS,
2775
2885
  amsMapping: finalAmsMapping,
@@ -2807,6 +2917,7 @@ class BambuPrinterMCPServer {
2807
2917
  projectName,
2808
2918
  filePath: preparedThreeMFPath,
2809
2919
  bambuModel: printModel,
2920
+ nozzleDiameters: [Number(printNozzle)],
2810
2921
  plateIndex: collarAnalysis.plateIndex,
2811
2922
  useAMS: true,
2812
2923
  amsSlots: collarAnalysis.amsSlots,
@@ -1,8 +1,11 @@
1
+ import { inspectPrintFile } from "../safety/print-file.js";
2
+ import { type PrinterStateRequirements } from "../safety/printer-state.js";
1
3
  export declare function assertDirectPrintSupported(model: string | undefined, serial?: string): void;
2
4
  interface BambuPrintOptionsInternal {
3
5
  projectName: string;
4
6
  filePath: string;
5
7
  bambuModel?: string;
8
+ nozzleDiameters?: number[];
6
9
  useAMS?: boolean;
7
10
  plateIndex?: number;
8
11
  bedType?: string;
@@ -24,12 +27,25 @@ interface BambuPrintOptionsInternal {
24
27
  md5?: string;
25
28
  }
26
29
  export declare class BambuImplementation {
30
+ private readonly confirm?;
27
31
  private printerStore;
28
- constructor();
32
+ private checkedJobs;
33
+ private clearedErrors;
34
+ constructor(confirm?: ((message: string) => Promise<boolean>) | undefined);
35
+ confirmHardwareAction(message: string, physicalCheck?: boolean): Promise<void>;
36
+ private finishedJobIdentity;
37
+ confirmPrintPreflight(serial: string, status: any, inspection: Awaited<ReturnType<typeof inspectPrintFile>>): Promise<string | undefined>;
38
+ assertBedClearance(status: any, confirmedFinishedJob: string | undefined): void;
29
39
  private getPrinter;
40
+ /** Internal handoff from a successful inspected transport; never exposed as an MCP tool. */
41
+ recordCheckedJob(host: string, serial: string, remotePath: string, requirements: PrinterStateRequirements): void;
42
+ private validateLoadedGcodeState;
30
43
  private resolveProjectFileMetadata;
44
+ /** Safety reads never use the display cache or configured-serial model inference. */
45
+ getSafetyStatus(host: string, serial: string, token: string): Promise<any>;
31
46
  getStatus(host: string, serial: string, token: string): Promise<any>;
32
47
  print3mf(host: string, serial: string, token: string, options: BambuPrintOptionsInternal): Promise<any>;
48
+ private print3mfPrepared;
33
49
  cancelJob(host: string, serial: string, token: string): Promise<any>;
34
50
  pauseJob(host: string, serial: string, token: string): Promise<any>;
35
51
  resumeJob(host: string, serial: string, token: string): Promise<any>;
@@ -37,7 +53,7 @@ export declare class BambuImplementation {
37
53
  setPrintSpeed(host: string, serial: string, token: string, speedMode: string | number): Promise<any>;
38
54
  setAirductMode(host: string, serial: string, token: string, mode: string): Promise<any>;
39
55
  rereadAmsRfid(host: string, serial: string, token: string, amsId: number, slotId: number): Promise<any>;
40
- setTemperature(host: string, serial: string, token: string, component: string, temperature: number): Promise<{
56
+ setTemperature(host: string, serial: string, token: string, component: string, temperature: unknown, bambuModel?: string, material?: string, nozzleDiameter?: number): Promise<{
41
57
  status: string;
42
58
  message: string;
43
59
  command: string;
@@ -68,12 +84,28 @@ export declare class BambuImplementation {
68
84
  name: string;
69
85
  exists: boolean;
70
86
  }>;
71
- uploadFile(host: string, serial: string, token: string, filePath: string, filename: string, print: boolean, bambuModel?: string): Promise<Record<string, unknown>>;
87
+ uploadFile(host: string, serial: string, token: string, filePath: string, filename: string, print: boolean, bambuModel?: string): Promise<{
88
+ status: string;
89
+ uploaded: boolean;
90
+ remotePath: string;
91
+ printRequested: boolean;
92
+ inspected: boolean;
93
+ } | {
94
+ status: string;
95
+ uploaded: boolean;
96
+ printRequested: boolean;
97
+ remotePath: string;
98
+ message: string;
99
+ }>;
100
+ private printRawPrepared;
72
101
  startJob(host: string, serial: string, token: string, filename: string, bambuModel?: string): Promise<{
73
102
  status: string;
103
+ uploaded: boolean;
104
+ printRequested: boolean;
105
+ remotePath: string;
74
106
  message: string;
75
- file: string;
76
107
  }>;
108
+ private ftpDownload;
77
109
  /**
78
110
  * Capture a single JPEG frame from the printer's chamber camera.
79
111
  *