bambu-printer-mcp 1.0.4 → 1.0.8

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
@@ -8,13 +8,15 @@ import fs from "fs";
8
8
  import path from "path";
9
9
  import { createServer as createHttpServer } from "node:http";
10
10
  import { randomUUID } from "node:crypto";
11
- import { STLManipulator } from "./stl/stl-manipulator.js";
11
+ import { STLManipulator, SLICER_TYPES, normalizeSlicerType, } from "./stl/stl-manipulator.js";
12
+ import { BambuNetworkBridge } from "./bambu-network-bridge.js";
12
13
  import { parse3MF } from './3mf_parser.js';
13
14
  import { BambuImplementation } from "./printers/bambu.js";
14
15
  dotenv.config();
15
16
  const DEFAULT_HOST = process.env.PRINTER_HOST || "localhost";
16
17
  const DEFAULT_BAMBU_SERIAL = process.env.BAMBU_SERIAL || "";
17
18
  const DEFAULT_BAMBU_TOKEN = process.env.BAMBU_TOKEN || "";
19
+ const DEFAULT_BAMBU_DEV_ID = process.env.BAMBU_DEV_ID || DEFAULT_BAMBU_SERIAL;
18
20
  const TEMP_DIR = process.env.TEMP_DIR || path.join(process.cwd(), "temp");
19
21
  // Printer model and bed type
20
22
  const DEFAULT_BAMBU_MODEL = process.env.BAMBU_MODEL?.trim().toLowerCase() || "";
@@ -48,8 +50,48 @@ function resolveBedType(argsBedType) {
48
50
  }
49
51
  // Slicer configuration (defaults to bambustudio)
50
52
  const DEFAULT_SLICER_TYPE = process.env.SLICER_TYPE || "bambustudio";
51
- const DEFAULT_SLICER_PATH = process.env.SLICER_PATH || "/Applications/BambuStudio.app/Contents/MacOS/BambuStudio";
52
53
  const DEFAULT_SLICER_PROFILE = process.env.SLICER_PROFILE || "";
54
+ const SLICER_SCHEMA_VALUES = [
55
+ ...SLICER_TYPES,
56
+ "fulu-orca",
57
+ "fulu-orcaslicer",
58
+ "orca-studio",
59
+ "orca-bambulab",
60
+ ];
61
+ function firstExistingPath(paths, fallback) {
62
+ return paths.find((candidate) => fs.existsSync(candidate)) || fallback;
63
+ }
64
+ function defaultSlicerPathFor(slicerType) {
65
+ if (slicerType === "orcaslicer" || slicerType === "orcaslicer-bambulab") {
66
+ if (process.platform === "darwin") {
67
+ return firstExistingPath([
68
+ "/Applications/OrcaSlicer.app/Contents/MacOS/OrcaSlicer",
69
+ "/Applications/Orca Studio.app/Contents/MacOS/Orca Studio",
70
+ "/Applications/OrcaSlicer-BMCU.app/Contents/MacOS/OrcaSlicer",
71
+ ], "/Applications/OrcaSlicer.app/Contents/MacOS/OrcaSlicer");
72
+ }
73
+ if (process.platform === "win32") {
74
+ return "C:\\Program Files\\OrcaSlicer\\OrcaSlicer.exe";
75
+ }
76
+ return "OrcaSlicer";
77
+ }
78
+ if (slicerType === "bambustudio") {
79
+ if (process.platform === "darwin") {
80
+ return "/Applications/BambuStudio.app/Contents/MacOS/BambuStudio";
81
+ }
82
+ if (process.platform === "win32") {
83
+ return "C:\\Program Files\\Bambu Studio\\bambu-studio.exe";
84
+ }
85
+ return "BambuStudio";
86
+ }
87
+ return slicerType;
88
+ }
89
+ function resolveSlicerConfig(args) {
90
+ const slicerType = normalizeSlicerType(String(args?.slicer_type || DEFAULT_SLICER_TYPE));
91
+ const slicerPath = String(args?.slicer_path || process.env.SLICER_PATH || defaultSlicerPathFor(slicerType));
92
+ const slicerProfile = String(args?.slicer_profile || DEFAULT_SLICER_PROFILE);
93
+ return { slicerType, slicerPath, slicerProfile };
94
+ }
53
95
  function parseBooleanEnv(rawValue, fallback) {
54
96
  if (rawValue === undefined)
55
97
  return fallback;
@@ -96,6 +138,116 @@ function readRuntimeConfig() {
96
138
  blenderBridgeCommand: process.env.BLENDER_MCP_BRIDGE_COMMAND?.trim() || undefined,
97
139
  };
98
140
  }
