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.
@@ -3,6 +3,7 @@ import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import JSZip from "jszip";
5
5
  import { Client as FTPClient } from "basic-ftp";
6
+ import { Readable } from "node:stream";
6
7
  import { BambuPrinter } from "bambu-js";
7
8
  import {
8
9
  BambuClient,
@@ -251,7 +252,7 @@ export class BambuImplementation {
251
252
  print: {
252
253
  command: "project_file",
253
254
  param: `Metadata/${projectMetadata.plateFileName}`,
254
- url: `file:///sdcard/${remoteProjectPath}`,
255
+ url: `ftp://${remoteFileName}`,
255
256
  subtask_name: options.projectName,
256
257
  md5,
257
258
  flow_cali: options.flowCalibration ?? true,
@@ -275,7 +276,7 @@ export class BambuImplementation {
275
276
 
276
277
  return {
277
278
  status: "success",
278
- message: `Uploaded and started 3MF print: ${options.projectName}`,
279
+ message: `Uploaded 3MF and sent project_file print command: ${options.projectName}`,
279
280
  remoteProjectPath,
280
281
  plateFile: projectMetadata.plateFileName,
281
282
  platePath: projectMetadata.plateInternalPath,
@@ -454,7 +455,7 @@ export class BambuImplementation {
454
455
  localPath: string,
455
456
  remotePath: string
456
457
  ): Promise<void> {
457
- const client = new FTPClient(15_000);
458
+ const client = new FTPClient(30_000);
458
459
  try {
459
460
  await client.access({
460
461
  host,
@@ -464,12 +465,9 @@ export class BambuImplementation {
464
465
  secure: "implicit",
465
466
  secureOptions: { rejectUnauthorized: false },
466
467
  });
467
- // Use absolute path to avoid CWD side-effects
468
468
  const absoluteRemote = remotePath.startsWith("/") ? remotePath : `/${remotePath}`;
469
- const remoteDir = path.posix.dirname(absoluteRemote);
470
- await client.ensureDir(remoteDir);
471
- // uploadFrom with just the basename since we're already in the right dir
472
- await client.uploadFrom(localPath, path.posix.basename(absoluteRemote));
469
+ const fileData = await fs.readFile(localPath);
470
+ await client.uploadFrom(Readable.from(fileData), absoluteRemote);
473
471
  } finally {
474
472
  client.close();
475
473
  }
@@ -3,6 +3,7 @@ import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js';
3
3
  import { STLExporter } from 'three/examples/jsm/exporters/STLExporter.js';
4
4
  import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js';
5
5
  import fs from 'fs';
6
+ import os from 'os';
6
7
  import path from 'path';
7
8
  import { promisify } from 'util';
8
9
  import { EventEmitter } from 'events';
@@ -40,7 +41,124 @@ export type TransformationParams = {
40
41
  selectionBounds?: THREE.Box3;
41
42
  };
42
43
 
43
- // BambuStudio CLI flags exposed via the slice_stl tool
44
+ export const SLICER_TYPES = [
45
+ 'bambustudio',
46
+ 'orcaslicer',
47
+ 'orcaslicer-bambulab',
48
+ 'prusaslicer',
49
+ 'cura',
50
+ 'slic3r',
51
+ ] as const;
52
+
53
+ export type SlicerType = typeof SLICER_TYPES[number];
54
+
55
+ const SLICER_TYPE_ALIASES: Record<string, SlicerType> = {
56
+ bambustudio: 'bambustudio',
57
+ 'bambu-studio': 'bambustudio',
58
+ bblstudio: 'bambustudio',
59
+ orcaslicer: 'orcaslicer',
60
+ 'orca-slicer': 'orcaslicer',
61
+ orca: 'orcaslicer',
62
+ 'orcaslicer-bambulab': 'orcaslicer-bambulab',
63
+ 'orca-slicer-bambulab': 'orcaslicer-bambulab',
64
+ 'orca-bambulab': 'orcaslicer-bambulab',
65
+ 'fulu-orca': 'orcaslicer-bambulab',
66
+ 'fulu-orcaslicer': 'orcaslicer-bambulab',
67
+ 'fulu-orca-slicer': 'orcaslicer-bambulab',
68
+ 'orca-studio': 'orcaslicer-bambulab',
69
+ orcastudio: 'orcaslicer-bambulab',
70
+ 'orcaslicer-bmcu': 'orcaslicer-bambulab',
71
+ 'orca-bmcu': 'orcaslicer-bambulab',
72
+ prusaslicer: 'prusaslicer',
73
+ 'prusa-slicer': 'prusaslicer',
74
+ cura: 'cura',
75
+ curaengine: 'cura',
76
+ slic3r: 'slic3r',
77
+ };
78
+
79
+ export function normalizeSlicerType(rawSlicerType: string): SlicerType {
80
+ const key = rawSlicerType.trim().toLowerCase().replace(/_/g, '-');
81
+ const normalized = SLICER_TYPE_ALIASES[key];
82
+
83
+ if (!normalized) {
84
+ throw new Error(
85
+ `Unsupported slicer_type: "${rawSlicerType}". Valid values: ${SLICER_TYPES.join(', ')}`
86
+ );
87
+ }
88
+
89
+ return normalized;
90
+ }
91
+
92
+ export function isBambuCompatibleSlicer(slicerType: SlicerType): boolean {
93
+ return (
94
+ slicerType === 'bambustudio' ||
95
+ slicerType === 'orcaslicer' ||
96
+ slicerType === 'orcaslicer-bambulab'
97
+ );
98
+ }
99
+
100
+ function isPathLikeExecutable(executable: string): boolean {
101
+ return path.isAbsolute(executable) || /[\\/]/.test(executable);
102
+ }
103
+
104
+ function configuredBambuProfileDirs(): string[] {
105
+ const override = process.env.BAMBU_SLICER_PROFILE_DIRS;
106
+ if (override !== undefined) {
107
+ return override.split(path.delimiter).filter(Boolean);
108
+ }
109
+
110
+ const home = os.homedir();
111
+ const macAppSupport = path.join(home, 'Library', 'Application Support');
112
+ const linuxConfig = path.join(home, '.config');
113
+ const appData = process.env.APPDATA;
114
+ const localAppData = process.env.LOCALAPPDATA;
115
+ const appNames = [
116
+ 'BambuStudio',
117
+ 'Bambu Studio',
118
+ 'OrcaSlicer',
119
+ 'Orca Slicer',
120
+ 'OrcaStudio',
121
+ 'Orca Studio',
122
+ ];
123
+
124
+ const roots = [
125
+ macAppSupport,
126
+ linuxConfig,
127
+ ...(appData ? [appData] : []),
128
+ ...(localAppData ? [localAppData] : []),
129
+ ];
130
+
131
+ return roots.flatMap((root) =>
132
+ appNames.map((appName) => path.join(root, appName, 'system', 'BBL', 'machine'))
133
+ );
134
+ }
135
+
136
+ function resolveBambuPrinterProfile(printerPreset: string): string | undefined {
137
+ if (printerPreset.includes('/') || printerPreset.includes('\\')) {
138
+ return undefined;
139
+ }
140
+
141
+ for (const profileDir of configuredBambuProfileDirs()) {
142
+ const profilePath = path.join(profileDir, `${printerPreset}.json`);
143
+ if (fs.existsSync(profilePath)) {
144
+ return profilePath;
145
+ }
146
+ }
147
+
148
+ return undefined;
149
+ }
150
+
151
+ function generatedBambuPrinterSettings(printerPreset: string): Record<string, string> {
152
+ return {
153
+ type: 'machine',
154
+ from: 'system',
155
+ name: printerPreset,
156
+ inherits: printerPreset,
157
+ printer_settings_id: printerPreset,
158
+ };
159
+ }
160
+
161
+ // Bambu-compatible slicer CLI flags exposed via the slice_stl tool
44
162
  export interface BambuSliceOptions {
45
163
  uptodate?: boolean; // --uptodate: update 3MF configs to latest presets
46
164
  repetitions?: number; // --repetitions N: print N copies
@@ -983,17 +1101,17 @@ export class STLManipulator extends EventEmitter {
983
1101
  /**
984
1102
  * Slice an STL or 3MF file using the specified slicer
985
1103
  * @param stlFilePath Path to the input STL or 3MF file
986
- * @param slicerType Type of slicer (prusaslicer, cura, slic3r, orcaslicer, bambustudio)
1104
+ * @param slicerType Type of slicer (prusaslicer, cura, slic3r, orcaslicer, orcaslicer-bambulab, bambustudio)
987
1105
  * @param slicerPath Path to the slicer executable
988
1106
  * @param slicerProfile Optional path to the slicer profile/config file
989
1107
  * @param progressCallback Optional callback for progress updates
990
- * @param printerPreset Optional BambuStudio printer preset name (e.g., "Bambu Lab P1S 0.4 nozzle")
991
- * @param bambuOptions Optional BambuStudio-specific CLI flags
1108
+ * @param printerPreset Optional Bambu-compatible printer preset name (e.g., "Bambu Lab P1S 0.4 nozzle")
1109
+ * @param bambuOptions Optional Bambu-compatible CLI flags
992
1110
  * @returns Path to the generated G-code or sliced 3MF file
993
1111
  */
994
1112
  async sliceSTL(
995
1113
  stlFilePath: string,
996
- slicerType: 'prusaslicer' | 'cura' | 'slic3r' | 'orcaslicer' | 'bambustudio',
1114
+ slicerType: SlicerType,
997
1115
  slicerPath: string,
998
1116
  slicerProfile?: string,
999
1117
  progressCallback?: ProgressCallback,
@@ -1003,7 +1121,7 @@ export class STLManipulator extends EventEmitter {
1003
1121
  const operationId = this.generateOperationId();
1004
1122
  this.activeOperations.set(operationId, true);
1005
1123
 
1006
- if (!fs.existsSync(slicerPath)) {
1124
+ if (isPathLikeExecutable(slicerPath) && !fs.existsSync(slicerPath)) {
1007
1125
  throw new Error(`Slicer executable not found at: ${slicerPath}`);
1008
1126
  }
1009
1127
  if (slicerProfile && !fs.existsSync(slicerProfile)) {
@@ -1032,25 +1150,6 @@ export class STLManipulator extends EventEmitter {
1032
1150
  args = args.filter(arg => arg !== '');
1033
1151
  break;
1034
1152
 
1035
- case 'orcaslicer': // Add OrcaSlicer case
1036
- args = [
1037
- // OrcaSlicer might use --load-settings like Bambu, or --load like Prusa
1038
- // Assuming --load-settings based on discussion, adjust if needed.
1039
- // Also assuming profile path points to the main .ini or .json config.
1040
- '--load-settings', slicerProfile || '',
1041
- '--output', outputFilePath, // Assuming --output works like PrusaSlicer
1042
- stlFilePath
1043
- ];
1044
- // Alternative if --output doesn't specify filename:
1045
- // args = [
1046
- // '--load-settings', slicerProfile || '',
1047
- // '--outputdir', this.tempDir,
1048
- // stlFilePath
1049
- // ];
1050
- // Remove empty profile arg if not provided
1051
- args = args.filter(arg => arg !== '');
1052
- break;
1053
-
1054
1153
  case 'cura':
1055
1154
  // CuraEngine CLI args are different, often requiring -s for settings
1056
1155
  // Example: curaengine slice -v -j cura_settings.json -s layer_height=0.2 -o output.gcode -l input.stl
@@ -1067,7 +1166,12 @@ export class STLManipulator extends EventEmitter {
1067
1166
  break;
1068
1167
 
1069
1168
  case 'bambustudio':
1070
- // Bambu Studio CLI: slice and export as 3MF with embedded gcode
1169
+ case 'orcaslicer':
1170
+ case 'orcaslicer-bambulab':
1171
+ // Bambu-compatible CLI: slice and export as 3MF with embedded gcode.
1172
+ // FULU's OrcaSlicer-bambulab fork keeps OrcaSlicer's Bambu-style
1173
+ // --slice/--export-3mf path, so it must not use the generic Prusa
1174
+ // --output G-code flow.
1071
1175
  // For 3MF input: slice in place and export sliced 3MF
1072
1176
  // For STL input: slice and export as 3MF
1073
1177
  {
@@ -1085,15 +1189,21 @@ export class STLManipulator extends EventEmitter {
1085
1189
  args.push('--allow-newer-file');
1086
1190
 
1087
1191
  // 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.
1192
+ // prefer the slicer's installed machine profile. Real BambuStudio rejects
1193
+ // skeletal JSON without profile metadata, so only fall back to generated
1194
+ // settings when no installed profile can be found.
1090
1195
  if (printerPreset && !slicerProfile) {
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);
1196
+ const installedProfile = resolveBambuPrinterProfile(printerPreset);
1197
+ if (installedProfile) {
1198
+ args.push('--load-settings', installedProfile);
1199
+ } else {
1200
+ const presetJson = path.join(this.tempDir, '_printer_preset.json');
1201
+ fs.writeFileSync(presetJson, JSON.stringify(generatedBambuPrinterSettings(printerPreset), null, 2));
1202
+ args.push('--load-settings', presetJson);
1203
+ }
1094
1204
  }
1095
1205
 
1096
- // BambuSliceOptions flags
1206
+ // Bambu-compatible slicer flags
1097
1207
  if (bambuOptions?.uptodate) args.push('--uptodate');
1098
1208
  if (bambuOptions?.ensureOnBed) args.push('--ensure-on-bed');
1099
1209
  if (bambuOptions?.enableTimelapse) args.push('--enable-timelapse');
@@ -1113,7 +1223,7 @@ export class STLManipulator extends EventEmitter {
1113
1223
  if (bambuOptions?.loadFilamentIds) args.push('--load-filament-ids', bambuOptions.loadFilamentIds);
1114
1224
 
1115
1225
  args.push(stlFilePath);
1116
- // Override outputFilePath for bambustudio since it produces 3MF, not gcode
1226
+ // Override outputFilePath because Bambu-compatible slicers produce 3MF, not G-code.
1117
1227
  outputFilePath = bambuOutputPath;
1118
1228
  }
1119
1229
  break;