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/src/index.ts CHANGED
@@ -16,7 +16,14 @@ import fs from "fs";
16
16
  import path from "path";
17
17
  import { createServer as createHttpServer, type IncomingMessage, type Server as HttpServer, type ServerResponse } from "node:http";
18
18
  import { randomUUID } from "node:crypto";
19
- import { STLManipulator, type BambuSliceOptions } from "./stl/stl-manipulator.js";
19
+ import {
20
+ STLManipulator,
21
+ SLICER_TYPES,
22
+ normalizeSlicerType,
23
+ type BambuSliceOptions,
24
+ type SlicerType,
25
+ } from "./stl/stl-manipulator.js";
26
+ import { BambuNetworkBridge, type BambuNetworkBridgeOptions } from "./bambu-network-bridge.js";
20
27
  import { parse3MF } from './3mf_parser.js';
21
28
  import { BambuImplementation } from "./printers/bambu.js";
22
29
 
@@ -25,6 +32,7 @@ dotenv.config();
25
32
  const DEFAULT_HOST = process.env.PRINTER_HOST || "localhost";
26
33
  const DEFAULT_BAMBU_SERIAL = process.env.BAMBU_SERIAL || "";
27
34
  const DEFAULT_BAMBU_TOKEN = process.env.BAMBU_TOKEN || "";
35
+ const DEFAULT_BAMBU_DEV_ID = process.env.BAMBU_DEV_ID || DEFAULT_BAMBU_SERIAL;
28
36
  const TEMP_DIR = process.env.TEMP_DIR || path.join(process.cwd(), "temp");
29
37
 
30
38
  // Printer model and bed type
