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/src/index.ts
CHANGED
|
@@ -24,38 +24,477 @@ import {
|
|
|
24
24
|
type SlicerType,
|
|
25
25
|
} from "./stl/stl-manipulator.js";
|
|
26
26
|
import { BambuNetworkBridge, type BambuNetworkBridgeOptions } from "./bambu-network-bridge.js";
|
|
27
|
-
import {
|
|
27
|
+
import { hasAmsMappingInput, normalizeAmsMappingObject, normalizeBridgeAmsTrayValue } from "./ams-mapping.js";
|
|
28
|
+
import { analyze3MFAmsRequirements, analyze3MFPlateObjects, analyzeCollarCharm3MF, extractBambuTemplateSettings, getCollarCharmRolePolicy, parse3MF } from './3mf_parser.js';
|
|
29
|
+
import type { ThreeMFAmsRequirements } from "./types.js";
|
|
28
30
|
import { BambuImplementation } from "./printers/bambu.js";
|
|
29
31
|
|
|
30
32
|
dotenv.config();
|
|
31
33
|
|
|
32
|
-
const DEFAULT_HOST = process.env.PRINTER_HOST || "localhost";
|
|
33
|
-
const DEFAULT_BAMBU_SERIAL = process.env.BAMBU_SERIAL || "";
|
|
34
|
-
const DEFAULT_BAMBU_TOKEN =
|
|
34
|
+
const DEFAULT_HOST = process.env.BAMBU_PRINTER_HOST || process.env.PRINTER_HOST || "localhost";
|
|
35
|
+
const DEFAULT_BAMBU_SERIAL = process.env.BAMBU_PRINTER_SERIAL || process.env.BAMBU_SERIAL || "";
|
|
36
|
+
const DEFAULT_BAMBU_TOKEN =
|
|
37
|
+
process.env.BAMBU_PRINTER_ACCESS_TOKEN || process.env.BAMBU_TOKEN || "";
|
|
35
38
|
const DEFAULT_BAMBU_DEV_ID = process.env.BAMBU_DEV_ID || DEFAULT_BAMBU_SERIAL;
|
|
36
39
|
const TEMP_DIR = process.env.TEMP_DIR || path.join(process.cwd(), "temp");
|
|
37
40
|
|
|
38
41
|
// Printer model and bed type
|
|
39
|
-
const DEFAULT_BAMBU_MODEL =
|
|
42
|
+
const DEFAULT_BAMBU_MODEL =
|
|
43
|
+
process.env.BAMBU_PRINTER_MODEL?.trim().toLowerCase() ||
|
|
44
|
+
process.env.BAMBU_MODEL?.trim().toLowerCase() ||
|
|
45
|
+
"";
|
|
40
46
|
const DEFAULT_BED_TYPE = process.env.BED_TYPE?.trim().toLowerCase() || "textured_plate";
|
|
41
47
|
const DEFAULT_NOZZLE_DIAMETER = process.env.NOZZLE_DIAMETER?.trim() || "0.4";
|
|
42
48
|
|
|
43
|
-
const VALID_BAMBU_MODELS = ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"] as const;
|
|
49
|
+
const VALID_BAMBU_MODELS = ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s"] as const;
|
|
44
50
|
type BambuModel = typeof VALID_BAMBU_MODELS[number];
|
|
45
51
|
|
|
46
|
-
const VALID_BED_TYPES = ["textured_plate", "cool_plate", "engineering_plate", "hot_plate"] as const;
|
|
52
|
+
const VALID_BED_TYPES = ["textured_plate", "cool_plate", "engineering_plate", "hot_plate", "supertack_plate"] as const;
|
|
53
|
+
const VALID_BAMBUSTUDIO_CLI_BED_TYPES = ["textured_plate", "cool_plate", "engineering_plate", "hot_plate"] as const;
|
|
47
54
|
|
|
48
55
|
// Map model IDs to BambuStudio --load-machine preset names
|
|
49
56
|
const BAMBU_MODEL_PRESETS: Record<string, (nozzle: string) => string> = {
|
|
50
57
|
p1s: (n) => `Bambu Lab P1S ${n} nozzle`,
|
|
51
58
|
p1p: (n) => `Bambu Lab P1P ${n} nozzle`,
|
|
59
|
+
p2s: (n) => `Bambu Lab P2S ${n} nozzle`,
|
|
52
60
|
x1c: (n) => `Bambu Lab X1 Carbon ${n} nozzle`,
|
|
53
61
|
x1e: (n) => `Bambu Lab X1E ${n} nozzle`,
|
|
54
62
|
a1: (n) => `Bambu Lab A1 ${n} nozzle`,
|
|
55
63
|
a1mini: (n) => `Bambu Lab A1 mini ${n} nozzle`,
|
|
56
64
|
h2d: (n) => `Bambu Lab H2D ${n} nozzle`,
|
|
65
|
+
h2s: (n) => `Bambu Lab H2S ${n} nozzle`,
|
|
57
66
|
};
|
|
58
67
|
|
|
68
|
+
const FILAMENT_PROFILE_DIR =
|
|
69
|
+
"/Applications/BambuStudio.app/Contents/Resources/profiles/BBL/filament";
|
|
70
|
+
const FILAMENT_MODEL_CODES: Record<string, string> = {
|
|
71
|
+
p1s: "P1S",
|
|
72
|
+
p1p: "P1P",
|
|
73
|
+
p2s: "P2S",
|
|
74
|
+
x1c: "X1C",
|
|
75
|
+
x1e: "X1E",
|
|
76
|
+
a1: "A1",
|
|
77
|
+
a1mini: "A1M",
|
|
78
|
+
h2d: "H2D",
|
|
79
|
+
h2s: "H2S",
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
type FilamentProfileIndex = {
|
|
83
|
+
byName: Map<string, string>;
|
|
84
|
+
baseNameByFilamentId: Map<string, string>;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
type PrinterFilamentInventory = {
|
|
88
|
+
current_slot: number | null;
|
|
89
|
+
current_source: "ams" | "external" | null;
|
|
90
|
+
summary: {
|
|
91
|
+
loaded_slots: number;
|
|
92
|
+
resolved_profile_slots: number;
|
|
93
|
+
unresolved_loaded_slots: number;
|
|
94
|
+
empty_slots: number;
|
|
95
|
+
current_slot: number | null;
|
|
96
|
+
current_source: "ams" | "external" | null;
|
|
97
|
+
recommended_slot: number | null;
|
|
98
|
+
recommended_reason: string | null;
|
|
99
|
+
};
|
|
100
|
+
trays: Array<{
|
|
101
|
+
ams_id: number | null;
|
|
102
|
+
tray_id: number | null;
|
|
103
|
+
slot: number | null;
|
|
104
|
+
state: number;
|
|
105
|
+
loaded: boolean;
|
|
106
|
+
tray_info_idx: string | null;
|
|
107
|
+
tray_type: string | null;
|
|
108
|
+
tray_sub_brands: string | null;
|
|
109
|
+
tray_color: string | null;
|
|
110
|
+
remain_percent: number | null;
|
|
111
|
+
nozzle_temp_min: number | null;
|
|
112
|
+
nozzle_temp_max: number | null;
|
|
113
|
+
resolved_base_profile_name: string | null;
|
|
114
|
+
resolved_profile_path: string | null;
|
|
115
|
+
profile_resolution: "exact-model-nozzle" | "model" | "generic" | "unresolved";
|
|
116
|
+
match_confidence: "high" | "medium" | "low" | "none";
|
|
117
|
+
display_name: string;
|
|
118
|
+
profile_candidates: string[];
|
|
119
|
+
}>;
|
|
120
|
+
recommended: {
|
|
121
|
+
slot: number | null;
|
|
122
|
+
tray_info_idx: string | null;
|
|
123
|
+
tray_type: string | null;
|
|
124
|
+
resolved_profile_path: string | null;
|
|
125
|
+
load_filaments: string | null;
|
|
126
|
+
reason: string;
|
|
127
|
+
} | null;
|
|
128
|
+
load_filaments_all: string | null;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const COLLAR_CHARM_POLICY = getCollarCharmRolePolicy();
|
|
132
|
+
|
|
133
|
+
let filamentProfileIndexCache: FilamentProfileIndex | null = null;
|
|
134
|
+
|
|
135
|
+
function buildFilamentProfileIndex(): FilamentProfileIndex {
|
|
136
|
+
const byName = new Map<string, string>();
|
|
137
|
+
const baseNameByFilamentId = new Map<string, string>();
|
|
138
|
+
|
|
139
|
+
if (!fs.existsSync(FILAMENT_PROFILE_DIR)) {
|
|
140
|
+
return { byName, baseNameByFilamentId };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
for (const entry of fs.readdirSync(FILAMENT_PROFILE_DIR)) {
|
|
144
|
+
if (!entry.endsWith(".json")) continue;
|
|
145
|
+
|
|
146
|
+
const filePath = path.join(FILAMENT_PROFILE_DIR, entry);
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
150
|
+
const parsed = JSON.parse(raw);
|
|
151
|
+
const name = typeof parsed?.name === "string" ? parsed.name.trim() : "";
|
|
152
|
+
const filamentId = typeof parsed?.filament_id === "string" ? parsed.filament_id.trim() : "";
|
|
153
|
+
|
|
154
|
+
if (name) {
|
|
155
|
+
byName.set(name, filePath);
|
|
156
|
+
}
|
|
157
|
+
if (name && filamentId) {
|
|
158
|
+
baseNameByFilamentId.set(filamentId, name);
|
|
159
|
+
}
|
|
160
|
+
} catch {
|
|
161
|
+
// Ignore malformed or non-profile JSON files.
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return { byName, baseNameByFilamentId };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function getFilamentProfileIndex(): FilamentProfileIndex {
|
|
169
|
+
if (!filamentProfileIndexCache) {
|
|
170
|
+
filamentProfileIndexCache = buildFilamentProfileIndex();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return filamentProfileIndexCache;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function resolveFilamentProfileCandidates(
|
|
177
|
+
trayInfoIdx: string,
|
|
178
|
+
bambuModel?: string,
|
|
179
|
+
nozzleDiameter?: string
|
|
180
|
+
): {
|
|
181
|
+
baseName: string | null;
|
|
182
|
+
paths: string[];
|
|
183
|
+
resolution: "exact-model-nozzle" | "model" | "generic" | "unresolved";
|
|
184
|
+
} {
|
|
185
|
+
const index = getFilamentProfileIndex();
|
|
186
|
+
const baseName = index.baseNameByFilamentId.get(trayInfoIdx) || null;
|
|
187
|
+
if (!baseName) {
|
|
188
|
+
return { baseName: null, paths: [], resolution: "unresolved" };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const bareName = baseName.replace(/\s*@base$/, "");
|
|
192
|
+
const modelCode = bambuModel ? FILAMENT_MODEL_CODES[bambuModel] : undefined;
|
|
193
|
+
const candidateNames: string[] = [];
|
|
194
|
+
|
|
195
|
+
if (modelCode && nozzleDiameter) {
|
|
196
|
+
candidateNames.push(`${bareName} @BBL ${modelCode} ${nozzleDiameter} nozzle`);
|
|
197
|
+
}
|
|
198
|
+
if (modelCode) {
|
|
199
|
+
candidateNames.push(`${bareName} @BBL ${modelCode}`);
|
|
200
|
+
}
|
|
201
|
+
candidateNames.push(bareName, baseName);
|
|
202
|
+
|
|
203
|
+
const resolvedPaths: string[] = [];
|
|
204
|
+
let resolution: "exact-model-nozzle" | "model" | "generic" | "unresolved" = "unresolved";
|
|
205
|
+
|
|
206
|
+
for (const [candidateIndex, candidateName] of candidateNames.entries()) {
|
|
207
|
+
const candidatePath = index.byName.get(candidateName);
|
|
208
|
+
if (!candidatePath || resolvedPaths.includes(candidatePath)) continue;
|
|
209
|
+
resolvedPaths.push(candidatePath);
|
|
210
|
+
if (resolution === "unresolved") {
|
|
211
|
+
if (candidateIndex === 0 && modelCode && nozzleDiameter) {
|
|
212
|
+
resolution = "exact-model-nozzle";
|
|
213
|
+
} else if (
|
|
214
|
+
(candidateIndex === 0 && modelCode && !nozzleDiameter) ||
|
|
215
|
+
(candidateIndex === 1 && modelCode && nozzleDiameter)
|
|
216
|
+
) {
|
|
217
|
+
resolution = "model";
|
|
218
|
+
} else {
|
|
219
|
+
resolution = "generic";
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return { baseName, paths: resolvedPaths, resolution };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function parseIntegerOrNull(value: unknown): number | null {
|
|
228
|
+
if (value === undefined || value === null || value === "") return null;
|
|
229
|
+
const parsed = Number.parseInt(String(value), 10);
|
|
230
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function normalizePrinterFilamentInventory(
|
|
234
|
+
status: any,
|
|
235
|
+
bambuModel?: string,
|
|
236
|
+
nozzleDiameter?: string
|
|
237
|
+
): PrinterFilamentInventory {
|
|
238
|
+
const rawAms = status?.raw?.ams || status?.ams || {};
|
|
239
|
+
const trayNow = parseIntegerOrNull(rawAms?.tray_now);
|
|
240
|
+
const trays: PrinterFilamentInventory["trays"] = [];
|
|
241
|
+
|
|
242
|
+
const amsArray = Array.isArray(rawAms?.ams) ? rawAms.ams : [];
|
|
243
|
+
for (const [amsIndex, amsUnit] of amsArray.entries()) {
|
|
244
|
+
const trayArray = Array.isArray(amsUnit?.tray) ? amsUnit.tray : [];
|
|
245
|
+
for (const [trayIndex, tray] of trayArray.entries()) {
|
|
246
|
+
const amsId = parseIntegerOrNull(amsUnit?.id) ?? amsIndex;
|
|
247
|
+
const trayId = parseIntegerOrNull(tray?.id) ?? trayIndex;
|
|
248
|
+
const slot = amsId !== null && trayId !== null ? amsId * 4 + trayId : null;
|
|
249
|
+
const state = parseIntegerOrNull(tray?.state) ?? 0;
|
|
250
|
+
const trayInfoIdx = typeof tray?.tray_info_idx === "string" ? tray.tray_info_idx : null;
|
|
251
|
+
const profileResolution =
|
|
252
|
+
trayInfoIdx
|
|
253
|
+
? resolveFilamentProfileCandidates(trayInfoIdx, bambuModel, nozzleDiameter)
|
|
254
|
+
: { baseName: null, paths: [], resolution: "unresolved" as const };
|
|
255
|
+
const resolvedProfilePath = profileResolution.paths[0] || null;
|
|
256
|
+
const matchConfidence =
|
|
257
|
+
profileResolution.resolution === "exact-model-nozzle" ? "high" :
|
|
258
|
+
profileResolution.resolution === "model" ? "medium" :
|
|
259
|
+
profileResolution.resolution === "generic" ? "low" :
|
|
260
|
+
"none";
|
|
261
|
+
const trayType = typeof tray?.tray_type === "string" ? tray.tray_type : null;
|
|
262
|
+
const traySubBrands = typeof tray?.tray_sub_brands === "string" ? tray.tray_sub_brands : null;
|
|
263
|
+
const trayColor = typeof tray?.tray_color === "string" ? tray.tray_color : null;
|
|
264
|
+
const displayParts = [
|
|
265
|
+
traySubBrands && traySubBrands !== trayType ? traySubBrands : null,
|
|
266
|
+
trayType,
|
|
267
|
+
trayColor ? `#${normalizeRgbColor(trayColor) ?? trayColor.replace(/^#/, "").slice(0, 6)}` : null,
|
|
268
|
+
].filter(Boolean);
|
|
269
|
+
|
|
270
|
+
trays.push({
|
|
271
|
+
ams_id: amsId,
|
|
272
|
+
tray_id: trayId,
|
|
273
|
+
slot,
|
|
274
|
+
state,
|
|
275
|
+
loaded: state !== 0 && Boolean(trayInfoIdx),
|
|
276
|
+
tray_info_idx: trayInfoIdx,
|
|
277
|
+
tray_type: trayType,
|
|
278
|
+
tray_sub_brands: traySubBrands,
|
|
279
|
+
tray_color: trayColor,
|
|
280
|
+
remain_percent:
|
|
281
|
+
typeof tray?.remain === "number" && tray.remain >= 0 ? tray.remain : null,
|
|
282
|
+
nozzle_temp_min: parseIntegerOrNull(tray?.nozzle_temp_min),
|
|
283
|
+
nozzle_temp_max: parseIntegerOrNull(tray?.nozzle_temp_max),
|
|
284
|
+
resolved_base_profile_name: profileResolution.baseName,
|
|
285
|
+
resolved_profile_path: resolvedProfilePath,
|
|
286
|
+
profile_resolution: profileResolution.resolution,
|
|
287
|
+
match_confidence: matchConfidence,
|
|
288
|
+
display_name: displayParts.length > 0 ? displayParts.join(" ") : "empty/unknown",
|
|
289
|
+
profile_candidates: profileResolution.paths,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const loadedTrays = trays.filter((tray) => tray.loaded);
|
|
295
|
+
const recommendedTray =
|
|
296
|
+
loadedTrays.find((tray) => tray.slot === trayNow && tray.resolved_profile_path) ||
|
|
297
|
+
loadedTrays.find((tray) => tray.resolved_profile_path) ||
|
|
298
|
+
null;
|
|
299
|
+
const recommendedReason =
|
|
300
|
+
recommendedTray?.slot === trayNow
|
|
301
|
+
? "current AMS slot with resolved slicer profile"
|
|
302
|
+
: recommendedTray
|
|
303
|
+
? "first loaded AMS slot with resolved slicer profile"
|
|
304
|
+
: null;
|
|
305
|
+
|
|
306
|
+
const allProfilePaths = Array.from(
|
|
307
|
+
new Set(
|
|
308
|
+
loadedTrays
|
|
309
|
+
.map((tray) => tray.resolved_profile_path)
|
|
310
|
+
.filter((candidate): candidate is string => Boolean(candidate))
|
|
311
|
+
)
|
|
312
|
+
);
|
|
313
|
+
const emptySlots = trays.filter((tray) => !tray.loaded).length;
|
|
314
|
+
const resolvedProfileSlots = loadedTrays.filter((tray) => tray.resolved_profile_path).length;
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
current_slot: trayNow !== null && trayNow >= 0 && trayNow < 254 ? trayNow : null,
|
|
318
|
+
current_source:
|
|
319
|
+
trayNow === 254 ? "external" : trayNow !== null && trayNow >= 0 && trayNow < 254 ? "ams" : null,
|
|
320
|
+
summary: {
|
|
321
|
+
loaded_slots: loadedTrays.length,
|
|
322
|
+
resolved_profile_slots: resolvedProfileSlots,
|
|
323
|
+
unresolved_loaded_slots: loadedTrays.length - resolvedProfileSlots,
|
|
324
|
+
empty_slots: emptySlots,
|
|
325
|
+
current_slot: trayNow !== null && trayNow >= 0 && trayNow < 254 ? trayNow : null,
|
|
326
|
+
current_source:
|
|
327
|
+
trayNow === 254 ? "external" : trayNow !== null && trayNow >= 0 && trayNow < 254 ? "ams" : null,
|
|
328
|
+
recommended_slot: recommendedTray?.slot ?? null,
|
|
329
|
+
recommended_reason: recommendedReason,
|
|
330
|
+
},
|
|
331
|
+
trays,
|
|
332
|
+
recommended: recommendedTray
|
|
333
|
+
? {
|
|
334
|
+
slot: recommendedTray.slot,
|
|
335
|
+
tray_info_idx: recommendedTray.tray_info_idx,
|
|
336
|
+
tray_type: recommendedTray.tray_type,
|
|
337
|
+
resolved_profile_path: recommendedTray.resolved_profile_path,
|
|
338
|
+
load_filaments: recommendedTray.resolved_profile_path,
|
|
339
|
+
reason: recommendedReason ?? "resolved slicer profile",
|
|
340
|
+
}
|
|
341
|
+
: null,
|
|
342
|
+
load_filaments_all: allProfilePaths.length > 0 ? allProfilePaths.join(";") : null,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Normalize a hex color string to lowercase 6-char RGB (drop leading "#"
|
|
348
|
+
* and any trailing alpha bytes). Returns null if the input doesn't look
|
|
349
|
+
* like a hex color.
|
|
350
|
+
*
|
|
351
|
+
* Bambu mixes formats: 3MF project_settings stores `#FFFFFF` or
|
|
352
|
+
* `#FF911A80` (RGB or RGBA with leading #). AMS push_status reports
|
|
353
|
+
* `FFFFFFFF` (RGBA without #). Comparing alpha is risky -- the same
|
|
354
|
+
* filament can show up as `#000000` in one place and `000000FF` in
|
|
355
|
+
* another. RGB-only is the right key.
|
|
356
|
+
*/
|
|
357
|
+
function normalizeRgbColor(color: string | null | undefined): string | null {
|
|
358
|
+
if (!color) return null;
|
|
359
|
+
const hex = color.replace(/^#/, "").toLowerCase();
|
|
360
|
+
if (!/^[0-9a-f]{6}([0-9a-f]{2})?$/.test(hex)) return null;
|
|
361
|
+
return hex.slice(0, 6);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function resolveAmsSlotsFromRequirements(
|
|
365
|
+
requirements: ThreeMFAmsRequirements,
|
|
366
|
+
inventory: PrinterFilamentInventory
|
|
367
|
+
): {
|
|
368
|
+
ams_slots: number[];
|
|
369
|
+
matches: Array<{ filament_position: number; tray_info_idx: string; slot: number; matched_color?: string | null; match_strategy: "color" | "sku-only" }>;
|
|
370
|
+
missing: Array<{ filament_position: number; tray_info_idx: string | null; required_color?: string | null; reason: "no_sku" | "no_loaded_match" | "color_mismatch" | "exhausted" }>;
|
|
371
|
+
} {
|
|
372
|
+
const matches: Array<{ filament_position: number; tray_info_idx: string; slot: number; matched_color?: string | null; match_strategy: "color" | "sku-only" }> = [];
|
|
373
|
+
const missing: Array<{ filament_position: number; tray_info_idx: string | null; required_color?: string | null; reason: "no_sku" | "no_loaded_match" | "color_mismatch" | "exhausted" }> = [];
|
|
374
|
+
const amsSlots: number[] = [];
|
|
375
|
+
|
|
376
|
+
// Track which AMS slots have already been claimed by an earlier
|
|
377
|
+
// requirement so two filaments with the same SKU don't collapse onto
|
|
378
|
+
// the same physical slot.
|
|
379
|
+
const claimedSlots = new Set<number>();
|
|
380
|
+
|
|
381
|
+
for (const filament of requirements.filaments) {
|
|
382
|
+
if (!filament.tray_info_idx) {
|
|
383
|
+
missing.push({
|
|
384
|
+
filament_position: filament.filamentPosition,
|
|
385
|
+
tray_info_idx: null,
|
|
386
|
+
required_color: filament.color ?? null,
|
|
387
|
+
reason: "no_sku",
|
|
388
|
+
});
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const requiredColor = normalizeRgbColor(filament.color);
|
|
393
|
+
|
|
394
|
+
// Pool of trays loaded with the required SKU that aren't already claimed.
|
|
395
|
+
const skuPool = inventory.trays.filter(
|
|
396
|
+
(candidate) =>
|
|
397
|
+
candidate.loaded &&
|
|
398
|
+
candidate.slot !== null &&
|
|
399
|
+
candidate.tray_info_idx === filament.tray_info_idx &&
|
|
400
|
+
!claimedSlots.has(candidate.slot)
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
if (skuPool.length === 0) {
|
|
404
|
+
// No trays of this SKU loaded at all (or all already claimed).
|
|
405
|
+
const reason: "no_loaded_match" | "exhausted" = inventory.trays.some(
|
|
406
|
+
(t) => t.tray_info_idx === filament.tray_info_idx
|
|
407
|
+
)
|
|
408
|
+
? "exhausted"
|
|
409
|
+
: "no_loaded_match";
|
|
410
|
+
missing.push({
|
|
411
|
+
filament_position: filament.filamentPosition,
|
|
412
|
+
tray_info_idx: filament.tray_info_idx,
|
|
413
|
+
required_color: filament.color ?? null,
|
|
414
|
+
reason,
|
|
415
|
+
});
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Prefer an exact RGB color match. This is what disambiguates
|
|
420
|
+
// same-SKU different-color cases (e.g. two GFG02 PETG HF trays
|
|
421
|
+
// loaded -- one black, one white).
|
|
422
|
+
let chosen = skuPool.find(
|
|
423
|
+
(candidate) => normalizeRgbColor(candidate.tray_color) === requiredColor && requiredColor !== null
|
|
424
|
+
);
|
|
425
|
+
let strategy: "color" | "sku-only" = "color";
|
|
426
|
+
|
|
427
|
+
// Fall back to SKU-only when the requirement has no color (legacy
|
|
428
|
+
// 3MF) or when only one tray of this SKU is loaded (no ambiguity).
|
|
429
|
+
if (!chosen) {
|
|
430
|
+
if (!requiredColor || skuPool.length === 1) {
|
|
431
|
+
chosen = skuPool[0];
|
|
432
|
+
strategy = "sku-only";
|
|
433
|
+
} else {
|
|
434
|
+
// Color was specified, multiple SKU candidates loaded, none
|
|
435
|
+
// match the requested color. Surface clearly rather than pick
|
|
436
|
+
// a wrong color silently.
|
|
437
|
+
missing.push({
|
|
438
|
+
filament_position: filament.filamentPosition,
|
|
439
|
+
tray_info_idx: filament.tray_info_idx,
|
|
440
|
+
required_color: filament.color ?? null,
|
|
441
|
+
reason: "color_mismatch",
|
|
442
|
+
});
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (chosen.slot === null) {
|
|
448
|
+
// Defensive: shouldn't happen because of the filter above, but
|
|
449
|
+
// keep the type contract honest.
|
|
450
|
+
missing.push({
|
|
451
|
+
filament_position: filament.filamentPosition,
|
|
452
|
+
tray_info_idx: filament.tray_info_idx,
|
|
453
|
+
required_color: filament.color ?? null,
|
|
454
|
+
reason: "no_loaded_match",
|
|
455
|
+
});
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
claimedSlots.add(chosen.slot);
|
|
460
|
+
amsSlots.push(chosen.slot);
|
|
461
|
+
matches.push({
|
|
462
|
+
filament_position: filament.filamentPosition,
|
|
463
|
+
tray_info_idx: filament.tray_info_idx,
|
|
464
|
+
slot: chosen.slot,
|
|
465
|
+
matched_color: chosen.tray_color ?? null,
|
|
466
|
+
match_strategy: strategy,
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return { ams_slots: amsSlots, matches, missing };
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function extractPrinterDiagnostics(status: any): Record<string, unknown> {
|
|
474
|
+
const raw = status?.raw && typeof status.raw === "object" ? status.raw : {};
|
|
475
|
+
const diagnosticFields: Record<string, unknown> = {};
|
|
476
|
+
|
|
477
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
478
|
+
if (/(hms|error|fail|warn)/i.test(key)) {
|
|
479
|
+
diagnosticFields[key] = value;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return {
|
|
484
|
+
connected: Boolean(status?.connected),
|
|
485
|
+
serial: status?.serial ?? null,
|
|
486
|
+
printer_status: status?.status ?? null,
|
|
487
|
+
active_error:
|
|
488
|
+
raw.print_error ??
|
|
489
|
+
raw.print_error_code ??
|
|
490
|
+
raw.mc_print_error_code ??
|
|
491
|
+
raw.fail_reason ??
|
|
492
|
+
null,
|
|
493
|
+
hms: raw.hms ?? raw.hms_info ?? raw.hms_list ?? null,
|
|
494
|
+
diagnostic_fields: diagnosticFields,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
59
498
|
function validateBambuModel(model: string): string {
|
|
60
499
|
const normalized = model.trim().toLowerCase();
|
|
61
500
|
if (!VALID_BAMBU_MODELS.includes(normalized as BambuModel)) {
|
|
@@ -76,9 +515,23 @@ function resolveBedType(argsBedType: string | undefined): string {
|
|
|
76
515
|
return bedType;
|
|
77
516
|
}
|
|
78
517
|
|
|
518
|
+
function resolveBambuStudioCliBedType(argsBedType: string | undefined): string {
|
|
519
|
+
const bedType = resolveBedType(argsBedType);
|
|
520
|
+
if (!(VALID_BAMBUSTUDIO_CLI_BED_TYPES as readonly string[]).includes(bedType)) {
|
|
521
|
+
throw new Error(
|
|
522
|
+
`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(", ")}`
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
return bedType;
|
|
526
|
+
}
|
|
527
|
+
|
|
79
528
|
// Slicer configuration (defaults to bambustudio)
|
|
80
529
|
const DEFAULT_SLICER_TYPE = process.env.SLICER_TYPE || "bambustudio";
|
|
81
|
-
const DEFAULT_SLICER_PROFILE = process.env.SLICER_PROFILE || "";
|
|
530
|
+
const DEFAULT_SLICER_PROFILE = process.env.BAMBU_SLICER_PROFILE || process.env.SLICER_PROFILE || "";
|
|
531
|
+
const DEFAULT_TEMPLATE_3MF_PATH = process.env.BAMBU_TEMPLATE_3MF_PATH || "";
|
|
532
|
+
const DEFAULT_TEMPLATE_DIR =
|
|
533
|
+
process.env.BAMBU_TEMPLATE_DIR ||
|
|
534
|
+
path.join(process.env.HOME || process.cwd(), "Sync", "bambu", "templates");
|
|
82
535
|
const SLICER_SCHEMA_VALUES = [
|
|
83
536
|
...SLICER_TYPES,
|
|
84
537
|
"fulu-orca",
|
|
@@ -110,6 +563,9 @@ function defaultSlicerPathFor(slicerType: SlicerType): string {
|
|
|
110
563
|
}
|
|
111
564
|
|
|
112
565
|
if (slicerType === "bambustudio") {
|
|
566
|
+
if (process.env.BAMBU_STUDIO_PATH) {
|
|
567
|
+
return process.env.BAMBU_STUDIO_PATH;
|
|
568
|
+
}
|
|
113
569
|
if (process.platform === "darwin") {
|
|
114
570
|
return "/Applications/BambuStudio.app/Contents/MacOS/BambuStudio";
|
|
115
571
|
}
|
|
@@ -172,6 +628,57 @@ function parseCsvEnv(value: string | undefined): Set<string> {
|
|
|
172
628
|
return new Set(value.split(",").map((e) => e.trim()).filter((e) => e.length > 0));
|
|
173
629
|
}
|
|
174
630
|
|
|
631
|
+
async function resolveSlicerProfilePath(
|
|
632
|
+
requestedProfile: string | undefined,
|
|
633
|
+
templatePath: string | undefined,
|
|
634
|
+
tempDir: string
|
|
635
|
+
): Promise<string | undefined> {
|
|
636
|
+
if (requestedProfile) {
|
|
637
|
+
return requestedProfile;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (templatePath) {
|
|
641
|
+
return resolveTemplateSlicerProfilePath(templatePath, tempDir);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
return undefined;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
async function resolveTemplateSlicerProfilePath(
|
|
648
|
+
templatePath: string,
|
|
649
|
+
tempDir: string
|
|
650
|
+
): Promise<string> {
|
|
651
|
+
const extension = path.extname(templatePath).toLowerCase();
|
|
652
|
+
if (extension === ".3mf") {
|
|
653
|
+
return extractBambuTemplateSettings(templatePath, tempDir);
|
|
654
|
+
}
|
|
655
|
+
if (extension === ".json" || extension === ".config") {
|
|
656
|
+
return templatePath;
|
|
657
|
+
}
|
|
658
|
+
throw new Error(`Template profile must be a .3mf, .json, or .config file: ${templatePath}`);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function hasExplicitSlicerProfile(args: any): boolean {
|
|
662
|
+
return typeof args?.slicer_profile === "string" && args.slicer_profile.trim().length > 0;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
async function resolveTemplateFirstSlicerProfilePath(
|
|
666
|
+
args: any,
|
|
667
|
+
configuredProfile: string | undefined,
|
|
668
|
+
template3mfPath: string | undefined,
|
|
669
|
+
tempDir: string
|
|
670
|
+
): Promise<string | undefined> {
|
|
671
|
+
if (hasExplicitSlicerProfile(args)) {
|
|
672
|
+
return configuredProfile || undefined;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
if (template3mfPath) {
|
|
676
|
+
return resolveTemplateSlicerProfilePath(template3mfPath, tempDir);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
return configuredProfile || undefined;
|
|
680
|
+
}
|
|
681
|
+
|
|
175
682
|
function readRuntimeConfig(): RuntimeConfig {
|
|
176
683
|
const rawTransport = process.env.MCP_TRANSPORT?.trim().toLowerCase();
|
|
177
684
|
const transport =
|
|
@@ -340,6 +847,157 @@ function redactPrintParams(params: Record<string, unknown>): Record<string, unkn
|
|
|
340
847
|
};
|
|
341
848
|
}
|
|
342
849
|
|
|
850
|
+
function parseLooseSlicerConfig(content: string): Record<string, any> {
|
|
851
|
+
try {
|
|
852
|
+
return JSON.parse(content);
|
|
853
|
+
} catch {
|
|
854
|
+
const config: Record<string, any> = {};
|
|
855
|
+
for (const line of content.split(/\r?\n/)) {
|
|
856
|
+
const trimmed = line.trim();
|
|
857
|
+
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith(";")) continue;
|
|
858
|
+
const separatorIndex = trimmed.indexOf("=");
|
|
859
|
+
if (separatorIndex === -1) continue;
|
|
860
|
+
const key = trimmed.slice(0, separatorIndex).trim();
|
|
861
|
+
const value = trimmed.slice(separatorIndex + 1).trim();
|
|
862
|
+
if (key) {
|
|
863
|
+
config[key] = value;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
return config;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function summarizeSliceSettings(config: Record<string, any>) {
|
|
871
|
+
return {
|
|
872
|
+
printer_settings_id:
|
|
873
|
+
typeof config.printer_settings_id === "string" ? config.printer_settings_id : null,
|
|
874
|
+
default_print_profile:
|
|
875
|
+
typeof config.default_print_profile === "string" ? config.default_print_profile : null,
|
|
876
|
+
default_filament_profile: Array.isArray(config.default_filament_profile)
|
|
877
|
+
? config.default_filament_profile
|
|
878
|
+
: typeof config.default_filament_profile === "string"
|
|
879
|
+
? [config.default_filament_profile]
|
|
880
|
+
: [],
|
|
881
|
+
filament_settings_id: Array.isArray(config.filament_settings_id)
|
|
882
|
+
? config.filament_settings_id.filter((value) => typeof value === "string" && value.length > 0)
|
|
883
|
+
: [],
|
|
884
|
+
filament_type: Array.isArray(config.filament_type)
|
|
885
|
+
? config.filament_type.filter((value) => typeof value === "string" && value.length > 0)
|
|
886
|
+
: [],
|
|
887
|
+
inherits: typeof config.inherits === "string" ? config.inherits : null,
|
|
888
|
+
print_settings_id: typeof config.print_settings_id === "string" ? config.print_settings_id : null,
|
|
889
|
+
compatible_printers: Array.isArray(config.compatible_printers)
|
|
890
|
+
? config.compatible_printers.filter((value) => typeof value === "string" && value.length > 0)
|
|
891
|
+
: [],
|
|
892
|
+
layer_height:
|
|
893
|
+
config.layer_height !== undefined && config.layer_height !== null
|
|
894
|
+
? Number(config.layer_height)
|
|
895
|
+
: null,
|
|
896
|
+
first_layer_height:
|
|
897
|
+
config.initial_layer_print_height !== undefined && config.initial_layer_print_height !== null
|
|
898
|
+
? Number(config.initial_layer_print_height)
|
|
899
|
+
: config.first_layer_height !== undefined && config.first_layer_height !== null
|
|
900
|
+
? Number(config.first_layer_height)
|
|
901
|
+
: null,
|
|
902
|
+
sparse_infill_density:
|
|
903
|
+
config.sparse_infill_density !== undefined && config.sparse_infill_density !== null
|
|
904
|
+
? String(config.sparse_infill_density)
|
|
905
|
+
: null,
|
|
906
|
+
wall_loops:
|
|
907
|
+
config.wall_loops !== undefined && config.wall_loops !== null
|
|
908
|
+
? Number(config.wall_loops)
|
|
909
|
+
: null,
|
|
910
|
+
top_shell_layers:
|
|
911
|
+
config.top_shell_layers !== undefined && config.top_shell_layers !== null
|
|
912
|
+
? Number(config.top_shell_layers)
|
|
913
|
+
: null,
|
|
914
|
+
bottom_shell_layers:
|
|
915
|
+
config.bottom_shell_layers !== undefined && config.bottom_shell_layers !== null
|
|
916
|
+
? Number(config.bottom_shell_layers)
|
|
917
|
+
: null,
|
|
918
|
+
brim_width:
|
|
919
|
+
config.brim_width !== undefined && config.brim_width !== null
|
|
920
|
+
? Number(config.brim_width)
|
|
921
|
+
: null,
|
|
922
|
+
support_enabled:
|
|
923
|
+
config.enable_support !== undefined
|
|
924
|
+
? String(config.enable_support) === "1" || String(config.enable_support).toLowerCase() === "true"
|
|
925
|
+
: config.support_enabled !== undefined
|
|
926
|
+
? Boolean(config.support_enabled)
|
|
927
|
+
: null,
|
|
928
|
+
support_type: typeof config.support_type === "string" ? config.support_type : null,
|
|
929
|
+
bed_type:
|
|
930
|
+
typeof config.curr_bed_type === "string"
|
|
931
|
+
? config.curr_bed_type
|
|
932
|
+
: typeof config.bed_type === "string"
|
|
933
|
+
? config.bed_type
|
|
934
|
+
: null,
|
|
935
|
+
nozzle_temperature: Array.isArray(config.nozzle_temperature)
|
|
936
|
+
? config.nozzle_temperature
|
|
937
|
+
: config.nozzle_temperature !== undefined
|
|
938
|
+
? [config.nozzle_temperature]
|
|
939
|
+
: [],
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
type TemplateEntry = {
|
|
944
|
+
name: string;
|
|
945
|
+
path: string;
|
|
946
|
+
source_type: "3mf" | "json" | "config";
|
|
947
|
+
relative_path: string;
|
|
948
|
+
};
|
|
949
|
+
type TemplateSourceType = TemplateEntry["source_type"];
|
|
950
|
+
|
|
951
|
+
function sanitizeTemplateName(templateName: string): string {
|
|
952
|
+
return templateName
|
|
953
|
+
.trim()
|
|
954
|
+
.replace(/\\/g, "/")
|
|
955
|
+
.replace(/^\/+|\/+$/g, "")
|
|
956
|
+
.replace(/\s+/g, "_")
|
|
957
|
+
.replace(/[^A-Za-z0-9/_-]/g, "_");
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function scanTemplateRegistry(templateDir: string): TemplateEntry[] {
|
|
961
|
+
if (!fs.existsSync(templateDir)) {
|
|
962
|
+
return [];
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
const allowedExtensions = new Set([".3mf", ".json", ".config"]);
|
|
966
|
+
const entries: TemplateEntry[] = [];
|
|
967
|
+
const stack = [templateDir];
|
|
968
|
+
|
|
969
|
+
while (stack.length > 0) {
|
|
970
|
+
const currentDir = stack.pop()!;
|
|
971
|
+
for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
|
|
972
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
973
|
+
if (entry.isDirectory()) {
|
|
974
|
+
stack.push(fullPath);
|
|
975
|
+
continue;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
const extension = path.extname(entry.name).toLowerCase();
|
|
979
|
+
if (!allowedExtensions.has(extension)) {
|
|
980
|
+
continue;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
const relativePath = path.relative(templateDir, fullPath);
|
|
984
|
+
const templateName = relativePath
|
|
985
|
+
.replace(/\\/g, "/")
|
|
986
|
+
.replace(/(\.gcode)?\.3mf$/i, "")
|
|
987
|
+
.replace(/\.(json|config)$/i, "");
|
|
988
|
+
|
|
989
|
+
entries.push({
|
|
990
|
+
name: templateName,
|
|
991
|
+
path: fullPath,
|
|
992
|
+
source_type: extension === ".3mf" ? "3mf" : extension === ".json" ? "json" : "config",
|
|
993
|
+
relative_path: relativePath,
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
return entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
999
|
+
}
|
|
1000
|
+
|
|
343
1001
|
if (!fs.existsSync(TEMP_DIR)) {
|
|
344
1002
|
fs.mkdirSync(TEMP_DIR, { recursive: true });
|
|
345
1003
|
}
|
|
@@ -396,7 +1054,7 @@ class BambuPrinterMCPServer {
|
|
|
396
1054
|
return validateBambuModel(fromArgs);
|
|
397
1055
|
}
|
|
398
1056
|
|
|
399
|
-
// No model from args or env
|
|
1057
|
+
// No model from args or env — ask the user via elicitation
|
|
400
1058
|
try {
|
|
401
1059
|
const result = await this.server.elicitInput({
|
|
402
1060
|
mode: "form" as const,
|
|
@@ -413,11 +1071,13 @@ class BambuPrinterMCPServer {
|
|
|
413
1071
|
oneOf: [
|
|
414
1072
|
{ const: "p1s", title: "P1S" },
|
|
415
1073
|
{ const: "p1p", title: "P1P" },
|
|
1074
|
+
{ const: "p2s", title: "P2S" },
|
|
416
1075
|
{ const: "x1c", title: "X1 Carbon" },
|
|
417
1076
|
{ const: "x1e", title: "X1E" },
|
|
418
1077
|
{ const: "a1", title: "A1" },
|
|
419
1078
|
{ const: "a1mini", title: "A1 Mini" },
|
|
420
1079
|
{ const: "h2d", title: "H2D" },
|
|
1080
|
+
{ const: "h2s", title: "H2S" },
|
|
421
1081
|
],
|
|
422
1082
|
},
|
|
423
1083
|
},
|
|
@@ -433,7 +1093,7 @@ class BambuPrinterMCPServer {
|
|
|
433
1093
|
"Printer model selection was cancelled. Cannot proceed without knowing the printer model."
|
|
434
1094
|
);
|
|
435
1095
|
} catch (elicitError: any) {
|
|
436
|
-
// Elicitation not supported by this client
|
|
1096
|
+
// Elicitation not supported by this client — fall back to a clear error
|
|
437
1097
|
const msg = elicitError?.message || String(elicitError);
|
|
438
1098
|
if (
|
|
439
1099
|
elicitError?.code === -32601 || elicitError?.code === -32600 ||
|
|
@@ -462,7 +1122,8 @@ class BambuPrinterMCPServer {
|
|
|
462
1122
|
private async ensurePrintableThreeMFPath(
|
|
463
1123
|
args: Record<string, any>,
|
|
464
1124
|
printModel: string,
|
|
465
|
-
printPreset: string | undefined
|
|
1125
|
+
printPreset: string | undefined,
|
|
1126
|
+
bedType?: string
|
|
466
1127
|
): Promise<{ threeMFPath: string; autoSliced: boolean }> {
|
|
467
1128
|
const { slicerType, slicerPath, slicerProfile } = resolveSlicerConfig(args);
|
|
468
1129
|
let threeMFPath = String(args.three_mf_path);
|
|
@@ -478,12 +1139,19 @@ class BambuPrinterMCPServer {
|
|
|
478
1139
|
return { threeMFPath, autoSliced: false };
|
|
479
1140
|
}
|
|
480
1141
|
|
|
1142
|
+
if (bedType === "supertack_plate") {
|
|
1143
|
+
throw new Error(
|
|
1144
|
+
'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.'
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
1147
|
+
|
|
481
1148
|
console.log(`3MF has no gcode - auto-slicing with ${slicerType} for ${printModel}`);
|
|
482
1149
|
const autoSliceOptions: BambuSliceOptions = {
|
|
483
1150
|
uptodate: true,
|
|
484
1151
|
ensureOnBed: true,
|
|
485
1152
|
minSave: true,
|
|
486
1153
|
skipModifiedGcodes: true,
|
|
1154
|
+
bedType: bedType ? resolveBambuStudioCliBedType(bedType) : undefined,
|
|
487
1155
|
};
|
|
488
1156
|
threeMFPath = await this.stlManipulator.sliceSTL(
|
|
489
1157
|
threeMFPath, slicerType, slicerPath, slicerProfile || undefined,
|
|
@@ -497,47 +1165,85 @@ class BambuPrinterMCPServer {
|
|
|
497
1165
|
|
|
498
1166
|
private async resolveAmsPrintSettings(
|
|
499
1167
|
threeMFPath: string,
|
|
500
|
-
args: Record<string, any
|
|
501
|
-
|
|
1168
|
+
args: Record<string, any>,
|
|
1169
|
+
host: string,
|
|
1170
|
+
bambuSerial: string,
|
|
1171
|
+
bambuToken: string,
|
|
1172
|
+
printModel?: string,
|
|
1173
|
+
printNozzle?: string
|
|
1174
|
+
): Promise<{ useAMS: boolean; finalAmsMapping?: number[]; finalAmsSlots?: number[] }> {
|
|
502
1175
|
const parsed3MFData = await parse3MF(threeMFPath);
|
|
503
1176
|
let parsedAmsMapping: number[] | undefined;
|
|
504
1177
|
|
|
505
1178
|
if (parsed3MFData.slicerConfig?.ams_mapping) {
|
|
506
|
-
const slots =
|
|
507
|
-
.filter(v => typeof v === 'number') as number[];
|
|
1179
|
+
const slots = normalizeAmsMappingObject(parsed3MFData.slicerConfig.ams_mapping as Record<string, unknown>);
|
|
508
1180
|
if (slots.length > 0) {
|
|
509
|
-
parsedAmsMapping = slots
|
|
1181
|
+
parsedAmsMapping = slots;
|
|
510
1182
|
}
|
|
511
1183
|
}
|
|
512
1184
|
|
|
513
1185
|
let finalAmsMapping = parsedAmsMapping;
|
|
514
|
-
|
|
515
|
-
let useAMS =
|
|
1186
|
+
let finalAmsSlots: number[] | undefined;
|
|
1187
|
+
let useAMS = args?.use_ams !== undefined ? Boolean(args.use_ams) : (!!finalAmsMapping && finalAmsMapping.length > 0);
|
|
1188
|
+
const hasUserAmsMapping = hasAmsMappingInput(args?.ams_mapping);
|
|
1189
|
+
const hasUserAmsSlots = Array.isArray(args?.ams_slots);
|
|
516
1190
|
|
|
517
|
-
if (
|
|
1191
|
+
if (hasUserAmsMapping) {
|
|
518
1192
|
let userMappingOverride: number[] | undefined;
|
|
519
1193
|
if (Array.isArray(args.ams_mapping)) {
|
|
520
|
-
userMappingOverride = args.ams_mapping.
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
1194
|
+
userMappingOverride = args.ams_mapping.map((v: unknown, i: number) =>
|
|
1195
|
+
normalizeBridgeAmsTrayValue(v, `ams_mapping[${i}]`)
|
|
1196
|
+
);
|
|
1197
|
+
} else if (args.ams_mapping && typeof args.ams_mapping === 'object') {
|
|
1198
|
+
userMappingOverride = normalizeAmsMappingObject(args.ams_mapping as Record<string, unknown>);
|
|
525
1199
|
}
|
|
526
1200
|
if (userMappingOverride && userMappingOverride.length > 0) {
|
|
527
1201
|
finalAmsMapping = userMappingOverride;
|
|
1202
|
+
finalAmsSlots = undefined;
|
|
528
1203
|
useAMS = true;
|
|
529
1204
|
}
|
|
530
1205
|
}
|
|
531
1206
|
|
|
1207
|
+
if (!hasUserAmsMapping && hasUserAmsSlots) {
|
|
1208
|
+
const userSlots = args.ams_slots.map((slot: unknown, i: number) =>
|
|
1209
|
+
normalizeBridgeAmsTrayValue(slot, `ams_slots[${i}]`)
|
|
1210
|
+
);
|
|
1211
|
+
finalAmsSlots = userSlots;
|
|
1212
|
+
finalAmsMapping = undefined;
|
|
1213
|
+
useAMS = userSlots.length > 0;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
if (!hasUserAmsMapping && !hasUserAmsSlots && args?.auto_match_ams) {
|
|
1217
|
+
const requirements = await analyze3MFAmsRequirements(
|
|
1218
|
+
threeMFPath,
|
|
1219
|
+
args?.plate_index !== undefined ? Number(args.plate_index) : 0
|
|
1220
|
+
);
|
|
1221
|
+
const inventory = await this.getResolvedPrinterFilamentInventory(
|
|
1222
|
+
host,
|
|
1223
|
+
bambuSerial,
|
|
1224
|
+
bambuToken,
|
|
1225
|
+
printModel,
|
|
1226
|
+
printNozzle
|
|
1227
|
+
);
|
|
1228
|
+
const resolved = resolveAmsSlotsFromRequirements(requirements, inventory);
|
|
1229
|
+
if (resolved.missing.length > 0) {
|
|
1230
|
+
throw new Error(`Could not auto-match AMS slots: ${JSON.stringify(resolved.missing)}`);
|
|
1231
|
+
}
|
|
1232
|
+
finalAmsSlots = resolved.ams_slots;
|
|
1233
|
+
finalAmsMapping = undefined;
|
|
1234
|
+
useAMS = true;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
532
1237
|
if (args?.use_ams === false) {
|
|
533
1238
|
finalAmsMapping = undefined;
|
|
1239
|
+
finalAmsSlots = undefined;
|
|
534
1240
|
useAMS = false;
|
|
535
1241
|
}
|
|
536
|
-
if ((!finalAmsMapping || finalAmsMapping.length === 0) && !
|
|
1242
|
+
if ((!finalAmsMapping || finalAmsMapping.length === 0) && (!finalAmsSlots || finalAmsSlots.length === 0)) {
|
|
537
1243
|
useAMS = false;
|
|
538
1244
|
}
|
|
539
1245
|
|
|
540
|
-
return { useAMS, finalAmsMapping };
|
|
1246
|
+
return { useAMS, finalAmsMapping, finalAmsSlots };
|
|
541
1247
|
}
|
|
542
1248
|
|
|
543
1249
|
private async print3mfViaBambuNetwork(
|
|
@@ -587,14 +1293,27 @@ class BambuPrinterMCPServer {
|
|
|
587
1293
|
throw new Error("bambu_token/access code is required for FULU BambuNetwork LAN/local print methods.");
|
|
588
1294
|
}
|
|
589
1295
|
|
|
590
|
-
const { threeMFPath, autoSliced } = await this.ensurePrintableThreeMFPath(
|
|
591
|
-
|
|
1296
|
+
const { threeMFPath, autoSliced } = await this.ensurePrintableThreeMFPath(
|
|
1297
|
+
args,
|
|
1298
|
+
printModel,
|
|
1299
|
+
printPreset,
|
|
1300
|
+
printBedType
|
|
1301
|
+
);
|
|
1302
|
+
const { useAMS, finalAmsMapping, finalAmsSlots } = await this.resolveAmsPrintSettings(
|
|
1303
|
+
threeMFPath,
|
|
1304
|
+
args,
|
|
1305
|
+
host,
|
|
1306
|
+
bambuSerial,
|
|
1307
|
+
bambuToken,
|
|
1308
|
+
printModel,
|
|
1309
|
+
printNozzle
|
|
1310
|
+
);
|
|
592
1311
|
const threeMfFilename = path.basename(threeMFPath);
|
|
593
1312
|
const projectName = String(args?.project_name || threeMfFilename.replace(/\.3mf$/i, ''));
|
|
594
1313
|
const presetName = String(args?.preset_name || `${projectName}_plate_${plateIndex + 1}`);
|
|
595
1314
|
const clientJobId = args?.client_job_id !== undefined ? Number(args.client_job_id) : Date.now();
|
|
596
1315
|
|
|
597
|
-
const amsMapping = stringifyBridgeJson(args?.ams_mapping_bridge ?? finalAmsMapping);
|
|
1316
|
+
const amsMapping = stringifyBridgeJson(args?.ams_mapping_bridge ?? finalAmsMapping ?? finalAmsSlots);
|
|
598
1317
|
const params: Record<string, unknown> = {
|
|
599
1318
|
dev_id: devId,
|
|
600
1319
|
task_name: String(args?.task_name || projectName),
|
|
@@ -670,11 +1389,259 @@ class BambuPrinterMCPServer {
|
|
|
670
1389
|
plateIndex,
|
|
671
1390
|
bridgePlateIndex: plateIndex + 1,
|
|
672
1391
|
useAMS,
|
|
673
|
-
amsMapping: finalAmsMapping,
|
|
1392
|
+
amsMapping: finalAmsMapping ?? finalAmsSlots,
|
|
674
1393
|
params: redactPrintParams(params),
|
|
675
1394
|
};
|
|
676
1395
|
}
|
|
677
1396
|
|
|
1397
|
+
private async getResolvedPrinterFilamentInventory(
|
|
1398
|
+
host: string,
|
|
1399
|
+
bambuSerial: string,
|
|
1400
|
+
bambuToken: string,
|
|
1401
|
+
bambuModel?: string,
|
|
1402
|
+
nozzleDiameter?: string
|
|
1403
|
+
): Promise<PrinterFilamentInventory> {
|
|
1404
|
+
const status = await this.bambu.getStatus(host, bambuSerial, bambuToken);
|
|
1405
|
+
let inventory = normalizePrinterFilamentInventory(status, bambuModel, nozzleDiameter);
|
|
1406
|
+
// The first MQTT push from an idle printer is sparse (model/modules
|
|
1407
|
+
// only). AMS data arrives on a second push a short while later. If
|
|
1408
|
+
// we have a live connection but no loaded trays, wait for the follow-
|
|
1409
|
+
// up push and retry.
|
|
1410
|
+
if (inventory.trays.length === 0 && status.connected) {
|
|
1411
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
1412
|
+
const retryStatus = await this.bambu.getStatus(host, bambuSerial, bambuToken);
|
|
1413
|
+
inventory = normalizePrinterFilamentInventory(retryStatus, bambuModel, nozzleDiameter);
|
|
1414
|
+
}
|
|
1415
|
+
return inventory;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
private async inspectSliceSettings(sourcePath: string) {
|
|
1419
|
+
if (!fs.existsSync(sourcePath)) {
|
|
1420
|
+
throw new Error(`Slice settings source not found: ${sourcePath}`);
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
const extension = path.extname(sourcePath).toLowerCase();
|
|
1424
|
+
const tempDir = path.join(TEMP_DIR, "slice-settings");
|
|
1425
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
1426
|
+
|
|
1427
|
+
if (extension === ".3mf") {
|
|
1428
|
+
const parsed = await parse3MF(sourcePath);
|
|
1429
|
+
const extractedSettingsPath = await extractBambuTemplateSettings(sourcePath, tempDir);
|
|
1430
|
+
const config = (parsed.slicerConfig || {}) as Record<string, any>;
|
|
1431
|
+
return {
|
|
1432
|
+
source_path: sourcePath,
|
|
1433
|
+
source_type: "3mf",
|
|
1434
|
+
extracted_settings_path: extractedSettingsPath,
|
|
1435
|
+
object_count: parsed.objects.length,
|
|
1436
|
+
build_item_count: parsed.build.items.length,
|
|
1437
|
+
metadata_keys: Object.keys(parsed.metadata),
|
|
1438
|
+
summary: summarizeSliceSettings(config),
|
|
1439
|
+
raw_key_count: Object.keys(config).length,
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
const content = fs.readFileSync(sourcePath, "utf8");
|
|
1444
|
+
const config = parseLooseSlicerConfig(content);
|
|
1445
|
+
return {
|
|
1446
|
+
source_path: sourcePath,
|
|
1447
|
+
source_type: extension === ".json" ? "json" : extension === ".config" ? "config" : "text",
|
|
1448
|
+
extracted_settings_path: sourcePath,
|
|
1449
|
+
summary: summarizeSliceSettings(config),
|
|
1450
|
+
raw_key_count: Object.keys(config).length,
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
private async resolveCollarCharmPrepared3MF(
|
|
1455
|
+
sourcePath: string,
|
|
1456
|
+
template3mfPath: string | undefined,
|
|
1457
|
+
slicerType: SlicerType,
|
|
1458
|
+
slicerPath: string,
|
|
1459
|
+
slicerProfile: string | undefined,
|
|
1460
|
+
printModel: string,
|
|
1461
|
+
printNozzle: string,
|
|
1462
|
+
bedType: string,
|
|
1463
|
+
host: string,
|
|
1464
|
+
bambuSerial: string,
|
|
1465
|
+
bambuToken: string
|
|
1466
|
+
): Promise<string> {
|
|
1467
|
+
if (!sourcePath.toLowerCase().endsWith(".3mf")) {
|
|
1468
|
+
throw new Error("print_collar_charm requires a prepared .3mf project or sliced 3MF.");
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
try {
|
|
1472
|
+
const JSZip = (await import('jszip')).default;
|
|
1473
|
+
const zipData = fs.readFileSync(sourcePath);
|
|
1474
|
+
const zip = await JSZip.loadAsync(zipData);
|
|
1475
|
+
const hasGcode = Object.keys(zip.files).some(
|
|
1476
|
+
(fileName) => fileName.match(/Metadata\/plate_\d+\.gcode/i) || fileName.endsWith('.gcode')
|
|
1477
|
+
);
|
|
1478
|
+
if (hasGcode) {
|
|
1479
|
+
return sourcePath;
|
|
1480
|
+
}
|
|
1481
|
+
} catch (error: any) {
|
|
1482
|
+
throw new Error(`Failed to inspect collar charm 3MF before slicing: ${error.message}`);
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
const printPreset = BAMBU_MODEL_PRESETS[printModel]?.(printNozzle);
|
|
1486
|
+
if (bedType === "supertack_plate") {
|
|
1487
|
+
throw new Error(
|
|
1488
|
+
'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.'
|
|
1489
|
+
);
|
|
1490
|
+
}
|
|
1491
|
+
const autoSliceOptions: BambuSliceOptions = {
|
|
1492
|
+
uptodate: true,
|
|
1493
|
+
ensureOnBed: true,
|
|
1494
|
+
minSave: true,
|
|
1495
|
+
skipModifiedGcodes: true,
|
|
1496
|
+
bedType,
|
|
1497
|
+
};
|
|
1498
|
+
|
|
1499
|
+
try {
|
|
1500
|
+
const liveFilaments = await this.getResolvedPrinterFilamentInventory(
|
|
1501
|
+
host,
|
|
1502
|
+
bambuSerial,
|
|
1503
|
+
bambuToken,
|
|
1504
|
+
printModel,
|
|
1505
|
+
printNozzle
|
|
1506
|
+
);
|
|
1507
|
+
if (liveFilaments.recommended?.load_filaments) {
|
|
1508
|
+
autoSliceOptions.loadFilaments = liveFilaments.recommended.load_filaments;
|
|
1509
|
+
}
|
|
1510
|
+
} catch (filamentError) {
|
|
1511
|
+
console.warn("Could not resolve live printer filaments for collar charm auto-slicing:", filamentError);
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
return this.stlManipulator.sliceSTL(
|
|
1515
|
+
sourcePath,
|
|
1516
|
+
slicerType,
|
|
1517
|
+
slicerPath,
|
|
1518
|
+
slicerProfile || template3mfPath || undefined,
|
|
1519
|
+
undefined,
|
|
1520
|
+
printPreset,
|
|
1521
|
+
autoSliceOptions
|
|
1522
|
+
);
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
private async preflightCollarCharmPolicy(
|
|
1526
|
+
host: string,
|
|
1527
|
+
bambuSerial: string,
|
|
1528
|
+
bambuToken: string,
|
|
1529
|
+
bambuModel: string,
|
|
1530
|
+
nozzleDiameter: string
|
|
1531
|
+
): Promise<PrinterFilamentInventory> {
|
|
1532
|
+
const inventory = await this.getResolvedPrinterFilamentInventory(
|
|
1533
|
+
host,
|
|
1534
|
+
bambuSerial,
|
|
1535
|
+
bambuToken,
|
|
1536
|
+
bambuModel,
|
|
1537
|
+
nozzleDiameter
|
|
1538
|
+
);
|
|
1539
|
+
|
|
1540
|
+
const requiredSlots = [COLLAR_CHARM_POLICY.amsSlots.inner, COLLAR_CHARM_POLICY.amsSlots.outer];
|
|
1541
|
+
for (const slot of requiredSlots) {
|
|
1542
|
+
const tray = inventory.trays.find((candidate) => candidate.slot === slot);
|
|
1543
|
+
if (!tray) {
|
|
1544
|
+
throw new Error(`Collar charm wrapper requires AMS tray ${slot}, but that tray is not reported by the printer.`);
|
|
1545
|
+
}
|
|
1546
|
+
if (!tray.loaded) {
|
|
1547
|
+
throw new Error(`Collar charm wrapper requires AMS tray ${slot} to be loaded, but it is currently empty or unavailable.`);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
return inventory;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
private listTemplateRegistry(templateDir?: string): {
|
|
1555
|
+
template_dir: string;
|
|
1556
|
+
templates: TemplateEntry[];
|
|
1557
|
+
} {
|
|
1558
|
+
const resolvedTemplateDir = templateDir && templateDir.trim().length > 0
|
|
1559
|
+
? templateDir
|
|
1560
|
+
: DEFAULT_TEMPLATE_DIR;
|
|
1561
|
+
return {
|
|
1562
|
+
template_dir: resolvedTemplateDir,
|
|
1563
|
+
templates: scanTemplateRegistry(resolvedTemplateDir),
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
private resolveTemplatePath(
|
|
1568
|
+
templateName?: string,
|
|
1569
|
+
templateDir?: string,
|
|
1570
|
+
sourceTypes?: TemplateSourceType[]
|
|
1571
|
+
): string | undefined {
|
|
1572
|
+
if (!templateName || templateName.trim().length === 0) {
|
|
1573
|
+
return undefined;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
const registry = this.listTemplateRegistry(templateDir);
|
|
1577
|
+
const normalizedName = sanitizeTemplateName(templateName).toLowerCase();
|
|
1578
|
+
const requestedName = templateName.trim().replace(/\\/g, "/").replace(/^\/+|\/+$/g, "").toLowerCase();
|
|
1579
|
+
const nameMatches = registry.templates.filter((entry) =>
|
|
1580
|
+
entry.name.toLowerCase() === requestedName ||
|
|
1581
|
+
sanitizeTemplateName(entry.name).toLowerCase() === normalizedName
|
|
1582
|
+
);
|
|
1583
|
+
const searchTypes = sourceTypes && sourceTypes.length > 0
|
|
1584
|
+
? sourceTypes
|
|
1585
|
+
: (["3mf", "json", "config"] as TemplateSourceType[]);
|
|
1586
|
+
const match = searchTypes
|
|
1587
|
+
.map((sourceType) => nameMatches.find((entry) => entry.source_type === sourceType))
|
|
1588
|
+
.find((entry): entry is TemplateEntry => Boolean(entry));
|
|
1589
|
+
if (!match) {
|
|
1590
|
+
const typeHint = sourceTypes && sourceTypes.length > 0
|
|
1591
|
+
? ` with source type ${sourceTypes.join("/")}`
|
|
1592
|
+
: "";
|
|
1593
|
+
const availableTypes = nameMatches.length > 0
|
|
1594
|
+
? ` Available source types: ${Array.from(new Set(nameMatches.map((entry) => entry.source_type))).join(", ")}.`
|
|
1595
|
+
: "";
|
|
1596
|
+
throw new Error(
|
|
1597
|
+
`Template "${templateName}"${typeHint} not found in ${registry.template_dir}.${availableTypes}`
|
|
1598
|
+
);
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
return match.path;
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
private saveTemplate(sourcePath: string, templateName?: string, templateDir?: string) {
|
|
1605
|
+
if (!fs.existsSync(sourcePath)) {
|
|
1606
|
+
throw new Error(`Template source not found: ${sourcePath}`);
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
const resolvedTemplateDir = templateDir && templateDir.trim().length > 0
|
|
1610
|
+
? templateDir
|
|
1611
|
+
: DEFAULT_TEMPLATE_DIR;
|
|
1612
|
+
fs.mkdirSync(resolvedTemplateDir, { recursive: true });
|
|
1613
|
+
|
|
1614
|
+
const sourceBaseName = path.basename(sourcePath);
|
|
1615
|
+
const extension = path.extname(sourceBaseName).toLowerCase();
|
|
1616
|
+
if (![".3mf", ".json", ".config"].includes(extension)) {
|
|
1617
|
+
throw new Error("Templates must be .3mf, .json, or .config files.");
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
const baseName = templateName && templateName.trim().length > 0
|
|
1621
|
+
? sanitizeTemplateName(templateName)
|
|
1622
|
+
: sanitizeTemplateName(
|
|
1623
|
+
sourceBaseName
|
|
1624
|
+
.replace(/(\.gcode)?\.3mf$/i, "")
|
|
1625
|
+
.replace(/\.(json|config)$/i, "")
|
|
1626
|
+
);
|
|
1627
|
+
|
|
1628
|
+
const destinationPath = path.join(resolvedTemplateDir, `${baseName}${extension}`);
|
|
1629
|
+
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
|
1630
|
+
fs.copyFileSync(sourcePath, destinationPath);
|
|
1631
|
+
|
|
1632
|
+
const sourceType: TemplateSourceType =
|
|
1633
|
+
extension === ".3mf" ? "3mf" : extension === ".json" ? "json" : "config";
|
|
1634
|
+
const registryEntry = this.resolveTemplatePath(baseName, resolvedTemplateDir, [sourceType]);
|
|
1635
|
+
return {
|
|
1636
|
+
saved: true,
|
|
1637
|
+
template_name: baseName,
|
|
1638
|
+
source_path: sourcePath,
|
|
1639
|
+
destination_path: destinationPath,
|
|
1640
|
+
template_dir: resolvedTemplateDir,
|
|
1641
|
+
resolved_path: registryEntry,
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
|
|
678
1645
|
setupResourceHandlers() {
|
|
679
1646
|
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
680
1647
|
return {
|
|
@@ -690,6 +1657,12 @@ class BambuPrinterMCPServer {
|
|
|
690
1657
|
name: "Bambu Printer Files",
|
|
691
1658
|
mimeType: "application/json",
|
|
692
1659
|
description: "List of files on the Bambu Lab printer"
|
|
1660
|
+
},
|
|
1661
|
+
{
|
|
1662
|
+
uri: `printer://${DEFAULT_HOST}/hms`,
|
|
1663
|
+
name: "Bambu Printer HMS Diagnostics",
|
|
1664
|
+
mimeType: "application/json",
|
|
1665
|
+
description: "HMS and error-related fields from the latest printer status"
|
|
693
1666
|
}
|
|
694
1667
|
],
|
|
695
1668
|
templates: [
|
|
@@ -702,6 +1675,11 @@ class BambuPrinterMCPServer {
|
|
|
702
1675
|
uriTemplate: "printer://{host}/files",
|
|
703
1676
|
name: "Bambu Printer Files",
|
|
704
1677
|
mimeType: "application/json"
|
|
1678
|
+
},
|
|
1679
|
+
{
|
|
1680
|
+
uriTemplate: "printer://{host}/hms",
|
|
1681
|
+
name: "Bambu Printer HMS Diagnostics",
|
|
1682
|
+
mimeType: "application/json"
|
|
705
1683
|
}
|
|
706
1684
|
]
|
|
707
1685
|
};
|
|
@@ -724,6 +1702,19 @@ class BambuPrinterMCPServer {
|
|
|
724
1702
|
content = await this.bambu.getStatus(host || DEFAULT_HOST, bambuSerial, bambuToken);
|
|
725
1703
|
} else if (resource === "files") {
|
|
726
1704
|
content = await this.bambu.getFiles(host || DEFAULT_HOST, bambuSerial, bambuToken);
|
|
1705
|
+
} else if (resource === "hms") {
|
|
1706
|
+
const status = await this.bambu.getStatus(host || DEFAULT_HOST, bambuSerial, bambuToken);
|
|
1707
|
+
let diagnostics = extractPrinterDiagnostics(status);
|
|
1708
|
+
// The first MQTT status push may not include HMS data — the printer
|
|
1709
|
+
// sends basic state first, then pushes HMS/error fields on a subsequent
|
|
1710
|
+
// report. If we got a connection but no HMS data, wait briefly and
|
|
1711
|
+
// retry so the incremental merge has time to arrive.
|
|
1712
|
+
if (!diagnostics.hms && status.connected) {
|
|
1713
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
1714
|
+
const retryStatus = await this.bambu.getStatus(host || DEFAULT_HOST, bambuSerial, bambuToken);
|
|
1715
|
+
diagnostics = extractPrinterDiagnostics(retryStatus);
|
|
1716
|
+
}
|
|
1717
|
+
content = diagnostics;
|
|
727
1718
|
} else {
|
|
728
1719
|
throw new McpError(ErrorCode.InvalidRequest, `Unknown resource: ${resource}`);
|
|
729
1720
|
}
|
|
@@ -766,15 +1757,78 @@ class BambuPrinterMCPServer {
|
|
|
766
1757
|
}
|
|
767
1758
|
},
|
|
768
1759
|
{
|
|
769
|
-
name: "
|
|
770
|
-
description: "
|
|
1760
|
+
name: "get_printer_filaments",
|
|
1761
|
+
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.",
|
|
771
1762
|
inputSchema: {
|
|
772
1763
|
type: "object",
|
|
773
1764
|
properties: {
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
1765
|
+
host: {
|
|
1766
|
+
type: "string",
|
|
1767
|
+
description: "Hostname or IP address of the printer (default: value from env)"
|
|
1768
|
+
},
|
|
1769
|
+
bambu_serial: {
|
|
1770
|
+
type: "string",
|
|
1771
|
+
description: "Serial number for the Bambu Lab printer (default: value from env)"
|
|
1772
|
+
},
|
|
1773
|
+
bambu_token: {
|
|
1774
|
+
type: "string",
|
|
1775
|
+
description: "Access token for the Bambu Lab printer (default: value from env)"
|
|
1776
|
+
},
|
|
1777
|
+
bambu_model: {
|
|
1778
|
+
type: "string",
|
|
1779
|
+
enum: ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s"],
|
|
1780
|
+
description: "Optional model hint used to resolve Bambu/Orca filament profile JSONs for each tray."
|
|
1781
|
+
},
|
|
1782
|
+
nozzle_diameter: {
|
|
1783
|
+
type: "string",
|
|
1784
|
+
description: "Optional nozzle diameter used when resolving model-specific filament profile JSONs (default: 0.4)."
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
},
|
|
1789
|
+
{
|
|
1790
|
+
name: "resolve_3mf_ams_slots",
|
|
1791
|
+
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.",
|
|
1792
|
+
inputSchema: {
|
|
1793
|
+
type: "object",
|
|
1794
|
+
properties: {
|
|
1795
|
+
three_mf_path: { type: "string", description: "Path to a sliced 3MF/.gcode.3mf file" },
|
|
1796
|
+
plate_index: { type: "number", description: "0-based plate index to inspect (default: 0)" },
|
|
1797
|
+
bambu_model: {
|
|
1798
|
+
type: "string",
|
|
1799
|
+
enum: ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s"],
|
|
1800
|
+
description: "Optional model hint used to resolve Bambu/Orca filament profile JSONs for each tray."
|
|
1801
|
+
},
|
|
1802
|
+
nozzle_diameter: { type: "string", description: "Nozzle diameter in mm (default: 0.4)" },
|
|
1803
|
+
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
1804
|
+
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
1805
|
+
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
1806
|
+
},
|
|
1807
|
+
required: ["three_mf_path"]
|
|
1808
|
+
}
|
|
1809
|
+
},
|
|
1810
|
+
{
|
|
1811
|
+
name: "list_3mf_plate_objects",
|
|
1812
|
+
description: "List object IDs from a sliced 3MF plate. Use these IDs with skip_objects during a running print.",
|
|
1813
|
+
inputSchema: {
|
|
1814
|
+
type: "object",
|
|
1815
|
+
properties: {
|
|
1816
|
+
three_mf_path: { type: "string", description: "Path to a sliced 3MF/.gcode.3mf file" },
|
|
1817
|
+
plate_index: { type: "number", description: "0-based plate index to inspect (default: 0)" }
|
|
1818
|
+
},
|
|
1819
|
+
required: ["three_mf_path"]
|
|
1820
|
+
}
|
|
1821
|
+
},
|
|
1822
|
+
{
|
|
1823
|
+
name: "extend_stl_base",
|
|
1824
|
+
description: "Extend the base of an STL file by a specified amount",
|
|
1825
|
+
inputSchema: {
|
|
1826
|
+
type: "object",
|
|
1827
|
+
properties: {
|
|
1828
|
+
stl_path: { type: "string", description: "Path to the STL file to modify" },
|
|
1829
|
+
extension_height: { type: "number", description: "Height in mm to extend the base by" }
|
|
1830
|
+
},
|
|
1831
|
+
required: ["stl_path", "extension_height"]
|
|
778
1832
|
}
|
|
779
1833
|
},
|
|
780
1834
|
{
|
|
@@ -816,6 +1870,110 @@ class BambuPrinterMCPServer {
|
|
|
816
1870
|
required: ["stl_path"]
|
|
817
1871
|
}
|
|
818
1872
|
},
|
|
1873
|
+
{
|
|
1874
|
+
name: "list_templates",
|
|
1875
|
+
description: "List saved slicing templates from the local template registry directory.",
|
|
1876
|
+
inputSchema: {
|
|
1877
|
+
type: "object",
|
|
1878
|
+
properties: {
|
|
1879
|
+
template_dir: {
|
|
1880
|
+
type: "string",
|
|
1881
|
+
description: "Optional template directory override. Defaults to BAMBU_TEMPLATE_DIR or the server's configured local template registry."
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
},
|
|
1886
|
+
{
|
|
1887
|
+
name: "save_template",
|
|
1888
|
+
description: "Copy a 3MF, JSON, or config file into the local template registry and register it under a template name.",
|
|
1889
|
+
inputSchema: {
|
|
1890
|
+
type: "object",
|
|
1891
|
+
properties: {
|
|
1892
|
+
source_path: {
|
|
1893
|
+
type: "string",
|
|
1894
|
+
description: "Path to a local .3mf, .json, or .config file to save into the template registry."
|
|
1895
|
+
},
|
|
1896
|
+
template_name: {
|
|
1897
|
+
type: "string",
|
|
1898
|
+
description: "Optional template name. Defaults to the source filename without extension."
|
|
1899
|
+
},
|
|
1900
|
+
template_dir: {
|
|
1901
|
+
type: "string",
|
|
1902
|
+
description: "Optional template directory override. Defaults to BAMBU_TEMPLATE_DIR or the server's configured local template registry."
|
|
1903
|
+
}
|
|
1904
|
+
},
|
|
1905
|
+
required: ["source_path"]
|
|
1906
|
+
}
|
|
1907
|
+
},
|
|
1908
|
+
{
|
|
1909
|
+
name: "get_slice_settings",
|
|
1910
|
+
description: "Inspect slicer settings from a saved 3MF template or a JSON/config slicer profile without slicing anything.",
|
|
1911
|
+
inputSchema: {
|
|
1912
|
+
type: "object",
|
|
1913
|
+
properties: {
|
|
1914
|
+
source_path: {
|
|
1915
|
+
type: "string",
|
|
1916
|
+
description: "Path to a 3MF template, extracted project_settings.config, or slicer profile JSON."
|
|
1917
|
+
},
|
|
1918
|
+
template_name: {
|
|
1919
|
+
type: "string",
|
|
1920
|
+
description: "Optional named template from the local registry. If provided, resolves source_path automatically."
|
|
1921
|
+
},
|
|
1922
|
+
template_dir: {
|
|
1923
|
+
type: "string",
|
|
1924
|
+
description: "Optional template directory override when resolving template_name."
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
},
|
|
1929
|
+
{
|
|
1930
|
+
name: "slice_with_template",
|
|
1931
|
+
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.",
|
|
1932
|
+
inputSchema: {
|
|
1933
|
+
type: "object",
|
|
1934
|
+
properties: {
|
|
1935
|
+
stl_path: { type: "string", description: "Path to the STL or 3MF file to slice" },
|
|
1936
|
+
template_name: { type: "string", description: "Named template from the local registry." },
|
|
1937
|
+
template_dir: { type: "string", description: "Optional template directory override when resolving template_name." },
|
|
1938
|
+
bambu_model: {
|
|
1939
|
+
type: "string",
|
|
1940
|
+
enum: ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s"],
|
|
1941
|
+
description: "REQUIRED: Bambu Lab printer model. Ask the user if not known. Using the wrong model can damage the printer."
|
|
1942
|
+
},
|
|
1943
|
+
slicer_type: {
|
|
1944
|
+
type: "string",
|
|
1945
|
+
enum: SLICER_SCHEMA_VALUES,
|
|
1946
|
+
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."
|
|
1947
|
+
},
|
|
1948
|
+
slicer_path: { type: "string", description: "Path to the slicer executable (default: value from env)" },
|
|
1949
|
+
slicer_profile: { type: "string", description: "Explicit slicer profile/config file. Overrides the named template only when provided in the tool call." },
|
|
1950
|
+
nozzle_diameter: { type: "string", description: "Nozzle diameter in mm (default: 0.4)" },
|
|
1951
|
+
bed_type: {
|
|
1952
|
+
type: "string",
|
|
1953
|
+
enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate", "supertack_plate"],
|
|
1954
|
+
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."
|
|
1955
|
+
},
|
|
1956
|
+
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." },
|
|
1957
|
+
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
1958
|
+
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
1959
|
+
bambu_token: { type: "string", description: "Access token (default: value from env)" },
|
|
1960
|
+
load_filaments: { type: "string", description: "Override filament profiles. Semicolon-separated paths to filament JSON configs." },
|
|
1961
|
+
load_filament_ids: { type: "string", description: "Optional filament-to-object mapping string." },
|
|
1962
|
+
ensure_on_bed: { type: "boolean", description: "Lift floating models onto the bed." },
|
|
1963
|
+
arrange: { type: "boolean", description: "Auto-arrange objects on the build plate." },
|
|
1964
|
+
orient: { type: "boolean", description: "Auto-orient model for optimal printability." },
|
|
1965
|
+
repetitions: { type: "number", description: "Number of copies to print." },
|
|
1966
|
+
scale: { type: "number", description: "Uniform scale factor." },
|
|
1967
|
+
rotate: { type: "number", description: "Z-axis rotation in degrees." },
|
|
1968
|
+
rotate_x: { type: "number", description: "X-axis rotation in degrees." },
|
|
1969
|
+
rotate_y: { type: "number", description: "Y-axis rotation in degrees." },
|
|
1970
|
+
min_save: { type: "boolean", description: "Produce smaller output 3MF." },
|
|
1971
|
+
skip_modified_gcodes: { type: "boolean", description: "Ignore stale custom gcodes in the 3MF." },
|
|
1972
|
+
slice_plate: { type: "number", description: "Which plate index to slice. 0 = all plates." }
|
|
1973
|
+
},
|
|
1974
|
+
required: ["stl_path", "template_name", "bambu_model"]
|
|
1975
|
+
}
|
|
1976
|
+
},
|
|
819
1977
|
{
|
|
820
1978
|
name: "slice_stl",
|
|
821
1979
|
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.",
|
|
@@ -825,7 +1983,7 @@ class BambuPrinterMCPServer {
|
|
|
825
1983
|
stl_path: { type: "string", description: "Path to the STL or 3MF file to slice" },
|
|
826
1984
|
bambu_model: {
|
|
827
1985
|
type: "string",
|
|
828
|
-
enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
|
|
1986
|
+
enum: ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s"],
|
|
829
1987
|
description: "REQUIRED: Bambu Lab printer model. Ask the user if not known. Using the wrong model can damage the printer."
|
|
830
1988
|
},
|
|
831
1989
|
slicer_type: {
|
|
@@ -833,10 +1991,19 @@ class BambuPrinterMCPServer {
|
|
|
833
1991
|
enum: SLICER_SCHEMA_VALUES,
|
|
834
1992
|
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."
|
|
835
1993
|
},
|
|
836
|
-
slicer_path: { type: "string", description: "Path to the slicer executable (default: value from env
|
|
1994
|
+
slicer_path: { type: "string", description: "Path to the slicer executable (default: value from env)" },
|
|
837
1995
|
slicer_profile: { type: "string", description: "Path to the slicer profile/config file (optional, overrides bambu_model preset)" },
|
|
1996
|
+
template_3mf_path: { type: "string", description: "Optional template 3MF whose embedded Bambu slicer settings should be reused when slicing a new STL or 3MF." },
|
|
1997
|
+
template_name: { type: "string", description: "Optional named template from the local registry. Resolves to template_3mf_path automatically." },
|
|
1998
|
+
template_dir: { type: "string", description: "Optional template directory override when resolving template_name." },
|
|
838
1999
|
nozzle_diameter: { type: "string", description: "Nozzle diameter in mm (default: 0.4)" },
|
|
839
|
-
|
|
2000
|
+
bed_type: {
|
|
2001
|
+
type: "string",
|
|
2002
|
+
enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate", "supertack_plate"],
|
|
2003
|
+
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."
|
|
2004
|
+
},
|
|
2005
|
+
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." },
|
|
2006
|
+
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." },
|
|
840
2007
|
repetitions: { type: "number", description: "Print N identical copies of the model. Each copy gets its own plate placement. Example: 3 prints three copies." },
|
|
841
2008
|
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." },
|
|
842
2009
|
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." },
|
|
@@ -844,7 +2011,7 @@ class BambuPrinterMCPServer {
|
|
|
844
2011
|
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." },
|
|
845
2012
|
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." },
|
|
846
2013
|
load_filaments: { type: "string", description: "Override filament profiles. Semicolon-separated paths to filament JSON configs, e.g. 'pla_basic.json;petg_cf.json'." },
|
|
847
|
-
filament_profile: { type: "string", description: "Compatibility alias for load_filaments. Semicolon-separated Orca/Bambu filament profile JSON paths
|
|
2014
|
+
filament_profile: { type: "string", description: "Compatibility alias for load_filaments. Semicolon-separated Orca/Bambu filament profile JSON paths." },
|
|
848
2015
|
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." },
|
|
849
2016
|
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." },
|
|
850
2017
|
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." },
|
|
@@ -913,7 +2080,7 @@ class BambuPrinterMCPServer {
|
|
|
913
2080
|
three_mf_path: { type: "string", description: "Path to the 3MF file to print; unsliced 3MFs are auto-sliced before sending." },
|
|
914
2081
|
bambu_model: {
|
|
915
2082
|
type: "string",
|
|
916
|
-
enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
|
|
2083
|
+
enum: ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s"],
|
|
917
2084
|
description: "REQUIRED: Bambu Lab printer model. Ask the user if not known. Using the wrong model can damage the printer."
|
|
918
2085
|
},
|
|
919
2086
|
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." },
|
|
@@ -925,7 +2092,7 @@ class BambuPrinterMCPServer {
|
|
|
925
2092
|
bambu_token: { type: "string", description: "Printer access code/password for LAN/local bridge methods." },
|
|
926
2093
|
username: { type: "string", description: "Printer username for LAN/local bridge methods; defaults to bblp." },
|
|
927
2094
|
password: { type: "string", description: "Printer password/access code override for LAN/local bridge methods." },
|
|
928
|
-
bed_type: { type: "string", enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate"], description: "Bed plate type currently installed (default: textured_plate)." },
|
|
2095
|
+
bed_type: { type: "string", enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate", "supertack_plate"], description: "Bed plate type currently installed (default: textured_plate)." },
|
|
929
2096
|
plate_index: { type: "number", description: "Zero-based plate index to print from the sliced 3MF; converted to FULU's one-based PrintParams plate_index." },
|
|
930
2097
|
project_name: { type: "string", description: "Optional project name sent in FULU PrintParams; defaults to the 3MF filename without extension." },
|
|
931
2098
|
preset_name: { type: "string", description: "Optional preset name sent in FULU PrintParams; defaults to project plus one-based plate index." },
|
|
@@ -942,6 +2109,7 @@ class BambuPrinterMCPServer {
|
|
|
942
2109
|
nozzle_diameter: { type: "string", description: "Nozzle diameter in mm for auto-slicing (default: 0.4)." },
|
|
943
2110
|
use_ams: { type: "boolean", description: "Whether to use the AMS; defaults to auto-detect from the 3MF mapping." },
|
|
944
2111
|
ams_mapping: { type: "array", description: "AMS slot mapping array used by both local MCP printing and FULU PrintParams.", items: { type: "number" } },
|
|
2112
|
+
ams_slots: { type: "array", description: "Per-used-filament AMS slot list, matching the local LAN print path.", items: { type: "number" } },
|
|
945
2113
|
ams_mapping_bridge: { type: "string", description: "Raw JSON string override for FULU PrintParams ams_mapping when the automatic array is not enough." },
|
|
946
2114
|
ams_mapping2: { type: "string", description: "Raw JSON string for FULU PrintParams ams_mapping2, matching OrcaSlicer-bambulab's v1 AMS mapping field." },
|
|
947
2115
|
ams_mapping_info: { type: "string", description: "Raw JSON string for FULU PrintParams ams_mapping_info, matching OrcaSlicer-bambulab's detailed AMS mapping field." },
|
|
@@ -962,6 +2130,44 @@ class BambuPrinterMCPServer {
|
|
|
962
2130
|
required: ["three_mf_path", "bambu_model"]
|
|
963
2131
|
}
|
|
964
2132
|
},
|
|
2133
|
+
{
|
|
2134
|
+
name: "camera_snapshot",
|
|
2135
|
+
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.",
|
|
2136
|
+
inputSchema: {
|
|
2137
|
+
type: "object",
|
|
2138
|
+
properties: {
|
|
2139
|
+
save_path: { type: "string", description: "Optional absolute path to write the JPEG to disk. If omitted, only the base64 payload is returned." },
|
|
2140
|
+
timeout_ms: { type: "number", description: "Max ms to wait for a full frame (default 8000). Camera may take a few seconds on cold start." },
|
|
2141
|
+
bambu_model: { type: "string", description: "Printer model. Used to route to the correct protocol or fail fast on unsupported models. Defaults to BAMBU_MODEL." },
|
|
2142
|
+
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." },
|
|
2143
|
+
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)." },
|
|
2144
|
+
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2145
|
+
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
2146
|
+
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
},
|
|
2150
|
+
{
|
|
2151
|
+
name: "delete_printer_file",
|
|
2152
|
+
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.",
|
|
2153
|
+
inputSchema: {
|
|
2154
|
+
type: "object",
|
|
2155
|
+
properties: {
|
|
2156
|
+
filename: {
|
|
2157
|
+
type: "string",
|
|
2158
|
+
description: "File to delete. Bare names default to cache/<name>; pass a relative path like timelapse/foo.mp4 to target other allowed directories."
|
|
2159
|
+
},
|
|
2160
|
+
confirm: {
|
|
2161
|
+
type: "boolean",
|
|
2162
|
+
description: "Must be true to actually delete. When false or omitted the call returns without sending an FTP request."
|
|
2163
|
+
},
|
|
2164
|
+
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2165
|
+
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
2166
|
+
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
2167
|
+
},
|
|
2168
|
+
required: ["filename"]
|
|
2169
|
+
}
|
|
2170
|
+
},
|
|
965
2171
|
{
|
|
966
2172
|
name: "upload_gcode",
|
|
967
2173
|
description: "Upload a G-code file to the Bambu Lab printer",
|
|
@@ -969,17 +2175,13 @@ class BambuPrinterMCPServer {
|
|
|
969
2175
|
type: "object",
|
|
970
2176
|
properties: {
|
|
971
2177
|
filename: { type: "string", description: "Name for the file on the printer" },
|
|
972
|
-
gcode: { type: "string", description: "G-code content to upload, or a readable local .gcode path. For large files, prefer gcode_path." },
|
|
973
|
-
gcode_path: { type: "string", description: "Local path to a .gcode file to upload. This avoids sending large G-code bodies through the MCP request." },
|
|
2178
|
+
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." },
|
|
2179
|
+
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." },
|
|
974
2180
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
975
2181
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
976
2182
|
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
977
2183
|
},
|
|
978
|
-
required: ["filename"]
|
|
979
|
-
anyOf: [
|
|
980
|
-
{ required: ["gcode"] },
|
|
981
|
-
{ required: ["gcode_path"] }
|
|
982
|
-
]
|
|
2184
|
+
required: ["filename"]
|
|
983
2185
|
}
|
|
984
2186
|
},
|
|
985
2187
|
{
|
|
@@ -993,7 +2195,7 @@ class BambuPrinterMCPServer {
|
|
|
993
2195
|
print: { type: "boolean", description: "Start printing after upload (default: false)" },
|
|
994
2196
|
bambu_model: {
|
|
995
2197
|
type: "string",
|
|
996
|
-
enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
|
|
2198
|
+
enum: ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s"],
|
|
997
2199
|
description: "Required when print is true. Bambu Lab printer model used as a safety confirmation before starting the uploaded file."
|
|
998
2200
|
},
|
|
999
2201
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
@@ -1012,7 +2214,7 @@ class BambuPrinterMCPServer {
|
|
|
1012
2214
|
filename: { type: "string", description: "Name of the file to print" },
|
|
1013
2215
|
bambu_model: {
|
|
1014
2216
|
type: "string",
|
|
1015
|
-
enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
|
|
2217
|
+
enum: ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s"],
|
|
1016
2218
|
description: "REQUIRED: Bambu Lab printer model. Ask the user if not known. Starting G-code for the wrong model can damage the printer."
|
|
1017
2219
|
},
|
|
1018
2220
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
@@ -1031,7 +2233,7 @@ class BambuPrinterMCPServer {
|
|
|
1031
2233
|
filename: { type: "string", description: "Name of the file to print" },
|
|
1032
2234
|
bambu_model: {
|
|
1033
2235
|
type: "string",
|
|
1034
|
-
enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
|
|
2236
|
+
enum: ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s"],
|
|
1035
2237
|
description: "REQUIRED: Bambu Lab printer model. Ask the user if not known. Starting G-code for the wrong model can damage the printer."
|
|
1036
2238
|
},
|
|
1037
2239
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
@@ -1053,6 +2255,93 @@ class BambuPrinterMCPServer {
|
|
|
1053
2255
|
}
|
|
1054
2256
|
}
|
|
1055
2257
|
},
|
|
2258
|
+
{
|
|
2259
|
+
name: "pause_print",
|
|
2260
|
+
description: "Pause the current print job on the Bambu Lab printer (resumable via resume_print)",
|
|
2261
|
+
inputSchema: {
|
|
2262
|
+
type: "object",
|
|
2263
|
+
properties: {
|
|
2264
|
+
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2265
|
+
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
2266
|
+
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
},
|
|
2270
|
+
{
|
|
2271
|
+
name: "resume_print",
|
|
2272
|
+
description: "Resume a paused print job on the Bambu Lab printer",
|
|
2273
|
+
inputSchema: {
|
|
2274
|
+
type: "object",
|
|
2275
|
+
properties: {
|
|
2276
|
+
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2277
|
+
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
2278
|
+
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
},
|
|
2282
|
+
{
|
|
2283
|
+
name: "clear_hms_errors",
|
|
2284
|
+
description: "Clear HMS or print error state on the Bambu Lab printer using the clean_print_error MQTT command.",
|
|
2285
|
+
inputSchema: {
|
|
2286
|
+
type: "object",
|
|
2287
|
+
properties: {
|
|
2288
|
+
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2289
|
+
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
2290
|
+
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
},
|
|
2294
|
+
{
|
|
2295
|
+
name: "set_print_speed",
|
|
2296
|
+
description: "Set the active print speed mode: silent, standard, sport, or ludicrous.",
|
|
2297
|
+
inputSchema: {
|
|
2298
|
+
type: "object",
|
|
2299
|
+
properties: {
|
|
2300
|
+
mode: {
|
|
2301
|
+
type: "string",
|
|
2302
|
+
enum: ["silent", "standard", "sport", "ludicrous", "1", "2", "3", "4"],
|
|
2303
|
+
description: "Speed mode to apply: silent/1, standard/2, sport/3, or ludicrous/4"
|
|
2304
|
+
},
|
|
2305
|
+
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2306
|
+
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
2307
|
+
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
2308
|
+
},
|
|
2309
|
+
required: ["mode"]
|
|
2310
|
+
}
|
|
2311
|
+
},
|
|
2312
|
+
{
|
|
2313
|
+
name: "set_airduct_mode",
|
|
2314
|
+
description: "Set H2/P2 airduct mode to cooling or heating.",
|
|
2315
|
+
inputSchema: {
|
|
2316
|
+
type: "object",
|
|
2317
|
+
properties: {
|
|
2318
|
+
mode: {
|
|
2319
|
+
type: "string",
|
|
2320
|
+
enum: ["cooling", "heating"],
|
|
2321
|
+
description: "Airduct mode to apply"
|
|
2322
|
+
},
|
|
2323
|
+
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2324
|
+
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
2325
|
+
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
2326
|
+
},
|
|
2327
|
+
required: ["mode"]
|
|
2328
|
+
}
|
|
2329
|
+
},
|
|
2330
|
+
{
|
|
2331
|
+
name: "reread_ams_rfid",
|
|
2332
|
+
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.",
|
|
2333
|
+
inputSchema: {
|
|
2334
|
+
type: "object",
|
|
2335
|
+
properties: {
|
|
2336
|
+
ams_id: { type: "number", description: "AMS unit index from 0 to 3" },
|
|
2337
|
+
slot_id: { type: "number", description: "Slot index within that AMS, from 0 to 3" },
|
|
2338
|
+
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2339
|
+
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
2340
|
+
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
2341
|
+
},
|
|
2342
|
+
required: ["ams_id", "slot_id"]
|
|
2343
|
+
}
|
|
2344
|
+
},
|
|
1056
2345
|
{
|
|
1057
2346
|
name: "set_temperature",
|
|
1058
2347
|
description: "Set the temperature of a printer component (bed, nozzle)",
|
|
@@ -1068,6 +2357,86 @@ class BambuPrinterMCPServer {
|
|
|
1068
2357
|
required: ["component", "temperature"]
|
|
1069
2358
|
}
|
|
1070
2359
|
},
|
|
2360
|
+
{
|
|
2361
|
+
name: "set_fan_speed",
|
|
2362
|
+
description: "Set a Bambu printer fan speed percentage using the printer's MQTT fan command",
|
|
2363
|
+
inputSchema: {
|
|
2364
|
+
type: "object",
|
|
2365
|
+
properties: {
|
|
2366
|
+
fan: {
|
|
2367
|
+
type: "string",
|
|
2368
|
+
description: "Fan to control: part, auxiliary, chamber, 1, 2, or 3"
|
|
2369
|
+
},
|
|
2370
|
+
speed: { type: "number", description: "Fan speed percentage from 0 to 100" },
|
|
2371
|
+
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2372
|
+
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
2373
|
+
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
2374
|
+
},
|
|
2375
|
+
required: ["fan", "speed"]
|
|
2376
|
+
}
|
|
2377
|
+
},
|
|
2378
|
+
{
|
|
2379
|
+
name: "set_light",
|
|
2380
|
+
description: "Set a Bambu printer light node mode using the printer's MQTT LED command",
|
|
2381
|
+
inputSchema: {
|
|
2382
|
+
type: "object",
|
|
2383
|
+
properties: {
|
|
2384
|
+
light: {
|
|
2385
|
+
type: "string",
|
|
2386
|
+
description: "Light node to control, for example chamber_light"
|
|
2387
|
+
},
|
|
2388
|
+
mode: {
|
|
2389
|
+
type: "string",
|
|
2390
|
+
enum: ["on", "off", "flashing"],
|
|
2391
|
+
description: "Light mode to apply"
|
|
2392
|
+
},
|
|
2393
|
+
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2394
|
+
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
2395
|
+
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
2396
|
+
},
|
|
2397
|
+
required: ["light", "mode"]
|
|
2398
|
+
}
|
|
2399
|
+
},
|
|
2400
|
+
{
|
|
2401
|
+
name: "skip_objects",
|
|
2402
|
+
description: "Skip specific object IDs during a running multi-object print using the printer's MQTT skip_objects command",
|
|
2403
|
+
inputSchema: {
|
|
2404
|
+
type: "object",
|
|
2405
|
+
properties: {
|
|
2406
|
+
object_ids: {
|
|
2407
|
+
type: "array",
|
|
2408
|
+
description: "Object IDs to skip. Use list_3mf_plate_objects on the sliced 3MF to find IDs.",
|
|
2409
|
+
items: { type: "number" }
|
|
2410
|
+
},
|
|
2411
|
+
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2412
|
+
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
2413
|
+
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
2414
|
+
},
|
|
2415
|
+
required: ["object_ids"]
|
|
2416
|
+
}
|
|
2417
|
+
},
|
|
2418
|
+
{
|
|
2419
|
+
name: "set_ams_drying",
|
|
2420
|
+
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.",
|
|
2421
|
+
inputSchema: {
|
|
2422
|
+
type: "object",
|
|
2423
|
+
properties: {
|
|
2424
|
+
action: {
|
|
2425
|
+
type: "string",
|
|
2426
|
+
enum: ["start", "stop"],
|
|
2427
|
+
description: "Whether to start or stop the drying cycle"
|
|
2428
|
+
},
|
|
2429
|
+
ams_id: {
|
|
2430
|
+
type: "number",
|
|
2431
|
+
description: "AMS unit index from 0 to 3"
|
|
2432
|
+
},
|
|
2433
|
+
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2434
|
+
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
2435
|
+
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
2436
|
+
},
|
|
2437
|
+
required: ["action", "ams_id"]
|
|
2438
|
+
}
|
|
2439
|
+
},
|
|
1071
2440
|
{
|
|
1072
2441
|
name: "print_3mf",
|
|
1073
2442
|
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.",
|
|
@@ -1077,13 +2446,13 @@ class BambuPrinterMCPServer {
|
|
|
1077
2446
|
three_mf_path: { type: "string", description: "Path to the 3MF file to print" },
|
|
1078
2447
|
bambu_model: {
|
|
1079
2448
|
type: "string",
|
|
1080
|
-
enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
|
|
2449
|
+
enum: ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s"],
|
|
1081
2450
|
description: "REQUIRED: Bambu Lab printer model. Ask the user if not known. Using the wrong model can damage the printer."
|
|
1082
2451
|
},
|
|
1083
2452
|
connection_mode: {
|
|
1084
2453
|
type: "string",
|
|
1085
2454
|
enum: ["lan_mqtt_ftps", "bambu_network"],
|
|
1086
|
-
description: "Print path to use: lan_mqtt_ftps uses this MCP's direct local MQTT/FTPS path; bambu_network uses
|
|
2455
|
+
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."
|
|
1087
2456
|
},
|
|
1088
2457
|
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." },
|
|
1089
2458
|
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." },
|
|
@@ -1095,34 +2464,77 @@ class BambuPrinterMCPServer {
|
|
|
1095
2464
|
user_info: { type: "string", description: "Optional BambuNetwork user_info JSON string passed to net.change_user for the FULU bridge." },
|
|
1096
2465
|
bed_type: {
|
|
1097
2466
|
type: "string",
|
|
1098
|
-
enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate"],
|
|
2467
|
+
enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate", "supertack_plate"],
|
|
1099
2468
|
description: "Bed plate type currently installed (default: textured_plate)"
|
|
1100
2469
|
},
|
|
1101
|
-
plate_index: { type: "number", description: "Zero-based plate index to print from the sliced 3MF (default: 0)" },
|
|
1102
2470
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
1103
2471
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
1104
2472
|
bambu_token: { type: "string", description: "Access token (default: value from env)" },
|
|
2473
|
+
plate_index: { type: "number", description: "Zero-based plate index to print from the sliced 3MF (default: 0)" },
|
|
1105
2474
|
slicer_type: {
|
|
1106
2475
|
type: "string",
|
|
1107
2476
|
enum: SLICER_SCHEMA_VALUES,
|
|
1108
|
-
description: "Slicer to use only if auto-slicing an unsliced 3MF.
|
|
2477
|
+
description: "Slicer to use only if auto-slicing an unsliced 3MF. Bambu-compatible slicer aliases such as fulu-orca and orca-studio are accepted."
|
|
1109
2478
|
},
|
|
1110
2479
|
slicer_path: { type: "string", description: "Path to the slicer executable for auto-slicing (default: value from env or a platform default)" },
|
|
1111
|
-
slicer_profile: { type: "string", description: "Path to an optional slicer profile/config file for auto-slicing" },
|
|
1112
2480
|
use_ams: { type: "boolean", description: "Whether to use the AMS (default: auto-detect from 3MF)" },
|
|
1113
2481
|
ams_mapping: {
|
|
1114
2482
|
type: "array",
|
|
1115
|
-
description: "AMS
|
|
2483
|
+
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.",
|
|
1116
2484
|
items: { type: "number" }
|
|
1117
2485
|
},
|
|
2486
|
+
ams_slots: {
|
|
2487
|
+
type: "array",
|
|
2488
|
+
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.",
|
|
2489
|
+
items: { type: "number" }
|
|
2490
|
+
},
|
|
2491
|
+
auto_match_ams: {
|
|
2492
|
+
type: "boolean",
|
|
2493
|
+
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."
|
|
2494
|
+
},
|
|
2495
|
+
bed_leveling: { type: "boolean", description: "Enable auto bed leveling (default: true)" },
|
|
2496
|
+
flow_calibration: { type: "boolean", description: "Enable flow calibration (default: true)" },
|
|
2497
|
+
vibration_calibration: { type: "boolean", description: "Enable vibration calibration (default: true)" },
|
|
2498
|
+
timelapse: { type: "boolean", description: "Enable timelapse recording (default: false)" },
|
|
2499
|
+
slicer_profile: { type: "string", description: "Path to the slicer profile/config file for auto-slicing (optional)." },
|
|
2500
|
+
template_3mf_path: { type: "string", description: "Optional template 3MF whose embedded Bambu slicer settings should be reused when auto-slicing this print job." },
|
|
2501
|
+
template_name: { type: "string", description: "Optional named template from the local registry. Resolves to template_3mf_path automatically." },
|
|
2502
|
+
template_dir: { type: "string", description: "Optional template directory override when resolving template_name." },
|
|
2503
|
+
nozzle_diameter: { type: "string", description: "Nozzle diameter in mm for auto-slicing (default: 0.4)" }
|
|
2504
|
+
},
|
|
2505
|
+
required: ["three_mf_path", "bambu_model"]
|
|
2506
|
+
}
|
|
2507
|
+
},
|
|
2508
|
+
{
|
|
2509
|
+
name: "print_collar_charm",
|
|
2510
|
+
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.",
|
|
2511
|
+
inputSchema: {
|
|
2512
|
+
type: "object",
|
|
2513
|
+
properties: {
|
|
2514
|
+
source_path: { type: "string", description: "Path to a prepared collar charm .3mf project or sliced 3MF. Required unless template_name is provided." },
|
|
2515
|
+
template_name: { type: "string", description: "Named collar charm template from the local registry. Required unless source_path is provided; resolves source_path automatically." },
|
|
2516
|
+
template_dir: { type: "string", description: "Optional template directory override when resolving template_name." },
|
|
2517
|
+
bambu_model: {
|
|
2518
|
+
type: "string",
|
|
2519
|
+
enum: ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s"],
|
|
2520
|
+
description: "REQUIRED: Bambu Lab printer model. H2D and H2S are the primary intended paths."
|
|
2521
|
+
},
|
|
2522
|
+
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2523
|
+
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
2524
|
+
bambu_token: { type: "string", description: "Access token (default: value from env)" },
|
|
2525
|
+
bed_type: {
|
|
2526
|
+
type: "string",
|
|
2527
|
+
enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate", "supertack_plate"],
|
|
2528
|
+
description: "Bed plate type currently installed (default: textured_plate)"
|
|
2529
|
+
},
|
|
1118
2530
|
bed_leveling: { type: "boolean", description: "Enable auto bed leveling (default: true)" },
|
|
1119
2531
|
flow_calibration: { type: "boolean", description: "Enable flow calibration (default: true)" },
|
|
1120
2532
|
vibration_calibration: { type: "boolean", description: "Enable vibration calibration (default: true)" },
|
|
1121
|
-
layer_inspect: { type: "boolean", description: "Enable first-layer inspection where supported (default: printer/profile behavior)" },
|
|
1122
2533
|
timelapse: { type: "boolean", description: "Enable timelapse recording (default: false)" },
|
|
2534
|
+
slicer_profile: { type: "string", description: "Path to the slicer profile/config file for auto-slicing (optional)." },
|
|
1123
2535
|
nozzle_diameter: { type: "string", description: "Nozzle diameter in mm for auto-slicing (default: 0.4)" }
|
|
1124
2536
|
},
|
|
1125
|
-
required: ["
|
|
2537
|
+
required: ["bambu_model"]
|
|
1126
2538
|
}
|
|
1127
2539
|
},
|
|
1128
2540
|
{
|
|
@@ -1187,6 +2599,16 @@ class BambuPrinterMCPServer {
|
|
|
1187
2599
|
const host = String(args?.host || DEFAULT_HOST);
|
|
1188
2600
|
const bambuSerial = String(args?.bambu_serial || DEFAULT_BAMBU_SERIAL);
|
|
1189
2601
|
const bambuToken = String(args?.bambu_token || DEFAULT_BAMBU_TOKEN);
|
|
2602
|
+
const requestedTemplateDir =
|
|
2603
|
+
typeof args?.template_dir === "string" && args.template_dir.trim().length > 0
|
|
2604
|
+
? String(args.template_dir)
|
|
2605
|
+
: undefined;
|
|
2606
|
+
const requestedTemplateName =
|
|
2607
|
+
typeof args?.template_name === "string" ? String(args.template_name) : undefined;
|
|
2608
|
+
const resolveTemplatePathFromName = (sourceTypes?: TemplateSourceType[]) =>
|
|
2609
|
+
this.resolveTemplatePath(requestedTemplateName, requestedTemplateDir, sourceTypes);
|
|
2610
|
+
const explicitTemplatePath = String(args?.template_3mf_path || DEFAULT_TEMPLATE_3MF_PATH);
|
|
2611
|
+
|
|
1190
2612
|
try {
|
|
1191
2613
|
let result;
|
|
1192
2614
|
|
|
@@ -1195,6 +2617,75 @@ class BambuPrinterMCPServer {
|
|
|
1195
2617
|
result = await this.bambu.getStatus(host, bambuSerial, bambuToken);
|
|
1196
2618
|
break;
|
|
1197
2619
|
|
|
2620
|
+
case "get_printer_filaments": {
|
|
2621
|
+
const requestedModel = (String(args?.bambu_model ?? DEFAULT_BAMBU_MODEL ?? "")).trim().toLowerCase();
|
|
2622
|
+
const normalizedModel = requestedModel ? validateBambuModel(requestedModel) : undefined;
|
|
2623
|
+
const nozzleDiam = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
2624
|
+
result = await this.getResolvedPrinterFilamentInventory(
|
|
2625
|
+
host,
|
|
2626
|
+
bambuSerial,
|
|
2627
|
+
bambuToken,
|
|
2628
|
+
normalizedModel,
|
|
2629
|
+
nozzleDiam
|
|
2630
|
+
);
|
|
2631
|
+
break;
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
case "resolve_3mf_ams_slots": {
|
|
2635
|
+
if (!args?.three_mf_path) {
|
|
2636
|
+
throw new Error("Missing required parameter: three_mf_path");
|
|
2637
|
+
}
|
|
2638
|
+
const requestedModel = (String(args?.bambu_model ?? DEFAULT_BAMBU_MODEL ?? "")).trim().toLowerCase();
|
|
2639
|
+
const normalizedModel = requestedModel ? validateBambuModel(requestedModel) : undefined;
|
|
2640
|
+
const nozzleDiam = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
2641
|
+
const requirements = await analyze3MFAmsRequirements(
|
|
2642
|
+
String(args.three_mf_path),
|
|
2643
|
+
args?.plate_index !== undefined ? Number(args.plate_index) : 0
|
|
2644
|
+
);
|
|
2645
|
+
const inventory = await this.getResolvedPrinterFilamentInventory(
|
|
2646
|
+
host,
|
|
2647
|
+
bambuSerial,
|
|
2648
|
+
bambuToken,
|
|
2649
|
+
normalizedModel,
|
|
2650
|
+
nozzleDiam
|
|
2651
|
+
);
|
|
2652
|
+
const resolved = resolveAmsSlotsFromRequirements(requirements, inventory);
|
|
2653
|
+
result = {
|
|
2654
|
+
status: resolved.missing.length === 0 ? "matched" : "missing",
|
|
2655
|
+
requirements,
|
|
2656
|
+
ams_slots: resolved.ams_slots,
|
|
2657
|
+
matches: resolved.matches,
|
|
2658
|
+
missing: resolved.missing,
|
|
2659
|
+
};
|
|
2660
|
+
break;
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2663
|
+
case "list_3mf_plate_objects": {
|
|
2664
|
+
if (!args?.three_mf_path) {
|
|
2665
|
+
throw new Error("Missing required parameter: three_mf_path");
|
|
2666
|
+
}
|
|
2667
|
+
result = await analyze3MFPlateObjects(
|
|
2668
|
+
String(args.three_mf_path),
|
|
2669
|
+
args?.plate_index !== undefined ? Number(args.plate_index) : 0
|
|
2670
|
+
);
|
|
2671
|
+
break;
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
case "list_templates":
|
|
2675
|
+
result = this.listTemplateRegistry(requestedTemplateDir);
|
|
2676
|
+
break;
|
|
2677
|
+
|
|
2678
|
+
case "save_template":
|
|
2679
|
+
if (!args?.source_path) {
|
|
2680
|
+
throw new Error("Missing required parameter: source_path");
|
|
2681
|
+
}
|
|
2682
|
+
result = this.saveTemplate(
|
|
2683
|
+
String(args.source_path),
|
|
2684
|
+
typeof args?.template_name === "string" ? String(args.template_name) : undefined,
|
|
2685
|
+
requestedTemplateDir
|
|
2686
|
+
);
|
|
2687
|
+
break;
|
|
2688
|
+
|
|
1198
2689
|
case "list_printer_files":
|
|
1199
2690
|
result = await this.bambu.getFiles(host, bambuSerial, bambuToken);
|
|
1200
2691
|
break;
|
|
@@ -1235,6 +2726,31 @@ class BambuPrinterMCPServer {
|
|
|
1235
2726
|
break;
|
|
1236
2727
|
}
|
|
1237
2728
|
|
|
2729
|
+
case "camera_snapshot": {
|
|
2730
|
+
const snapshotModel = (args?.bambu_model as string | undefined) ?? process.env.BAMBU_MODEL ?? process.env.BAMBU_PRINTER_MODEL;
|
|
2731
|
+
result = await this.bambu.cameraSnapshot(host, bambuSerial, bambuToken, {
|
|
2732
|
+
savePath: args?.save_path ? String(args.save_path) : undefined,
|
|
2733
|
+
timeoutMs: args?.timeout_ms !== undefined ? Number(args.timeout_ms) : undefined,
|
|
2734
|
+
bambuModel: snapshotModel ? String(snapshotModel) : undefined,
|
|
2735
|
+
experimental: Boolean(args?.experimental),
|
|
2736
|
+
ffmpegPath: args?.ffmpeg_path ? String(args.ffmpeg_path) : undefined,
|
|
2737
|
+
});
|
|
2738
|
+
break;
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
case "delete_printer_file":
|
|
2742
|
+
if (!args?.filename) {
|
|
2743
|
+
throw new Error("Missing required parameter: filename");
|
|
2744
|
+
}
|
|
2745
|
+
result = await this.bambu.deleteFile(
|
|
2746
|
+
host,
|
|
2747
|
+
bambuSerial,
|
|
2748
|
+
bambuToken,
|
|
2749
|
+
String(args.filename),
|
|
2750
|
+
args.confirm
|
|
2751
|
+
);
|
|
2752
|
+
break;
|
|
2753
|
+
|
|
1238
2754
|
case "upload_gcode": {
|
|
1239
2755
|
if (!args?.filename) {
|
|
1240
2756
|
throw new Error("Missing required parameter: filename");
|
|
@@ -1279,6 +2795,45 @@ class BambuPrinterMCPServer {
|
|
|
1279
2795
|
result = await this.bambu.cancelJob(host, bambuSerial, bambuToken);
|
|
1280
2796
|
break;
|
|
1281
2797
|
|
|
2798
|
+
case "pause_print":
|
|
2799
|
+
result = await this.bambu.pauseJob(host, bambuSerial, bambuToken);
|
|
2800
|
+
break;
|
|
2801
|
+
|
|
2802
|
+
case "resume_print":
|
|
2803
|
+
result = await this.bambu.resumeJob(host, bambuSerial, bambuToken);
|
|
2804
|
+
break;
|
|
2805
|
+
|
|
2806
|
+
case "clear_hms_errors":
|
|
2807
|
+
result = await this.bambu.clearHmsErrors(host, bambuSerial, bambuToken);
|
|
2808
|
+
break;
|
|
2809
|
+
|
|
2810
|
+
case "set_print_speed":
|
|
2811
|
+
if (!args?.mode) {
|
|
2812
|
+
throw new Error("Missing required parameter: mode");
|
|
2813
|
+
}
|
|
2814
|
+
result = await this.bambu.setPrintSpeed(host, bambuSerial, bambuToken, String(args.mode));
|
|
2815
|
+
break;
|
|
2816
|
+
|
|
2817
|
+
case "set_airduct_mode":
|
|
2818
|
+
if (!args?.mode) {
|
|
2819
|
+
throw new Error("Missing required parameter: mode");
|
|
2820
|
+
}
|
|
2821
|
+
result = await this.bambu.setAirductMode(host, bambuSerial, bambuToken, String(args.mode));
|
|
2822
|
+
break;
|
|
2823
|
+
|
|
2824
|
+
case "reread_ams_rfid":
|
|
2825
|
+
if (args?.ams_id === undefined || args?.slot_id === undefined) {
|
|
2826
|
+
throw new Error("Missing required parameters: ams_id and slot_id");
|
|
2827
|
+
}
|
|
2828
|
+
result = await this.bambu.rereadAmsRfid(
|
|
2829
|
+
host,
|
|
2830
|
+
bambuSerial,
|
|
2831
|
+
bambuToken,
|
|
2832
|
+
Number(args.ams_id),
|
|
2833
|
+
Number(args.slot_id)
|
|
2834
|
+
);
|
|
2835
|
+
break;
|
|
2836
|
+
|
|
1282
2837
|
case "set_temperature":
|
|
1283
2838
|
if (!args?.component || args?.temperature === undefined) {
|
|
1284
2839
|
throw new Error("Missing required parameters: component and temperature");
|
|
@@ -1289,6 +2844,57 @@ class BambuPrinterMCPServer {
|
|
|
1289
2844
|
);
|
|
1290
2845
|
break;
|
|
1291
2846
|
|
|
2847
|
+
case "set_fan_speed":
|
|
2848
|
+
if (!args?.fan || args?.speed === undefined) {
|
|
2849
|
+
throw new Error("Missing required parameters: fan and speed");
|
|
2850
|
+
}
|
|
2851
|
+
result = await this.bambu.setFanSpeed(
|
|
2852
|
+
host,
|
|
2853
|
+
bambuSerial,
|
|
2854
|
+
bambuToken,
|
|
2855
|
+
String(args.fan),
|
|
2856
|
+
Number(args.speed)
|
|
2857
|
+
);
|
|
2858
|
+
break;
|
|
2859
|
+
|
|
2860
|
+
case "set_light":
|
|
2861
|
+
if (!args?.light || !args?.mode) {
|
|
2862
|
+
throw new Error("Missing required parameters: light and mode");
|
|
2863
|
+
}
|
|
2864
|
+
result = await this.bambu.setLight(
|
|
2865
|
+
host,
|
|
2866
|
+
bambuSerial,
|
|
2867
|
+
bambuToken,
|
|
2868
|
+
String(args.light),
|
|
2869
|
+
String(args.mode)
|
|
2870
|
+
);
|
|
2871
|
+
break;
|
|
2872
|
+
|
|
2873
|
+
case "skip_objects":
|
|
2874
|
+
if (!Array.isArray(args?.object_ids)) {
|
|
2875
|
+
throw new Error("Missing required parameter: object_ids");
|
|
2876
|
+
}
|
|
2877
|
+
result = await this.bambu.skipObjects(
|
|
2878
|
+
host,
|
|
2879
|
+
bambuSerial,
|
|
2880
|
+
bambuToken,
|
|
2881
|
+
args.object_ids.map((id: unknown) => Number(id))
|
|
2882
|
+
);
|
|
2883
|
+
break;
|
|
2884
|
+
|
|
2885
|
+
case "set_ams_drying":
|
|
2886
|
+
if (!args?.action || args?.ams_id === undefined) {
|
|
2887
|
+
throw new Error("Missing required parameters: action and ams_id");
|
|
2888
|
+
}
|
|
2889
|
+
result = await this.bambu.setAmsDrying(
|
|
2890
|
+
host,
|
|
2891
|
+
bambuSerial,
|
|
2892
|
+
bambuToken,
|
|
2893
|
+
String(args.action),
|
|
2894
|
+
Number(args.ams_id)
|
|
2895
|
+
);
|
|
2896
|
+
break;
|
|
2897
|
+
|
|
1292
2898
|
case "extend_stl_base":
|
|
1293
2899
|
if (!args?.stl_path || args?.extension_height === undefined) {
|
|
1294
2900
|
throw new Error("Missing required parameters: stl_path and extension_height");
|
|
@@ -1333,13 +2939,113 @@ class BambuPrinterMCPServer {
|
|
|
1333
2939
|
result = await this.stlManipulator.getSTLInfo(String(args.stl_path));
|
|
1334
2940
|
break;
|
|
1335
2941
|
|
|
2942
|
+
case "get_slice_settings":
|
|
2943
|
+
if (!args?.source_path && !args?.template_name) {
|
|
2944
|
+
throw new Error("Missing required parameter: source_path or template_name");
|
|
2945
|
+
}
|
|
2946
|
+
result = await this.inspectSliceSettings(
|
|
2947
|
+
String(args?.source_path || resolveTemplatePathFromName(["3mf", "json", "config"]))
|
|
2948
|
+
);
|
|
2949
|
+
break;
|
|
2950
|
+
|
|
2951
|
+
case "slice_with_template": {
|
|
2952
|
+
if (!args?.stl_path) {
|
|
2953
|
+
throw new Error("Missing required parameter: stl_path");
|
|
2954
|
+
}
|
|
2955
|
+
if (!args?.template_name) {
|
|
2956
|
+
throw new Error("Missing required parameter: template_name");
|
|
2957
|
+
}
|
|
2958
|
+
const { slicerType, slicerPath, slicerProfile } = resolveSlicerConfig(args as Record<string, unknown> | undefined);
|
|
2959
|
+
const sliceModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
2960
|
+
const nozzleDiam = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
2961
|
+
const activeSlicerProfile = await resolveTemplateFirstSlicerProfilePath(
|
|
2962
|
+
args,
|
|
2963
|
+
slicerProfile || undefined,
|
|
2964
|
+
resolveTemplatePathFromName(["json", "config", "3mf"]) || explicitTemplatePath || undefined,
|
|
2965
|
+
TEMP_DIR
|
|
2966
|
+
);
|
|
2967
|
+
const explicitSlicerProfile = hasExplicitSlicerProfile(args);
|
|
2968
|
+
const printerPreset = BAMBU_MODEL_PRESETS[sliceModel]?.(nozzleDiam);
|
|
2969
|
+
|
|
2970
|
+
const sliceBambuOptions: BambuSliceOptions = {};
|
|
2971
|
+
if (args?.uptodate !== undefined) sliceBambuOptions.uptodate = Boolean(args.uptodate);
|
|
2972
|
+
if (args?.repetitions !== undefined) sliceBambuOptions.repetitions = Number(args.repetitions);
|
|
2973
|
+
if (args?.orient !== undefined) sliceBambuOptions.orient = Boolean(args.orient);
|
|
2974
|
+
if (args?.arrange !== undefined) sliceBambuOptions.arrange = Boolean(args.arrange);
|
|
2975
|
+
if (args?.ensure_on_bed !== undefined) sliceBambuOptions.ensureOnBed = Boolean(args.ensure_on_bed);
|
|
2976
|
+
if (args?.clone_objects !== undefined) sliceBambuOptions.cloneObjects = String(args.clone_objects);
|
|
2977
|
+
if (args?.skip_objects !== undefined) sliceBambuOptions.skipObjects = String(args.skip_objects);
|
|
2978
|
+
if (
|
|
2979
|
+
args?.load_filaments !== undefined &&
|
|
2980
|
+
args?.filament_profile !== undefined &&
|
|
2981
|
+
String(args.load_filaments) !== String(args.filament_profile)
|
|
2982
|
+
) {
|
|
2983
|
+
throw new Error("Provide either load_filaments or filament_profile, not conflicting values.");
|
|
2984
|
+
}
|
|
2985
|
+
if (args?.load_filaments !== undefined) {
|
|
2986
|
+
sliceBambuOptions.loadFilaments = String(args.load_filaments);
|
|
2987
|
+
} else if (args?.filament_profile !== undefined) {
|
|
2988
|
+
sliceBambuOptions.loadFilaments = String(args.filament_profile);
|
|
2989
|
+
}
|
|
2990
|
+
if (args?.load_filament_ids !== undefined) sliceBambuOptions.loadFilamentIds = String(args.load_filament_ids);
|
|
2991
|
+
sliceBambuOptions.bedType = resolveBambuStudioCliBedType(args?.bed_type as string | undefined);
|
|
2992
|
+
if (args?.enable_timelapse !== undefined) sliceBambuOptions.enableTimelapse = Boolean(args.enable_timelapse);
|
|
2993
|
+
if (args?.allow_mix_temp !== undefined) sliceBambuOptions.allowMixTemp = Boolean(args.allow_mix_temp);
|
|
2994
|
+
if (args?.scale !== undefined) sliceBambuOptions.scale = Number(args.scale);
|
|
2995
|
+
if (args?.rotate !== undefined) sliceBambuOptions.rotate = Number(args.rotate);
|
|
2996
|
+
if (args?.rotate_x !== undefined) sliceBambuOptions.rotateX = Number(args.rotate_x);
|
|
2997
|
+
if (args?.rotate_y !== undefined) sliceBambuOptions.rotateY = Number(args.rotate_y);
|
|
2998
|
+
if (args?.min_save !== undefined) sliceBambuOptions.minSave = Boolean(args.min_save);
|
|
2999
|
+
if (args?.skip_modified_gcodes !== undefined) sliceBambuOptions.skipModifiedGcodes = Boolean(args.skip_modified_gcodes);
|
|
3000
|
+
if (args?.slice_plate !== undefined) sliceBambuOptions.slicePlate = Number(args.slice_plate);
|
|
3001
|
+
const usePrinterFilaments =
|
|
3002
|
+
args?.use_printer_filaments !== undefined ? Boolean(args.use_printer_filaments) : true;
|
|
3003
|
+
if (
|
|
3004
|
+
usePrinterFilaments &&
|
|
3005
|
+
!explicitSlicerProfile &&
|
|
3006
|
+
!sliceBambuOptions.loadFilaments &&
|
|
3007
|
+
bambuSerial &&
|
|
3008
|
+
bambuToken
|
|
3009
|
+
) {
|
|
3010
|
+
try {
|
|
3011
|
+
const liveFilaments = await this.getResolvedPrinterFilamentInventory(
|
|
3012
|
+
host,
|
|
3013
|
+
bambuSerial,
|
|
3014
|
+
bambuToken,
|
|
3015
|
+
sliceModel,
|
|
3016
|
+
nozzleDiam
|
|
3017
|
+
);
|
|
3018
|
+
if (liveFilaments.recommended?.load_filaments) {
|
|
3019
|
+
sliceBambuOptions.loadFilaments = liveFilaments.recommended.load_filaments;
|
|
3020
|
+
}
|
|
3021
|
+
} catch (filamentError) {
|
|
3022
|
+
console.warn("Could not resolve live printer filaments for slicing:", filamentError);
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
|
|
3026
|
+
result = await this.stlManipulator.sliceSTL(
|
|
3027
|
+
String(args.stl_path), slicerType, slicerPath,
|
|
3028
|
+
activeSlicerProfile,
|
|
3029
|
+
undefined,
|
|
3030
|
+
printerPreset,
|
|
3031
|
+
sliceBambuOptions
|
|
3032
|
+
);
|
|
3033
|
+
break;
|
|
3034
|
+
}
|
|
3035
|
+
|
|
1336
3036
|
case "slice_stl": {
|
|
1337
3037
|
if (!args?.stl_path) {
|
|
1338
3038
|
throw new Error("Missing required parameter: stl_path");
|
|
1339
3039
|
}
|
|
1340
|
-
const { slicerType, slicerPath, slicerProfile } = resolveSlicerConfig(args);
|
|
3040
|
+
const { slicerType, slicerPath, slicerProfile } = resolveSlicerConfig(args as Record<string, unknown> | undefined);
|
|
1341
3041
|
const sliceModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
1342
3042
|
const nozzleDiam = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
3043
|
+
const activeSlicerProfile = await resolveSlicerProfilePath(
|
|
3044
|
+
slicerProfile || undefined,
|
|
3045
|
+
resolveTemplatePathFromName(["json", "config", "3mf"]) || explicitTemplatePath || undefined,
|
|
3046
|
+
TEMP_DIR
|
|
3047
|
+
);
|
|
3048
|
+
const explicitSlicerProfile = hasExplicitSlicerProfile(args);
|
|
1343
3049
|
// Resolve printer preset for BambuStudio slicer
|
|
1344
3050
|
const printerPreset = BAMBU_MODEL_PRESETS[sliceModel]?.(nozzleDiam);
|
|
1345
3051
|
|
|
@@ -1364,6 +3070,7 @@ class BambuPrinterMCPServer {
|
|
|
1364
3070
|
sliceBambuOptions.loadFilaments = String(args.filament_profile);
|
|
1365
3071
|
}
|
|
1366
3072
|
if (args?.load_filament_ids !== undefined) sliceBambuOptions.loadFilamentIds = String(args.load_filament_ids);
|
|
3073
|
+
sliceBambuOptions.bedType = resolveBambuStudioCliBedType(args?.bed_type as string | undefined);
|
|
1367
3074
|
if (args?.enable_timelapse !== undefined) sliceBambuOptions.enableTimelapse = Boolean(args.enable_timelapse);
|
|
1368
3075
|
if (args?.allow_mix_temp !== undefined) sliceBambuOptions.allowMixTemp = Boolean(args.allow_mix_temp);
|
|
1369
3076
|
if (args?.scale !== undefined) sliceBambuOptions.scale = Number(args.scale);
|
|
@@ -1373,10 +3080,34 @@ class BambuPrinterMCPServer {
|
|
|
1373
3080
|
if (args?.min_save !== undefined) sliceBambuOptions.minSave = Boolean(args.min_save);
|
|
1374
3081
|
if (args?.skip_modified_gcodes !== undefined) sliceBambuOptions.skipModifiedGcodes = Boolean(args.skip_modified_gcodes);
|
|
1375
3082
|
if (args?.slice_plate !== undefined) sliceBambuOptions.slicePlate = Number(args.slice_plate);
|
|
3083
|
+
const usePrinterFilaments =
|
|
3084
|
+
args?.use_printer_filaments !== undefined ? Boolean(args.use_printer_filaments) : true;
|
|
3085
|
+
if (
|
|
3086
|
+
usePrinterFilaments &&
|
|
3087
|
+
!explicitSlicerProfile &&
|
|
3088
|
+
!sliceBambuOptions.loadFilaments &&
|
|
3089
|
+
bambuSerial &&
|
|
3090
|
+
bambuToken
|
|
3091
|
+
) {
|
|
3092
|
+
try {
|
|
3093
|
+
const liveFilaments = await this.getResolvedPrinterFilamentInventory(
|
|
3094
|
+
host,
|
|
3095
|
+
bambuSerial,
|
|
3096
|
+
bambuToken,
|
|
3097
|
+
sliceModel,
|
|
3098
|
+
nozzleDiam
|
|
3099
|
+
);
|
|
3100
|
+
if (liveFilaments.recommended?.load_filaments) {
|
|
3101
|
+
sliceBambuOptions.loadFilaments = liveFilaments.recommended.load_filaments;
|
|
3102
|
+
}
|
|
3103
|
+
} catch (filamentError) {
|
|
3104
|
+
console.warn("Could not resolve live printer filaments for slicing:", filamentError);
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
1376
3107
|
|
|
1377
3108
|
result = await this.stlManipulator.sliceSTL(
|
|
1378
3109
|
String(args.stl_path), slicerType, slicerPath,
|
|
1379
|
-
|
|
3110
|
+
activeSlicerProfile,
|
|
1380
3111
|
undefined, // progressCallback
|
|
1381
3112
|
printerPreset,
|
|
1382
3113
|
sliceBambuOptions
|
|
@@ -1388,20 +3119,25 @@ class BambuPrinterMCPServer {
|
|
|
1388
3119
|
if (!args?.three_mf_path) {
|
|
1389
3120
|
throw new Error("Missing required parameter: three_mf_path");
|
|
1390
3121
|
}
|
|
1391
|
-
|
|
1392
3122
|
if (String(args?.connection_mode || "lan_mqtt_ftps") === "bambu_network") {
|
|
1393
3123
|
result = await this.print3mfViaBambuNetwork(args as Record<string, any>, host, bambuSerial, bambuToken);
|
|
1394
3124
|
break;
|
|
1395
3125
|
}
|
|
1396
|
-
|
|
1397
3126
|
if (!bambuSerial || !bambuToken) {
|
|
1398
3127
|
throw new Error("Bambu serial number and access token are required for print_3mf.");
|
|
1399
3128
|
}
|
|
1400
3129
|
|
|
1401
|
-
const { slicerType, slicerPath, slicerProfile } = resolveSlicerConfig(args);
|
|
3130
|
+
const { slicerType, slicerPath, slicerProfile } = resolveSlicerConfig(args as Record<string, unknown> | undefined);
|
|
1402
3131
|
const printModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
1403
3132
|
const printBedType = resolveBedType(args?.bed_type as string | undefined);
|
|
1404
3133
|
const printNozzle = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
3134
|
+
const activeSlicerProfile = await resolveTemplateFirstSlicerProfilePath(
|
|
3135
|
+
args,
|
|
3136
|
+
slicerProfile || undefined,
|
|
3137
|
+
resolveTemplatePathFromName(["json", "config", "3mf"]) || explicitTemplatePath || undefined,
|
|
3138
|
+
TEMP_DIR
|
|
3139
|
+
);
|
|
3140
|
+
const explicitSlicerProfile = hasExplicitSlicerProfile(args);
|
|
1405
3141
|
const printPreset = BAMBU_MODEL_PRESETS[printModel]?.(printNozzle);
|
|
1406
3142
|
const plateIndex = args?.plate_index !== undefined ? Number(args.plate_index) : 0;
|
|
1407
3143
|
|
|
@@ -1420,15 +3156,37 @@ class BambuPrinterMCPServer {
|
|
|
1420
3156
|
f => f.match(/Metadata\/plate_\d+\.gcode/i) || f.endsWith('.gcode')
|
|
1421
3157
|
);
|
|
1422
3158
|
if (!hasGcode) {
|
|
1423
|
-
|
|
3159
|
+
if (printBedType === "supertack_plate") {
|
|
3160
|
+
throw new Error(
|
|
3161
|
+
'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.'
|
|
3162
|
+
);
|
|
3163
|
+
}
|
|
3164
|
+
console.log(`3MF has no gcode — auto-slicing with ${slicerType} for ${printModel}`);
|
|
1424
3165
|
const autoSliceOptions: BambuSliceOptions = {
|
|
1425
3166
|
uptodate: true,
|
|
1426
3167
|
ensureOnBed: true,
|
|
1427
3168
|
minSave: true,
|
|
1428
3169
|
skipModifiedGcodes: true,
|
|
3170
|
+
bedType: printBedType,
|
|
1429
3171
|
};
|
|
3172
|
+
if (!explicitSlicerProfile) {
|
|
3173
|
+
try {
|
|
3174
|
+
const liveFilaments = await this.getResolvedPrinterFilamentInventory(
|
|
3175
|
+
host,
|
|
3176
|
+
bambuSerial,
|
|
3177
|
+
bambuToken,
|
|
3178
|
+
printModel,
|
|
3179
|
+
printNozzle
|
|
3180
|
+
);
|
|
3181
|
+
if (liveFilaments.recommended?.load_filaments) {
|
|
3182
|
+
autoSliceOptions.loadFilaments = liveFilaments.recommended.load_filaments;
|
|
3183
|
+
}
|
|
3184
|
+
} catch (filamentError) {
|
|
3185
|
+
console.warn("Could not resolve live printer filaments for auto-slicing:", filamentError);
|
|
3186
|
+
}
|
|
3187
|
+
}
|
|
1430
3188
|
threeMFPath = await this.stlManipulator.sliceSTL(
|
|
1431
|
-
threeMFPath, slicerType, slicerPath,
|
|
3189
|
+
threeMFPath, slicerType, slicerPath, activeSlicerProfile,
|
|
1432
3190
|
undefined, // progressCallback
|
|
1433
3191
|
printPreset,
|
|
1434
3192
|
autoSliceOptions
|
|
@@ -1436,43 +3194,93 @@ class BambuPrinterMCPServer {
|
|
|
1436
3194
|
console.log("Auto-sliced to: " + threeMFPath);
|
|
1437
3195
|
}
|
|
1438
3196
|
} catch (sliceCheckErr: any) {
|
|
3197
|
+
if (String(sliceCheckErr?.message || "").includes("SuperTack")) {
|
|
3198
|
+
throw sliceCheckErr;
|
|
3199
|
+
}
|
|
1439
3200
|
console.warn("Could not check/slice 3MF, proceeding with original:", sliceCheckErr.message);
|
|
1440
3201
|
}
|
|
1441
3202
|
|
|
1442
3203
|
const parsed3MFData = await parse3MF(threeMFPath);
|
|
3204
|
+
const isH2Print = printModel === "h2s" || printModel === "h2d";
|
|
1443
3205
|
let parsedAmsMapping: number[] | undefined;
|
|
1444
|
-
if (parsed3MFData.slicerConfig?.ams_mapping) {
|
|
1445
|
-
const slots =
|
|
1446
|
-
.filter(v => typeof v === 'number') as number[];
|
|
3206
|
+
if (!isH2Print && parsed3MFData.slicerConfig?.ams_mapping) {
|
|
3207
|
+
const slots = normalizeAmsMappingObject(parsed3MFData.slicerConfig.ams_mapping as Record<string, unknown>);
|
|
1447
3208
|
if (slots.length > 0) {
|
|
1448
|
-
parsedAmsMapping = slots
|
|
3209
|
+
parsedAmsMapping = slots;
|
|
1449
3210
|
}
|
|
1450
3211
|
}
|
|
1451
3212
|
|
|
1452
3213
|
let finalAmsMapping = parsedAmsMapping;
|
|
1453
|
-
|
|
1454
|
-
let useAMS =
|
|
3214
|
+
let finalAmsSlots: number[] | undefined;
|
|
3215
|
+
let useAMS = args?.use_ams !== undefined ? Boolean(args.use_ams) : (!!finalAmsMapping && finalAmsMapping.length > 0);
|
|
3216
|
+
const hasUserAmsMapping = hasAmsMappingInput(args?.ams_mapping);
|
|
3217
|
+
const hasUserAmsSlots = Array.isArray(args?.ams_slots);
|
|
1455
3218
|
|
|
1456
|
-
if (
|
|
3219
|
+
if (hasUserAmsMapping) {
|
|
1457
3220
|
let userMappingOverride: number[] | undefined;
|
|
1458
3221
|
if (Array.isArray(args.ams_mapping)) {
|
|
1459
|
-
userMappingOverride = args.ams_mapping.
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
3222
|
+
userMappingOverride = args.ams_mapping.map((v: unknown, i: number) =>
|
|
3223
|
+
normalizeBridgeAmsTrayValue(v, `ams_mapping[${i}]`)
|
|
3224
|
+
);
|
|
3225
|
+
} else if (args.ams_mapping && typeof args.ams_mapping === 'object') {
|
|
3226
|
+
userMappingOverride = normalizeAmsMappingObject(args.ams_mapping as Record<string, unknown>);
|
|
1464
3227
|
}
|
|
1465
3228
|
if (userMappingOverride && userMappingOverride.length > 0) {
|
|
1466
3229
|
finalAmsMapping = userMappingOverride;
|
|
1467
3230
|
useAMS = true;
|
|
1468
3231
|
}
|
|
1469
3232
|
}
|
|
3233
|
+
if (hasUserAmsSlots) {
|
|
3234
|
+
const userSlots = (args.ams_slots as unknown[]).filter((v: any) => typeof v === 'number');
|
|
3235
|
+
if (userSlots.length > 0) {
|
|
3236
|
+
finalAmsSlots = userSlots;
|
|
3237
|
+
finalAmsMapping = undefined;
|
|
3238
|
+
useAMS = true;
|
|
3239
|
+
}
|
|
3240
|
+
}
|
|
3241
|
+
if (!hasUserAmsMapping && !hasUserAmsSlots && args?.auto_match_ams === true && args?.use_ams !== false) {
|
|
3242
|
+
const requirements = await analyze3MFAmsRequirements(threeMFPath, plateIndex);
|
|
3243
|
+
const inventory = await this.getResolvedPrinterFilamentInventory(
|
|
3244
|
+
host,
|
|
3245
|
+
bambuSerial,
|
|
3246
|
+
bambuToken,
|
|
3247
|
+
printModel,
|
|
3248
|
+
printNozzle
|
|
3249
|
+
);
|
|
3250
|
+
const resolved = resolveAmsSlotsFromRequirements(requirements, inventory);
|
|
3251
|
+
if (resolved.missing.length > 0) {
|
|
3252
|
+
throw new Error(
|
|
3253
|
+
`auto_match_ams could not find loaded AMS trays for: ${resolved.missing
|
|
3254
|
+
.map((missing) => missing.tray_info_idx || `filament position ${missing.filament_position}`)
|
|
3255
|
+
.join(", ")}`
|
|
3256
|
+
);
|
|
3257
|
+
}
|
|
3258
|
+
finalAmsMapping = undefined;
|
|
3259
|
+
finalAmsSlots = resolved.ams_slots;
|
|
3260
|
+
useAMS = finalAmsSlots.length > 0;
|
|
3261
|
+
}
|
|
3262
|
+
if (
|
|
3263
|
+
isH2Print &&
|
|
3264
|
+
!hasUserAmsMapping &&
|
|
3265
|
+
!hasUserAmsSlots &&
|
|
3266
|
+
args?.auto_match_ams !== true &&
|
|
3267
|
+
(!finalAmsMapping || finalAmsMapping.length === 0) &&
|
|
3268
|
+
(!finalAmsSlots || finalAmsSlots.length === 0)
|
|
3269
|
+
) {
|
|
3270
|
+
const requirements = await analyze3MFAmsRequirements(threeMFPath, plateIndex);
|
|
3271
|
+
if (requirements.usedFilamentPositions.length > 0) {
|
|
3272
|
+
throw new Error(
|
|
3273
|
+
`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.`
|
|
3274
|
+
);
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
1470
3277
|
|
|
1471
3278
|
if (args?.use_ams === false) {
|
|
1472
3279
|
finalAmsMapping = undefined;
|
|
3280
|
+
finalAmsSlots = undefined;
|
|
1473
3281
|
useAMS = false;
|
|
1474
3282
|
}
|
|
1475
|
-
if ((!finalAmsMapping || finalAmsMapping.length === 0) && !
|
|
3283
|
+
if ((!finalAmsMapping || finalAmsMapping.length === 0) && (!finalAmsSlots || finalAmsSlots.length === 0)) {
|
|
1476
3284
|
useAMS = false;
|
|
1477
3285
|
}
|
|
1478
3286
|
|
|
@@ -1485,6 +3293,7 @@ class BambuPrinterMCPServer {
|
|
|
1485
3293
|
plateIndex,
|
|
1486
3294
|
useAMS: useAMS,
|
|
1487
3295
|
amsMapping: finalAmsMapping,
|
|
3296
|
+
amsSlots: finalAmsSlots,
|
|
1488
3297
|
bedType: printBedType,
|
|
1489
3298
|
bedLeveling: args?.bed_leveling !== undefined ? Boolean(args.bed_leveling) : undefined,
|
|
1490
3299
|
flowCalibration: args?.flow_calibration !== undefined ? Boolean(args.flow_calibration) : undefined,
|
|
@@ -1496,6 +3305,92 @@ class BambuPrinterMCPServer {
|
|
|
1496
3305
|
break;
|
|
1497
3306
|
}
|
|
1498
3307
|
|
|
3308
|
+
case "print_collar_charm": {
|
|
3309
|
+
const resolvedTemplateSourcePath = resolveTemplatePathFromName(["3mf"]);
|
|
3310
|
+
const resolvedSourcePath = String(resolvedTemplateSourcePath || args?.source_path || "");
|
|
3311
|
+
if (!resolvedSourcePath) {
|
|
3312
|
+
throw new Error("Missing required parameter: source_path or template_name");
|
|
3313
|
+
}
|
|
3314
|
+
if (!bambuSerial || !bambuToken) {
|
|
3315
|
+
throw new Error("Bambu serial number and access token are required for print_collar_charm.");
|
|
3316
|
+
}
|
|
3317
|
+
|
|
3318
|
+
const { slicerType, slicerPath, slicerProfile } = resolveSlicerConfig(args as Record<string, unknown> | undefined);
|
|
3319
|
+
const printModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
3320
|
+
const printBedType = resolveBedType(args?.bed_type as string | undefined);
|
|
3321
|
+
const printNozzle = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
3322
|
+
const activeSlicerProfile = await resolveTemplateFirstSlicerProfilePath(
|
|
3323
|
+
args,
|
|
3324
|
+
slicerProfile || undefined,
|
|
3325
|
+
resolvedTemplateSourcePath || explicitTemplatePath || undefined,
|
|
3326
|
+
TEMP_DIR
|
|
3327
|
+
);
|
|
3328
|
+
|
|
3329
|
+
const preparedThreeMFPath = await this.resolveCollarCharmPrepared3MF(
|
|
3330
|
+
resolvedSourcePath,
|
|
3331
|
+
resolvedTemplateSourcePath || explicitTemplatePath || undefined,
|
|
3332
|
+
slicerType,
|
|
3333
|
+
slicerPath,
|
|
3334
|
+
activeSlicerProfile || undefined,
|
|
3335
|
+
printModel,
|
|
3336
|
+
printNozzle,
|
|
3337
|
+
printBedType,
|
|
3338
|
+
host,
|
|
3339
|
+
bambuSerial,
|
|
3340
|
+
bambuToken
|
|
3341
|
+
);
|
|
3342
|
+
|
|
3343
|
+
const collarAnalysis = await analyzeCollarCharm3MF(preparedThreeMFPath, 0);
|
|
3344
|
+
const inventory = await this.preflightCollarCharmPolicy(
|
|
3345
|
+
host,
|
|
3346
|
+
bambuSerial,
|
|
3347
|
+
bambuToken,
|
|
3348
|
+
printModel,
|
|
3349
|
+
printNozzle
|
|
3350
|
+
);
|
|
3351
|
+
|
|
3352
|
+
const projectName = path.basename(preparedThreeMFPath).replace(/\.3mf$/i, '');
|
|
3353
|
+
result = await this.bambu.print3mf(host, bambuSerial, bambuToken, {
|
|
3354
|
+
projectName,
|
|
3355
|
+
filePath: preparedThreeMFPath,
|
|
3356
|
+
plateIndex: collarAnalysis.plateIndex,
|
|
3357
|
+
useAMS: true,
|
|
3358
|
+
amsSlots: collarAnalysis.amsSlots,
|
|
3359
|
+
bedType: printBedType,
|
|
3360
|
+
bedLeveling: args?.bed_leveling !== undefined ? Boolean(args.bed_leveling) : true,
|
|
3361
|
+
flowCalibration: args?.flow_calibration !== undefined ? Boolean(args.flow_calibration) : true,
|
|
3362
|
+
vibrationCalibration: args?.vibration_calibration !== undefined ? Boolean(args.vibration_calibration) : true,
|
|
3363
|
+
timelapse: args?.timelapse !== undefined ? Boolean(args.timelapse) : false,
|
|
3364
|
+
});
|
|
3365
|
+
|
|
3366
|
+
result = {
|
|
3367
|
+
...result,
|
|
3368
|
+
source_path: resolvedSourcePath,
|
|
3369
|
+
prepared_three_mf_path: preparedThreeMFPath,
|
|
3370
|
+
tray_policy: {
|
|
3371
|
+
inner: {
|
|
3372
|
+
color: COLLAR_CHARM_POLICY.colors.inner,
|
|
3373
|
+
absolute_tray: COLLAR_CHARM_POLICY.amsSlots.inner,
|
|
3374
|
+
},
|
|
3375
|
+
outer: {
|
|
3376
|
+
color: COLLAR_CHARM_POLICY.colors.outer,
|
|
3377
|
+
absolute_tray: COLLAR_CHARM_POLICY.amsSlots.outer,
|
|
3378
|
+
},
|
|
3379
|
+
},
|
|
3380
|
+
collar_roles: collarAnalysis.roles,
|
|
3381
|
+
inventory_slots_checked: inventory.trays
|
|
3382
|
+
.filter((tray) => tray.slot === COLLAR_CHARM_POLICY.amsSlots.inner || tray.slot === COLLAR_CHARM_POLICY.amsSlots.outer)
|
|
3383
|
+
.map((tray) => ({
|
|
3384
|
+
slot: tray.slot,
|
|
3385
|
+
loaded: tray.loaded,
|
|
3386
|
+
tray_color: tray.tray_color,
|
|
3387
|
+
tray_type: tray.tray_type,
|
|
3388
|
+
tray_info_idx: tray.tray_info_idx,
|
|
3389
|
+
})),
|
|
3390
|
+
};
|
|
3391
|
+
break;
|
|
3392
|
+
}
|
|
3393
|
+
|
|
1499
3394
|
case "merge_vertices":
|
|
1500
3395
|
if (!args?.stl_path) throw new Error("Missing required parameter: stl_path");
|
|
1501
3396
|
result = await this.stlManipulator.mergeVertices(
|