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