bambu-printer-mcp 1.0.2 → 1.0.4

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/README.md CHANGED
@@ -716,6 +716,53 @@ Slice an STL or 3MF file using an external slicer and return the path to the out
716
716
 
717
717
  For printing on a Bambu printer, the recommended workflow is: slice with `bambustudio` to get a sliced 3MF, then pass that output path to `print_3mf`.
718
718
 
719
+ #### BambuStudio Slicer Options
720
+
721
+ When `slicer_type` is `bambustudio` (the default), these additional parameters are available on `slice_stl`:
722
+
723
+ | Parameter | Type | Description |
724
+ |-----------|------|-------------|
725
+ | `uptodate` | boolean | Update 3MF configs to latest BambuStudio presets |
726
+ | `repetitions` | number | Number of copies to print |
727
+ | `orient` | boolean | Auto-orient model for optimal printability |
728
+ | `arrange` | boolean | Auto-arrange objects on the build plate |
729
+ | `ensure_on_bed` | boolean | Lift floating models onto the bed |
730
+ | `clone_objects` | string | Clone counts per object, comma-separated (e.g. `"1,3,1,10"`) |
731
+ | `skip_objects` | string | Object indices to skip, comma-separated (e.g. `"3,5,10"`) |
732
+ | `load_filaments` | string | Filament profile paths, semicolon-separated |
733
+ | `load_filament_ids` | string | Filament-to-object mapping, comma-separated |
734
+ | `enable_timelapse` | boolean | Enable timelapse-aware slicing |
735
+ | `allow_mix_temp` | boolean | Allow mixed-temperature filaments on one plate |
736
+ | `scale` | number | Uniform scale factor |
737
+ | `rotate` | number | Z-axis rotation in degrees |
738
+ | `rotate_x` | number | X-axis rotation in degrees |
739
+ | `rotate_y` | number | Y-axis rotation in degrees |
740
+ | `min_save` | boolean | Produce smaller output 3MF (faster uploads) |
741
+ | `skip_modified_gcodes` | boolean | Ignore stale custom gcodes in the 3MF |
742
+ | `slice_plate` | number | Which plate to slice (0 = all plates, default: 0) |
743
+
744
+ **Example: Slice with auto-orient and 3 copies**
745
+ ```json
746
+ {
747
+ "stl_path": "/path/to/model.stl",
748
+ "bambu_model": "p1s",
749
+ "orient": true,
750
+ "arrange": true,
751
+ "repetitions": 3
752
+ }
753
+ ```
754
+
755
+ #### Smart Defaults (print_3mf auto-slice)
756
+
757
+ When `print_3mf` detects an unsliced 3MF and auto-slices it, these defaults are applied automatically:
758
+
759
+ - `uptodate: true` -- prevents stale config bugs from downloaded 3MFs
760
+ - `ensure_on_bed: true` -- safety net, lifts floating models onto the bed
761
+ - `min_save: true` -- smaller output for faster FTP uploads to the printer
762
+ - `skip_modified_gcodes: true` -- strips custom gcodes from other users' profiles
763
+
764
+ These defaults keep you safe when printing downloaded models. When calling `slice_stl` directly, you have full control over every flag.
765
+
719
766
  </details>
720
767
 
721
768
  <details>
package/dist/index.js CHANGED
@@ -333,7 +333,25 @@ class BambuPrinterMCPServer {
333
333
  },
334
334
  slicer_path: { type: "string", description: "Path to the slicer executable (default: value from env)" },
335
335
  slicer_profile: { type: "string", description: "Path to the slicer profile/config file (optional, overrides bambu_model preset)" },
336
- nozzle_diameter: { type: "string", description: "Nozzle diameter in mm (default: 0.4)" }
336
+ 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." },
338
+ repetitions: { type: "number", description: "Print N identical copies of the model. Each copy gets its own plate placement. Example: 3 prints three copies." },
339
+ 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
+ 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." },
341
+ ensure_on_bed: { type: "boolean", description: "Detect models floating above the bed and lower them onto the build surface. Safety net for imported models with incorrect Z origins." },
342
+ 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
+ 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
+ load_filaments: { type: "string", description: "Override filament profiles. Semicolon-separated paths to filament JSON configs, e.g. 'pla_basic.json;petg_cf.json'." },
345
+ 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
+ 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
+ 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." },
348
+ scale: { type: "number", description: "Uniform scale factor applied to all axes. 1.0 = original size, 2.0 = double, 0.5 = half. Applied before slicing." },
349
+ rotate: { type: "number", description: "Rotate the model around the Z-axis (vertical) by this many degrees before slicing. Positive = counterclockwise when viewed from above." },
350
+ rotate_x: { type: "number", description: "Rotate the model around the X-axis by this many degrees before slicing. Useful for reorienting prints for better layer adhesion." },
351
+ rotate_y: { type: "number", description: "Rotate the model around the Y-axis by this many degrees before slicing. Useful for reorienting prints for better layer adhesion." },
352
+ min_save: { type: "boolean", description: "Write a smaller output 3MF by omitting non-essential metadata. Reduces file size for faster FTP upload to the printer." },
353
+ skip_modified_gcodes: { type: "boolean", description: "Strip custom start/end gcodes embedded in the 3MF. Recommended for downloaded 3MFs since custom gcodes from other users' profiles may be unsafe for your printer." },
354
+ slice_plate: { type: "number", description: "Which plate index to slice. 0 = all plates (default). Use 1, 2, etc. to slice only a specific plate in multi-plate 3MF projects." }
337
355
  },
