bambu-printer-mcp 1.1.10 → 1.1.11

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/CONTRIBUTORS.md CHANGED
@@ -19,7 +19,7 @@ Thank you to everyone who builds, tests, reports problems, and shares real print
19
19
 
20
20
  Thank you also to the existing contributors whose work the project builds on:
21
21
 
22
- - [David Montgomery (DMontgomery40)](https://github.com/DMontgomery40) — project maintainer.
22
+ - [David Montgomery (DMontgomery40)](https://github.com/DMontgomery40) — project maintainer; flags the FULU/Orca missing-machine-preset safety gap during article review, prompting the 1.1.11 fix.
23
23
  - [rowbotik](https://github.com/rowbotik) — printer, AMS, slicing, and operational work across the existing release history.
24
24
  - [len-foss](https://github.com/len-foss) — project code contribution.
25
25
  - [thebitrock](https://github.com/thebitrock) — project code contribution.
package/README.md CHANGED
@@ -43,8 +43,8 @@ or tokens in chat. Never guess the printer model.
43
43
 
44
44
  Use direct LAN printing by default. Explain any LAN/Developer Mode setting
45
45
  I need to enable. Prefer FULU OrcaSlicer-bambulab GUI slicing/export; discover
46
- an existing slicer before suggesting an install. Do not configure FULU/Orca
47
- CLI auto-slicing while its machine-preset safety gate is missing (see guide).
46
+ an existing slicer before suggesting an install. For FULU/Orca CLI auto-slicing,
47
+ require MCP 1.1.11+ and its matching installed profile tree (see guide).
48
48
  A slicer is not needed here to print a pre-sliced file. Configure the optional FULU
49
49
  BambuNetwork bridge only if I choose it, and explain its runtime/auth needs.
50
50
  X2D supports status and slicing here, but direct printing is not supported.
@@ -137,7 +137,7 @@ See the [changelog](https://github.com/DMontgomery40/bambu-printer-mcp/blob/main
137
137
 
138
138
  ## FULU and open-source printing
139
139
 
140
- [FULU OrcaSlicer-bambulab](https://github.com/FULU-Foundation/OrcaSlicer-bambulab) is a supported slicer target (`SLICER_TYPE=orcaslicer-bambulab`; aliases include `fulu-orca` and `orca-studio`). Use its GUI to export a sliced project for direct LAN printing. FULU/Orca CLI aliases are recognized, but **do not use them for unattended slicing or auto-slicing** while the [machine-preset safety gate is missing](https://github.com/DMontgomery40/bambu-printer-mcp/blob/main/docs/FULU.md#fulu-and-orca-cli-safety-limit).
140
+ [FULU OrcaSlicer-bambulab](https://github.com/FULU-Foundation/OrcaSlicer-bambulab) is a supported slicer target (`SLICER_TYPE=orcaslicer-bambulab`; aliases include `fulu-orca` and `orca-studio`). Use its GUI to export a sliced project for direct LAN printing, or configure its CLI with matching installed profiles. From 1.1.11, FULU/Orca share the [machine-preset gate and profile safety checks](https://github.com/DMontgomery40/bambu-printer-mcp/blob/main/docs/FULU.md#fulu-and-orca-cli-safety-limit).
141
141
 
142
142
  The optional FULU **BambuNetwork bridge** exposes `bambu_network_bridge_status`, `bambu_network_call`, and `print_3mf_bambu_network`, also reachable through `print_3mf` with `connection_mode: "bambu_network"`. Slicer selection does not enable the bridge. It needs a separately installed FULU runtime and an explicit launch command; cloud printing also needs an authenticated BambuNetwork session.
143
143
 
@@ -169,7 +169,7 @@ The optional FULU **BambuNetwork bridge** exposes `bambu_network_bridge_status`,
169
169
  - Start G-code files already stored on the printer
170
170
  - **Collar charm print wrapper** (`print_collar_charm`) — specialized two-color workflow with fixed tray policy for inner (black, AMS 1 slot 1) and outer (white, AMS 2 slot 1) charm parts
171
171
  - STL manipulation: scale, rotate, extend base, merge vertices, center at origin, lay flat, and inspect model info
172
- - Slice STL or 3MF files using an external CLI. For Bambu headless workflows, use the validated BambuStudio profile path; FULU/Orca users should [GUI-export while the CLI safety limitation remains](https://github.com/DMontgomery40/bambu-printer-mcp/blob/main/docs/FULU.md#fulu-and-orca-cli-safety-limit).
172
+ - Slice STL or 3MF files using an external CLI. BambuStudio, FULU, and Orca require the exact machine preset and resolve BBL dependencies before slicing; see [FULU/Orca CLI setup and validation limits](https://github.com/DMontgomery40/bambu-printer-mcp/blob/main/docs/FULU.md#fulu-and-orca-cli-safety-limit).
173
173
  - Inspect slicer settings from a saved 3MF template or extracted profile via `get_slice_settings`
174
174
  - Enumerate saved slicing templates from the local registry via `list_templates`
175
175
  - Save templates into the local registry via `save_template`
@@ -856,7 +856,7 @@ The primary tool for starting a Bambu print. **Recommended input: a pre-sliced `
856
856
  }
857
857
  ```
858
858
 
859
- `bambu_model` is **required** for model-specific routing and preset selection. It does not by itself validate the G-code or guarantee matching output from the FULU/Orca CLI; use GUI-exported sliced projects for those slicers while the machine-preset gate is missing. Using the wrong model can damage hardware. If `bambu_model` is not provided in the tool call and `BAMBU_MODEL` is not set in the environment, the server will ask you interactively via MCP elicitation (if your client supports it) or return a clear error.
859
+ `bambu_model` is **required** for model-specific routing and preset selection. It does not by itself validate pre-sliced G-code. BambuStudio, FULU, and Orca CLI preparation additionally require the exact model/nozzle machine preset and reject incomplete profiles. Using the wrong model can damage hardware. If `bambu_model` is not provided in the tool call and `BAMBU_MODEL` is not set in the environment, the server will ask you interactively via MCP elicitation (if your client supports it) or return a clear error.
860
860
 
861
861
  `bed_type` defaults to `textured_plate` if omitted. `ams_slots` is the preferred override input; `ams_mapping` remains the raw escape hatch. On AMS-equipped H2 printers, `use_ams: false` does not suppress mapping lookup if the sliced file declares filaments. If no mapping is provided for an H2 pre-sliced job with declared filaments, the server fails before sending; pass explicit `ams_slots`, raw `ams_mapping`, or `auto_match_ams: true`.
862
862
 
@@ -1015,7 +1015,7 @@ Slice an STL or 3MF file using an external slicer and return the path to the out
1015
1015
 
1016
1016
  `slicer_type` options: `bambustudio`, `orcaslicer`, `orcaslicer-bambulab` (FULU), `prusaslicer`, `cura`, `slic3r`. Aliases include `fulu-orca` and `orca-studio`. When omitted, the value from the `SLICER_TYPE` environment variable is used (default: `bambustudio`).
1017
1017
 
1018
- **FULU/Orca CLI limitation:** recognized slicer names do not guarantee that a missing machine preset is rejected. Use GUI-exported sliced projects instead of those CLI backends until the [machine-preset gate is implemented](https://github.com/DMontgomery40/bambu-printer-mcp/blob/main/docs/FULU.md#fulu-and-orca-cli-safety-limit).
1018
+ **FULU/Orca CLI safety:** from 1.1.11, these backends require the exact model/nozzle preset from the selected installation and stop on profile preparation failures. A process override cannot bypass that gate. See [configuration and validation limits](https://github.com/DMontgomery40/bambu-printer-mcp/blob/main/docs/FULU.md#fulu-and-orca-cli-safety-limit).
1019
1019
 
1020
1020
  `slicer_path` and `slicer_profile` fall back to the `SLICER_PATH` and `SLICER_PROFILE` environment variables when omitted. Per-call `slicer_path` overrides require `MCP_ALLOW_EXECUTABLE_ARG=1`.
1021
1021
 
@@ -1228,7 +1228,7 @@ After connecting the MCP server in Claude Desktop or Claude Code, you can ask Cl
1228
1228
 
1229
1229
  Understanding these constraints will help you avoid frustrating errors and set appropriate expectations.
1230
1230
 
1231
- 1. **Printable 3MF required for print_3mf.** The `print_3mf` tool expects a sliced 3MF containing at least one `Metadata/plate_<n>.gcode` entry. If you pass an unsliced 3MF (one exported from a CAD tool without slicing), the server attempts auto-slicing. Use the validated BambuStudio CLI path for that; FULU/Orca users must supply GUI-exported sliced files while their CLI preset gate is missing — any inspection or slicing failure stops before upload. For a previewable workflow, pre-slice in FULU OrcaSlicer-bambulab, OrcaSlicer, or Bambu Studio and pass the resulting `.gcode.3mf`. See [slicing guide](https://github.com/DMontgomery40/bambu-printer-mcp/blob/main/docs/SLICING.md) for the full procedure.
1231
+ 1. **Printable 3MF required for print_3mf.** The `print_3mf` tool expects a sliced 3MF containing at least one `Metadata/plate_<n>.gcode` entry. If you pass an unsliced 3MF (one exported from a CAD tool without slicing), the server attempts auto-slicing. BambuStudio, FULU, and Orca CLI paths require the matching machine preset and complete profile preparation; any inspection or slicing failure stops before upload. For a previewable workflow, pre-slice in FULU OrcaSlicer-bambulab, OrcaSlicer, or Bambu Studio and pass the resulting `.gcode.3mf`. See [slicing guide](https://github.com/DMontgomery40/bambu-printer-mcp/blob/main/docs/SLICING.md) for the full procedure.
1232
1232
 
1233
1233
  2. **Layer height, temperature, and slicer settings are baked in.** The `project_file` MQTT command tells the printer which plate to run. It does not support overriding layer height, temperature targets, infill percentage, or other slicing parameters at print time. These must be set in your slicer before generating the 3MF.
1234
1234
 
package/dist/index.js CHANGED
@@ -1599,7 +1599,7 @@ class BambuPrinterMCPServer {
1599
1599
  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."
1600
1600
  },
1601
1601
  slicer_path: { type: "string", description: "Path to the slicer executable (default: value from env). Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1." },
1602
- slicer_profile: { type: "string", description: "Path to the slicer profile/config file (optional, overrides bambu_model preset)" },
1602
+ slicer_profile: { type: "string", description: "Path to an optional process profile/config file. The exact bambu_model/nozzle machine preset is still required." },
1603
1603
  template_3mf_path: { type: "string", description: "Optional template 3MF whose embedded Bambu slicer settings should be reused when slicing a new STL or 3MF." },
1604
1604
  template_name: { type: "string", description: "Optional named template from the local registry. Resolves to template_3mf_path automatically." },
1605
1605
  template_dir: { type: "string", description: "Optional template directory override when resolving template_name." },
@@ -49,6 +49,8 @@ export interface FlattenOptions {
49
49
  filamentLeaves: string[];
50
50
  /** Absolute path to `.../Resources/profiles`. */
51
51
  profilesRoot: string;
52
+ /** Configured BBL directories for custom process/filament dependencies only. */
53
+ userProfileRoots?: string[];
52
54
  /** Where to write flattened temp files. */
53
55
  tempDir: string;
54
56
  /** Vendor subdir under profilesRoot. Currently only "BBL" supported. */
@@ -82,6 +84,8 @@ export interface FlattenOptions {
82
84
  wipe_tower_y?: unknown;
83
85
  };
84
86
  }
87
+ /** Resolve bundled machine defaults before choosing process and filament leaves. */
88
+ export declare function resolveBblMachineProfile(profilesRoot: string, machineLeaf: string): Promise<Record<string, unknown>>;
85
89
  /** Best-effort extruder count for fallback nozzle_volume_type sizing. */
86
90
  /** BambuStudio's built-in filament_colour default. */
87
91
  export declare const DEFAULT_FILAMENT_COLOUR = "#00AE42";
@@ -97,4 +101,4 @@ export declare function flattenForCli(opts: FlattenOptions): Promise<FlattenedPr
97
101
  *
98
102
  * Override via BAMBU_PROFILES_ROOT env.
99
103
  */
100
- export declare function detectProfilesRoot(slicerPath?: string): string;
104
+ export declare function detectProfilesRoot(slicerPath?: string, slicerType?: string): string;
@@ -39,11 +39,14 @@ import crypto from "node:crypto";
39
39
  * abstract bases like `fdm_machine_common` and `fdm_bbl_3dp_001_common`,
40
40
  * because those are the parents we'll walk to.
41
41
  */
42
- async function buildNameIndex(profilesRoot, vendor) {
42
+ async function buildNameIndex(profilesRoot, vendor, userProfileRoots = []) {
43
43
  const index = new Map();
44
44
  const subdirs = ["machine", "process", "filament"];
45
- for (const sub of subdirs) {
46
- const dir = path.join(profilesRoot, vendor, sub);
45
+ const directories = [
46
+ ...subdirs.map(sub => path.join(profilesRoot, vendor, sub)),
47
+ ...userProfileRoots.flatMap(root => ['process', 'filament'].map(sub => path.join(root, sub))),
48
+ ];
49
+ for (const dir of directories) {
47
50
  let entries;
48
51
  try {
49
52
  entries = await fs.readdir(dir);
@@ -72,6 +75,8 @@ async function buildNameIndex(profilesRoot, vendor) {
72
75
  // Malformed profile -- skip, don't poison the index.
73
76
  continue;
74
77
  }
78
+ if (!data || typeof data !== 'object' || Array.isArray(data))
79
+ continue;
75
80
  const name = data["name"];
76
81
  if (typeof name !== "string" || name.length === 0)
77
82
  continue;
@@ -123,6 +128,10 @@ function flattenData(data, index, visiting = new Set()) {
123
128
  delete merged["include"];
124
129
  return merged;
125
130
  }
131
+ /** Resolve bundled machine defaults before choosing process and filament leaves. */
132
+ export async function resolveBblMachineProfile(profilesRoot, machineLeaf) {
133
+ return flattenByName(machineLeaf, await buildNameIndex(profilesRoot, 'BBL'));
134
+ }
126
135
  /** Keys of an include template that describe the template, not the config. */
127
136
  const INCLUDE_METADATA_KEYS = new Set(["name", "type", "from", "instantiation", "inherits", "include", "setting_id"]);
128
137
  /**
@@ -514,14 +523,18 @@ export async function flattenForCli(opts) {
514
523
  throw new Error(`profile-flatten: profilesRoot "${opts.profilesRoot}" does not contain "${vendor}/machine". ` +
515
524
  `Set BAMBU_PROFILES_ROOT or check your BambuStudio install.`);
516
525
  }
517
- const index = await buildNameIndex(opts.profilesRoot, vendor);
526
+ // Machine inheritance must never resolve through user process/filament files.
527
+ const machineIndex = await buildNameIndex(opts.profilesRoot, vendor);
528
+ const index = opts.userProfileRoots?.length
529
+ ? await buildNameIndex(opts.profilesRoot, vendor, opts.userProfileRoots)
530
+ : machineIndex;
518
531
  // Flatten each leaf.
519
532
  if (opts.sourceProfiles?.filaments && opts.sourceProfiles.filaments.length !== opts.filamentLeaves.length) {
520
533
  throw new Error("Every filament slot must have a source profile.");
521
534
  }
522
535
  const machineFlat = opts.sourceProfiles?.machine
523
- ? flattenData(opts.sourceProfiles.machine, index)
524
- : flattenByName(opts.machineLeaf, index);
536
+ ? flattenData(opts.sourceProfiles.machine, machineIndex)
537
+ : flattenByName(opts.machineLeaf, machineIndex);
525
538
  const processFlat = opts.sourceProfiles?.process
526
539
  ? flattenData(opts.sourceProfiles.process, index)
527
540
  : flattenByName(opts.processLeaf, index);
@@ -531,7 +544,7 @@ export async function flattenForCli(opts) {
531
544
  });
532
545
  // CLI-specific post-processing on machine profile only.
533
546
  deriveNozzleVolumeType(machineFlat, opts.nozzleVolumeType);
534
- applyMachineModelBedMetadata(machineFlat, index);
547
+ applyMachineModelBedMetadata(machineFlat, machineIndex);
535
548
  // A standalone custom machine can accompany process/filament profiles that
536
549
  // need BBL resolution; do not impose bundled model config on that machine.
537
550
  const sourceMachine = opts.sourceProfiles?.machine;
@@ -603,7 +616,7 @@ async function writeTemp(tempDir, kind, leafName, data) {
603
616
  *
604
617
  * Override via BAMBU_PROFILES_ROOT env.
605
618
  */
606
- export function detectProfilesRoot(slicerPath) {
619
+ export function detectProfilesRoot(slicerPath, slicerType = 'bambustudio') {
607
620
  if (process.env["BAMBU_PROFILES_ROOT"]) {
608
621
  return process.env["BAMBU_PROFILES_ROOT"];
609
622
  }
@@ -622,14 +635,18 @@ export function detectProfilesRoot(slicerPath) {
622
635
  }
623
636
  catch { /* Probe the supplied path below. */ }
624
637
  const bin = path.dirname(executable);
638
+ const installNames = slicerType === 'bambustudio'
639
+ ? ['BambuStudio', 'bambu-studio']
640
+ : ['OrcaSlicer', 'orca-slicer', 'OrcaStudio', 'orca-studio'];
625
641
  const candidates = path.basename(bin) === 'MacOS'
626
642
  ? [path.resolve(bin, '..', 'Resources', 'profiles')]
627
643
  : [
628
644
  path.join(bin, 'resources', 'profiles'),
629
645
  path.join(bin, 'Resources', 'profiles'),
630
- path.resolve(bin, '..', 'share', 'BambuStudio', 'profiles'),
631
- path.resolve(bin, '..', 'share', 'bambu-studio', 'profiles'),
632
- path.resolve(bin, '..', 'share', 'BambuStudio', 'resources', 'profiles'),
646
+ ...installNames.flatMap(name => [
647
+ path.resolve(bin, '..', 'share', name, 'profiles'),
648
+ path.resolve(bin, '..', 'share', name, 'resources', 'profiles'),
649
+ ]),
633
650
  ];
634
651
  const found = candidates.find(root => existsSync(path.join(root, 'BBL', 'machine')));
635
652
  // Do not select another installation if this executable has no profile tree.
@@ -637,5 +654,7 @@ export function detectProfilesRoot(slicerPath) {
637
654
  return found ?? candidates[0];
638
655
  }
639
656
  // Default macOS install.
640
- return "/Applications/BambuStudio.app/Contents/Resources/profiles";
657
+ return slicerType === 'bambustudio'
658
+ ? '/Applications/BambuStudio.app/Contents/Resources/profiles'
659
+ : '/Applications/OrcaSlicer.app/Contents/Resources/profiles';
641
660
  }
@@ -9,7 +9,7 @@ import { promisify } from 'util';
9
9
  import { EventEmitter } from 'events';
10
10
  import * as crypto from 'crypto';
11
11
  import { execFile } from 'child_process';
12
- import { flattenForCli, detectProfilesRoot } from '../slicer/profile-flatten.js';
12
+ import { flattenForCli, detectProfilesRoot, resolveBblMachineProfile } from '../slicer/profile-flatten.js';
13
13
  const readFileAsync = promisify(fs.readFile);
14
14
  const writeFileAsync = promisify(fs.writeFile);
15
15
  const BAMBU_PROFILE_ROOTS = [
@@ -206,16 +206,31 @@ export class STLManipulator extends EventEmitter {
206
206
  fs.writeFileSync(outPath, serialized);
207
207
  return outPath;
208
208
  }
209
- resolveBambuLikeSettingsBundle(outputBase, slicerType, slicerProfile, printerPreset, bambuOptions, activeProfilesRoot) {
209
+ async resolveBambuLikeSettingsBundle(outputBase, slicerType, slicerProfile, printerPreset, bambuOptions, activeProfilesRoot) {
210
210
  const roots = activeProfilesRoot
211
- ? [path.join(activeProfilesRoot, 'BBL'), ...this.getAvailableProfileRoots()]
211
+ ? [path.join(activeProfilesRoot, 'BBL')]
212
212
  : this.getAvailableProfileRoots();
213
- const findProfile = (kind, name) => this.findProfileFile(kind, name, roots);
213
+ const findProfile = (kind, name) => this.findProfileFile(kind, name, kind === 'machine' ? roots : [...roots, ...configuredBambuProfileDirs()]);
214
+ if (!printerPreset) {
215
+ throw new Error('Printer preset is required for Bambu-compatible CLI slicing; select the exact bambu_model and nozzle_diameter.');
216
+ }
214
217
  const machinePath = findProfile('machine', printerPreset);
215
- if (slicerType === 'bambustudio' && printerPreset && !machinePath) {
216
- throw new Error(`Printer profile "${printerPreset}" was not found. Set BAMBU_PROFILES_ROOT to the matching BambuStudio profile tree; refusing to use printer defaults.`);
218
+ if (!machinePath) {
219
+ throw new Error(`Printer profile "${printerPreset}" was not found. Set BAMBU_PROFILES_ROOT to the matching ${slicerType} profile tree; refusing to use printer defaults.`);
220
+ }
221
+ let machineConfig;
222
+ try {
223
+ machineConfig = this.readJsonFile(machinePath);
224
+ if (!machineConfig || Array.isArray(machineConfig) || machineConfig.name !== printerPreset) {
225
+ throw new Error('expected a JSON machine profile with the selected preset name');
226
+ }
217
227
  }
218
- const machineConfig = machinePath ? this.readJsonFile(machinePath) : null;
228
+ catch (error) {
229
+ throw new Error(`Printer profile "${printerPreset}" is invalid: ${error?.message ?? error}`);
230
+ }
231
+ // Some FULU/Orca machine leaves inherit their default process/filaments.
232
+ // Resolve those selections before building the positional CLI bundle.
233
+ machineConfig = await resolveBblMachineProfile(activeProfilesRoot ?? path.dirname(path.dirname(path.dirname(machinePath))), printerPreset);
219
234
  const hasSlicerProfile = !!slicerProfile && fs.existsSync(slicerProfile);
220
235
  const filamentPaths = [];
221
236
  let processPath;
@@ -267,7 +282,7 @@ export class STLManipulator extends EventEmitter {
267
282
  }
268
283
  }
269
284
  if (!bambuOptions?.loadFilaments && filamentPaths.length === 0 && Array.isArray(parsedProfile.filament_ids)) {
270
- const filamentIdIndex = this.buildFilamentIdIndex(roots);
285
+ const filamentIdIndex = this.buildFilamentIdIndex([...roots, ...configuredBambuProfileDirs()]);
271
286
  for (const filamentId of parsedProfile.filament_ids) {
272
287
  const filamentPath = filamentIdIndex.get(String(filamentId));
273
288
  if (filamentPath) {
@@ -324,7 +339,7 @@ export class STLManipulator extends EventEmitter {
324
339
  };
325
340
  }
326
341
  /** Resolve BBL dependencies before invoking the CLI; failures stop the slice. */
327
- async maybeFlattenBundle(bundle, bambuOptions, activeSlicerPath) {
342
+ async maybeFlattenBundle(bundle, bambuOptions, activeProfilesRoot) {
328
343
  const parts = bundle.settingsArg?.split(';').filter(Boolean) ?? [];
329
344
  const readProfile = (filePath) => {
330
345
  try {
@@ -341,8 +356,8 @@ export class STLManipulator extends EventEmitter {
341
356
  const settings = parts.map(readProfile);
342
357
  // Read every slot: filtering unreadable names would change positional mapping.
343
358
  const filaments = bundle.filamentPaths.map(readProfile);
344
- const profilesRoot = detectProfilesRoot(activeSlicerPath || process.env.SLICER_PATH);
345
- const profileRoots = [path.join(profilesRoot, 'BBL'), ...this.getAvailableProfileRoots()];
359
+ const profilesRoot = activeProfilesRoot ?? detectProfilesRoot(process.env.SLICER_PATH);
360
+ const profileRoots = [path.join(profilesRoot, 'BBL')];
346
361
  const hasReferences = (profile) => (typeof profile.inherits === 'string' && profile.inherits.length > 0) ||
347
362
  (profile.include !== undefined && profile.include !== null);
348
363
  const isBundledFile = (filePath) => profileRoots.some(root => {
@@ -380,6 +395,7 @@ export class STLManipulator extends EventEmitter {
380
395
  : leafName(processPath, processProfile),
381
396
  filamentLeaves: filaments.map((profile, i) => leafName(bundle.filamentPaths[i], profile)),
382
397
  profilesRoot,
398
+ userProfileRoots: configuredBambuProfileDirs(),
383
399
  tempDir: this.tempDir,
384
400
  bedType: this.resolveBambuStudioBedType(bambuOptions?.bedType),
385
401
  filamentColours: bambuOptions?.filamentColours ?? bundle.filamentColours,
@@ -1222,11 +1238,16 @@ export class STLManipulator extends EventEmitter {
1222
1238
  const outputBase = path.basename(stlFilePath, is3mf ? '.3mf' : '.stl');
1223
1239
  const bambuOutputPath = path.join(this.tempDir, outputBase + '_sliced.3mf');
1224
1240
  const outputDir = path.dirname(bambuOutputPath);
1225
- const rawBundle = this.resolveBambuLikeSettingsBundle(outputBase, slicerType, slicerProfile, printerPreset, bambuOptions, slicerType === 'bambustudio' ? detectProfilesRoot(slicerPath) : undefined);
1241
+ const profilesRoot = detectProfilesRoot(slicerPath, slicerType);
1242
+ const rawBundle = await this.resolveBambuLikeSettingsBundle(outputBase, slicerType, slicerProfile, printerPreset, bambuOptions, profilesRoot);
1226
1243
  // Resolve every BBL dependency before launching the slicer.
1227
- const settingsBundle = slicerType === 'bambustudio'
1228
- ? await this.maybeFlattenBundle(await this.expandProjectFilaments(stlFilePath, rawBundle), bambuOptions, slicerPath)
1229
- : rawBundle;
1244
+ const settingsBundle = await this.maybeFlattenBundle(await this.expandProjectFilaments(stlFilePath, rawBundle), bambuOptions, profilesRoot);
1245
+ // Inherited process G-code is now present; apply Orca's existing
1246
+ // absolute-extrusion normalization after resolving those ancestors.
1247
+ if (slicerType === 'orcaslicer' && settingsBundle.settingsArg) {
1248
+ const [machinePath, processPath] = settingsBundle.settingsArg.split(';');
1249
+ settingsBundle.settingsArg = [machinePath, this.writeTempJson(outputBase, 'process_orca_resolved', this.sanitizeProcessForOrca(this.readJsonFile(processPath), printerPreset))].join(';');
1250
+ }
1230
1251
  args = [
1231
1252
  '--slice', String(bambuOptions?.slicePlate ?? 0),
1232
1253
  '--outputdir', outputDir,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bambu-printer-mcp",
3
- "version": "1.1.10",
3
+ "version": "1.1.11",
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": {
package/src/index.ts CHANGED
@@ -2095,7 +2095,7 @@ class BambuPrinterMCPServer {
2095
2095
  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."
2096
2096
  },
2097
2097
  slicer_path: { type: "string", description: "Path to the slicer executable (default: value from env). Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1." },
2098
- slicer_profile: { type: "string", description: "Path to the slicer profile/config file (optional, overrides bambu_model preset)" },
2098
+ slicer_profile: { type: "string", description: "Path to an optional process profile/config file. The exact bambu_model/nozzle machine preset is still required." },
2099
2099
  template_3mf_path: { type: "string", description: "Optional template 3MF whose embedded Bambu slicer settings should be reused when slicing a new STL or 3MF." },
2100
2100
  template_name: { type: "string", description: "Optional named template from the local registry. Resolves to template_3mf_path automatically." },
2101
2101
  template_dir: { type: "string", description: "Optional template directory override when resolving template_name." },
@@ -57,6 +57,8 @@ export interface FlattenOptions {
57
57
  filamentLeaves: string[];
58
58
  /** Absolute path to `.../Resources/profiles`. */
59
59
  profilesRoot: string;
60
+ /** Configured BBL directories for custom process/filament dependencies only. */
61
+ userProfileRoots?: string[];
60
62
  /** Where to write flattened temp files. */
61
63
  tempDir: string;
62
64
  /** Vendor subdir under profilesRoot. Currently only "BBL" supported. */
@@ -110,13 +112,17 @@ type NameIndex = Map<string, IndexedProfile>;
110
112
  */
111
113
  async function buildNameIndex(
112
114
  profilesRoot: string,
113
- vendor: string
115
+ vendor: string,
116
+ userProfileRoots: string[] = []
114
117
  ): Promise<NameIndex> {
115
118
  const index: NameIndex = new Map();
116
119
  const subdirs: ProfileKind[] = ["machine", "process", "filament"];
117
120
 
118
- for (const sub of subdirs) {
119
- const dir = path.join(profilesRoot, vendor, sub);
121
+ const directories = [
122
+ ...subdirs.map(sub => path.join(profilesRoot, vendor, sub)),
123
+ ...userProfileRoots.flatMap(root => ['process', 'filament'].map(sub => path.join(root, sub))),
124
+ ];
125
+ for (const dir of directories) {
120
126
  let entries: string[];
121
127
  try {
122
128
  entries = await fs.readdir(dir);
@@ -141,6 +147,7 @@ async function buildNameIndex(
141
147
  // Malformed profile -- skip, don't poison the index.
142
148
  continue;
143
149
  }
150
+ if (!data || typeof data !== 'object' || Array.isArray(data)) continue;
144
151
  const name = data["name"];
145
152
  if (typeof name !== "string" || name.length === 0) continue;
146
153
  // First-write wins. BBL doesn't have name collisions in practice;
@@ -203,6 +210,14 @@ function flattenData(
203
210
  return merged;
204
211
  }
205
212
 
213
+ /** Resolve bundled machine defaults before choosing process and filament leaves. */
214
+ export async function resolveBblMachineProfile(
215
+ profilesRoot: string,
216
+ machineLeaf: string
217
+ ): Promise<Record<string, unknown>> {
218
+ return flattenByName(machineLeaf, await buildNameIndex(profilesRoot, 'BBL'));
219
+ }
220
+
206
221
  /** Keys of an include template that describe the template, not the config. */
207
222
  const INCLUDE_METADATA_KEYS = new Set(["name", "type", "from", "instantiation", "inherits", "include", "setting_id"]);
208
223
 
@@ -644,15 +659,19 @@ export async function flattenForCli(opts: FlattenOptions): Promise<FlattenedProf
644
659
  );
645
660
  }
646
661
 
647
- const index = await buildNameIndex(opts.profilesRoot, vendor);
662
+ // Machine inheritance must never resolve through user process/filament files.
663
+ const machineIndex = await buildNameIndex(opts.profilesRoot, vendor);
664
+ const index = opts.userProfileRoots?.length
665
+ ? await buildNameIndex(opts.profilesRoot, vendor, opts.userProfileRoots)
666
+ : machineIndex;
648
667
 
649
668
  // Flatten each leaf.
650
669
  if (opts.sourceProfiles?.filaments && opts.sourceProfiles.filaments.length !== opts.filamentLeaves.length) {
651
670
  throw new Error("Every filament slot must have a source profile.");
652
671
  }
653
672
  const machineFlat = opts.sourceProfiles?.machine
654
- ? flattenData(opts.sourceProfiles.machine, index)
655
- : flattenByName(opts.machineLeaf, index);
673
+ ? flattenData(opts.sourceProfiles.machine, machineIndex)
674
+ : flattenByName(opts.machineLeaf, machineIndex);
656
675
  const processFlat = opts.sourceProfiles?.process
657
676
  ? flattenData(opts.sourceProfiles.process, index)
658
677
  : flattenByName(opts.processLeaf, index);
@@ -663,7 +682,7 @@ export async function flattenForCli(opts: FlattenOptions): Promise<FlattenedProf
663
682
 
664
683
  // CLI-specific post-processing on machine profile only.
665
684
  deriveNozzleVolumeType(machineFlat, opts.nozzleVolumeType);
666
- applyMachineModelBedMetadata(machineFlat, index);
685
+ applyMachineModelBedMetadata(machineFlat, machineIndex);
667
686
  // A standalone custom machine can accompany process/filament profiles that
668
687
  // need BBL resolution; do not impose bundled model config on that machine.
669
688
  const sourceMachine = opts.sourceProfiles?.machine;
@@ -751,7 +770,7 @@ async function writeTemp(
751
770
  *
752
771
  * Override via BAMBU_PROFILES_ROOT env.
753
772
  */
754
- export function detectProfilesRoot(slicerPath?: string): string {
773
+ export function detectProfilesRoot(slicerPath?: string, slicerType = 'bambustudio'): string {
755
774
  if (process.env["BAMBU_PROFILES_ROOT"]) {
756
775
  return process.env["BAMBU_PROFILES_ROOT"];
757
776
  }
@@ -767,14 +786,18 @@ export function detectProfilesRoot(slicerPath?: string): string {
767
786
  // Package-manager launchers may be symlinks into the installation prefix.
768
787
  try { executable = realpathSync(executable); } catch { /* Probe the supplied path below. */ }
769
788
  const bin = path.dirname(executable);
789
+ const installNames = slicerType === 'bambustudio'
790
+ ? ['BambuStudio', 'bambu-studio']
791
+ : ['OrcaSlicer', 'orca-slicer', 'OrcaStudio', 'orca-studio'];
770
792
  const candidates = path.basename(bin) === 'MacOS'
771
793
  ? [path.resolve(bin, '..', 'Resources', 'profiles')]
772
794
  : [
773
795
  path.join(bin, 'resources', 'profiles'),
774
796
  path.join(bin, 'Resources', 'profiles'),
775
- path.resolve(bin, '..', 'share', 'BambuStudio', 'profiles'),
776
- path.resolve(bin, '..', 'share', 'bambu-studio', 'profiles'),
777
- path.resolve(bin, '..', 'share', 'BambuStudio', 'resources', 'profiles'),
797
+ ...installNames.flatMap(name => [
798
+ path.resolve(bin, '..', 'share', name, 'profiles'),
799
+ path.resolve(bin, '..', 'share', name, 'resources', 'profiles'),
800
+ ]),
778
801
  ];
779
802
  const found = candidates.find(root => existsSync(path.join(root, 'BBL', 'machine')));
780
803
  // Do not select another installation if this executable has no profile tree.
@@ -783,5 +806,7 @@ export function detectProfilesRoot(slicerPath?: string): string {
783
806
  }
784
807
 
785
808
  // Default macOS install.
786
- return "/Applications/BambuStudio.app/Contents/Resources/profiles";
809
+ return slicerType === 'bambustudio'
810
+ ? '/Applications/BambuStudio.app/Contents/Resources/profiles'
811
+ : '/Applications/OrcaSlicer.app/Contents/Resources/profiles';
787
812
  }
@@ -9,7 +9,7 @@ import { promisify } from 'util';
9
9
  import { EventEmitter } from 'events';
10
10
  import * as crypto from 'crypto';
11
11
  import { execFile } from 'child_process';
12
- import { flattenForCli, detectProfilesRoot } from '../slicer/profile-flatten.js';
12
+ import { flattenForCli, detectProfilesRoot, resolveBblMachineProfile } from '../slicer/profile-flatten.js';
13
13
 
14
14
  const readFileAsync = promisify(fs.readFile);
15
15
  const writeFileAsync = promisify(fs.writeFile);
@@ -324,24 +324,39 @@ export class STLManipulator extends EventEmitter {
324
324
  return outPath;
325
325
  }
326
326
 
327
- private resolveBambuLikeSettingsBundle(
327
+ private async resolveBambuLikeSettingsBundle(
328
328
  outputBase: string,
329
329
  slicerType: 'orcaslicer' | 'orcaslicer-bambulab' | 'bambustudio',
330
330
  slicerProfile?: string,
331
331
  printerPreset?: string,
332
332
  bambuOptions?: BambuSliceOptions,
333
333
  activeProfilesRoot?: string
334
- ): BambuSettingsBundle {
334
+ ): Promise<BambuSettingsBundle> {
335
335
  const roots = activeProfilesRoot
336
- ? [path.join(activeProfilesRoot, 'BBL'), ...this.getAvailableProfileRoots()]
336
+ ? [path.join(activeProfilesRoot, 'BBL')]
337
337
  : this.getAvailableProfileRoots();
338
338
  const findProfile = (kind: 'machine' | 'process' | 'filament', name?: string) =>
339
- this.findProfileFile(kind, name, roots);
339
+ this.findProfileFile(kind, name, kind === 'machine' ? roots : [...roots, ...configuredBambuProfileDirs()]);
340
+ if (!printerPreset) {
341
+ throw new Error('Printer preset is required for Bambu-compatible CLI slicing; select the exact bambu_model and nozzle_diameter.');
342
+ }
340
343
  const machinePath = findProfile('machine', printerPreset);
341
- if (slicerType === 'bambustudio' && printerPreset && !machinePath) {
342
- throw new Error(`Printer profile "${printerPreset}" was not found. Set BAMBU_PROFILES_ROOT to the matching BambuStudio profile tree; refusing to use printer defaults.`);
344
+ if (!machinePath) {
345
+ throw new Error(`Printer profile "${printerPreset}" was not found. Set BAMBU_PROFILES_ROOT to the matching ${slicerType} profile tree; refusing to use printer defaults.`);
343
346
  }
344
- const machineConfig = machinePath ? this.readJsonFile(machinePath) : null;
347
+ let machineConfig: any;
348
+ try {
349
+ machineConfig = this.readJsonFile(machinePath);
350
+ if (!machineConfig || Array.isArray(machineConfig) || machineConfig.name !== printerPreset) {
351
+ throw new Error('expected a JSON machine profile with the selected preset name');
352
+ }
353
+ } catch (error: any) {
354
+ throw new Error(`Printer profile "${printerPreset}" is invalid: ${error?.message ?? error}`);
355
+ }
356
+ // Some FULU/Orca machine leaves inherit their default process/filaments.
357
+ // Resolve those selections before building the positional CLI bundle.
358
+ machineConfig = await resolveBblMachineProfile(
359
+ activeProfilesRoot ?? path.dirname(path.dirname(path.dirname(machinePath))), printerPreset);
345
360
  const hasSlicerProfile = !!slicerProfile && fs.existsSync(slicerProfile);
346
361
  const filamentPaths: string[] = [];
347
362
 
@@ -405,7 +420,7 @@ export class STLManipulator extends EventEmitter {
405
420
  }
406
421
 
407
422
  if (!bambuOptions?.loadFilaments && filamentPaths.length === 0 && Array.isArray(parsedProfile.filament_ids)) {
408
- const filamentIdIndex = this.buildFilamentIdIndex(roots);
423
+ const filamentIdIndex = this.buildFilamentIdIndex([...roots, ...configuredBambuProfileDirs()]);
409
424
  for (const filamentId of parsedProfile.filament_ids) {
410
425
  const filamentPath = filamentIdIndex.get(String(filamentId));
411
426
  if (filamentPath) {
@@ -477,7 +492,7 @@ export class STLManipulator extends EventEmitter {
477
492
  private async maybeFlattenBundle(
478
493
  bundle: BambuSettingsBundle,
479
494
  bambuOptions?: BambuSliceOptions,
480
- activeSlicerPath?: string
495
+ activeProfilesRoot?: string
481
496
  ): Promise<BambuSettingsBundle> {
482
497
  const parts = bundle.settingsArg?.split(';').filter(Boolean) ?? [];
483
498
  const readProfile = (filePath: string): Record<string, unknown> => {
@@ -494,8 +509,8 @@ export class STLManipulator extends EventEmitter {
494
509
  const settings = parts.map(readProfile);
495
510
  // Read every slot: filtering unreadable names would change positional mapping.
496
511
  const filaments = bundle.filamentPaths.map(readProfile);
497
- const profilesRoot = detectProfilesRoot(activeSlicerPath || process.env.SLICER_PATH);
498
- const profileRoots = [path.join(profilesRoot, 'BBL'), ...this.getAvailableProfileRoots()];
512
+ const profilesRoot = activeProfilesRoot ?? detectProfilesRoot(process.env.SLICER_PATH);
513
+ const profileRoots = [path.join(profilesRoot, 'BBL')];
499
514
  const hasReferences = (profile: Record<string, unknown>) =>
500
515
  (typeof profile.inherits === 'string' && profile.inherits.length > 0) ||
501
516
  (profile.include !== undefined && profile.include !== null);
@@ -537,6 +552,7 @@ export class STLManipulator extends EventEmitter {
537
552
  : leafName(processPath, processProfile),
538
553
  filamentLeaves: filaments.map((profile, i) => leafName(bundle.filamentPaths[i], profile)),
539
554
  profilesRoot,
555
+ userProfileRoots: configuredBambuProfileDirs(),
540
556
  tempDir: this.tempDir,
541
557
  bedType: this.resolveBambuStudioBedType(bambuOptions?.bedType),
542
558
  filamentColours: bambuOptions?.filamentColours ?? bundle.filamentColours,
@@ -1591,19 +1607,27 @@ export class STLManipulator extends EventEmitter {
1591
1607
  const outputBase = path.basename(stlFilePath, is3mf ? '.3mf' : '.stl');
1592
1608
  const bambuOutputPath = path.join(this.tempDir, outputBase + '_sliced.3mf');
1593
1609
  const outputDir = path.dirname(bambuOutputPath);
1594
- const rawBundle = this.resolveBambuLikeSettingsBundle(
1610
+ const profilesRoot = detectProfilesRoot(slicerPath, slicerType);
1611
+ const rawBundle = await this.resolveBambuLikeSettingsBundle(
1595
1612
  outputBase,
1596
1613
  slicerType,
1597
1614
  slicerProfile,
1598
1615
  printerPreset,
1599
1616
  bambuOptions,
1600
- slicerType === 'bambustudio' ? detectProfilesRoot(slicerPath) : undefined
1617
+ profilesRoot
1601
1618
  );
1602
1619
  // Resolve every BBL dependency before launching the slicer.
1603
- const settingsBundle = slicerType === 'bambustudio'
1604
- ? await this.maybeFlattenBundle(
1605
- await this.expandProjectFilaments(stlFilePath, rawBundle), bambuOptions, slicerPath)
1606
- : rawBundle;
1620
+ const settingsBundle = await this.maybeFlattenBundle(
1621
+ await this.expandProjectFilaments(stlFilePath, rawBundle), bambuOptions, profilesRoot);
1622
+ // Inherited process G-code is now present; apply Orca's existing
1623
+ // absolute-extrusion normalization after resolving those ancestors.
1624
+ if (slicerType === 'orcaslicer' && settingsBundle.settingsArg) {
1625
+ const [machinePath, processPath] = settingsBundle.settingsArg.split(';');
1626
+ settingsBundle.settingsArg = [machinePath, this.writeTempJson(
1627
+ outputBase, 'process_orca_resolved',
1628
+ this.sanitizeProcessForOrca(this.readJsonFile(processPath), printerPreset)
1629
+ )].join(';');
1630
+ }
1607
1631
  args = [
1608
1632
  '--slice', String(bambuOptions?.slicePlate ?? 0),
1609
1633
  '--outputdir', outputDir,