bambu-printer-mcp 1.1.12 → 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/src/index.ts CHANGED
@@ -30,6 +30,9 @@ import { hasAmsMappingInput, normalizeAmsMappingObject, normalizeBridgeAmsTrayVa
30
30
  import { analyze3MFAmsRequirements, analyze3MFPlateObjects, analyzeCollarCharm3MF, extractBambuTemplateSettings, getCollarCharmRolePolicy, parse3MF } from './3mf_parser.js';
31
31
  import type { ThreeMFAmsRequirements } from "./types.js";
32
32
  import { BambuImplementation, assertDirectPrintSupported } from "./printers/bambu.js";
33
+ import { inspectPrintFile } from "./safety/print-file.js";
34
+ import { validatePrinterState } from "./safety/printer-state.js";
35
+ import { withPrinterOperation, withPrintSnapshot, uniquePrintName } from "./safety/artifact.js";
33
36
 
34
37
  dotenv.config();
35
38
 
@@ -64,6 +67,29 @@ const DEFAULT_BAMBU_MODEL =
64
67
  const DEFAULT_BED_TYPE = process.env.BED_TYPE?.trim().toLowerCase() || "textured_plate";
65
68
  const DEFAULT_NOZZLE_DIAMETER = process.env.NOZZLE_DIAMETER?.trim() || "0.4";
66
69
 
70
+ function requestedPrintNozzles(args: Record<string, any>): number[] | undefined {
71
+ if (args.nozzle_diameters !== undefined && args.nozzle_diameter !== undefined) {
72
+ throw new Error("Supply nozzle_diameters or nozzle_diameter, not both.");
73
+ }
74
+ const raw = args.nozzle_diameters !== undefined ? args.nozzle_diameters :
75
+ args.nozzle_diameter !== undefined ? [args.nozzle_diameter] : undefined;
76
+ // A pre-sliced job already declares every nozzle; compare those with the
77
+ // printer rather than broadcasting the CLI's default diameter over them.
78
+ if (raw === undefined) return undefined;
79
+ if (!Array.isArray(raw) || !raw.length || raw.length > 2 || raw.some(value =>
80
+ (typeof value !== "number" && typeof value !== "string") || ![0.2, 0.4, 0.6, 0.8].includes(Number(value)))) {
81
+ throw new Error("Nozzle diameters must contain one or two supported sizes: 0.2, 0.4, 0.6, 0.8 mm.");
82
+ }
83
+ return raw.map(Number);
84
+ }
85
+
86
+ function assertUniformCliNozzles(args: Record<string, any>): void {
87
+ const nozzles = requestedPrintNozzles(args);
88
+ if (nozzles && new Set(nozzles).size > 1) {
89
+ throw new Error("Mixed nozzle diameters require a pre-sliced 3MF with complete per-nozzle metadata. Export it from the slicer GUI before printing.");
90
+ }
91
+ }
92
+
67
93
  const VALID_BAMBU_MODELS = ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s", "h2c", "x2d"] as const;
68
94
  type BambuModel = typeof VALID_BAMBU_MODELS[number];
69
95
  const H2_BAMBU_MODELS = new Set<string>(["h2d", "h2s", "h2c"]);
@@ -1069,7 +1095,22 @@ class BambuPrinterMCPServer {
1069
1095
  }
1070
1096
  );
1071
1097
 
1072
- this.bambu = new BambuImplementation();
1098
+ this.bambu = new BambuImplementation(async message => {
1099
+ try {
1100
+ const response = await this.server.elicitInput({
1101
+ mode: "form",
1102
+ message,
1103
+ requestedSchema: {
1104
+ type: "object",
1105
+ properties: { confirmed: { type: "boolean", title: "I checked the printer and confirm this operation", default: false } },
1106
+ required: ["confirmed"],
1107
+ },
1108
+ });
1109
+ return response.action === "accept" && response.content?.confirmed === true;
1110
+ } catch {
1111
+ 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.");
1112
+ }
1113
+ });
1073
1114
  this.bambuNetwork = new BambuNetworkBridge();