338
356
  required: ["stl_path", "bambu_model"]
339
357
  }
@@ -599,8 +617,45 @@ class BambuPrinterMCPServer {
599
617
  const nozzleDiam = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
600
618
  // Resolve printer preset for BambuStudio slicer
601
619
  const printerPreset = BAMBU_MODEL_PRESETS[sliceModel]?.(nozzleDiam);
620
+ const sliceBambuOptions = {};
621
+ if (args?.uptodate !== undefined)
622
+ sliceBambuOptions.uptodate = Boolean(args.uptodate);
623
+ if (args?.repetitions !== undefined)
624
+ sliceBambuOptions.repetitions = Number(args.repetitions);
625
+ if (args?.orient !== undefined)
626
+ sliceBambuOptions.orient = Boolean(args.orient);
627
+ if (args?.arrange !== undefined)
628
+ sliceBambuOptions.arrange = Boolean(args.arrange);
629
+ if (args?.ensure_on_bed !== undefined)
630
+ sliceBambuOptions.ensureOnBed = Boolean(args.ensure_on_bed);
631
+ if (args?.clone_objects !== undefined)
632
+ sliceBambuOptions.cloneObjects = String(args.clone_objects);
633
+ if (args?.skip_objects !== undefined)
634
+ sliceBambuOptions.skipObjects = String(args.skip_objects);
635
+ if (args?.load_filaments !== undefined)
636
+ sliceBambuOptions.loadFilaments = String(args.load_filaments);
637
+ if (args?.load_filament_ids !== undefined)
638
+ sliceBambuOptions.loadFilamentIds = String(args.load_filament_ids);
639
+ if (args?.enable_timelapse !== undefined)
640
+ sliceBambuOptions.enableTimelapse = Boolean(args.enable_timelapse);
641
+ if (args?.allow_mix_temp !== undefined)
642
+ sliceBambuOptions.allowMixTemp = Boolean(args.allow_mix_temp);
643
+ if (args?.scale !== undefined)
644
+ sliceBambuOptions.scale = Number(args.scale);
645
+ if (args?.rotate !== undefined)
646
+ sliceBambuOptions.rotate = Number(args.rotate);
647
+ if (args?.rotate_x !== undefined)
648
+ sliceBambuOptions.rotateX = Number(args.rotate_x);
649
+ if (args?.rotate_y !== undefined)
650
+ sliceBambuOptions.rotateY = Number(args.rotate_y);
651
+ if (args?.min_save !== undefined)
652
+ sliceBambuOptions.minSave = Boolean(args.min_save);
653
+ if (args?.skip_modified_gcodes !== undefined)
654
+ sliceBambuOptions.skipModifiedGcodes = Boolean(args.skip_modified_gcodes);
655
+ if (args?.slice_plate !== undefined)
656
+ sliceBambuOptions.slicePlate = Number(args.slice_plate);
602
657
  result = await this.stlManipulator.sliceSTL(String(args.stl_path), slicerType, slicerPath, slicerProfile || undefined, undefined, // progressCallback
603
- printerPreset);
658
+ printerPreset, sliceBambuOptions);
604
659
  break;
605
660
  }
606
661
  case "print_3mf": {
@@ -623,8 +678,14 @@ class BambuPrinterMCPServer {
623
678
  const hasGcode = Object.keys(zip.files).some(f => f.match(/Metadata\/plate_\d+\.gcode/i) || f.endsWith('.gcode'));
624
679
  if (!hasGcode) {
625
680
  console.log(`3MF has no gcode — auto-slicing with ${slicerType} for ${printModel}`);
681
+ const autoSliceOptions = {
682
+ uptodate: true,
683
+ ensureOnBed: true,
684
+ minSave: true,
685
+ skipModifiedGcodes: true,
686
+ };
626
687
  threeMFPath = await this.stlManipulator.sliceSTL(threeMFPath, slicerType, slicerPath, slicerProfile || undefined, undefined, // progressCallback
627
- printPreset);
688
+ printPreset, autoSliceOptions);
628
689
  console.log("Auto-sliced to: " + threeMFPath);
629
690
  }
630
691
  }
@@ -22,6 +22,26 @@ export type TransformationParams = {
22
22
  relative?: boolean;
23
23
  selectionBounds?: THREE.Box3;
24
24
  };
