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/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 { parse3MF } from './3mf_parser.js';
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.BAMBU_MODEL?.trim().toLowerCase() || "";
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 DEFAULT_SLICER_PATH = process.env.SLICER_PATH || "/Applications/BambuStudio.app/Contents/MacOS/BambuStudio";
52
- const DEFAULT_SLICER_PROFILE = process.env.SLICER_PROFILE || "";
375
+ const DEFAULT_SLICER_PROFILE = process.env.BAMBU_SLICER_PROFILE || process.env.SLICER_PROFILE || "";
376
+ const DEFAULT_TEMPLATE_3MF_PATH = process.env.BAMBU_TEMPLATE_3MF_PATH || "";
377
+ const DEFAULT_TEMPLATE_DIR = process.env.BAMBU_TEMPLATE_DIR ||
378
+ path.join(process.env.HOME || process.cwd(), "Sync", "bambu", "templates");
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: "slice_stl",
320
- 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.",
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
- stl_path: { type: "string", description: "Path to the STL or 3MF file to slice" },
325
- bambu_model: {
1853
+ mode: {
326
1854
  type: "string",
327
- enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
328
- description: "REQUIRED: Bambu Lab printer model. Ask the user if not known. Using the wrong model can damage the printer."
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
- slicer_path: { type: "string", description: "Path to the slicer executable (default: value from env)" },
335
- slicer_profile: { type: "string", description: "Path to the slicer profile/config file (optional, overrides bambu_model preset)" },
336
- nozzle_diameter: { type: "string", description: "Nozzle diameter in mm (default: 0.4)" },
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: ["stl_path", "bambu_model"]
1862
+ required: ["mode"]
357
1863
  }
358
1864
  },
359
1865
  {
360
- name: "list_printer_files",
361
- description: "List files stored on the Bambu Lab printer",
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: "upload_gcode",
373
- description: "Upload a G-code file to the Bambu Lab printer",
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
- filename: { type: "string", description: "Name for the file on the printer" },
378
- gcode: { type: "string", description: "G-code content to upload" },
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: ["filename", "gcode"]
1892
+ required: ["component", "temperature"]
384
1893
  }
385
1894
  },
386
1895
  {
387
- name: "upload_file",
388
- description: "Upload a local file to the Bambu Lab printer",
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
- file_path: { type: "string", description: "Local path to the file to upload" },
393
- filename: { type: "string", description: "Name for the file on the printer" },
394
- print: { type: "boolean", description: "Start printing after upload (default: false)" },
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: ["file_path", "filename"]
1910
+ required: ["fan", "speed"]
400
1911
  }
401
1912
  },
402
1913
  {
403
- name: "start_print_job",
404
- description: "Start printing a G-code file already on the Bambu Lab printer",
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
- filename: { type: "string", description: "Name of the file to print" },
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: ["filename"]
1932
+ required: ["light", "mode"]
414
1933
  }
415
1934
  },
416
1935
  {
417
- name: "cancel_print",
418
- description: "Cancel the current print job on the Bambu Lab printer",
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: "set_temperature",
430
- description: "Set the temperature of a printer component (bed, nozzle)",
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
- component: { type: "string", description: "Component to heat: bed, nozzle, or extruder" },
435
- temperature: { type: "number", description: "Target temperature in °C" },
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: ["component", "temperature"]
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 slot mapping array, e.g. [0, 2] maps filaments to AMS slots 0 and 2",
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 slicerType = String(args?.slicer_type || DEFAULT_SLICER_TYPE);
539
- const slicerPath = String(args?.slicer_path || DEFAULT_SLICER_PATH);
540
- const slicerProfile = String(args?.slicer_profile || DEFAULT_SLICER_PROFILE);
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 || !args?.gcode) {
552
- throw new Error("Missing required parameters: filename and gcode");
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
- result = await this.stlManipulator.sliceSTL(String(args.stl_path), slicerType, slicerPath, slicerProfile || undefined, undefined, // progressCallback
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
- threeMFPath = await this.stlManipulator.sliceSTL(threeMFPath, slicerType, slicerPath, slicerProfile || undefined, undefined, // progressCallback
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 = Object.values(parsed3MFData.slicerConfig.ams_mapping)
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.sort((a, b) => a - b);
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
- if (args?.ams_mapping) {
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.filter((v) => typeof v === 'number');
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 = Object.values(args.ams_mapping)
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: 0,
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");