@@ -70,8 +78,60 @@ function resolveBedType(argsBedType: string | undefined): string {
70
78
 
71
79
  // Slicer configuration (defaults to bambustudio)
72
80
  const DEFAULT_SLICER_TYPE = process.env.SLICER_TYPE || "bambustudio";
73
- const DEFAULT_SLICER_PATH = process.env.SLICER_PATH || "/Applications/BambuStudio.app/Contents/MacOS/BambuStudio";
74
81
  const DEFAULT_SLICER_PROFILE = process.env.SLICER_PROFILE || "";
82
+ const SLICER_SCHEMA_VALUES = [
83
+ ...SLICER_TYPES,
84
+ "fulu-orca",
85
+ "fulu-orcaslicer",
86
+ "orca-studio",
87
+ "orca-bambulab",
88
+ ];
89
+
90
+ function firstExistingPath(paths: string[], fallback: string): string {
91
+ return paths.find((candidate) => fs.existsSync(candidate)) || fallback;
92
+ }
93
+
94
+ function defaultSlicerPathFor(slicerType: SlicerType): string {
95
+ if (slicerType === "orcaslicer" || slicerType === "orcaslicer-bambulab") {
96
+ if (process.platform === "darwin") {
97
+ return firstExistingPath(
98
+ [
99
+ "/Applications/OrcaSlicer.app/Contents/MacOS/OrcaSlicer",
100
+ "/Applications/Orca Studio.app/Contents/MacOS/Orca Studio",
101
+ "/Applications/OrcaSlicer-BMCU.app/Contents/MacOS/OrcaSlicer",
102
+ ],
103
+ "/Applications/OrcaSlicer.app/Contents/MacOS/OrcaSlicer"
104
+ );
105
+ }
106
+ if (process.platform === "win32") {
107
+ return "C:\\Program Files\\OrcaSlicer\\OrcaSlicer.exe";
108
+ }
109
+ return "OrcaSlicer";
110
+ }
111
+
112
+ if (slicerType === "bambustudio") {
113
+ if (process.platform === "darwin") {
114
+ return "/Applications/BambuStudio.app/Contents/MacOS/BambuStudio";
115
+ }
116
+ if (process.platform === "win32") {
117
+ return "C:\\Program Files\\Bambu Studio\\bambu-studio.exe";
118
+ }
119
+ return "BambuStudio";
120
+ }
121
+
122
+ return slicerType;
123
+ }
124
+
125
+ function resolveSlicerConfig(args: Record<string, unknown> | undefined): {
126
+ slicerType: SlicerType;
127
+ slicerPath: string;
128
+ slicerProfile: string;
129
+ } {
130
+ const slicerType = normalizeSlicerType(String(args?.slicer_type || DEFAULT_SLICER_TYPE));
131
+ const slicerPath = String(args?.slicer_path || process.env.SLICER_PATH || defaultSlicerPathFor(slicerType));
132
+ const slicerProfile = String(args?.slicer_profile || DEFAULT_SLICER_PROFILE);
133
+ return { slicerType, slicerPath, slicerProfile };
134
+ }
75
135
 
76
136
  type RuntimeConfig = {
77
137
  transport: "stdio" | "streamable-http";
@@ -139,6 +199,147 @@ type StructuredToolError = {
139
199
  tool: string;
140
200
  };
141
201
 
202
+ type UploadGcodeSource = {
203
+ filePath: string;
204
+ cleanupDir?: string;
205
+ };
206
+
207
+ function expandUserPath(rawPath: string): string {
208
+ const trimmed = rawPath.trim();
209
+ if (trimmed === "~") {
210
+ return process.env.HOME || trimmed;
211
+ }
212
+ if (trimmed.startsWith("~/")) {
213
+ return path.join(process.env.HOME || "", trimmed.slice(2));
214
+ }
215
+ return path.resolve(trimmed);
216
+ }
217
+
218
+ function readableFilePathFromString(value: string): string | undefined {
219
+ if (!value.trim() || value.includes("\n") || value.includes("\r")) {
220
+ return undefined;
221
+ }
222
+
223
+ const candidate = expandUserPath(value);
224
+ try {
225
+ return fs.statSync(candidate).isFile() ? candidate : undefined;
226
+ } catch {
227
+ return undefined;
228
+ }
229
+ }
230
+
231
+ function looksLikeGcodeFilePath(value: string): boolean {
232
+ const trimmed = value.trim();
233
+ if (!trimmed || trimmed.includes("\n") || trimmed.includes("\r")) {
234
+ return false;
235
+ }
236
+ return (
237
+ trimmed.startsWith("/") ||
238
+ trimmed.startsWith("./") ||
239
+ trimmed.startsWith("../") ||
240
+ trimmed.startsWith("~/") ||
241
+ trimmed.includes("\\") ||
242
+ /\.(gcode|gco|gc)$/i.test(trimmed)
243
+ );
244
+ }
245
+
246
+ function requireReadableFilePath(rawPath: string, label: string): string {
247
+ const candidate = expandUserPath(rawPath);
248
+ try {
249
+ if (!fs.statSync(candidate).isFile()) {
250
+ throw new Error(`${label} is not a file: ${candidate}`);
251
+ }
252
+ } catch (error) {
253
+ if ((error as Error).message.startsWith(`${label} is not a file:`)) {
254
+ throw error;
255
+ }
256
+ throw new Error(`${label} does not exist or is not readable: ${candidate}`);
257
+ }
258
+ return candidate;
259
+ }
260
+
261
+ function writeGcodeContentToTempFile(filename: string, gcode: string): UploadGcodeSource {
262
+ fs.mkdirSync(TEMP_DIR, { recursive: true });
263
+ const cleanupDir = fs.mkdtempSync(path.join(TEMP_DIR, "upload-gcode-"));
264
+ const safeName = path.basename(filename.replace(/^\/+/, "")) || "upload.gcode";
265
+ const filePath = path.join(cleanupDir, safeName);
266
+ fs.writeFileSync(filePath, gcode);
267
+ return { filePath, cleanupDir };
268
+ }
269
+
270
+ function resolveUploadGcodeSource(args: Record<string, unknown>): UploadGcodeSource {
271
+ const gcodePath = args.gcode_path !== undefined ? String(args.gcode_path) : "";
272
+ const gcode = args.gcode !== undefined ? String(args.gcode) : "";
273
+
274
+ if (gcodePath && gcode) {
275
+ throw new Error("Provide either gcode_path or gcode, not both.");
276
+ }
277
+
278
+ if (gcodePath) {
279
+ return { filePath: requireReadableFilePath(gcodePath, "gcode_path") };
280
+ }
281
+
282
+ if (!gcode) {
283
+ throw new Error("Missing required parameter: gcode or gcode_path");
284
+ }
285
+
286
+ const detectedPath = readableFilePathFromString(gcode);
287
+ if (detectedPath) {
288
+ return { filePath: detectedPath };
289
+ }
290
+ if (looksLikeGcodeFilePath(gcode)) {
291
+ throw new Error(
292
+ "gcode looks like a local G-code path, but the file is not readable. " +
293
+ "Pass readable gcode_path or literal G-code content."
294
+ );
295
+ }
296
+
297
+ return writeGcodeContentToTempFile(String(args.filename), gcode);
298
+ }
299
+
300
+ const BAMBU_NETWORK_PRINT_METHODS = [
301
+ "start_print",
302
+ "start_local_print",
303
+ "start_local_print_with_record",
304
+ "start_send_gcode_to_sdcard",
305
+ "start_sdcard_print",
306
+ ] as const;
307
+
308
+ type BambuNetworkPrintMethod = typeof BAMBU_NETWORK_PRINT_METHODS[number];
309
+
310
+ function resolveBambuNetworkPrintMethod(
311
+ rawMethod: string | undefined,
312
+ connectionType: string
313
+ ): BambuNetworkPrintMethod {
314
+ const defaultMethod = connectionType === "lan" ? "start_local_print" : "start_print";
315
+ const method = (rawMethod || defaultMethod).trim();
316
+
317
+ if (!(BAMBU_NETWORK_PRINT_METHODS as readonly string[]).includes(method)) {
318
+ throw new Error(
319
+ `Invalid bambu_network_method: "${method}". Valid methods: ${BAMBU_NETWORK_PRINT_METHODS.join(", ")}`
320
+ );
321
+ }
322
+
323
+ return method as BambuNetworkPrintMethod;
324
+ }
325
+
326
+ function toBridgeMethod(method: BambuNetworkPrintMethod): string {
327
+ return `net.${method}`;
328
+ }
329
+
330
+ function stringifyBridgeJson(value: unknown): string | undefined {
331
+ if (value === undefined || value === null) return undefined;
332
+ if (typeof value === "string") return value;
333
+ return JSON.stringify(value);
334
+ }
335
+
336
+ function redactPrintParams(params: Record<string, unknown>): Record<string, unknown> {
337
+ return {
338
+ ...params,
339
+ password: params.password ? "[redacted]" : "",
340
+ };
341
+ }
342
+
142
343
  if (!fs.existsSync(TEMP_DIR)) {
143
344
  fs.mkdirSync(TEMP_DIR, { recursive: true });
144
345
  }
@@ -146,6 +347,7 @@ if (!fs.existsSync(TEMP_DIR)) {
146
347
  class BambuPrinterMCPServer {
147
348
  private server: Server;
148
349
  private bambu: BambuImplementation;
350
+ private bambuNetwork: BambuNetworkBridge;
149
351
  private stlManipulator: STLManipulator;
150
352
  private readonly runtimeConfig: RuntimeConfig;
151
353
  private httpRuntime?: { transport: StreamableHTTPServerTransport; httpServer: HttpServer };
@@ -166,6 +368,7 @@ class BambuPrinterMCPServer {
166
368
  );
167
369
 
168
370
  this.bambu = new BambuImplementation();
371
+ this.bambuNetwork = new BambuNetworkBridge();
169
372
  this.stlManipulator = new STLManipulator(TEMP_DIR);
170
373
 
171
374
  this.setupHandlers();
@@ -193,7 +396,7 @@ class BambuPrinterMCPServer {
193
396
  return validateBambuModel(fromArgs);
194
397
  }
195
398
 
196
- // No model from args or env — ask the user via elicitation
399
+ // No model from args or env - ask the user via elicitation
197
400
  try {
198
401
  const result = await this.server.elicitInput({
199
402
  mode: "form" as const,
@@ -230,7 +433,7 @@ class BambuPrinterMCPServer {
230
433
  "Printer model selection was cancelled. Cannot proceed without knowing the printer model."
231
434
  );
232
435
  } catch (elicitError: any) {
233
- // Elicitation not supported by this client — fall back to a clear error
436
+ // Elicitation not supported by this client - fall back to a clear error
234
437
  const msg = elicitError?.message || String(elicitError);
235
438
  if (
236
439
  elicitError?.code === -32601 || elicitError?.code === -32600 ||
@@ -246,6 +449,232 @@ class BambuPrinterMCPServer {
246
449
  }
247
450
  }
248
451
 
452
+ private bridgeOptionsFromArgs(args: Record<string, any> | undefined): BambuNetworkBridgeOptions {
453
+ return {
454
+ bridgeCommand: args?.bridge_command !== undefined ? String(args.bridge_command) : undefined,
455
+ configDir: args?.bambu_network_config_dir !== undefined ? String(args.bambu_network_config_dir) : undefined,
456
+ countryCode: args?.country_code !== undefined ? String(args.country_code) : undefined,
457
+ userInfo: args?.user_info !== undefined ? String(args.user_info) : undefined,
458
+ timeoutMs: args?.timeout_ms !== undefined ? Number(args.timeout_ms) : undefined,
459
+ };
460
+ }
461
+
462
+ private async ensurePrintableThreeMFPath(
463
+ args: Record<string, any>,
464
+ printModel: string,
465
+ printPreset: string | undefined
466
+ ): Promise<{ threeMFPath: string; autoSliced: boolean }> {
467
+ const { slicerType, slicerPath, slicerProfile } = resolveSlicerConfig(args);
468
+ let threeMFPath = String(args.three_mf_path);
469
+
470
+ const JSZip = (await import('jszip')).default;
471
+ const zipData = fs.readFileSync(threeMFPath);
472
+ const zip = await JSZip.loadAsync(zipData);
473
+ const hasGcode = Object.keys(zip.files).some(
474
+ f => f.match(/Metadata\/plate_\d+\.gcode/i) || f.endsWith('.gcode')
475
+ );
476
+
477
+ if (hasGcode) {
478
+ return { threeMFPath, autoSliced: false };
479
+ }
480
+
481
+ console.log(`3MF has no gcode - auto-slicing with ${slicerType} for ${printModel}`);
482
+ const autoSliceOptions: BambuSliceOptions = {
483
+ uptodate: true,
484
+ ensureOnBed: true,
485
+ minSave: true,
486
+ skipModifiedGcodes: true,
487
+ };
488
+ threeMFPath = await this.stlManipulator.sliceSTL(
489
+ threeMFPath, slicerType, slicerPath, slicerProfile || undefined,
490
+ undefined,
491
+ printPreset,
492
+ autoSliceOptions
493
+ );
494
+ console.log("Auto-sliced to: " + threeMFPath);
495
+ return { threeMFPath, autoSliced: true };
496
+ }
497
+
498
+ private async resolveAmsPrintSettings(
499
+ threeMFPath: string,
500
+ args: Record<string, any>
501
+ ): Promise<{ useAMS: boolean; finalAmsMapping?: number[] }> {
502
+ const parsed3MFData = await parse3MF(threeMFPath);
503
+ let parsedAmsMapping: number[] | undefined;
504
+
505
+ if (parsed3MFData.slicerConfig?.ams_mapping) {
506
+ const slots = Object.values(parsed3MFData.slicerConfig.ams_mapping)
507
+ .filter(v => typeof v === 'number') as number[];
508
+ if (slots.length > 0) {
509
+ parsedAmsMapping = slots.sort((a, b) => a - b);
510
+ }
511
+ }
512
+
513
+ let finalAmsMapping = parsedAmsMapping;
514
+ const explicitUseAMS = args?.use_ams !== undefined;
515
+ let useAMS = explicitUseAMS ? Boolean(args.use_ams) : (!!finalAmsMapping && finalAmsMapping.length > 0);
516
+
517
+ if (args?.ams_mapping) {
518
+ let userMappingOverride: number[] | undefined;
519
+ if (Array.isArray(args.ams_mapping)) {
520
+ userMappingOverride = args.ams_mapping.filter((v: any) => typeof v === 'number');
521
+ } else if (typeof args.ams_mapping === 'object') {
522
+ userMappingOverride = Object.values(args.ams_mapping)
523
+ .filter((v: any) => typeof v === 'number')
524
+ .sort((a: any, b: any) => a - b) as number[];
525
+ }
526
+ if (userMappingOverride && userMappingOverride.length > 0) {
527
+ finalAmsMapping = userMappingOverride;
528
+ useAMS = true;
529
+ }
530
+ }
531
+
532
+ if (args?.use_ams === false) {
533
+ finalAmsMapping = undefined;
534
+ useAMS = false;
535
+ }
536
+ if ((!finalAmsMapping || finalAmsMapping.length === 0) && !explicitUseAMS) {
537
+ useAMS = false;
538
+ }
539
+
540
+ return { useAMS, finalAmsMapping };
541
+ }
542
+
543
+ private async print3mfViaBambuNetwork(
544
+ args: Record<string, any>,
545
+ host: string,
546
+ bambuSerial: string,
547
+ bambuToken: string
548
+ ): Promise<Record<string, unknown>> {
549
+ if (!args?.three_mf_path) {
550
+ throw new Error("Missing required parameter: three_mf_path");
551
+ }
552
+
553
+ const printModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
554
+ const printBedType = resolveBedType(args?.bed_type as string | undefined);
555
+ const printNozzle = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
556
+ const printPreset = BAMBU_MODEL_PRESETS[printModel]?.(printNozzle);
557
+ const plateIndex = args?.plate_index !== undefined ? Number(args.plate_index) : 0;
558
+
559
+ if (!Number.isInteger(plateIndex) || plateIndex < 0) {
560
+ throw new Error("plate_index must be a non-negative integer.");
561
+ }
562
+
563
+ const connectionType = String(args?.connection_type || "cloud").trim().toLowerCase();
564
+ if (!["cloud", "lan"].includes(connectionType)) {
565
+ throw new Error('connection_type must be "cloud" or "lan".');
566
+ }
567
+
568
+ const bridgePrintMethod = resolveBambuNetworkPrintMethod(
569
+ args?.bambu_network_method !== undefined ? String(args.bambu_network_method) : undefined,
570
+ connectionType
571
+ );
572
+ const bridgeMethod = toBridgeMethod(bridgePrintMethod);
573
+ const isLocalBridgePrint = bridgePrintMethod !== "start_print";
574
+
575
+ const devId = String(args?.dev_id || bambuSerial || DEFAULT_BAMBU_DEV_ID).trim();
576
+ if (!devId) {
577
+ throw new Error("dev_id is required for FULU BambuNetwork printing. Pass dev_id or set BAMBU_DEV_ID/BAMBU_SERIAL.");
578
+ }
579
+
580
+ const devIp = String(args?.dev_ip || args?.host || host || "").trim();
581
+ const explicitPassword = String(args?.password || args?.bambu_token || "").trim();
582
+ const password = isLocalBridgePrint ? (explicitPassword || String(bambuToken || "").trim()) : explicitPassword;
583
+ if (isLocalBridgePrint && (!devIp || devIp === "localhost")) {
584
+ throw new Error("dev_ip or host is required for FULU BambuNetwork LAN/local print methods.");
585
+ }
586
+ if (isLocalBridgePrint && !password) {
587
+ throw new Error("bambu_token/access code is required for FULU BambuNetwork LAN/local print methods.");
588
+ }
589
+
590
+ const { threeMFPath, autoSliced } = await this.ensurePrintableThreeMFPath(args, printModel, printPreset);
591
+ const { useAMS, finalAmsMapping } = await this.resolveAmsPrintSettings(threeMFPath, args);
592
+ const threeMfFilename = path.basename(threeMFPath);
593
+ const projectName = String(args?.project_name || threeMfFilename.replace(/\.3mf$/i, ''));
594
+ const presetName = String(args?.preset_name || `${projectName}_plate_${plateIndex + 1}`);
595
+ const clientJobId = args?.client_job_id !== undefined ? Number(args.client_job_id) : Date.now();
596
+
597
+ const amsMapping = stringifyBridgeJson(args?.ams_mapping_bridge ?? finalAmsMapping);
598
+ const params: Record<string, unknown> = {
599
+ dev_id: devId,
600
+ task_name: String(args?.task_name || projectName),
601
+ project_name: projectName,
602
+ preset_name: presetName,
603
+ filename: threeMFPath,
604
+ config_filename: String(args?.config_filename || threeMFPath),
605
+ plate_index: plateIndex + 1,
606
+ ftp_folder: String(args?.ftp_folder || ""),
607
+ ftp_file: String(args?.ftp_file || ""),
608
+ ftp_file_md5: String(args?.ftp_file_md5 || ""),
609
+ nozzle_mapping: stringifyBridgeJson(args?.nozzle_mapping) || "",
610
+ ams_mapping: amsMapping || "",
611
+ ams_mapping2: stringifyBridgeJson(args?.ams_mapping2) || "",
612
+ ams_mapping_info: stringifyBridgeJson(args?.ams_mapping_info) || "",
613
+ nozzles_info: stringifyBridgeJson(args?.nozzles_info) || "",
614
+ connection_type: connectionType,
615
+ comments: String(args?.comments || ""),
616
+ origin_profile_id: args?.origin_profile_id !== undefined ? Number(args.origin_profile_id) : 0,
617
+ stl_design_id: args?.stl_design_id !== undefined ? Number(args.stl_design_id) : 0,
618
+ origin_model_id: String(args?.origin_model_id || ""),
619
+ print_type: String(args?.print_type || "from_normal"),
620
+ dst_file: String(args?.dst_file || ""),
621
+ dev_name: String(args?.dev_name || ""),
622
+ dev_ip: devIp,
623
+ use_ssl_for_ftp: args?.use_ssl_for_ftp !== undefined ? Boolean(args.use_ssl_for_ftp) : true,
624
+ use_ssl_for_mqtt: args?.use_ssl_for_mqtt !== undefined ? Boolean(args.use_ssl_for_mqtt) : true,
625
+ username: String(args?.username || "bblp"),
626
+ password,
627
+ task_bed_leveling: args?.bed_leveling !== undefined ? Boolean(args.bed_leveling) : true,
628
+ task_flow_cali: args?.flow_calibration !== undefined ? Boolean(args.flow_calibration) : true,
629
+ task_vibration_cali: args?.vibration_calibration !== undefined ? Boolean(args.vibration_calibration) : true,
630
+ task_layer_inspect: args?.layer_inspect !== undefined ? Boolean(args.layer_inspect) : false,
631
+ task_record_timelapse: args?.timelapse !== undefined ? Boolean(args.timelapse) : false,
632
+ task_use_ams: useAMS,
633
+ task_bed_type: printBedType,
634
+ extra_options: stringifyBridgeJson(args?.extra_options) || "",
635
+ auto_bed_leveling: args?.auto_bed_leveling !== undefined ? Number(args.auto_bed_leveling) : 0,
636
+ auto_flow_cali: args?.auto_flow_cali !== undefined ? Number(args.auto_flow_cali) : 0,
637
+ auto_offset_cali: args?.auto_offset_cali !== undefined ? Number(args.auto_offset_cali) : 0,
638
+ extruder_cali_manual_mode: args?.extruder_cali_manual_mode !== undefined ? Number(args.extruder_cali_manual_mode) : -1,
639
+ task_ext_change_assist: args?.external_change_assist !== undefined ? Boolean(args.external_change_assist) : false,
640
+ try_emmc_print: args?.try_emmc_print !== undefined ? Boolean(args.try_emmc_print) : false,
641
+ };
642
+
643
+ const bridgeResult = await this.bambuNetwork.callWithAgent(
644
+ bridgeMethod,
645
+ { client_job_id: clientJobId, params },
646
+ this.bridgeOptionsFromArgs(args)
647
+ );
648
+
649
+ if (typeof bridgeResult === "object" && bridgeResult !== null && (bridgeResult as { ok?: unknown }).ok === false) {
650
+ throw new Error(`FULU BambuNetwork bridge method ${bridgeMethod} failed: ${String((bridgeResult as { error?: unknown }).error || "unknown bridge error")}`);
651
+ }
652
+ if (
653
+ typeof bridgeResult === "object" &&
654
+ bridgeResult !== null &&
655
+ typeof (bridgeResult as { value?: unknown }).value === "number" &&
656
+ (bridgeResult as { value: number }).value !== 0
657
+ ) {
658
+ const value = (bridgeResult as { value: number }).value;
659
+ throw new Error(`FULU BambuNetwork bridge method ${bridgeMethod} returned non-zero result ${value}.`);
660
+ }
661
+
662
+ return {
663
+ status: "success",
664
+ message: `FULU BambuNetwork ${bridgePrintMethod} command for ${threeMfFilename} sent successfully.`,
665
+ bridgeMethod,
666
+ bridgeResult,
667
+ clientJobId,
668
+ autoSliced,
669
+ projectName,
670
+ plateIndex,
671
+ bridgePlateIndex: plateIndex + 1,
672
+ useAMS,
673
+ amsMapping: finalAmsMapping,
674
+ params: redactPrintParams(params),
675
+ };
676
+ }
677
+
249
678
  setupResourceHandlers() {
250
679
  this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
251
680
  return {
@@ -401,12 +830,13 @@ class BambuPrinterMCPServer {
401
830
  },
402
831
  slicer_type: {
403
832
  type: "string",
404
- description: "Type of slicer to use (bambustudio, prusaslicer, cura, slic3r, orcaslicer) (default: bambustudio)"
833
+ enum: SLICER_SCHEMA_VALUES,
834
+ 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."
405
835
  },
406
- slicer_path: { type: "string", description: "Path to the slicer executable (default: value from env)" },
836
+ slicer_path: { type: "string", description: "Path to the slicer executable (default: value from env or a platform default)" },
407
837
  slicer_profile: { type: "string", description: "Path to the slicer profile/config file (optional, overrides bambu_model preset)" },
408
838
  nozzle_diameter: { type: "string", description: "Nozzle diameter in mm (default: 0.4)" },
409
- 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." },
839
+ 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." },
410
840
  repetitions: { type: "number", description: "Print N identical copies of the model. Each copy gets its own plate placement. Example: 3 prints three copies." },
411
841
  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." },
412
842
  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." },
@@ -414,6 +844,7 @@ class BambuPrinterMCPServer {
414
844
  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." },
415
845
  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." },
416
846
  load_filaments: { type: "string", description: "Override filament profiles. Semicolon-separated paths to filament JSON configs, e.g. 'pla_basic.json;petg_cf.json'." },
847
+ 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." },
417
848
  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." },
418
849
  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." },
419
850
  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." },
@@ -440,6 +871,97 @@ class BambuPrinterMCPServer {
440
871
  }
441
872
  }
442
873
  },
874
+ {
875
+ name: "bambu_network_bridge_status",
876
+ description: "Inspect or probe the FULU OrcaSlicer-bambulab BambuNetwork bridge runtime used for cloud and restored BambuNetwork printing.",
877
+ inputSchema: {
878
+ type: "object",
879
+ properties: {
880
+ connect: { type: "boolean", description: "When true, start the bridge command and run a handshake plus agent initialization probe." },
881
+ bridge_command: { type: "string", description: "Override command for the FULU bridge host or macOS/WSL wrapper; defaults to BAMBU_NETWORK_BRIDGE_COMMAND." },
882
+ 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." },
883
+ country_code: { type: "string", description: "BambuNetwork country code, such as US, used by the agent during startup." },
884
+ user_info: { type: "string", description: "Optional BambuNetwork user_info JSON string to pass to net.change_user after the agent starts." },
885
+ timeout_ms: { type: "number", description: "Bridge request timeout in milliseconds for the connect probe." }
886
+ }
887
+ }
888
+ },
889
+ {
890
+ name: "bambu_network_call",
891
+ description: "Call a raw FULU OrcaSlicer-bambulab BambuNetwork bridge method, optionally with an initialized network agent injected into the payload.",
892
+ inputSchema: {
893
+ type: "object",
894
+ properties: {
895
+ method: { type: "string", description: "FULU bridge method name, for example bridge.handshake, net.is_user_login, or net.get_user_selected_machine." },
896
+ payload: { type: "object", description: "JSON payload passed to the bridge method." },
897
+ with_agent: { type: "boolean", description: "When true, initialize a BambuNetwork agent and add its agent id to the payload before calling the method." },
898
+ bridge_command: { type: "string", description: "Override command for the FULU bridge host or macOS/WSL wrapper; defaults to BAMBU_NETWORK_BRIDGE_COMMAND." },
899
+ 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." },
900
+ country_code: { type: "string", description: "BambuNetwork country code, such as US, used by the agent during startup." },
901
+ user_info: { type: "string", description: "Optional BambuNetwork user_info JSON string to pass to net.change_user after the agent starts." },
902
+ timeout_ms: { type: "number", description: "Bridge request timeout in milliseconds." }
903
+ },
904
+ required: ["method"]
905
+ }
906
+ },
907
+ {
908
+ name: "print_3mf_bambu_network",
909
+ description: "Print a 3MF through FULU OrcaSlicer-bambulab's restored BambuNetwork path instead of the MCP LAN MQTT/FTPS path.",
910
+ inputSchema: {
911
+ type: "object",
912
+ properties: {
913
+ three_mf_path: { type: "string", description: "Path to the 3MF file to print; unsliced 3MFs are auto-sliced before sending." },
914
+ bambu_model: {
915
+ type: "string",
916
+ enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
917
+ description: "REQUIRED: Bambu Lab printer model. Ask the user if not known. Using the wrong model can damage the printer."
918
+ },
919
+ 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." },
920
+ 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." },
921
+ dev_id: { type: "string", description: "Bambu device id used by BambuNetwork; defaults to BAMBU_DEV_ID or BAMBU_SERIAL." },
922
+ dev_ip: { type: "string", description: "Printer IP address for LAN/local bridge methods; defaults to host when provided." },
923
+ host: { type: "string", description: "Printer host or IP address, used as dev_ip for LAN/local bridge methods." },
924
+ bambu_serial: { type: "string", description: "Fallback Bambu device id when dev_id is not supplied." },
925
+ bambu_token: { type: "string", description: "Printer access code/password for LAN/local bridge methods." },
926
+ username: { type: "string", description: "Printer username for LAN/local bridge methods; defaults to bblp." },
927
+ password: { type: "string", description: "Printer password/access code override for LAN/local bridge methods." },
928
+ bed_type: { type: "string", enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate"], description: "Bed plate type currently installed (default: textured_plate)." },
929
+ plate_index: { type: "number", description: "Zero-based plate index to print from the sliced 3MF; converted to FULU's one-based PrintParams plate_index." },
930
+ project_name: { type: "string", description: "Optional project name sent in FULU PrintParams; defaults to the 3MF filename without extension." },
931
+ preset_name: { type: "string", description: "Optional preset name sent in FULU PrintParams; defaults to project plus one-based plate index." },
932
+ task_name: { type: "string", description: "Optional BambuNetwork task name; defaults to the project name." },
933
+ config_filename: { type: "string", description: "Optional config 3MF path for cloud print; defaults to the same 3MF path." },
934
+ bridge_command: { type: "string", description: "Override command for the FULU bridge host or macOS/WSL wrapper; defaults to BAMBU_NETWORK_BRIDGE_COMMAND." },
935
+ 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." },
936
+ country_code: { type: "string", description: "BambuNetwork country code, such as US, used by the agent during startup." },
937
+ user_info: { type: "string", description: "Optional BambuNetwork user_info JSON string to pass to net.change_user after the agent starts." },
938
+ timeout_ms: { type: "number", description: "Bridge request timeout in milliseconds." },
939
+ 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." },
940
+ slicer_path: { type: "string", description: "Path to the slicer executable for auto-slicing; defaults to value from env or a platform default." },
941
+ slicer_profile: { type: "string", description: "Path to an optional slicer profile/config file for auto-slicing." },
942
+ nozzle_diameter: { type: "string", description: "Nozzle diameter in mm for auto-slicing (default: 0.4)." },
943
+ use_ams: { type: "boolean", description: "Whether to use the AMS; defaults to auto-detect from the 3MF mapping." },
944
+ ams_mapping: { type: "array", description: "AMS slot mapping array used by both local MCP printing and FULU PrintParams.", items: { type: "number" } },
945
+ ams_mapping_bridge: { type: "string", description: "Raw JSON string override for FULU PrintParams ams_mapping when the automatic array is not enough." },
946
+ ams_mapping2: { type: "string", description: "Raw JSON string for FULU PrintParams ams_mapping2, matching OrcaSlicer-bambulab's v1 AMS mapping field." },
947
+ ams_mapping_info: { type: "string", description: "Raw JSON string for FULU PrintParams ams_mapping_info, matching OrcaSlicer-bambulab's detailed AMS mapping field." },
948
+ nozzle_mapping: { type: "string", description: "Raw JSON string for FULU PrintParams nozzle_mapping." },
949
+ nozzles_info: { type: "string", description: "Raw JSON string for FULU PrintParams nozzles_info." },
950
+ bed_leveling: { type: "boolean", description: "Enable auto bed leveling in FULU PrintParams (default: true)." },
951
+ flow_calibration: { type: "boolean", description: "Enable flow calibration in FULU PrintParams (default: true)." },
952
+ vibration_calibration: { type: "boolean", description: "Enable vibration calibration in FULU PrintParams (default: true)." },
953
+ layer_inspect: { type: "boolean", description: "Enable first-layer inspection where supported (default: false for BambuNetwork bridge)." },
954
+ timelapse: { type: "boolean", description: "Enable timelapse recording in FULU PrintParams (default: false)." },
955
+ use_ssl_for_ftp: { type: "boolean", description: "Whether FULU local print should use SSL for FTP (default: true)." },
956
+ use_ssl_for_mqtt: { type: "boolean", description: "Whether FULU local print should use SSL for MQTT (default: true)." },
957
+ external_change_assist: { type: "boolean", description: "Enable FULU PrintParams task_ext_change_assist for external filament change assistance." },
958
+ try_emmc_print: { type: "boolean", description: "Enable FULU PrintParams try_emmc_print for printers that support internal storage printing." },
959
+ extra_options: { type: "string", description: "Raw JSON string or text for FULU PrintParams extra_options." },
960
+ client_job_id: { type: "number", description: "Optional client job id sent to the bridge; defaults to the current timestamp." }
961
+ },
962
+ required: ["three_mf_path", "bambu_model"]
963
+ }
964
+ },
443
965
  {
444
966
  name: "upload_gcode",
445
967
  description: "Upload a G-code file to the Bambu Lab printer",
@@ -447,12 +969,17 @@ class BambuPrinterMCPServer {
447
969
  type: "object",
448
970
  properties: {
449
971
  filename: { type: "string", description: "Name for the file on the printer" },
450
- gcode: { type: "string", description: "G-code content to upload" },
972
+ gcode: { type: "string", description: "G-code content to upload, or a readable local .gcode path. For large files, prefer gcode_path." },
973
+ gcode_path: { type: "string", description: "Local path to a .gcode file to upload. This avoids sending large G-code bodies through the MCP request." },
451
974
  host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
452
975
  bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
453
976
  bambu_token: { type: "string", description: "Access token (default: value from env)" }
454
977
  },
455
- required: ["filename", "gcode"]
978
+ required: ["filename"],
979
+ anyOf: [
980
+ { required: ["gcode"] },
981
+ { required: ["gcode_path"] }
982
+ ]
456
983
  }
457
984
  },
458
985
  {
@@ -464,6 +991,11 @@ class BambuPrinterMCPServer {
464
991
  file_path: { type: "string", description: "Local path to the file to upload" },
465
992
  filename: { type: "string", description: "Name for the file on the printer" },
466
993
  print: { type: "boolean", description: "Start printing after upload (default: false)" },
994
+ bambu_model: {
995
+ type: "string",
996
+ enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
997
+ description: "Required when print is true. Bambu Lab printer model used as a safety confirmation before starting the uploaded file."
998
+ },
467
999
  host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
468
1000
  bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
469
1001
  bambu_token: { type: "string", description: "Access token (default: value from env)" }
@@ -471,6 +1003,25 @@ class BambuPrinterMCPServer {
471
1003
  required: ["file_path", "filename"]
472
1004
  }
473
1005
  },
1006
+ {
1007
+ name: "start_print",
1008
+ description: "Start printing a G-code file already on the Bambu Lab printer. Alias of start_print_job for upstream MCP compatibility.",
1009
+ inputSchema: {
1010
+ type: "object",
1011
+ properties: {
1012
+ filename: { type: "string", description: "Name of the file to print" },
1013
+ bambu_model: {
1014
+ type: "string",
1015
+ enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
1016
+ description: "REQUIRED: Bambu Lab printer model. Ask the user if not known. Starting G-code for the wrong model can damage the printer."
1017
+ },
1018
+ host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
1019
+ bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
1020
+ bambu_token: { type: "string", description: "Access token (default: value from env)" }
1021
+ },
1022
+ required: ["filename", "bambu_model"]
1023
+ }
1024
+ },
474
1025
  {
475
1026
  name: "start_print_job",
476
1027
  description: "Start printing a G-code file already on the Bambu Lab printer",
@@ -478,11 +1029,16 @@ class BambuPrinterMCPServer {
478
1029
  type: "object",
479
1030
  properties: {
480
1031
  filename: { type: "string", description: "Name of the file to print" },
1032
+ bambu_model: {
1033
+ type: "string",
1034
+ enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
1035
+ description: "REQUIRED: Bambu Lab printer model. Ask the user if not known. Starting G-code for the wrong model can damage the printer."
1036
+ },
481
1037
  host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
482
1038
  bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
483
1039
  bambu_token: { type: "string", description: "Access token (default: value from env)" }
484
1040
  },
485
- required: ["filename"]
1041
+ required: ["filename", "bambu_model"]
486
1042
  }
487
1043
  },
488
1044
  {
@@ -524,14 +1080,35 @@ class BambuPrinterMCPServer {
524
1080
  enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
525
1081
  description: "REQUIRED: Bambu Lab printer model. Ask the user if not known. Using the wrong model can damage the printer."
526
1082
  },
1083
+ connection_mode: {
1084
+ type: "string",
1085
+ enum: ["lan_mqtt_ftps", "bambu_network"],
1086
+ 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."
1087
+ },
1088
+ 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." },
1089
+ 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." },
1090
+ dev_id: { type: "string", description: "Bambu device id for FULU BambuNetwork printing; defaults to BAMBU_DEV_ID or BAMBU_SERIAL." },
1091
+ dev_ip: { type: "string", description: "Printer IP address for FULU BambuNetwork LAN/local print methods; defaults to host when provided." },
1092
+ bridge_command: { type: "string", description: "Override command for the FULU bridge host or macOS/WSL wrapper; defaults to BAMBU_NETWORK_BRIDGE_COMMAND." },
1093
+ bambu_network_config_dir: { type: "string", description: "Config/log directory used by the FULU BambuNetwork agent." },
1094
+ country_code: { type: "string", description: "BambuNetwork country code, such as US, used by the FULU bridge agent." },
1095
+ user_info: { type: "string", description: "Optional BambuNetwork user_info JSON string passed to net.change_user for the FULU bridge." },
527
1096
  bed_type: {
528
1097
  type: "string",
529
1098
  enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate"],
530
1099
  description: "Bed plate type currently installed (default: textured_plate)"
531
1100
  },
1101
+ plate_index: { type: "number", description: "Zero-based plate index to print from the sliced 3MF (default: 0)" },
532
1102
  host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
533
1103
  bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
534
1104
  bambu_token: { type: "string", description: "Access token (default: value from env)" },
1105
+ slicer_type: {
1106
+ type: "string",
1107
+ enum: SLICER_SCHEMA_VALUES,
1108
+ 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."
1109
+ },
1110
+ slicer_path: { type: "string", description: "Path to the slicer executable for auto-slicing (default: value from env or a platform default)" },
1111
+ slicer_profile: { type: "string", description: "Path to an optional slicer profile/config file for auto-slicing" },
535
1112
  use_ams: { type: "boolean", description: "Whether to use the AMS (default: auto-detect from 3MF)" },
536
1113
  ams_mapping: {
537
1114
  type: "array",
@@ -541,6 +1118,7 @@ class BambuPrinterMCPServer {
541
1118
  bed_leveling: { type: "boolean", description: "Enable auto bed leveling (default: true)" },
542
1119
  flow_calibration: { type: "boolean", description: "Enable flow calibration (default: true)" },
543
1120
  vibration_calibration: { type: "boolean", description: "Enable vibration calibration (default: true)" },
1121
+ layer_inspect: { type: "boolean", description: "Enable first-layer inspection where supported (default: printer/profile behavior)" },
544
1122
  timelapse: { type: "boolean", description: "Enable timelapse recording (default: false)" },
545
1123
  nozzle_diameter: { type: "string", description: "Nozzle diameter in mm for auto-slicing (default: 0.4)" }
546
1124
  },
@@ -609,10 +1187,6 @@ class BambuPrinterMCPServer {
609
1187
  const host = String(args?.host || DEFAULT_HOST);
610
1188
  const bambuSerial = String(args?.bambu_serial || DEFAULT_BAMBU_SERIAL);
611
1189
  const bambuToken = String(args?.bambu_token || DEFAULT_BAMBU_TOKEN);
612
- const slicerType = String(args?.slicer_type || DEFAULT_SLICER_TYPE) as 'prusaslicer' | 'cura' | 'slic3r' | 'orcaslicer' | 'bambustudio';
613
- const slicerPath = String(args?.slicer_path || DEFAULT_SLICER_PATH);
614
- const slicerProfile = String(args?.slicer_profile || DEFAULT_SLICER_PROFILE);
615
-
616
1190
  try {
617
1191
  let result;
618
1192
 
@@ -625,15 +1199,56 @@ class BambuPrinterMCPServer {
625
1199
  result = await this.bambu.getFiles(host, bambuSerial, bambuToken);
626
1200
  break;
627
1201
 
1202
+ case "bambu_network_bridge_status": {
1203
+ const bridgeArgs = args as Record<string, any> | undefined;
1204
+ const options = this.bridgeOptionsFromArgs(bridgeArgs);
1205
+ result = this.bambuNetwork.getStatus(options);
1206
+ if (Boolean(bridgeArgs?.connect)) {
1207
+ const probe = await this.bambuNetwork.ensureAgent(options);
1208
+ result = {
1209
+ ...this.bambuNetwork.getStatus(options),
1210
+ connected: true,
1211
+ agent: probe.agent,
1212
+ handshake: probe.handshake,
1213
+ };
1214
+ }
1215
+ break;
1216
+ }
1217
+
1218
+ case "bambu_network_call": {
1219
+ if (!args?.method) {
1220
+ throw new Error("Missing required parameter: method");
1221
+ }
1222
+ const bridgeArgs = args as Record<string, any>;
1223
+ const payload = bridgeArgs.payload && typeof bridgeArgs.payload === "object"
1224
+ ? bridgeArgs.payload as Record<string, unknown>
1225
+ : {};
1226
+ const options = this.bridgeOptionsFromArgs(bridgeArgs);
1227
+ result = bridgeArgs.with_agent === false
1228
+ ? await this.bambuNetwork.request(String(bridgeArgs.method), payload, options)
1229
+ : await this.bambuNetwork.callWithAgent(String(bridgeArgs.method), payload, options);
1230
+ break;
1231
+ }
1232
+
1233
+ case "print_3mf_bambu_network": {
1234
+ result = await this.print3mfViaBambuNetwork(args as Record<string, any>, host, bambuSerial, bambuToken);
1235
+ break;
1236
+ }
1237
+
628
1238
  case "upload_gcode": {
629
- if (!args?.filename || !args?.gcode) {
630
- throw new Error("Missing required parameters: filename and gcode");
1239
+ if (!args?.filename) {
1240
+ throw new Error("Missing required parameter: filename");
1241
+ }
1242
+ const uploadSource = resolveUploadGcodeSource(args as Record<string, unknown>);
1243
+ try {
1244
+ result = await this.bambu.uploadFile(
1245
+ host, bambuSerial, bambuToken, uploadSource.filePath, String(args.filename), false
1246
+ );
1247
+ } finally {
1248
+ if (uploadSource.cleanupDir) {
1249
+ fs.rmSync(uploadSource.cleanupDir, { recursive: true, force: true });
1250
+ }
631
1251
  }
632
- const tmpPath = path.join(TEMP_DIR, String(args.filename));
633
- fs.writeFileSync(tmpPath, String(args.gcode));
634
- result = await this.bambu.uploadFile(
635
- host, bambuSerial, bambuToken, tmpPath, String(args.filename), false
636
- );
637
1252
  break;
638
1253
  }
639
1254
 
@@ -641,6 +1256,9 @@ class BambuPrinterMCPServer {
641
1256
  if (!args?.file_path || !args?.filename) {
642
1257
  throw new Error("Missing required parameters: file_path and filename");
643
1258
  }
1259
+ if (Boolean(args.print ?? false)) {
1260
+ await this.resolveBambuModel(args?.bambu_model as string | undefined);
1261
+ }
644
1262
  result = await this.bambu.uploadFile(
645
1263
  host, bambuSerial, bambuToken,
646
1264
  String(args.file_path), String(args.filename),
@@ -648,10 +1266,12 @@ class BambuPrinterMCPServer {
648
1266
  );
649
1267
  break;
650
1268
 
1269
+ case "start_print":
651
1270
  case "start_print_job":
652
1271
  if (!args?.filename) {
653
1272
  throw new Error("Missing required parameter: filename");
654
1273
  }
1274
+ await this.resolveBambuModel(args?.bambu_model as string | undefined);
655
1275
  result = await this.bambu.startJob(host, bambuSerial, bambuToken, String(args.filename));
656
1276
  break;
657
1277
 
@@ -717,6 +1337,7 @@ class BambuPrinterMCPServer {
717
1337
  if (!args?.stl_path) {
718
1338
  throw new Error("Missing required parameter: stl_path");
719
1339
  }
1340
+ const { slicerType, slicerPath, slicerProfile } = resolveSlicerConfig(args);
720
1341
  const sliceModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
721
1342
  const nozzleDiam = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
722
1343
  // Resolve printer preset for BambuStudio slicer
@@ -730,7 +1351,18 @@ class BambuPrinterMCPServer {
730
1351
  if (args?.ensure_on_bed !== undefined) sliceBambuOptions.ensureOnBed = Boolean(args.ensure_on_bed);
731
1352
  if (args?.clone_objects !== undefined) sliceBambuOptions.cloneObjects = String(args.clone_objects);
732
1353
  if (args?.skip_objects !== undefined) sliceBambuOptions.skipObjects = String(args.skip_objects);
733
- if (args?.load_filaments !== undefined) sliceBambuOptions.loadFilaments = String(args.load_filaments);
1354
+ if (
1355
+ args?.load_filaments !== undefined &&
1356
+ args?.filament_profile !== undefined &&
1357
+ String(args.load_filaments) !== String(args.filament_profile)
1358
+ ) {
1359
+ throw new Error("Provide either load_filaments or filament_profile, not conflicting values.");
1360
+ }
1361
+ if (args?.load_filaments !== undefined) {
1362
+ sliceBambuOptions.loadFilaments = String(args.load_filaments);
1363
+ } else if (args?.filament_profile !== undefined) {
1364
+ sliceBambuOptions.loadFilaments = String(args.filament_profile);
1365
+ }
734
1366
  if (args?.load_filament_ids !== undefined) sliceBambuOptions.loadFilamentIds = String(args.load_filament_ids);
735
1367
  if (args?.enable_timelapse !== undefined) sliceBambuOptions.enableTimelapse = Boolean(args.enable_timelapse);
736
1368
  if (args?.allow_mix_temp !== undefined) sliceBambuOptions.allowMixTemp = Boolean(args.allow_mix_temp);
@@ -756,14 +1388,26 @@ class BambuPrinterMCPServer {
756
1388
  if (!args?.three_mf_path) {
757
1389
  throw new Error("Missing required parameter: three_mf_path");
758
1390
  }
1391
+
1392
+ if (String(args?.connection_mode || "lan_mqtt_ftps") === "bambu_network") {
1393
+ result = await this.print3mfViaBambuNetwork(args as Record<string, any>, host, bambuSerial, bambuToken);
1394
+ break;
1395
+ }
1396
+
759
1397
  if (!bambuSerial || !bambuToken) {
760
1398
  throw new Error("Bambu serial number and access token are required for print_3mf.");
761
1399
  }
762
1400
 
1401
+ const { slicerType, slicerPath, slicerProfile } = resolveSlicerConfig(args);
763
1402
  const printModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
764
1403
  const printBedType = resolveBedType(args?.bed_type as string | undefined);
765
1404
  const printNozzle = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
766
1405
  const printPreset = BAMBU_MODEL_PRESETS[printModel]?.(printNozzle);
1406
+ const plateIndex = args?.plate_index !== undefined ? Number(args.plate_index) : 0;
1407
+
1408
+ if (!Number.isInteger(plateIndex) || plateIndex < 0) {
1409
+ throw new Error("plate_index must be a non-negative integer.");
1410
+ }
767
1411
 
768
1412
  let threeMFPath = String(args.three_mf_path);
769
1413
 
@@ -776,7 +1420,7 @@ class BambuPrinterMCPServer {
776
1420
  f => f.match(/Metadata\/plate_\d+\.gcode/i) || f.endsWith('.gcode')
777
1421
  );
778
1422
  if (!hasGcode) {
779
- console.log(`3MF has no gcode — auto-slicing with ${slicerType} for ${printModel}`);
1423
+ console.log(`3MF has no gcode - auto-slicing with ${slicerType} for ${printModel}`);
780
1424
  const autoSliceOptions: BambuSliceOptions = {
781
1425
  uptodate: true,
782
1426
  ensureOnBed: true,
@@ -806,7 +1450,8 @@ class BambuPrinterMCPServer {
806
1450
  }
807
1451
 
808
1452
  let finalAmsMapping = parsedAmsMapping;
809
- let useAMS = args?.use_ams !== undefined ? Boolean(args.use_ams) : (!!finalAmsMapping && finalAmsMapping.length > 0);
1453
+ const explicitUseAMS = args?.use_ams !== undefined;
1454
+ let useAMS = explicitUseAMS ? Boolean(args.use_ams) : (!!finalAmsMapping && finalAmsMapping.length > 0);
810
1455
 
811
1456
  if (args?.ams_mapping) {
812
1457
  let userMappingOverride: number[] | undefined;
@@ -827,7 +1472,7 @@ class BambuPrinterMCPServer {
827
1472
  finalAmsMapping = undefined;
828
1473
  useAMS = false;
829
1474
  }
830
- if (!finalAmsMapping || finalAmsMapping.length === 0) {
1475
+ if ((!finalAmsMapping || finalAmsMapping.length === 0) && !explicitUseAMS) {
831
1476
  useAMS = false;
832
1477
  }
833
1478
 
@@ -837,7 +1482,7 @@ class BambuPrinterMCPServer {
837
1482
  result = await this.bambu.print3mf(host, bambuSerial, bambuToken, {
838
1483
  projectName,
839
1484
  filePath: threeMFPath,
840
- plateIndex: 0,
1485
+ plateIndex,
841
1486
  useAMS: useAMS,
842
1487
  amsMapping: finalAmsMapping,
843
1488
  bedType: printBedType,