141
+ function expandUserPath(rawPath) {
142
+ const trimmed = rawPath.trim();
143
+ if (trimmed === "~") {
144
+ return process.env.HOME || trimmed;
145
+ }
146
+ if (trimmed.startsWith("~/")) {
147
+ return path.join(process.env.HOME || "", trimmed.slice(2));
148
+ }
149
+ return path.resolve(trimmed);
150
+ }
151
+ function readableFilePathFromString(value) {
152
+ if (!value.trim() || value.includes("\n") || value.includes("\r")) {
153
+ return undefined;
154
+ }
155
+ const candidate = expandUserPath(value);
156
+ try {
157
+ return fs.statSync(candidate).isFile() ? candidate : undefined;
158
+ }
159
+ catch {
160
+ return undefined;
161
+ }
162
+ }
163
+ function looksLikeGcodeFilePath(value) {
164
+ const trimmed = value.trim();
165
+ if (!trimmed || trimmed.includes("\n") || trimmed.includes("\r")) {
166
+ return false;
167
+ }
168
+ return (trimmed.startsWith("/") ||
169
+ trimmed.startsWith("./") ||
170
+ trimmed.startsWith("../") ||
171
+ trimmed.startsWith("~/") ||
172
+ trimmed.includes("\\") ||
173
+ /\.(gcode|gco|gc)$/i.test(trimmed));
174
+ }
175
+ function requireReadableFilePath(rawPath, label) {
176
+ const candidate = expandUserPath(rawPath);
177
+ try {
178
+ if (!fs.statSync(candidate).isFile()) {
179
+ throw new Error(`${label} is not a file: ${candidate}`);
180
+ }
181
+ }
182
+ catch (error) {
183
+ if (error.message.startsWith(`${label} is not a file:`)) {
184
+ throw error;
185
+ }
186
+ throw new Error(`${label} does not exist or is not readable: ${candidate}`);
187
+ }
188
+ return candidate;
189
+ }
190
+ function writeGcodeContentToTempFile(filename, gcode) {
191
+ fs.mkdirSync(TEMP_DIR, { recursive: true });
192
+ const cleanupDir = fs.mkdtempSync(path.join(TEMP_DIR, "upload-gcode-"));
193
+ const safeName = path.basename(filename.replace(/^\/+/, "")) || "upload.gcode";
194
+ const filePath = path.join(cleanupDir, safeName);
195
+ fs.writeFileSync(filePath, gcode);
196
+ return { filePath, cleanupDir };
197
+ }
198
+ function resolveUploadGcodeSource(args) {
199
+ const gcodePath = args.gcode_path !== undefined ? String(args.gcode_path) : "";
200
+ const gcode = args.gcode !== undefined ? String(args.gcode) : "";
201
+ if (gcodePath && gcode) {
202
+ throw new Error("Provide either gcode_path or gcode, not both.");
203
+ }
204
+ if (gcodePath) {
205
+ return { filePath: requireReadableFilePath(gcodePath, "gcode_path") };
206
+ }
207
+ if (!gcode) {
208
+ throw new Error("Missing required parameter: gcode or gcode_path");
209
+ }
210
+ const detectedPath = readableFilePathFromString(gcode);
211
+ if (detectedPath) {
212
+ return { filePath: detectedPath };
213
+ }
214
+ if (looksLikeGcodeFilePath(gcode)) {
215
+ throw new Error("gcode looks like a local G-code path, but the file is not readable. " +
216
+ "Pass readable gcode_path or literal G-code content.");
217
+ }
218
+ return writeGcodeContentToTempFile(String(args.filename), gcode);
219
+ }
220
+ const BAMBU_NETWORK_PRINT_METHODS = [
221
+ "start_print",
222
+ "start_local_print",
223
+ "start_local_print_with_record",
224
+ "start_send_gcode_to_sdcard",
225
+ "start_sdcard_print",
226
+ ];
227
+ function resolveBambuNetworkPrintMethod(rawMethod, connectionType) {
228
+ const defaultMethod = connectionType === "lan" ? "start_local_print" : "start_print";
229
+ const method = (rawMethod || defaultMethod).trim();
230
+ if (!BAMBU_NETWORK_PRINT_METHODS.includes(method)) {
231
+ throw new Error(`Invalid bambu_network_method: "${method}". Valid methods: ${BAMBU_NETWORK_PRINT_METHODS.join(", ")}`);
232
+ }
233
+ return method;
234
+ }
235
+ function toBridgeMethod(method) {
236
+ return `net.${method}`;
237
+ }
238
+ function stringifyBridgeJson(value) {
239
+ if (value === undefined || value === null)
240
+ return undefined;
241
+ if (typeof value === "string")
242
+ return value;
243
+ return JSON.stringify(value);
244
+ }
245
+ function redactPrintParams(params) {
246
+ return {
247
+ ...params,
248
+ password: params.password ? "[redacted]" : "",
249
+ };
250
+ }
99
251
  if (!fs.existsSync(TEMP_DIR)) {
100
252
  fs.mkdirSync(TEMP_DIR, { recursive: true });
101
253
  }
@@ -112,6 +264,7 @@ class BambuPrinterMCPServer {
112
264
  }
113
265
  });
114
266
  this.bambu = new BambuImplementation();
267
+ this.bambuNetwork = new BambuNetworkBridge();
115
268
  this.stlManipulator = new STLManipulator(TEMP_DIR);
116
269
  this.setupHandlers();
117
270
  this.setupErrorHandling();
