bambu-printer-mcp 1.1.26 → 1.1.28

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.
@@ -27,6 +27,7 @@ import fs from "node:fs/promises";
27
27
  import { existsSync, realpathSync } from "node:fs";
28
28
  import path from "node:path";
29
29
  import crypto from "node:crypto";
30
+ import { profileJsonFiles } from './profile-files.js';
30
31
  /* -------------------------------------------------------------------------- */
31
32
  /* Indexing */
32
33
  /* -------------------------------------------------------------------------- */
@@ -48,19 +49,7 @@ async function buildNameIndex(profilesRoot, vendor, userProfileRoots = []) {
48
49
  ...userProfileRoots.flatMap(root => ['process', 'filament'].map(sub => path.join(root, sub))),
49
50
  ];
50
51
  for (const dir of directories) {
51
- let entries;
52
- try {
53
- entries = await fs.readdir(dir);
54
- }
55
- catch {
56
- // Vendor or subdir missing -- skip silently; flatten will fail later
57
- // with a precise "name not found" error.
58
- continue;
59
- }
60
- for (const entry of entries) {
61
- if (!entry.endsWith(".json"))
62
- continue;
63
- const filePath = path.join(dir, entry);
52
+ for (const filePath of profileJsonFiles(dir)) {
64
53
  let raw;
65
54
  try {
66
55
  raw = await fs.readFile(filePath, "utf8");
@@ -239,6 +228,29 @@ function enforceMatchingNozzles(arr) {
239
228
  }
240
229
  }
241
230
  }
231
+ /**
232
+ * BambuStudio picks each per-variant machine, process, and filament value
233
+ * (e.g. filament_max_volumetric_speed) by "<extruder_type> <nozzle_volume_type>"
234
+ * among the variants each extruder declares in extruder_variant_list. A flow
235
+ * type the preset doesn't declare has no values to pick, so refuse it instead
236
+ * of labelling a job for a nozzle it wasn't sliced for.
237
+ */
238
+ function assertNozzleVariantDeclared(flat, volumeType, machineLeaf) {
239
+ const strings = (key) => Array.isArray(flat[key]) ? flat[key].map(String) : [];
240
+ const extruderTypes = strings("extruder_type");
241
+ const declaredLists = strings("extruder_variant_list");
242
+ const count = flat["nozzle_volume_type"].length;
243
+ for (let i = 0; i < count; i++) {
244
+ const extruderType = extruderTypes[i] ?? extruderTypes[0];
245
+ const declared = (declaredLists[i] ?? declaredLists[0] ?? "").split(",").map((v) => v.trim()).filter(Boolean);
246
+ const matches = (variant) => extruderType ? variant === `${extruderType} ${volumeType}` : variant.endsWith(` ${volumeType}`);
247
+ // Presets without variant declarations only have Standard values.
248
+ if (declared.length === 0 ? volumeType === "Standard" : declared.some(matches))
249
+ continue;
250
+ throw new Error(`Slicing for a ${volumeType} nozzle is refused: "${machineLeaf}" declares no ${volumeType} variant ` +
251
+ `for extruder ${i + 1} (declared: ${declared.join(", ") || "none"}).`);
252
+ }
253
+ }
242
254
  /** Best-effort extruder count for fallback nozzle_volume_type sizing. */
243
255
  /** BambuStudio's built-in filament_colour default. */
244
256
  export const DEFAULT_FILAMENT_COLOUR = "#00AE42";
@@ -556,6 +568,8 @@ export async function flattenForCli(opts) {
556
568
  });
557
569
  // CLI-specific post-processing on machine profile only.
558
570
  deriveNozzleVolumeType(machineFlat, opts.nozzleVolumeType);
571
+ if (opts.nozzleVolumeType)
572
+ assertNozzleVariantDeclared(machineFlat, opts.nozzleVolumeType, opts.machineLeaf);
559
573
  applyMachineModelBedMetadata(machineFlat, machineIndex);
560
574
  // A standalone custom machine can accompany process/filament profiles that
561
575
  // need BBL resolution; do not impose bundled model config on that machine.
@@ -39,6 +39,7 @@ export interface BambuSliceOptions {
39
39
  filamentColours?: string[];
40
40
  bedType?: string;
41
41
  nozzleType?: string;
42
+ nozzleVolumeType?: 'Standard' | 'High Flow';
42
43
  enableTimelapse?: boolean;
43
44
  allowMixTemp?: boolean;
44
45
  scale?: number;
@@ -10,6 +10,7 @@ import { EventEmitter } from 'events';
10
10
  import * as crypto from 'crypto';
11
11
  import { execFile } from 'child_process';
12
12
  import { flattenForCli, detectProfilesRoot, resolveBblMachineProfile } from '../slicer/profile-flatten.js';
13
+ import { profileJsonFiles } from '../slicer/profile-files.js';
13
14
  const readFileAsync = promisify(fs.readFile);
14
15
  const writeFileAsync = promisify(fs.writeFile);
15
16
  const BAMBU_PROFILE_ROOTS = [
@@ -134,10 +135,10 @@ export class STLManipulator extends EventEmitter {
134
135
  return undefined;
135
136
  }
136
137
  for (const root of roots) {
137
- const candidate = path.join(root, category, `${profileName}.json`);
138
- if (fs.existsSync(candidate)) {
138
+ const candidate = profileJsonFiles(path.join(root, category))
139
+ .find(file => path.basename(file) === `${profileName}.json`);
140
+ if (candidate)
139
141
  return candidate;
140
- }
141
142
  }
142
143
  return undefined;
143
144
  }
@@ -148,11 +149,7 @@ export class STLManipulator extends EventEmitter {
148
149
  if (!fs.existsSync(filamentDir)) {
149
150
  continue;
150
151
  }
151
- for (const entry of fs.readdirSync(filamentDir)) {
152
- if (!entry.endsWith('.json')) {
153
- continue;
154
- }
155
- const filePath = path.join(filamentDir, entry);
152
+ for (const filePath of profileJsonFiles(filamentDir)) {
156
153
  try {
157
154
  const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
158
155
  const filamentId = typeof parsed?.filament_id === 'string' ? parsed.filament_id.trim() : '';
@@ -408,6 +405,7 @@ export class STLManipulator extends EventEmitter {
408
405
  userProfileRoots: configuredBambuProfileDirs(),
409
406
  tempDir: this.tempDir,
410
407
  bedType: this.resolveBambuStudioBedType(bambuOptions?.bedType),
408
+ nozzleVolumeType: bambuOptions?.nozzleVolumeType,
411
409
  filamentColours: bambuOptions?.filamentColours ?? bundle.filamentColours,
412
410
  projectTowerPosition: bundle.projectTowerPosition,
413
411
  sourceProfiles: {
@@ -14,6 +14,10 @@
14
14
  #include <sstream>
15
15
  #include <string>
16
16
  #include <sys/stat.h>
17
+ #if !defined(__APPLE__)
18
+ #include <pwd.h>
19
+ #include <unistd.h>
20
+ #endif
17
21
  #include <thread>
18
22
 
19
23
  // This small adapter intentionally mirrors the public C++ ABI declarations
@@ -111,13 +115,37 @@ void outputLine(const std::string &line) {
111
115
  std::cout << line << std::endl;
112
116
  }
113
117
 
118
+ // End a one-shot run once the plug-in's own cleanup (destroy_agent) is done.
119
+ // Its worker threads can outlive that cleanup; process-exit destructors and
120
+ // dlclose then crashed successful command, upload and probe runs with SIGSEGV
121
+ // on Linux. Flush the protocol output and let the OS reclaim the rest.
122
+ [[noreturn]] void exitNow(int code) {
123
+ std::cout.flush();
124
+ std::cerr.flush();
125
+ std::_Exit(code);
126
+ }
127
+
114
128
  std::string envOr(const char *name, const std::string &fallback = {}) {
115
129
  const char *value = std::getenv(name);
116
130
  return value && *value ? std::string(value) : fallback;
117
131
  }
118
132
 
119
133
  std::string homeDir() {
134
+ #if defined(__APPLE__)
120
135
  return envOr("HOME", "/Users/Shared");
136
+ #else
137
+ // Match Node's os.homedir() so the server and helper agree without HOME.
138
+ const std::string home = envOr("HOME");
139
+ if (!home.empty()) return home;
140
+ const passwd *entry = getpwuid(getuid());
141
+ return entry && entry->pw_dir ? std::string(entry->pw_dir) : std::string("/");
142
+ #endif
143
+ }
144
+
145
+ #if defined(__APPLE__)
146
+ bool isRegularFile(const std::string &path) {
147
+ struct stat fileStat {};
148
+ return stat(path.c_str(), &fileStat) == 0 && S_ISREG(fileStat.st_mode);
121
149
  }
122
150
 
123
151
  std::string defaultPluginPath() {
@@ -125,13 +153,43 @@ std::string defaultPluginPath() {
125
153
  // present; older Bambu Studio installations remain a compatible fallback.
126
154
  const std::string connectPlugin =
127
155
  "/Applications/Bambu Connect.app/Contents/Resources/app.asar.unpacked/plugins/plugins-mac/libbambu_networking.dylib";
128
- struct stat connectStat {};
129
- if (stat(connectPlugin.c_str(), &connectStat) == 0 && S_ISREG(connectStat.st_mode)) {
130
- return connectPlugin;
131
- }
156
+ if (isRegularFile(connectPlugin)) return connectPlugin;
132
157
  return homeDir() + "/Library/Application Support/BambuStudio/plugins/libbambu_networking.dylib";
133
158
  }
134
159
 
160
+ std::string defaultConfigDir() {
161
+ return envOr("BAMBU_NATIVE_CONFIG_DIR", homeDir() + "/Library/Application Support/BambuStudio");
162
+ }
163
+
164
+ std::string defaultCertDir() {
165
+ return envOr("BAMBU_NATIVE_CERT_DIR", "/Applications/BambuStudio.app/Contents/Resources/cert");
166
+ }
167
+ #else
168
+ // Linux has no application bundle to borrow from. The setup script installs a
169
+ // pinned plug-in and certificates under one private directory. The server
170
+ // passes the resolved paths explicitly; these defaults only serve direct use
171
+ // and mirror bambuNativeManagedPaths() in src/bambu-native.ts.
172
+ std::string nativeHomeDir() {
173
+ const std::string configured = envOr("BAMBU_NATIVE_HOME");
174
+ if (!configured.empty()) return configured;
175
+ return envOr("XDG_CONFIG_HOME", homeDir() + "/.config") + "/bambu-printer-mcp/bambu-native";
176
+ }
177
+
178
+ std::string defaultPluginPath() {
179
+ // Only the pinned plug-in: Bambu Studio's own copy auto-updates and its
180
+ // PrintParams layout is not checked against this helper.
181
+ return nativeHomeDir() + "/plugins/libbambu_networking.so";
182
+ }
183
+
184
+ std::string defaultConfigDir() {
185
+ return envOr("BAMBU_NATIVE_CONFIG_DIR", nativeHomeDir() + "/config");
186
+ }
187
+
188
+ std::string defaultCertDir() {
189
+ return envOr("BAMBU_NATIVE_CERT_DIR", nativeHomeDir() + "/cert");
190
+ }
191
+ #endif
192
+
135
193
  void *symbol(void *handle, const char *name) {
136
194
  return dlsym(handle, name);
137
195
  }
@@ -238,10 +296,8 @@ void runProbe(NativeApi &api) {
238
296
 
239
297
  // Avoid calling the C++ string-returning symbol in the probe. The tunnel
240
298
  // API is C ABI and is the only part needed to verify the X2D route.
241
- const std::string configDir = envOr("BAMBU_NATIVE_CONFIG_DIR",
242
- homeDir() + "/Library/Application Support/BambuStudio");
243
- const std::string certDir = envOr("BAMBU_NATIVE_CERT_DIR",
244
- "/Applications/BambuStudio.app/Contents/Resources/cert");
299
+ const std::string configDir = defaultConfigDir();
300
+ const std::string certDir = defaultCertDir();
245
301
  const std::string certFile = envOr("BAMBU_NATIVE_CERT_FILE", "slicer_base64.cer");
246
302
  void *agent = api.createAgent(configDir);
247
303
  if (!agent) throw std::runtime_error("bambu_network_create_agent returned null in probe");
@@ -256,7 +312,8 @@ void runProbe(NativeApi &api) {
256
312
  const FtErr createResult = api.ftTunnelCreate(localUrl(host, token).c_str(), &tunnel);
257
313
  if (createResult != 0 || !tunnel) {
258
314
  outputLine("probe=failed stage=tunnel_create result=" + std::to_string(createResult));
259
- return;
315
+ api.destroyAgent(agent);
316
+ exitNow(22);
260
317
  }
261
318
 
262
319
  const FtErr connectResult = api.ftTunnelSyncConnect(tunnel);
@@ -264,6 +321,7 @@ void runProbe(NativeApi &api) {
264
321
  api.destroyAgent(agent);
265
322
  outputLine("probe=local_tunnel version=" + version + " ft_abi=" + std::to_string(abi) +
266
323
  " connect_result=" + std::to_string(connectResult));
324
+ if (connectResult != 0) exitNow(22);
267
325
  }
268
326
 
269
327
  void runMqttProbe(NativeApi &api) {
@@ -274,10 +332,8 @@ void runMqttProbe(NativeApi &api) {
274
332
  throw std::runtime_error("BAMBU_NATIVE_HOST, BAMBU_NATIVE_ACCESS_CODE, and BAMBU_NATIVE_SERIAL are required for --probe-mqtt");
275
333
  }
276
334
 
277
- const std::string configDir = envOr("BAMBU_NATIVE_CONFIG_DIR",
278
- homeDir() + "/Library/Application Support/BambuStudio");
279
- const std::string certDir = envOr("BAMBU_NATIVE_CERT_DIR",
280
- "/Applications/BambuStudio.app/Contents/Resources/cert");
335
+ const std::string configDir = defaultConfigDir();
336
+ const std::string certDir = defaultCertDir();
281
337
  const std::string certFile = envOr("BAMBU_NATIVE_CERT_FILE", "slicer_base64.cer");
282
338
  void *agent = api.createAgent(configDir);
283
339
  if (!agent) throw std::runtime_error("bambu_network_create_agent returned null in MQTT probe");
@@ -384,7 +440,7 @@ void runMqttProbe(NativeApi &api) {
384
440
  destroyAgent();
385
441
  outputLine("probe=mqtt status=" + std::to_string(status) +
386
442
  (message.empty() ? std::string{} : " message=" + message));
387
- if (!ok) std::exit(21);
443
+ exitNow(ok ? 0 : 21);
388
444
  }
389
445
 
390
446
  std::string bedTypeForNative(const std::string &bedType) {
@@ -475,10 +531,8 @@ void runOperation(NativeApi &api, NativeOperation operation) {
475
531
  " file is not readable: " + file);
476
532
  }
477
533
 
478
- const std::string configDir = envOr("BAMBU_NATIVE_CONFIG_DIR",
479
- homeDir() + "/Library/Application Support/BambuStudio");
480
- const std::string certDir = envOr("BAMBU_NATIVE_CERT_DIR",
481
- "/Applications/BambuStudio.app/Contents/Resources/cert");
534
+ const std::string configDir = defaultConfigDir();
535
+ const std::string certDir = defaultCertDir();
482
536
  const std::string certFile = envOr("BAMBU_NATIVE_CERT_FILE", "slicer_base64.cer");
483
537
  const std::string country = envOr("BAMBU_NATIVE_COUNTRY", "US");
484
538
 
@@ -653,8 +707,7 @@ void runOperation(NativeApi &api, NativeOperation operation) {
653
707
  }
654
708
  outputLine("native_command result=" + std::to_string(result));
655
709
  destroyAgent();
656
- if (result != 0) std::exit(20);
657
- return;
710
+ exitNow(result == 0 ? 0 : 20);
658
711
  }
659
712
 
660
713
  std::this_thread::sleep_for(std::chrono::seconds(3));
@@ -729,35 +782,56 @@ void runOperation(NativeApi &api, NativeOperation operation) {
729
782
  // crash after acceptance; let the OS reclaim it without changing that result.
730
783
  // outputLine flushes the receipt before exiting. Upload/control paths retain
731
784
  // their existing cleanup, and no dispatched print is replayed here.
732
- if (!uploadOnly) std::_Exit(result == 0 ? 0 : 20);
785
+ if (!uploadOnly) exitNow(result == 0 ? 0 : 20);
733
786
  destroyAgent();
734
- if (result != 0) std::exit(20);
787
+ exitNow(result == 0 ? 0 : 20);
735
788
  }
736
789
 
737
790
  } // namespace
738
791
 
792
+ // Load the plug-in and resolve every required symbol without any network
793
+ // access. On Linux also load the file-tunnel library the plug-in opens from its
794
+ // own directory, so missing system libraries surface at setup time.
795
+ void runPluginCheck(const NativeApi &) {
796
+ #if !defined(__APPLE__)
797
+ const std::string plugin = envOr("BAMBU_NATIVE_PLUGIN", defaultPluginPath());
798
+ // Resolve beside the plug-in, never through the system library path.
799
+ const std::string::size_type slash = plugin.find_last_of('/');
800
+ const std::string source = (slash == std::string::npos ? std::string("./") : plugin.substr(0, slash + 1)) + "libBambuSource.so";
801
+ void *handle = dlopen(source.c_str(), RTLD_NOW | RTLD_LOCAL);
802
+ if (!handle) {
803
+ const char *error = dlerror();
804
+ throw std::runtime_error("could not load Bambu tunnel library: " + source + (error ? std::string(": ") + error : std::string()));
805
+ }
806
+ dlclose(handle);
807
+ #endif
808
+ outputLine("plugin=ok");
809
+ }
810
+
739
811
  int main(int argc, char **argv) {
740
812
  try {
813
+ const bool pluginCheck = argc == 2 && std::strcmp(argv[1], "--check-plugin") == 0;
741
814
  const bool probe = argc == 2 && std::strcmp(argv[1], "--probe") == 0;
742
815
  const bool mqttProbe = argc == 2 && std::strcmp(argv[1], "--probe-mqtt") == 0;
743
816
  const bool print = argc == 2 && std::strcmp(argv[1], "--print-authorized") == 0;
744
817
  const bool upload = argc == 2 && std::strcmp(argv[1], "--upload") == 0;
745
818
  const bool command = argc == 2 && std::strcmp(argv[1], "--command") == 0;
746
819
  const bool authorizedCommand = argc == 2 && std::strcmp(argv[1], "--command-authorized") == 0;
747
- if (!probe && !mqttProbe && !print && !upload && !command && !authorizedCommand) {
748
- std::cerr << "usage: bambu-native-print --probe|--probe-mqtt|--print-authorized|--upload|--command|--command-authorized" << std::endl;
820
+ if (!pluginCheck && !probe && !mqttProbe && !print && !upload && !command && !authorizedCommand) {
821
+ std::cerr << "usage: bambu-native-print --check-plugin|--probe|--probe-mqtt|--print-authorized|--upload|--command|--command-authorized" << std::endl;
749
822
  return 2;
750
823
  }
751
824
  NativeApi api = loadApi();
825
+ if (pluginCheck) runPluginCheck(api);
752
826
  if (probe) runProbe(api);
753
827
  if (mqttProbe) runMqttProbe(api);
754
828
  if (print) runOperation(api, NativeOperation::Print);
755
829
  if (upload) runOperation(api, NativeOperation::Upload);
756
830
  if (command) runOperation(api, NativeOperation::Command);
757
831
  if (authorizedCommand) runOperation(api, NativeOperation::AuthorizedCommand);
758
- return 0;
832
+ exitNow(0);
759
833
  } catch (const std::exception &error) {
760
834
  std::cerr << "native_error=" << error.what() << std::endl;
761
- return 1;
835
+ exitNow(1);
762
836
  }
763
837
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bambu-printer-mcp",
3
- "version": "1.1.26",
3
+ "version": "1.1.28",
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": {
@@ -17,10 +17,11 @@
17
17
  "package:mcpb": "npm run build && node scripts/package-mcpb.mjs",
18
18
  "version": "node scripts/sync-manifest-version.mjs",
19
19
  "postinstall": "node scripts/install-patches.mjs",
20
- "build:native": "scripts/build-bambu-native.zsh"
20
+ "build:native": "sh scripts/build-bambu-native.sh"
21
21
  },
22
22
  "bin": {
23
- "bambu-printer-mcp": "dist/index.js"
23
+ "bambu-printer-mcp": "dist/index.js",
24
+ "bambu-printer-mcp-native-setup": "scripts/setup-bambu-native.mjs"
24
25
  },
25
26
  "files": [
26
27
  "dist",
@@ -32,7 +33,8 @@
32
33
  "LICENSE",
33
34
  "native/*.cpp",
34
35
  "native/*.h",
35
- "scripts/build-bambu-native.zsh"
36
+ "scripts/build-bambu-native.sh",
37
+ "scripts/setup-bambu-native.mjs"
36
38
  ],
37
39
  "keywords": [
38
40
  "mcp",
@@ -49,7 +51,7 @@
49
51
  "@modelcontextprotocol/sdk": "^1.32.1",
50
52
  "bambu-js": "^2.2.2",
51
53
  "bambu-node": "3.22.21",
52
- "basic-ftp": "^6.2.2",
54
+ "basic-ftp": "^6.2.3",
53
55
  "dotenv": "^16.3.1",
54
56
  "jszip": "^3.10.2",
55
57
  "mqtt": "^5.16.0",
@@ -0,0 +1,53 @@
1
+ #!/bin/sh
2
+
3
+ # Build the optional adapter that calls Bambu's networking plug-in at runtime.
4
+ # The plug-in itself is not bundled here. Supported hosts: macOS and Linux.
5
+ # Usage: build-bambu-native.sh [--output PATH]
6
+ set -eu
7
+
8
+ case "$(uname -s)" in
9
+ Darwin) default_cxx=clang++ ;;
10
+ Linux) default_cxx=c++ ;;
11
+ *)
12
+ echo "The Bambu native helper supports macOS and Linux only." >&2
13
+ exit 1
14
+ ;;
15
+ esac
16
+
17
+ script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
18
+ project_root=$(dirname -- "$script_dir")
19
+ source_file="$project_root/native/bambu-native-print.cpp"
20
+ output="$project_root/native/bambu-native-print"
21
+
22
+ while [ "$#" -gt 0 ]; do
23
+ case "$1" in
24
+ --output)
25
+ [ "$#" -ge 2 ] && [ -n "$2" ] || { echo "--output requires a path" >&2; exit 2; }
26
+ output=$2
27
+ shift 2
28
+ ;;
29
+ *)
30
+ echo "usage: $0 [--output PATH]" >&2
31
+ exit 2
32
+ ;;
33
+ esac
34
+ done
35
+
36
+ cxx_bin=${CXX:-$default_cxx}
37
+ if ! command -v "$cxx_bin" >/dev/null 2>&1; then
38
+ echo "C++ compiler not found: $cxx_bin (install clang++ or g++, or set CXX)" >&2
39
+ exit 1
40
+ fi
41
+
42
+ mkdir -p "$(dirname -- "$output")"
43
+ # Compile beside the target and rename so a running server never executes a
44
+ # partially written helper.
45
+ temporary="$output.tmp.$$"
46
+ trap 'rm -f "$temporary"' EXIT
47
+ # dash skips EXIT traps on signals; clean up a killed build explicitly.
48
+ trap 'rm -f "$temporary"; exit 1' HUP INT TERM
49
+ "$cxx_bin" -std=c++17 -O2 -Wall -Wextra -pthread -o "$temporary" "$source_file" -ldl
50
+ chmod 755 "$temporary"
51
+ mv -f "$temporary" "$output"
52
+ trap - EXIT HUP INT TERM
53
+ echo "Built $output"