25
+ export interface BambuSliceOptions {
26
+ uptodate?: boolean;
27
+ repetitions?: number;
28
+ orient?: boolean;
29
+ arrange?: boolean;
30
+ ensureOnBed?: boolean;
31
+ cloneObjects?: string;
32
+ skipObjects?: string;
33
+ loadFilaments?: string;
34
+ loadFilamentIds?: string;
35
+ enableTimelapse?: boolean;
36
+ allowMixTemp?: boolean;
37
+ scale?: number;
38
+ rotate?: number;
39
+ rotateX?: number;
40
+ rotateY?: number;
41
+ minSave?: boolean;
42
+ skipModifiedGcodes?: boolean;
43
+ slicePlate?: number;
44
+ }
25
45
  export declare class STLManipulator extends EventEmitter {
26
46
  private tempDir;
27
47
  private activeOperations;
@@ -48,7 +68,17 @@ export declare class STLManipulator extends EventEmitter {
48
68
  boundingBox: BoundingBox;
49
69
  vertexCount: number;
50
70
  faceCount: number;
71
+ objects?: Array<{
72
+ name: string;
73
+ vertexCount: number;
74
+ faceCount: number;
75
+ boundingBox: BoundingBox;
76
+ }>;
51
77
  }>;
78
+ /**
79
+ * Extract mesh info from a 3MF file by parsing the XML object models.
80
+ */
81
+ private get3MFInfo;
52
82
  /**
53
83
  * Scale an STL model uniformly or along specific axes
54
84
  */
@@ -97,9 +127,10 @@ export declare class STLManipulator extends EventEmitter {
97
127
  * @param slicerProfile Optional path to the slicer profile/config file
98
128
  * @param progressCallback Optional callback for progress updates
99
129
  * @param printerPreset Optional BambuStudio printer preset name (e.g., "Bambu Lab P1S 0.4 nozzle")
130
+ * @param bambuOptions Optional BambuStudio-specific CLI flags
100
131
  * @returns Path to the generated G-code or sliced 3MF file
101
132
  */
102
- sliceSTL(stlFilePath: string, slicerType: 'prusaslicer' | 'cura' | 'slic3r' | 'orcaslicer' | 'bambustudio', slicerPath: string, slicerProfile?: string, progressCallback?: ProgressCallback, printerPreset?: string): Promise<string>;
133
+ sliceSTL(stlFilePath: string, slicerType: 'prusaslicer' | 'cura' | 'slic3r' | 'orcaslicer' | 'bambustudio', slicerPath: string, slicerProfile?: string, progressCallback?: ProgressCallback, printerPreset?: string, bambuOptions?: BambuSliceOptions): Promise<string>;
103
134
  /**
104
135
  * Enhanced version of confirmTemperatures with better error handling
105
136
  * @param gcodePath Path to the G-code file
@@ -85,8 +85,12 @@ export class STLManipulator extends EventEmitter {
85
85
  */
86
86
  async getSTLInfo(stlFilePath) {
87
87
  try {
88
- const { geometry, boundingBox } = await this.loadSTL(stlFilePath);
89
88
  const fileStats = fs.statSync(stlFilePath);
89
+ // Handle 3MF files: extract mesh data from embedded object models
90
+ if (stlFilePath.toLowerCase().endsWith('.3mf')) {
91
+ return await this.get3MFInfo(stlFilePath, fileStats.size);
92
+ }
93
+ const { geometry, boundingBox } = await this.loadSTL(stlFilePath);
90
94
  // Count faces (each face is a triangle in STL)
91
95
  const positionAttribute = geometry.getAttribute('position');
92
96
  const vertexCount = positionAttribute.count;
@@ -115,6 +119,89 @@ export class STLManipulator extends EventEmitter {
115
119
  throw new Error(`Failed to get STL info: ${error instanceof Error ? error.message : String(error)}`);
116
120
  }
117
121
  }
122
+ /**
123
+ * Extract mesh info from a 3MF file by parsing the XML object models.
124
+ */
125
+ async get3MFInfo(filePath, fileSize) {
126
+ const JSZip = (await import('jszip')).default;
127
+ const data = fs.readFileSync(filePath);
128
+ const zip = await JSZip.loadAsync(data);
129
+ // Find all object model files
130
+ const objectFiles = Object.keys(zip.files).filter((name) => /^3D\/Objects\/.*\.model$/i.test(name));
131
+ const overallBox = new THREE.Box3(new THREE.Vector3(Infinity, Infinity, Infinity), new THREE.Vector3(-Infinity, -Infinity, -Infinity));
132
+ let totalVerts = 0;
133
+ let totalFaces = 0;
134
+ const objects = [];
135
+ for (const objFile of objectFiles) {
136
+ const content = await zip.file(objFile).async('string');
137
+ // Parse vertices from XML: <vertex x="..." y="..." z="..."/>
138
+ const vertexRegex = /<(?:\w+:)?vertex\s+x="([^"]+)"\s+y="([^"]+)"\s+z="([^"]+)"/g;
139
+ const triangleRegex = /<(?:\w+:)?triangle\s/g;
140
+ const objBox = new THREE.Box3(new THREE.Vector3(Infinity, Infinity, Infinity), new THREE.Vector3(-Infinity, -Infinity, -Infinity));
141
+ let vCount = 0;
142
+ let match;
143
+ while ((match = vertexRegex.exec(content)) !== null) {
144
+ const x = parseFloat(match[1]);
145
+ const y = parseFloat(match[2]);
146
+ const z = parseFloat(match[3]);
147
+ objBox.expandByPoint(new THREE.Vector3(x, y, z));
148
+ overallBox.expandByPoint(new THREE.Vector3(x, y, z));
149
+ vCount++;
150
+ }
151
+ let fCount = 0;
152
+ while (triangleRegex.exec(content) !== null)
153
+ fCount++;
154
+ totalVerts += vCount;
155
+ totalFaces += fCount;
156
+ if (vCount > 0) {
157
+ const center = new THREE.Vector3();
158
+ objBox.getCenter(center);
159
+ const dims = new THREE.Vector3();
160
+ objBox.getSize(dims);
161
+ objects.push({
162
+ name: path.basename(objFile),
163
+ vertexCount: vCount,
164
+ faceCount: fCount,
165
+ boundingBox: { min: objBox.min, max: objBox.max, center, dimensions: dims },
166
+ });
167
+ }
168
+ }
169
+ // If no object files found, try the main 3dmodel.model
170
+ if (objects.length === 0) {
171
+ const mainModel = zip.file('3D/3dmodel.model');
172
+ if (mainModel) {
173
+ const content = await mainModel.async('string');
174
+ const vertexRegex = /<(?:\w+:)?vertex\s+x="([^"]+)"\s+y="([^"]+)"\s+z="([^"]+)"/g;
175
+ const triangleRegex = /<(?:\w+:)?triangle\s/g;
176
+ let match;
177
+ while ((match = vertexRegex.exec(content)) !== null) {
178
+ const x = parseFloat(match[1]);
179
+ const y = parseFloat(match[2]);
180
+ const z = parseFloat(match[3]);
181
+ overallBox.expandByPoint(new THREE.Vector3(x, y, z));
182
+ totalVerts++;
183
+ }
184
+ while (triangleRegex.exec(content) !== null)
185
+ totalFaces++;
186
+ }
187
+ }
188
+ if (totalVerts === 0) {
189
+ throw new Error('No mesh data found in 3MF file. The archive may not contain any 3D objects.');
190
+ }
191
+ const center = new THREE.Vector3();
192
+ overallBox.getCenter(center);
193
+ const dimensions = new THREE.Vector3();
194
+ overallBox.getSize(dimensions);
195
+ return {
196
+ filePath,
197
+ fileName: path.basename(filePath),
198
+ fileSize,
199
+ boundingBox: { min: overallBox.min, max: overallBox.max, center, dimensions },
200
+ vertexCount: totalVerts,
201
+ faceCount: totalFaces,
202
+ objects,
203
+ };
204
+ }
118
205
  /**
119
206
  * Scale an STL model uniformly or along specific axes
120
207
  */