@@ -134,7 +287,7 @@ class BambuPrinterMCPServer {
134
287
  if (fromArgs) {
135
288
  return validateBambuModel(fromArgs);
136
289
  }
137
- // No model from args or env — ask the user via elicitation
290
+ // No model from args or env - ask the user via elicitation
138
291
  try {
139
292
  const result = await this.server.elicitInput({
140
293
  mode: "form",
@@ -167,7 +320,7 @@ class BambuPrinterMCPServer {
167
320
  throw new Error("Printer model selection was cancelled. Cannot proceed without knowing the printer model.");
168
321
  }
169
322
  catch (elicitError) {
170
- // Elicitation not supported by this client — fall back to a clear error
323
+ // Elicitation not supported by this client - fall back to a clear error
171
324
  const msg = elicitError?.message || String(elicitError);
172
325
  if (elicitError?.code === -32601 || elicitError?.code === -32600 ||
173
326
  msg.includes("does not support") || msg.includes("elicitation")) {
@@ -178,6 +331,182 @@ class BambuPrinterMCPServer {
178
331
  throw elicitError;
179
332
  }
180
333
  }
334
+ bridgeOptionsFromArgs(args) {
335
+ return {
336
+ bridgeCommand: args?.bridge_command !== undefined ? String(args.bridge_command) : undefined,
337
+ configDir: args?.bambu_network_config_dir !== undefined ? String(args.bambu_network_config_dir) : undefined,
338
+ countryCode: args?.country_code !== undefined ? String(args.country_code) : undefined,
339
+ userInfo: args?.user_info !== undefined ? String(args.user_info) : undefined,
340
+ timeoutMs: args?.timeout_ms !== undefined ? Number(args.timeout_ms) : undefined,
341
+ };
342
+ }
343
+ async ensurePrintableThreeMFPath(args, printModel, printPreset) {
344
+ const { slicerType, slicerPath, slicerProfile } = resolveSlicerConfig(args);
345
+ let threeMFPath = String(args.three_mf_path);
346
+ const JSZip = (await import('jszip')).default;
347
+ const zipData = fs.readFileSync(threeMFPath);
348
+ const zip = await JSZip.loadAsync(zipData);
349
+ const hasGcode = Object.keys(zip.files).some(f => f.match(/Metadata\/plate_\d+\.gcode/i) || f.endsWith('.gcode'));
350
+ if (hasGcode) {
351
+ return { threeMFPath, autoSliced: false };
352
+ }
353
+ console.log(`3MF has no gcode - auto-slicing with ${slicerType} for ${printModel}`);
354
+ const autoSliceOptions = {
355
+ uptodate: true,
356
+ ensureOnBed: true,
357
+ minSave: true,
358
+ skipModifiedGcodes: true,
359
+ };
360
+ threeMFPath = await this.stlManipulator.sliceSTL(threeMFPath, slicerType, slicerPath, slicerProfile || undefined, undefined, printPreset, autoSliceOptions);
361
+ console.log("Auto-sliced to: " + threeMFPath);
362
+ return { threeMFPath, autoSliced: true };
363
+ }
364
+ async resolveAmsPrintSettings(threeMFPath, args) {
365
+ const parsed3MFData = await parse3MF(threeMFPath);
366
+ let parsedAmsMapping;
367
+ if (parsed3MFData.slicerConfig?.ams_mapping) {
368
+ const slots = Object.values(parsed3MFData.slicerConfig.ams_mapping)
369
+ .filter(v => typeof v === 'number');
370
+ if (slots.length > 0) {
371
+ parsedAmsMapping = slots.sort((a, b) => a - b);
372
+ }
373
+ }
374
+ let finalAmsMapping = parsedAmsMapping;
375
+ const explicitUseAMS = args?.use_ams !== undefined;
376
+ let useAMS = explicitUseAMS ? Boolean(args.use_ams) : (!!finalAmsMapping && finalAmsMapping.length > 0);
377
+ if (args?.ams_mapping) {
378
+ let userMappingOverride;
379
+ if (Array.isArray(args.ams_mapping)) {
380
+ userMappingOverride = args.ams_mapping.filter((v) => typeof v === 'number');
381
+ }
382
+ else if (typeof args.ams_mapping === 'object') {
383
+ userMappingOverride = Object.values(args.ams_mapping)
384
+ .filter((v) => typeof v === 'number')
385
+ .sort((a, b) => a - b);
386
+ }
387
+ if (userMappingOverride && userMappingOverride.length > 0) {
388
+ finalAmsMapping = userMappingOverride;
389
+ useAMS = true;
390
+ }
391
+ }
392
+ if (args?.use_ams === false) {
393
+ finalAmsMapping = undefined;
394
+ useAMS = false;
395
+ }
396
+ if ((!finalAmsMapping || finalAmsMapping.length === 0) && !explicitUseAMS) {
397
+ useAMS = false;
398
+ }
399
+ return { useAMS, finalAmsMapping };
400
+ }
401
+ async print3mfViaBambuNetwork(args, host, bambuSerial, bambuToken) {
402
+ if (!args?.three_mf_path) {
403
+ throw new Error("Missing required parameter: three_mf_path");
404
+ }
405
+ const printModel = await this.resolveBambuModel(args?.bambu_model);
406
+ const printBedType = resolveBedType(args?.bed_type);
407
+ const printNozzle = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
408
+ const printPreset = BAMBU_MODEL_PRESETS[printModel]?.(printNozzle);
409
+ const plateIndex = args?.plate_index !== undefined ? Number(args.plate_index) : 0;
410
+ if (!Number.isInteger(plateIndex) || plateIndex < 0) {
411
+ throw new Error("plate_index must be a non-negative integer.");
412
+ }
413
+ const connectionType = String(args?.connection_type || "cloud").trim().toLowerCase();
414
+ if (!["cloud", "lan"].includes(connectionType)) {
415
+ throw new Error('connection_type must be "cloud" or "lan".');
416
+ }
417
+ const bridgePrintMethod = resolveBambuNetworkPrintMethod(args?.bambu_network_method !== undefined ? String(args.bambu_network_method) : undefined, connectionType);
418
+ const bridgeMethod = toBridgeMethod(bridgePrintMethod);
419
+ const isLocalBridgePrint = bridgePrintMethod !== "start_print";
420
+ const devId = String(args?.dev_id || bambuSerial || DEFAULT_BAMBU_DEV_ID).trim();
421
+ if (!devId) {
422
+ throw new Error("dev_id is required for FULU BambuNetwork printing. Pass dev_id or set BAMBU_DEV_ID/BAMBU_SERIAL.");
423
+ }
424
+ const devIp = String(args?.dev_ip || args?.host || host || "").trim();
425
+ const explicitPassword = String(args?.password || args?.bambu_token || "").trim();
426
+ const password = isLocalBridgePrint ? (explicitPassword || String(bambuToken || "").trim()) : explicitPassword;
427
+ if (isLocalBridgePrint && (!devIp || devIp === "localhost")) {
428
+ throw new Error("dev_ip or host is required for FULU BambuNetwork LAN/local print methods.");
429
+ }
430
+ if (isLocalBridgePrint && !password) {
431
+ throw new Error("bambu_token/access code is required for FULU BambuNetwork LAN/local print methods.");
432
+ }
433
+ const { threeMFPath, autoSliced } = await this.ensurePrintableThreeMFPath(args, printModel, printPreset);
434
+ const { useAMS, finalAmsMapping } = await this.resolveAmsPrintSettings(threeMFPath, args);
435
+ const threeMfFilename = path.basename(threeMFPath);
436
+ const projectName = String(args?.project_name || threeMfFilename.replace(/\.3mf$/i, ''));
437
+ const presetName = String(args?.preset_name || `${projectName}_plate_${plateIndex + 1}`);
438
+ const clientJobId = args?.client_job_id !== undefined ? Number(args.client_job_id) : Date.now();
439
+ const amsMapping = stringifyBridgeJson(args?.ams_mapping_bridge ?? finalAmsMapping);
440
+ const params = {
441
+ dev_id: devId,
442
+ task_name: String(args?.task_name || projectName),
443
+ project_name: projectName,
444
+ preset_name: presetName,
445
+ filename: threeMFPath,
446
+ config_filename: String(args?.config_filename || threeMFPath),
447
+ plate_index: plateIndex + 1,
448
+ ftp_folder: String(args?.ftp_folder || ""),
449
+ ftp_file: String(args?.ftp_file || ""),
450
+ ftp_file_md5: String(args?.ftp_file_md5 || ""),
451
+ nozzle_mapping: stringifyBridgeJson(args?.nozzle_mapping) || "",
452
+ ams_mapping: amsMapping || "",
453
+ ams_mapping2: stringifyBridgeJson(args?.ams_mapping2) || "",
454
+ ams_mapping_info: stringifyBridgeJson(args?.ams_mapping_info) || "",
455
+ nozzles_info: stringifyBridgeJson(args?.nozzles_info) || "",
456
+ connection_type: connectionType,
457
+ comments: String(args?.comments || ""),
458
+ origin_profile_id: args?.origin_profile_id !== undefined ? Number(args.origin_profile_id) : 0,
459
+ stl_design_id: args?.stl_design_id !== undefined ? Number(args.stl_design_id) : 0,
460
+ origin_model_id: String(args?.origin_model_id || ""),
461
+ print_type: String(args?.print_type || "from_normal"),
462
+ dst_file: String(args?.dst_file || ""),
463
+ dev_name: String(args?.dev_name || ""),
464
+ dev_ip: devIp,
465
+ use_ssl_for_ftp: args?.use_ssl_for_ftp !== undefined ? Boolean(args.use_ssl_for_ftp) : true,
466
+ use_ssl_for_mqtt: args?.use_ssl_for_mqtt !== undefined ? Boolean(args.use_ssl_for_mqtt) : true,
467
+ username: String(args?.username || "bblp"),
468
+ password,
469
+ task_bed_leveling: args?.bed_leveling !== undefined ? Boolean(args.bed_leveling) : true,
470
+ task_flow_cali: args?.flow_calibration !== undefined ? Boolean(args.flow_calibration) : true,
471
+ task_vibration_cali: args?.vibration_calibration !== undefined ? Boolean(args.vibration_calibration) : true,
472
+ task_layer_inspect: args?.layer_inspect !== undefined ? Boolean(args.layer_inspect) : false,
473
+ task_record_timelapse: args?.timelapse !== undefined ? Boolean(args.timelapse) : false,
474
+ task_use_ams: useAMS,
475
+ task_bed_type: printBedType,
476
+ extra_options: stringifyBridgeJson(args?.extra_options) || "",
477
+ auto_bed_leveling: args?.auto_bed_leveling !== undefined ? Number(args.auto_bed_leveling) : 0,
478
+ auto_flow_cali: args?.auto_flow_cali !== undefined ? Number(args.auto_flow_cali) : 0,
479
+ auto_offset_cali: args?.auto_offset_cali !== undefined ? Number(args.auto_offset_cali) : 0,
480
+ extruder_cali_manual_mode: args?.extruder_cali_manual_mode !== undefined ? Number(args.extruder_cali_manual_mode) : -1,
481
+ task_ext_change_assist: args?.external_change_assist !== undefined ? Boolean(args.external_change_assist) : false,
482
+ try_emmc_print: args?.try_emmc_print !== undefined ? Boolean(args.try_emmc_print) : false,
483
+ };
484
+ const bridgeResult = await this.bambuNetwork.callWithAgent(bridgeMethod, { client_job_id: clientJobId, params }, this.bridgeOptionsFromArgs(args));
485
+ if (typeof bridgeResult === "object" && bridgeResult !== null && bridgeResult.ok === false) {
486
+ throw new Error(`FULU BambuNetwork bridge method ${bridgeMethod} failed: ${String(bridgeResult.error || "unknown bridge error")}`);
487
+ }
488
+ if (typeof bridgeResult === "object" &&
489
+ bridgeResult !== null &&
490
+ typeof bridgeResult.value === "number" &&
491
+ bridgeResult.value !== 0) {
492
+ const value = bridgeResult.value;
493
+ throw new Error(`FULU BambuNetwork bridge method ${bridgeMethod} returned non-zero result ${value}.`);
494
+ }
495
+ return {
496
+ status: "success",
497
+ message: `FULU BambuNetwork ${bridgePrintMethod} command for ${threeMfFilename} sent successfully.`,
498
+ bridgeMethod,
499
+ bridgeResult,
500
+ clientJobId,
501
+ autoSliced,
502
+ projectName,
503
+ plateIndex,
504
+ bridgePlateIndex: plateIndex + 1,
505
+ useAMS,
506
+ amsMapping: finalAmsMapping,
507
+ params: redactPrintParams(params),
508
+ };
509
+ }
181
510
  setupResourceHandlers() {
182
511
  this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
183
512
  return {
@@ -329,12 +658,13 @@ class BambuPrinterMCPServer {
329
658
  },
330
659
  slicer_type: {
331
660
  type: "string",
332
- description: "Type of slicer to use (bambustudio, prusaslicer, cura, slic3r, orcaslicer) (default: bambustudio)"
661
+ enum: SLICER_SCHEMA_VALUES,
662
+ description: "Type of slicer to use. Bambu-compatible choices (bambustudio, orcaslicer, orcaslicer-bambulab) export sliced 3MF; aliases such as fulu-orca and orca-studio are accepted."
333
663
  },
334
- slicer_path: { type: "string", description: "Path to the slicer executable (default: value from env)" },
664
+ slicer_path: { type: "string", description: "Path to the slicer executable (default: value from env or a platform default)" },
335
665
  slicer_profile: { type: "string", description: "Path to the slicer profile/config file (optional, overrides bambu_model preset)" },
336
666
  nozzle_diameter: { type: "string", description: "Nozzle diameter in mm (default: 0.4)" },
337
- uptodate: { type: "boolean", description: "Refresh 3MF preset configs to match the latest BambuStudio version. Use when slicing downloaded or older 3MF files to prevent stale-config failures." },
667
+ uptodate: { type: "boolean", description: "Refresh 3MF preset configs to match the latest Bambu-compatible slicer presets. Use when slicing downloaded or older 3MF files to prevent stale-config failures." },
338
668
  repetitions: { type: "number", description: "Print N identical copies of the model. Each copy gets its own plate placement. Example: 3 prints three copies." },
339
669
  orient: { type: "boolean", description: "Auto-orient the model for optimal printability (minimize supports, maximize bed adhesion). Recommended for raw STL imports that lack a pre-set orientation." },
340
670
  arrange: { type: "boolean", description: "Auto-arrange all objects on the build plate with optimal spacing. Recommended when importing STLs or adding multiple objects. Set false to preserve existing plate layout." },
@@ -342,6 +672,7 @@ class BambuPrinterMCPServer {
342
672
  clone_objects: { type: "string", description: "Duplicate specific objects on the plate. Comma-separated clone counts per object index, e.g. '1,3,1,10' clones object 0 once, object 1 three times, etc." },
343
673
  skip_objects: { type: "string", description: "Skip specific objects during slicing by index. Comma-separated, e.g. '3,5,10'. Useful for multi-object 3MFs where you only want to print some parts." },
344
674
  load_filaments: { type: "string", description: "Override filament profiles. Semicolon-separated paths to filament JSON configs, e.g. 'pla_basic.json;petg_cf.json'." },
675
+ filament_profile: { type: "string", description: "Compatibility alias for load_filaments. Semicolon-separated Orca/Bambu filament profile JSON paths; flat self-contained profiles are safest because Orca does not resolve every inherited system filament setting from arbitrary file paths." },
345
676
  load_filament_ids: { type: "string", description: "Map filaments to objects/parts. Comma-separated IDs matching load_filaments order, e.g. '1,2,3,1' assigns filament 1 to objects 0 and 3." },
346
677
  enable_timelapse: { type: "boolean", description: "Insert timelapse parking moves into gcode. The toolhead parks at a fixed position each layer for camera capture. Adds ~10% print time." },
347
678
  allow_mix_temp: { type: "boolean", description: "Allow filaments with different temperature requirements on the same plate. Required for multi-material prints mixing e.g. PLA and PETG." },
@@ -368,6 +699,97 @@ class BambuPrinterMCPServer {
368
699
  }
369
700
  }
370
701
  },
702
+ {
703
+ name: "bambu_network_bridge_status",
704
+ description: "Inspect or probe the FULU OrcaSlicer-bambulab BambuNetwork bridge runtime used for cloud and restored BambuNetwork printing.",
705
+ inputSchema: {
706
+ type: "object",
707
+ properties: {
708
+ connect: { type: "boolean", description: "When true, start the bridge command and run a handshake plus agent initialization probe." },
709
+ bridge_command: { type: "string", description: "Override command for the FULU bridge host or macOS/WSL wrapper; defaults to BAMBU_NETWORK_BRIDGE_COMMAND." },
710
+ 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." },
711
+ country_code: { type: "string", description: "BambuNetwork country code, such as US, used by the agent during startup." },
712
+ user_info: { type: "string", description: "Optional BambuNetwork user_info JSON string to pass to net.change_user after the agent starts." },
713
+ timeout_ms: { type: "number", description: "Bridge request timeout in milliseconds for the connect probe." }
714
+ }
715
+ }
716
+ },
717
+ {
718
+ name: "bambu_network_call",
719
+ description: "Call a raw FULU OrcaSlicer-bambulab BambuNetwork bridge method, optionally with an initialized network agent injected into the payload.",
720
+ inputSchema: {
721
+ type: "object",
722
+ properties: {
723
+ method: { type: "string", description: "FULU bridge method name, for example bridge.handshake, net.is_user_login, or net.get_user_selected_machine." },
724
+ payload: { type: "object", description: "JSON payload passed to the bridge method." },
725
+ with_agent: { type: "boolean", description: "When true, initialize a BambuNetwork agent and add its agent id to the payload before calling the method." },
726
+ bridge_command: { type: "string", description: "Override command for the FULU bridge host or macOS/WSL wrapper; defaults to BAMBU_NETWORK_BRIDGE_COMMAND." },
727
+ 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." },
728
+ country_code: { type: "string", description: "BambuNetwork country code, such as US, used by the agent during startup." },
729
+ user_info: { type: "string", description: "Optional BambuNetwork user_info JSON string to pass to net.change_user after the agent starts." },
730
+ timeout_ms: { type: "number", description: "Bridge request timeout in milliseconds." }
731
+ },
732
+ required: ["method"]
733
+ }
734
+ },
735
+ {
736
+ name: "print_3mf_bambu_network",
737
+ description: "Print a 3MF through FULU OrcaSlicer-bambulab's restored BambuNetwork path instead of the MCP LAN MQTT/FTPS path.",
738
+ inputSchema: {
739
+ type: "object",
740
+ properties: {
741
+ three_mf_path: { type: "string", description: "Path to the 3MF file to print; unsliced 3MFs are auto-sliced before sending." },
742
+ bambu_model: {
743
+ type: "string",
744
+ enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
745
+ description: "REQUIRED: Bambu Lab printer model. Ask the user if not known. Using the wrong model can damage the printer."
746
+ },
747
+ connection_type: { type: "string", enum: ["cloud", "lan"], description: "BambuNetwork connection type to put in FULU PrintParams; cloud uses restored internet printing, lan uses local bridge printing." },
748
+ bambu_network_method: { type: "string", enum: BAMBU_NETWORK_PRINT_METHODS, description: "FULU print method to invoke; defaults to start_print for cloud and start_local_print for lan." },
749
+ dev_id: { type: "string", description: "Bambu device id used by BambuNetwork; defaults to BAMBU_DEV_ID or BAMBU_SERIAL." },
750
+ dev_ip: { type: "string", description: "Printer IP address for LAN/local bridge methods; defaults to host when provided." },
751
+ host: { type: "string", description: "Printer host or IP address, used as dev_ip for LAN/local bridge methods." },
752
+ bambu_serial: { type: "string", description: "Fallback Bambu device id when dev_id is not supplied." },
753
+ bambu_token: { type: "string", description: "Printer access code/password for LAN/local bridge methods." },
754
+ username: { type: "string", description: "Printer username for LAN/local bridge methods; defaults to bblp." },
755
+ password: { type: "string", description: "Printer password/access code override for LAN/local bridge methods." },
756
+ bed_type: { type: "string", enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate"], description: "Bed plate type currently installed (default: textured_plate)." },
757
+ plate_index: { type: "number", description: "Zero-based plate index to print from the sliced 3MF; converted to FULU's one-based PrintParams plate_index." },
758
+ project_name: { type: "string", description: "Optional project name sent in FULU PrintParams; defaults to the 3MF filename without extension." },
759
+ preset_name: { type: "string", description: "Optional preset name sent in FULU PrintParams; defaults to project plus one-based plate index." },
760
+ task_name: { type: "string", description: "Optional BambuNetwork task name; defaults to the project name." },
761
+ config_filename: { type: "string", description: "Optional config 3MF path for cloud print; defaults to the same 3MF path." },
762
+ bridge_command: { type: "string", description: "Override command for the FULU bridge host or macOS/WSL wrapper; defaults to BAMBU_NETWORK_BRIDGE_COMMAND." },
763
+ 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." },
764
+ country_code: { type: "string", description: "BambuNetwork country code, such as US, used by the agent during startup." },
765
+ user_info: { type: "string", description: "Optional BambuNetwork user_info JSON string to pass to net.change_user after the agent starts." },
766
+ timeout_ms: { type: "number", description: "Bridge request timeout in milliseconds." },
767
+ slicer_type: { type: "string", enum: SLICER_SCHEMA_VALUES, description: "Slicer to use only if auto-slicing an unsliced 3MF; use orcaslicer-bambulab for FULU's fork." },
768
+ slicer_path: { type: "string", description: "Path to the slicer executable for auto-slicing; defaults to value from env or a platform default." },
769
+ slicer_profile: { type: "string", description: "Path to an optional slicer profile/config file for auto-slicing." },
770
+ nozzle_diameter: { type: "string", description: "Nozzle diameter in mm for auto-slicing (default: 0.4)." },
771
+ use_ams: { type: "boolean", description: "Whether to use the AMS; defaults to auto-detect from the 3MF mapping." },
772
+ ams_mapping: { type: "array", description: "AMS slot mapping array used by both local MCP printing and FULU PrintParams.", items: { type: "number" } },
773
+ ams_mapping_bridge: { type: "string", description: "Raw JSON string override for FULU PrintParams ams_mapping when the automatic array is not enough." },
774
+ ams_mapping2: { type: "string", description: "Raw JSON string for FULU PrintParams ams_mapping2, matching OrcaSlicer-bambulab's v1 AMS mapping field." },
775
+ ams_mapping_info: { type: "string", description: "Raw JSON string for FULU PrintParams ams_mapping_info, matching OrcaSlicer-bambulab's detailed AMS mapping field." },
776
+ nozzle_mapping: { type: "string", description: "Raw JSON string for FULU PrintParams nozzle_mapping." },
777
+ nozzles_info: { type: "string", description: "Raw JSON string for FULU PrintParams nozzles_info." },
778
+ bed_leveling: { type: "boolean", description: "Enable auto bed leveling in FULU PrintParams (default: true)." },
779
+ flow_calibration: { type: "boolean", description: "Enable flow calibration in FULU PrintParams (default: true)." },
780
+ vibration_calibration: { type: "boolean", description: "Enable vibration calibration in FULU PrintParams (default: true)." },
781
+ layer_inspect: { type: "boolean", description: "Enable first-layer inspection where supported (default: false for BambuNetwork bridge)." },
782
+ timelapse: { type: "boolean", description: "Enable timelapse recording in FULU PrintParams (default: false)." },
783
+ use_ssl_for_ftp: { type: "boolean", description: "Whether FULU local print should use SSL for FTP (default: true)." },
784
+ use_ssl_for_mqtt: { type: "boolean", description: "Whether FULU local print should use SSL for MQTT (default: true)." },
785
+ external_change_assist: { type: "boolean", description: "Enable FULU PrintParams task_ext_change_assist for external filament change assistance." },
786
+ try_emmc_print: { type: "boolean", description: "Enable FULU PrintParams try_emmc_print for printers that support internal storage printing." },
787
+ extra_options: { type: "string", description: "Raw JSON string or text for FULU PrintParams extra_options." },
788
+ client_job_id: { type: "number", description: "Optional client job id sent to the bridge; defaults to the current timestamp." }
789
+ },
790
+ required: ["three_mf_path", "bambu_model"]
791
+ }
792
+ },
371
793
  {
372
794
  name: "upload_gcode",
373
795
  description: "Upload a G-code file to the Bambu Lab printer",
@@ -375,12 +797,17 @@ class BambuPrinterMCPServer {
375
797
  type: "object",
376
798
  properties: {
377
799
  filename: { type: "string", description: "Name for the file on the printer" },
378
- gcode: { type: "string", description: "G-code content to upload" },
800
+ gcode: { type: "string", description: "G-code content to upload, or a readable local .gcode path. For large files, prefer gcode_path." },
801
+ gcode_path: { type: "string", description: "Local path to a .gcode file to upload. This avoids sending large G-code bodies through the MCP request." },
379
802
  host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
380
803
  bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
381
804
  bambu_token: { type: "string", description: "Access token (default: value from env)" }
382
805
  },
383
- required: ["filename", "gcode"]
806
+ required: ["filename"],
807
+ anyOf: [
808
+ { required: ["gcode"] },
809
+ { required: ["gcode_path"] }
810
+ ]
384
811
  }
385
812
  },
386
813
  {
@@ -392,6 +819,11 @@ class BambuPrinterMCPServer {
392
819
  file_path: { type: "string", description: "Local path to the file to upload" },
393
820
  filename: { type: "string", description: "Name for the file on the printer" },
394
821
  print: { type: "boolean", description: "Start printing after upload (default: false)" },
822
+ bambu_model: {
823
+ type: "string",
824
+ enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
825
+ description: "Required when print is true. Bambu Lab printer model used as a safety confirmation before starting the uploaded file."
826
+ },
395
827
  host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
396
828
  bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
397
829
  bambu_token: { type: "string", description: "Access token (default: value from env)" }
@@ -399,6 +831,25 @@ class BambuPrinterMCPServer {
399
831
  required: ["file_path", "filename"]
400
832
  }
401
833
  },
834
+ {
835
+ name: "start_print",
836
+ description: "Start printing a G-code file already on the Bambu Lab printer. Alias of start_print_job for upstream MCP compatibility.",
837
+ inputSchema: {
838
+ type: "object",
839
+ properties: {
840
+ filename: { type: "string", description: "Name of the file to print" },
841
+ bambu_model: {
842
+ type: "string",
843
+ enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
844
+ description: "REQUIRED: Bambu Lab printer model. Ask the user if not known. Starting G-code for the wrong model can damage the printer."
845
+ },
846
+ host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
847
+ bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
848
+ bambu_token: { type: "string", description: "Access token (default: value from env)" }
849
+ },
850
+ required: ["filename", "bambu_model"]
851
+ }
852
+ },
402
853
  {
403
854
  name: "start_print_job",
404
855
  description: "Start printing a G-code file already on the Bambu Lab printer",
@@ -406,11 +857,16 @@ class BambuPrinterMCPServer {
406
857
  type: "object",
407
858
  properties: {
408
859
  filename: { type: "string", description: "Name of the file to print" },
860
+ bambu_model: {
861
+ type: "string",
862
+ enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
863
+ description: "REQUIRED: Bambu Lab printer model. Ask the user if not known. Starting G-code for the wrong model can damage the printer."
864
+ },
409
865
  host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
410
866
  bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
411
867
  bambu_token: { type: "string", description: "Access token (default: value from env)" }
412
868
  },
413
- required: ["filename"]
869
+ required: ["filename", "bambu_model"]
414
870
  }
415
871
  },
416
872
  {
@@ -452,14 +908,35 @@ class BambuPrinterMCPServer {
452
908
  enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
453
909
  description: "REQUIRED: Bambu Lab printer model. Ask the user if not known. Using the wrong model can damage the printer."
454
910
  },
911
+ connection_mode: {
912
+ type: "string",
913
+ enum: ["lan_mqtt_ftps", "bambu_network"],
914
+ description: "Print path to use: lan_mqtt_ftps uses this MCP's direct local MQTT/FTPS path; bambu_network uses FULU OrcaSlicer-bambulab's restored BambuNetwork bridge."
915
+ },
916
+ connection_type: { type: "string", enum: ["cloud", "lan"], description: "BambuNetwork connection type when connection_mode is bambu_network; cloud uses restored internet printing, lan uses local bridge printing." },
917
+ bambu_network_method: { type: "string", enum: BAMBU_NETWORK_PRINT_METHODS, description: "FULU print method when connection_mode is bambu_network; defaults to start_print for cloud and start_local_print for lan." },
918
+ dev_id: { type: "string", description: "Bambu device id for FULU BambuNetwork printing; defaults to BAMBU_DEV_ID or BAMBU_SERIAL." },
919
+ dev_ip: { type: "string", description: "Printer IP address for FULU BambuNetwork LAN/local print methods; defaults to host when provided." },
920
+ bridge_command: { type: "string", description: "Override command for the FULU bridge host or macOS/WSL wrapper; defaults to BAMBU_NETWORK_BRIDGE_COMMAND." },
921
+ bambu_network_config_dir: { type: "string", description: "Config/log directory used by the FULU BambuNetwork agent." },
922
+ country_code: { type: "string", description: "BambuNetwork country code, such as US, used by the FULU bridge agent." },
923
+ user_info: { type: "string", description: "Optional BambuNetwork user_info JSON string passed to net.change_user for the FULU bridge." },
455
924
  bed_type: {
456
925
  type: "string",
457
926
  enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate"],
458
927
  description: "Bed plate type currently installed (default: textured_plate)"
459
928
  },
929
+ plate_index: { type: "number", description: "Zero-based plate index to print from the sliced 3MF (default: 0)" },
460
930
  host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
461
931
  bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
462
932
  bambu_token: { type: "string", description: "Access token (default: value from env)" },
933
+ slicer_type: {
934
+ type: "string",
935
+ enum: SLICER_SCHEMA_VALUES,
936
+ description: "Slicer to use only if auto-slicing an unsliced 3MF. Use orcaslicer-bambulab for FULU's OrcaSlicer-bambulab fork; aliases such as fulu-orca and orca-studio are accepted."
937
+ },
938
+ slicer_path: { type: "string", description: "Path to the slicer executable for auto-slicing (default: value from env or a platform default)" },
939
+ slicer_profile: { type: "string", description: "Path to an optional slicer profile/config file for auto-slicing" },
463
940
  use_ams: { type: "boolean", description: "Whether to use the AMS (default: auto-detect from 3MF)" },
464
941
  ams_mapping: {
465
942
  type: "array",
@@ -469,6 +946,7 @@ class BambuPrinterMCPServer {
469
946
  bed_leveling: { type: "boolean", description: "Enable auto bed leveling (default: true)" },
470
947
  flow_calibration: { type: "boolean", description: "Enable flow calibration (default: true)" },
471
948
  vibration_calibration: { type: "boolean", description: "Enable vibration calibration (default: true)" },
949
+ layer_inspect: { type: "boolean", description: "Enable first-layer inspection where supported (default: printer/profile behavior)" },
472
950
  timelapse: { type: "boolean", description: "Enable timelapse recording (default: false)" },
473
951
  nozzle_diameter: { type: "string", description: "Nozzle diameter in mm for auto-slicing (default: 0.4)" }
474
952
  },
@@ -535,9 +1013,6 @@ class BambuPrinterMCPServer {
535
1013
  const host = String(args?.host || DEFAULT_HOST);
536
1014
  const bambuSerial = String(args?.bambu_serial || DEFAULT_BAMBU_SERIAL);
537
1015
  const bambuToken = String(args?.bambu_token || DEFAULT_BAMBU_TOKEN);
538
- const slicerType = String(args?.slicer_type || DEFAULT_SLICER_TYPE);
539
- const slicerPath = String(args?.slicer_path || DEFAULT_SLICER_PATH);
540
- const slicerProfile = String(args?.slicer_profile || DEFAULT_SLICER_PROFILE);
541
1016
  try {
542
1017
  let result;
543
1018
  switch (name) {
@@ -547,25 +1022,69 @@ class BambuPrinterMCPServer {
547
1022
  case "list_printer_files":
548
1023
  result = await this.bambu.getFiles(host, bambuSerial, bambuToken);
549
1024
  break;
1025
+ case "bambu_network_bridge_status": {
1026
+ const bridgeArgs = args;
1027
+ const options = this.bridgeOptionsFromArgs(bridgeArgs);
1028
+ result = this.bambuNetwork.getStatus(options);
1029
+ if (Boolean(bridgeArgs?.connect)) {
1030
+ const probe = await this.bambuNetwork.ensureAgent(options);
1031
+ result = {
1032
+ ...this.bambuNetwork.getStatus(options),
1033
+ connected: true,
1034
+ agent: probe.agent,
1035
+ handshake: probe.handshake,
1036
+ };
1037
+ }
1038
+ break;
1039
+ }
1040
+ case "bambu_network_call": {
1041
+ if (!args?.method) {
1042
+ throw new Error("Missing required parameter: method");
1043
+ }
1044
+ const bridgeArgs = args;
1045
+ const payload = bridgeArgs.payload && typeof bridgeArgs.payload === "object"
1046
+ ? bridgeArgs.payload
1047
+ : {};
1048
+ const options = this.bridgeOptionsFromArgs(bridgeArgs);
1049
+ result = bridgeArgs.with_agent === false
1050
+ ? await this.bambuNetwork.request(String(bridgeArgs.method), payload, options)
1051
+ : await this.bambuNetwork.callWithAgent(String(bridgeArgs.method), payload, options);
1052
+ break;
1053
+ }
1054
+ case "print_3mf_bambu_network": {
1055
+ result = await this.print3mfViaBambuNetwork(args, host, bambuSerial, bambuToken);
1056
+ break;
1057
+ }
550
1058
  case "upload_gcode": {
551
- if (!args?.filename || !args?.gcode) {
552
- throw new Error("Missing required parameters: filename and gcode");
1059
+ if (!args?.filename) {
1060
+ throw new Error("Missing required parameter: filename");
1061
+ }
1062
+ const uploadSource = resolveUploadGcodeSource(args);
1063
+ try {
1064
+ result = await this.bambu.uploadFile(host, bambuSerial, bambuToken, uploadSource.filePath, String(args.filename), false);
1065
+ }
1066
+ finally {
1067
+ if (uploadSource.cleanupDir) {
1068
+ fs.rmSync(uploadSource.cleanupDir, { recursive: true, force: true });
1069
+ }
553
1070
  }
554
- const tmpPath = path.join(TEMP_DIR, String(args.filename));
555
- fs.writeFileSync(tmpPath, String(args.gcode));
556
- result = await this.bambu.uploadFile(host, bambuSerial, bambuToken, tmpPath, String(args.filename), false);
557
1071
  break;
558
1072
  }
559
1073
  case "upload_file":
560
1074
  if (!args?.file_path || !args?.filename) {
561
1075
  throw new Error("Missing required parameters: file_path and filename");
562
1076
  }
1077
+ if (Boolean(args.print ?? false)) {
1078
+ await this.resolveBambuModel(args?.bambu_model);
1079
+ }
563
1080
  result = await this.bambu.uploadFile(host, bambuSerial, bambuToken, String(args.file_path), String(args.filename), Boolean(args.print ?? false));
564
1081
  break;
1082
+ case "start_print":
565
1083
  case "start_print_job":
566
1084
  if (!args?.filename) {
567
1085
  throw new Error("Missing required parameter: filename");
568
1086
  }
1087
+ await this.resolveBambuModel(args?.bambu_model);
569
1088
  result = await this.bambu.startJob(host, bambuSerial, bambuToken, String(args.filename));
570
1089
  break;
571
1090
  case "cancel_print":
@@ -613,6 +1132,7 @@ class BambuPrinterMCPServer {
613
1132
  if (!args?.stl_path) {
614
1133
  throw new Error("Missing required parameter: stl_path");
615
1134
  }
1135
+ const { slicerType, slicerPath, slicerProfile } = resolveSlicerConfig(args);
616
1136
  const sliceModel = await this.resolveBambuModel(args?.bambu_model);
617
1137
  const nozzleDiam = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
618
1138
  // Resolve printer preset for BambuStudio slicer
@@ -632,8 +1152,17 @@ class BambuPrinterMCPServer {
632
1152
  sliceBambuOptions.cloneObjects = String(args.clone_objects);
633
1153
  if (args?.skip_objects !== undefined)
634
1154
  sliceBambuOptions.skipObjects = String(args.skip_objects);
635
- if (args?.load_filaments !== undefined)
1155
+ if (args?.load_filaments !== undefined &&
1156
+ args?.filament_profile !== undefined &&
1157
+ String(args.load_filaments) !== String(args.filament_profile)) {
1158
+ throw new Error("Provide either load_filaments or filament_profile, not conflicting values.");
1159
+ }
1160
+ if (args?.load_filaments !== undefined) {
636
1161
  sliceBambuOptions.loadFilaments = String(args.load_filaments);
1162
+ }
1163
+ else if (args?.filament_profile !== undefined) {
1164
+ sliceBambuOptions.loadFilaments = String(args.filament_profile);
1165
+ }
637
1166
  if (args?.load_filament_ids !== undefined)
638
1167
  sliceBambuOptions.loadFilamentIds = String(args.load_filament_ids);
639
1168
  if (args?.enable_timelapse !== undefined)
@@ -662,13 +1191,22 @@ class BambuPrinterMCPServer {
662
1191
  if (!args?.three_mf_path) {
663
1192
  throw new Error("Missing required parameter: three_mf_path");
664
1193
  }
1194
+ if (String(args?.connection_mode || "lan_mqtt_ftps") === "bambu_network") {
1195
+ result = await this.print3mfViaBambuNetwork(args, host, bambuSerial, bambuToken);
1196
+ break;
1197
+ }
665
1198
  if (!bambuSerial || !bambuToken) {
666
1199
  throw new Error("Bambu serial number and access token are required for print_3mf.");
667
1200
  }
1201
+ const { slicerType, slicerPath, slicerProfile } = resolveSlicerConfig(args);
668
1202
  const printModel = await this.resolveBambuModel(args?.bambu_model);
669
1203
  const printBedType = resolveBedType(args?.bed_type);
670
1204
  const printNozzle = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
671
1205
  const printPreset = BAMBU_MODEL_PRESETS[printModel]?.(printNozzle);
1206
+ const plateIndex = args?.plate_index !== undefined ? Number(args.plate_index) : 0;
1207
+ if (!Number.isInteger(plateIndex) || plateIndex < 0) {
1208
+ throw new Error("plate_index must be a non-negative integer.");
1209
+ }
672
1210
  let threeMFPath = String(args.three_mf_path);
673
1211
  // Auto-slice if 3MF has no gcode
674
1212
  try {
@@ -677,7 +1215,7 @@ class BambuPrinterMCPServer {
677
1215
  const zip = await JSZip.loadAsync(zipData);
678
1216
  const hasGcode = Object.keys(zip.files).some(f => f.match(/Metadata\/plate_\d+\.gcode/i) || f.endsWith('.gcode'));
679
1217
  if (!hasGcode) {
680
- console.log(`3MF has no gcode — auto-slicing with ${slicerType} for ${printModel}`);
1218
+ console.log(`3MF has no gcode - auto-slicing with ${slicerType} for ${printModel}`);
681
1219
  const autoSliceOptions = {
682
1220
  uptodate: true,
683
1221
  ensureOnBed: true,
@@ -702,7 +1240,8 @@ class BambuPrinterMCPServer {
702
1240
  }
703
1241
  }
704
1242
  let finalAmsMapping = parsedAmsMapping;
705
- let useAMS = args?.use_ams !== undefined ? Boolean(args.use_ams) : (!!finalAmsMapping && finalAmsMapping.length > 0);
1243
+ const explicitUseAMS = args?.use_ams !== undefined;
1244
+ let useAMS = explicitUseAMS ? Boolean(args.use_ams) : (!!finalAmsMapping && finalAmsMapping.length > 0);
706
1245
  if (args?.ams_mapping) {
707
1246
  let userMappingOverride;
708
1247
  if (Array.isArray(args.ams_mapping)) {
@@ -722,7 +1261,7 @@ class BambuPrinterMCPServer {
722
1261
  finalAmsMapping = undefined;
723
1262
  useAMS = false;
724
1263
  }
725
- if (!finalAmsMapping || finalAmsMapping.length === 0) {
1264
+ if ((!finalAmsMapping || finalAmsMapping.length === 0) && !explicitUseAMS) {
726
1265
  useAMS = false;
727
1266
  }
728
1267
  const threeMfFilename = path.basename(threeMFPath);
@@ -730,7 +1269,7 @@ class BambuPrinterMCPServer {
730
1269
  result = await this.bambu.print3mf(host, bambuSerial, bambuToken, {
731
1270
  projectName,
732
1271
  filePath: threeMFPath,
733
- plateIndex: 0,
1272
+ plateIndex,
734
1273
  useAMS: useAMS,
735
1274
  amsMapping: finalAmsMapping,
736
1275
  bedType: printBedType,