1074
1115
  this.stlManipulator = new STLManipulator(TEMP_DIR);
1075
1116
 
@@ -1233,13 +1274,12 @@ class BambuPrinterMCPServer {
1233
1274
  const JSZip = (await import('jszip')).default;
1234
1275
  const zipData = fs.readFileSync(threeMFPath);
1235
1276
  const zip = await JSZip.loadAsync(zipData);
1236
- const hasGcode = Object.keys(zip.files).some(
1237
- f => f.match(/Metadata\/plate_\d+\.gcode/i) || f.endsWith('.gcode')
1238
- );
1277
+ const hasGcode = Object.values(zip.files).some(entry => !entry.dir && /\.gcode$/i.test(entry.name));
1239
1278
 
1240
1279
  if (hasGcode) {
1241
1280
  return { threeMFPath, autoSliced: false };
1242
1281
  }
1282
+ assertUniformCliNozzles(args);
1243
1283
 
1244
1284
  if (bedType === "supertack_plate") {
1245
1285
  throw new Error(
@@ -1360,7 +1400,8 @@ class BambuPrinterMCPServer {
1360
1400
 
1361
1401
  const printModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
1362
1402
  const printBedType = resolveBedType(args?.bed_type as string | undefined);
1363
- const printNozzle = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
1403
+ const printNozzles = requestedPrintNozzles(args);
1404
+ const printNozzle = String(printNozzles?.[0] ?? DEFAULT_NOZZLE_DIAMETER);
1364
1405
  const printPreset = BAMBU_MODEL_PRESETS[printModel]?.(printNozzle);
1365
1406
  const plateIndex = args?.plate_index !== undefined ? Number(args.plate_index) : 0;
1366
1407
 
@@ -1395,6 +1436,13 @@ class BambuPrinterMCPServer {
1395
1436
  throw new Error("bambu_token/access code is required for FULU BambuNetwork LAN/local print methods.");
1396
1437
  }
1397
1438
 
1439
+ if (!bambuSerial || devId !== bambuSerial || !devIp || devIp === "localhost" || !(password || bambuToken)) {
1440
+ 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.");
1441
+ }
1442
+ 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"]) {
1443
+ if (args[field] !== undefined && args[field] !== "") throw new Error(`${field} cannot override a verified print. Use the structured plate, nozzle, and AMS arguments.`);
1444
+ }
1445
+
1398
1446
  const { threeMFPath, autoSliced } = await this.ensurePrintableThreeMFPath(
1399
1447
  args,
1400
1448
  printModel,
@@ -1411,89 +1459,127 @@ class BambuPrinterMCPServer {
1411
1459
  printNozzle
1412
1460
  );
1413
1461
  const threeMfFilename = path.basename(threeMFPath);
1414
- const projectName = String(args?.project_name || threeMfFilename.replace(/\.3mf$/i, ''));
1462
+ const projectName = String(args?.project_name || args?.task_name || threeMfFilename.replace(/\.3mf$/i, ''));
1415
1463
  const presetName = String(args?.preset_name || `${projectName}_plate_${plateIndex + 1}`);
1416
1464
  const clientJobId = args?.client_job_id !== undefined ? Number(args.client_job_id) : Date.now();
1417
1465
 
1418
- const amsMapping = stringifyBridgeJson(args?.ams_mapping_bridge ?? finalAmsMapping ?? finalAmsSlots);
1419
- const params: Record<string, unknown> = {
1420
- dev_id: devId,
1421
- task_name: String(args?.task_name || projectName),
1422
- project_name: projectName,
1423
- preset_name: presetName,
1424
- filename: threeMFPath,
1425
- config_filename: String(args?.config_filename || threeMFPath),
1426
- plate_index: plateIndex + 1,
1427
- ftp_folder: String(args?.ftp_folder || ""),
1428
- ftp_file: String(args?.ftp_file || ""),
1429
- ftp_file_md5: String(args?.ftp_file_md5 || ""),
1430
- nozzle_mapping: stringifyBridgeJson(args?.nozzle_mapping) || "",
1431
- ams_mapping: amsMapping || "",
1432
- ams_mapping2: stringifyBridgeJson(args?.ams_mapping2) || "",
1433
- ams_mapping_info: stringifyBridgeJson(args?.ams_mapping_info) || "",
1434
- nozzles_info: stringifyBridgeJson(args?.nozzles_info) || "",
1435
- connection_type: connectionType,
1436
- comments: String(args?.comments || ""),
1437
- origin_profile_id: args?.origin_profile_id !== undefined ? Number(args.origin_profile_id) : 0,
1438
- stl_design_id: args?.stl_design_id !== undefined ? Number(args.stl_design_id) : 0,
1439
- origin_model_id: String(args?.origin_model_id || ""),
1440
- print_type: String(args?.print_type || "from_normal"),
1441
- dst_file: String(args?.dst_file || ""),
1442
- dev_name: String(args?.dev_name || ""),
1443
- dev_ip: devIp,
1444
- use_ssl_for_ftp: args?.use_ssl_for_ftp !== undefined ? Boolean(args.use_ssl_for_ftp) : true,
1445
- use_ssl_for_mqtt: args?.use_ssl_for_mqtt !== undefined ? Boolean(args.use_ssl_for_mqtt) : true,
1446
- username: String(args?.username || "bblp"),
1447
- password,
1448
- task_bed_leveling: args?.bed_leveling !== undefined ? Boolean(args.bed_leveling) : true,
1449
- task_flow_cali: args?.flow_calibration !== undefined ? Boolean(args.flow_calibration) : true,
1450
- task_vibration_cali: args?.vibration_calibration !== undefined ? Boolean(args.vibration_calibration) : true,
1451
- task_layer_inspect: args?.layer_inspect !== undefined ? Boolean(args.layer_inspect) : false,
1452
- task_record_timelapse: args?.timelapse !== undefined ? Boolean(args.timelapse) : false,
1453
- task_use_ams: useAMS,
1454
- task_bed_type: printBedType,
1455
- extra_options: stringifyBridgeJson(args?.extra_options) || "",
1456
- auto_bed_leveling: args?.auto_bed_leveling !== undefined ? Number(args.auto_bed_leveling) : 0,
1457
- auto_flow_cali: args?.auto_flow_cali !== undefined ? Number(args.auto_flow_cali) : 0,
1458
- auto_offset_cali: args?.auto_offset_cali !== undefined ? Number(args.auto_offset_cali) : 0,
1459
- extruder_cali_manual_mode: args?.extruder_cali_manual_mode !== undefined ? Number(args.extruder_cali_manual_mode) : -1,
1460
- task_ext_change_assist: args?.external_change_assist !== undefined ? Boolean(args.external_change_assist) : false,
1461
- try_emmc_print: args?.try_emmc_print !== undefined ? Boolean(args.try_emmc_print) : false,
1462
- };
1466
+ return withPrinterOperation(devIp, devId, assertActive => withPrintSnapshot(threeMFPath, async (snapshot, retainForDispatchedUpload) => {
1467
+ const inspection = await inspectPrintFile(snapshot, { model: printModel, nozzleDiameters: printNozzles, plateIndex, bedType: printBedType });
1468
+ let mapping = finalAmsMapping?.slice();
1469
+ if (finalAmsSlots) {
1470
+ if (finalAmsSlots.length !== inspection.usedFilamentPositions.length) throw new Error("ams_slots must supply exactly one physical tray per used filament.");
1471
+ mapping = Array(inspection.materials.length).fill(-1);
1472
+ inspection.usedFilamentPositions.forEach((position, index) => { mapping![position] = finalAmsSlots[index]; });
1473
+ }
1474
+ if (!useAMS) {
1475
+ mapping = Array(inspection.materials.length).fill(-1);
1476
+ inspection.usedFilamentPositions.forEach(position => { mapping![position] = 254; });
1477
+ }
1478
+ const requirements = { ...inspection, amsMapping: mapping, useAMS };
1479
+ const initialStatus = await this.bambu.getSafetyStatus(devIp, devId, password || bambuToken);
1480
+ validatePrinterState(initialStatus, requirements);
1481
+ const bedClearance = await this.bambu.confirmPrintPreflight(devId, initialStatus, inspection);
1482
+ const confirmedStatus = await this.bambu.getSafetyStatus(devIp, devId, password || bambuToken);
1483
+ validatePrinterState(confirmedStatus, requirements);
1484
+ this.bambu.assertBedClearance(confirmedStatus, bedClearance);
1485
+ const amsMapping = stringifyBridgeJson(mapping);
1486
+ const remoteFileName = uniquePrintName(threeMFPath);
1487
+ const params: Record<string, unknown> = {
1488
+ dev_id: devId,
1489
+ // Firmware can report the task name instead of the uploaded filename.
1490
+ // Bind either report to this inspected artifact for a later safe resume.
1491
+ task_name: remoteFileName.replace(/(?:\.gcode)?\.3mf$/i, ""),
1492
+ project_name: projectName,
1493
+ preset_name: presetName,
1494
+ filename: snapshot,
1495
+ config_filename: snapshot,
1496
+ plate_index: plateIndex + 1,
1497
+ ftp_folder: String(args?.ftp_folder || ""),
1498
+ ftp_file: String(args?.ftp_file || ""),
1499
+ ftp_file_md5: String(args?.ftp_file_md5 || ""),
1500
+ nozzle_mapping: stringifyBridgeJson(args?.nozzle_mapping) || "",
1501
+ ams_mapping: amsMapping || "",
1502
+ ams_mapping2: stringifyBridgeJson(args?.ams_mapping2) || "",
1503
+ ams_mapping_info: stringifyBridgeJson(args?.ams_mapping_info) || "",
1504
+ nozzles_info: stringifyBridgeJson(args?.nozzles_info) || "",
1505
+ connection_type: connectionType,
1506
+ comments: String(args?.comments || ""),
1507
+ origin_profile_id: args?.origin_profile_id !== undefined ? Number(args.origin_profile_id) : 0,
1508
+ stl_design_id: args?.stl_design_id !== undefined ? Number(args.stl_design_id) : 0,
1509
+ origin_model_id: String(args?.origin_model_id || ""),
1510
+ print_type: String(args?.print_type || "from_normal"),
1511
+ dst_file: remoteFileName,
1512
+ dev_name: String(args?.dev_name || ""),
1513
+ dev_ip: devIp,
1514
+ use_ssl_for_ftp: args?.use_ssl_for_ftp !== undefined ? Boolean(args.use_ssl_for_ftp) : true,
1515
+ use_ssl_for_mqtt: args?.use_ssl_for_mqtt !== undefined ? Boolean(args.use_ssl_for_mqtt) : true,
1516
+ username: String(args?.username || "bblp"),
1517
+ password,
1518
+ task_bed_leveling: args?.bed_leveling !== undefined ? Boolean(args.bed_leveling) : true,
1519
+ task_flow_cali: args?.flow_calibration !== undefined ? Boolean(args.flow_calibration) : true,
1520
+ task_vibration_cali: args?.vibration_calibration !== undefined ? Boolean(args.vibration_calibration) : true,
1521
+ task_layer_inspect: args?.layer_inspect !== undefined ? Boolean(args.layer_inspect) : false,
1522
+ task_record_timelapse: args?.timelapse !== undefined ? Boolean(args.timelapse) : false,
1523
+ task_use_ams: useAMS,
1524
+ task_bed_type: printBedType,
1525
+ extra_options: stringifyBridgeJson(args?.extra_options) || "",
1526
+ auto_bed_leveling: args?.auto_bed_leveling !== undefined ? Number(args.auto_bed_leveling) : 0,
1527
+ auto_flow_cali: args?.auto_flow_cali !== undefined ? Number(args.auto_flow_cali) : 0,
1528
+ auto_offset_cali: args?.auto_offset_cali !== undefined ? Number(args.auto_offset_cali) : 0,
1529
+ extruder_cali_manual_mode: args?.extruder_cali_manual_mode !== undefined ? Number(args.extruder_cali_manual_mode) : -1,
1530
+ task_ext_change_assist: args?.external_change_assist !== undefined ? Boolean(args.external_change_assist) : false,
1531
+ try_emmc_print: args?.try_emmc_print !== undefined ? Boolean(args.try_emmc_print) : false,
1532
+ };
1463
1533
 
1464
- const bridgeResult = await this.bambuNetwork.callWithAgent(
1465
- bridgeMethod,
1466
- { client_job_id: clientJobId, params },
1467
- this.bridgeOptionsFromArgs(args, "print_3mf_bambu_network")
1468
- );
1534
+ assertActive();
1535
+ const bridgeResult = await this.bambuNetwork.callWithAgent(
1536
+ bridgeMethod,
1537
+ { client_job_id: clientJobId, params },
1538
+ {
1539
+ ...this.bridgeOptionsFromArgs(args, "print_3mf_bambu_network"),
1540
+ beforeDispatch: async method => {
1541
+ if (method !== bridgeMethod) return;
1542
+ assertActive();
1543
+ const dispatchStatus = await this.bambu.getSafetyStatus(devIp, devId, password || bambuToken);
1544
+ validatePrinterState(dispatchStatus, requirements);
1545
+ this.bambu.assertBedClearance(dispatchStatus, bedClearance);
1546
+ },
1547
+ assertDispatchAllowed: method => { if (method === bridgeMethod) assertActive(); },
1548
+ onDispatched: method => { if (method === bridgeMethod) retainForDispatchedUpload(); },
1549
+ }
1550
+ );
1469
1551
 
1470
- if (typeof bridgeResult === "object" && bridgeResult !== null && (bridgeResult as { ok?: unknown }).ok === false) {
1471
- throw new Error(`FULU BambuNetwork bridge method ${bridgeMethod} failed: ${String((bridgeResult as { error?: unknown }).error || "unknown bridge error")}`);
1472
- }
1473
- if (
1474
- typeof bridgeResult === "object" &&
1475
- bridgeResult !== null &&
1476
- typeof (bridgeResult as { value?: unknown }).value === "number" &&
1477
- (bridgeResult as { value: number }).value !== 0
1478
- ) {
1479
- const value = (bridgeResult as { value: number }).value;
1480
- throw new Error(`FULU BambuNetwork bridge method ${bridgeMethod} returned non-zero result ${value}.`);
1481
- }
1552
+ if (typeof bridgeResult === "object" && bridgeResult !== null && (bridgeResult as { ok?: unknown }).ok === false) {
1553
+ throw new Error(`FULU BambuNetwork bridge method ${bridgeMethod} failed: ${String((bridgeResult as { error?: unknown }).error || "unknown bridge error")}`);
1554
+ }
1555
+ if (
1556
+ typeof bridgeResult === "object" &&
1557
+ bridgeResult !== null &&
1558
+ typeof (bridgeResult as { value?: unknown }).value === "number" &&
1559
+ (bridgeResult as { value: number }).value !== 0
1560
+ ) {
1561
+ const value = (bridgeResult as { value: number }).value;
1562
+ throw new Error(`FULU BambuNetwork bridge method ${bridgeMethod} returned non-zero result ${value}.`);
1563
+ }
1482
1564
 
1483
- return {
1484
- status: "success",
1485
- message: `FULU BambuNetwork ${bridgePrintMethod} command for ${threeMfFilename} sent successfully.`,
1486
- bridgeMethod,
1487
- bridgeResult,
1488
- clientJobId,
1489
- autoSliced,
1490
- projectName,
1491
- plateIndex,
1492
- bridgePlateIndex: plateIndex + 1,
1493
- useAMS,
1494
- amsMapping: finalAmsMapping ?? finalAmsSlots,
1495
- params: redactPrintParams(params),
1496
- };
1565
+ assertActive();
1566
+ this.bambu.recordCheckedJob(devIp, devId, remoteFileName, requirements);
1567
+
1568
+ return {
1569
+ status: "success",
1570
+ message: `FULU BambuNetwork ${bridgePrintMethod} command for ${threeMfFilename} sent successfully.`,
1571
+ bridgeMethod,
1572
+ bridgeResult,
1573
+ clientJobId,
1574
+ autoSliced,
1575
+ projectName,
1576
+ plateIndex,
1577
+ bridgePlateIndex: plateIndex + 1,
1578
+ useAMS,
1579
+ amsMapping: mapping,
1580
+ params: redactPrintParams(params),
1581
+ };
1582
+ }));
1497
1583
  }
1498
1584
 
1499
1585
  private async getResolvedPrinterFilamentInventory(
@@ -2200,7 +2286,7 @@ class BambuPrinterMCPServer {
2200
2286
  plate_index: { type: "number", description: "Zero-based plate index to print from the sliced 3MF; converted to FULU's one-based PrintParams plate_index." },
2201
2287
  project_name: { type: "string", description: "Optional project name sent in FULU PrintParams; defaults to the 3MF filename without extension." },
2202
2288
  preset_name: { type: "string", description: "Optional preset name sent in FULU PrintParams; defaults to project plus one-based plate index." },
2203
- task_name: { type: "string", description: "Optional BambuNetwork task name; defaults to the project name." },
2289
+ 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." },
2204
2290
  config_filename: { type: "string", description: "Optional config 3MF path for cloud print; defaults to the same 3MF path." },
2205
2291
  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." },
2206
2292
  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." },
@@ -2211,6 +2297,7 @@ class BambuPrinterMCPServer {
2211
2297
  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." },
2212
2298
  slicer_profile: { type: "string", description: "Path to an optional slicer profile/config file for auto-slicing." },
2213
2299
  nozzle_diameter: { type: "string", description: "Nozzle diameter in mm for auto-slicing (default: 0.4)." },
2300
+ 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." },
2214
2301
  use_ams: { type: "boolean", description: "Whether to use the AMS; defaults to auto-detect from the 3MF mapping." },
2215
2302
  ams_mapping: { type: "array", description: "AMS slot mapping array used by both local MCP printing and FULU PrintParams.", items: { type: "number" } },
2216
2303
  ams_slots: { type: "array", description: "Per-used-filament AMS slot list, matching the local LAN print path.", items: { type: "number" } },
@@ -2274,13 +2361,14 @@ class BambuPrinterMCPServer {
2274
2361
  },
2275
2362
  {
2276
2363
  name: "upload_gcode",
2277
- description: "Upload a G-code file to the Bambu Lab printer",
2364
+ description: "Inspect a G-code file and upload a uniquely named copy without overwriting existing printer files.",
2278
2365
  inputSchema: {
2279
2366
  type: "object",
2280
2367
  properties: {
2281
2368
  filename: { type: "string", description: "Name for the file on the printer" },
2282
2369
  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." },
2283
2370
  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." },
2371
+ bambu_model: { type: "string", enum: [...VALID_BAMBU_MODELS], description: "Required printer model for inspecting the uploaded G-code, even when not starting it." },
2284
2372
  host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
2285
2373
  bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
2286
2374
  bambu_token: { type: "string", description: "Access token (default: value from env)" }
@@ -2300,7 +2388,7 @@ class BambuPrinterMCPServer {
2300
2388
  bambu_model: {
2301
2389
  type: "string",
2302
2390
  enum: [...VALID_BAMBU_MODELS],
2303
- description: "Required when print is true. Bambu Lab printer model used as a safety confirmation before starting the uploaded file."
2391
+ description: "Required for every printable .gcode or .3mf upload, including upload-only operations."
2304
2392
  },
2305
2393
  host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
2306
2394
  bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
@@ -2448,12 +2536,15 @@ class BambuPrinterMCPServer {
2448
2536
  },
2449
2537
  {
2450
2538
  name: "set_temperature",
2451
- description: "Set the temperature of a printer component (bed, nozzle)",
2539
+ 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.",
2452
2540
  inputSchema: {
2453
2541
  type: "object",
2454
2542
  properties: {
2455
2543
  component: { type: "string", description: "Component to heat: bed, nozzle, or extruder" },
2456
- temperature: { type: "number", description: "Target temperature in °C" },
2544
+ temperature: { type: "number", minimum: 0, description: "Finite target temperature in °C, checked against model/component and declared material limits" },
2545
+ bambu_model: { type: "string", enum: [...VALID_BAMBU_MODELS], description: "Printer model, required for positive heating unless configured in the environment" },
2546
+ material: { type: "string", description: "Loaded material, for example PLA or PETG. Required for positive nozzle heating, including non-RFID spools." },
2547
+ 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)" },
2457
2548
  host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
2458
2549
  bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
2459
2550
  bambu_token: { type: "string", description: "Access token (default: value from env)" }
@@ -2604,7 +2695,8 @@ class BambuPrinterMCPServer {
2604
2695
  template_3mf_path: { type: "string", description: "Optional template 3MF whose embedded Bambu slicer settings should be reused when auto-slicing this print job." },
2605
2696
  template_name: { type: "string", description: "Optional named template from the local registry. Resolves to template_3mf_path automatically." },
2606
2697
  template_dir: { type: "string", description: "Optional template directory override when resolving template_name." },
2607
- nozzle_diameter: { type: "string", description: "Nozzle diameter in mm for auto-slicing (default: 0.4)" }
2698
+ nozzle_diameter: { type: "string", description: "Nozzle diameter in mm for auto-slicing (default: 0.4)" },
2699
+ 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." }
2608
2700
  },
2609
2701
  required: ["three_mf_path", "bambu_model"]
2610
2702
  }
@@ -2852,6 +2944,10 @@ class BambuPrinterMCPServer {
2852
2944
  throw new Error("Missing required parameter: method");
2853
2945
  }
2854
2946
  const bridgeArgs = args as Record<string, any>;
2947
+ 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"]);
2948
+ if (!readMethods.has(String(bridgeArgs.method))) {
2949
+ throw new Error("Raw bridge mutations and unknown methods are disabled. Use print_3mf_bambu_network for verified printing or a dedicated printer tool.");
2950
+ }
2855
2951
  const payload = bridgeArgs.payload && typeof bridgeArgs.payload === "object"
2856
2952
  ? bridgeArgs.payload as Record<string, unknown>
2857
2953
  : {};
@@ -2902,8 +2998,9 @@ class BambuPrinterMCPServer {
2902
2998
  }
2903
2999
  const uploadSource = resolveUploadGcodeSource(args as Record<string, unknown>);
2904
3000
  try {
3001
+ const uploadModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
2905
3002
  result = await this.bambu.uploadFile(
2906
- host, bambuSerial, bambuToken, uploadSource.filePath, String(args.filename), false
3003
+ host, bambuSerial, bambuToken, uploadSource.filePath, String(args.filename), false, uploadModel
2907
3004
  );
2908
3005
  } finally {
2909
3006
  if (uploadSource.cleanupDir) {
@@ -2918,7 +3015,8 @@ class BambuPrinterMCPServer {
2918
3015
  throw new Error("Missing required parameters: file_path and filename");
2919
3016
  }
2920
3017
  const print = Boolean(args.print ?? false);
2921
- const uploadModel = print ? await this.resolveBambuModel(args?.bambu_model as string | undefined) : undefined;
3018
+ const printableUpload = /\.(?:gcode|3mf)$/i.test(String(args.file_path)) || /\.(?:gcode|3mf)$/i.test(String(args.filename));
3019
+ const uploadModel = print || printableUpload ? await this.resolveBambuModel(args?.bambu_model as string | undefined) : undefined;
2922
3020
  if (print) assertDirectPrintSupported(uploadModel, bambuSerial);
2923
3021
  result = await this.bambu.uploadFile(
2924
3022
  host, bambuSerial, bambuToken,
@@ -2982,15 +3080,22 @@ class BambuPrinterMCPServer {
2982
3080
  );
2983
3081
  break;
2984
3082
 
2985
- case "set_temperature":
3083
+ case "set_temperature": {
2986
3084
  if (!args?.component || args?.temperature === undefined) {
2987
3085
  throw new Error("Missing required parameters: component and temperature");
2988
3086
  }
3087
+ if (typeof args.temperature !== "number" || !Number.isFinite(args.temperature) || args.temperature < 0) {
3088
+ throw new Error("Temperature must be a finite, non-negative number in °C.");
3089
+ }
2989
3090
  result = await this.bambu.setTemperature(
2990
3091
  host, bambuSerial, bambuToken,
2991
- String(args.component), Number(args.temperature)
3092
+ String(args.component), args.temperature,
3093
+ args.temperature === 0 ? undefined : await this.resolveBambuModel(args?.bambu_model as string | undefined),
3094
+ args?.material !== undefined ? String(args.material) : undefined,
3095
+ Number(args?.nozzle_diameter ?? DEFAULT_NOZZLE_DIAMETER)
2992
3096
  );
2993
3097
  break;
3098
+ }
2994
3099
 
2995
3100
  case "set_fan_speed":
2996
3101
  if (!args?.fan || args?.speed === undefined) {
@@ -3290,7 +3395,8 @@ class BambuPrinterMCPServer {
3290
3395
  const printModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
3291
3396
  assertDirectPrintSupported(printModel, bambuSerial);
3292
3397
  const printBedType = resolveBedType(args?.bed_type as string | undefined);
3293
- const printNozzle = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
3398
+ const printNozzles = requestedPrintNozzles(args as Record<string, any>);
3399
+ const printNozzle = String(printNozzles?.[0] ?? DEFAULT_NOZZLE_DIAMETER);
3294
3400
  const activeSlicerProfile = await resolveTemplateFirstSlicerProfilePath(
3295
3401
  args,
3296
3402
  slicerProfile || undefined,
@@ -3315,6 +3421,7 @@ class BambuPrinterMCPServer {
3315
3421
  entry => !entry.dir && /\.gcode$/i.test(entry.name)
3316
3422
  );
3317
3423
  if (!hasGcode) {
3424
+ assertUniformCliNozzles(args as Record<string, any>);
3318
3425
  if (printBedType === "supertack_plate") {
3319
3426
  throw new Error(
3320
3427
  '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.'
@@ -3414,6 +3521,7 @@ class BambuPrinterMCPServer {
3414
3521
  }
3415
3522
  if (
3416
3523
  isH2Print &&
3524
+ args?.use_ams !== false &&
3417
3525
  !hasUserAmsMapping &&
3418
3526
  !hasUserAmsSlots &&
3419
3527
  args?.auto_match_ams !== true &&
@@ -3444,6 +3552,7 @@ class BambuPrinterMCPServer {
3444
3552
  projectName,
3445
3553
  filePath: threeMFPath,
3446
3554
  bambuModel: printModel,
3555
+ nozzleDiameters: printNozzles,
3447
3556
  plateIndex,
3448
3557
  useAMS: useAMS,
3449
3558
  amsMapping: finalAmsMapping,
@@ -3512,6 +3621,7 @@ class BambuPrinterMCPServer {
3512
3621
  projectName,
3513
3622
  filePath: preparedThreeMFPath,
3514
3623
  bambuModel: printModel,
3624
+ nozzleDiameters: [Number(printNozzle)],
3515
3625
  plateIndex: collarAnalysis.plateIndex,
3516
3626
  useAMS: true,
3517
3627
  amsSlots: collarAnalysis.amsSlots,