@@ -651,9 +738,10 @@ export class STLManipulator extends EventEmitter {
651
738
  * @param slicerProfile Optional path to the slicer profile/config file
652
739
  * @param progressCallback Optional callback for progress updates
653
740
  * @param printerPreset Optional BambuStudio printer preset name (e.g., "Bambu Lab P1S 0.4 nozzle")
741
+ * @param bambuOptions Optional BambuStudio-specific CLI flags
654
742
  * @returns Path to the generated G-code or sliced 3MF file
655
743
  */
656
- async sliceSTL(stlFilePath, slicerType, slicerPath, slicerProfile, progressCallback, printerPreset) {
744
+ async sliceSTL(stlFilePath, slicerType, slicerPath, slicerProfile, progressCallback, printerPreset, bambuOptions) {
657
745
  const operationId = this.generateOperationId();
658
746
  this.activeOperations.set(operationId, true);
659
747
  if (!fs.existsSync(slicerPath)) {
@@ -722,19 +810,57 @@ export class STLManipulator extends EventEmitter {
722
810
  const outputBase = path.basename(stlFilePath, is3mf ? '.3mf' : '.stl');
723
811
  const bambuOutputPath = path.join(this.tempDir, outputBase + '_sliced.3mf');
724
812
  args = [
725
- '--slice', '0', // Slice all plates
813
+ '--slice', String(bambuOptions?.slicePlate ?? 0),
726
814
  '--export-3mf', bambuOutputPath,
727
815
  ];
728
816
  if (slicerProfile) {
729
817
  args.push('--load-settings', slicerProfile);
730
818
  }
731
- // CRITICAL: Pass printer preset so slicer generates correct G-code
732
- // for the target printer model. Without this, the slicer uses its
733
- // default which may be a completely different printer, causing
734
- // dangerous bed crashes or axis limit violations.
819
+ // Always allow newer-version 3MF files (the CLI rejects them by default)
820
+ args.push('--allow-newer-file');
821
+ // If a printer preset name is given AND no explicit profile was provided,
822
+ // write a minimal settings JSON containing the printer_settings_id so
823
+ // BambuStudio resolves the correct machine profile from its built-in presets.
735
824
  if (printerPreset && !slicerProfile) {
736
- args.push('--load-machine', printerPreset);
825
+ const presetJson = path.join(this.tempDir, '_printer_preset.json');
826
+ fs.writeFileSync(presetJson, JSON.stringify({ printer_settings_id: printerPreset }));
827
+ args.push('--load-settings', presetJson);
737
828
  }
829
+ // BambuSliceOptions flags
830
+ if (bambuOptions?.uptodate)
831
+ args.push('--uptodate');
832
+ if (bambuOptions?.ensureOnBed)
833
+ args.push('--ensure-on-bed');
834
+ if (bambuOptions?.enableTimelapse)
835
+ args.push('--enable-timelapse');
836
+ if (bambuOptions?.allowMixTemp)
837
+ args.push('--allow-mix-temp');
838
+ if (bambuOptions?.minSave)
839
+ args.push('--min-save');
840
+ if (bambuOptions?.skipModifiedGcodes)
841
+ args.push('--skip-modified-gcodes');
842
+ if (bambuOptions?.orient !== undefined)
843
+ args.push('--orient', bambuOptions.orient ? '1' : '0');
844
+ if (bambuOptions?.arrange !== undefined)
845
+ args.push('--arrange', bambuOptions.arrange ? '1' : '0');
846
+ if (bambuOptions?.repetitions !== undefined)
847
+ args.push('--repetitions', String(bambuOptions.repetitions));
848
+ if (bambuOptions?.scale !== undefined)
849
+ args.push('--scale', String(bambuOptions.scale));
850
+ if (bambuOptions?.rotate !== undefined)
851
+ args.push('--rotate', String(bambuOptions.rotate));
852
+ if (bambuOptions?.rotateX !== undefined)
853
+ args.push('--rotate-x', String(bambuOptions.rotateX));
854
+ if (bambuOptions?.rotateY !== undefined)
855
+ args.push('--rotate-y', String(bambuOptions.rotateY));
856
+ if (bambuOptions?.cloneObjects)
857
+ args.push('--clone-objects', bambuOptions.cloneObjects);
858
+ if (bambuOptions?.skipObjects)
859
+ args.push('--skip-objects', bambuOptions.skipObjects);
860
+ if (bambuOptions?.loadFilaments)
861
+ args.push('--load-filaments', bambuOptions.loadFilaments);
862
+ if (bambuOptions?.loadFilamentIds)
863
+ args.push('--load-filament-ids', bambuOptions.loadFilamentIds);
738
864
  args.push(stlFilePath);
739
865
  // Override outputFilePath for bambustudio since it produces 3MF, not gcode
740
866
  outputFilePath = bambuOutputPath;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bambu-printer-mcp",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "MCP server for Bambu Lab 3D printers with STL manipulation, slicing, and print management",
5
5
  "main": "dist/index.js",
6
6
  "repository": "https://github.com/DMontgomery40/bambu-printer-mcp",
package/src/index.ts CHANGED
@@ -16,7 +16,7 @@ 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 } from "./stl/stl-manipulator.js";
19
+ import { STLManipulator, type BambuSliceOptions } from "./stl/stl-manipulator.js";
20
20
  import { parse3MF } from './3mf_parser.js';
21
21
  import { BambuImplementation } from "./printers/bambu.js";
22
22
 
@@ -405,7 +405,25 @@ class BambuPrinterMCPServer {
405
405
  },
406
406
  slicer_path: { type: "string", description: "Path to the slicer executable (default: value from env)" },
407
407
  slicer_profile: { type: "string", description: "Path to the slicer profile/config file (optional, overrides bambu_model preset)" },
408
- nozzle_diameter: { type: "string", description: "Nozzle diameter in mm (default: 0.4)" }
408
+ 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." },
410
+ repetitions: { type: "number", description: "Print N identical copies of the model. Each copy gets its own plate placement. Example: 3 prints three copies." },
411
+ 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
+ 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." },
413
+ ensure_on_bed: { type: "boolean", description: "Detect models floating above the bed and lower them onto the build surface. Safety net for imported models with incorrect Z origins." },
414
+ 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
+ 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
+ load_filaments: { type: "string", description: "Override filament profiles. Semicolon-separated paths to filament JSON configs, e.g. 'pla_basic.json;petg_cf.json'." },
417
+ 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
+ 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
+ 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." },
420
+ scale: { type: "number", description: "Uniform scale factor applied to all axes. 1.0 = original size, 2.0 = double, 0.5 = half. Applied before slicing." },
421
+ rotate: { type: "number", description: "Rotate the model around the Z-axis (vertical) by this many degrees before slicing. Positive = counterclockwise when viewed from above." },
422
+ rotate_x: { type: "number", description: "Rotate the model around the X-axis by this many degrees before slicing. Useful for reorienting prints for better layer adhesion." },
423
+ rotate_y: { type: "number", description: "Rotate the model around the Y-axis by this many degrees before slicing. Useful for reorienting prints for better layer adhesion." },
424
+ min_save: { type: "boolean", description: "Write a smaller output 3MF by omitting non-essential metadata. Reduces file size for faster FTP upload to the printer." },
425
+ skip_modified_gcodes: { type: "boolean", description: "Strip custom start/end gcodes embedded in the 3MF. Recommended for downloaded 3MFs since custom gcodes from other users' profiles may be unsafe for your printer." },
426
+ slice_plate: { type: "number", description: "Which plate index to slice. 0 = all plates (default). Use 1, 2, etc. to slice only a specific plate in multi-plate 3MF projects." }
409
427
  },
410
428
  required: ["stl_path", "bambu_model"]
411
429
  }
@@ -703,11 +721,33 @@ class BambuPrinterMCPServer {
703
721
  const nozzleDiam = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
704
722
  // Resolve printer preset for BambuStudio slicer
705
723
  const printerPreset = BAMBU_MODEL_PRESETS[sliceModel]?.(nozzleDiam);
724
+
725
+ const sliceBambuOptions: BambuSliceOptions = {};
726
+ if (args?.uptodate !== undefined) sliceBambuOptions.uptodate = Boolean(args.uptodate);
727
+ if (args?.repetitions !== undefined) sliceBambuOptions.repetitions = Number(args.repetitions);
728
+ if (args?.orient !== undefined) sliceBambuOptions.orient = Boolean(args.orient);
729
+ if (args?.arrange !== undefined) sliceBambuOptions.arrange = Boolean(args.arrange);
730
+ if (args?.ensure_on_bed !== undefined) sliceBambuOptions.ensureOnBed = Boolean(args.ensure_on_bed);
731
+ if (args?.clone_objects !== undefined) sliceBambuOptions.cloneObjects = String(args.clone_objects);
732
+ if (args?.skip_objects !== undefined) sliceBambuOptions.skipObjects = String(args.skip_objects);
733
+ if (args?.load_filaments !== undefined) sliceBambuOptions.loadFilaments = String(args.load_filaments);
734
+ if (args?.load_filament_ids !== undefined) sliceBambuOptions.loadFilamentIds = String(args.load_filament_ids);
735
+ if (args?.enable_timelapse !== undefined) sliceBambuOptions.enableTimelapse = Boolean(args.enable_timelapse);
736
+ if (args?.allow_mix_temp !== undefined) sliceBambuOptions.allowMixTemp = Boolean(args.allow_mix_temp);
737
+ if (args?.scale !== undefined) sliceBambuOptions.scale = Number(args.scale);
738
+ if (args?.rotate !== undefined) sliceBambuOptions.rotate = Number(args.rotate);
739
+ if (args?.rotate_x !== undefined) sliceBambuOptions.rotateX = Number(args.rotate_x);
740
+ if (args?.rotate_y !== undefined) sliceBambuOptions.rotateY = Number(args.rotate_y);
741
+ if (args?.min_save !== undefined) sliceBambuOptions.minSave = Boolean(args.min_save);
742
+ if (args?.skip_modified_gcodes !== undefined) sliceBambuOptions.skipModifiedGcodes = Boolean(args.skip_modified_gcodes);
743
+ if (args?.slice_plate !== undefined) sliceBambuOptions.slicePlate = Number(args.slice_plate);
744
+
706
745
  result = await this.stlManipulator.sliceSTL(
707
746
  String(args.stl_path), slicerType, slicerPath,
708
747
  slicerProfile || undefined,
709
748
  undefined, // progressCallback
710
- printerPreset
749
+ printerPreset,
750
+ sliceBambuOptions
711
751
  );
712
752
  break;
713
753
  }
@@ -737,10 +777,17 @@ class BambuPrinterMCPServer {
737
777
  );
738
778
  if (!hasGcode) {
739
779
  console.log(`3MF has no gcode — auto-slicing with ${slicerType} for ${printModel}`);
780
+ const autoSliceOptions: BambuSliceOptions = {
781
+ uptodate: true,
782
+ ensureOnBed: true,
783
+ minSave: true,
784
+ skipModifiedGcodes: true,
785
+ };
740
786
  threeMFPath = await this.stlManipulator.sliceSTL(
741
787
  threeMFPath, slicerType, slicerPath, slicerProfile || undefined,
742
788
  undefined, // progressCallback
743
- printPreset
789
+ printPreset,
790
+ autoSliceOptions
744
791
  );
745
792
  console.log("Auto-sliced to: " + threeMFPath);
746
793
  }
@@ -40,6 +40,28 @@ export type TransformationParams = {
40
40
  selectionBounds?: THREE.Box3;
41
41
  };
42
42
 
43
+ // BambuStudio CLI flags exposed via the slice_stl tool
44
+ export interface BambuSliceOptions {
45
+ uptodate?: boolean; // --uptodate: update 3MF configs to latest presets
46
+ repetitions?: number; // --repetitions N: print N copies
47
+ orient?: boolean; // --orient 1: auto-orient for printability
48
+ arrange?: boolean; // --arrange 1: auto-arrange objects on plate
49
+ ensureOnBed?: boolean; // --ensure-on-bed: lift floating models onto bed
50
+ cloneObjects?: string; // --clone-objects "1,3,1,10": clone counts per object
51
+ skipObjects?: string; // --skip-objects "3,5,10": skip specific objects
52
+ loadFilaments?: string; // --load-filaments "f1.json;f2.json": filament profiles
53
+ loadFilamentIds?: string; // --load-filament-ids "1,2,3,1": filament-to-object mapping
54
+ enableTimelapse?: boolean; // --enable-timelapse: timelapse-aware slicing
55
+ allowMixTemp?: boolean; // --allow-mix-temp: allow mixed-temp filaments
56
+ scale?: number; // --scale factor: uniform scale
57
+ rotate?: number; // --rotate degrees: Z-axis rotation
58
+ rotateX?: number; // --rotate-x degrees
59
+ rotateY?: number; // --rotate-y degrees
60
+ minSave?: boolean; // --min-save: smaller output 3MF
61
+ skipModifiedGcodes?: boolean;// --skip-modified-gcodes: ignore stale custom gcodes
62
+ slicePlate?: number; // --slice N: which plate (default: 0 = all)
63
+ }
64
+
43
65
  export class STLManipulator extends EventEmitter {
44
66
  private tempDir: string;
45
67
  private activeOperations: Map<string, boolean> = new Map();
@@ -141,23 +163,30 @@ export class STLManipulator extends EventEmitter {
141
163
  boundingBox: BoundingBox;
142
164
  vertexCount: number;
143
165
  faceCount: number;
166
+ objects?: Array<{ name: string; vertexCount: number; faceCount: number; boundingBox: BoundingBox }>;
144
167
  }> {
145
168
  try {
146
- const { geometry, boundingBox } = await this.loadSTL(stlFilePath);
147
169
  const fileStats = fs.statSync(stlFilePath);
148
-
170
+
171
+ // Handle 3MF files: extract mesh data from embedded object models
172
+ if (stlFilePath.toLowerCase().endsWith('.3mf')) {
173
+ return await this.get3MFInfo(stlFilePath, fileStats.size);
174
+ }
175
+
176
+ const { geometry, boundingBox } = await this.loadSTL(stlFilePath);
177
+
149
178
  // Count faces (each face is a triangle in STL)
150
179
  const positionAttribute = geometry.getAttribute('position');
151
180
  const vertexCount = positionAttribute.count;
152
181
  const faceCount = vertexCount / 3;
153
-
182
+
154
183
  // Calculate center and dimensions
155
184
  const center = new THREE.Vector3();
156
185
  boundingBox.getCenter(center);
157
-
186
+
158
187
  const dimensions = new THREE.Vector3();
159
188
  boundingBox.getSize(dimensions);
160
-
189
+
161
190
  return {
162
191
  filePath: stlFilePath,
163
192
  fileName: path.basename(stlFilePath),
@@ -177,6 +206,115 @@ export class STLManipulator extends EventEmitter {
177
206
  }
178
207
  }
179
208
 
209
+ /**
210
+ * Extract mesh info from a 3MF file by parsing the XML object models.
211
+ */
212
+ private async get3MFInfo(filePath: string, fileSize: number): Promise<{
213
+ filePath: string;
214
+ fileName: string;
215
+ fileSize: number;
216
+ boundingBox: BoundingBox;
217
+ vertexCount: number;
218
+ faceCount: number;
219
+ objects: Array<{ name: string; vertexCount: number; faceCount: number; boundingBox: BoundingBox }>;
220
+ }> {
221
+ const JSZip = (await import('jszip')).default;
222
+ const data = fs.readFileSync(filePath);
223
+ const zip = await JSZip.loadAsync(data);
224
+
225
+ // Find all object model files
226
+ const objectFiles = Object.keys(zip.files).filter(
227
+ (name) => /^3D\/Objects\/.*\.model$/i.test(name)
228
+ );
229
+
230
+ const overallBox = new THREE.Box3(
231
+ new THREE.Vector3(Infinity, Infinity, Infinity),
232
+ new THREE.Vector3(-Infinity, -Infinity, -Infinity)
233
+ );
234
+ let totalVerts = 0;
235
+ let totalFaces = 0;
236
+ const objects: Array<{ name: string; vertexCount: number; faceCount: number; boundingBox: BoundingBox }> = [];
237
+
238
+ for (const objFile of objectFiles) {
239
+ const content = await zip.file(objFile)!.async('string');
240
+ // Parse vertices from XML: <vertex x="..." y="..." z="..."/>
241
+ const vertexRegex = /<(?:\w+:)?vertex\s+x="([^"]+)"\s+y="([^"]+)"\s+z="([^"]+)"/g;
242
+ const triangleRegex = /<(?:\w+:)?triangle\s/g;
243
+
244
+ const objBox = new THREE.Box3(
245
+ new THREE.Vector3(Infinity, Infinity, Infinity),
246
+ new THREE.Vector3(-Infinity, -Infinity, -Infinity)
247
+ );
248
+ let vCount = 0;
249
+ let match: RegExpExecArray | null;
250
+ while ((match = vertexRegex.exec(content)) !== null) {
251
+ const x = parseFloat(match[1]);
252
+ const y = parseFloat(match[2]);
253
+ const z = parseFloat(match[3]);
254
+ objBox.expandByPoint(new THREE.Vector3(x, y, z));
255
+ overallBox.expandByPoint(new THREE.Vector3(x, y, z));
256
+ vCount++;
257
+ }
258
+
259
+ let fCount = 0;
260
+ while (triangleRegex.exec(content) !== null) fCount++;
261
+
262
+ totalVerts += vCount;
263
+ totalFaces += fCount;
264
+
265
+ if (vCount > 0) {
266
+ const center = new THREE.Vector3();
267
+ objBox.getCenter(center);
268
+ const dims = new THREE.Vector3();
269
+ objBox.getSize(dims);
270
+ objects.push({
271
+ name: path.basename(objFile),
272
+ vertexCount: vCount,
273
+ faceCount: fCount,
274
+ boundingBox: { min: objBox.min, max: objBox.max, center, dimensions: dims },
275
+ });
276
+ }
277
+ }
278
+
279
+ // If no object files found, try the main 3dmodel.model
280
+ if (objects.length === 0) {
281
+ const mainModel = zip.file('3D/3dmodel.model');
282
+ if (mainModel) {
283
+ const content = await mainModel.async('string');
284
+ const vertexRegex = /<(?:\w+:)?vertex\s+x="([^"]+)"\s+y="([^"]+)"\s+z="([^"]+)"/g;
285
+ const triangleRegex = /<(?:\w+:)?triangle\s/g;
286
+ let match: RegExpExecArray | null;
287
+ while ((match = vertexRegex.exec(content)) !== null) {
288
+ const x = parseFloat(match[1]);
289
+ const y = parseFloat(match[2]);
290
+ const z = parseFloat(match[3]);
291
+ overallBox.expandByPoint(new THREE.Vector3(x, y, z));
292
+ totalVerts++;
293
+ }
294
+ while (triangleRegex.exec(content) !== null) totalFaces++;
295
+ }
296
+ }
297
+
298
+ if (totalVerts === 0) {
299
+ throw new Error('No mesh data found in 3MF file. The archive may not contain any 3D objects.');
300
+ }
301
+
302
+ const center = new THREE.Vector3();
303
+ overallBox.getCenter(center);
304
+ const dimensions = new THREE.Vector3();
305
+ overallBox.getSize(dimensions);
306
+
307
+ return {
308
+ filePath,
309
+ fileName: path.basename(filePath),
310
+ fileSize,
311
+ boundingBox: { min: overallBox.min, max: overallBox.max, center, dimensions },
312
+ vertexCount: totalVerts,
313
+ faceCount: totalFaces,
314
+ objects,
315
+ };
316
+ }
317
+
180
318
  /**
181
319
  * Scale an STL model uniformly or along specific axes
182
320
  */
@@ -850,6 +988,7 @@ export class STLManipulator extends EventEmitter {
850
988
  * @param slicerProfile Optional path to the slicer profile/config file
851
989
  * @param progressCallback Optional callback for progress updates
852
990
  * @param printerPreset Optional BambuStudio printer preset name (e.g., "Bambu Lab P1S 0.4 nozzle")
991
+ * @param bambuOptions Optional BambuStudio-specific CLI flags
853
992
  * @returns Path to the generated G-code or sliced 3MF file
854
993
  */
855
994
  async sliceSTL(
@@ -858,7 +997,8 @@ export class STLManipulator extends EventEmitter {
858
997
  slicerPath: string,
859
998
  slicerProfile?: string,
860
999
  progressCallback?: ProgressCallback,
861
- printerPreset?: string
1000
+ printerPreset?: string,
1001
+ bambuOptions?: BambuSliceOptions
862
1002
  ): Promise<string> {
863
1003
  const operationId = this.generateOperationId();
864
1004
  this.activeOperations.set(operationId, true);
@@ -935,19 +1075,43 @@ export class STLManipulator extends EventEmitter {
935
1075
  const outputBase = path.basename(stlFilePath, is3mf ? '.3mf' : '.stl');
936
1076
  const bambuOutputPath = path.join(this.tempDir, outputBase + '_sliced.3mf');
937
1077
  args = [
938
- '--slice', '0', // Slice all plates
1078
+ '--slice', String(bambuOptions?.slicePlate ?? 0),
939
1079
  '--export-3mf', bambuOutputPath,
940
1080
  ];
941
1081
  if (slicerProfile) {
942
1082
  args.push('--load-settings', slicerProfile);
943
1083
  }
944
- // CRITICAL: Pass printer preset so slicer generates correct G-code
945
- // for the target printer model. Without this, the slicer uses its
946
- // default which may be a completely different printer, causing
947
- // dangerous bed crashes or axis limit violations.
1084
+ // Always allow newer-version 3MF files (the CLI rejects them by default)
1085
+ args.push('--allow-newer-file');
1086
+
1087
+ // If a printer preset name is given AND no explicit profile was provided,
1088
+ // write a minimal settings JSON containing the printer_settings_id so
1089
+ // BambuStudio resolves the correct machine profile from its built-in presets.
948
1090
  if (printerPreset && !slicerProfile) {
949
- args.push('--load-machine', printerPreset);
1091
+ const presetJson = path.join(this.tempDir, '_printer_preset.json');
1092
+ fs.writeFileSync(presetJson, JSON.stringify({ printer_settings_id: printerPreset }));
1093
+ args.push('--load-settings', presetJson);
950
1094
  }
1095
+
1096
+ // BambuSliceOptions flags
1097
+ if (bambuOptions?.uptodate) args.push('--uptodate');
1098
+ if (bambuOptions?.ensureOnBed) args.push('--ensure-on-bed');
1099
+ if (bambuOptions?.enableTimelapse) args.push('--enable-timelapse');
1100
+ if (bambuOptions?.allowMixTemp) args.push('--allow-mix-temp');
1101
+ if (bambuOptions?.minSave) args.push('--min-save');
1102
+ if (bambuOptions?.skipModifiedGcodes) args.push('--skip-modified-gcodes');
1103
+ if (bambuOptions?.orient !== undefined) args.push('--orient', bambuOptions.orient ? '1' : '0');
1104
+ if (bambuOptions?.arrange !== undefined) args.push('--arrange', bambuOptions.arrange ? '1' : '0');
1105
+ if (bambuOptions?.repetitions !== undefined) args.push('--repetitions', String(bambuOptions.repetitions));
1106
+ if (bambuOptions?.scale !== undefined) args.push('--scale', String(bambuOptions.scale));
1107
+ if (bambuOptions?.rotate !== undefined) args.push('--rotate', String(bambuOptions.rotate));
1108
+ if (bambuOptions?.rotateX !== undefined) args.push('--rotate-x', String(bambuOptions.rotateX));
1109
+ if (bambuOptions?.rotateY !== undefined) args.push('--rotate-y', String(bambuOptions.rotateY));
1110
+ if (bambuOptions?.cloneObjects) args.push('--clone-objects', bambuOptions.cloneObjects);
1111
+ if (bambuOptions?.skipObjects) args.push('--skip-objects', bambuOptions.skipObjects);
1112
+ if (bambuOptions?.loadFilaments) args.push('--load-filaments', bambuOptions.loadFilaments);
1113
+ if (bambuOptions?.loadFilamentIds) args.push('--load-filament-ids', bambuOptions.loadFilamentIds);
1114
+
951
1115
  args.push(stlFilePath);
952
1116
  // Override outputFilePath for bambustudio since it produces 3MF, not gcode
953
1117
  outputFilePath = bambuOutputPath;