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 +1 -1
- package/README.md +7 -7
- package/dist/index.js +1 -1
- package/dist/slicer/profile-flatten.d.ts +5 -1
- package/dist/slicer/profile-flatten.js +31 -12
- package/dist/stl/stl-manipulator.js +36 -15
- package/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/slicer/profile-flatten.ts +37 -12
- package/src/stl/stl-manipulator.ts +42 -18
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.
|
|
47
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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,
|
|
524
|
-
: flattenByName(opts.machineLeaf,
|
|
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,
|
|
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
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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
|
|
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')
|
|
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 (
|
|
216
|
-
throw new Error(`Printer profile "${printerPreset}" was not found. Set BAMBU_PROFILES_ROOT to the matching
|
|
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
|
-
|
|
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,
|
|
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(
|
|
345
|
-
const profileRoots = [path.join(profilesRoot, 'BBL')
|
|
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
|
|
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 =
|
|
1228
|
-
|
|
1229
|
-
|
|
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
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
|
|
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
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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,
|
|
655
|
-
: flattenByName(opts.machineLeaf,
|
|
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,
|
|
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
|
-
|
|
776
|
-
|
|
777
|
-
|
|
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
|
|
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')
|
|
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 (
|
|
342
|
-
throw new Error(`Printer profile "${printerPreset}" was not found. Set BAMBU_PROFILES_ROOT to the matching
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
498
|
-
const profileRoots = [path.join(profilesRoot, 'BBL')
|
|
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
|
|
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
|
-
|
|
1617
|
+
profilesRoot
|
|
1601
1618
|
);
|
|
1602
1619
|
// Resolve every BBL dependency before launching the slicer.
|
|
1603
|
-
const settingsBundle =
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
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,
|