bambu-printer-mcp 1.0.8 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +517 -477
- package/dist/3mf_parser.d.ts +15 -1
- package/dist/3mf_parser.js +258 -1
- package/dist/ams-mapping.d.ts +3 -0
- package/dist/ams-mapping.js +45 -0
- package/dist/index.js +1530 -80
- package/dist/printers/bambu.d.ts +133 -0
- package/dist/printers/bambu.js +1005 -53
- package/dist/slicer/profile-flatten.d.ts +75 -0
- package/dist/slicer/profile-flatten.js +446 -0
- package/dist/stl/stl-manipulator.d.ts +31 -3
- package/dist/stl/stl-manipulator.js +300 -52
- package/dist/types.d.ts +35 -0
- package/package.json +8 -3
- package/patches/bambu-node+3.22.21.patch +16 -0
- package/src/3mf_parser.ts +301 -3
- package/src/ams-mapping.ts +47 -0
- package/src/index.ts +1976 -81
- package/src/printers/bambu.ts +1216 -56
- package/src/slicer/profile-flatten.ts +562 -0
- package/src/stl/stl-manipulator.ts +384 -55
- package/src/types.ts +41 -0
package/dist/index.js
CHANGED
|
@@ -10,30 +10,345 @@ import { createServer as createHttpServer } from "node:http";
|
|
|
10
10
|
import { randomUUID } from "node:crypto";
|
|
11
11
|
import { STLManipulator, SLICER_TYPES, normalizeSlicerType, } from "./stl/stl-manipulator.js";
|
|
12
12
|
import { BambuNetworkBridge } from "./bambu-network-bridge.js";
|
|
13
|
-
import {
|
|
13
|
+
import { hasAmsMappingInput, normalizeAmsMappingObject, normalizeBridgeAmsTrayValue } from "./ams-mapping.js";
|
|
14
|
+
import { analyze3MFAmsRequirements, analyze3MFPlateObjects, analyzeCollarCharm3MF, extractBambuTemplateSettings, getCollarCharmRolePolicy, parse3MF } from './3mf_parser.js';
|
|
14
15
|
import { BambuImplementation } from "./printers/bambu.js";
|
|
15
16
|
dotenv.config();
|
|
16
|
-
const DEFAULT_HOST = process.env.PRINTER_HOST || "localhost";
|
|
17
|
-
const DEFAULT_BAMBU_SERIAL = process.env.BAMBU_SERIAL || "";
|
|
18
|
-
const DEFAULT_BAMBU_TOKEN = process.env.BAMBU_TOKEN || "";
|
|
17
|
+
const DEFAULT_HOST = process.env.BAMBU_PRINTER_HOST || process.env.PRINTER_HOST || "localhost";
|
|
18
|
+
const DEFAULT_BAMBU_SERIAL = process.env.BAMBU_PRINTER_SERIAL || process.env.BAMBU_SERIAL || "";
|
|
19
|
+
const DEFAULT_BAMBU_TOKEN = process.env.BAMBU_PRINTER_ACCESS_TOKEN || process.env.BAMBU_TOKEN || "";
|
|
19
20
|
const DEFAULT_BAMBU_DEV_ID = process.env.BAMBU_DEV_ID || DEFAULT_BAMBU_SERIAL;
|
|
20
21
|
const TEMP_DIR = process.env.TEMP_DIR || path.join(process.cwd(), "temp");
|
|
21
22
|
// Printer model and bed type
|
|
22
|
-
const DEFAULT_BAMBU_MODEL = process.env.
|
|
23
|
+
const DEFAULT_BAMBU_MODEL = process.env.BAMBU_PRINTER_MODEL?.trim().toLowerCase() ||
|
|
24
|
+
process.env.BAMBU_MODEL?.trim().toLowerCase() ||
|
|
25
|
+
"";
|
|
23
26
|
const DEFAULT_BED_TYPE = process.env.BED_TYPE?.trim().toLowerCase() || "textured_plate";
|
|
24
27
|
const DEFAULT_NOZZLE_DIAMETER = process.env.NOZZLE_DIAMETER?.trim() || "0.4";
|
|
25
|
-
const VALID_BAMBU_MODELS = ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"];
|
|
26
|
-
const VALID_BED_TYPES = ["textured_plate", "cool_plate", "engineering_plate", "hot_plate"];
|
|
28
|
+
const VALID_BAMBU_MODELS = ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s"];
|
|
29
|
+
const VALID_BED_TYPES = ["textured_plate", "cool_plate", "engineering_plate", "hot_plate", "supertack_plate"];
|
|
30
|
+
const VALID_BAMBUSTUDIO_CLI_BED_TYPES = ["textured_plate", "cool_plate", "engineering_plate", "hot_plate"];
|
|
27
31
|
// Map model IDs to BambuStudio --load-machine preset names
|
|
28
32
|
const BAMBU_MODEL_PRESETS = {
|
|
29
33
|
p1s: (n) => `Bambu Lab P1S ${n} nozzle`,
|
|
30
34
|
p1p: (n) => `Bambu Lab P1P ${n} nozzle`,
|
|
35
|
+
p2s: (n) => `Bambu Lab P2S ${n} nozzle`,
|
|
31
36
|
x1c: (n) => `Bambu Lab X1 Carbon ${n} nozzle`,
|
|
32
37
|
x1e: (n) => `Bambu Lab X1E ${n} nozzle`,
|
|
33
38
|
a1: (n) => `Bambu Lab A1 ${n} nozzle`,
|
|
34
39
|
a1mini: (n) => `Bambu Lab A1 mini ${n} nozzle`,
|
|
35
40
|
h2d: (n) => `Bambu Lab H2D ${n} nozzle`,
|
|
41
|
+
h2s: (n) => `Bambu Lab H2S ${n} nozzle`,
|
|
36
42
|
};
|
|
43
|
+
const FILAMENT_PROFILE_DIR = "/Applications/BambuStudio.app/Contents/Resources/profiles/BBL/filament";
|
|
44
|
+
const FILAMENT_MODEL_CODES = {
|
|
45
|
+
p1s: "P1S",
|
|
46
|
+
p1p: "P1P",
|
|
47
|
+
p2s: "P2S",
|
|
48
|
+
x1c: "X1C",
|
|
49
|
+
x1e: "X1E",
|
|
50
|
+
a1: "A1",
|
|
51
|
+
a1mini: "A1M",
|
|
52
|
+
h2d: "H2D",
|
|
53
|
+
h2s: "H2S",
|
|
54
|
+
};
|
|
55
|
+
const COLLAR_CHARM_POLICY = getCollarCharmRolePolicy();
|
|
56
|
+
let filamentProfileIndexCache = null;
|
|
57
|
+
function buildFilamentProfileIndex() {
|
|
58
|
+
const byName = new Map();
|
|
59
|
+
const baseNameByFilamentId = new Map();
|
|
60
|
+
if (!fs.existsSync(FILAMENT_PROFILE_DIR)) {
|
|
61
|
+
return { byName, baseNameByFilamentId };
|
|
62
|
+
}
|
|
63
|
+
for (const entry of fs.readdirSync(FILAMENT_PROFILE_DIR)) {
|
|
64
|
+
if (!entry.endsWith(".json"))
|
|
65
|
+
continue;
|
|
66
|
+
const filePath = path.join(FILAMENT_PROFILE_DIR, entry);
|
|
67
|
+
try {
|
|
68
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
69
|
+
const parsed = JSON.parse(raw);
|
|
70
|
+
const name = typeof parsed?.name === "string" ? parsed.name.trim() : "";
|
|
71
|
+
const filamentId = typeof parsed?.filament_id === "string" ? parsed.filament_id.trim() : "";
|
|
72
|
+
if (name) {
|
|
73
|
+
byName.set(name, filePath);
|
|
74
|
+
}
|
|
75
|
+
if (name && filamentId) {
|
|
76
|
+
baseNameByFilamentId.set(filamentId, name);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Ignore malformed or non-profile JSON files.
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return { byName, baseNameByFilamentId };
|
|
84
|
+
}
|
|
85
|
+
function getFilamentProfileIndex() {
|
|
86
|
+
if (!filamentProfileIndexCache) {
|
|
87
|
+
filamentProfileIndexCache = buildFilamentProfileIndex();
|
|
88
|
+
}
|
|
89
|
+
return filamentProfileIndexCache;
|
|
90
|
+
}
|
|
91
|
+
function resolveFilamentProfileCandidates(trayInfoIdx, bambuModel, nozzleDiameter) {
|
|
92
|
+
const index = getFilamentProfileIndex();
|
|
93
|
+
const baseName = index.baseNameByFilamentId.get(trayInfoIdx) || null;
|
|
94
|
+
if (!baseName) {
|
|
95
|
+
return { baseName: null, paths: [], resolution: "unresolved" };
|
|
96
|
+
}
|
|
97
|
+
const bareName = baseName.replace(/\s*@base$/, "");
|
|
98
|
+
const modelCode = bambuModel ? FILAMENT_MODEL_CODES[bambuModel] : undefined;
|
|
99
|
+
const candidateNames = [];
|
|
100
|
+
if (modelCode && nozzleDiameter) {
|
|
101
|
+
candidateNames.push(`${bareName} @BBL ${modelCode} ${nozzleDiameter} nozzle`);
|
|
102
|
+
}
|
|
103
|
+
if (modelCode) {
|
|
104
|
+
candidateNames.push(`${bareName} @BBL ${modelCode}`);
|
|
105
|
+
}
|
|
106
|
+
candidateNames.push(bareName, baseName);
|
|
107
|
+
const resolvedPaths = [];
|
|
108
|
+
let resolution = "unresolved";
|
|
109
|
+
for (const [candidateIndex, candidateName] of candidateNames.entries()) {
|
|
110
|
+
const candidatePath = index.byName.get(candidateName);
|
|
111
|
+
if (!candidatePath || resolvedPaths.includes(candidatePath))
|
|
112
|
+
continue;
|
|
113
|
+
resolvedPaths.push(candidatePath);
|
|
114
|
+
if (resolution === "unresolved") {
|
|
115
|
+
if (candidateIndex === 0 && modelCode && nozzleDiameter) {
|
|
116
|
+
resolution = "exact-model-nozzle";
|
|
117
|
+
}
|
|
118
|
+
else if ((candidateIndex === 0 && modelCode && !nozzleDiameter) ||
|
|
119
|
+
(candidateIndex === 1 && modelCode && nozzleDiameter)) {
|
|
120
|
+
resolution = "model";
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
resolution = "generic";
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return { baseName, paths: resolvedPaths, resolution };
|
|
128
|
+
}
|
|
129
|
+
function parseIntegerOrNull(value) {
|
|
130
|
+
if (value === undefined || value === null || value === "")
|
|
131
|
+
return null;
|
|
132
|
+
const parsed = Number.parseInt(String(value), 10);
|
|
133
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
134
|
+
}
|
|
135
|
+
function normalizePrinterFilamentInventory(status, bambuModel, nozzleDiameter) {
|
|
136
|
+
const rawAms = status?.raw?.ams || status?.ams || {};
|
|
137
|
+
const trayNow = parseIntegerOrNull(rawAms?.tray_now);
|
|
138
|
+
const trays = [];
|
|
139
|
+
const amsArray = Array.isArray(rawAms?.ams) ? rawAms.ams : [];
|
|
140
|
+
for (const [amsIndex, amsUnit] of amsArray.entries()) {
|
|
141
|
+
const trayArray = Array.isArray(amsUnit?.tray) ? amsUnit.tray : [];
|
|
142
|
+
for (const [trayIndex, tray] of trayArray.entries()) {
|
|
143
|
+
const amsId = parseIntegerOrNull(amsUnit?.id) ?? amsIndex;
|
|
144
|
+
const trayId = parseIntegerOrNull(tray?.id) ?? trayIndex;
|
|
145
|
+
const slot = amsId !== null && trayId !== null ? amsId * 4 + trayId : null;
|
|
146
|
+
const state = parseIntegerOrNull(tray?.state) ?? 0;
|
|
147
|
+
const trayInfoIdx = typeof tray?.tray_info_idx === "string" ? tray.tray_info_idx : null;
|
|
148
|
+
const profileResolution = trayInfoIdx
|
|
149
|
+
? resolveFilamentProfileCandidates(trayInfoIdx, bambuModel, nozzleDiameter)
|
|
150
|
+
: { baseName: null, paths: [], resolution: "unresolved" };
|
|
151
|
+
const resolvedProfilePath = profileResolution.paths[0] || null;
|
|
152
|
+
const matchConfidence = profileResolution.resolution === "exact-model-nozzle" ? "high" :
|
|
153
|
+
profileResolution.resolution === "model" ? "medium" :
|
|
154
|
+
profileResolution.resolution === "generic" ? "low" :
|
|
155
|
+
"none";
|
|
156
|
+
const trayType = typeof tray?.tray_type === "string" ? tray.tray_type : null;
|
|
157
|
+
const traySubBrands = typeof tray?.tray_sub_brands === "string" ? tray.tray_sub_brands : null;
|
|
158
|
+
const trayColor = typeof tray?.tray_color === "string" ? tray.tray_color : null;
|
|
159
|
+
const displayParts = [
|
|
160
|
+
traySubBrands && traySubBrands !== trayType ? traySubBrands : null,
|
|
161
|
+
trayType,
|
|
162
|
+
trayColor ? `#${normalizeRgbColor(trayColor) ?? trayColor.replace(/^#/, "").slice(0, 6)}` : null,
|
|
163
|
+
].filter(Boolean);
|
|
164
|
+
trays.push({
|
|
165
|
+
ams_id: amsId,
|
|
166
|
+
tray_id: trayId,
|
|
167
|
+
slot,
|
|
168
|
+
state,
|
|
169
|
+
loaded: state !== 0 && Boolean(trayInfoIdx),
|
|
170
|
+
tray_info_idx: trayInfoIdx,
|
|
171
|
+
tray_type: trayType,
|
|
172
|
+
tray_sub_brands: traySubBrands,
|
|
173
|
+
tray_color: trayColor,
|
|
174
|
+
remain_percent: typeof tray?.remain === "number" && tray.remain >= 0 ? tray.remain : null,
|
|
175
|
+
nozzle_temp_min: parseIntegerOrNull(tray?.nozzle_temp_min),
|
|
176
|
+
nozzle_temp_max: parseIntegerOrNull(tray?.nozzle_temp_max),
|
|
177
|
+
resolved_base_profile_name: profileResolution.baseName,
|
|
178
|
+
resolved_profile_path: resolvedProfilePath,
|
|
179
|
+
profile_resolution: profileResolution.resolution,
|
|
180
|
+
match_confidence: matchConfidence,
|
|
181
|
+
display_name: displayParts.length > 0 ? displayParts.join(" ") : "empty/unknown",
|
|
182
|
+
profile_candidates: profileResolution.paths,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const loadedTrays = trays.filter((tray) => tray.loaded);
|
|
187
|
+
const recommendedTray = loadedTrays.find((tray) => tray.slot === trayNow && tray.resolved_profile_path) ||
|
|
188
|
+
loadedTrays.find((tray) => tray.resolved_profile_path) ||
|
|
189
|
+
null;
|
|
190
|
+
const recommendedReason = recommendedTray?.slot === trayNow
|
|
191
|
+
? "current AMS slot with resolved slicer profile"
|
|
192
|
+
: recommendedTray
|
|
193
|
+
? "first loaded AMS slot with resolved slicer profile"
|
|
194
|
+
: null;
|
|
195
|
+
const allProfilePaths = Array.from(new Set(loadedTrays
|
|
196
|
+
.map((tray) => tray.resolved_profile_path)
|
|
197
|
+
.filter((candidate) => Boolean(candidate))));
|
|
198
|
+
const emptySlots = trays.filter((tray) => !tray.loaded).length;
|
|
199
|
+
const resolvedProfileSlots = loadedTrays.filter((tray) => tray.resolved_profile_path).length;
|
|
200
|
+
return {
|
|
201
|
+
current_slot: trayNow !== null && trayNow >= 0 && trayNow < 254 ? trayNow : null,
|
|
202
|
+
current_source: trayNow === 254 ? "external" : trayNow !== null && trayNow >= 0 && trayNow < 254 ? "ams" : null,
|
|
203
|
+
summary: {
|
|
204
|
+
loaded_slots: loadedTrays.length,
|
|
205
|
+
resolved_profile_slots: resolvedProfileSlots,
|
|
206
|
+
unresolved_loaded_slots: loadedTrays.length - resolvedProfileSlots,
|
|
207
|
+
empty_slots: emptySlots,
|
|
208
|
+
current_slot: trayNow !== null && trayNow >= 0 && trayNow < 254 ? trayNow : null,
|
|
209
|
+
current_source: trayNow === 254 ? "external" : trayNow !== null && trayNow >= 0 && trayNow < 254 ? "ams" : null,
|
|
210
|
+
recommended_slot: recommendedTray?.slot ?? null,
|
|
211
|
+
recommended_reason: recommendedReason,
|
|
212
|
+
},
|
|
213
|
+
trays,
|
|
214
|
+
recommended: recommendedTray
|
|
215
|
+
? {
|
|
216
|
+
slot: recommendedTray.slot,
|
|
217
|
+
tray_info_idx: recommendedTray.tray_info_idx,
|
|
218
|
+
tray_type: recommendedTray.tray_type,
|
|
219
|
+
resolved_profile_path: recommendedTray.resolved_profile_path,
|
|
220
|
+
load_filaments: recommendedTray.resolved_profile_path,
|
|
221
|
+
reason: recommendedReason ?? "resolved slicer profile",
|
|
222
|
+
}
|
|
223
|
+
: null,
|
|
224
|
+
load_filaments_all: allProfilePaths.length > 0 ? allProfilePaths.join(";") : null,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Normalize a hex color string to lowercase 6-char RGB (drop leading "#"
|
|
229
|
+
* and any trailing alpha bytes). Returns null if the input doesn't look
|
|
230
|
+
* like a hex color.
|
|
231
|
+
*
|
|
232
|
+
* Bambu mixes formats: 3MF project_settings stores `#FFFFFF` or
|
|
233
|
+
* `#FF911A80` (RGB or RGBA with leading #). AMS push_status reports
|
|
234
|
+
* `FFFFFFFF` (RGBA without #). Comparing alpha is risky -- the same
|
|
235
|
+
* filament can show up as `#000000` in one place and `000000FF` in
|
|
236
|
+
* another. RGB-only is the right key.
|
|
237
|
+
*/
|
|
238
|
+
function normalizeRgbColor(color) {
|
|
239
|
+
if (!color)
|
|
240
|
+
return null;
|
|
241
|
+
const hex = color.replace(/^#/, "").toLowerCase();
|
|
242
|
+
if (!/^[0-9a-f]{6}([0-9a-f]{2})?$/.test(hex))
|
|
243
|
+
return null;
|
|
244
|
+
return hex.slice(0, 6);
|
|
245
|
+
}
|
|
246
|
+
function resolveAmsSlotsFromRequirements(requirements, inventory) {
|
|
247
|
+
const matches = [];
|
|
248
|
+
const missing = [];
|
|
249
|
+
const amsSlots = [];
|
|
250
|
+
// Track which AMS slots have already been claimed by an earlier
|
|
251
|
+
// requirement so two filaments with the same SKU don't collapse onto
|
|
252
|
+
// the same physical slot.
|
|
253
|
+
const claimedSlots = new Set();
|
|
254
|
+
for (const filament of requirements.filaments) {
|
|
255
|
+
if (!filament.tray_info_idx) {
|
|
256
|
+
missing.push({
|
|
257
|
+
filament_position: filament.filamentPosition,
|
|
258
|
+
tray_info_idx: null,
|
|
259
|
+
required_color: filament.color ?? null,
|
|
260
|
+
reason: "no_sku",
|
|
261
|
+
});
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const requiredColor = normalizeRgbColor(filament.color);
|
|
265
|
+
// Pool of trays loaded with the required SKU that aren't already claimed.
|
|
266
|
+
const skuPool = inventory.trays.filter((candidate) => candidate.loaded &&
|
|
267
|
+
candidate.slot !== null &&
|
|
268
|
+
candidate.tray_info_idx === filament.tray_info_idx &&
|
|
269
|
+
!claimedSlots.has(candidate.slot));
|
|
270
|
+
if (skuPool.length === 0) {
|
|
271
|
+
// No trays of this SKU loaded at all (or all already claimed).
|
|
272
|
+
const reason = inventory.trays.some((t) => t.tray_info_idx === filament.tray_info_idx)
|
|
273
|
+
? "exhausted"
|
|
274
|
+
: "no_loaded_match";
|
|
275
|
+
missing.push({
|
|
276
|
+
filament_position: filament.filamentPosition,
|
|
277
|
+
tray_info_idx: filament.tray_info_idx,
|
|
278
|
+
required_color: filament.color ?? null,
|
|
279
|
+
reason,
|
|
280
|
+
});
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
// Prefer an exact RGB color match. This is what disambiguates
|
|
284
|
+
// same-SKU different-color cases (e.g. two GFG02 PETG HF trays
|
|
285
|
+
// loaded -- one black, one white).
|
|
286
|
+
let chosen = skuPool.find((candidate) => normalizeRgbColor(candidate.tray_color) === requiredColor && requiredColor !== null);
|
|
287
|
+
let strategy = "color";
|
|
288
|
+
// Fall back to SKU-only when the requirement has no color (legacy
|
|
289
|
+
// 3MF) or when only one tray of this SKU is loaded (no ambiguity).
|
|
290
|
+
if (!chosen) {
|
|
291
|
+
if (!requiredColor || skuPool.length === 1) {
|
|
292
|
+
chosen = skuPool[0];
|
|
293
|
+
strategy = "sku-only";
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
// Color was specified, multiple SKU candidates loaded, none
|
|
297
|
+
// match the requested color. Surface clearly rather than pick
|
|
298
|
+
// a wrong color silently.
|
|
299
|
+
missing.push({
|
|
300
|
+
filament_position: filament.filamentPosition,
|
|
301
|
+
tray_info_idx: filament.tray_info_idx,
|
|
302
|
+
required_color: filament.color ?? null,
|
|
303
|
+
reason: "color_mismatch",
|
|
304
|
+
});
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (chosen.slot === null) {
|
|
309
|
+
// Defensive: shouldn't happen because of the filter above, but
|
|
310
|
+
// keep the type contract honest.
|
|
311
|
+
missing.push({
|
|
312
|
+
filament_position: filament.filamentPosition,
|
|
313
|
+
tray_info_idx: filament.tray_info_idx,
|
|
314
|
+
required_color: filament.color ?? null,
|
|
315
|
+
reason: "no_loaded_match",
|
|
316
|
+
});
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
claimedSlots.add(chosen.slot);
|
|
320
|
+
amsSlots.push(chosen.slot);
|
|
321
|
+
matches.push({
|
|
322
|
+
filament_position: filament.filamentPosition,
|
|
323
|
+
tray_info_idx: filament.tray_info_idx,
|
|
324
|
+
slot: chosen.slot,
|
|
325
|
+
matched_color: chosen.tray_color ?? null,
|
|
326
|
+
match_strategy: strategy,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
return { ams_slots: amsSlots, matches, missing };
|
|
330
|
+
}
|
|
331
|
+
function extractPrinterDiagnostics(status) {
|
|
332
|
+
const raw = status?.raw && typeof status.raw === "object" ? status.raw : {};
|
|
333
|
+
const diagnosticFields = {};
|
|
334
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
335
|
+
if (/(hms|error|fail|warn)/i.test(key)) {
|
|
336
|
+
diagnosticFields[key] = value;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
connected: Boolean(status?.connected),
|
|
341
|
+
serial: status?.serial ?? null,
|
|
342
|
+
printer_status: status?.status ?? null,
|
|
343
|
+
active_error: raw.print_error ??
|
|
344
|
+
raw.print_error_code ??
|
|
345
|
+
raw.mc_print_error_code ??
|
|
346
|
+
raw.fail_reason ??
|
|
347
|
+
null,
|
|
348
|
+
hms: raw.hms ?? raw.hms_info ?? raw.hms_list ?? null,
|
|
349
|
+
diagnostic_fields: diagnosticFields,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
37
352
|
function validateBambuModel(model) {
|
|
38
353
|
const normalized = model.trim().toLowerCase();
|
|
39
354
|
if (!VALID_BAMBU_MODELS.includes(normalized)) {
|
|
@@ -48,9 +363,19 @@ function resolveBedType(argsBedType) {
|
|
|
48
363
|
}
|
|
49
364
|
return bedType;
|
|
50
365
|
}
|
|
366
|
+
function resolveBambuStudioCliBedType(argsBedType) {
|
|
367
|
+
const bedType = resolveBedType(argsBedType);
|
|
368
|
+
if (!VALID_BAMBUSTUDIO_CLI_BED_TYPES.includes(bedType)) {
|
|
369
|
+
throw new Error(`BambuStudio CLI bed_type "${bedType}" is not verified. Use a pre-sliced 3MF for SuperTack, or choose one of: ${VALID_BAMBUSTUDIO_CLI_BED_TYPES.join(", ")}`);
|
|
370
|
+
}
|
|
371
|
+
return bedType;
|
|
372
|
+
}
|
|
51
373
|
// Slicer configuration (defaults to bambustudio)
|
|
52
374
|
const DEFAULT_SLICER_TYPE = process.env.SLICER_TYPE || "bambustudio";
|
|
53
|
-
const DEFAULT_SLICER_PROFILE = process.env.SLICER_PROFILE || "";
|
|
375
|
+
const DEFAULT_SLICER_PROFILE = process.env.BAMBU_SLICER_PROFILE || process.env.SLICER_PROFILE || "";
|
|
376
|
+
const DEFAULT_TEMPLATE_3MF_PATH = process.env.BAMBU_TEMPLATE_3MF_PATH || "";
|
|
377
|
+
const DEFAULT_TEMPLATE_DIR = process.env.BAMBU_TEMPLATE_DIR ||
|
|
378
|
+
path.join(process.env.HOME || process.cwd(), "Sync", "bambu", "templates");
|
|
54
379
|
const SLICER_SCHEMA_VALUES = [
|
|
55
380
|
...SLICER_TYPES,
|
|
56
381
|
"fulu-orca",
|
|
@@ -76,6 +401,9 @@ function defaultSlicerPathFor(slicerType) {
|
|
|
76
401
|
return "OrcaSlicer";
|
|
77
402
|
}
|
|
78
403
|
if (slicerType === "bambustudio") {
|
|
404
|
+
if (process.env.BAMBU_STUDIO_PATH) {
|
|
405
|
+
return process.env.BAMBU_STUDIO_PATH;
|
|
406
|
+
}
|
|
79
407
|
if (process.platform === "darwin") {
|
|
80
408
|
return "/Applications/BambuStudio.app/Contents/MacOS/BambuStudio";
|
|
81
409
|
}
|
|
@@ -122,6 +450,37 @@ function parseCsvEnv(value) {
|
|
|
122
450
|
return new Set();
|
|
123
451
|
return new Set(value.split(",").map((e) => e.trim()).filter((e) => e.length > 0));
|
|
124
452
|
}
|
|
453
|
+
async function resolveSlicerProfilePath(requestedProfile, templatePath, tempDir) {
|
|
454
|
+
if (requestedProfile) {
|
|
455
|
+
return requestedProfile;
|
|
456
|
+
}
|
|
457
|
+
if (templatePath) {
|
|
458
|
+
return resolveTemplateSlicerProfilePath(templatePath, tempDir);
|
|
459
|
+
}
|
|
460
|
+
return undefined;
|
|
461
|
+
}
|
|
462
|
+
async function resolveTemplateSlicerProfilePath(templatePath, tempDir) {
|
|
463
|
+
const extension = path.extname(templatePath).toLowerCase();
|
|
464
|
+
if (extension === ".3mf") {
|
|
465
|
+
return extractBambuTemplateSettings(templatePath, tempDir);
|
|
466
|
+
}
|
|
467
|
+
if (extension === ".json" || extension === ".config") {
|
|
468
|
+
return templatePath;
|
|
469
|
+
}
|
|
470
|
+
throw new Error(`Template profile must be a .3mf, .json, or .config file: ${templatePath}`);
|
|
471
|
+
}
|
|
472
|
+
function hasExplicitSlicerProfile(args) {
|
|
473
|
+
return typeof args?.slicer_profile === "string" && args.slicer_profile.trim().length > 0;
|
|
474
|
+
}
|
|
475
|
+
async function resolveTemplateFirstSlicerProfilePath(args, configuredProfile, template3mfPath, tempDir) {
|
|
476
|
+
if (hasExplicitSlicerProfile(args)) {
|
|
477
|
+
return configuredProfile || undefined;
|
|
478
|
+
}
|
|
479
|
+
if (template3mfPath) {
|
|
480
|
+
return resolveTemplateSlicerProfilePath(template3mfPath, tempDir);
|
|
481
|
+
}
|
|
482
|
+
return configuredProfile || undefined;
|
|
483
|
+
}
|
|
125
484
|
function readRuntimeConfig() {
|
|
126
485
|
const rawTransport = process.env.MCP_TRANSPORT?.trim().toLowerCase();
|
|
127
486
|
const transport = rawTransport === "streamable-http" || rawTransport === "http"
|
|
@@ -248,6 +607,131 @@ function redactPrintParams(params) {
|
|
|
248
607
|
password: params.password ? "[redacted]" : "",
|
|
249
608
|
};
|
|
250
609
|
}
|
|
610
|
+
function parseLooseSlicerConfig(content) {
|
|
611
|
+
try {
|
|
612
|
+
return JSON.parse(content);
|
|
613
|
+
}
|
|
614
|
+
catch {
|
|
615
|
+
const config = {};
|
|
616
|
+
for (const line of content.split(/\r?\n/)) {
|
|
617
|
+
const trimmed = line.trim();
|
|
618
|
+
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith(";"))
|
|
619
|
+
continue;
|
|
620
|
+
const separatorIndex = trimmed.indexOf("=");
|
|
621
|
+
if (separatorIndex === -1)
|
|
622
|
+
continue;
|
|
623
|
+
const key = trimmed.slice(0, separatorIndex).trim();
|
|
624
|
+
const value = trimmed.slice(separatorIndex + 1).trim();
|
|
625
|
+
if (key) {
|
|
626
|
+
config[key] = value;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return config;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
function summarizeSliceSettings(config) {
|
|
633
|
+
return {
|
|
634
|
+
printer_settings_id: typeof config.printer_settings_id === "string" ? config.printer_settings_id : null,
|
|
635
|
+
default_print_profile: typeof config.default_print_profile === "string" ? config.default_print_profile : null,
|
|
636
|
+
default_filament_profile: Array.isArray(config.default_filament_profile)
|
|
637
|
+
? config.default_filament_profile
|
|
638
|
+
: typeof config.default_filament_profile === "string"
|
|
639
|
+
? [config.default_filament_profile]
|
|
640
|
+
: [],
|
|
641
|
+
filament_settings_id: Array.isArray(config.filament_settings_id)
|
|
642
|
+
? config.filament_settings_id.filter((value) => typeof value === "string" && value.length > 0)
|
|
643
|
+
: [],
|
|
644
|
+
filament_type: Array.isArray(config.filament_type)
|
|
645
|
+
? config.filament_type.filter((value) => typeof value === "string" && value.length > 0)
|
|
646
|
+
: [],
|
|
647
|
+
inherits: typeof config.inherits === "string" ? config.inherits : null,
|
|
648
|
+
print_settings_id: typeof config.print_settings_id === "string" ? config.print_settings_id : null,
|
|
649
|
+
compatible_printers: Array.isArray(config.compatible_printers)
|
|
650
|
+
? config.compatible_printers.filter((value) => typeof value === "string" && value.length > 0)
|
|
651
|
+
: [],
|
|
652
|
+
layer_height: config.layer_height !== undefined && config.layer_height !== null
|
|
653
|
+
? Number(config.layer_height)
|
|
654
|
+
: null,
|
|
655
|
+
first_layer_height: config.initial_layer_print_height !== undefined && config.initial_layer_print_height !== null
|
|
656
|
+
? Number(config.initial_layer_print_height)
|
|
657
|
+
: config.first_layer_height !== undefined && config.first_layer_height !== null
|
|
658
|
+
? Number(config.first_layer_height)
|
|
659
|
+
: null,
|
|
660
|
+
sparse_infill_density: config.sparse_infill_density !== undefined && config.sparse_infill_density !== null
|
|
661
|
+
? String(config.sparse_infill_density)
|
|
662
|
+
: null,
|
|
663
|
+
wall_loops: config.wall_loops !== undefined && config.wall_loops !== null
|
|
664
|
+
? Number(config.wall_loops)
|
|
665
|
+
: null,
|
|
666
|
+
top_shell_layers: config.top_shell_layers !== undefined && config.top_shell_layers !== null
|
|
667
|
+
? Number(config.top_shell_layers)
|
|
668
|
+
: null,
|
|
669
|
+
bottom_shell_layers: config.bottom_shell_layers !== undefined && config.bottom_shell_layers !== null
|
|
670
|
+
? Number(config.bottom_shell_layers)
|
|
671
|
+
: null,
|
|
672
|
+
brim_width: config.brim_width !== undefined && config.brim_width !== null
|
|
673
|
+
? Number(config.brim_width)
|
|
674
|
+
: null,
|
|
675
|
+
support_enabled: config.enable_support !== undefined
|
|
676
|
+
? String(config.enable_support) === "1" || String(config.enable_support).toLowerCase() === "true"
|
|
677
|
+
: config.support_enabled !== undefined
|
|
678
|
+
? Boolean(config.support_enabled)
|
|
679
|
+
: null,
|
|
680
|
+
support_type: typeof config.support_type === "string" ? config.support_type : null,
|
|
681
|
+
bed_type: typeof config.curr_bed_type === "string"
|
|
682
|
+
? config.curr_bed_type
|
|
683
|
+
: typeof config.bed_type === "string"
|
|
684
|
+
? config.bed_type
|
|
685
|
+
: null,
|
|
686
|
+
nozzle_temperature: Array.isArray(config.nozzle_temperature)
|
|
687
|
+
? config.nozzle_temperature
|
|
688
|
+
: config.nozzle_temperature !== undefined
|
|
689
|
+
? [config.nozzle_temperature]
|
|
690
|
+
: [],
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
function sanitizeTemplateName(templateName) {
|
|
694
|
+
return templateName
|
|
695
|
+
.trim()
|
|
696
|
+
.replace(/\\/g, "/")
|
|
697
|
+
.replace(/^\/+|\/+$/g, "")
|
|
698
|
+
.replace(/\s+/g, "_")
|
|
699
|
+
.replace(/[^A-Za-z0-9/_-]/g, "_");
|
|
700
|
+
}
|
|
701
|
+
function scanTemplateRegistry(templateDir) {
|
|
702
|
+
if (!fs.existsSync(templateDir)) {
|
|
703
|
+
return [];
|
|
704
|
+
}
|
|
705
|
+
const allowedExtensions = new Set([".3mf", ".json", ".config"]);
|
|
706
|
+
const entries = [];
|
|
707
|
+
const stack = [templateDir];
|
|
708
|
+
while (stack.length > 0) {
|
|
709
|
+
const currentDir = stack.pop();
|
|
710
|
+
for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
|
|
711
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
712
|
+
if (entry.isDirectory()) {
|
|
713
|
+
stack.push(fullPath);
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
const extension = path.extname(entry.name).toLowerCase();
|
|
717
|
+
if (!allowedExtensions.has(extension)) {
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
const relativePath = path.relative(templateDir, fullPath);
|
|
721
|
+
const templateName = relativePath
|
|
722
|
+
.replace(/\\/g, "/")
|
|
723
|
+
.replace(/(\.gcode)?\.3mf$/i, "")
|
|
724
|
+
.replace(/\.(json|config)$/i, "");
|
|
725
|
+
entries.push({
|
|
726
|
+
name: templateName,
|
|
727
|
+
path: fullPath,
|
|
728
|
+
source_type: extension === ".3mf" ? "3mf" : extension === ".json" ? "json" : "config",
|
|
729
|
+
relative_path: relativePath,
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
return entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
734
|
+
}
|
|
251
735
|
if (!fs.existsSync(TEMP_DIR)) {
|
|
252
736
|
fs.mkdirSync(TEMP_DIR, { recursive: true });
|
|
253
737
|
}
|
|
@@ -287,7 +771,7 @@ class BambuPrinterMCPServer {
|
|
|
287
771
|
if (fromArgs) {
|
|
288
772
|
return validateBambuModel(fromArgs);
|
|
289
773
|
}
|
|
290
|
-
// No model from args or env
|
|
774
|
+
// No model from args or env — ask the user via elicitation
|
|
291
775
|
try {
|
|
292
776
|
const result = await this.server.elicitInput({
|
|
293
777
|
mode: "form",
|
|
@@ -303,11 +787,13 @@ class BambuPrinterMCPServer {
|
|
|
303
787
|
oneOf: [
|
|
304
788
|
{ const: "p1s", title: "P1S" },
|
|
305
789
|
{ const: "p1p", title: "P1P" },
|
|
790
|
+
{ const: "p2s", title: "P2S" },
|
|
306
791
|
{ const: "x1c", title: "X1 Carbon" },
|
|
307
792
|
{ const: "x1e", title: "X1E" },
|
|
308
793
|
{ const: "a1", title: "A1" },
|
|
309
794
|
{ const: "a1mini", title: "A1 Mini" },
|
|
310
795
|
{ const: "h2d", title: "H2D" },
|
|
796
|
+
{ const: "h2s", title: "H2S" },
|
|
311
797
|
],
|
|
312
798
|
},
|
|
313
799
|
},
|
|
@@ -320,7 +806,7 @@ class BambuPrinterMCPServer {
|
|
|
320
806
|
throw new Error("Printer model selection was cancelled. Cannot proceed without knowing the printer model.");
|
|
321
807
|
}
|
|
322
808
|
catch (elicitError) {
|
|
323
|
-
// Elicitation not supported by this client
|
|
809
|
+
// Elicitation not supported by this client — fall back to a clear error
|
|
324
810
|
const msg = elicitError?.message || String(elicitError);
|
|
325
811
|
if (elicitError?.code === -32601 || elicitError?.code === -32600 ||
|
|
326
812
|
msg.includes("does not support") || msg.includes("elicitation")) {
|
|
@@ -340,7 +826,7 @@ class BambuPrinterMCPServer {
|
|
|
340
826
|
timeoutMs: args?.timeout_ms !== undefined ? Number(args.timeout_ms) : undefined,
|
|
341
827
|
};
|
|
342
828
|
}
|
|
343
|
-
async ensurePrintableThreeMFPath(args, printModel, printPreset) {
|
|
829
|
+
async ensurePrintableThreeMFPath(args, printModel, printPreset, bedType) {
|
|
344
830
|
const { slicerType, slicerPath, slicerProfile } = resolveSlicerConfig(args);
|
|
345
831
|
let threeMFPath = String(args.three_mf_path);
|
|
346
832
|
const JSZip = (await import('jszip')).default;
|
|
@@ -350,53 +836,75 @@ class BambuPrinterMCPServer {
|
|
|
350
836
|
if (hasGcode) {
|
|
351
837
|
return { threeMFPath, autoSliced: false };
|
|
352
838
|
}
|
|
839
|
+
if (bedType === "supertack_plate") {
|
|
840
|
+
throw new Error('BambuStudio CLI SuperTack bed type is not verified; use a pre-sliced 3MF for SuperTack or choose textured_plate, cool_plate, engineering_plate, or hot_plate.');
|
|
841
|
+
}
|
|
353
842
|
console.log(`3MF has no gcode - auto-slicing with ${slicerType} for ${printModel}`);
|
|
354
843
|
const autoSliceOptions = {
|
|
355
844
|
uptodate: true,
|
|
356
845
|
ensureOnBed: true,
|
|
357
846
|
minSave: true,
|
|
358
847
|
skipModifiedGcodes: true,
|
|
848
|
+
bedType: bedType ? resolveBambuStudioCliBedType(bedType) : undefined,
|
|
359
849
|
};
|
|
360
850
|
threeMFPath = await this.stlManipulator.sliceSTL(threeMFPath, slicerType, slicerPath, slicerProfile || undefined, undefined, printPreset, autoSliceOptions);
|
|
361
851
|
console.log("Auto-sliced to: " + threeMFPath);
|
|
362
852
|
return { threeMFPath, autoSliced: true };
|
|
363
853
|
}
|
|
364
|
-
async resolveAmsPrintSettings(threeMFPath, args) {
|
|
854
|
+
async resolveAmsPrintSettings(threeMFPath, args, host, bambuSerial, bambuToken, printModel, printNozzle) {
|
|
365
855
|
const parsed3MFData = await parse3MF(threeMFPath);
|
|
366
856
|
let parsedAmsMapping;
|
|
367
857
|
if (parsed3MFData.slicerConfig?.ams_mapping) {
|
|
368
|
-
const slots =
|
|
369
|
-
.filter(v => typeof v === 'number');
|
|
858
|
+
const slots = normalizeAmsMappingObject(parsed3MFData.slicerConfig.ams_mapping);
|
|
370
859
|
if (slots.length > 0) {
|
|
371
|
-
parsedAmsMapping = slots
|
|
860
|
+
parsedAmsMapping = slots;
|
|
372
861
|
}
|
|
373
862
|
}
|
|
374
863
|
let finalAmsMapping = parsedAmsMapping;
|
|
375
|
-
|
|
376
|
-
let useAMS =
|
|
377
|
-
|
|
864
|
+
let finalAmsSlots;
|
|
865
|
+
let useAMS = args?.use_ams !== undefined ? Boolean(args.use_ams) : (!!finalAmsMapping && finalAmsMapping.length > 0);
|
|
866
|
+
const hasUserAmsMapping = hasAmsMappingInput(args?.ams_mapping);
|
|
867
|
+
const hasUserAmsSlots = Array.isArray(args?.ams_slots);
|
|
868
|
+
if (hasUserAmsMapping) {
|
|
378
869
|
let userMappingOverride;
|
|
379
870
|
if (Array.isArray(args.ams_mapping)) {
|
|
380
|
-
userMappingOverride = args.ams_mapping.
|
|
871
|
+
userMappingOverride = args.ams_mapping.map((v, i) => normalizeBridgeAmsTrayValue(v, `ams_mapping[${i}]`));
|
|
381
872
|
}
|
|
382
|
-
else if (typeof args.ams_mapping === 'object') {
|
|
383
|
-
userMappingOverride =
|
|
384
|
-
.filter((v) => typeof v === 'number')
|
|
385
|
-
.sort((a, b) => a - b);
|
|
873
|
+
else if (args.ams_mapping && typeof args.ams_mapping === 'object') {
|
|
874
|
+
userMappingOverride = normalizeAmsMappingObject(args.ams_mapping);
|
|
386
875
|
}
|
|
387
876
|
if (userMappingOverride && userMappingOverride.length > 0) {
|
|
388
877
|
finalAmsMapping = userMappingOverride;
|
|
878
|
+
finalAmsSlots = undefined;
|
|
389
879
|
useAMS = true;
|
|
390
880
|
}
|
|
391
881
|
}
|
|
882
|
+
if (!hasUserAmsMapping && hasUserAmsSlots) {
|
|
883
|
+
const userSlots = args.ams_slots.map((slot, i) => normalizeBridgeAmsTrayValue(slot, `ams_slots[${i}]`));
|
|
884
|
+
finalAmsSlots = userSlots;
|
|
885
|
+
finalAmsMapping = undefined;
|
|
886
|
+
useAMS = userSlots.length > 0;
|
|
887
|
+
}
|
|
888
|
+
if (!hasUserAmsMapping && !hasUserAmsSlots && args?.auto_match_ams) {
|
|
889
|
+
const requirements = await analyze3MFAmsRequirements(threeMFPath, args?.plate_index !== undefined ? Number(args.plate_index) : 0);
|
|
890
|
+
const inventory = await this.getResolvedPrinterFilamentInventory(host, bambuSerial, bambuToken, printModel, printNozzle);
|
|
891
|
+
const resolved = resolveAmsSlotsFromRequirements(requirements, inventory);
|
|
892
|
+
if (resolved.missing.length > 0) {
|
|
893
|
+
throw new Error(`Could not auto-match AMS slots: ${JSON.stringify(resolved.missing)}`);
|
|
894
|
+
}
|
|
895
|
+
finalAmsSlots = resolved.ams_slots;
|
|
896
|
+
finalAmsMapping = undefined;
|
|
897
|
+
useAMS = true;
|
|
898
|
+
}
|
|
392
899
|
if (args?.use_ams === false) {
|
|
393
900
|
finalAmsMapping = undefined;
|
|
901
|
+
finalAmsSlots = undefined;
|
|
394
902
|
useAMS = false;
|
|
395
903
|
}
|
|
396
|
-
if ((!finalAmsMapping || finalAmsMapping.length === 0) && !
|
|
904
|
+
if ((!finalAmsMapping || finalAmsMapping.length === 0) && (!finalAmsSlots || finalAmsSlots.length === 0)) {
|
|
397
905
|
useAMS = false;
|
|
398
906
|
}
|
|
399
|
-
return { useAMS, finalAmsMapping };
|
|
907
|
+
return { useAMS, finalAmsMapping, finalAmsSlots };
|
|
400
908
|
}
|
|
401
909
|
async print3mfViaBambuNetwork(args, host, bambuSerial, bambuToken) {
|
|
402
910
|
if (!args?.three_mf_path) {
|
|
@@ -430,13 +938,13 @@ class BambuPrinterMCPServer {
|
|
|
430
938
|
if (isLocalBridgePrint && !password) {
|
|
431
939
|
throw new Error("bambu_token/access code is required for FULU BambuNetwork LAN/local print methods.");
|
|
432
940
|
}
|
|
433
|
-
const { threeMFPath, autoSliced } = await this.ensurePrintableThreeMFPath(args, printModel, printPreset);
|
|
434
|
-
const { useAMS, finalAmsMapping } = await this.resolveAmsPrintSettings(threeMFPath, args);
|
|
941
|
+
const { threeMFPath, autoSliced } = await this.ensurePrintableThreeMFPath(args, printModel, printPreset, printBedType);
|
|
942
|
+
const { useAMS, finalAmsMapping, finalAmsSlots } = await this.resolveAmsPrintSettings(threeMFPath, args, host, bambuSerial, bambuToken, printModel, printNozzle);
|
|
435
943
|
const threeMfFilename = path.basename(threeMFPath);
|
|
436
944
|
const projectName = String(args?.project_name || threeMfFilename.replace(/\.3mf$/i, ''));
|
|
437
945
|
const presetName = String(args?.preset_name || `${projectName}_plate_${plateIndex + 1}`);
|
|
438
946
|
const clientJobId = args?.client_job_id !== undefined ? Number(args.client_job_id) : Date.now();
|
|
439
|
-
const amsMapping = stringifyBridgeJson(args?.ams_mapping_bridge ?? finalAmsMapping);
|
|
947
|
+
const amsMapping = stringifyBridgeJson(args?.ams_mapping_bridge ?? finalAmsMapping ?? finalAmsSlots);
|
|
440
948
|
const params = {
|
|
441
949
|
dev_id: devId,
|
|
442
950
|
task_name: String(args?.task_name || projectName),
|
|
@@ -503,10 +1011,175 @@ class BambuPrinterMCPServer {
|
|
|
503
1011
|
plateIndex,
|
|
504
1012
|
bridgePlateIndex: plateIndex + 1,
|
|
505
1013
|
useAMS,
|
|
506
|
-
amsMapping: finalAmsMapping,
|
|
1014
|
+
amsMapping: finalAmsMapping ?? finalAmsSlots,
|
|
507
1015
|
params: redactPrintParams(params),
|
|
508
1016
|
};
|
|
509
1017
|
}
|
|
1018
|
+
async getResolvedPrinterFilamentInventory(host, bambuSerial, bambuToken, bambuModel, nozzleDiameter) {
|
|
1019
|
+
const status = await this.bambu.getStatus(host, bambuSerial, bambuToken);
|
|
1020
|
+
let inventory = normalizePrinterFilamentInventory(status, bambuModel, nozzleDiameter);
|
|
1021
|
+
// The first MQTT push from an idle printer is sparse (model/modules
|
|
1022
|
+
// only). AMS data arrives on a second push a short while later. If
|
|
1023
|
+
// we have a live connection but no loaded trays, wait for the follow-
|
|
1024
|
+
// up push and retry.
|
|
1025
|
+
if (inventory.trays.length === 0 && status.connected) {
|
|
1026
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
1027
|
+
const retryStatus = await this.bambu.getStatus(host, bambuSerial, bambuToken);
|
|
1028
|
+
inventory = normalizePrinterFilamentInventory(retryStatus, bambuModel, nozzleDiameter);
|
|
1029
|
+
}
|
|
1030
|
+
return inventory;
|
|
1031
|
+
}
|
|
1032
|
+
async inspectSliceSettings(sourcePath) {
|
|
1033
|
+
if (!fs.existsSync(sourcePath)) {
|
|
1034
|
+
throw new Error(`Slice settings source not found: ${sourcePath}`);
|
|
1035
|
+
}
|
|
1036
|
+
const extension = path.extname(sourcePath).toLowerCase();
|
|
1037
|
+
const tempDir = path.join(TEMP_DIR, "slice-settings");
|
|
1038
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
1039
|
+
if (extension === ".3mf") {
|
|
1040
|
+
const parsed = await parse3MF(sourcePath);
|
|
1041
|
+
const extractedSettingsPath = await extractBambuTemplateSettings(sourcePath, tempDir);
|
|
1042
|
+
const config = (parsed.slicerConfig || {});
|
|
1043
|
+
return {
|
|
1044
|
+
source_path: sourcePath,
|
|
1045
|
+
source_type: "3mf",
|
|
1046
|
+
extracted_settings_path: extractedSettingsPath,
|
|
1047
|
+
object_count: parsed.objects.length,
|
|
1048
|
+
build_item_count: parsed.build.items.length,
|
|
1049
|
+
metadata_keys: Object.keys(parsed.metadata),
|
|
1050
|
+
summary: summarizeSliceSettings(config),
|
|
1051
|
+
raw_key_count: Object.keys(config).length,
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
const content = fs.readFileSync(sourcePath, "utf8");
|
|
1055
|
+
const config = parseLooseSlicerConfig(content);
|
|
1056
|
+
return {
|
|
1057
|
+
source_path: sourcePath,
|
|
1058
|
+
source_type: extension === ".json" ? "json" : extension === ".config" ? "config" : "text",
|
|
1059
|
+
extracted_settings_path: sourcePath,
|
|
1060
|
+
summary: summarizeSliceSettings(config),
|
|
1061
|
+
raw_key_count: Object.keys(config).length,
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
async resolveCollarCharmPrepared3MF(sourcePath, template3mfPath, slicerType, slicerPath, slicerProfile, printModel, printNozzle, bedType, host, bambuSerial, bambuToken) {
|
|
1065
|
+
if (!sourcePath.toLowerCase().endsWith(".3mf")) {
|
|
1066
|
+
throw new Error("print_collar_charm requires a prepared .3mf project or sliced 3MF.");
|
|
1067
|
+
}
|
|
1068
|
+
try {
|
|
1069
|
+
const JSZip = (await import('jszip')).default;
|
|
1070
|
+
const zipData = fs.readFileSync(sourcePath);
|
|
1071
|
+
const zip = await JSZip.loadAsync(zipData);
|
|
1072
|
+
const hasGcode = Object.keys(zip.files).some((fileName) => fileName.match(/Metadata\/plate_\d+\.gcode/i) || fileName.endsWith('.gcode'));
|
|
1073
|
+
if (hasGcode) {
|
|
1074
|
+
return sourcePath;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
catch (error) {
|
|
1078
|
+
throw new Error(`Failed to inspect collar charm 3MF before slicing: ${error.message}`);
|
|
1079
|
+
}
|
|
1080
|
+
const printPreset = BAMBU_MODEL_PRESETS[printModel]?.(printNozzle);
|
|
1081
|
+
if (bedType === "supertack_plate") {
|
|
1082
|
+
throw new Error('BambuStudio CLI SuperTack bed type is not verified; use a pre-sliced 3MF for SuperTack or choose textured_plate, cool_plate, engineering_plate, or hot_plate.');
|
|
1083
|
+
}
|
|
1084
|
+
const autoSliceOptions = {
|
|
1085
|
+
uptodate: true,
|
|
1086
|
+
ensureOnBed: true,
|
|
1087
|
+
minSave: true,
|
|
1088
|
+
skipModifiedGcodes: true,
|
|
1089
|
+
bedType,
|
|
1090
|
+
};
|
|
1091
|
+
try {
|
|
1092
|
+
const liveFilaments = await this.getResolvedPrinterFilamentInventory(host, bambuSerial, bambuToken, printModel, printNozzle);
|
|
1093
|
+
if (liveFilaments.recommended?.load_filaments) {
|
|
1094
|
+
autoSliceOptions.loadFilaments = liveFilaments.recommended.load_filaments;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
catch (filamentError) {
|
|
1098
|
+
console.warn("Could not resolve live printer filaments for collar charm auto-slicing:", filamentError);
|
|
1099
|
+
}
|
|
1100
|
+
return this.stlManipulator.sliceSTL(sourcePath, slicerType, slicerPath, slicerProfile || template3mfPath || undefined, undefined, printPreset, autoSliceOptions);
|
|
1101
|
+
}
|
|
1102
|
+
async preflightCollarCharmPolicy(host, bambuSerial, bambuToken, bambuModel, nozzleDiameter) {
|
|
1103
|
+
const inventory = await this.getResolvedPrinterFilamentInventory(host, bambuSerial, bambuToken, bambuModel, nozzleDiameter);
|
|
1104
|
+
const requiredSlots = [COLLAR_CHARM_POLICY.amsSlots.inner, COLLAR_CHARM_POLICY.amsSlots.outer];
|
|
1105
|
+
for (const slot of requiredSlots) {
|
|
1106
|
+
const tray = inventory.trays.find((candidate) => candidate.slot === slot);
|
|
1107
|
+
if (!tray) {
|
|
1108
|
+
throw new Error(`Collar charm wrapper requires AMS tray ${slot}, but that tray is not reported by the printer.`);
|
|
1109
|
+
}
|
|
1110
|
+
if (!tray.loaded) {
|
|
1111
|
+
throw new Error(`Collar charm wrapper requires AMS tray ${slot} to be loaded, but it is currently empty or unavailable.`);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
return inventory;
|
|
1115
|
+
}
|
|
1116
|
+
listTemplateRegistry(templateDir) {
|
|
1117
|
+
const resolvedTemplateDir = templateDir && templateDir.trim().length > 0
|
|
1118
|
+
? templateDir
|
|
1119
|
+
: DEFAULT_TEMPLATE_DIR;
|
|
1120
|
+
return {
|
|
1121
|
+
template_dir: resolvedTemplateDir,
|
|
1122
|
+
templates: scanTemplateRegistry(resolvedTemplateDir),
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
resolveTemplatePath(templateName, templateDir, sourceTypes) {
|
|
1126
|
+
if (!templateName || templateName.trim().length === 0) {
|
|
1127
|
+
return undefined;
|
|
1128
|
+
}
|
|
1129
|
+
const registry = this.listTemplateRegistry(templateDir);
|
|
1130
|
+
const normalizedName = sanitizeTemplateName(templateName).toLowerCase();
|
|
1131
|
+
const requestedName = templateName.trim().replace(/\\/g, "/").replace(/^\/+|\/+$/g, "").toLowerCase();
|
|
1132
|
+
const nameMatches = registry.templates.filter((entry) => entry.name.toLowerCase() === requestedName ||
|
|
1133
|
+
sanitizeTemplateName(entry.name).toLowerCase() === normalizedName);
|
|
1134
|
+
const searchTypes = sourceTypes && sourceTypes.length > 0
|
|
1135
|
+
? sourceTypes
|
|
1136
|
+
: ["3mf", "json", "config"];
|
|
1137
|
+
const match = searchTypes
|
|
1138
|
+
.map((sourceType) => nameMatches.find((entry) => entry.source_type === sourceType))
|
|
1139
|
+
.find((entry) => Boolean(entry));
|
|
1140
|
+
if (!match) {
|
|
1141
|
+
const typeHint = sourceTypes && sourceTypes.length > 0
|
|
1142
|
+
? ` with source type ${sourceTypes.join("/")}`
|
|
1143
|
+
: "";
|
|
1144
|
+
const availableTypes = nameMatches.length > 0
|
|
1145
|
+
? ` Available source types: ${Array.from(new Set(nameMatches.map((entry) => entry.source_type))).join(", ")}.`
|
|
1146
|
+
: "";
|
|
1147
|
+
throw new Error(`Template "${templateName}"${typeHint} not found in ${registry.template_dir}.${availableTypes}`);
|
|
1148
|
+
}
|
|
1149
|
+
return match.path;
|
|
1150
|
+
}
|
|
1151
|
+
saveTemplate(sourcePath, templateName, templateDir) {
|
|
1152
|
+
if (!fs.existsSync(sourcePath)) {
|
|
1153
|
+
throw new Error(`Template source not found: ${sourcePath}`);
|
|
1154
|
+
}
|
|
1155
|
+
const resolvedTemplateDir = templateDir && templateDir.trim().length > 0
|
|
1156
|
+
? templateDir
|
|
1157
|
+
: DEFAULT_TEMPLATE_DIR;
|
|
1158
|
+
fs.mkdirSync(resolvedTemplateDir, { recursive: true });
|
|
1159
|
+
const sourceBaseName = path.basename(sourcePath);
|
|
1160
|
+
const extension = path.extname(sourceBaseName).toLowerCase();
|
|
1161
|
+
if (![".3mf", ".json", ".config"].includes(extension)) {
|
|
1162
|
+
throw new Error("Templates must be .3mf, .json, or .config files.");
|
|
1163
|
+
}
|
|
1164
|
+
const baseName = templateName && templateName.trim().length > 0
|
|
1165
|
+
? sanitizeTemplateName(templateName)
|
|
1166
|
+
: sanitizeTemplateName(sourceBaseName
|
|
1167
|
+
.replace(/(\.gcode)?\.3mf$/i, "")
|
|
1168
|
+
.replace(/\.(json|config)$/i, ""));
|
|
1169
|
+
const destinationPath = path.join(resolvedTemplateDir, `${baseName}${extension}`);
|
|
1170
|
+
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
|
1171
|
+
fs.copyFileSync(sourcePath, destinationPath);
|
|
1172
|
+
const sourceType = extension === ".3mf" ? "3mf" : extension === ".json" ? "json" : "config";
|
|
1173
|
+
const registryEntry = this.resolveTemplatePath(baseName, resolvedTemplateDir, [sourceType]);
|
|
1174
|
+
return {
|
|
1175
|
+
saved: true,
|
|
1176
|
+
template_name: baseName,
|
|
1177
|
+
source_path: sourcePath,
|
|
1178
|
+
destination_path: destinationPath,
|
|
1179
|
+
template_dir: resolvedTemplateDir,
|
|
1180
|
+
resolved_path: registryEntry,
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
510
1183
|
setupResourceHandlers() {
|
|
511
1184
|
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
512
1185
|
return {
|
|
@@ -522,6 +1195,12 @@ class BambuPrinterMCPServer {
|
|
|
522
1195
|
name: "Bambu Printer Files",
|
|
523
1196
|
mimeType: "application/json",
|
|
524
1197
|
description: "List of files on the Bambu Lab printer"
|
|
1198
|
+
},
|
|
1199
|
+
{
|
|
1200
|
+
uri: `printer://${DEFAULT_HOST}/hms`,
|
|
1201
|
+
name: "Bambu Printer HMS Diagnostics",
|
|
1202
|
+
mimeType: "application/json",
|
|
1203
|
+
description: "HMS and error-related fields from the latest printer status"
|
|
525
1204
|
}
|
|
526
1205
|
],
|
|
527
1206
|
templates: [
|
|
@@ -534,6 +1213,11 @@ class BambuPrinterMCPServer {
|
|
|
534
1213
|
uriTemplate: "printer://{host}/files",
|
|
535
1214
|
name: "Bambu Printer Files",
|
|
536
1215
|
mimeType: "application/json"
|
|
1216
|
+
},
|
|
1217
|
+
{
|
|
1218
|
+
uriTemplate: "printer://{host}/hms",
|
|
1219
|
+
name: "Bambu Printer HMS Diagnostics",
|
|
1220
|
+
mimeType: "application/json"
|
|
537
1221
|
}
|
|
538
1222
|
]
|
|
539
1223
|
};
|
|
@@ -554,6 +1238,20 @@ class BambuPrinterMCPServer {
|
|
|
554
1238
|
else if (resource === "files") {
|
|
555
1239
|
content = await this.bambu.getFiles(host || DEFAULT_HOST, bambuSerial, bambuToken);
|
|
556
1240
|
}
|
|
1241
|
+
else if (resource === "hms") {
|
|
1242
|
+
const status = await this.bambu.getStatus(host || DEFAULT_HOST, bambuSerial, bambuToken);
|
|
1243
|
+
let diagnostics = extractPrinterDiagnostics(status);
|
|
1244
|
+
// The first MQTT status push may not include HMS data — the printer
|
|
1245
|
+
// sends basic state first, then pushes HMS/error fields on a subsequent
|
|
1246
|
+
// report. If we got a connection but no HMS data, wait briefly and
|
|
1247
|
+
// retry so the incremental merge has time to arrive.
|
|
1248
|
+
if (!diagnostics.hms && status.connected) {
|
|
1249
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
1250
|
+
const retryStatus = await this.bambu.getStatus(host || DEFAULT_HOST, bambuSerial, bambuToken);
|
|
1251
|
+
diagnostics = extractPrinterDiagnostics(retryStatus);
|
|
1252
|
+
}
|
|
1253
|
+
content = diagnostics;
|
|
1254
|
+
}
|
|
557
1255
|
else {
|
|
558
1256
|
throw new McpError(ErrorCode.InvalidRequest, `Unknown resource: ${resource}`);
|
|
559
1257
|
}
|
|
@@ -593,6 +1291,69 @@ class BambuPrinterMCPServer {
|
|
|
593
1291
|
}
|
|
594
1292
|
}
|
|
595
1293
|
},
|
|
1294
|
+
{
|
|
1295
|
+
name: "get_printer_filaments",
|
|
1296
|
+
description: "Get the live AMS/external filament inventory from the printer over MQTT, including loaded/empty slot summary, resolved slicer profile paths, match confidence, and recommended load_filaments when the printer model is known.",
|
|
1297
|
+
inputSchema: {
|
|
1298
|
+
type: "object",
|
|
1299
|
+
properties: {
|
|
1300
|
+
host: {
|
|
1301
|
+
type: "string",
|
|
1302
|
+
description: "Hostname or IP address of the printer (default: value from env)"
|
|
1303
|
+
},
|
|
1304
|
+
bambu_serial: {
|
|
1305
|
+
type: "string",
|
|
1306
|
+
description: "Serial number for the Bambu Lab printer (default: value from env)"
|
|
1307
|
+
},
|
|
1308
|
+
bambu_token: {
|
|
1309
|
+
type: "string",
|
|
1310
|
+
description: "Access token for the Bambu Lab printer (default: value from env)"
|
|
1311
|
+
},
|
|
1312
|
+
bambu_model: {
|
|
1313
|
+
type: "string",
|
|
1314
|
+
enum: ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s"],
|
|
1315
|
+
description: "Optional model hint used to resolve Bambu/Orca filament profile JSONs for each tray."
|
|
1316
|
+
},
|
|
1317
|
+
nozzle_diameter: {
|
|
1318
|
+
type: "string",
|
|
1319
|
+
description: "Optional nozzle diameter used when resolving model-specific filament profile JSONs (default: 0.4)."
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
},
|
|
1324
|
+
{
|
|
1325
|
+
name: "resolve_3mf_ams_slots",
|
|
1326
|
+
description: "Inspect a sliced 3MF and match its tray_info_idx filament requirements against the live AMS inventory. Does not upload or start a print.",
|
|
1327
|
+
inputSchema: {
|
|
1328
|
+
type: "object",
|
|
1329
|
+
properties: {
|
|
1330
|
+
three_mf_path: { type: "string", description: "Path to a sliced 3MF/.gcode.3mf file" },
|
|
1331
|
+
plate_index: { type: "number", description: "0-based plate index to inspect (default: 0)" },
|
|
1332
|
+
bambu_model: {
|
|
1333
|
+
type: "string",
|
|
1334
|
+
enum: ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s"],
|
|
1335
|
+
description: "Optional model hint used to resolve Bambu/Orca filament profile JSONs for each tray."
|
|
1336
|
+
},
|
|
1337
|
+
nozzle_diameter: { type: "string", description: "Nozzle diameter in mm (default: 0.4)" },
|
|
1338
|
+
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
1339
|
+
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
1340
|
+
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
1341
|
+
},
|
|
1342
|
+
required: ["three_mf_path"]
|
|
1343
|
+
}
|
|
1344
|
+
},
|
|
1345
|
+
{
|
|
1346
|
+
name: "list_3mf_plate_objects",
|
|
1347
|
+
description: "List object IDs from a sliced 3MF plate. Use these IDs with skip_objects during a running print.",
|
|
1348
|
+
inputSchema: {
|
|
1349
|
+
type: "object",
|
|
1350
|
+
properties: {
|
|
1351
|
+
three_mf_path: { type: "string", description: "Path to a sliced 3MF/.gcode.3mf file" },
|
|
1352
|
+
plate_index: { type: "number", description: "0-based plate index to inspect (default: 0)" }
|
|
1353
|
+
},
|
|
1354
|
+
required: ["three_mf_path"]
|
|
1355
|
+
}
|
|
1356
|
+
},
|
|
596
1357
|
{
|
|
597
1358
|
name: "extend_stl_base",
|
|
598
1359
|
description: "Extend the base of an STL file by a specified amount",
|
|
@@ -644,6 +1405,110 @@ class BambuPrinterMCPServer {
|
|
|
644
1405
|
required: ["stl_path"]
|
|
645
1406
|
}
|
|
646
1407
|
},
|
|
1408
|
+
{
|
|
1409
|
+
name: "list_templates",
|
|
1410
|
+
description: "List saved slicing templates from the local template registry directory.",
|
|
1411
|
+
inputSchema: {
|
|
1412
|
+
type: "object",
|
|
1413
|
+
properties: {
|
|
1414
|
+
template_dir: {
|
|
1415
|
+
type: "string",
|
|
1416
|
+
description: "Optional template directory override. Defaults to BAMBU_TEMPLATE_DIR or the server's configured local template registry."
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
},
|
|
1421
|
+
{
|
|
1422
|
+
name: "save_template",
|
|
1423
|
+
description: "Copy a 3MF, JSON, or config file into the local template registry and register it under a template name.",
|
|
1424
|
+
inputSchema: {
|
|
1425
|
+
type: "object",
|
|
1426
|
+
properties: {
|
|
1427
|
+
source_path: {
|
|
1428
|
+
type: "string",
|
|
1429
|
+
description: "Path to a local .3mf, .json, or .config file to save into the template registry."
|
|
1430
|
+
},
|
|
1431
|
+
template_name: {
|
|
1432
|
+
type: "string",
|
|
1433
|
+
description: "Optional template name. Defaults to the source filename without extension."
|
|
1434
|
+
},
|
|
1435
|
+
template_dir: {
|
|
1436
|
+
type: "string",
|
|
1437
|
+
description: "Optional template directory override. Defaults to BAMBU_TEMPLATE_DIR or the server's configured local template registry."
|
|
1438
|
+
}
|
|
1439
|
+
},
|
|
1440
|
+
required: ["source_path"]
|
|
1441
|
+
}
|
|
1442
|
+
},
|
|
1443
|
+
{
|
|
1444
|
+
name: "get_slice_settings",
|
|
1445
|
+
description: "Inspect slicer settings from a saved 3MF template or a JSON/config slicer profile without slicing anything.",
|
|
1446
|
+
inputSchema: {
|
|
1447
|
+
type: "object",
|
|
1448
|
+
properties: {
|
|
1449
|
+
source_path: {
|
|
1450
|
+
type: "string",
|
|
1451
|
+
description: "Path to a 3MF template, extracted project_settings.config, or slicer profile JSON."
|
|
1452
|
+
},
|
|
1453
|
+
template_name: {
|
|
1454
|
+
type: "string",
|
|
1455
|
+
description: "Optional named template from the local registry. If provided, resolves source_path automatically."
|
|
1456
|
+
},
|
|
1457
|
+
template_dir: {
|
|
1458
|
+
type: "string",
|
|
1459
|
+
description: "Optional template directory override when resolving template_name."
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
},
|
|
1464
|
+
{
|
|
1465
|
+
name: "slice_with_template",
|
|
1466
|
+
description: "Slice an STL or 3MF using a named template from the local registry. This is a higher-level wrapper around slice_stl for template-based workflows.",
|
|
1467
|
+
inputSchema: {
|
|
1468
|
+
type: "object",
|
|
1469
|
+
properties: {
|
|
1470
|
+
stl_path: { type: "string", description: "Path to the STL or 3MF file to slice" },
|
|
1471
|
+
template_name: { type: "string", description: "Named template from the local registry." },
|
|
1472
|
+
template_dir: { type: "string", description: "Optional template directory override when resolving template_name." },
|
|
1473
|
+
bambu_model: {
|
|
1474
|
+
type: "string",
|
|
1475
|
+
enum: ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s"],
|
|
1476
|
+
description: "REQUIRED: Bambu Lab printer model. Ask the user if not known. Using the wrong model can damage the printer."
|
|
1477
|
+
},
|
|
1478
|
+
slicer_type: {
|
|
1479
|
+
type: "string",
|
|
1480
|
+
enum: SLICER_SCHEMA_VALUES,
|
|
1481
|
+
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."
|
|
1482
|
+
},
|
|
1483
|
+
slicer_path: { type: "string", description: "Path to the slicer executable (default: value from env)" },
|
|
1484
|
+
slicer_profile: { type: "string", description: "Explicit slicer profile/config file. Overrides the named template only when provided in the tool call." },
|
|
1485
|
+
nozzle_diameter: { type: "string", description: "Nozzle diameter in mm (default: 0.4)" },
|
|
1486
|
+
bed_type: {
|
|
1487
|
+
type: "string",
|
|
1488
|
+
enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate", "supertack_plate"],
|
|
1489
|
+
description: "Bed plate type for slicing (default: textured_plate). SuperTack is accepted only for pre-sliced print jobs until the BambuStudio CLI identifier is verified."
|
|
1490
|
+
},
|
|
1491
|
+
use_printer_filaments: { type: "boolean", description: "When true, and no explicit slicer profile or load_filaments override is provided, use the printer's current or first loaded AMS filament as the slicer filament profile." },
|
|
1492
|
+
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
1493
|
+
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
1494
|
+
bambu_token: { type: "string", description: "Access token (default: value from env)" },
|
|
1495
|
+
load_filaments: { type: "string", description: "Override filament profiles. Semicolon-separated paths to filament JSON configs." },
|
|
1496
|
+
load_filament_ids: { type: "string", description: "Optional filament-to-object mapping string." },
|
|
1497
|
+
ensure_on_bed: { type: "boolean", description: "Lift floating models onto the bed." },
|
|
1498
|
+
arrange: { type: "boolean", description: "Auto-arrange objects on the build plate." },
|
|
1499
|
+
orient: { type: "boolean", description: "Auto-orient model for optimal printability." },
|
|
1500
|
+
repetitions: { type: "number", description: "Number of copies to print." },
|
|
1501
|
+
scale: { type: "number", description: "Uniform scale factor." },
|
|
1502
|
+
rotate: { type: "number", description: "Z-axis rotation in degrees." },
|
|
1503
|
+
rotate_x: { type: "number", description: "X-axis rotation in degrees." },
|
|
1504
|
+
rotate_y: { type: "number", description: "Y-axis rotation in degrees." },
|
|
1505
|
+
min_save: { type: "boolean", description: "Produce smaller output 3MF." },
|
|
1506
|
+
skip_modified_gcodes: { type: "boolean", description: "Ignore stale custom gcodes in the 3MF." },
|
|
1507
|
+
slice_plate: { type: "number", description: "Which plate index to slice. 0 = all plates." }
|
|
1508
|
+
},
|
|
1509
|
+
required: ["stl_path", "template_name", "bambu_model"]
|
|
1510
|
+
}
|
|
1511
|
+
},
|
|
647
1512
|
{
|
|
648
1513
|
name: "slice_stl",
|
|
649
1514
|
description: "Slice an STL or 3MF file using a slicer to generate printable G-code or sliced 3MF. IMPORTANT: bambu_model must be specified to ensure the slicer generates safe G-code for the correct printer.",
|
|
@@ -653,7 +1518,7 @@ class BambuPrinterMCPServer {
|
|
|
653
1518
|
stl_path: { type: "string", description: "Path to the STL or 3MF file to slice" },
|
|
654
1519
|
bambu_model: {
|
|
655
1520
|
type: "string",
|
|
656
|
-
enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
|
|
1521
|
+
enum: ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s"],
|
|
657
1522
|
description: "REQUIRED: Bambu Lab printer model. Ask the user if not known. Using the wrong model can damage the printer."
|
|
658
1523
|
},
|
|
659
1524
|
slicer_type: {
|
|
@@ -661,10 +1526,19 @@ class BambuPrinterMCPServer {
|
|
|
661
1526
|
enum: SLICER_SCHEMA_VALUES,
|
|
662
1527
|
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."
|
|
663
1528
|
},
|
|
664
|
-
slicer_path: { type: "string", description: "Path to the slicer executable (default: value from env
|
|
1529
|
+
slicer_path: { type: "string", description: "Path to the slicer executable (default: value from env)" },
|
|
665
1530
|
slicer_profile: { type: "string", description: "Path to the slicer profile/config file (optional, overrides bambu_model preset)" },
|
|
1531
|
+
template_3mf_path: { type: "string", description: "Optional template 3MF whose embedded Bambu slicer settings should be reused when slicing a new STL or 3MF." },
|
|
1532
|
+
template_name: { type: "string", description: "Optional named template from the local registry. Resolves to template_3mf_path automatically." },
|
|
1533
|
+
template_dir: { type: "string", description: "Optional template directory override when resolving template_name." },
|
|
666
1534
|
nozzle_diameter: { type: "string", description: "Nozzle diameter in mm (default: 0.4)" },
|
|
667
|
-
|
|
1535
|
+
bed_type: {
|
|
1536
|
+
type: "string",
|
|
1537
|
+
enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate", "supertack_plate"],
|
|
1538
|
+
description: "Bed plate type for slicing (default: textured_plate). SuperTack is accepted only for pre-sliced print jobs until the BambuStudio CLI identifier is verified."
|
|
1539
|
+
},
|
|
1540
|
+
use_printer_filaments: { type: "boolean", description: "When true, and no explicit slicer profile or load_filaments override is provided, use the printer's current or first loaded AMS filament as the slicer filament profile. Template 3MF process settings can still be used at the same time." },
|
|
1541
|
+
uptodate: { type: "boolean", description: "Refresh 3MF preset configs to match the latest BambuStudio version. Use when slicing downloaded or older 3MF files to prevent stale-config failures." },
|
|
668
1542
|
repetitions: { type: "number", description: "Print N identical copies of the model. Each copy gets its own plate placement. Example: 3 prints three copies." },
|
|
669
1543
|
orient: { type: "boolean", description: "Auto-orient the model for optimal printability (minimize supports, maximize bed adhesion). Recommended for raw STL imports that lack a pre-set orientation." },
|
|
670
1544
|
arrange: { type: "boolean", description: "Auto-arrange all objects on the build plate with optimal spacing. Recommended when importing STLs or adding multiple objects. Set false to preserve existing plate layout." },
|
|
@@ -672,7 +1546,7 @@ class BambuPrinterMCPServer {
|
|
|
672
1546
|
clone_objects: { type: "string", description: "Duplicate specific objects on the plate. Comma-separated clone counts per object index, e.g. '1,3,1,10' clones object 0 once, object 1 three times, etc." },
|
|
673
1547
|
skip_objects: { type: "string", description: "Skip specific objects during slicing by index. Comma-separated, e.g. '3,5,10'. Useful for multi-object 3MFs where you only want to print some parts." },
|
|
674
1548
|
load_filaments: { type: "string", description: "Override filament profiles. Semicolon-separated paths to filament JSON configs, e.g. 'pla_basic.json;petg_cf.json'." },
|
|
675
|
-
filament_profile: { type: "string", description: "Compatibility alias for load_filaments. Semicolon-separated Orca/Bambu filament profile JSON paths
|
|
1549
|
+
filament_profile: { type: "string", description: "Compatibility alias for load_filaments. Semicolon-separated Orca/Bambu filament profile JSON paths." },
|
|
676
1550
|
load_filament_ids: { type: "string", description: "Map filaments to objects/parts. Comma-separated IDs matching load_filaments order, e.g. '1,2,3,1' assigns filament 1 to objects 0 and 3." },
|
|
677
1551
|
enable_timelapse: { type: "boolean", description: "Insert timelapse parking moves into gcode. The toolhead parks at a fixed position each layer for camera capture. Adds ~10% print time." },
|
|
678
1552
|
allow_mix_temp: { type: "boolean", description: "Allow filaments with different temperature requirements on the same plate. Required for multi-material prints mixing e.g. PLA and PETG." },
|
|
@@ -741,7 +1615,7 @@ class BambuPrinterMCPServer {
|
|
|
741
1615
|
three_mf_path: { type: "string", description: "Path to the 3MF file to print; unsliced 3MFs are auto-sliced before sending." },
|
|
742
1616
|
bambu_model: {
|
|
743
1617
|
type: "string",
|
|
744
|
-
enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
|
|
1618
|
+
enum: ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s"],
|
|
745
1619
|
description: "REQUIRED: Bambu Lab printer model. Ask the user if not known. Using the wrong model can damage the printer."
|
|
746
1620
|
},
|
|
747
1621
|
connection_type: { type: "string", enum: ["cloud", "lan"], description: "BambuNetwork connection type to put in FULU PrintParams; cloud uses restored internet printing, lan uses local bridge printing." },
|
|
@@ -753,7 +1627,7 @@ class BambuPrinterMCPServer {
|
|
|
753
1627
|
bambu_token: { type: "string", description: "Printer access code/password for LAN/local bridge methods." },
|
|
754
1628
|
username: { type: "string", description: "Printer username for LAN/local bridge methods; defaults to bblp." },
|
|
755
1629
|
password: { type: "string", description: "Printer password/access code override for LAN/local bridge methods." },
|
|
756
|
-
bed_type: { type: "string", enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate"], description: "Bed plate type currently installed (default: textured_plate)." },
|
|
1630
|
+
bed_type: { type: "string", enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate", "supertack_plate"], description: "Bed plate type currently installed (default: textured_plate)." },
|
|
757
1631
|
plate_index: { type: "number", description: "Zero-based plate index to print from the sliced 3MF; converted to FULU's one-based PrintParams plate_index." },
|
|
758
1632
|
project_name: { type: "string", description: "Optional project name sent in FULU PrintParams; defaults to the 3MF filename without extension." },
|
|
759
1633
|
preset_name: { type: "string", description: "Optional preset name sent in FULU PrintParams; defaults to project plus one-based plate index." },
|
|
@@ -770,6 +1644,7 @@ class BambuPrinterMCPServer {
|
|
|
770
1644
|
nozzle_diameter: { type: "string", description: "Nozzle diameter in mm for auto-slicing (default: 0.4)." },
|
|
771
1645
|
use_ams: { type: "boolean", description: "Whether to use the AMS; defaults to auto-detect from the 3MF mapping." },
|
|
772
1646
|
ams_mapping: { type: "array", description: "AMS slot mapping array used by both local MCP printing and FULU PrintParams.", items: { type: "number" } },
|
|
1647
|
+
ams_slots: { type: "array", description: "Per-used-filament AMS slot list, matching the local LAN print path.", items: { type: "number" } },
|
|
773
1648
|
ams_mapping_bridge: { type: "string", description: "Raw JSON string override for FULU PrintParams ams_mapping when the automatic array is not enough." },
|
|
774
1649
|
ams_mapping2: { type: "string", description: "Raw JSON string for FULU PrintParams ams_mapping2, matching OrcaSlicer-bambulab's v1 AMS mapping field." },
|
|
775
1650
|
ams_mapping_info: { type: "string", description: "Raw JSON string for FULU PrintParams ams_mapping_info, matching OrcaSlicer-bambulab's detailed AMS mapping field." },
|
|
@@ -791,38 +1666,72 @@ class BambuPrinterMCPServer {
|
|
|
791
1666
|
}
|
|
792
1667
|
},
|
|
793
1668
|
{
|
|
794
|
-
name: "
|
|
795
|
-
description: "
|
|
1669
|
+
name: "camera_snapshot",
|
|
1670
|
+
description: "Capture a single JPEG frame from the printer's chamber camera. A1/P1 use TCP-on-6000; X1/P2S/H2 use RTSP via ffmpeg. Returns JPEG as base64; pass save_path to also write the bytes to disk.",
|
|
796
1671
|
inputSchema: {
|
|
797
1672
|
type: "object",
|
|
798
1673
|
properties: {
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
1674
|
+
save_path: { type: "string", description: "Optional absolute path to write the JPEG to disk. If omitted, only the base64 payload is returned." },
|
|
1675
|
+
timeout_ms: { type: "number", description: "Max ms to wait for a full frame (default 8000). Camera may take a few seconds on cold start." },
|
|
1676
|
+
bambu_model: { type: "string", description: "Printer model. Used to route to the correct protocol or fail fast on unsupported models. Defaults to BAMBU_MODEL." },
|
|
1677
|
+
experimental: { type: "boolean", description: "Deprecated and ignored. Earlier this flag let callers probe H2 series via the A1/P1 TCP-on-6000 path; live testing on an H2S confirmed H2 uses RTSP instead, so the flag has no effect now." },
|
|
1678
|
+
ffmpeg_path: { type: "string", description: "Override path to the ffmpeg binary used by the RTSP path. Defaults to ffmpeg via $PATH. Required only for the RTSP transport (X1, P2S, H2 series)." },
|
|
802
1679
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
803
1680
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
804
1681
|
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
805
|
-
}
|
|
806
|
-
required: ["filename"],
|
|
807
|
-
anyOf: [
|
|
808
|
-
{ required: ["gcode"] },
|
|
809
|
-
{ required: ["gcode_path"] }
|
|
810
|
-
]
|
|
1682
|
+
}
|
|
811
1683
|
}
|
|
812
1684
|
},
|
|
813
1685
|
{
|
|
814
|
-
name: "
|
|
815
|
-
description: "
|
|
1686
|
+
name: "delete_printer_file",
|
|
1687
|
+
description: "Delete a file from the Bambu Lab printer's SD card via FTPS. Destructive: requires confirm:true. Restricted to cache/, timelapse/, and logs/ directories. Path traversal segments (..) are rejected.",
|
|
816
1688
|
inputSchema: {
|
|
817
1689
|
type: "object",
|
|
818
1690
|
properties: {
|
|
819
|
-
|
|
820
|
-
filename: { type: "string", description: "Name for the file on the printer" },
|
|
821
|
-
print: { type: "boolean", description: "Start printing after upload (default: false)" },
|
|
822
|
-
bambu_model: {
|
|
1691
|
+
filename: {
|
|
823
1692
|
type: "string",
|
|
824
|
-
|
|
825
|
-
|
|
1693
|
+
description: "File to delete. Bare names default to cache/<name>; pass a relative path like timelapse/foo.mp4 to target other allowed directories."
|
|
1694
|
+
},
|
|
1695
|
+
confirm: {
|
|
1696
|
+
type: "boolean",
|
|
1697
|
+
description: "Must be true to actually delete. When false or omitted the call returns without sending an FTP request."
|
|
1698
|
+
},
|
|
1699
|
+
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
1700
|
+
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
1701
|
+
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
1702
|
+
},
|
|
1703
|
+
required: ["filename"]
|
|
1704
|
+
}
|
|
1705
|
+
},
|
|
1706
|
+
{
|
|
1707
|
+
name: "upload_gcode",
|
|
1708
|
+
description: "Upload a G-code file to the Bambu Lab printer",
|
|
1709
|
+
inputSchema: {
|
|
1710
|
+
type: "object",
|
|
1711
|
+
properties: {
|
|
1712
|
+
filename: { type: "string", description: "Name for the file on the printer" },
|
|
1713
|
+
gcode: { type: "string", description: "G-code content to upload, or a readable local .gcode path. Required unless gcode_path is provided. For large files, prefer gcode_path." },
|
|
1714
|
+
gcode_path: { type: "string", description: "Local path to a .gcode file to upload. Required unless gcode is provided. This avoids sending large G-code bodies through the MCP request." },
|
|
1715
|
+
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
1716
|
+
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
1717
|
+
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
1718
|
+
},
|
|
1719
|
+
required: ["filename"]
|
|
1720
|
+
}
|
|
1721
|
+
},
|
|
1722
|
+
{
|
|
1723
|
+
name: "upload_file",
|
|
1724
|
+
description: "Upload a local file to the Bambu Lab printer",
|
|
1725
|
+
inputSchema: {
|
|
1726
|
+
type: "object",
|
|
1727
|
+
properties: {
|
|
1728
|
+
file_path: { type: "string", description: "Local path to the file to upload" },
|
|
1729
|
+
filename: { type: "string", description: "Name for the file on the printer" },
|
|
1730
|
+
print: { type: "boolean", description: "Start printing after upload (default: false)" },
|
|
1731
|
+
bambu_model: {
|
|
1732
|
+
type: "string",
|
|
1733
|
+
enum: ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s"],
|
|
1734
|
+
description: "Required when print is true. Bambu Lab printer model used as a safety confirmation before starting the uploaded file."
|
|
826
1735
|
},
|
|
827
1736
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
828
1737
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
@@ -840,7 +1749,7 @@ class BambuPrinterMCPServer {
|
|
|
840
1749
|
filename: { type: "string", description: "Name of the file to print" },
|
|
841
1750
|
bambu_model: {
|
|
842
1751
|
type: "string",
|
|
843
|
-
enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
|
|
1752
|
+
enum: ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s"],
|
|
844
1753
|
description: "REQUIRED: Bambu Lab printer model. Ask the user if not known. Starting G-code for the wrong model can damage the printer."
|
|
845
1754
|
},
|
|
846
1755
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
@@ -859,7 +1768,7 @@ class BambuPrinterMCPServer {
|
|
|
859
1768
|
filename: { type: "string", description: "Name of the file to print" },
|
|
860
1769
|
bambu_model: {
|
|
861
1770
|
type: "string",
|
|
862
|
-
enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
|
|
1771
|
+
enum: ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s"],
|
|
863
1772
|
description: "REQUIRED: Bambu Lab printer model. Ask the user if not known. Starting G-code for the wrong model can damage the printer."
|
|
864
1773
|
},
|
|
865
1774
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
@@ -881,6 +1790,93 @@ class BambuPrinterMCPServer {
|
|
|
881
1790
|
}
|
|
882
1791
|
}
|
|
883
1792
|
},
|
|
1793
|
+
{
|
|
1794
|
+
name: "pause_print",
|
|
1795
|
+
description: "Pause the current print job on the Bambu Lab printer (resumable via resume_print)",
|
|
1796
|
+
inputSchema: {
|
|
1797
|
+
type: "object",
|
|
1798
|
+
properties: {
|
|
1799
|
+
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
1800
|
+
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
1801
|
+
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
},
|
|
1805
|
+
{
|
|
1806
|
+
name: "resume_print",
|
|
1807
|
+
description: "Resume a paused print job on the Bambu Lab printer",
|
|
1808
|
+
inputSchema: {
|
|
1809
|
+
type: "object",
|
|
1810
|
+
properties: {
|
|
1811
|
+
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
1812
|
+
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
1813
|
+
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
},
|
|
1817
|
+
{
|
|
1818
|
+
name: "clear_hms_errors",
|
|
1819
|
+
description: "Clear HMS or print error state on the Bambu Lab printer using the clean_print_error MQTT command.",
|
|
1820
|
+
inputSchema: {
|
|
1821
|
+
type: "object",
|
|
1822
|
+
properties: {
|
|
1823
|
+
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
1824
|
+
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
1825
|
+
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
},
|
|
1829
|
+
{
|
|
1830
|
+
name: "set_print_speed",
|
|
1831
|
+
description: "Set the active print speed mode: silent, standard, sport, or ludicrous.",
|
|
1832
|
+
inputSchema: {
|
|
1833
|
+
type: "object",
|
|
1834
|
+
properties: {
|
|
1835
|
+
mode: {
|
|
1836
|
+
type: "string",
|
|
1837
|
+
enum: ["silent", "standard", "sport", "ludicrous", "1", "2", "3", "4"],
|
|
1838
|
+
description: "Speed mode to apply: silent/1, standard/2, sport/3, or ludicrous/4"
|
|
1839
|
+
},
|
|
1840
|
+
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
1841
|
+
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
1842
|
+
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
1843
|
+
},
|
|
1844
|
+
required: ["mode"]
|
|
1845
|
+
}
|
|
1846
|
+
},
|
|
1847
|
+
{
|
|
1848
|
+
name: "set_airduct_mode",
|
|
1849
|
+
description: "Set H2/P2 airduct mode to cooling or heating.",
|
|
1850
|
+
inputSchema: {
|
|
1851
|
+
type: "object",
|
|
1852
|
+
properties: {
|
|
1853
|
+
mode: {
|
|
1854
|
+
type: "string",
|
|
1855
|
+
enum: ["cooling", "heating"],
|
|
1856
|
+
description: "Airduct mode to apply"
|
|
1857
|
+
},
|
|
1858
|
+
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
1859
|
+
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
1860
|
+
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
1861
|
+
},
|
|
1862
|
+
required: ["mode"]
|
|
1863
|
+
}
|
|
1864
|
+
},
|
|
1865
|
+
{
|
|
1866
|
+
name: "reread_ams_rfid",
|
|
1867
|
+
description: "Trigger a Bambu AMS RFID re-read for one AMS slot. This can move AMS filament; use only when the printer is idle and unloaded.",
|
|
1868
|
+
inputSchema: {
|
|
1869
|
+
type: "object",
|
|
1870
|
+
properties: {
|
|
1871
|
+
ams_id: { type: "number", description: "AMS unit index from 0 to 3" },
|
|
1872
|
+
slot_id: { type: "number", description: "Slot index within that AMS, from 0 to 3" },
|
|
1873
|
+
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
1874
|
+
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
1875
|
+
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
1876
|
+
},
|
|
1877
|
+
required: ["ams_id", "slot_id"]
|
|
1878
|
+
}
|
|
1879
|
+
},
|
|
884
1880
|
{
|
|
885
1881
|
name: "set_temperature",
|
|
886
1882
|
description: "Set the temperature of a printer component (bed, nozzle)",
|
|
@@ -896,6 +1892,86 @@ class BambuPrinterMCPServer {
|
|
|
896
1892
|
required: ["component", "temperature"]
|
|
897
1893
|
}
|
|
898
1894
|
},
|
|
1895
|
+
{
|
|
1896
|
+
name: "set_fan_speed",
|
|
1897
|
+
description: "Set a Bambu printer fan speed percentage using the printer's MQTT fan command",
|
|
1898
|
+
inputSchema: {
|
|
1899
|
+
type: "object",
|
|
1900
|
+
properties: {
|
|
1901
|
+
fan: {
|
|
1902
|
+
type: "string",
|
|
1903
|
+
description: "Fan to control: part, auxiliary, chamber, 1, 2, or 3"
|
|
1904
|
+
},
|
|
1905
|
+
speed: { type: "number", description: "Fan speed percentage from 0 to 100" },
|
|
1906
|
+
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
1907
|
+
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
1908
|
+
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
1909
|
+
},
|
|
1910
|
+
required: ["fan", "speed"]
|
|
1911
|
+
}
|
|
1912
|
+
},
|
|
1913
|
+
{
|
|
1914
|
+
name: "set_light",
|
|
1915
|
+
description: "Set a Bambu printer light node mode using the printer's MQTT LED command",
|
|
1916
|
+
inputSchema: {
|
|
1917
|
+
type: "object",
|
|
1918
|
+
properties: {
|
|
1919
|
+
light: {
|
|
1920
|
+
type: "string",
|
|
1921
|
+
description: "Light node to control, for example chamber_light"
|
|
1922
|
+
},
|
|
1923
|
+
mode: {
|
|
1924
|
+
type: "string",
|
|
1925
|
+
enum: ["on", "off", "flashing"],
|
|
1926
|
+
description: "Light mode to apply"
|
|
1927
|
+
},
|
|
1928
|
+
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
1929
|
+
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
1930
|
+
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
1931
|
+
},
|
|
1932
|
+
required: ["light", "mode"]
|
|
1933
|
+
}
|
|
1934
|
+
},
|
|
1935
|
+
{
|
|
1936
|
+
name: "skip_objects",
|
|
1937
|
+
description: "Skip specific object IDs during a running multi-object print using the printer's MQTT skip_objects command",
|
|
1938
|
+
inputSchema: {
|
|
1939
|
+
type: "object",
|
|
1940
|
+
properties: {
|
|
1941
|
+
object_ids: {
|
|
1942
|
+
type: "array",
|
|
1943
|
+
description: "Object IDs to skip. Use list_3mf_plate_objects on the sliced 3MF to find IDs.",
|
|
1944
|
+
items: { type: "number" }
|
|
1945
|
+
},
|
|
1946
|
+
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
1947
|
+
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
1948
|
+
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
1949
|
+
},
|
|
1950
|
+
required: ["object_ids"]
|
|
1951
|
+
}
|
|
1952
|
+
},
|
|
1953
|
+
{
|
|
1954
|
+
name: "set_ams_drying",
|
|
1955
|
+
description: "Start or stop the AMS filament drying cycle. Available on AMS units with heating capability (AMS Pro / AMS-HT). Sends an ams_control MQTT command to the printer.",
|
|
1956
|
+
inputSchema: {
|
|
1957
|
+
type: "object",
|
|
1958
|
+
properties: {
|
|
1959
|
+
action: {
|
|
1960
|
+
type: "string",
|
|
1961
|
+
enum: ["start", "stop"],
|
|
1962
|
+
description: "Whether to start or stop the drying cycle"
|
|
1963
|
+
},
|
|
1964
|
+
ams_id: {
|
|
1965
|
+
type: "number",
|
|
1966
|
+
description: "AMS unit index from 0 to 3"
|
|
1967
|
+
},
|
|
1968
|
+
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
1969
|
+
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
1970
|
+
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
1971
|
+
},
|
|
1972
|
+
required: ["action", "ams_id"]
|
|
1973
|
+
}
|
|
1974
|
+
},
|
|
899
1975
|
{
|
|
900
1976
|
name: "print_3mf",
|
|
901
1977
|
description: "Print a 3MF file on a Bambu Lab printer. Auto-slices if the 3MF has no gcode. IMPORTANT: bambu_model must be specified to ensure safe printer operation.",
|
|
@@ -905,13 +1981,13 @@ class BambuPrinterMCPServer {
|
|
|
905
1981
|
three_mf_path: { type: "string", description: "Path to the 3MF file to print" },
|
|
906
1982
|
bambu_model: {
|
|
907
1983
|
type: "string",
|
|
908
|
-
enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
|
|
1984
|
+
enum: ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s"],
|
|
909
1985
|
description: "REQUIRED: Bambu Lab printer model. Ask the user if not known. Using the wrong model can damage the printer."
|
|
910
1986
|
},
|
|
911
1987
|
connection_mode: {
|
|
912
1988
|
type: "string",
|
|
913
1989
|
enum: ["lan_mqtt_ftps", "bambu_network"],
|
|
914
|
-
description: "Print path to use: lan_mqtt_ftps uses this MCP's direct local MQTT/FTPS path; bambu_network uses
|
|
1990
|
+
description: "Print path to use: lan_mqtt_ftps uses this MCP's direct local MQTT/FTPS path; bambu_network uses the restored FULU BambuNetwork bridge."
|
|
915
1991
|
},
|
|
916
1992
|
connection_type: { type: "string", enum: ["cloud", "lan"], description: "BambuNetwork connection type when connection_mode is bambu_network; cloud uses restored internet printing, lan uses local bridge printing." },
|
|
917
1993
|
bambu_network_method: { type: "string", enum: BAMBU_NETWORK_PRINT_METHODS, description: "FULU print method when connection_mode is bambu_network; defaults to start_print for cloud and start_local_print for lan." },
|
|
@@ -923,36 +1999,79 @@ class BambuPrinterMCPServer {
|
|
|
923
1999
|
user_info: { type: "string", description: "Optional BambuNetwork user_info JSON string passed to net.change_user for the FULU bridge." },
|
|
924
2000
|
bed_type: {
|
|
925
2001
|
type: "string",
|
|
926
|
-
enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate"],
|
|
2002
|
+
enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate", "supertack_plate"],
|
|
927
2003
|
description: "Bed plate type currently installed (default: textured_plate)"
|
|
928
2004
|
},
|
|
929
|
-
plate_index: { type: "number", description: "Zero-based plate index to print from the sliced 3MF (default: 0)" },
|
|
930
2005
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
931
2006
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
932
2007
|
bambu_token: { type: "string", description: "Access token (default: value from env)" },
|
|
2008
|
+
plate_index: { type: "number", description: "Zero-based plate index to print from the sliced 3MF (default: 0)" },
|
|
933
2009
|
slicer_type: {
|
|
934
2010
|
type: "string",
|
|
935
2011
|
enum: SLICER_SCHEMA_VALUES,
|
|
936
|
-
description: "Slicer to use only if auto-slicing an unsliced 3MF.
|
|
2012
|
+
description: "Slicer to use only if auto-slicing an unsliced 3MF. Bambu-compatible slicer aliases such as fulu-orca and orca-studio are accepted."
|
|
937
2013
|
},
|
|
938
2014
|
slicer_path: { type: "string", description: "Path to the slicer executable for auto-slicing (default: value from env or a platform default)" },
|
|
939
|
-
slicer_profile: { type: "string", description: "Path to an optional slicer profile/config file for auto-slicing" },
|
|
940
2015
|
use_ams: { type: "boolean", description: "Whether to use the AMS (default: auto-detect from 3MF)" },
|
|
941
2016
|
ams_mapping: {
|
|
942
2017
|
type: "array",
|
|
943
|
-
description: "AMS
|
|
2018
|
+
description: "Project-level AMS mapping array. Position = project filament index, value = absolute AMS tray (0-3=AMS 0, 4-7=AMS 1, 8-11=AMS 2, 128+=AMS-HT, 254=external, -1=unused). Prefer ams_slots unless you know the project-level layout.",
|
|
944
2019
|
items: { type: "number" }
|
|
945
2020
|
},
|
|
2021
|
+
ams_slots: {
|
|
2022
|
+
type: "array",
|
|
2023
|
+
description: "Preferred AMS input: one absolute tray index per USED filament in plate order, e.g. [1] for a single-filament print pulling from AMS 0 slot 1. Expanded to project-level ams_mapping automatically from the 3MF's plate_N.json and gcode header.",
|
|
2024
|
+
items: { type: "number" }
|
|
2025
|
+
},
|
|
2026
|
+
auto_match_ams: {
|
|
2027
|
+
type: "boolean",
|
|
2028
|
+
description: "When true, match the sliced 3MF's tray_info_idx requirements against live AMS inventory and use the resulting ams_slots. Ignored when ams_mapping or ams_slots is provided."
|
|
2029
|
+
},
|
|
946
2030
|
bed_leveling: { type: "boolean", description: "Enable auto bed leveling (default: true)" },
|
|
947
2031
|
flow_calibration: { type: "boolean", description: "Enable flow calibration (default: true)" },
|
|
948
2032
|
vibration_calibration: { type: "boolean", description: "Enable vibration calibration (default: true)" },
|
|
949
|
-
layer_inspect: { type: "boolean", description: "Enable first-layer inspection where supported (default: printer/profile behavior)" },
|
|
950
2033
|
timelapse: { type: "boolean", description: "Enable timelapse recording (default: false)" },
|
|
2034
|
+
slicer_profile: { type: "string", description: "Path to the slicer profile/config file for auto-slicing (optional)." },
|
|
2035
|
+
template_3mf_path: { type: "string", description: "Optional template 3MF whose embedded Bambu slicer settings should be reused when auto-slicing this print job." },
|
|
2036
|
+
template_name: { type: "string", description: "Optional named template from the local registry. Resolves to template_3mf_path automatically." },
|
|
2037
|
+
template_dir: { type: "string", description: "Optional template directory override when resolving template_name." },
|
|
951
2038
|
nozzle_diameter: { type: "string", description: "Nozzle diameter in mm for auto-slicing (default: 0.4)" }
|
|
952
2039
|
},
|
|
953
2040
|
required: ["three_mf_path", "bambu_model"]
|
|
954
2041
|
}
|
|
955
2042
|
},
|
|
2043
|
+
{
|
|
2044
|
+
name: "print_collar_charm",
|
|
2045
|
+
description: "Print a prepared two-part dog collar charm project using the fixed tray policy: inner/smaller object -> black on AMS 1 slot 1, outer/larger object -> white on AMS 2 slot 1.",
|
|
2046
|
+
inputSchema: {
|
|
2047
|
+
type: "object",
|
|
2048
|
+
properties: {
|
|
2049
|
+
source_path: { type: "string", description: "Path to a prepared collar charm .3mf project or sliced 3MF. Required unless template_name is provided." },
|
|
2050
|
+
template_name: { type: "string", description: "Named collar charm template from the local registry. Required unless source_path is provided; resolves source_path automatically." },
|
|
2051
|
+
template_dir: { type: "string", description: "Optional template directory override when resolving template_name." },
|
|
2052
|
+
bambu_model: {
|
|
2053
|
+
type: "string",
|
|
2054
|
+
enum: ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s"],
|
|
2055
|
+
description: "REQUIRED: Bambu Lab printer model. H2D and H2S are the primary intended paths."
|
|
2056
|
+
},
|
|
2057
|
+
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2058
|
+
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
2059
|
+
bambu_token: { type: "string", description: "Access token (default: value from env)" },
|
|
2060
|
+
bed_type: {
|
|
2061
|
+
type: "string",
|
|
2062
|
+
enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate", "supertack_plate"],
|
|
2063
|
+
description: "Bed plate type currently installed (default: textured_plate)"
|
|
2064
|
+
},
|
|
2065
|
+
bed_leveling: { type: "boolean", description: "Enable auto bed leveling (default: true)" },
|
|
2066
|
+
flow_calibration: { type: "boolean", description: "Enable flow calibration (default: true)" },
|
|
2067
|
+
vibration_calibration: { type: "boolean", description: "Enable vibration calibration (default: true)" },
|
|
2068
|
+
timelapse: { type: "boolean", description: "Enable timelapse recording (default: false)" },
|
|
2069
|
+
slicer_profile: { type: "string", description: "Path to the slicer profile/config file for auto-slicing (optional)." },
|
|
2070
|
+
nozzle_diameter: { type: "string", description: "Nozzle diameter in mm for auto-slicing (default: 0.4)" }
|
|
2071
|
+
},
|
|
2072
|
+
required: ["bambu_model"]
|
|
2073
|
+
}
|
|
2074
|
+
},
|
|
956
2075
|
{
|
|
957
2076
|
name: "merge_vertices",
|
|
958
2077
|
description: "Merge vertices in an STL file closer than the specified tolerance",
|
|
@@ -1013,12 +2132,60 @@ class BambuPrinterMCPServer {
|
|
|
1013
2132
|
const host = String(args?.host || DEFAULT_HOST);
|
|
1014
2133
|
const bambuSerial = String(args?.bambu_serial || DEFAULT_BAMBU_SERIAL);
|
|
1015
2134
|
const bambuToken = String(args?.bambu_token || DEFAULT_BAMBU_TOKEN);
|
|
2135
|
+
const requestedTemplateDir = typeof args?.template_dir === "string" && args.template_dir.trim().length > 0
|
|
2136
|
+
? String(args.template_dir)
|
|
2137
|
+
: undefined;
|
|
2138
|
+
const requestedTemplateName = typeof args?.template_name === "string" ? String(args.template_name) : undefined;
|
|
2139
|
+
const resolveTemplatePathFromName = (sourceTypes) => this.resolveTemplatePath(requestedTemplateName, requestedTemplateDir, sourceTypes);
|
|
2140
|
+
const explicitTemplatePath = String(args?.template_3mf_path || DEFAULT_TEMPLATE_3MF_PATH);
|
|
1016
2141
|
try {
|
|
1017
2142
|
let result;
|
|
1018
2143
|
switch (name) {
|
|
1019
2144
|
case "get_printer_status":
|
|
1020
2145
|
result = await this.bambu.getStatus(host, bambuSerial, bambuToken);
|
|
1021
2146
|
break;
|
|
2147
|
+
case "get_printer_filaments": {
|
|
2148
|
+
const requestedModel = (String(args?.bambu_model ?? DEFAULT_BAMBU_MODEL ?? "")).trim().toLowerCase();
|
|
2149
|
+
const normalizedModel = requestedModel ? validateBambuModel(requestedModel) : undefined;
|
|
2150
|
+
const nozzleDiam = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
2151
|
+
result = await this.getResolvedPrinterFilamentInventory(host, bambuSerial, bambuToken, normalizedModel, nozzleDiam);
|
|
2152
|
+
break;
|
|
2153
|
+
}
|
|
2154
|
+
case "resolve_3mf_ams_slots": {
|
|
2155
|
+
if (!args?.three_mf_path) {
|
|
2156
|
+
throw new Error("Missing required parameter: three_mf_path");
|
|
2157
|
+
}
|
|
2158
|
+
const requestedModel = (String(args?.bambu_model ?? DEFAULT_BAMBU_MODEL ?? "")).trim().toLowerCase();
|
|
2159
|
+
const normalizedModel = requestedModel ? validateBambuModel(requestedModel) : undefined;
|
|
2160
|
+
const nozzleDiam = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
2161
|
+
const requirements = await analyze3MFAmsRequirements(String(args.three_mf_path), args?.plate_index !== undefined ? Number(args.plate_index) : 0);
|
|
2162
|
+
const inventory = await this.getResolvedPrinterFilamentInventory(host, bambuSerial, bambuToken, normalizedModel, nozzleDiam);
|
|
2163
|
+
const resolved = resolveAmsSlotsFromRequirements(requirements, inventory);
|
|
2164
|
+
result = {
|
|
2165
|
+
status: resolved.missing.length === 0 ? "matched" : "missing",
|
|
2166
|
+
requirements,
|
|
2167
|
+
ams_slots: resolved.ams_slots,
|
|
2168
|
+
matches: resolved.matches,
|
|
2169
|
+
missing: resolved.missing,
|
|
2170
|
+
};
|
|
2171
|
+
break;
|
|
2172
|
+
}
|
|
2173
|
+
case "list_3mf_plate_objects": {
|
|
2174
|
+
if (!args?.three_mf_path) {
|
|
2175
|
+
throw new Error("Missing required parameter: three_mf_path");
|
|
2176
|
+
}
|
|
2177
|
+
result = await analyze3MFPlateObjects(String(args.three_mf_path), args?.plate_index !== undefined ? Number(args.plate_index) : 0);
|
|
2178
|
+
break;
|
|
2179
|
+
}
|
|
2180
|
+
case "list_templates":
|
|
2181
|
+
result = this.listTemplateRegistry(requestedTemplateDir);
|
|
2182
|
+
break;
|
|
2183
|
+
case "save_template":
|
|
2184
|
+
if (!args?.source_path) {
|
|
2185
|
+
throw new Error("Missing required parameter: source_path");
|
|
2186
|
+
}
|
|
2187
|
+
result = this.saveTemplate(String(args.source_path), typeof args?.template_name === "string" ? String(args.template_name) : undefined, requestedTemplateDir);
|
|
2188
|
+
break;
|
|
1022
2189
|
case "list_printer_files":
|
|
1023
2190
|
result = await this.bambu.getFiles(host, bambuSerial, bambuToken);
|
|
1024
2191
|
break;
|
|
@@ -1055,6 +2222,23 @@ class BambuPrinterMCPServer {
|
|
|
1055
2222
|
result = await this.print3mfViaBambuNetwork(args, host, bambuSerial, bambuToken);
|
|
1056
2223
|
break;
|
|
1057
2224
|
}
|
|
2225
|
+
case "camera_snapshot": {
|
|
2226
|
+
const snapshotModel = args?.bambu_model ?? process.env.BAMBU_MODEL ?? process.env.BAMBU_PRINTER_MODEL;
|
|
2227
|
+
result = await this.bambu.cameraSnapshot(host, bambuSerial, bambuToken, {
|
|
2228
|
+
savePath: args?.save_path ? String(args.save_path) : undefined,
|
|
2229
|
+
timeoutMs: args?.timeout_ms !== undefined ? Number(args.timeout_ms) : undefined,
|
|
2230
|
+
bambuModel: snapshotModel ? String(snapshotModel) : undefined,
|
|
2231
|
+
experimental: Boolean(args?.experimental),
|
|
2232
|
+
ffmpegPath: args?.ffmpeg_path ? String(args.ffmpeg_path) : undefined,
|
|
2233
|
+
});
|
|
2234
|
+
break;
|
|
2235
|
+
}
|
|
2236
|
+
case "delete_printer_file":
|
|
2237
|
+
if (!args?.filename) {
|
|
2238
|
+
throw new Error("Missing required parameter: filename");
|
|
2239
|
+
}
|
|
2240
|
+
result = await this.bambu.deleteFile(host, bambuSerial, bambuToken, String(args.filename), args.confirm);
|
|
2241
|
+
break;
|
|
1058
2242
|
case "upload_gcode": {
|
|
1059
2243
|
if (!args?.filename) {
|
|
1060
2244
|
throw new Error("Missing required parameter: filename");
|
|
@@ -1090,12 +2274,63 @@ class BambuPrinterMCPServer {
|
|
|
1090
2274
|
case "cancel_print":
|
|
1091
2275
|
result = await this.bambu.cancelJob(host, bambuSerial, bambuToken);
|
|
1092
2276
|
break;
|
|
2277
|
+
case "pause_print":
|
|
2278
|
+
result = await this.bambu.pauseJob(host, bambuSerial, bambuToken);
|
|
2279
|
+
break;
|
|
2280
|
+
case "resume_print":
|
|
2281
|
+
result = await this.bambu.resumeJob(host, bambuSerial, bambuToken);
|
|
2282
|
+
break;
|
|
2283
|
+
case "clear_hms_errors":
|
|
2284
|
+
result = await this.bambu.clearHmsErrors(host, bambuSerial, bambuToken);
|
|
2285
|
+
break;
|
|
2286
|
+
case "set_print_speed":
|
|
2287
|
+
if (!args?.mode) {
|
|
2288
|
+
throw new Error("Missing required parameter: mode");
|
|
2289
|
+
}
|
|
2290
|
+
result = await this.bambu.setPrintSpeed(host, bambuSerial, bambuToken, String(args.mode));
|
|
2291
|
+
break;
|
|
2292
|
+
case "set_airduct_mode":
|
|
2293
|
+
if (!args?.mode) {
|
|
2294
|
+
throw new Error("Missing required parameter: mode");
|
|
2295
|
+
}
|
|
2296
|
+
result = await this.bambu.setAirductMode(host, bambuSerial, bambuToken, String(args.mode));
|
|
2297
|
+
break;
|
|
2298
|
+
case "reread_ams_rfid":
|
|
2299
|
+
if (args?.ams_id === undefined || args?.slot_id === undefined) {
|
|
2300
|
+
throw new Error("Missing required parameters: ams_id and slot_id");
|
|
2301
|
+
}
|
|
2302
|
+
result = await this.bambu.rereadAmsRfid(host, bambuSerial, bambuToken, Number(args.ams_id), Number(args.slot_id));
|
|
2303
|
+
break;
|
|
1093
2304
|
case "set_temperature":
|
|
1094
2305
|
if (!args?.component || args?.temperature === undefined) {
|
|
1095
2306
|
throw new Error("Missing required parameters: component and temperature");
|
|
1096
2307
|
}
|
|
1097
2308
|
result = await this.bambu.setTemperature(host, bambuSerial, bambuToken, String(args.component), Number(args.temperature));
|
|
1098
2309
|
break;
|
|
2310
|
+
case "set_fan_speed":
|
|
2311
|
+
if (!args?.fan || args?.speed === undefined) {
|
|
2312
|
+
throw new Error("Missing required parameters: fan and speed");
|
|
2313
|
+
}
|
|
2314
|
+
result = await this.bambu.setFanSpeed(host, bambuSerial, bambuToken, String(args.fan), Number(args.speed));
|
|
2315
|
+
break;
|
|
2316
|
+
case "set_light":
|
|
2317
|
+
if (!args?.light || !args?.mode) {
|
|
2318
|
+
throw new Error("Missing required parameters: light and mode");
|
|
2319
|
+
}
|
|
2320
|
+
result = await this.bambu.setLight(host, bambuSerial, bambuToken, String(args.light), String(args.mode));
|
|
2321
|
+
break;
|
|
2322
|
+
case "skip_objects":
|
|
2323
|
+
if (!Array.isArray(args?.object_ids)) {
|
|
2324
|
+
throw new Error("Missing required parameter: object_ids");
|
|
2325
|
+
}
|
|
2326
|
+
result = await this.bambu.skipObjects(host, bambuSerial, bambuToken, args.object_ids.map((id) => Number(id)));
|
|
2327
|
+
break;
|
|
2328
|
+
case "set_ams_drying":
|
|
2329
|
+
if (!args?.action || args?.ams_id === undefined) {
|
|
2330
|
+
throw new Error("Missing required parameters: action and ams_id");
|
|
2331
|
+
}
|
|
2332
|
+
result = await this.bambu.setAmsDrying(host, bambuSerial, bambuToken, String(args.action), Number(args.ams_id));
|
|
2333
|
+
break;
|
|
1099
2334
|
case "extend_stl_base":
|
|
1100
2335
|
if (!args?.stl_path || args?.extension_height === undefined) {
|
|
1101
2336
|
throw new Error("Missing required parameters: stl_path and extension_height");
|
|
@@ -1128,6 +2363,91 @@ class BambuPrinterMCPServer {
|
|
|
1128
2363
|
}
|
|
1129
2364
|
result = await this.stlManipulator.getSTLInfo(String(args.stl_path));
|
|
1130
2365
|
break;
|
|
2366
|
+
case "get_slice_settings":
|
|
2367
|
+
if (!args?.source_path && !args?.template_name) {
|
|
2368
|
+
throw new Error("Missing required parameter: source_path or template_name");
|
|
2369
|
+
}
|
|
2370
|
+
result = await this.inspectSliceSettings(String(args?.source_path || resolveTemplatePathFromName(["3mf", "json", "config"])));
|
|
2371
|
+
break;
|
|
2372
|
+
case "slice_with_template": {
|
|
2373
|
+
if (!args?.stl_path) {
|
|
2374
|
+
throw new Error("Missing required parameter: stl_path");
|
|
2375
|
+
}
|
|
2376
|
+
if (!args?.template_name) {
|
|
2377
|
+
throw new Error("Missing required parameter: template_name");
|
|
2378
|
+
}
|
|
2379
|
+
const { slicerType, slicerPath, slicerProfile } = resolveSlicerConfig(args);
|
|
2380
|
+
const sliceModel = await this.resolveBambuModel(args?.bambu_model);
|
|
2381
|
+
const nozzleDiam = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
2382
|
+
const activeSlicerProfile = await resolveTemplateFirstSlicerProfilePath(args, slicerProfile || undefined, resolveTemplatePathFromName(["json", "config", "3mf"]) || explicitTemplatePath || undefined, TEMP_DIR);
|
|
2383
|
+
const explicitSlicerProfile = hasExplicitSlicerProfile(args);
|
|
2384
|
+
const printerPreset = BAMBU_MODEL_PRESETS[sliceModel]?.(nozzleDiam);
|
|
2385
|
+
const sliceBambuOptions = {};
|
|
2386
|
+
if (args?.uptodate !== undefined)
|
|
2387
|
+
sliceBambuOptions.uptodate = Boolean(args.uptodate);
|
|
2388
|
+
if (args?.repetitions !== undefined)
|
|
2389
|
+
sliceBambuOptions.repetitions = Number(args.repetitions);
|
|
2390
|
+
if (args?.orient !== undefined)
|
|
2391
|
+
sliceBambuOptions.orient = Boolean(args.orient);
|
|
2392
|
+
if (args?.arrange !== undefined)
|
|
2393
|
+
sliceBambuOptions.arrange = Boolean(args.arrange);
|
|
2394
|
+
if (args?.ensure_on_bed !== undefined)
|
|
2395
|
+
sliceBambuOptions.ensureOnBed = Boolean(args.ensure_on_bed);
|
|
2396
|
+
if (args?.clone_objects !== undefined)
|
|
2397
|
+
sliceBambuOptions.cloneObjects = String(args.clone_objects);
|
|
2398
|
+
if (args?.skip_objects !== undefined)
|
|
2399
|
+
sliceBambuOptions.skipObjects = String(args.skip_objects);
|
|
2400
|
+
if (args?.load_filaments !== undefined &&
|
|
2401
|
+
args?.filament_profile !== undefined &&
|
|
2402
|
+
String(args.load_filaments) !== String(args.filament_profile)) {
|
|
2403
|
+
throw new Error("Provide either load_filaments or filament_profile, not conflicting values.");
|
|
2404
|
+
}
|
|
2405
|
+
if (args?.load_filaments !== undefined) {
|
|
2406
|
+
sliceBambuOptions.loadFilaments = String(args.load_filaments);
|
|
2407
|
+
}
|
|
2408
|
+
else if (args?.filament_profile !== undefined) {
|
|
2409
|
+
sliceBambuOptions.loadFilaments = String(args.filament_profile);
|
|
2410
|
+
}
|
|
2411
|
+
if (args?.load_filament_ids !== undefined)
|
|
2412
|
+
sliceBambuOptions.loadFilamentIds = String(args.load_filament_ids);
|
|
2413
|
+
sliceBambuOptions.bedType = resolveBambuStudioCliBedType(args?.bed_type);
|
|
2414
|
+
if (args?.enable_timelapse !== undefined)
|
|
2415
|
+
sliceBambuOptions.enableTimelapse = Boolean(args.enable_timelapse);
|
|
2416
|
+
if (args?.allow_mix_temp !== undefined)
|
|
2417
|
+
sliceBambuOptions.allowMixTemp = Boolean(args.allow_mix_temp);
|
|
2418
|
+
if (args?.scale !== undefined)
|
|
2419
|
+
sliceBambuOptions.scale = Number(args.scale);
|
|
2420
|
+
if (args?.rotate !== undefined)
|
|
2421
|
+
sliceBambuOptions.rotate = Number(args.rotate);
|
|
2422
|
+
if (args?.rotate_x !== undefined)
|
|
2423
|
+
sliceBambuOptions.rotateX = Number(args.rotate_x);
|
|
2424
|
+
if (args?.rotate_y !== undefined)
|
|
2425
|
+
sliceBambuOptions.rotateY = Number(args.rotate_y);
|
|
2426
|
+
if (args?.min_save !== undefined)
|
|
2427
|
+
sliceBambuOptions.minSave = Boolean(args.min_save);
|
|
2428
|
+
if (args?.skip_modified_gcodes !== undefined)
|
|
2429
|
+
sliceBambuOptions.skipModifiedGcodes = Boolean(args.skip_modified_gcodes);
|
|
2430
|
+
if (args?.slice_plate !== undefined)
|
|
2431
|
+
sliceBambuOptions.slicePlate = Number(args.slice_plate);
|
|
2432
|
+
const usePrinterFilaments = args?.use_printer_filaments !== undefined ? Boolean(args.use_printer_filaments) : true;
|
|
2433
|
+
if (usePrinterFilaments &&
|
|
2434
|
+
!explicitSlicerProfile &&
|
|
2435
|
+
!sliceBambuOptions.loadFilaments &&
|
|
2436
|
+
bambuSerial &&
|
|
2437
|
+
bambuToken) {
|
|
2438
|
+
try {
|
|
2439
|
+
const liveFilaments = await this.getResolvedPrinterFilamentInventory(host, bambuSerial, bambuToken, sliceModel, nozzleDiam);
|
|
2440
|
+
if (liveFilaments.recommended?.load_filaments) {
|
|
2441
|
+
sliceBambuOptions.loadFilaments = liveFilaments.recommended.load_filaments;
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
catch (filamentError) {
|
|
2445
|
+
console.warn("Could not resolve live printer filaments for slicing:", filamentError);
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
result = await this.stlManipulator.sliceSTL(String(args.stl_path), slicerType, slicerPath, activeSlicerProfile, undefined, printerPreset, sliceBambuOptions);
|
|
2449
|
+
break;
|
|
2450
|
+
}
|
|
1131
2451
|
case "slice_stl": {
|
|
1132
2452
|
if (!args?.stl_path) {
|
|
1133
2453
|
throw new Error("Missing required parameter: stl_path");
|
|
@@ -1135,6 +2455,8 @@ class BambuPrinterMCPServer {
|
|
|
1135
2455
|
const { slicerType, slicerPath, slicerProfile } = resolveSlicerConfig(args);
|
|
1136
2456
|
const sliceModel = await this.resolveBambuModel(args?.bambu_model);
|
|
1137
2457
|
const nozzleDiam = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
2458
|
+
const activeSlicerProfile = await resolveSlicerProfilePath(slicerProfile || undefined, resolveTemplatePathFromName(["json", "config", "3mf"]) || explicitTemplatePath || undefined, TEMP_DIR);
|
|
2459
|
+
const explicitSlicerProfile = hasExplicitSlicerProfile(args);
|
|
1138
2460
|
// Resolve printer preset for BambuStudio slicer
|
|
1139
2461
|
const printerPreset = BAMBU_MODEL_PRESETS[sliceModel]?.(nozzleDiam);
|
|
1140
2462
|
const sliceBambuOptions = {};
|
|
@@ -1165,6 +2487,7 @@ class BambuPrinterMCPServer {
|
|
|
1165
2487
|
}
|
|
1166
2488
|
if (args?.load_filament_ids !== undefined)
|
|
1167
2489
|
sliceBambuOptions.loadFilamentIds = String(args.load_filament_ids);
|
|
2490
|
+
sliceBambuOptions.bedType = resolveBambuStudioCliBedType(args?.bed_type);
|
|
1168
2491
|
if (args?.enable_timelapse !== undefined)
|
|
1169
2492
|
sliceBambuOptions.enableTimelapse = Boolean(args.enable_timelapse);
|
|
1170
2493
|
if (args?.allow_mix_temp !== undefined)
|
|
@@ -1183,7 +2506,23 @@ class BambuPrinterMCPServer {
|
|
|
1183
2506
|
sliceBambuOptions.skipModifiedGcodes = Boolean(args.skip_modified_gcodes);
|
|
1184
2507
|
if (args?.slice_plate !== undefined)
|
|
1185
2508
|
sliceBambuOptions.slicePlate = Number(args.slice_plate);
|
|
1186
|
-
|
|
2509
|
+
const usePrinterFilaments = args?.use_printer_filaments !== undefined ? Boolean(args.use_printer_filaments) : true;
|
|
2510
|
+
if (usePrinterFilaments &&
|
|
2511
|
+
!explicitSlicerProfile &&
|
|
2512
|
+
!sliceBambuOptions.loadFilaments &&
|
|
2513
|
+
bambuSerial &&
|
|
2514
|
+
bambuToken) {
|
|
2515
|
+
try {
|
|
2516
|
+
const liveFilaments = await this.getResolvedPrinterFilamentInventory(host, bambuSerial, bambuToken, sliceModel, nozzleDiam);
|
|
2517
|
+
if (liveFilaments.recommended?.load_filaments) {
|
|
2518
|
+
sliceBambuOptions.loadFilaments = liveFilaments.recommended.load_filaments;
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
catch (filamentError) {
|
|
2522
|
+
console.warn("Could not resolve live printer filaments for slicing:", filamentError);
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
result = await this.stlManipulator.sliceSTL(String(args.stl_path), slicerType, slicerPath, activeSlicerProfile, undefined, // progressCallback
|
|
1187
2526
|
printerPreset, sliceBambuOptions);
|
|
1188
2527
|
break;
|
|
1189
2528
|
}
|
|
@@ -1202,6 +2541,8 @@ class BambuPrinterMCPServer {
|
|
|
1202
2541
|
const printModel = await this.resolveBambuModel(args?.bambu_model);
|
|
1203
2542
|
const printBedType = resolveBedType(args?.bed_type);
|
|
1204
2543
|
const printNozzle = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
2544
|
+
const activeSlicerProfile = await resolveTemplateFirstSlicerProfilePath(args, slicerProfile || undefined, resolveTemplatePathFromName(["json", "config", "3mf"]) || explicitTemplatePath || undefined, TEMP_DIR);
|
|
2545
|
+
const explicitSlicerProfile = hasExplicitSlicerProfile(args);
|
|
1205
2546
|
const printPreset = BAMBU_MODEL_PRESETS[printModel]?.(printNozzle);
|
|
1206
2547
|
const plateIndex = args?.plate_index !== undefined ? Number(args.plate_index) : 0;
|
|
1207
2548
|
if (!Number.isInteger(plateIndex) || plateIndex < 0) {
|
|
@@ -1215,53 +2556,104 @@ class BambuPrinterMCPServer {
|
|
|
1215
2556
|
const zip = await JSZip.loadAsync(zipData);
|
|
1216
2557
|
const hasGcode = Object.keys(zip.files).some(f => f.match(/Metadata\/plate_\d+\.gcode/i) || f.endsWith('.gcode'));
|
|
1217
2558
|
if (!hasGcode) {
|
|
1218
|
-
|
|
2559
|
+
if (printBedType === "supertack_plate") {
|
|
2560
|
+
throw new Error('BambuStudio CLI SuperTack bed type is not verified; use a pre-sliced 3MF for SuperTack or choose textured_plate, cool_plate, engineering_plate, or hot_plate.');
|
|
2561
|
+
}
|
|
2562
|
+
console.log(`3MF has no gcode — auto-slicing with ${slicerType} for ${printModel}`);
|
|
1219
2563
|
const autoSliceOptions = {
|
|
1220
2564
|
uptodate: true,
|
|
1221
2565
|
ensureOnBed: true,
|
|
1222
2566
|
minSave: true,
|
|
1223
2567
|
skipModifiedGcodes: true,
|
|
2568
|
+
bedType: printBedType,
|
|
1224
2569
|
};
|
|
1225
|
-
|
|
2570
|
+
if (!explicitSlicerProfile) {
|
|
2571
|
+
try {
|
|
2572
|
+
const liveFilaments = await this.getResolvedPrinterFilamentInventory(host, bambuSerial, bambuToken, printModel, printNozzle);
|
|
2573
|
+
if (liveFilaments.recommended?.load_filaments) {
|
|
2574
|
+
autoSliceOptions.loadFilaments = liveFilaments.recommended.load_filaments;
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
catch (filamentError) {
|
|
2578
|
+
console.warn("Could not resolve live printer filaments for auto-slicing:", filamentError);
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
threeMFPath = await this.stlManipulator.sliceSTL(threeMFPath, slicerType, slicerPath, activeSlicerProfile, undefined, // progressCallback
|
|
1226
2582
|
printPreset, autoSliceOptions);
|
|
1227
2583
|
console.log("Auto-sliced to: " + threeMFPath);
|
|
1228
2584
|
}
|
|
1229
2585
|
}
|
|
1230
2586
|
catch (sliceCheckErr) {
|
|
2587
|
+
if (String(sliceCheckErr?.message || "").includes("SuperTack")) {
|
|
2588
|
+
throw sliceCheckErr;
|
|
2589
|
+
}
|
|
1231
2590
|
console.warn("Could not check/slice 3MF, proceeding with original:", sliceCheckErr.message);
|
|
1232
2591
|
}
|
|
1233
2592
|
const parsed3MFData = await parse3MF(threeMFPath);
|
|
2593
|
+
const isH2Print = printModel === "h2s" || printModel === "h2d";
|
|
1234
2594
|
let parsedAmsMapping;
|
|
1235
|
-
if (parsed3MFData.slicerConfig?.ams_mapping) {
|
|
1236
|
-
const slots =
|
|
1237
|
-
.filter(v => typeof v === 'number');
|
|
2595
|
+
if (!isH2Print && parsed3MFData.slicerConfig?.ams_mapping) {
|
|
2596
|
+
const slots = normalizeAmsMappingObject(parsed3MFData.slicerConfig.ams_mapping);
|
|
1238
2597
|
if (slots.length > 0) {
|
|
1239
|
-
parsedAmsMapping = slots
|
|
2598
|
+
parsedAmsMapping = slots;
|
|
1240
2599
|
}
|
|
1241
2600
|
}
|
|
1242
2601
|
let finalAmsMapping = parsedAmsMapping;
|
|
1243
|
-
|
|
1244
|
-
let useAMS =
|
|
1245
|
-
|
|
2602
|
+
let finalAmsSlots;
|
|
2603
|
+
let useAMS = args?.use_ams !== undefined ? Boolean(args.use_ams) : (!!finalAmsMapping && finalAmsMapping.length > 0);
|
|
2604
|
+
const hasUserAmsMapping = hasAmsMappingInput(args?.ams_mapping);
|
|
2605
|
+
const hasUserAmsSlots = Array.isArray(args?.ams_slots);
|
|
2606
|
+
if (hasUserAmsMapping) {
|
|
1246
2607
|
let userMappingOverride;
|
|
1247
2608
|
if (Array.isArray(args.ams_mapping)) {
|
|
1248
|
-
userMappingOverride = args.ams_mapping.
|
|
2609
|
+
userMappingOverride = args.ams_mapping.map((v, i) => normalizeBridgeAmsTrayValue(v, `ams_mapping[${i}]`));
|
|
1249
2610
|
}
|
|
1250
|
-
else if (typeof args.ams_mapping === 'object') {
|
|
1251
|
-
userMappingOverride =
|
|
1252
|
-
.filter((v) => typeof v === 'number')
|
|
1253
|
-
.sort((a, b) => a - b);
|
|
2611
|
+
else if (args.ams_mapping && typeof args.ams_mapping === 'object') {
|
|
2612
|
+
userMappingOverride = normalizeAmsMappingObject(args.ams_mapping);
|
|
1254
2613
|
}
|
|
1255
2614
|
if (userMappingOverride && userMappingOverride.length > 0) {
|
|
1256
2615
|
finalAmsMapping = userMappingOverride;
|
|
1257
2616
|
useAMS = true;
|
|
1258
2617
|
}
|
|
1259
2618
|
}
|
|
2619
|
+
if (hasUserAmsSlots) {
|
|
2620
|
+
const userSlots = args.ams_slots.filter((v) => typeof v === 'number');
|
|
2621
|
+
if (userSlots.length > 0) {
|
|
2622
|
+
finalAmsSlots = userSlots;
|
|
2623
|
+
finalAmsMapping = undefined;
|
|
2624
|
+
useAMS = true;
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
if (!hasUserAmsMapping && !hasUserAmsSlots && args?.auto_match_ams === true && args?.use_ams !== false) {
|
|
2628
|
+
const requirements = await analyze3MFAmsRequirements(threeMFPath, plateIndex);
|
|
2629
|
+
const inventory = await this.getResolvedPrinterFilamentInventory(host, bambuSerial, bambuToken, printModel, printNozzle);
|
|
2630
|
+
const resolved = resolveAmsSlotsFromRequirements(requirements, inventory);
|
|
2631
|
+
if (resolved.missing.length > 0) {
|
|
2632
|
+
throw new Error(`auto_match_ams could not find loaded AMS trays for: ${resolved.missing
|
|
2633
|
+
.map((missing) => missing.tray_info_idx || `filament position ${missing.filament_position}`)
|
|
2634
|
+
.join(", ")}`);
|
|
2635
|
+
}
|
|
2636
|
+
finalAmsMapping = undefined;
|
|
2637
|
+
finalAmsSlots = resolved.ams_slots;
|
|
2638
|
+
useAMS = finalAmsSlots.length > 0;
|
|
2639
|
+
}
|
|
2640
|
+
if (isH2Print &&
|
|
2641
|
+
!hasUserAmsMapping &&
|
|
2642
|
+
!hasUserAmsSlots &&
|
|
2643
|
+
args?.auto_match_ams !== true &&
|
|
2644
|
+
(!finalAmsMapping || finalAmsMapping.length === 0) &&
|
|
2645
|
+
(!finalAmsSlots || finalAmsSlots.length === 0)) {
|
|
2646
|
+
const requirements = await analyze3MFAmsRequirements(threeMFPath, plateIndex);
|
|
2647
|
+
if (requirements.usedFilamentPositions.length > 0) {
|
|
2648
|
+
throw new Error(`H2 ${printModel.toUpperCase()} pre-sliced jobs with declared filaments require ams_slots, ams_mapping, or auto_match_ams: true. Plate uses project filament positions ${JSON.stringify(requirements.usedFilamentPositions)}; pass one physical tray per used filament, for example ams_slots: [0] for AMS 0 slot 0 or [1] for AMS 0 slot 1.`);
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
1260
2651
|
if (args?.use_ams === false) {
|
|
1261
2652
|
finalAmsMapping = undefined;
|
|
2653
|
+
finalAmsSlots = undefined;
|
|
1262
2654
|
useAMS = false;
|
|
1263
2655
|
}
|
|
1264
|
-
if ((!finalAmsMapping || finalAmsMapping.length === 0) && !
|
|
2656
|
+
if ((!finalAmsMapping || finalAmsMapping.length === 0) && (!finalAmsSlots || finalAmsSlots.length === 0)) {
|
|
1265
2657
|
useAMS = false;
|
|
1266
2658
|
}
|
|
1267
2659
|
const threeMfFilename = path.basename(threeMFPath);
|
|
@@ -1272,6 +2664,7 @@ class BambuPrinterMCPServer {
|
|
|
1272
2664
|
plateIndex,
|
|
1273
2665
|
useAMS: useAMS,
|
|
1274
2666
|
amsMapping: finalAmsMapping,
|
|
2667
|
+
amsSlots: finalAmsSlots,
|
|
1275
2668
|
bedType: printBedType,
|
|
1276
2669
|
bedLeveling: args?.bed_leveling !== undefined ? Boolean(args.bed_leveling) : undefined,
|
|
1277
2670
|
flowCalibration: args?.flow_calibration !== undefined ? Boolean(args.flow_calibration) : undefined,
|
|
@@ -1282,6 +2675,63 @@ class BambuPrinterMCPServer {
|
|
|
1282
2675
|
result = `Print command for ${threeMfFilename} sent successfully.`;
|
|
1283
2676
|
break;
|
|
1284
2677
|
}
|
|
2678
|
+
case "print_collar_charm": {
|
|
2679
|
+
const resolvedTemplateSourcePath = resolveTemplatePathFromName(["3mf"]);
|
|
2680
|
+
const resolvedSourcePath = String(resolvedTemplateSourcePath || args?.source_path || "");
|
|
2681
|
+
if (!resolvedSourcePath) {
|
|
2682
|
+
throw new Error("Missing required parameter: source_path or template_name");
|
|
2683
|
+
}
|
|
2684
|
+
if (!bambuSerial || !bambuToken) {
|
|
2685
|
+
throw new Error("Bambu serial number and access token are required for print_collar_charm.");
|
|
2686
|
+
}
|
|
2687
|
+
const { slicerType, slicerPath, slicerProfile } = resolveSlicerConfig(args);
|
|
2688
|
+
const printModel = await this.resolveBambuModel(args?.bambu_model);
|
|
2689
|
+
const printBedType = resolveBedType(args?.bed_type);
|
|
2690
|
+
const printNozzle = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
2691
|
+
const activeSlicerProfile = await resolveTemplateFirstSlicerProfilePath(args, slicerProfile || undefined, resolvedTemplateSourcePath || explicitTemplatePath || undefined, TEMP_DIR);
|
|
2692
|
+
const preparedThreeMFPath = await this.resolveCollarCharmPrepared3MF(resolvedSourcePath, resolvedTemplateSourcePath || explicitTemplatePath || undefined, slicerType, slicerPath, activeSlicerProfile || undefined, printModel, printNozzle, printBedType, host, bambuSerial, bambuToken);
|
|
2693
|
+
const collarAnalysis = await analyzeCollarCharm3MF(preparedThreeMFPath, 0);
|
|
2694
|
+
const inventory = await this.preflightCollarCharmPolicy(host, bambuSerial, bambuToken, printModel, printNozzle);
|
|
2695
|
+
const projectName = path.basename(preparedThreeMFPath).replace(/\.3mf$/i, '');
|
|
2696
|
+
result = await this.bambu.print3mf(host, bambuSerial, bambuToken, {
|
|
2697
|
+
projectName,
|
|
2698
|
+
filePath: preparedThreeMFPath,
|
|
2699
|
+
plateIndex: collarAnalysis.plateIndex,
|
|
2700
|
+
useAMS: true,
|
|
2701
|
+
amsSlots: collarAnalysis.amsSlots,
|
|
2702
|
+
bedType: printBedType,
|
|
2703
|
+
bedLeveling: args?.bed_leveling !== undefined ? Boolean(args.bed_leveling) : true,
|
|
2704
|
+
flowCalibration: args?.flow_calibration !== undefined ? Boolean(args.flow_calibration) : true,
|
|
2705
|
+
vibrationCalibration: args?.vibration_calibration !== undefined ? Boolean(args.vibration_calibration) : true,
|
|
2706
|
+
timelapse: args?.timelapse !== undefined ? Boolean(args.timelapse) : false,
|
|
2707
|
+
});
|
|
2708
|
+
result = {
|
|
2709
|
+
...result,
|
|
2710
|
+
source_path: resolvedSourcePath,
|
|
2711
|
+
prepared_three_mf_path: preparedThreeMFPath,
|
|
2712
|
+
tray_policy: {
|
|
2713
|
+
inner: {
|
|
2714
|
+
color: COLLAR_CHARM_POLICY.colors.inner,
|
|
2715
|
+
absolute_tray: COLLAR_CHARM_POLICY.amsSlots.inner,
|
|
2716
|
+
},
|
|
2717
|
+
outer: {
|
|
2718
|
+
color: COLLAR_CHARM_POLICY.colors.outer,
|
|
2719
|
+
absolute_tray: COLLAR_CHARM_POLICY.amsSlots.outer,
|
|
2720
|
+
},
|
|
2721
|
+
},
|
|
2722
|
+
collar_roles: collarAnalysis.roles,
|
|
2723
|
+
inventory_slots_checked: inventory.trays
|
|
2724
|
+
.filter((tray) => tray.slot === COLLAR_CHARM_POLICY.amsSlots.inner || tray.slot === COLLAR_CHARM_POLICY.amsSlots.outer)
|
|
2725
|
+
.map((tray) => ({
|
|
2726
|
+
slot: tray.slot,
|
|
2727
|
+
loaded: tray.loaded,
|
|
2728
|
+
tray_color: tray.tray_color,
|
|
2729
|
+
tray_type: tray.tray_type,
|
|
2730
|
+
tray_info_idx: tray.tray_info_idx,
|
|
2731
|
+
})),
|
|
2732
|
+
};
|
|
2733
|
+
break;
|
|
2734
|
+
}
|
|
1285
2735
|
case "merge_vertices":
|
|
1286
2736
|
if (!args?.stl_path)
|
|
1287
2737
|
throw new Error("Missing required parameter: stl_path");
|