bambu-printer-mcp 1.0.5 → 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 +537 -38
- 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/bambu-network-bridge.d.ts +63 -0
- package/dist/bambu-network-bridge.js +393 -0
- package/dist/index.js +2084 -95
- package/dist/printers/bambu.d.ts +133 -0
- package/dist/printers/bambu.js +1004 -54
- package/dist/slicer/profile-flatten.d.ts +75 -0
- package/dist/slicer/profile-flatten.js +446 -0
- package/dist/stl/stl-manipulator.d.ts +33 -1
- package/dist/stl/stl-manipulator.js +370 -33
- 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/bambu-network-bridge.ts +526 -0
- package/src/index.ts +2648 -108
- package/src/printers/bambu.ts +1215 -57
- package/src/slicer/profile-flatten.ts +562 -0
- package/src/stl/stl-manipulator.ts +475 -36
- package/src/types.ts +41 -0
package/src/index.ts
CHANGED
|
@@ -16,38 +16,485 @@ import fs from "fs";
|
|
|
16
16
|
import path from "path";
|
|
17
17
|
import { createServer as createHttpServer, type IncomingMessage, type Server as HttpServer, type ServerResponse } from "node:http";
|
|
18
18
|
import { randomUUID } from "node:crypto";
|
|
19
|
-
import {
|
|
20
|
-
|
|
19
|
+
import {
|
|
20
|
+
STLManipulator,
|
|
21
|
+
SLICER_TYPES,
|
|
22
|
+
normalizeSlicerType,
|
|
23
|
+
type BambuSliceOptions,
|
|
24
|
+
type SlicerType,
|
|
25
|
+
} from "./stl/stl-manipulator.js";
|
|
26
|
+
import { BambuNetworkBridge, type BambuNetworkBridgeOptions } from "./bambu-network-bridge.js";
|
|
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";
|
|
21
30
|
import { BambuImplementation } from "./printers/bambu.js";
|
|
22
31
|
|
|
23
32
|
dotenv.config();
|
|
24
33
|
|
|
25
|
-
const DEFAULT_HOST = process.env.PRINTER_HOST || "localhost";
|
|
26
|
-
const DEFAULT_BAMBU_SERIAL = process.env.BAMBU_SERIAL || "";
|
|
27
|
-
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 || "";
|
|
38
|
+
const DEFAULT_BAMBU_DEV_ID = process.env.BAMBU_DEV_ID || DEFAULT_BAMBU_SERIAL;
|
|
28
39
|
const TEMP_DIR = process.env.TEMP_DIR || path.join(process.cwd(), "temp");
|
|
29
40
|
|
|
30
41
|
// Printer model and bed type
|
|
31
|
-
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
|
+
"";
|
|
32
46
|
const DEFAULT_BED_TYPE = process.env.BED_TYPE?.trim().toLowerCase() || "textured_plate";
|
|
33
47
|
const DEFAULT_NOZZLE_DIAMETER = process.env.NOZZLE_DIAMETER?.trim() || "0.4";
|
|
34
48
|
|
|
35
|
-
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;
|
|
36
50
|
type BambuModel = typeof VALID_BAMBU_MODELS[number];
|
|
37
51
|
|
|
38
|
-
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;
|
|
39
54
|
|
|
40
55
|
// Map model IDs to BambuStudio --load-machine preset names
|
|
41
56
|
const BAMBU_MODEL_PRESETS: Record<string, (nozzle: string) => string> = {
|
|
42
57
|
p1s: (n) => `Bambu Lab P1S ${n} nozzle`,
|
|
43
58
|
p1p: (n) => `Bambu Lab P1P ${n} nozzle`,
|
|
59
|
+
p2s: (n) => `Bambu Lab P2S ${n} nozzle`,
|
|
44
60
|
x1c: (n) => `Bambu Lab X1 Carbon ${n} nozzle`,
|
|
45
61
|
x1e: (n) => `Bambu Lab X1E ${n} nozzle`,
|
|
46
62
|
a1: (n) => `Bambu Lab A1 ${n} nozzle`,
|
|
47
63
|
a1mini: (n) => `Bambu Lab A1 mini ${n} nozzle`,
|
|
48
64
|
h2d: (n) => `Bambu Lab H2D ${n} nozzle`,
|
|
65
|
+
h2s: (n) => `Bambu Lab H2S ${n} nozzle`,
|
|
66
|
+
};
|
|
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;
|
|
49
129
|
};
|
|
50
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
|
+
|
|
51
498
|
function validateBambuModel(model: string): string {
|
|
52
499
|
const normalized = model.trim().toLowerCase();
|
|
53
500
|
if (!VALID_BAMBU_MODELS.includes(normalized as BambuModel)) {
|
|
@@ -68,10 +515,79 @@ function resolveBedType(argsBedType: string | undefined): string {
|
|
|
68
515
|
return bedType;
|
|
69
516
|
}
|
|
70
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
|
+
|
|
71
528
|
// Slicer configuration (defaults to bambustudio)
|
|
72
529
|
const DEFAULT_SLICER_TYPE = process.env.SLICER_TYPE || "bambustudio";
|
|
73
|
-
const
|
|
74
|
-
const
|
|
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");
|
|
535
|
+
const SLICER_SCHEMA_VALUES = [
|
|
536
|
+
...SLICER_TYPES,
|
|
537
|
+
"fulu-orca",
|
|
538
|
+
"fulu-orcaslicer",
|
|
539
|
+
"orca-studio",
|
|
540
|
+
"orca-bambulab",
|
|
541
|
+
];
|
|
542
|
+
|
|
543
|
+
function firstExistingPath(paths: string[], fallback: string): string {
|
|
544
|
+
return paths.find((candidate) => fs.existsSync(candidate)) || fallback;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function defaultSlicerPathFor(slicerType: SlicerType): string {
|
|
548
|
+
if (slicerType === "orcaslicer" || slicerType === "orcaslicer-bambulab") {
|
|
549
|
+
if (process.platform === "darwin") {
|
|
550
|
+
return firstExistingPath(
|
|
551
|
+
[
|
|
552
|
+
"/Applications/OrcaSlicer.app/Contents/MacOS/OrcaSlicer",
|
|
553
|
+
"/Applications/Orca Studio.app/Contents/MacOS/Orca Studio",
|
|
554
|
+
"/Applications/OrcaSlicer-BMCU.app/Contents/MacOS/OrcaSlicer",
|
|
555
|
+
],
|
|
556
|
+
"/Applications/OrcaSlicer.app/Contents/MacOS/OrcaSlicer"
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
if (process.platform === "win32") {
|
|
560
|
+
return "C:\\Program Files\\OrcaSlicer\\OrcaSlicer.exe";
|
|
561
|
+
}
|
|
562
|
+
return "OrcaSlicer";
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (slicerType === "bambustudio") {
|
|
566
|
+
if (process.env.BAMBU_STUDIO_PATH) {
|
|
567
|
+
return process.env.BAMBU_STUDIO_PATH;
|
|
568
|
+
}
|
|
569
|
+
if (process.platform === "darwin") {
|
|
570
|
+
return "/Applications/BambuStudio.app/Contents/MacOS/BambuStudio";
|
|
571
|
+
}
|
|
572
|
+
if (process.platform === "win32") {
|
|
573
|
+
return "C:\\Program Files\\Bambu Studio\\bambu-studio.exe";
|
|
574
|
+
}
|
|
575
|
+
return "BambuStudio";
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return slicerType;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function resolveSlicerConfig(args: Record<string, unknown> | undefined): {
|
|
582
|
+
slicerType: SlicerType;
|
|
583
|
+
slicerPath: string;
|
|
584
|
+
slicerProfile: string;
|
|
585
|
+
} {
|
|
586
|
+
const slicerType = normalizeSlicerType(String(args?.slicer_type || DEFAULT_SLICER_TYPE));
|
|
587
|
+
const slicerPath = String(args?.slicer_path || process.env.SLICER_PATH || defaultSlicerPathFor(slicerType));
|
|
588
|
+
const slicerProfile = String(args?.slicer_profile || DEFAULT_SLICER_PROFILE);
|
|
589
|
+
return { slicerType, slicerPath, slicerProfile };
|
|
590
|
+
}
|
|
75
591
|
|
|
76
592
|
type RuntimeConfig = {
|
|
77
593
|
transport: "stdio" | "streamable-http";
|
|
@@ -112,6 +628,57 @@ function parseCsvEnv(value: string | undefined): Set<string> {
|
|
|
112
628
|
return new Set(value.split(",").map((e) => e.trim()).filter((e) => e.length > 0));
|
|
113
629
|
}
|
|
114
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
|
+
|
|
115
682
|
function readRuntimeConfig(): RuntimeConfig {
|
|
116
683
|
const rawTransport = process.env.MCP_TRANSPORT?.trim().toLowerCase();
|
|
117
684
|
const transport =
|
|
@@ -139,6 +706,298 @@ type StructuredToolError = {
|
|
|
139
706
|
tool: string;
|
|
140
707
|
};
|
|
141
708
|
|
|
709
|
+
type UploadGcodeSource = {
|
|
710
|
+
filePath: string;
|
|
711
|
+
cleanupDir?: string;
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
function expandUserPath(rawPath: string): string {
|
|
715
|
+
const trimmed = rawPath.trim();
|
|
716
|
+
if (trimmed === "~") {
|
|
717
|
+
return process.env.HOME || trimmed;
|
|
718
|
+
}
|
|
719
|
+
if (trimmed.startsWith("~/")) {
|
|
720
|
+
return path.join(process.env.HOME || "", trimmed.slice(2));
|
|
721
|
+
}
|
|
722
|
+
return path.resolve(trimmed);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function readableFilePathFromString(value: string): string | undefined {
|
|
726
|
+
if (!value.trim() || value.includes("\n") || value.includes("\r")) {
|
|
727
|
+
return undefined;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const candidate = expandUserPath(value);
|
|
731
|
+
try {
|
|
732
|
+
return fs.statSync(candidate).isFile() ? candidate : undefined;
|
|
733
|
+
} catch {
|
|
734
|
+
return undefined;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function looksLikeGcodeFilePath(value: string): boolean {
|
|
739
|
+
const trimmed = value.trim();
|
|
740
|
+
if (!trimmed || trimmed.includes("\n") || trimmed.includes("\r")) {
|
|
741
|
+
return false;
|
|
742
|
+
}
|
|
743
|
+
return (
|
|
744
|
+
trimmed.startsWith("/") ||
|
|
745
|
+
trimmed.startsWith("./") ||
|
|
746
|
+
trimmed.startsWith("../") ||
|
|
747
|
+
trimmed.startsWith("~/") ||
|
|
748
|
+
trimmed.includes("\\") ||
|
|
749
|
+
/\.(gcode|gco|gc)$/i.test(trimmed)
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function requireReadableFilePath(rawPath: string, label: string): string {
|
|
754
|
+
const candidate = expandUserPath(rawPath);
|
|
755
|
+
try {
|
|
756
|
+
if (!fs.statSync(candidate).isFile()) {
|
|
757
|
+
throw new Error(`${label} is not a file: ${candidate}`);
|
|
758
|
+
}
|
|
759
|
+
} catch (error) {
|
|
760
|
+
if ((error as Error).message.startsWith(`${label} is not a file:`)) {
|
|
761
|
+
throw error;
|
|
762
|
+
}
|
|
763
|
+
throw new Error(`${label} does not exist or is not readable: ${candidate}`);
|
|
764
|
+
}
|
|
765
|
+
return candidate;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function writeGcodeContentToTempFile(filename: string, gcode: string): UploadGcodeSource {
|
|
769
|
+
fs.mkdirSync(TEMP_DIR, { recursive: true });
|
|
770
|
+
const cleanupDir = fs.mkdtempSync(path.join(TEMP_DIR, "upload-gcode-"));
|
|
771
|
+
const safeName = path.basename(filename.replace(/^\/+/, "")) || "upload.gcode";
|
|
772
|
+
const filePath = path.join(cleanupDir, safeName);
|
|
773
|
+
fs.writeFileSync(filePath, gcode);
|
|
774
|
+
return { filePath, cleanupDir };
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function resolveUploadGcodeSource(args: Record<string, unknown>): UploadGcodeSource {
|
|
778
|
+
const gcodePath = args.gcode_path !== undefined ? String(args.gcode_path) : "";
|
|
779
|
+
const gcode = args.gcode !== undefined ? String(args.gcode) : "";
|
|
780
|
+
|
|
781
|
+
if (gcodePath && gcode) {
|
|
782
|
+
throw new Error("Provide either gcode_path or gcode, not both.");
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
if (gcodePath) {
|
|
786
|
+
return { filePath: requireReadableFilePath(gcodePath, "gcode_path") };
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
if (!gcode) {
|
|
790
|
+
throw new Error("Missing required parameter: gcode or gcode_path");
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
const detectedPath = readableFilePathFromString(gcode);
|
|
794
|
+
if (detectedPath) {
|
|
795
|
+
return { filePath: detectedPath };
|
|
796
|
+
}
|
|
797
|
+
if (looksLikeGcodeFilePath(gcode)) {
|
|
798
|
+
throw new Error(
|
|
799
|
+
"gcode looks like a local G-code path, but the file is not readable. " +
|
|
800
|
+
"Pass readable gcode_path or literal G-code content."
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
return writeGcodeContentToTempFile(String(args.filename), gcode);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
const BAMBU_NETWORK_PRINT_METHODS = [
|
|
808
|
+
"start_print",
|
|
809
|
+
"start_local_print",
|
|
810
|
+
"start_local_print_with_record",
|
|
811
|
+
"start_send_gcode_to_sdcard",
|
|
812
|
+
"start_sdcard_print",
|
|
813
|
+
] as const;
|
|
814
|
+
|
|
815
|
+
type BambuNetworkPrintMethod = typeof BAMBU_NETWORK_PRINT_METHODS[number];
|
|
816
|
+
|
|
817
|
+
function resolveBambuNetworkPrintMethod(
|
|
818
|
+
rawMethod: string | undefined,
|
|
819
|
+
connectionType: string
|
|
820
|
+
): BambuNetworkPrintMethod {
|
|
821
|
+
const defaultMethod = connectionType === "lan" ? "start_local_print" : "start_print";
|
|
822
|
+
const method = (rawMethod || defaultMethod).trim();
|
|
823
|
+
|
|
824
|
+
if (!(BAMBU_NETWORK_PRINT_METHODS as readonly string[]).includes(method)) {
|
|
825
|
+
throw new Error(
|
|
826
|
+
`Invalid bambu_network_method: "${method}". Valid methods: ${BAMBU_NETWORK_PRINT_METHODS.join(", ")}`
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
return method as BambuNetworkPrintMethod;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function toBridgeMethod(method: BambuNetworkPrintMethod): string {
|
|
834
|
+
return `net.${method}`;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function stringifyBridgeJson(value: unknown): string | undefined {
|
|
838
|
+
if (value === undefined || value === null) return undefined;
|
|
839
|
+
if (typeof value === "string") return value;
|
|
840
|
+
return JSON.stringify(value);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function redactPrintParams(params: Record<string, unknown>): Record<string, unknown> {
|
|
844
|
+
return {
|
|
845
|
+
...params,
|
|
846
|
+
password: params.password ? "[redacted]" : "",
|
|
847
|
+
};
|
|
848
|
+
}
|
|
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
|
+
|
|
142
1001
|
if (!fs.existsSync(TEMP_DIR)) {
|
|
143
1002
|
fs.mkdirSync(TEMP_DIR, { recursive: true });
|
|
144
1003
|
}
|
|
@@ -146,6 +1005,7 @@ if (!fs.existsSync(TEMP_DIR)) {
|
|
|
146
1005
|
class BambuPrinterMCPServer {
|
|
147
1006
|
private server: Server;
|
|
148
1007
|
private bambu: BambuImplementation;
|
|
1008
|
+
private bambuNetwork: BambuNetworkBridge;
|
|
149
1009
|
private stlManipulator: STLManipulator;
|
|
150
1010
|
private readonly runtimeConfig: RuntimeConfig;
|
|
151
1011
|
private httpRuntime?: { transport: StreamableHTTPServerTransport; httpServer: HttpServer };
|
|
@@ -166,6 +1026,7 @@ class BambuPrinterMCPServer {
|
|
|
166
1026
|
);
|
|
167
1027
|
|
|
168
1028
|
this.bambu = new BambuImplementation();
|
|
1029
|
+
this.bambuNetwork = new BambuNetworkBridge();
|
|
169
1030
|
this.stlManipulator = new STLManipulator(TEMP_DIR);
|
|
170
1031
|
|
|
171
1032
|
this.setupHandlers();
|
|
@@ -210,11 +1071,13 @@ class BambuPrinterMCPServer {
|
|
|
210
1071
|
oneOf: [
|
|
211
1072
|
{ const: "p1s", title: "P1S" },
|
|
212
1073
|
{ const: "p1p", title: "P1P" },
|
|
1074
|
+
{ const: "p2s", title: "P2S" },
|
|
213
1075
|
{ const: "x1c", title: "X1 Carbon" },
|
|
214
1076
|
{ const: "x1e", title: "X1E" },
|
|
215
1077
|
{ const: "a1", title: "A1" },
|
|
216
1078
|
{ const: "a1mini", title: "A1 Mini" },
|
|
217
1079
|
{ const: "h2d", title: "H2D" },
|
|
1080
|
+
{ const: "h2s", title: "H2S" },
|
|
218
1081
|
],
|
|
219
1082
|
},
|
|
220
1083
|
},
|
|
@@ -246,56 +1109,613 @@ class BambuPrinterMCPServer {
|
|
|
246
1109
|
}
|
|
247
1110
|
}
|
|
248
1111
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
},
|
|
259
|
-
{
|
|
260
|
-
uri: `printer://${DEFAULT_HOST}/files`,
|
|
261
|
-
name: "Bambu Printer Files",
|
|
262
|
-
mimeType: "application/json",
|
|
263
|
-
description: "List of files on the Bambu Lab printer"
|
|
264
|
-
}
|
|
265
|
-
],
|
|
266
|
-
templates: [
|
|
267
|
-
{
|
|
268
|
-
uriTemplate: "printer://{host}/status",
|
|
269
|
-
name: "Bambu Printer Status",
|
|
270
|
-
mimeType: "application/json"
|
|
271
|
-
},
|
|
272
|
-
{
|
|
273
|
-
uriTemplate: "printer://{host}/files",
|
|
274
|
-
name: "Bambu Printer Files",
|
|
275
|
-
mimeType: "application/json"
|
|
276
|
-
}
|
|
277
|
-
]
|
|
278
|
-
};
|
|
279
|
-
});
|
|
1112
|
+
private bridgeOptionsFromArgs(args: Record<string, any> | undefined): BambuNetworkBridgeOptions {
|
|
1113
|
+
return {
|
|
1114
|
+
bridgeCommand: args?.bridge_command !== undefined ? String(args.bridge_command) : undefined,
|
|
1115
|
+
configDir: args?.bambu_network_config_dir !== undefined ? String(args.bambu_network_config_dir) : undefined,
|
|
1116
|
+
countryCode: args?.country_code !== undefined ? String(args.country_code) : undefined,
|
|
1117
|
+
userInfo: args?.user_info !== undefined ? String(args.user_info) : undefined,
|
|
1118
|
+
timeoutMs: args?.timeout_ms !== undefined ? Number(args.timeout_ms) : undefined,
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
280
1121
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
1122
|
+
private async ensurePrintableThreeMFPath(
|
|
1123
|
+
args: Record<string, any>,
|
|
1124
|
+
printModel: string,
|
|
1125
|
+
printPreset: string | undefined,
|
|
1126
|
+
bedType?: string
|
|
1127
|
+
): Promise<{ threeMFPath: string; autoSliced: boolean }> {
|
|
1128
|
+
const { slicerType, slicerPath, slicerProfile } = resolveSlicerConfig(args);
|
|
1129
|
+
let threeMFPath = String(args.three_mf_path);
|
|
1130
|
+
|
|
1131
|
+
const JSZip = (await import('jszip')).default;
|
|
1132
|
+
const zipData = fs.readFileSync(threeMFPath);
|
|
1133
|
+
const zip = await JSZip.loadAsync(zipData);
|
|
1134
|
+
const hasGcode = Object.keys(zip.files).some(
|
|
1135
|
+
f => f.match(/Metadata\/plate_\d+\.gcode/i) || f.endsWith('.gcode')
|
|
1136
|
+
);
|
|
284
1137
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
1138
|
+
if (hasGcode) {
|
|
1139
|
+
return { threeMFPath, autoSliced: false };
|
|
1140
|
+
}
|
|
288
1141
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
+
}
|
|
293
1147
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
1148
|
+
console.log(`3MF has no gcode - auto-slicing with ${slicerType} for ${printModel}`);
|
|
1149
|
+
const autoSliceOptions: BambuSliceOptions = {
|
|
1150
|
+
uptodate: true,
|
|
1151
|
+
ensureOnBed: true,
|
|
1152
|
+
minSave: true,
|
|
1153
|
+
skipModifiedGcodes: true,
|
|
1154
|
+
bedType: bedType ? resolveBambuStudioCliBedType(bedType) : undefined,
|
|
1155
|
+
};
|
|
1156
|
+
threeMFPath = await this.stlManipulator.sliceSTL(
|
|
1157
|
+
threeMFPath, slicerType, slicerPath, slicerProfile || undefined,
|
|
1158
|
+
undefined,
|
|
1159
|
+
printPreset,
|
|
1160
|
+
autoSliceOptions
|
|
1161
|
+
);
|
|
1162
|
+
console.log("Auto-sliced to: " + threeMFPath);
|
|
1163
|
+
return { threeMFPath, autoSliced: true };
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
private async resolveAmsPrintSettings(
|
|
1167
|
+
threeMFPath: string,
|
|
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[] }> {
|
|
1175
|
+
const parsed3MFData = await parse3MF(threeMFPath);
|
|
1176
|
+
let parsedAmsMapping: number[] | undefined;
|
|
1177
|
+
|
|
1178
|
+
if (parsed3MFData.slicerConfig?.ams_mapping) {
|
|
1179
|
+
const slots = normalizeAmsMappingObject(parsed3MFData.slicerConfig.ams_mapping as Record<string, unknown>);
|
|
1180
|
+
if (slots.length > 0) {
|
|
1181
|
+
parsedAmsMapping = slots;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
let finalAmsMapping = parsedAmsMapping;
|
|
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);
|
|
1190
|
+
|
|
1191
|
+
if (hasUserAmsMapping) {
|
|
1192
|
+
let userMappingOverride: number[] | undefined;
|
|
1193
|
+
if (Array.isArray(args.ams_mapping)) {
|
|
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>);
|
|
1199
|
+
}
|
|
1200
|
+
if (userMappingOverride && userMappingOverride.length > 0) {
|
|
1201
|
+
finalAmsMapping = userMappingOverride;
|
|
1202
|
+
finalAmsSlots = undefined;
|
|
1203
|
+
useAMS = true;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
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
|
+
|
|
1237
|
+
if (args?.use_ams === false) {
|
|
1238
|
+
finalAmsMapping = undefined;
|
|
1239
|
+
finalAmsSlots = undefined;
|
|
1240
|
+
useAMS = false;
|
|
1241
|
+
}
|
|
1242
|
+
if ((!finalAmsMapping || finalAmsMapping.length === 0) && (!finalAmsSlots || finalAmsSlots.length === 0)) {
|
|
1243
|
+
useAMS = false;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
return { useAMS, finalAmsMapping, finalAmsSlots };
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
private async print3mfViaBambuNetwork(
|
|
1250
|
+
args: Record<string, any>,
|
|
1251
|
+
host: string,
|
|
1252
|
+
bambuSerial: string,
|
|
1253
|
+
bambuToken: string
|
|
1254
|
+
): Promise<Record<string, unknown>> {
|
|
1255
|
+
if (!args?.three_mf_path) {
|
|
1256
|
+
throw new Error("Missing required parameter: three_mf_path");
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
const printModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
1260
|
+
const printBedType = resolveBedType(args?.bed_type as string | undefined);
|
|
1261
|
+
const printNozzle = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
1262
|
+
const printPreset = BAMBU_MODEL_PRESETS[printModel]?.(printNozzle);
|
|
1263
|
+
const plateIndex = args?.plate_index !== undefined ? Number(args.plate_index) : 0;
|
|
1264
|
+
|
|
1265
|
+
if (!Number.isInteger(plateIndex) || plateIndex < 0) {
|
|
1266
|
+
throw new Error("plate_index must be a non-negative integer.");
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
const connectionType = String(args?.connection_type || "cloud").trim().toLowerCase();
|
|
1270
|
+
if (!["cloud", "lan"].includes(connectionType)) {
|
|
1271
|
+
throw new Error('connection_type must be "cloud" or "lan".');
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
const bridgePrintMethod = resolveBambuNetworkPrintMethod(
|
|
1275
|
+
args?.bambu_network_method !== undefined ? String(args.bambu_network_method) : undefined,
|
|
1276
|
+
connectionType
|
|
1277
|
+
);
|
|
1278
|
+
const bridgeMethod = toBridgeMethod(bridgePrintMethod);
|
|
1279
|
+
const isLocalBridgePrint = bridgePrintMethod !== "start_print";
|
|
1280
|
+
|
|
1281
|
+
const devId = String(args?.dev_id || bambuSerial || DEFAULT_BAMBU_DEV_ID).trim();
|
|
1282
|
+
if (!devId) {
|
|
1283
|
+
throw new Error("dev_id is required for FULU BambuNetwork printing. Pass dev_id or set BAMBU_DEV_ID/BAMBU_SERIAL.");
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
const devIp = String(args?.dev_ip || args?.host || host || "").trim();
|
|
1287
|
+
const explicitPassword = String(args?.password || args?.bambu_token || "").trim();
|
|
1288
|
+
const password = isLocalBridgePrint ? (explicitPassword || String(bambuToken || "").trim()) : explicitPassword;
|
|
1289
|
+
if (isLocalBridgePrint && (!devIp || devIp === "localhost")) {
|
|
1290
|
+
throw new Error("dev_ip or host is required for FULU BambuNetwork LAN/local print methods.");
|
|
1291
|
+
}
|
|
1292
|
+
if (isLocalBridgePrint && !password) {
|
|
1293
|
+
throw new Error("bambu_token/access code is required for FULU BambuNetwork LAN/local print methods.");
|
|
1294
|
+
}
|
|
1295
|
+
|
|
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
|
+
);
|
|
1311
|
+
const threeMfFilename = path.basename(threeMFPath);
|
|
1312
|
+
const projectName = String(args?.project_name || threeMfFilename.replace(/\.3mf$/i, ''));
|
|
1313
|
+
const presetName = String(args?.preset_name || `${projectName}_plate_${plateIndex + 1}`);
|
|
1314
|
+
const clientJobId = args?.client_job_id !== undefined ? Number(args.client_job_id) : Date.now();
|
|
1315
|
+
|
|
1316
|
+
const amsMapping = stringifyBridgeJson(args?.ams_mapping_bridge ?? finalAmsMapping ?? finalAmsSlots);
|
|
1317
|
+
const params: Record<string, unknown> = {
|
|
1318
|
+
dev_id: devId,
|
|
1319
|
+
task_name: String(args?.task_name || projectName),
|
|
1320
|
+
project_name: projectName,
|
|
1321
|
+
preset_name: presetName,
|
|
1322
|
+
filename: threeMFPath,
|
|
1323
|
+
config_filename: String(args?.config_filename || threeMFPath),
|
|
1324
|
+
plate_index: plateIndex + 1,
|
|
1325
|
+
ftp_folder: String(args?.ftp_folder || ""),
|
|
1326
|
+
ftp_file: String(args?.ftp_file || ""),
|
|
1327
|
+
ftp_file_md5: String(args?.ftp_file_md5 || ""),
|
|
1328
|
+
nozzle_mapping: stringifyBridgeJson(args?.nozzle_mapping) || "",
|
|
1329
|
+
ams_mapping: amsMapping || "",
|
|
1330
|
+
ams_mapping2: stringifyBridgeJson(args?.ams_mapping2) || "",
|
|
1331
|
+
ams_mapping_info: stringifyBridgeJson(args?.ams_mapping_info) || "",
|
|
1332
|
+
nozzles_info: stringifyBridgeJson(args?.nozzles_info) || "",
|
|
1333
|
+
connection_type: connectionType,
|
|
1334
|
+
comments: String(args?.comments || ""),
|
|
1335
|
+
origin_profile_id: args?.origin_profile_id !== undefined ? Number(args.origin_profile_id) : 0,
|
|
1336
|
+
stl_design_id: args?.stl_design_id !== undefined ? Number(args.stl_design_id) : 0,
|
|
1337
|
+
origin_model_id: String(args?.origin_model_id || ""),
|
|
1338
|
+
print_type: String(args?.print_type || "from_normal"),
|
|
1339
|
+
dst_file: String(args?.dst_file || ""),
|
|
1340
|
+
dev_name: String(args?.dev_name || ""),
|
|
1341
|
+
dev_ip: devIp,
|
|
1342
|
+
use_ssl_for_ftp: args?.use_ssl_for_ftp !== undefined ? Boolean(args.use_ssl_for_ftp) : true,
|
|
1343
|
+
use_ssl_for_mqtt: args?.use_ssl_for_mqtt !== undefined ? Boolean(args.use_ssl_for_mqtt) : true,
|
|
1344
|
+
username: String(args?.username || "bblp"),
|
|
1345
|
+
password,
|
|
1346
|
+
task_bed_leveling: args?.bed_leveling !== undefined ? Boolean(args.bed_leveling) : true,
|
|
1347
|
+
task_flow_cali: args?.flow_calibration !== undefined ? Boolean(args.flow_calibration) : true,
|
|
1348
|
+
task_vibration_cali: args?.vibration_calibration !== undefined ? Boolean(args.vibration_calibration) : true,
|
|
1349
|
+
task_layer_inspect: args?.layer_inspect !== undefined ? Boolean(args.layer_inspect) : false,
|
|
1350
|
+
task_record_timelapse: args?.timelapse !== undefined ? Boolean(args.timelapse) : false,
|
|
1351
|
+
task_use_ams: useAMS,
|
|
1352
|
+
task_bed_type: printBedType,
|
|
1353
|
+
extra_options: stringifyBridgeJson(args?.extra_options) || "",
|
|
1354
|
+
auto_bed_leveling: args?.auto_bed_leveling !== undefined ? Number(args.auto_bed_leveling) : 0,
|
|
1355
|
+
auto_flow_cali: args?.auto_flow_cali !== undefined ? Number(args.auto_flow_cali) : 0,
|
|
1356
|
+
auto_offset_cali: args?.auto_offset_cali !== undefined ? Number(args.auto_offset_cali) : 0,
|
|
1357
|
+
extruder_cali_manual_mode: args?.extruder_cali_manual_mode !== undefined ? Number(args.extruder_cali_manual_mode) : -1,
|
|
1358
|
+
task_ext_change_assist: args?.external_change_assist !== undefined ? Boolean(args.external_change_assist) : false,
|
|
1359
|
+
try_emmc_print: args?.try_emmc_print !== undefined ? Boolean(args.try_emmc_print) : false,
|
|
1360
|
+
};
|
|
1361
|
+
|
|
1362
|
+
const bridgeResult = await this.bambuNetwork.callWithAgent(
|
|
1363
|
+
bridgeMethod,
|
|
1364
|
+
{ client_job_id: clientJobId, params },
|
|
1365
|
+
this.bridgeOptionsFromArgs(args)
|
|
1366
|
+
);
|
|
1367
|
+
|
|
1368
|
+
if (typeof bridgeResult === "object" && bridgeResult !== null && (bridgeResult as { ok?: unknown }).ok === false) {
|
|
1369
|
+
throw new Error(`FULU BambuNetwork bridge method ${bridgeMethod} failed: ${String((bridgeResult as { error?: unknown }).error || "unknown bridge error")}`);
|
|
1370
|
+
}
|
|
1371
|
+
if (
|
|
1372
|
+
typeof bridgeResult === "object" &&
|
|
1373
|
+
bridgeResult !== null &&
|
|
1374
|
+
typeof (bridgeResult as { value?: unknown }).value === "number" &&
|
|
1375
|
+
(bridgeResult as { value: number }).value !== 0
|
|
1376
|
+
) {
|
|
1377
|
+
const value = (bridgeResult as { value: number }).value;
|
|
1378
|
+
throw new Error(`FULU BambuNetwork bridge method ${bridgeMethod} returned non-zero result ${value}.`);
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
return {
|
|
1382
|
+
status: "success",
|
|
1383
|
+
message: `FULU BambuNetwork ${bridgePrintMethod} command for ${threeMfFilename} sent successfully.`,
|
|
1384
|
+
bridgeMethod,
|
|
1385
|
+
bridgeResult,
|
|
1386
|
+
clientJobId,
|
|
1387
|
+
autoSliced,
|
|
1388
|
+
projectName,
|
|
1389
|
+
plateIndex,
|
|
1390
|
+
bridgePlateIndex: plateIndex + 1,
|
|
1391
|
+
useAMS,
|
|
1392
|
+
amsMapping: finalAmsMapping ?? finalAmsSlots,
|
|
1393
|
+
params: redactPrintParams(params),
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
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
|
+
|
|
1645
|
+
setupResourceHandlers() {
|
|
1646
|
+
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
1647
|
+
return {
|
|
1648
|
+
resources: [
|
|
1649
|
+
{
|
|
1650
|
+
uri: `printer://${DEFAULT_HOST}/status`,
|
|
1651
|
+
name: "Bambu Printer Status",
|
|
1652
|
+
mimeType: "application/json",
|
|
1653
|
+
description: "Current status of the Bambu Lab printer"
|
|
1654
|
+
},
|
|
1655
|
+
{
|
|
1656
|
+
uri: `printer://${DEFAULT_HOST}/files`,
|
|
1657
|
+
name: "Bambu Printer Files",
|
|
1658
|
+
mimeType: "application/json",
|
|
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"
|
|
1666
|
+
}
|
|
1667
|
+
],
|
|
1668
|
+
templates: [
|
|
1669
|
+
{
|
|
1670
|
+
uriTemplate: "printer://{host}/status",
|
|
1671
|
+
name: "Bambu Printer Status",
|
|
1672
|
+
mimeType: "application/json"
|
|
1673
|
+
},
|
|
1674
|
+
{
|
|
1675
|
+
uriTemplate: "printer://{host}/files",
|
|
1676
|
+
name: "Bambu Printer Files",
|
|
1677
|
+
mimeType: "application/json"
|
|
1678
|
+
},
|
|
1679
|
+
{
|
|
1680
|
+
uriTemplate: "printer://{host}/hms",
|
|
1681
|
+
name: "Bambu Printer HMS Diagnostics",
|
|
1682
|
+
mimeType: "application/json"
|
|
1683
|
+
}
|
|
1684
|
+
]
|
|
1685
|
+
};
|
|
1686
|
+
});
|
|
1687
|
+
|
|
1688
|
+
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
1689
|
+
const uri = request.params.uri;
|
|
1690
|
+
const match = uri.match(/^printer:\/\/([^\/]+)\/(.+)$/);
|
|
1691
|
+
|
|
1692
|
+
if (!match) {
|
|
1693
|
+
throw new McpError(ErrorCode.InvalidRequest, `Invalid resource URI: ${uri}`);
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
const [, host, resource] = match;
|
|
1697
|
+
const bambuSerial = DEFAULT_BAMBU_SERIAL;
|
|
1698
|
+
const bambuToken = DEFAULT_BAMBU_TOKEN;
|
|
1699
|
+
let content;
|
|
1700
|
+
|
|
1701
|
+
if (resource === "status") {
|
|
1702
|
+
content = await this.bambu.getStatus(host || DEFAULT_HOST, bambuSerial, bambuToken);
|
|
1703
|
+
} else if (resource === "files") {
|
|
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;
|
|
1718
|
+
} else {
|
|
299
1719
|
throw new McpError(ErrorCode.InvalidRequest, `Unknown resource: ${resource}`);
|
|
300
1720
|
}
|
|
301
1721
|
|
|
@@ -336,6 +1756,69 @@ class BambuPrinterMCPServer {
|
|
|
336
1756
|
}
|
|
337
1757
|
}
|
|
338
1758
|
},
|
|
1759
|
+
{
|
|
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.",
|
|
1762
|
+
inputSchema: {
|
|
1763
|
+
type: "object",
|
|
1764
|
+
properties: {
|
|
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
|
+
},
|
|
339
1822
|
{
|
|
340
1823
|
name: "extend_stl_base",
|
|
341
1824
|
description: "Extend the base of an STL file by a specified amount",
|
|
@@ -387,6 +1870,110 @@ class BambuPrinterMCPServer {
|
|
|
387
1870
|
required: ["stl_path"]
|
|
388
1871
|
}
|
|
389
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
|
+
},
|
|
390
1977
|
{
|
|
391
1978
|
name: "slice_stl",
|
|
392
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.",
|
|
@@ -396,16 +1983,26 @@ class BambuPrinterMCPServer {
|
|
|
396
1983
|
stl_path: { type: "string", description: "Path to the STL or 3MF file to slice" },
|
|
397
1984
|
bambu_model: {
|
|
398
1985
|
type: "string",
|
|
399
|
-
enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
|
|
1986
|
+
enum: ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s"],
|
|
400
1987
|
description: "REQUIRED: Bambu Lab printer model. Ask the user if not known. Using the wrong model can damage the printer."
|
|
401
1988
|
},
|
|
402
1989
|
slicer_type: {
|
|
403
1990
|
type: "string",
|
|
404
|
-
|
|
1991
|
+
enum: SLICER_SCHEMA_VALUES,
|
|
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."
|
|
405
1993
|
},
|
|
406
1994
|
slicer_path: { type: "string", description: "Path to the slicer executable (default: value from env)" },
|
|
407
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." },
|
|
408
1999
|
nozzle_diameter: { type: "string", description: "Nozzle diameter in mm (default: 0.4)" },
|
|
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." },
|
|
409
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." },
|
|
410
2007
|
repetitions: { type: "number", description: "Print N identical copies of the model. Each copy gets its own plate placement. Example: 3 prints three copies." },
|
|
411
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." },
|
|
@@ -414,6 +2011,7 @@ class BambuPrinterMCPServer {
|
|
|
414
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." },
|
|
415
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." },
|
|
416
2013
|
load_filaments: { type: "string", description: "Override filament profiles. Semicolon-separated paths to filament JSON configs, e.g. 'pla_basic.json;petg_cf.json'." },
|
|
2014
|
+
filament_profile: { type: "string", description: "Compatibility alias for load_filaments. Semicolon-separated Orca/Bambu filament profile JSON paths." },
|
|
417
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." },
|
|
418
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." },
|
|
419
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." },
|
|
@@ -440,76 +2038,403 @@ class BambuPrinterMCPServer {
|
|
|
440
2038
|
}
|
|
441
2039
|
}
|
|
442
2040
|
},
|
|
2041
|
+
{
|
|
2042
|
+
name: "bambu_network_bridge_status",
|
|
2043
|
+
description: "Inspect or probe the FULU OrcaSlicer-bambulab BambuNetwork bridge runtime used for cloud and restored BambuNetwork printing.",
|
|
2044
|
+
inputSchema: {
|
|
2045
|
+
type: "object",
|
|
2046
|
+
properties: {
|
|
2047
|
+
connect: { type: "boolean", description: "When true, start the bridge command and run a handshake plus agent initialization probe." },
|
|
2048
|
+
bridge_command: { type: "string", description: "Override command for the FULU bridge host or macOS/WSL wrapper; defaults to BAMBU_NETWORK_BRIDGE_COMMAND." },
|
|
2049
|
+
bambu_network_config_dir: { type: "string", description: "Config/log directory used by the BambuNetwork agent; defaults to BAMBU_NETWORK_CONFIG_DIR or a user config directory." },
|
|
2050
|
+
country_code: { type: "string", description: "BambuNetwork country code, such as US, used by the agent during startup." },
|
|
2051
|
+
user_info: { type: "string", description: "Optional BambuNetwork user_info JSON string to pass to net.change_user after the agent starts." },
|
|
2052
|
+
timeout_ms: { type: "number", description: "Bridge request timeout in milliseconds for the connect probe." }
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
},
|
|
2056
|
+
{
|
|
2057
|
+
name: "bambu_network_call",
|
|
2058
|
+
description: "Call a raw FULU OrcaSlicer-bambulab BambuNetwork bridge method, optionally with an initialized network agent injected into the payload.",
|
|
2059
|
+
inputSchema: {
|
|
2060
|
+
type: "object",
|
|
2061
|
+
properties: {
|
|
2062
|
+
method: { type: "string", description: "FULU bridge method name, for example bridge.handshake, net.is_user_login, or net.get_user_selected_machine." },
|
|
2063
|
+
payload: { type: "object", description: "JSON payload passed to the bridge method." },
|
|
2064
|
+
with_agent: { type: "boolean", description: "When true, initialize a BambuNetwork agent and add its agent id to the payload before calling the method." },
|
|
2065
|
+
bridge_command: { type: "string", description: "Override command for the FULU bridge host or macOS/WSL wrapper; defaults to BAMBU_NETWORK_BRIDGE_COMMAND." },
|
|
2066
|
+
bambu_network_config_dir: { type: "string", description: "Config/log directory used by the BambuNetwork agent; defaults to BAMBU_NETWORK_CONFIG_DIR or a user config directory." },
|
|
2067
|
+
country_code: { type: "string", description: "BambuNetwork country code, such as US, used by the agent during startup." },
|
|
2068
|
+
user_info: { type: "string", description: "Optional BambuNetwork user_info JSON string to pass to net.change_user after the agent starts." },
|
|
2069
|
+
timeout_ms: { type: "number", description: "Bridge request timeout in milliseconds." }
|
|
2070
|
+
},
|
|
2071
|
+
required: ["method"]
|
|
2072
|
+
}
|
|
2073
|
+
},
|
|
2074
|
+
{
|
|
2075
|
+
name: "print_3mf_bambu_network",
|
|
2076
|
+
description: "Print a 3MF through FULU OrcaSlicer-bambulab's restored BambuNetwork path instead of the MCP LAN MQTT/FTPS path.",
|
|
2077
|
+
inputSchema: {
|
|
2078
|
+
type: "object",
|
|
2079
|
+
properties: {
|
|
2080
|
+
three_mf_path: { type: "string", description: "Path to the 3MF file to print; unsliced 3MFs are auto-sliced before sending." },
|
|
2081
|
+
bambu_model: {
|
|
2082
|
+
type: "string",
|
|
2083
|
+
enum: ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s"],
|
|
2084
|
+
description: "REQUIRED: Bambu Lab printer model. Ask the user if not known. Using the wrong model can damage the printer."
|
|
2085
|
+
},
|
|
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." },
|
|
2087
|
+
bambu_network_method: { type: "string", enum: BAMBU_NETWORK_PRINT_METHODS, description: "FULU print method to invoke; defaults to start_print for cloud and start_local_print for lan." },
|
|
2088
|
+
dev_id: { type: "string", description: "Bambu device id used by BambuNetwork; defaults to BAMBU_DEV_ID or BAMBU_SERIAL." },
|
|
2089
|
+
dev_ip: { type: "string", description: "Printer IP address for LAN/local bridge methods; defaults to host when provided." },
|
|
2090
|
+
host: { type: "string", description: "Printer host or IP address, used as dev_ip for LAN/local bridge methods." },
|
|
2091
|
+
bambu_serial: { type: "string", description: "Fallback Bambu device id when dev_id is not supplied." },
|
|
2092
|
+
bambu_token: { type: "string", description: "Printer access code/password for LAN/local bridge methods." },
|
|
2093
|
+
username: { type: "string", description: "Printer username for LAN/local bridge methods; defaults to bblp." },
|
|
2094
|
+
password: { type: "string", description: "Printer password/access code override for LAN/local bridge methods." },
|
|
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)." },
|
|
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." },
|
|
2097
|
+
project_name: { type: "string", description: "Optional project name sent in FULU PrintParams; defaults to the 3MF filename without extension." },
|
|
2098
|
+
preset_name: { type: "string", description: "Optional preset name sent in FULU PrintParams; defaults to project plus one-based plate index." },
|
|
2099
|
+
task_name: { type: "string", description: "Optional BambuNetwork task name; defaults to the project name." },
|
|
2100
|
+
config_filename: { type: "string", description: "Optional config 3MF path for cloud print; defaults to the same 3MF path." },
|
|
2101
|
+
bridge_command: { type: "string", description: "Override command for the FULU bridge host or macOS/WSL wrapper; defaults to BAMBU_NETWORK_BRIDGE_COMMAND." },
|
|
2102
|
+
bambu_network_config_dir: { type: "string", description: "Config/log directory used by the BambuNetwork agent; defaults to BAMBU_NETWORK_CONFIG_DIR or a user config directory." },
|
|
2103
|
+
country_code: { type: "string", description: "BambuNetwork country code, such as US, used by the agent during startup." },
|
|
2104
|
+
user_info: { type: "string", description: "Optional BambuNetwork user_info JSON string to pass to net.change_user after the agent starts." },
|
|
2105
|
+
timeout_ms: { type: "number", description: "Bridge request timeout in milliseconds." },
|
|
2106
|
+
slicer_type: { type: "string", enum: SLICER_SCHEMA_VALUES, description: "Slicer to use only if auto-slicing an unsliced 3MF; use orcaslicer-bambulab for FULU's fork." },
|
|
2107
|
+
slicer_path: { type: "string", description: "Path to the slicer executable for auto-slicing; defaults to value from env or a platform default." },
|
|
2108
|
+
slicer_profile: { type: "string", description: "Path to an optional slicer profile/config file for auto-slicing." },
|
|
2109
|
+
nozzle_diameter: { type: "string", description: "Nozzle diameter in mm for auto-slicing (default: 0.4)." },
|
|
2110
|
+
use_ams: { type: "boolean", description: "Whether to use the AMS; defaults to auto-detect from the 3MF mapping." },
|
|
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" } },
|
|
2113
|
+
ams_mapping_bridge: { type: "string", description: "Raw JSON string override for FULU PrintParams ams_mapping when the automatic array is not enough." },
|
|
2114
|
+
ams_mapping2: { type: "string", description: "Raw JSON string for FULU PrintParams ams_mapping2, matching OrcaSlicer-bambulab's v1 AMS mapping field." },
|
|
2115
|
+
ams_mapping_info: { type: "string", description: "Raw JSON string for FULU PrintParams ams_mapping_info, matching OrcaSlicer-bambulab's detailed AMS mapping field." },
|
|
2116
|
+
nozzle_mapping: { type: "string", description: "Raw JSON string for FULU PrintParams nozzle_mapping." },
|
|
2117
|
+
nozzles_info: { type: "string", description: "Raw JSON string for FULU PrintParams nozzles_info." },
|
|
2118
|
+
bed_leveling: { type: "boolean", description: "Enable auto bed leveling in FULU PrintParams (default: true)." },
|
|
2119
|
+
flow_calibration: { type: "boolean", description: "Enable flow calibration in FULU PrintParams (default: true)." },
|
|
2120
|
+
vibration_calibration: { type: "boolean", description: "Enable vibration calibration in FULU PrintParams (default: true)." },
|
|
2121
|
+
layer_inspect: { type: "boolean", description: "Enable first-layer inspection where supported (default: false for BambuNetwork bridge)." },
|
|
2122
|
+
timelapse: { type: "boolean", description: "Enable timelapse recording in FULU PrintParams (default: false)." },
|
|
2123
|
+
use_ssl_for_ftp: { type: "boolean", description: "Whether FULU local print should use SSL for FTP (default: true)." },
|
|
2124
|
+
use_ssl_for_mqtt: { type: "boolean", description: "Whether FULU local print should use SSL for MQTT (default: true)." },
|
|
2125
|
+
external_change_assist: { type: "boolean", description: "Enable FULU PrintParams task_ext_change_assist for external filament change assistance." },
|
|
2126
|
+
try_emmc_print: { type: "boolean", description: "Enable FULU PrintParams try_emmc_print for printers that support internal storage printing." },
|
|
2127
|
+
extra_options: { type: "string", description: "Raw JSON string or text for FULU PrintParams extra_options." },
|
|
2128
|
+
client_job_id: { type: "number", description: "Optional client job id sent to the bridge; defaults to the current timestamp." }
|
|
2129
|
+
},
|
|
2130
|
+
required: ["three_mf_path", "bambu_model"]
|
|
2131
|
+
}
|
|
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
|
+
},
|
|
443
2171
|
{
|
|
444
2172
|
name: "upload_gcode",
|
|
445
2173
|
description: "Upload a G-code file to the Bambu Lab printer",
|
|
446
2174
|
inputSchema: {
|
|
447
2175
|
type: "object",
|
|
448
2176
|
properties: {
|
|
449
|
-
filename: { type: "string", description: "Name for the file on the printer" },
|
|
450
|
-
gcode: { type: "string", description: "G-code content to upload" },
|
|
2177
|
+
filename: { type: "string", description: "Name for the file on the printer" },
|
|
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." },
|
|
2180
|
+
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2181
|
+
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
2182
|
+
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
2183
|
+
},
|
|
2184
|
+
required: ["filename"]
|
|
2185
|
+
}
|
|
2186
|
+
},
|
|
2187
|
+
{
|
|
2188
|
+
name: "upload_file",
|
|
2189
|
+
description: "Upload a local file to the Bambu Lab printer",
|
|
2190
|
+
inputSchema: {
|
|
2191
|
+
type: "object",
|
|
2192
|
+
properties: {
|
|
2193
|
+
file_path: { type: "string", description: "Local path to the file to upload" },
|
|
2194
|
+
filename: { type: "string", description: "Name for the file on the printer" },
|
|
2195
|
+
print: { type: "boolean", description: "Start printing after upload (default: false)" },
|
|
2196
|
+
bambu_model: {
|
|
2197
|
+
type: "string",
|
|
2198
|
+
enum: ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s"],
|
|
2199
|
+
description: "Required when print is true. Bambu Lab printer model used as a safety confirmation before starting the uploaded file."
|
|
2200
|
+
},
|
|
2201
|
+
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2202
|
+
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
2203
|
+
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
2204
|
+
},
|
|
2205
|
+
required: ["file_path", "filename"]
|
|
2206
|
+
}
|
|
2207
|
+
},
|
|
2208
|
+
{
|
|
2209
|
+
name: "start_print",
|
|
2210
|
+
description: "Start printing a G-code file already on the Bambu Lab printer. Alias of start_print_job for upstream MCP compatibility.",
|
|
2211
|
+
inputSchema: {
|
|
2212
|
+
type: "object",
|
|
2213
|
+
properties: {
|
|
2214
|
+
filename: { type: "string", description: "Name of the file to print" },
|
|
2215
|
+
bambu_model: {
|
|
2216
|
+
type: "string",
|
|
2217
|
+
enum: ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s"],
|
|
2218
|
+
description: "REQUIRED: Bambu Lab printer model. Ask the user if not known. Starting G-code for the wrong model can damage the printer."
|
|
2219
|
+
},
|
|
2220
|
+
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2221
|
+
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
2222
|
+
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
2223
|
+
},
|
|
2224
|
+
required: ["filename", "bambu_model"]
|
|
2225
|
+
}
|
|
2226
|
+
},
|
|
2227
|
+
{
|
|
2228
|
+
name: "start_print_job",
|
|
2229
|
+
description: "Start printing a G-code file already on the Bambu Lab printer",
|
|
2230
|
+
inputSchema: {
|
|
2231
|
+
type: "object",
|
|
2232
|
+
properties: {
|
|
2233
|
+
filename: { type: "string", description: "Name of the file to print" },
|
|
2234
|
+
bambu_model: {
|
|
2235
|
+
type: "string",
|
|
2236
|
+
enum: ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s"],
|
|
2237
|
+
description: "REQUIRED: Bambu Lab printer model. Ask the user if not known. Starting G-code for the wrong model can damage the printer."
|
|
2238
|
+
},
|
|
2239
|
+
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2240
|
+
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
2241
|
+
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
2242
|
+
},
|
|
2243
|
+
required: ["filename", "bambu_model"]
|
|
2244
|
+
}
|
|
2245
|
+
},
|
|
2246
|
+
{
|
|
2247
|
+
name: "cancel_print",
|
|
2248
|
+
description: "Cancel the current print job on the Bambu Lab printer",
|
|
2249
|
+
inputSchema: {
|
|
2250
|
+
type: "object",
|
|
2251
|
+
properties: {
|
|
2252
|
+
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2253
|
+
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
2254
|
+
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
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
|
+
},
|
|
2345
|
+
{
|
|
2346
|
+
name: "set_temperature",
|
|
2347
|
+
description: "Set the temperature of a printer component (bed, nozzle)",
|
|
2348
|
+
inputSchema: {
|
|
2349
|
+
type: "object",
|
|
2350
|
+
properties: {
|
|
2351
|
+
component: { type: "string", description: "Component to heat: bed, nozzle, or extruder" },
|
|
2352
|
+
temperature: { type: "number", description: "Target temperature in °C" },
|
|
451
2353
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
452
2354
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
453
2355
|
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
454
2356
|
},
|
|
455
|
-
required: ["
|
|
2357
|
+
required: ["component", "temperature"]
|
|
456
2358
|
}
|
|
457
2359
|
},
|
|
458
2360
|
{
|
|
459
|
-
name: "
|
|
460
|
-
description: "
|
|
2361
|
+
name: "set_fan_speed",
|
|
2362
|
+
description: "Set a Bambu printer fan speed percentage using the printer's MQTT fan command",
|
|
461
2363
|
inputSchema: {
|
|
462
2364
|
type: "object",
|
|
463
2365
|
properties: {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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" },
|
|
467
2371
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
468
2372
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
469
2373
|
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
470
2374
|
},
|
|
471
|
-
required: ["
|
|
2375
|
+
required: ["fan", "speed"]
|
|
472
2376
|
}
|
|
473
2377
|
},
|
|
474
2378
|
{
|
|
475
|
-
name: "
|
|
476
|
-
description: "
|
|
2379
|
+
name: "set_light",
|
|
2380
|
+
description: "Set a Bambu printer light node mode using the printer's MQTT LED command",
|
|
477
2381
|
inputSchema: {
|
|
478
2382
|
type: "object",
|
|
479
2383
|
properties: {
|
|
480
|
-
|
|
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
|
+
},
|
|
481
2393
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
482
2394
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
483
2395
|
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
484
2396
|
},
|
|
485
|
-
required: ["
|
|
2397
|
+
required: ["light", "mode"]
|
|
486
2398
|
}
|
|
487
2399
|
},
|
|
488
2400
|
{
|
|
489
|
-
name: "
|
|
490
|
-
description: "
|
|
2401
|
+
name: "skip_objects",
|
|
2402
|
+
description: "Skip specific object IDs during a running multi-object print using the printer's MQTT skip_objects command",
|
|
491
2403
|
inputSchema: {
|
|
492
2404
|
type: "object",
|
|
493
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
|
+
},
|
|
494
2411
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
495
2412
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
496
2413
|
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
497
|
-
}
|
|
2414
|
+
},
|
|
2415
|
+
required: ["object_ids"]
|
|
498
2416
|
}
|
|
499
2417
|
},
|
|
500
2418
|
{
|
|
501
|
-
name: "
|
|
502
|
-
description: "
|
|
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.",
|
|
503
2421
|
inputSchema: {
|
|
504
2422
|
type: "object",
|
|
505
2423
|
properties: {
|
|
506
|
-
|
|
507
|
-
|
|
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
|
+
},
|
|
508
2433
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
509
2434
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
510
2435
|
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
511
2436
|
},
|
|
512
|
-
required: ["
|
|
2437
|
+
required: ["action", "ams_id"]
|
|
513
2438
|
}
|
|
514
2439
|
},
|
|
515
2440
|
{
|
|
@@ -521,30 +2446,95 @@ class BambuPrinterMCPServer {
|
|
|
521
2446
|
three_mf_path: { type: "string", description: "Path to the 3MF file to print" },
|
|
522
2447
|
bambu_model: {
|
|
523
2448
|
type: "string",
|
|
524
|
-
enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
|
|
2449
|
+
enum: ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s"],
|
|
525
2450
|
description: "REQUIRED: Bambu Lab printer model. Ask the user if not known. Using the wrong model can damage the printer."
|
|
526
2451
|
},
|
|
2452
|
+
connection_mode: {
|
|
2453
|
+
type: "string",
|
|
2454
|
+
enum: ["lan_mqtt_ftps", "bambu_network"],
|
|
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."
|
|
2456
|
+
},
|
|
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." },
|
|
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." },
|
|
2459
|
+
dev_id: { type: "string", description: "Bambu device id for FULU BambuNetwork printing; defaults to BAMBU_DEV_ID or BAMBU_SERIAL." },
|
|
2460
|
+
dev_ip: { type: "string", description: "Printer IP address for FULU BambuNetwork LAN/local print methods; defaults to host when provided." },
|
|
2461
|
+
bridge_command: { type: "string", description: "Override command for the FULU bridge host or macOS/WSL wrapper; defaults to BAMBU_NETWORK_BRIDGE_COMMAND." },
|
|
2462
|
+
bambu_network_config_dir: { type: "string", description: "Config/log directory used by the FULU BambuNetwork agent." },
|
|
2463
|
+
country_code: { type: "string", description: "BambuNetwork country code, such as US, used by the FULU bridge agent." },
|
|
2464
|
+
user_info: { type: "string", description: "Optional BambuNetwork user_info JSON string passed to net.change_user for the FULU bridge." },
|
|
527
2465
|
bed_type: {
|
|
528
2466
|
type: "string",
|
|
529
|
-
enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate"],
|
|
2467
|
+
enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate", "supertack_plate"],
|
|
530
2468
|
description: "Bed plate type currently installed (default: textured_plate)"
|
|
531
2469
|
},
|
|
532
2470
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
533
2471
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
534
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)" },
|
|
2474
|
+
slicer_type: {
|
|
2475
|
+
type: "string",
|
|
2476
|
+
enum: SLICER_SCHEMA_VALUES,
|
|
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."
|
|
2478
|
+
},
|
|
2479
|
+
slicer_path: { type: "string", description: "Path to the slicer executable for auto-slicing (default: value from env or a platform default)" },
|
|
535
2480
|
use_ams: { type: "boolean", description: "Whether to use the AMS (default: auto-detect from 3MF)" },
|
|
536
2481
|
ams_mapping: {
|
|
537
2482
|
type: "array",
|
|
538
|
-
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.",
|
|
2484
|
+
items: { type: "number" }
|
|
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.",
|
|
539
2489
|
items: { type: "number" }
|
|
540
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
|
+
},
|
|
541
2530
|
bed_leveling: { type: "boolean", description: "Enable auto bed leveling (default: true)" },
|
|
542
2531
|
flow_calibration: { type: "boolean", description: "Enable flow calibration (default: true)" },
|
|
543
2532
|
vibration_calibration: { type: "boolean", description: "Enable vibration calibration (default: true)" },
|
|
544
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)." },
|
|
545
2535
|
nozzle_diameter: { type: "string", description: "Nozzle diameter in mm for auto-slicing (default: 0.4)" }
|
|
546
2536
|
},
|
|
547
|
-
required: ["
|
|
2537
|
+
required: ["bambu_model"]
|
|
548
2538
|
}
|
|
549
2539
|
},
|
|
550
2540
|
{
|
|
@@ -609,9 +2599,15 @@ class BambuPrinterMCPServer {
|
|
|
609
2599
|
const host = String(args?.host || DEFAULT_HOST);
|
|
610
2600
|
const bambuSerial = String(args?.bambu_serial || DEFAULT_BAMBU_SERIAL);
|
|
611
2601
|
const bambuToken = String(args?.bambu_token || DEFAULT_BAMBU_TOKEN);
|
|
612
|
-
const
|
|
613
|
-
|
|
614
|
-
|
|
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);
|
|
615
2611
|
|
|
616
2612
|
try {
|
|
617
2613
|
let result;
|
|
@@ -621,26 +2617,164 @@ class BambuPrinterMCPServer {
|
|
|
621
2617
|
result = await this.bambu.getStatus(host, bambuSerial, bambuToken);
|
|
622
2618
|
break;
|
|
623
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
|
+
|
|
624
2689
|
case "list_printer_files":
|
|
625
2690
|
result = await this.bambu.getFiles(host, bambuSerial, bambuToken);
|
|
626
2691
|
break;
|
|
627
2692
|
|
|
628
|
-
case "
|
|
629
|
-
|
|
630
|
-
|
|
2693
|
+
case "bambu_network_bridge_status": {
|
|
2694
|
+
const bridgeArgs = args as Record<string, any> | undefined;
|
|
2695
|
+
const options = this.bridgeOptionsFromArgs(bridgeArgs);
|
|
2696
|
+
result = this.bambuNetwork.getStatus(options);
|
|
2697
|
+
if (Boolean(bridgeArgs?.connect)) {
|
|
2698
|
+
const probe = await this.bambuNetwork.ensureAgent(options);
|
|
2699
|
+
result = {
|
|
2700
|
+
...this.bambuNetwork.getStatus(options),
|
|
2701
|
+
connected: true,
|
|
2702
|
+
agent: probe.agent,
|
|
2703
|
+
handshake: probe.handshake,
|
|
2704
|
+
};
|
|
631
2705
|
}
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
2706
|
+
break;
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2709
|
+
case "bambu_network_call": {
|
|
2710
|
+
if (!args?.method) {
|
|
2711
|
+
throw new Error("Missing required parameter: method");
|
|
2712
|
+
}
|
|
2713
|
+
const bridgeArgs = args as Record<string, any>;
|
|
2714
|
+
const payload = bridgeArgs.payload && typeof bridgeArgs.payload === "object"
|
|
2715
|
+
? bridgeArgs.payload as Record<string, unknown>
|
|
2716
|
+
: {};
|
|
2717
|
+
const options = this.bridgeOptionsFromArgs(bridgeArgs);
|
|
2718
|
+
result = bridgeArgs.with_agent === false
|
|
2719
|
+
? await this.bambuNetwork.request(String(bridgeArgs.method), payload, options)
|
|
2720
|
+
: await this.bambuNetwork.callWithAgent(String(bridgeArgs.method), payload, options);
|
|
2721
|
+
break;
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2724
|
+
case "print_3mf_bambu_network": {
|
|
2725
|
+
result = await this.print3mfViaBambuNetwork(args as Record<string, any>, host, bambuSerial, bambuToken);
|
|
2726
|
+
break;
|
|
2727
|
+
}
|
|
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
|
|
636
2751
|
);
|
|
637
2752
|
break;
|
|
2753
|
+
|
|
2754
|
+
case "upload_gcode": {
|
|
2755
|
+
if (!args?.filename) {
|
|
2756
|
+
throw new Error("Missing required parameter: filename");
|
|
2757
|
+
}
|
|
2758
|
+
const uploadSource = resolveUploadGcodeSource(args as Record<string, unknown>);
|
|
2759
|
+
try {
|
|
2760
|
+
result = await this.bambu.uploadFile(
|
|
2761
|
+
host, bambuSerial, bambuToken, uploadSource.filePath, String(args.filename), false
|
|
2762
|
+
);
|
|
2763
|
+
} finally {
|
|
2764
|
+
if (uploadSource.cleanupDir) {
|
|
2765
|
+
fs.rmSync(uploadSource.cleanupDir, { recursive: true, force: true });
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
break;
|
|
638
2769
|
}
|
|
639
2770
|
|
|
640
2771
|
case "upload_file":
|
|
641
2772
|
if (!args?.file_path || !args?.filename) {
|
|
642
2773
|
throw new Error("Missing required parameters: file_path and filename");
|
|
643
2774
|
}
|
|
2775
|
+
if (Boolean(args.print ?? false)) {
|
|
2776
|
+
await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
2777
|
+
}
|
|
644
2778
|
result = await this.bambu.uploadFile(
|
|
645
2779
|
host, bambuSerial, bambuToken,
|
|
646
2780
|
String(args.file_path), String(args.filename),
|
|
@@ -648,10 +2782,12 @@ class BambuPrinterMCPServer {
|
|
|
648
2782
|
);
|
|
649
2783
|
break;
|
|
650
2784
|
|
|
2785
|
+
case "start_print":
|
|
651
2786
|
case "start_print_job":
|
|
652
2787
|
if (!args?.filename) {
|
|
653
2788
|
throw new Error("Missing required parameter: filename");
|
|
654
2789
|
}
|
|
2790
|
+
await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
655
2791
|
result = await this.bambu.startJob(host, bambuSerial, bambuToken, String(args.filename));
|
|
656
2792
|
break;
|
|
657
2793
|
|
|
@@ -659,6 +2795,45 @@ class BambuPrinterMCPServer {
|
|
|
659
2795
|
result = await this.bambu.cancelJob(host, bambuSerial, bambuToken);
|
|
660
2796
|
break;
|
|
661
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
|
+
|
|
662
2837
|
case "set_temperature":
|
|
663
2838
|
if (!args?.component || args?.temperature === undefined) {
|
|
664
2839
|
throw new Error("Missing required parameters: component and temperature");
|
|
@@ -669,6 +2844,57 @@ class BambuPrinterMCPServer {
|
|
|
669
2844
|
);
|
|
670
2845
|
break;
|
|
671
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
|
+
|
|
672
2898
|
case "extend_stl_base":
|
|
673
2899
|
if (!args?.stl_path || args?.extension_height === undefined) {
|
|
674
2900
|
throw new Error("Missing required parameters: stl_path and extension_height");
|
|
@@ -713,12 +2939,113 @@ class BambuPrinterMCPServer {
|
|
|
713
2939
|
result = await this.stlManipulator.getSTLInfo(String(args.stl_path));
|
|
714
2940
|
break;
|
|
715
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
|
+
|
|
716
3036
|
case "slice_stl": {
|
|
717
3037
|
if (!args?.stl_path) {
|
|
718
3038
|
throw new Error("Missing required parameter: stl_path");
|
|
719
3039
|
}
|
|
3040
|
+
const { slicerType, slicerPath, slicerProfile } = resolveSlicerConfig(args as Record<string, unknown> | undefined);
|
|
720
3041
|
const sliceModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
721
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);
|
|
722
3049
|
// Resolve printer preset for BambuStudio slicer
|
|
723
3050
|
const printerPreset = BAMBU_MODEL_PRESETS[sliceModel]?.(nozzleDiam);
|
|
724
3051
|
|
|
@@ -730,8 +3057,20 @@ class BambuPrinterMCPServer {
|
|
|
730
3057
|
if (args?.ensure_on_bed !== undefined) sliceBambuOptions.ensureOnBed = Boolean(args.ensure_on_bed);
|
|
731
3058
|
if (args?.clone_objects !== undefined) sliceBambuOptions.cloneObjects = String(args.clone_objects);
|
|
732
3059
|
if (args?.skip_objects !== undefined) sliceBambuOptions.skipObjects = String(args.skip_objects);
|
|
733
|
-
if (
|
|
3060
|
+
if (
|
|
3061
|
+
args?.load_filaments !== undefined &&
|
|
3062
|
+
args?.filament_profile !== undefined &&
|
|
3063
|
+
String(args.load_filaments) !== String(args.filament_profile)
|
|
3064
|
+
) {
|
|
3065
|
+
throw new Error("Provide either load_filaments or filament_profile, not conflicting values.");
|
|
3066
|
+
}
|
|
3067
|
+
if (args?.load_filaments !== undefined) {
|
|
3068
|
+
sliceBambuOptions.loadFilaments = String(args.load_filaments);
|
|
3069
|
+
} else if (args?.filament_profile !== undefined) {
|
|
3070
|
+
sliceBambuOptions.loadFilaments = String(args.filament_profile);
|
|
3071
|
+
}
|
|
734
3072
|
if (args?.load_filament_ids !== undefined) sliceBambuOptions.loadFilamentIds = String(args.load_filament_ids);
|
|
3073
|
+
sliceBambuOptions.bedType = resolveBambuStudioCliBedType(args?.bed_type as string | undefined);
|
|
735
3074
|
if (args?.enable_timelapse !== undefined) sliceBambuOptions.enableTimelapse = Boolean(args.enable_timelapse);
|
|
736
3075
|
if (args?.allow_mix_temp !== undefined) sliceBambuOptions.allowMixTemp = Boolean(args.allow_mix_temp);
|
|
737
3076
|
if (args?.scale !== undefined) sliceBambuOptions.scale = Number(args.scale);
|
|
@@ -741,10 +3080,34 @@ class BambuPrinterMCPServer {
|
|
|
741
3080
|
if (args?.min_save !== undefined) sliceBambuOptions.minSave = Boolean(args.min_save);
|
|
742
3081
|
if (args?.skip_modified_gcodes !== undefined) sliceBambuOptions.skipModifiedGcodes = Boolean(args.skip_modified_gcodes);
|
|
743
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
|
+
}
|
|
744
3107
|
|
|
745
3108
|
result = await this.stlManipulator.sliceSTL(
|
|
746
3109
|
String(args.stl_path), slicerType, slicerPath,
|
|
747
|
-
|
|
3110
|
+
activeSlicerProfile,
|
|
748
3111
|
undefined, // progressCallback
|
|
749
3112
|
printerPreset,
|
|
750
3113
|
sliceBambuOptions
|
|
@@ -756,14 +3119,31 @@ class BambuPrinterMCPServer {
|
|
|
756
3119
|
if (!args?.three_mf_path) {
|
|
757
3120
|
throw new Error("Missing required parameter: three_mf_path");
|
|
758
3121
|
}
|
|
3122
|
+
if (String(args?.connection_mode || "lan_mqtt_ftps") === "bambu_network") {
|
|
3123
|
+
result = await this.print3mfViaBambuNetwork(args as Record<string, any>, host, bambuSerial, bambuToken);
|
|
3124
|
+
break;
|
|
3125
|
+
}
|
|
759
3126
|
if (!bambuSerial || !bambuToken) {
|
|
760
3127
|
throw new Error("Bambu serial number and access token are required for print_3mf.");
|
|
761
3128
|
}
|
|
762
3129
|
|
|
3130
|
+
const { slicerType, slicerPath, slicerProfile } = resolveSlicerConfig(args as Record<string, unknown> | undefined);
|
|
763
3131
|
const printModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
764
3132
|
const printBedType = resolveBedType(args?.bed_type as string | undefined);
|
|
765
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);
|
|
766
3141
|
const printPreset = BAMBU_MODEL_PRESETS[printModel]?.(printNozzle);
|
|
3142
|
+
const plateIndex = args?.plate_index !== undefined ? Number(args.plate_index) : 0;
|
|
3143
|
+
|
|
3144
|
+
if (!Number.isInteger(plateIndex) || plateIndex < 0) {
|
|
3145
|
+
throw new Error("plate_index must be a non-negative integer.");
|
|
3146
|
+
}
|
|
767
3147
|
|
|
768
3148
|
let threeMFPath = String(args.three_mf_path);
|
|
769
3149
|
|
|
@@ -776,15 +3156,37 @@ class BambuPrinterMCPServer {
|
|
|
776
3156
|
f => f.match(/Metadata\/plate_\d+\.gcode/i) || f.endsWith('.gcode')
|
|
777
3157
|
);
|
|
778
3158
|
if (!hasGcode) {
|
|
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
|
+
}
|
|
779
3164
|
console.log(`3MF has no gcode — auto-slicing with ${slicerType} for ${printModel}`);
|
|
780
3165
|
const autoSliceOptions: BambuSliceOptions = {
|
|
781
3166
|
uptodate: true,
|
|
782
3167
|
ensureOnBed: true,
|
|
783
3168
|
minSave: true,
|
|
784
3169
|
skipModifiedGcodes: true,
|
|
3170
|
+
bedType: printBedType,
|
|
785
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
|
+
}
|
|
786
3188
|
threeMFPath = await this.stlManipulator.sliceSTL(
|
|
787
|
-
threeMFPath, slicerType, slicerPath,
|
|
3189
|
+
threeMFPath, slicerType, slicerPath, activeSlicerProfile,
|
|
788
3190
|
undefined, // progressCallback
|
|
789
3191
|
printPreset,
|
|
790
3192
|
autoSliceOptions
|
|
@@ -792,42 +3194,93 @@ class BambuPrinterMCPServer {
|
|
|
792
3194
|
console.log("Auto-sliced to: " + threeMFPath);
|
|
793
3195
|
}
|
|
794
3196
|
} catch (sliceCheckErr: any) {
|
|
3197
|
+
if (String(sliceCheckErr?.message || "").includes("SuperTack")) {
|
|
3198
|
+
throw sliceCheckErr;
|
|
3199
|
+
}
|
|
795
3200
|
console.warn("Could not check/slice 3MF, proceeding with original:", sliceCheckErr.message);
|
|
796
3201
|
}
|
|
797
3202
|
|
|
798
3203
|
const parsed3MFData = await parse3MF(threeMFPath);
|
|
3204
|
+
const isH2Print = printModel === "h2s" || printModel === "h2d";
|
|
799
3205
|
let parsedAmsMapping: number[] | undefined;
|
|
800
|
-
if (parsed3MFData.slicerConfig?.ams_mapping) {
|
|
801
|
-
const slots =
|
|
802
|
-
.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>);
|
|
803
3208
|
if (slots.length > 0) {
|
|
804
|
-
parsedAmsMapping = slots
|
|
3209
|
+
parsedAmsMapping = slots;
|
|
805
3210
|
}
|
|
806
3211
|
}
|
|
807
3212
|
|
|
808
3213
|
let finalAmsMapping = parsedAmsMapping;
|
|
3214
|
+
let finalAmsSlots: number[] | undefined;
|
|
809
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);
|
|
810
3218
|
|
|
811
|
-
if (
|
|
3219
|
+
if (hasUserAmsMapping) {
|
|
812
3220
|
let userMappingOverride: number[] | undefined;
|
|
813
3221
|
if (Array.isArray(args.ams_mapping)) {
|
|
814
|
-
userMappingOverride = args.ams_mapping.
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
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>);
|
|
819
3227
|
}
|
|
820
3228
|
if (userMappingOverride && userMappingOverride.length > 0) {
|
|
821
3229
|
finalAmsMapping = userMappingOverride;
|
|
822
3230
|
useAMS = true;
|
|
823
3231
|
}
|
|
824
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
|
+
}
|
|
825
3277
|
|
|
826
3278
|
if (args?.use_ams === false) {
|
|
827
3279
|
finalAmsMapping = undefined;
|
|
3280
|
+
finalAmsSlots = undefined;
|
|
828
3281
|
useAMS = false;
|
|
829
3282
|
}
|
|
830
|
-
if (!finalAmsMapping || finalAmsMapping.length === 0) {
|
|
3283
|
+
if ((!finalAmsMapping || finalAmsMapping.length === 0) && (!finalAmsSlots || finalAmsSlots.length === 0)) {
|
|
831
3284
|
useAMS = false;
|
|
832
3285
|
}
|
|
833
3286
|
|
|
@@ -837,9 +3290,10 @@ class BambuPrinterMCPServer {
|
|
|
837
3290
|
result = await this.bambu.print3mf(host, bambuSerial, bambuToken, {
|
|
838
3291
|
projectName,
|
|
839
3292
|
filePath: threeMFPath,
|
|
840
|
-
plateIndex
|
|
3293
|
+
plateIndex,
|
|
841
3294
|
useAMS: useAMS,
|
|
842
3295
|
amsMapping: finalAmsMapping,
|
|
3296
|
+
amsSlots: finalAmsSlots,
|
|
843
3297
|
bedType: printBedType,
|
|
844
3298
|
bedLeveling: args?.bed_leveling !== undefined ? Boolean(args.bed_leveling) : undefined,
|
|
845
3299
|
flowCalibration: args?.flow_calibration !== undefined ? Boolean(args.flow_calibration) : undefined,
|
|
@@ -851,6 +3305,92 @@ class BambuPrinterMCPServer {
|
|
|
851
3305
|
break;
|
|
852
3306
|
}
|
|
853
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
|
+
|
|
854
3394
|
case "merge_vertices":
|
|
855
3395
|
if (!args?.stl_path) throw new Error("Missing required parameter: stl_path");
|
|
856
3396
|
result = await this.stlManipulator.mergeVertices(
|