mcp-3d-printer-server 1.2.9 → 1.2.11

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.
Files changed (86) hide show
  1. package/CHANGELOG.md +207 -0
  2. package/CONTRIBUTORS.md +49 -0
  3. package/README.md +528 -811
  4. package/dist/3mf_parser.js +8 -8
  5. package/dist/blender-mcp-bridge.d.ts +1 -0
  6. package/dist/blender-mcp-bridge.js +209 -21
  7. package/dist/fulu-orca.js +29 -5
  8. package/dist/index.js +614 -110
  9. package/dist/printers/bambu-node-guard.d.ts +2 -0
  10. package/dist/printers/bambu-node-guard.js +38 -0
  11. package/dist/printers/bambu.d.ts +53 -7
  12. package/dist/printers/bambu.js +420 -126
  13. package/dist/printers/creality.d.ts +15 -6
  14. package/dist/printers/creality.js +42 -9
  15. package/dist/printers/duet.d.ts +11 -6
  16. package/dist/printers/duet.js +57 -34
  17. package/dist/printers/generic-base.d.ts +69 -0
  18. package/dist/printers/generic-base.js +209 -0
  19. package/dist/printers/klipper.d.ts +17 -6
  20. package/dist/printers/klipper.js +63 -16
  21. package/dist/printers/octoprint.d.ts +18 -6
  22. package/dist/printers/octoprint.js +64 -9
  23. package/dist/printers/printer-factory.d.ts +11 -2
  24. package/dist/printers/printer-factory.js +9 -8
  25. package/dist/printers/prusa.d.ts +14 -6
  26. package/dist/printers/prusa.js +56 -9
  27. package/dist/printers/repetier.d.ts +18 -6
  28. package/dist/printers/repetier.js +48 -6
  29. package/dist/safety/archive.d.ts +11 -0
  30. package/dist/safety/archive.js +214 -0
  31. package/dist/safety/artifact.d.ts +6 -0
  32. package/dist/safety/artifact.js +66 -0
  33. package/dist/safety/confirmation.d.ts +22 -0
  34. package/dist/safety/confirmation.js +43 -0
  35. package/dist/safety/expected-peaks.d.ts +24 -0
  36. package/dist/safety/expected-peaks.js +24 -0
  37. package/dist/safety/gcode-thermal.d.ts +64 -0
  38. package/dist/safety/gcode-thermal.js +382 -0
  39. package/dist/safety/limits.d.ts +31 -0
  40. package/dist/safety/limits.js +105 -0
  41. package/dist/safety/print-file.d.ts +25 -0
  42. package/dist/safety/print-file.js +517 -0
  43. package/dist/safety/printer-state.d.ts +51 -0
  44. package/dist/safety/printer-state.js +365 -0
  45. package/dist/slicer/bambu-cli.d.ts +110 -0
  46. package/dist/slicer/bambu-cli.js +547 -0
  47. package/dist/slicer/profile-flatten.d.ts +104 -0
  48. package/dist/slicer/profile-flatten.js +676 -0
  49. package/dist/slicer/run-slicer.d.ts +18 -0
  50. package/dist/slicer/run-slicer.js +102 -0
  51. package/dist/slicer/slicer-error.d.ts +27 -0
  52. package/dist/slicer/slicer-error.js +53 -0
  53. package/dist/slicer/templates.d.ts +126 -0
  54. package/dist/slicer/templates.js +255 -0
  55. package/dist/stl/stl-manipulator.d.ts +25 -3
  56. package/dist/stl/stl-manipulator.js +214 -232
  57. package/package.json +16 -9
  58. package/server.json +29 -0
  59. package/src/3mf_parser.ts +8 -8
  60. package/src/blender-mcp-bridge.ts +200 -19
  61. package/src/fulu-orca.ts +36 -6
  62. package/src/index.ts +686 -137
  63. package/src/printers/bambu-node-guard.ts +44 -0
  64. package/src/printers/bambu.ts +458 -164
  65. package/src/printers/creality.ts +40 -9
  66. package/src/printers/duet.ts +64 -53
  67. package/src/printers/generic-base.ts +286 -0
  68. package/src/printers/klipper.ts +83 -28
  69. package/src/printers/octoprint.ts +74 -21
  70. package/src/printers/printer-factory.ts +21 -11
  71. package/src/printers/prusa.ts +53 -10
  72. package/src/printers/repetier.ts +55 -17
  73. package/src/safety/archive.ts +181 -0
  74. package/src/safety/artifact.ts +58 -0
  75. package/src/safety/confirmation.ts +56 -0
  76. package/src/safety/expected-peaks.ts +47 -0
  77. package/src/safety/gcode-thermal.ts +404 -0
  78. package/src/safety/limits.ts +98 -0
  79. package/src/safety/print-file.ts +354 -0
  80. package/src/safety/printer-state.ts +339 -0
  81. package/src/slicer/bambu-cli.ts +633 -0
  82. package/src/slicer/profile-flatten.ts +830 -0
  83. package/src/slicer/run-slicer.ts +126 -0
  84. package/src/slicer/slicer-error.ts +72 -0
  85. package/src/slicer/templates.ts +288 -0
  86. package/src/stl/stl-manipulator.ts +247 -275
package/dist/index.js CHANGED
@@ -14,9 +14,13 @@ import { randomUUID } from "node:crypto";
14
14
  import * as THREE from 'three';
15
15
  import { PrinterFactory } from "./printers/printer-factory.js";
16
16
  import { STLManipulator } from "./stl/stl-manipulator.js";
17
+ import { SlicerError } from "./slicer/slicer-error.js";
18
+ import { inspectSliceSettings, listTemplateRegistry, resolveTemplatePath, resolveTemplateSlicerProfilePath, saveTemplate, } from "./slicer/templates.js";
17
19
  import { parse3MF } from './3mf_parser.js';
18
20
  import { BambuImplementation } from "./printers/bambu.js";
19
- import { inspectFuluOrcaSetup, invokeFuluBridgeRpc, isFuluPrintRpcMethod, } from "./fulu-orca.js";
21
+ import { inspectFuluOrcaSetup, invokeFuluBridgeRpc, } from "./fulu-orca.js";
22
+ import { CONFIRMATION_UNAVAILABLE, confirmationTimedOut, confirmationTimeoutMs } from "./safety/confirmation.js";
23
+ import { validateExpectedPeaks } from "./safety/expected-peaks.js";
20
24
  // Load environment variables from .env file
21
25
  dotenv.config();
22
26
  // Default values
@@ -127,6 +131,208 @@ const BAMBU_MODEL_PRESETS = {
127
131
  a1mini: (n) => `Bambu Lab A1 mini ${n} nozzle`,
128
132
  h2d: (n) => `Bambu Lab H2D ${n} nozzle`,
129
133
  };
134
+ // Models whose bundled machine presets the Bambu-compatible CLI slicing path
135
+ // can target. The installed slicer must still contain the exact
136
+ // "<model> <nozzle> nozzle" preset; a missing preset stops the slice.
137
+ const SLICE_BAMBU_MODELS = ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s", "h2c"];
138
+ const SLICE_BAMBU_MODEL_PRESETS = {
139
+ ...BAMBU_MODEL_PRESETS,
140
+ p2s: (n) => `Bambu Lab P2S ${n} nozzle`,
141
+ h2s: (n) => `Bambu Lab H2S ${n} nozzle`,
142
+ h2c: (n) => `Bambu Lab H2C ${n} nozzle`,
143
+ };
144
+ const DEFAULT_TEMPLATE_3MF_PATH = process.env.BAMBU_TEMPLATE_3MF_PATH || "";
145
+ const VALID_NOZZLE_TYPES = ["stainless_steel", "hardened_steel", "tungsten_carbide", "brass"];
146
+ /** The installed hotend material. Bambu machine presets assume the stock nozzle; the print gate compares this with the printer. */
147
+ function resolveNozzleType(value) {
148
+ const raw = value === undefined || value === null || value === "" ? process.env.BAMBU_NOZZLE_TYPE?.trim() : value;
149
+ if (raw === undefined || raw === "")
150
+ return undefined;
151
+ const normalized = String(raw).trim().toLowerCase().replace(/[\s-]+/g, "_");
152
+ if (!VALID_NOZZLE_TYPES.includes(normalized)) {
153
+ throw new Error(`Invalid nozzle type "${raw}". Valid types: ${VALID_NOZZLE_TYPES.join(", ")}`);
154
+ }
155
+ return normalized;
156
+ }
157
+ // Shared slice_stl / slice_with_template argument schema.
158
+ const SLICE_TOOL_PROPERTIES = {
159
+ bambu_model: {
160
+ type: "string",
161
+ enum: [...SLICE_BAMBU_MODELS],
162
+ description: "Bambu Lab printer model. Required for bambustudio and orcaslicer-bambulab (elicited or read from BAMBU_MODEL when omitted); " +
163
+ "passing it with orcaslicer selects the Bambu-compatible path. The installed slicer must contain the exact model/nozzle preset.",
164
+ },
165
+ nozzle_diameter: {
166
+ type: "string",
167
+ description: "Nozzle diameter in mm (default: NOZZLE_DIAMETER or 0.4). Selects the '<model> <diameter> nozzle' machine preset.",
168
+ },
169
+ slicer_type: {
170
+ type: "string",
171
+ enum: [...SLICER_TYPE_ENUM],
172
+ description: "Type of slicer to use (prusaslicer, cura, slic3r, orcaslicer, orcaslicer-bambulab, bambustudio). Use orcaslicer-bambulab for the FULU fork. " +
173
+ "bambustudio and orcaslicer-bambulab (and orcaslicer with bambu_model) export a sliced 3MF.",
174
+ },
175
+ slicer_path: {
176
+ type: "string",
177
+ description: "Path to the slicer executable (default: value from env). Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1.",
178
+ },
179
+ slicer_profile: {
180
+ type: "string",
181
+ description: "Profile to use for slicing (default: SLICER_PROFILE env). Bambu-compatible slicing: one process profile JSON (the machine preset comes from bambu_model). " +
182
+ "Generic OrcaSlicer: machine/process profiles separated with ';', optionally followed by '|filament.json'.",
183
+ },
184
+ filament_profile: {
185
+ type: "string",
186
+ description: "Filament profile path(s), ';'-separated in slot order, loaded with --load-filaments (default: FILAMENT_PROFILE/SLICER_FILAMENT_PROFILE env). Alias of load_filaments.",
187
+ },
188
+ load_filaments: {
189
+ type: "string",
190
+ description: "Bambu-compatible slicing: filament profile JSON paths in slot order, ';'-separated. One profile applies to every project slot; otherwise supply one per slot.",
191
+ },
192
+ load_filament_ids: {
193
+ type: "string",
194
+ description: "Bambu-compatible slicing: comma-separated filament IDs mapping load_filaments to objects, e.g. '1,2,3,1'.",
195
+ },
196
+ filament_colours: {
197
+ type: "string",
198
+ description: "Bambu-compatible slicing: one #RRGGBB per filament slot, ';'-separated. Defaults to the input 3MF's colours, then each profile's colour.",
199
+ },
200
+ bed_type: {
201
+ type: "string",
202
+ enum: [...VALID_BED_TYPES],
203
+ description: "Bambu-compatible slicing: build plate type (default: BED_TYPE or textured_plate).",
204
+ },
205
+ nozzle_type: {
206
+ type: "string",
207
+ enum: [...VALID_NOZZLE_TYPES],
208
+ description: "Bambu-compatible slicing: the hotend nozzle material installed on the printer (default: BAMBU_NOZZLE_TYPE, else the model preset's stock nozzle, usually stainless_steel; X1C/X1E presets use hardened_steel). Printing compares it with the printer's reported nozzle.",
209
+ },
210
+ template_3mf_path: {
211
+ type: "string",
212
+ description: "Bambu-compatible slicing: 3MF or profile whose embedded slicer settings are reused as the process profile (default: BAMBU_TEMPLATE_3MF_PATH). An explicit slicer_profile takes precedence.",
213
+ },
214
+ template_name: {
215
+ type: "string",
216
+ description: "Named template from the local registry (see list_templates); resolves to its file.",
217
+ },
218
+ template_dir: {
219
+ type: "string",
220
+ description: "Template directory override when resolving template_name (default: BAMBU_TEMPLATE_DIR or ~/Sync/bambu/templates).",
221
+ },
222
+ uptodate: { type: "boolean", description: "Bambu-compatible slicing: refresh 3MF preset configs to the installed slicer version (--uptodate)." },
223
+ repetitions: { type: "number", description: "Bambu-compatible slicing: print N identical copies (--repetitions)." },
224
+ orient: { type: "boolean", description: "Bambu-compatible slicing: auto-orient for printability (--orient)." },
225
+ arrange: { type: "boolean", description: "Bambu-compatible slicing: auto-arrange objects on the plate (--arrange). Set false to keep an existing layout." },
226
+ ensure_on_bed: { type: "boolean", description: "Bambu-compatible slicing: lower floating models onto the bed (--ensure-on-bed)." },
227
+ clone_objects: { type: "string", description: "Bambu-compatible slicing: comma-separated clone counts per object index, e.g. '1,3,1,10'." },
228
+ skip_objects: { type: "string", description: "Bambu-compatible slicing: comma-separated object indices to skip, e.g. '3,5,10'." },
229
+ enable_timelapse: { type: "boolean", description: "Bambu-compatible slicing: insert timelapse parking moves (--enable-timelapse)." },
230
+ allow_mix_temp: { type: "boolean", description: "Bambu-compatible slicing: allow filaments with different temperature requirements on one plate." },
231
+ scale: { type: "number", description: "Bambu-compatible slicing: uniform scale factor applied before slicing (1.0 = original size)." },
232
+ rotate: { type: "number", description: "Bambu-compatible slicing: Z-axis rotation in degrees before slicing." },
233
+ rotate_x: { type: "number", description: "Bambu-compatible slicing: X-axis rotation in degrees before slicing." },
234
+ rotate_y: { type: "number", description: "Bambu-compatible slicing: Y-axis rotation in degrees before slicing." },
235
+ min_save: { type: "boolean", description: "Bambu-compatible slicing: write a smaller output 3MF (--min-save)." },
236
+ skip_modified_gcodes: { type: "boolean", description: "Bambu-compatible slicing: ignore custom G-code embedded in an input 3MF (--skip-modified-gcodes)." },
237
+ slice_plate: { type: "number", description: "Bambu-compatible slicing: plate number to slice; 0 slices all plates (default)." },
238
+ };
239
+ function validateSliceBambuModel(model) {
240
+ const normalized = model.trim().toLowerCase();
241
+ if (!SLICE_BAMBU_MODELS.includes(normalized)) {
242
+ throw new Error(`Invalid bambu_model "${model}" for slicing. Valid models: ${SLICE_BAMBU_MODELS.join(", ")}`);
243
+ }
244
+ return normalized;
245
+ }
246
+ function normalizeSliceNozzleDiameter(raw) {
247
+ const text = String(raw ?? "").trim() || DEFAULT_NOZZLE_DIAMETER;
248
+ const value = Number(text);
249
+ if (!/^\d+(\.\d+)?$/.test(text) || !Number.isFinite(value) || value <= 0 || value > 2) {
250
+ throw new Error(`Invalid nozzle_diameter "${text}". Use a diameter in mm such as 0.2, 0.4, 0.6, or 0.8.`);
251
+ }
252
+ return String(value);
253
+ }
254
+ /** Parse "#RRGGBB;#RRGGBB" (leading '#' optional) into positional slot colours. */
255
+ function parseFilamentColours(value) {
256
+ const colours = value.split(";").map((c) => c.trim()).filter(Boolean)
257
+ .map((c) => (c.startsWith("#") ? c : `#${c}`).toUpperCase());
258
+ for (const c of colours) {
259
+ if (!/^#[0-9A-F]{6}([0-9A-F]{2})?$/.test(c)) {
260
+ throw new Error(`Invalid filament colour "${c}"; expected #RRGGBB.`);
261
+ }
262
+ }
263
+ if (colours.length === 0)
264
+ throw new Error("Invalid filament_colours: list at least one colour.");
265
+ return colours;
266
+ }
267
+ function optionalFiniteNumber(args, key) {
268
+ if (args?.[key] === undefined)
269
+ return undefined;
270
+ const value = Number(args[key]);
271
+ if (!Number.isFinite(value))
272
+ throw new Error(`Invalid ${key}: expected a number.`);
273
+ return value;
274
+ }
275
+ function optionalBoolean(args, key) {
276
+ const value = args?.[key];
277
+ if (value === undefined)
278
+ return undefined;
279
+ if (typeof value !== "boolean")
280
+ throw new Error(`Invalid ${key}: expected a boolean.`);
281
+ return value;
282
+ }
283
+ /** Map slice_stl/slice_with_template arguments to Bambu-compatible CLI options. */
284
+ function buildBambuSliceOptions(args) {
285
+ const options = {};
286
+ const text = (key) => (args?.[key] !== undefined && String(args[key]).trim() ? String(args[key]).trim() : undefined);
287
+ options.uptodate = optionalBoolean(args, "uptodate");
288
+ options.orient = optionalBoolean(args, "orient");
289
+ options.arrange = optionalBoolean(args, "arrange");
290
+ options.ensureOnBed = optionalBoolean(args, "ensure_on_bed");
291
+ options.enableTimelapse = optionalBoolean(args, "enable_timelapse");
292
+ options.allowMixTemp = optionalBoolean(args, "allow_mix_temp");
293
+ options.minSave = optionalBoolean(args, "min_save");
294
+ options.skipModifiedGcodes = optionalBoolean(args, "skip_modified_gcodes");
295
+ options.repetitions = optionalFiniteNumber(args, "repetitions");
296
+ options.scale = optionalFiniteNumber(args, "scale");
297
+ options.rotate = optionalFiniteNumber(args, "rotate");
298
+ options.rotateX = optionalFiniteNumber(args, "rotate_x");
299
+ options.rotateY = optionalFiniteNumber(args, "rotate_y");
300
+ options.slicePlate = optionalFiniteNumber(args, "slice_plate");
301
+ if (options.repetitions !== undefined && (!Number.isInteger(options.repetitions) || options.repetitions < 1)) {
302
+ throw new Error("Invalid repetitions: expected a positive integer.");
303
+ }
304
+ if (options.slicePlate !== undefined && (!Number.isInteger(options.slicePlate) || options.slicePlate < 0)) {
305
+ throw new Error("Invalid slice_plate: expected 0 (all plates) or a plate number.");
306
+ }
307
+ if (options.scale !== undefined && options.scale <= 0)
308
+ throw new Error("Invalid scale: expected a positive factor.");
309
+ options.cloneObjects = text("clone_objects");
310
+ options.skipObjects = text("skip_objects");
311
+ options.loadFilamentIds = text("load_filament_ids");
312
+ const loadFilaments = text("load_filaments");
313
+ const filamentAlias = text("filament_profile");
314
+ if (loadFilaments && filamentAlias && loadFilaments !== filamentAlias) {
315
+ throw new Error("Provide either load_filaments or filament_profile, not conflicting values.");
316
+ }
317
+ options.loadFilaments = loadFilaments ?? filamentAlias;
318
+ const colours = text("filament_colours");
319
+ if (colours)
320
+ options.filamentColours = parseFilamentColours(colours);
321
+ options.bedType = resolveBedType(args?.bed_type);
322
+ options.nozzleType = resolveNozzleType(args?.nozzle_type);
323
+ return Object.fromEntries(Object.entries(options).filter(([, value]) => value !== undefined));
324
+ }
325
+ function defaultSlicerPathFor(slicerType) {
326
+ if (process.env.SLICER_PATH)
327
+ return process.env.SLICER_PATH;
328
+ if (slicerType === DEFAULT_SLICER_TYPE)
329
+ return DEFAULT_SLICER_PATH;
330
+ if (slicerType === "bambustudio")
331
+ return defaultBambuStudioPath();
332
+ if (slicerType === "orcaslicer-bambulab")
333
+ return defaultFuluOrcaPath();
334
+ return DEFAULT_SLICER_PATH;
335
+ }
130
336
  function validateBambuModel(model) {
131
337
  const normalized = model.trim().toLowerCase();
132
338
  if (!VALID_BAMBU_MODELS.includes(normalized)) {
@@ -141,6 +347,34 @@ function resolveBedType(bedType) {
141
347
  }
142
348
  return resolved;
143
349
  }
350
+ const SUPPORTED_NOZZLE_DIAMETERS = [0.2, 0.4, 0.6, 0.8];
351
+ /** Optional declared material; blank means "not declared". */
352
+ function optionalMaterialArg(value) {
353
+ if (value === undefined || value === null || value === "")
354
+ return undefined;
355
+ if (typeof value !== "string" || !value.trim()) {
356
+ throw new Error("material must be a non-empty string such as PLA, PETG, ABS, ASA, TPU, PA or PC.");
357
+ }
358
+ return value.trim();
359
+ }
360
+ /** Temperatures are validated as finite numbers before any printer connection. */
361
+ function optionalTemperatureArg(value, name) {
362
+ if (value === undefined)
363
+ return undefined;
364
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
365
+ throw new Error(`${name} must be a finite, non-negative number in °C.`);
366
+ }
367
+ return value;
368
+ }
369
+ function optionalNozzleDiameterArg(value) {
370
+ if (value === undefined || value === null || value === "")
371
+ return undefined;
372
+ const diameter = Number(value);
373
+ if ((typeof value !== "number" && typeof value !== "string") || !SUPPORTED_NOZZLE_DIAMETERS.includes(diameter)) {
374
+ throw new Error(`nozzle_diameter must be one of ${SUPPORTED_NOZZLE_DIAMETERS.join(", ")} mm.`);
375
+ }
376
+ return diameter;
377
+ }
144
378
  function parseBooleanEnv(rawValue, fallback) {
145
379
  if (rawValue === undefined) {
146
380
  return fallback;
@@ -199,10 +433,21 @@ function readRuntimeConfig() {
199
433
  allowBridgeCommandArg: parseBooleanEnv(process.env.MCP_ALLOW_BRIDGE_COMMAND_ARG, false),
200
434
  };
201
435
  }
436
+ const SLICING_TOOLS = new Set(["slice_stl", "slice_with_template", "list_templates", "save_template", "get_slice_settings"]);
202
437
  // Ensure temp directory exists
203
438
  if (!fs.existsSync(TEMP_DIR)) {
204
439
  fs.mkdirSync(TEMP_DIR, { recursive: true });
205
440
  }
441
+ /** MQTT "Not authorized", FTP 530, and HTTP 401/403 all mean the printer rejected its credentials. */
442
+ function isCredentialRejection(message) {
443
+ return /not authori[sz]ed|\b530\b|login incorrect|\b401\b|\b403\b|unauthori[sz]ed|forbidden|bad user name or password/i.test(message);
444
+ }
445
+ function credentialSuggestion() {
446
+ return (process.env.PRINTER_TYPE ?? "").toLowerCase() === "bambu"
447
+ ? "The printer rejected its LAN access code. Read the current Access Code on the printer (Settings > Network/WLAN; it changes " +
448
+ "when LAN mode is toggled and after some resets), update BAMBU_TOKEN, and check BAMBU_SERIAL. Retrying unchanged will not help."
449
+ : "The printer rejected its credentials. Check API_KEY (and PRINTER_HOST/PRINTER_PORT) for this printer system. Retrying unchanged will not help.";
450
+ }
206
451
  class ThreeDPrinterMCPServer {
207
452
  constructor() {
208
453
  this.blender = new BlenderMcpBridge();
@@ -216,11 +461,36 @@ class ThreeDPrinterMCPServer {
216
461
  tools: {}
217
462
  }
218
463
  });
219
- this.printerFactory = new PrinterFactory();
464
+ this.printerFactory = new PrinterFactory((message) => this.confirmHardware(message));
220
465
  this.stlManipulator = new STLManipulator(TEMP_DIR);
221
466
  this.setupHandlers();
222
467
  this.setupErrorHandling();
223
468
  }
469
+ /** Human MCP elicitation before a print start or positive heating command. */
470
+ async confirmHardware(message) {
471
+ let response;
472
+ const timeout = confirmationTimeoutMs();
473
+ try {
474
+ response = await this.server.elicitInput({
475
+ mode: "form",
476
+ message,
477
+ requestedSchema: {
478
+ type: "object",
479
+ properties: {
480
+ confirmed: { type: "boolean", title: "I checked the printer and confirm this operation", default: false },
481
+ },
482
+ required: ["confirmed"],
483
+ },
484
+ }, { timeout });
485
+ }
486
+ catch (error) {
487
+ // A person may take minutes to check the bed; a timeout is not a missing capability.
488
+ if (error?.code === ErrorCode.RequestTimeout)
489
+ throw new Error(confirmationTimedOut(timeout));
490
+ throw new Error(CONFIRMATION_UNAVAILABLE);
491
+ }
492
+ return response.action === "accept" && response.content?.confirmed === true;
493
+ }
224
494
  async resolveBambuModel(argsModel) {
225
495
  const fromArgs = (argsModel || DEFAULT_BAMBU_MODEL).trim().toLowerCase();
226
496
  if (fromArgs) {
@@ -252,7 +522,7 @@ class ThreeDPrinterMCPServer {
252
522
  },
253
523
  required: ["bambu_model"],
254
524
  },
255
- });
525
+ }, { timeout: confirmationTimeoutMs() });
256
526
  if (result.action === "accept" && result.content?.bambu_model) {
257
527
  return validateBambuModel(String(result.content.bambu_model));
258
528
  }
@@ -260,6 +530,10 @@ class ThreeDPrinterMCPServer {
260
530
  }
261
531
  catch (elicitError) {
262
532
  const msg = elicitError?.message || String(elicitError);
533
+ if (elicitError?.code === ErrorCode.RequestTimeout) {
534
+ throw new Error("No printer model was chosen in time; nothing was sent. Set BAMBU_MODEL or pass bambu_model, " +
535
+ `or answer the prompt within PRINT_CONFIRMATION_TIMEOUT_MS. Valid models: ${VALID_BAMBU_MODELS.join(", ")}`);
536
+ }
263
537
  if (elicitError?.code === -32601 || elicitError?.code === -32600 ||
264
538
  msg.includes("does not support") || msg.includes("elicitation")) {
265
539
  throw new Error("bambu_model is required but your MCP client does not support elicitation. " +
@@ -269,6 +543,54 @@ class ThreeDPrinterMCPServer {
269
543
  throw elicitError;
270
544
  }
271
545
  }
546
+ /** Slicing accepts every model with a Bambu-compatible CLI preset mapping. */
547
+ async resolveSliceBambuModel(argsModel) {
548
+ const requested = (argsModel || DEFAULT_BAMBU_MODEL).trim().toLowerCase();
549
+ if (requested) {
550
+ return validateSliceBambuModel(requested);
551
+ }
552
+ return this.resolveBambuModel(undefined);
553
+ }
554
+ /** Shared slice_stl / slice_with_template implementation. */
555
+ async runSliceTool(toolName, args) {
556
+ if (!args?.stl_path) {
557
+ throw new Error("Missing required parameter: stl_path");
558
+ }
559
+ const slicerType = normalizeSlicerType(String(args.slicer_type || DEFAULT_SLICER_TYPE));
560
+ const explicitModel = typeof args.bambu_model === "string" && args.bambu_model.trim() ? args.bambu_model : undefined;
561
+ // Generic OrcaSlicer keeps its user-supplied machine profile unless the
562
+ // call explicitly targets a Bambu model.
563
+ const bambuCli = isBambuProjectSlicer(slicerType) || (slicerType === "orcaslicer" && explicitModel !== undefined);
564
+ let printerPreset;
565
+ let options;
566
+ if (bambuCli) {
567
+ const model = await this.resolveSliceBambuModel(explicitModel);
568
+ printerPreset = SLICE_BAMBU_MODEL_PRESETS[model](normalizeSliceNozzleDiameter(args.nozzle_diameter));
569
+ options = buildBambuSliceOptions(args);
570
+ }
571
+ // Process profile precedence: explicit slicer_profile, then a template, then SLICER_PROFILE.
572
+ const explicitProfile = typeof args.slicer_profile === "string" && args.slicer_profile.trim() ? args.slicer_profile.trim() : undefined;
573
+ const templateDir = typeof args.template_dir === "string" ? args.template_dir : undefined;
574
+ const requestedTemplate = resolveTemplatePath(typeof args.template_name === "string" ? args.template_name : undefined, templateDir, ["json", "config", "3mf"]) ||
575
+ (typeof args.template_3mf_path === "string" && args.template_3mf_path.trim() ? args.template_3mf_path.trim() : undefined);
576
+ if (requestedTemplate && !bambuCli) {
577
+ throw new Error("template_name/template_3mf_path apply to Bambu-compatible slicing (bambustudio, orcaslicer-bambulab, or orcaslicer with bambu_model).");
578
+ }
579
+ let slicerProfile = explicitProfile;
580
+ const templatePath = requestedTemplate || (bambuCli && DEFAULT_TEMPLATE_3MF_PATH ? DEFAULT_TEMPLATE_3MF_PATH : undefined);
581
+ if (!slicerProfile && templatePath) {
582
+ if (!fs.existsSync(templatePath)) {
583
+ throw new Error(`Template not found: ${templatePath}`);
584
+ }
585
+ slicerProfile = await resolveTemplateSlicerProfilePath(templatePath, path.join(TEMP_DIR, "templates"));
586
+ }
587
+ slicerProfile ?? (slicerProfile = DEFAULT_SLICER_PROFILE || undefined);
588
+ const explicitFilament = typeof args.filament_profile === "string" && args.filament_profile.trim() ? args.filament_profile.trim() : undefined;
589
+ const filamentProfile = options?.loadFilaments ?? explicitFilament ?? (DEFAULT_FILAMENT_PROFILE || undefined);
590
+ const slicerPath = this.resolveExecutableSelectorArg(args.slicer_path, toolName, "slicer_path") ?? defaultSlicerPathFor(slicerType);
591
+ return this.stlManipulator.sliceSTL(String(args.stl_path), slicerType, slicerPath, slicerProfile, undefined, // progressCallback
592
+ printerPreset, filamentProfile, options);
593
+ }
272
594
  setupErrorHandling() {
273
595
  this.server.onerror = (error) => {
274
596
  console.error("[MCP Error]", error);
@@ -466,7 +788,7 @@ class ThreeDPrinterMCPServer {
466
788
  },
467
789
  {
468
790
  name: "upload_gcode",
469
- description: "Upload G-code content or a local G-code file path to the printer",
791
+ description: "Upload G-code content or a local G-code file path to the printer. With print=true the exact uploaded bytes are inspected first (every S/R heater target, tool changes, hardware and material ceilings), printer state is checked, and a human confirmation is requested before the print starts.",
470
792
  inputSchema: {
471
793
  type: "object",
472
794
  properties: {
@@ -513,14 +835,18 @@ class ThreeDPrinterMCPServer {
513
835
  },
514
836
  print: {
515
837
  type: "boolean",
516
- description: "Start printing after upload when the printer backend supports it."
838
+ description: "Start printing after upload when the printer backend supports it. Printing requires a declared material from slicer metadata (; filament_type = PLA) or the material argument."
839
+ },
840
+ material: {
841
+ type: "string",
842
+ description: "Declared filament material (for example PLA, PETG, ABS, ASA, TPU, PA, PC) when the G-code has no slicer filament_type metadata. Must not contradict the file. Material ceilings limit nozzle targets."
517
843
  }
518
844
  }
519
845
  }
520
846
  },
521
847
  {
522
848
  name: "start_print",
523
- description: "Start printing a file that is already available on the printer",
849
+ description: "Start printing a G-code file already stored on the printer. The server downloads and inspects the exact file, then starts a uniquely named checked copy after printer-state checks and human confirmation. Printers whose API cannot download files (Repetier, Prusa, Creality) refuse; use upload_gcode with print=true instead.",
524
850
  inputSchema: {
525
851
  type: "object",
526
852
  properties: {
@@ -556,6 +882,10 @@ class ThreeDPrinterMCPServer {
556
882
  filename: {
557
883
  type: "string",
558
884
  description: "Name/path of the printer-side G-code file to start."
885
+ },
886
+ material: {
887
+ type: "string",
888
+ description: "Declared filament material when the G-code has no slicer filament_type metadata (non-Bambu printers). Must not contradict the file."
559
889
  }
560
890
  },
561
891
  required: ["filename"]
@@ -596,7 +926,7 @@ class ThreeDPrinterMCPServer {
596
926
  },
597
927
  {
598
928
  name: "set_printer_temperature",
599
- description: "Set the temperature of a printer component",
929
+ description: "Set the temperature of a printer component. Temperature 0 switches a heater off and is never gated. Positive targets are validated before connecting, limited by independent hardware and material ceilings, require a ready printer and a human confirmation.",
600
930
  inputSchema: {
601
931
  type: "object",
602
932
  properties: {
@@ -630,7 +960,20 @@ class ThreeDPrinterMCPServer {
630
960
  },
631
961
  temperature: {
632
962
  type: "number",
633
- description: "Target temperature in Celsius."
963
+ description: "Target temperature in Celsius: a finite number >= 0. 0 switches the heater off."
964
+ },
965
+ material: {
966
+ type: "string",
967
+ description: "Declared material at the nozzle (for example PLA, PETG, ABS). Required for positive nozzle heating, including non-RFID spools."
968
+ },
969
+ bambu_model: {
970
+ type: "string",
971
+ enum: [...VALID_BAMBU_MODELS],
972
+ description: "Bambu printer model; required for positive Bambu heating unless BAMBU_MODEL is configured. Checked against the live printer."
973
+ },
974
+ nozzle_diameter: {
975
+ type: "string",
976
+ description: "Installed Bambu nozzle diameter in mm for nozzle heating (default: NOZZLE_DIAMETER or 0.4). Checked against the live printer."
634
977
  }
635
978
  },
636
979
  required: ["component", "temperature"]
@@ -657,7 +1000,10 @@ class ThreeDPrinterMCPServer {
657
1000
  },
658
1001
  {
659
1002
  name: "slice_stl",
660
- description: "Slice an STL or 3MF file to generate G-code or sliced 3MF",
1003
+ description: "Slice an STL or 3MF file to generate G-code or a sliced 3MF. For Bambu-compatible CLI slicing " +
1004
+ "(bambustudio, orcaslicer-bambulab, or orcaslicer with bambu_model), the exact bambu_model/nozzle machine " +
1005
+ "preset from the selected slicer installation is required, profile inheritance is resolved before the CLI " +
1006
+ "runs, and the result must contain plate G-code. Failures stop with the slicer's exit status and output.",
661
1007
  inputSchema: {
662
1008
  type: "object",
663
1009
  properties: {
@@ -665,39 +1011,73 @@ class ThreeDPrinterMCPServer {
665
1011
  type: "string",
666
1012
  description: "Path to the STL or 3MF file to slice"
667
1013
  },
668
- bambu_model: {
669
- type: "string",
670
- enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
671
- description: "Bambu Lab printer model - required when using Bambu project slicers to ensure correct G-code."
672
- },
673
- nozzle_diameter: {
674
- type: "string",
675
- description: "Nozzle diameter in mm (default: 0.4)."
676
- },
677
- slicer_type: {
678
- type: "string",
679
- enum: [...SLICER_TYPE_ENUM],
680
- description: "Type of slicer to use (prusaslicer, cura, slic3r, orcaslicer, orcaslicer-bambulab, bambustudio). Use orcaslicer-bambulab for the FULU fork."
681
- },
682
- slicer_path: {
683
- type: "string",
684
- description: "Path to the slicer executable (default: value from env). Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1."
685
- },
1014
+ ...SLICE_TOOL_PROPERTIES,
1015
+ },
1016
+ required: ["stl_path"]
1017
+ }
1018
+ },
1019
+ {
1020
+ name: "slice_with_template",
1021
+ description: "Slice an STL or 3MF with a named template from the local template registry (BAMBU_TEMPLATE_DIR). " +
1022
+ "The template supplies process settings; the machine preset still comes from bambu_model and nozzle_diameter.",
1023
+ inputSchema: {
1024
+ type: "object",
1025
+ properties: {
1026
+ stl_path: { type: "string", description: "Path to the STL or 3MF file to slice" },
1027
+ ...SLICE_TOOL_PROPERTIES,
1028
+ template_name: { type: "string", description: "Named template from the local registry (required)." },
686
1029
  slicer_profile: {
687
1030
  type: "string",
688
- description: "Profile to use for slicing (default: value from env). OrcaSlicer also accepts machine/process profiles separated with ';', optionally followed by '|filament.json'."
1031
+ description: "Explicit process profile that overrides the named template only when provided in this call."
689
1032
  },
690
- filament_profile: {
1033
+ },
1034
+ required: ["stl_path", "template_name"]
1035
+ }
1036
+ },
1037
+ {
1038
+ name: "list_templates",
1039
+ description: "List saved slicing templates (.3mf, .json, .config) in the local template registry directory.",
1040
+ inputSchema: {
1041
+ type: "object",
1042
+ properties: {
1043
+ template_dir: {
691
1044
  type: "string",
692
- description: "OrcaSlicer filament profile path loaded with --load-filaments (default: FILAMENT_PROFILE/SLICER_FILAMENT_PROFILE env)."
1045
+ description: "Template directory override. Defaults to BAMBU_TEMPLATE_DIR or ~/Sync/bambu/templates."
1046
+ }
1047
+ }
1048
+ }
1049
+ },
1050
+ {
1051
+ name: "save_template",
1052
+ description: "Copy a .3mf, .json, or .config file into the local template registry under a template name.",
1053
+ inputSchema: {
1054
+ type: "object",
1055
+ properties: {
1056
+ source_path: { type: "string", description: "Local .3mf, .json, or .config file to save as a template." },
1057
+ template_name: { type: "string", description: "Template name. Defaults to the source filename without extension." },
1058
+ template_dir: {
1059
+ type: "string",
1060
+ description: "Template directory override. Defaults to BAMBU_TEMPLATE_DIR or ~/Sync/bambu/templates."
693
1061
  }
694
1062
  },
695
- required: ["stl_path"]
1063
+ required: ["source_path"]
1064
+ }
1065
+ },
1066
+ {
1067
+ name: "get_slice_settings",
1068
+ description: "Inspect slicer settings in a 3MF template or JSON/config profile without slicing (layer height, infill, walls, supports, brim, bed, printer, filaments).",
1069
+ inputSchema: {
1070
+ type: "object",
1071
+ properties: {
1072
+ source_path: { type: "string", description: "Path to a 3MF, extracted project_settings.config, or slicer profile JSON." },
1073
+ template_name: { type: "string", description: "Named template from the local registry; used when source_path is omitted." },
1074
+ template_dir: { type: "string", description: "Template directory override when resolving template_name." }
1075
+ }
696
1076
  }
697
1077
  },
698
1078
  {
699
1079
  name: "confirm_temperatures",
700
- description: "Confirm temperature settings in a G-code file",
1080
+ description: "Report every heater target in a G-code file (S and R forms, tool-addressed, RepRapFirmware G10/M568 and Klipper SET_HEATER_TEMPERATURE). An expected temperature matches only when it equals the file's highest target. Read-only; printing tools enforce their own safety gate.",
701
1081
  inputSchema: {
702
1082
  type: "object",
703
1083
  properties: {
@@ -707,11 +1087,11 @@ class ThreeDPrinterMCPServer {
707
1087
  },
708
1088
  extruder_temp: {
709
1089
  type: "number",
710
- description: "Expected extruder temperature"
1090
+ description: "Expected highest nozzle target"
711
1091
  },
712
1092
  bed_temp: {
713
1093
  type: "number",
714
- description: "Expected bed temperature"
1094
+ description: "Expected highest bed target"
715
1095
  }
716
1096
  },
717
1097
  required: ["gcode_path"]
@@ -719,7 +1099,7 @@ class ThreeDPrinterMCPServer {
719
1099
  },
720
1100
  {
721
1101
  name: "process_and_print_stl",
722
- description: "Process an STL file (extend base), slice it, confirm temperatures, and start printing",
1102
+ description: "Process an STL file (extend base), slice it, and start printing through the same checked print gate as upload_gcode/print_3mf. Expected temperatures are enforced: a mismatch refuses before upload.",
723
1103
  inputSchema: {
724
1104
  type: "object",
725
1105
  properties: {
@@ -733,11 +1113,15 @@ class ThreeDPrinterMCPServer {
733
1113
  },
734
1114
  extruder_temp: {
735
1115
  type: "number",
736
- description: "Expected extruder temperature"
1116
+ description: "Expected highest nozzle target in the sliced G-code (S and R forms, every tool). Printing stops before upload if it differs."
737
1117
  },
738
1118
  bed_temp: {
739
1119
  type: "number",
740
- description: "Expected bed temperature"
1120
+ description: "Expected highest bed target in the sliced G-code (S and R forms). Printing stops before upload if it differs."
1121
+ },
1122
+ material: {
1123
+ type: "string",
1124
+ description: "Declared filament material when the sliced G-code has no filament_type metadata. Must not contradict the file."
741
1125
  },
742
1126
  host: {
743
1127
  type: "string",
@@ -765,6 +1149,11 @@ class ThreeDPrinterMCPServer {
765
1149
  enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate"],
766
1150
  description: "Bed/plate type installed on the printer (default: textured_plate)."
767
1151
  },
1152
+ nozzle_type: {
1153
+ type: "string",
1154
+ enum: [...VALID_NOZZLE_TYPES],
1155
+ description: "Installed Bambu nozzle material used when slicing (default: BAMBU_NOZZLE_TYPE, else the preset's stock nozzle). The print gate compares it with the printer's report."
1156
+ },
768
1157
  nozzle_diameter: {
769
1158
  type: "string",
770
1159
  description: "Nozzle diameter in mm (default: 0.4)."
@@ -971,7 +1360,7 @@ class ThreeDPrinterMCPServer {
971
1360
  },
972
1361
  {
973
1362
  name: "print_3mf",
974
- description: "Print a 3MF file on a Bambu Lab printer, potentially overriding settings.",
1363
+ description: "Print a 3MF file on a Bambu Lab printer. The exact selected plate is inspected (model, nozzle, bed type, materials, every heater target) and checked against a fresh MQTT report of the printer's identity, nozzle, state, errors and loaded filament, then a human confirmation is requested before upload and start.",
975
1364
  inputSchema: {
976
1365
  type: "object",
977
1366
  properties: {
@@ -989,6 +1378,11 @@ class ThreeDPrinterMCPServer {
989
1378
  enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate"],
990
1379
  description: "Bed/plate type installed on the printer (default: textured_plate)."
991
1380
  },
1381
+ nozzle_type: {
1382
+ type: "string",
1383
+ enum: [...VALID_NOZZLE_TYPES],
1384
+ description: "Installed nozzle material, used when the 3MF must be auto-sliced (default: BAMBU_NOZZLE_TYPE, else the preset's stock nozzle)."
1385
+ },
992
1386
  nozzle_diameter: {
993
1387
  type: "string",
994
1388
  description: "Nozzle diameter in mm (default: 0.4)."
@@ -1106,7 +1500,7 @@ class ThreeDPrinterMCPServer {
1106
1500
  },
1107
1501
  {
1108
1502
  name: "fulu_bambu_network_rpc",
1109
- description: "Advanced FULU bridge RPC for BambuNetwork diagnostics and development. Read-only methods are allowed by default; mutating methods require allow_mutating_method=true, and print methods require bambu_model.",
1503
+ description: "Advanced FULU bridge RPC for BambuNetwork diagnostics and development. Read-only methods are allowed by default. Agent/session setup methods require allow_mutating_method=true. Raw print methods, printer messages, file transfers and unknown methods are refused because they would bypass the print safety gate; use print_3mf for checked printing.",
1110
1504
  inputSchema: {
1111
1505
  type: "object",
1112
1506
  properties: {
@@ -1131,12 +1525,12 @@ class ThreeDPrinterMCPServer {
1131
1525
  },
1132
1526
  allow_mutating_method: {
1133
1527
  type: "boolean",
1134
- description: "Required for methods outside the read-only bridge/net allowlist."
1528
+ description: "Required for the allowlisted agent/session setup methods. It never enables print, printer-message, or unknown methods."
1135
1529
  },
1136
1530
  bambu_model: {
1137
1531
  type: "string",
1138
1532
  enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
1139
- description: "Required when calling FULU print RPC methods; preserves the Bambu model safety gate."
1533
+ description: "Informational only. Raw FULU print RPC methods are disabled; use print_3mf."
1140
1534
  }
1141
1535
  },
1142
1536
  required: ["method"]
@@ -1190,11 +1584,13 @@ class ThreeDPrinterMCPServer {
1190
1584
  },
1191
1585
  {
1192
1586
  name: "blender_mcp_status",
1193
- description: "Inspect Blender MCP configuration or connect and discover the remote server's tools and schemas. Connecting does not edit the scene; use get_scene_info through blender_mcp_call to check the Blender addon.",
1587
+ description: "Inspect Blender MCP configuration or connect and discover the remote server's tools. Lists tool names and summaries; pass tool_names for the full input schemas of the tools you will call. Connecting does not edit the scene; use get_scene_info through blender_mcp_call to check the Blender addon.",
1194
1588
  inputSchema: {
1195
1589
  type: "object",
1196
1590
  properties: {
1197
1591
  connect: { type: "boolean", description: "Initialize the configured stdio MCP server and discover its tools (default false)." },
1592
+ tool_names: { type: "array", items: { type: "string" }, maxItems: 64, description: "Return full input schemas for these discovered tools, such as [\"execute_blender_code\"]." },
1593
+ include_schemas: { type: "boolean", description: "Return every discovered tool's full definition (large; default false)." },
1198
1594
  timeout_ms: { type: "integer", minimum: 100, maximum: 300000, description: "Total connection and discovery deadline in milliseconds; defaults to BLENDER_MCP_TIMEOUT_MS or 120000." }
1199
1595
  },
1200
1596
  additionalProperties: false
@@ -1214,6 +1610,23 @@ class ThreeDPrinterMCPServer {
1214
1610
  additionalProperties: false
1215
1611
  }
1216
1612
  },
1613
+ {
1614
+ name: "blender_mcp_export_stl",
1615
+ description: "Export named objects from the live Blender scene to a new, verified STL for slicing. Writes world-space geometry with modifiers applied, without changing the scene, selection, or mode, and reports triangle count and bounding-box dimensions from the written file. Use this after editing or modelling through blender_mcp_call; Blender MCP's own export_scene writes GLB/FBX only. Requires standard Blender MCP and a shared local filesystem.",
1616
+ inputSchema: {
1617
+ type: "object",
1618
+ properties: {
1619
+ object_names: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 64, description: "Blender object names to export together as one STL (mesh, curve, surface, metaball, or text objects)." },
1620
+ output_path: { type: "string", description: "New local .stl path. Its parent must exist; existing files are never overwritten." },
1621
+ apply_modifiers: { type: "boolean", description: "Export the evaluated geometry with modifiers applied (default true)." },
1622
+ scale: { type: "number", exclusiveMinimum: 0, description: "Multiply coordinates before writing (default 1). Slicers read STL units as millimetres, so use 1000 for a scene modelled in metres." },
1623
+ user_prompt: { type: "string", description: "The user's own words, passed unchanged to Blender MCP." },
1624
+ timeout_ms: { type: "integer", minimum: 100, maximum: 300000, description: "Total Blender request deadline in milliseconds; defaults to BLENDER_MCP_TIMEOUT_MS or 120000." }
1625
+ },
1626
+ required: ["object_names", "output_path"],
1627
+ additionalProperties: false
1628
+ }
1629
+ },
1217
1630
  {
1218
1631
  name: "blender_mcp_edit_model",
1219
1632
  description: "Import, edit, and export a local STL through standard Blender MCP with verified output and existing scene objects preserved. Requires a shared local filesystem and Blender Object Mode. Also supports a separately configured legacy executable bridge.",
@@ -1276,21 +1689,23 @@ class ThreeDPrinterMCPServer {
1276
1689
  if (args.print !== undefined && typeof args.print !== "boolean") {
1277
1690
  throw new Error("print must be a boolean.");
1278
1691
  }
1692
+ const uploadOptions = { material: optionalMaterialArg(args?.material) };
1279
1693
  if (type.toLowerCase() === "bambu" && args.print === true) {
1280
- await this.resolveBambuModel(args?.bambu_model);
1694
+ uploadOptions.bambuModel = await this.resolveBambuModel(args?.bambu_model);
1281
1695
  }
1282
1696
  const uploadFilename = String(args?.filename || path.basename(String(args.gcode_path)));
1283
1697
  const uploadGcodeSource = String(args?.gcode_path || args.gcode);
1284
- result = await this.uploadGcode(host, port, type, apiKey, bambuSerial, bambuToken, uploadFilename, uploadGcodeSource, Boolean(args.print || false));
1698
+ result = await this.uploadGcode(host, port, type, apiKey, bambuSerial, bambuToken, uploadFilename, uploadGcodeSource, Boolean(args.print || false), uploadOptions);
1285
1699
  break;
1286
1700
  case "start_print":
1287
1701
  if (!args?.filename) {
1288
1702
  throw new Error("Missing required parameter: filename");
1289
1703
  }
1704
+ const startOptions = { material: optionalMaterialArg(args?.material) };
1290
1705
  if (type.toLowerCase() === "bambu") {
1291
- await this.resolveBambuModel(args?.bambu_model);
1706
+ startOptions.bambuModel = await this.resolveBambuModel(args?.bambu_model);
1292
1707
  }
1293
- result = await this.startPrint(host, port, type, apiKey, bambuSerial, bambuToken, String(args.filename));
1708
+ result = await this.startPrint(host, port, type, apiKey, bambuSerial, bambuToken, String(args.filename), startOptions);
1294
1709
  break;
1295
1710
  case "cancel_print":
1296
1711
  result = await this.cancelPrint(host, port, type, apiKey, bambuSerial, bambuToken);
@@ -1299,7 +1714,15 @@ class ThreeDPrinterMCPServer {
1299
1714
  if (!args?.component || args?.temperature === undefined) {
1300
1715
  throw new Error("Missing required parameters: component and temperature");
1301
1716
  }
1302
- result = await this.setPrinterTemperature(host, port, type, apiKey, bambuSerial, bambuToken, String(args.component), Number(args.temperature));
1717
+ // Never coerce: "not-a-number" must not become NaN in a heater command.
1718
+ const requestedTemperature = optionalTemperatureArg(args.temperature, "temperature");
1719
+ const heatOptions = { material: optionalMaterialArg(args.material) };
1720
+ if (type.toLowerCase() === "bambu" && requestedTemperature > 0) {
1721
+ heatOptions.bambuModel = await this.resolveBambuModel(args?.bambu_model);
1722
+ heatOptions.nozzleDiameter =
1723
+ optionalNozzleDiameterArg(args.nozzle_diameter) ?? Number(DEFAULT_NOZZLE_DIAMETER);
1724
+ }
1725
+ result = await this.setPrinterTemperature(host, port, type, apiKey, bambuSerial, bambuToken, String(args.component), requestedTemperature, heatOptions);
1303
1726
  break;
1304
1727
  // New STL manipulation tools
1305
1728
  case "extend_stl_base":
@@ -1308,19 +1731,32 @@ class ThreeDPrinterMCPServer {
1308
1731
  }
1309
1732
  result = await this.stlManipulator.extendBase(String(args.stl_path), Number(args.extension_inches));
1310
1733
  break;
1311
- case "slice_stl": {
1312
- if (!args?.stl_path) {
1313
- throw new Error("Missing required parameter: stl_path");
1734
+ case "slice_stl":
1735
+ result = await this.runSliceTool("slice_stl", args);
1736
+ break;
1737
+ case "slice_with_template":
1738
+ if (!args?.template_name || !String(args.template_name).trim()) {
1739
+ throw new Error("Missing required parameter: template_name");
1314
1740
  }
1315
- // Resolve printer model for BambuStudio slicer
1316
- let slicePreset;
1317
- if (isBambuProjectSlicer(slicerType)) {
1318
- const sliceModel = await this.resolveBambuModel(args?.bambu_model);
1319
- const nozzleDiam = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
1320
- slicePreset = BAMBU_MODEL_PRESETS[sliceModel]?.(nozzleDiam);
1741
+ result = await this.runSliceTool("slice_with_template", args);
1742
+ break;
1743
+ case "list_templates":
1744
+ result = listTemplateRegistry(typeof args?.template_dir === "string" ? args.template_dir : undefined);
1745
+ break;
1746
+ case "save_template":
1747
+ if (!args?.source_path) {
1748
+ throw new Error("Missing required parameter: source_path");
1321
1749
  }
1322
- result = await this.stlManipulator.sliceSTL(String(args.stl_path), slicerType, this.resolveExecutableSelectorArg(args?.slicer_path, "slice_stl", "slicer_path") ?? DEFAULT_SLICER_PATH, slicerProfile || undefined, undefined, // progressCallback
1323
- slicePreset, filamentProfile || undefined);
1750
+ result = saveTemplate(String(args.source_path), typeof args?.template_name === "string" ? args.template_name : undefined, typeof args?.template_dir === "string" ? args.template_dir : undefined);
1751
+ break;
1752
+ case "get_slice_settings": {
1753
+ if (!args?.source_path && !args?.template_name) {
1754
+ throw new Error("Missing required parameter: source_path or template_name");
1755
+ }
1756
+ const settingsSource = args?.source_path
1757
+ ? String(args.source_path)
1758
+ : resolveTemplatePath(String(args.template_name), typeof args?.template_dir === "string" ? args.template_dir : undefined, ["3mf", "json", "config"]);
1759
+ result = await inspectSliceSettings(settingsSource, TEMP_DIR);
1324
1760
  break;
1325
1761
  }
1326
1762
  case "confirm_temperatures":
@@ -1336,22 +1772,38 @@ class ThreeDPrinterMCPServer {
1336
1772
  if (!args?.stl_path || args?.extension_inches === undefined) {
1337
1773
  throw new Error("Missing required parameters: stl_path and extension_inches");
1338
1774
  }
1775
+ // Validate every safety input before extending, slicing or connecting.
1776
+ const expectedPeaks = {
1777
+ nozzle: optionalTemperatureArg(args.extruder_temp, "extruder_temp"),
1778
+ bed: optionalTemperatureArg(args.bed_temp, "bed_temp"),
1779
+ };
1780
+ validateExpectedPeaks(expectedPeaks);
1781
+ const processMaterial = optionalMaterialArg(args?.material);
1782
+ let processModel;
1783
+ let processNozzle = String(DEFAULT_NOZZLE_DIAMETER);
1784
+ if (type.toLowerCase() === 'bambu' || isBambuProjectSlicer(slicerType)) {
1785
+ processModel = await this.resolveBambuModel(args?.bambu_model);
1786
+ processNozzle = String(optionalNozzleDiameterArg(args?.nozzle_diameter) ?? DEFAULT_NOZZLE_DIAMETER);
1787
+ }
1339
1788
  // Define progress callback for UI updates
1340
1789
  const processProgressCallback = (progress, message) => {
1341
- console.log(`Process progress: ${progress}% - ${message || ''}`);
1790
+ console.error(`Process progress: ${progress}% - ${message || ''}`);
1342
1791
  };
1343
1792
  // 1. Extend the base of the STL file
1344
1793
  const extendedStlPath = await this.stlManipulator.extendBase(String(args.stl_path), Number(args.extension_inches), processProgressCallback);
1345
1794
  // 2. Slice the extended STL file
1346
1795
  let processPreset;
1347
- if (type.toLowerCase() === 'bambu' || isBambuProjectSlicer(slicerType)) {
1348
- const processModel = await this.resolveBambuModel(args?.bambu_model);
1349
- const processNozzle = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
1350
- if (isBambuProjectSlicer(slicerType)) {
1351
- processPreset = BAMBU_MODEL_PRESETS[processModel]?.(processNozzle);
1796
+ if (processModel && isBambuProjectSlicer(slicerType)) {
1797
+ processPreset = BAMBU_MODEL_PRESETS[processModel]?.(processNozzle);
1798
+ }
1799
+ const gcodePath = await this.stlManipulator.sliceSTL(extendedStlPath, slicerType, this.resolveExecutableSelectorArg(args?.slicer_path, "process_and_print_stl", "slicer_path") ?? DEFAULT_SLICER_PATH, slicerProfile || undefined, processProgressCallback, processPreset, filamentProfile || undefined,
1800
+ // Slice for the plate and nozzle the print gate will check against.
1801
+ processPreset
1802
+ ? {
1803
+ bedType: resolveBedType(args?.bed_type),
1804
+ ...(resolveNozzleType(args?.nozzle_type) ? { nozzleType: resolveNozzleType(args?.nozzle_type) } : {}),
1352
1805
  }
1353
- }
1354
- const gcodePath = await this.stlManipulator.sliceSTL(extendedStlPath, slicerType, this.resolveExecutableSelectorArg(args?.slicer_path, "process_and_print_stl", "slicer_path") ?? DEFAULT_SLICER_PATH, slicerProfile || undefined, processProgressCallback, processPreset, filamentProfile || undefined);
1806
+ : undefined);
1355
1807
  if (type.toLowerCase() === 'bambu' && gcodePath.toLowerCase().endsWith(".3mf")) {
1356
1808
  if (!bambuSerial || !bambuToken) {
1357
1809
  throw new Error("Bambu serial number and access token are required for Bambu 3MF printing.");
@@ -1372,6 +1824,9 @@ class ThreeDPrinterMCPServer {
1372
1824
  result = await factoryImplementation.print3mf(host, bambuSerial, bambuToken, {
1373
1825
  projectName: path.basename(gcodePath).replace(/\.3mf$/i, ''),
1374
1826
  filePath: gcodePath,
1827
+ bambuModel: processModel,
1828
+ nozzleDiameters: [Number(processNozzle)],
1829
+ expectedPeaks,
1375
1830
  plateIndex: 0,
1376
1831
  ...amsOptions,
1377
1832
  bedType: printBedType,
@@ -1388,26 +1843,20 @@ class ThreeDPrinterMCPServer {
1388
1843
  if (gcodePath.toLowerCase().endsWith(".3mf")) {
1389
1844
  throw new Error("process_and_print_stl produced a sliced 3MF. Direct process-and-print for sliced 3MF is only supported for Bambu printers via print_3mf.");
1390
1845
  }
1391
- // 3. Confirm temperatures if specified
1392
- if (args.extruder_temp !== undefined || args.bed_temp !== undefined) {
1393
- const tempConfirmation = await this.stlManipulator.confirmTemperatures(gcodePath, {
1394
- extruder: args.extruder_temp !== undefined ? Number(args.extruder_temp) : undefined,
1395
- bed: args.bed_temp !== undefined ? Number(args.bed_temp) : undefined
1396
- }, processProgressCallback);
1397
- if (!tempConfirmation.match) {
1398
- console.warn("Temperature mismatch:", tempConfirmation);
1399
- }
1400
- }
1401
- // 4. Upload the G-code file to the printer
1402
- const gcodeContent = await fs.promises.readFile(gcodePath, 'utf8');
1846
+ // 3. Upload and print through the adapter's hard safety gate. It
1847
+ // inspects the exact bytes sent (S and R targets, tool changes,
1848
+ // hardware and material ceilings), requires the expected peaks to
1849
+ // match, reads fresh printer state and asks for human confirmation.
1850
+ // A mismatch refuses before upload; nothing is only logged.
1403
1851
  const filename = path.basename(gcodePath);
1404
- await this.uploadGcode(host, port, type, apiKey, bambuSerial, bambuToken, filename, gcodeContent, true // Start printing immediately
1405
- );
1852
+ const printResult = await this.uploadGcode(host, port, type, apiKey, bambuSerial, bambuToken, filename, gcodePath, true, // Start printing immediately
1853
+ { bambuModel: processModel, material: processMaterial, expectedPeaks });
1406
1854
  result = {
1407
1855
  extended_stl_path: extendedStlPath,
1408
1856
  gcode_path: gcodePath,
1409
1857
  filename,
1410
- status: "Print job started"
1858
+ status: "Checked print job started",
1859
+ print: printResult,
1411
1860
  };
1412
1861
  break;
1413
1862
  // New STL manipulation tool handlers
@@ -1423,7 +1872,7 @@ class ThreeDPrinterMCPServer {
1423
1872
  }
1424
1873
  // Define progress callback for UI updates
1425
1874
  const scaleProgressCallback = (progress, message) => {
1426
- console.log(`Scale progress: ${progress}% - ${message || ''}`);
1875
+ console.error(`Scale progress: ${progress}% - ${message || ''}`);
1427
1876
  };
1428
1877
  let scaleFactors;
1429
1878
  // Check if we have individual axis scaling factors
@@ -1447,7 +1896,7 @@ class ThreeDPrinterMCPServer {
1447
1896
  }
1448
1897
  // Define progress callback for UI updates
1449
1898
  const rotateProgressCallback = (progress, message) => {
1450
- console.log(`Rotate progress: ${progress}% - ${message || ''}`);
1899
+ console.error(`Rotate progress: ${progress}% - ${message || ''}`);
1451
1900
  };
1452
1901
  // Get rotation angles, defaulting to 0 for any undefined axis
1453
1902
  const rotationAngles = [
@@ -1463,7 +1912,7 @@ class ThreeDPrinterMCPServer {
1463
1912
  }
1464
1913
  // Define progress callback for UI updates
1465
1914
  const translateProgressCallback = (progress, message) => {
1466
- console.log(`Translate progress: ${progress}% - ${message || ''}`);
1915
+ console.error(`Translate progress: ${progress}% - ${message || ''}`);
1467
1916
  };
1468
1917
  // Get translation values, defaulting to 0 for any undefined axis
1469
1918
  const translationValues = [
@@ -1479,7 +1928,7 @@ class ThreeDPrinterMCPServer {
1479
1928
  }
1480
1929
  // Define progress callback for UI updates
1481
1930
  const modifySectionProgressCallback = (progress, message) => {
1482
- console.log(`Modify section progress: ${progress}% - ${message || ''}`);
1931
+ console.error(`Modify section progress: ${progress}% - ${message || ''}`);
1483
1932
  };
1484
1933
  // Determine the section to modify
1485
1934
  let sectionBox;
@@ -1536,7 +1985,7 @@ class ThreeDPrinterMCPServer {
1536
1985
  }
1537
1986
  // Define progress callback for UI updates
1538
1987
  const visualizationProgressCallback = (progress, message) => {
1539
- console.log(`Visualization progress: ${progress}% - ${message || ''}`);
1988
+ console.error(`Visualization progress: ${progress}% - ${message || ''}`);
1540
1989
  };
1541
1990
  // Get width and height parameters, with defaults
1542
1991
  const width = args.width !== undefined ? Number(args.width) : 300;
@@ -1555,33 +2004,51 @@ class ThreeDPrinterMCPServer {
1555
2004
  }
1556
2005
  const printModel = await this.resolveBambuModel(args?.bambu_model);
1557
2006
  const printBedType = resolveBedType(args?.bed_type);
1558
- const printNozzle = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
2007
+ const explicitPrintNozzle = optionalNozzleDiameterArg(args?.nozzle_diameter);
2008
+ const printNozzle = String(explicitPrintNozzle ?? DEFAULT_NOZZLE_DIAMETER);
1559
2009
  const printPreset = BAMBU_MODEL_PRESETS[printModel]?.(printNozzle);
1560
2010
  let threeMFPath = String(args.three_mf_path);
2011
+ // Only the selected plate's exact G-code entry counts as sliced output;
2012
+ // a stray .gcode entry elsewhere in the archive is not printable.
2013
+ const selectedPlateEntry = "Metadata/plate_1.gcode";
1561
2014
  let shouldAutoSlice = false;
1562
2015
  try {
1563
2016
  const JSZip = (await import('jszip')).default;
1564
2017
  const zipData = fs.readFileSync(threeMFPath);
1565
2018
  const zip = await JSZip.loadAsync(zipData);
1566
- shouldAutoSlice = !Object.keys(zip.files).some(f => f.match(/Metadata\/plate_\d+\.gcode/i) || f.endsWith('.gcode'));
2019
+ const plateEntry = zip.file(selectedPlateEntry);
2020
+ shouldAutoSlice = !plateEntry || plateEntry.dir;
1567
2021
  }
1568
2022
  catch (sliceCheckErr) {
1569
- console.warn("Could not check 3MF for embedded gcode, proceeding with original:", sliceCheckErr.message);
2023
+ throw new Error(`Cannot read 3MF archive ${threeMFPath}; nothing was uploaded or started: ${sliceCheckErr?.message ?? String(sliceCheckErr)}`);
1570
2024
  }
1571
2025
  // Only treat slicer_path as an executable selector when a slicer
1572
2026
  // will actually launch. Ready-to-print archives ignore it.
1573
2027
  if (shouldAutoSlice) {
1574
2028
  const printSlicerPath = this.resolveExecutableSelectorArg(args?.slicer_path, "print_3mf", "slicer_path") ?? DEFAULT_SLICER_PATH;
1575
2029
  try {
1576
- console.log(`3MF has no gcode — auto-slicing with ${slicerType} for ${printModel}`);
2030
+ console.error(`3MF has no gcode — auto-slicing with ${slicerType} for ${printModel}`);
1577
2031
  threeMFPath = await this.stlManipulator.sliceSTL(threeMFPath, slicerType, printSlicerPath, slicerProfile || undefined, undefined, // progressCallback
1578
- printPreset, filamentProfile || undefined);
1579
- console.log("Auto-sliced to: " + threeMFPath);
2032
+ printPreset, filamentProfile || undefined,
2033
+ // Slice for the plate and nozzle the print will be checked against.
2034
+ { bedType: printBedType, ...(resolveNozzleType(args?.nozzle_type) ? { nozzleType: resolveNozzleType(args?.nozzle_type) } : {}) });
2035
+ console.error("Auto-sliced to: " + threeMFPath);
1580
2036
  }
1581
2037
  catch (sliceErr) {
1582
- console.warn("Could not auto-slice 3MF, proceeding with original:", sliceErr.message);
2038
+ // Never fall back to uploading the original unsliced project.
2039
+ // Rethrow the original instance (for example a SlicerError) so
2040
+ // structured slicer suggestions survive.
2041
+ const prefix = "Auto-slicing failed; nothing was uploaded or started: ";
2042
+ if (sliceErr instanceof Error) {
2043
+ if (!sliceErr.message.startsWith(prefix))
2044
+ sliceErr.message = `${prefix}${sliceErr.message}`;
2045
+ throw sliceErr;
2046
+ }
2047
+ throw new Error(`${prefix}${String(sliceErr)}`);
1583
2048
  }
1584
2049
  }
2050
+ // Compare the sliced file with an explicit or auto-slice nozzle request.
2051
+ const requestedPrintNozzles = explicitPrintNozzle !== undefined || shouldAutoSlice ? [Number(printNozzle)] : undefined;
1585
2052
  // Define variables needed outside the parse try block
1586
2053
  let implementation;
1587
2054
  let threeMfFilename;
@@ -1590,7 +2057,7 @@ class ThreeDPrinterMCPServer {
1590
2057
  try {
1591
2058
  // --- Parse 3MF ---
1592
2059
  const parsed3MFData = await parse3MF(threeMFPath);
1593
- console.log(`Successfully parsed 3MF file: ${threeMFPath}`);
2060
+ console.error(`Successfully parsed 3MF file: ${threeMFPath}`);
1594
2061
  const amsOptions = this.resolveAmsPrintOptions(parsed3MFData, args);
1595
2062
  // --- Prepare Implementation and Print Options ---
1596
2063
  const factoryImplementation = this.printerFactory.getImplementation('bambu');
@@ -1602,6 +2069,8 @@ class ThreeDPrinterMCPServer {
1602
2069
  projectName = threeMfFilename.replace(/\.3mf$/i, ''); // Assign to outer scope variable
1603
2070
  printOptions = {
1604
2071
  ...amsOptions,
2072
+ bambuModel: printModel,
2073
+ nozzleDiameters: requestedPrintNozzles,
1605
2074
  bedType: printBedType,
1606
2075
  bedLeveling: args?.bed_leveling !== undefined ? Boolean(args.bed_leveling) : undefined,
1607
2076
  flowCalibration: args?.flow_calibration !== undefined ? Boolean(args.flow_calibration) : undefined,
@@ -1623,7 +2092,7 @@ class ThreeDPrinterMCPServer {
1623
2092
  plateIndex: 0,
1624
2093
  ...printOptions // Spread the final options
1625
2094
  });
1626
- result = `Print command for ${threeMfFilename} sent successfully.`;
2095
+ // Report what the printer did, not merely that a command was published.
1627
2096
  }
1628
2097
  catch (printError) {
1629
2098
  console.error(`Error starting 3MF print for ${threeMfFilename}:`, printError);
@@ -1648,11 +2117,10 @@ class ThreeDPrinterMCPServer {
1648
2117
  if (!args?.method) {
1649
2118
  throw new Error("Missing required parameter: method");
1650
2119
  }
2120
+ // Raw print methods, printer messages and unknown methods are refused
2121
+ // inside invokeFuluBridgeRpc before any bridge process starts.
1651
2122
  const method = String(args.method);
1652
- let rpcBambuModel;
1653
- if (isFuluPrintRpcMethod(method)) {
1654
- rpcBambuModel = await this.resolveBambuModel(args?.bambu_model);
1655
- }
2123
+ const rpcBambuModel = typeof args?.bambu_model === "string" && args.bambu_model.trim() ? args.bambu_model.trim() : undefined;
1656
2124
  const payload = typeof args.payload === "object" && args.payload !== null && !Array.isArray(args.payload)
1657
2125
  ? args.payload
1658
2126
  : undefined;
@@ -1690,6 +2158,9 @@ class ThreeDPrinterMCPServer {
1690
2158
  break;
1691
2159
  case "blender_mcp_call":
1692
2160
  return await this.blender.call(args ?? {}, extra.signal);
2161
+ case "blender_mcp_export_stl":
2162
+ result = await this.blender.exportStl(args ?? {}, extra.signal);
2163
+ break;
1693
2164
  case "blender_mcp_edit_model":
1694
2165
  result = await this.blender.edit(args ?? {}, this.resolveExecutableSelectorArg(args?.bridge_command, "blender_mcp_edit_model", "bridge_command", true, true) ?? (!process.env.BLENDER_MCP_COMMAND?.trim() ? this.runtimeConfig.blenderBridgeCommand : undefined), extra.signal);
1695
2166
  break;
@@ -1708,7 +2179,7 @@ class ThreeDPrinterMCPServer {
1708
2179
  catch (error) {
1709
2180
  console.error(`Error calling tool ${name}:`, error);
1710
2181
  const errorMessage = error instanceof Error ? error.message : String(error);
1711
- const structuredError = this.toStructuredToolError(name, errorMessage);
2182
+ const structuredError = this.toStructuredToolError(name, errorMessage, error);
1712
2183
  return {
1713
2184
  content: [
1714
2185
  {
@@ -1747,7 +2218,7 @@ class ThreeDPrinterMCPServer {
1747
2218
  }
1748
2219
  return implementation.getFile(host, port, apiKey, filename);
1749
2220
  }
1750
- async uploadGcode(host, port, type, apiKey, bambuSerial, bambuToken, filename, gcode, print) {
2221
+ async uploadGcode(host, port, type, apiKey, bambuSerial, bambuToken, filename, gcode, print, options = {}) {
1751
2222
  const sourceIsFilePath = fs.existsSync(gcode) && fs.statSync(gcode).isFile();
1752
2223
  // The remote filename must never select a local write/delete target.
1753
2224
  const localFilename = path.basename(filename);
@@ -1763,22 +2234,22 @@ class ThreeDPrinterMCPServer {
1763
2234
  const implementation = this.printerFactory.getImplementation(type);
1764
2235
  if (type.toLowerCase() === "bambu") {
1765
2236
  const bambuApiKey = `${bambuSerial}:${bambuToken}`;
1766
- return await implementation.uploadFile(host, port, bambuApiKey, tempFilePath, filename, print);
2237
+ return await implementation.uploadFile(host, port, bambuApiKey, tempFilePath, filename, print, options);
1767
2238
  }
1768
- return await implementation.uploadFile(host, port, apiKey, tempFilePath, filename, print);
2239
+ return await implementation.uploadFile(host, port, apiKey, tempFilePath, filename, print, options);
1769
2240
  }
1770
2241
  finally {
1771
2242
  if (scratchDir)
1772
2243
  fs.rmSync(scratchDir, { recursive: true, force: true });
1773
2244
  }
1774
2245
  }
1775
- async startPrint(host, port, type, apiKey, bambuSerial, bambuToken, gcodeFilename) {
2246
+ async startPrint(host, port, type, apiKey, bambuSerial, bambuToken, gcodeFilename, options = {}) {
1776
2247
  const implementation = this.printerFactory.getImplementation(type);
1777
2248
  if (type.toLowerCase() === "bambu") {
1778
2249
  const bambuApiKey = `${bambuSerial}:${bambuToken}`;
1779
- return await implementation.startJob(host, port, bambuApiKey, gcodeFilename);
2250
+ return await implementation.startJob(host, port, bambuApiKey, gcodeFilename, options);
1780
2251
  }
1781
- return await implementation.startJob(host, port, apiKey, gcodeFilename);
2252
+ return await implementation.startJob(host, port, apiKey, gcodeFilename, options);
1782
2253
  }
1783
2254
  async cancelPrint(host, port, type, apiKey, bambuSerial, bambuToken) {
1784
2255
  const implementation = this.printerFactory.getImplementation(type);
@@ -1788,15 +2259,48 @@ class ThreeDPrinterMCPServer {
1788
2259
  }
1789
2260
  return await implementation.cancelJob(host, port, apiKey);
1790
2261
  }
1791
- async setPrinterTemperature(host, port, type, apiKey, bambuSerial, bambuToken, component, temperature) {
2262
+ async setPrinterTemperature(host, port, type, apiKey, bambuSerial, bambuToken, component, temperature, options = {}) {
1792
2263
  const implementation = this.printerFactory.getImplementation(type);
1793
2264
  if (type.toLowerCase() === "bambu") {
1794
2265
  const bambuApiKey = `${bambuSerial}:${bambuToken}`;
1795
- return implementation.setTemperature(host, port, bambuApiKey, component, temperature);
2266
+ return implementation.setTemperature(host, port, bambuApiKey, component, temperature, options);
1796
2267
  }
1797
- return implementation.setTemperature(host, port, apiKey, component, temperature);
2268
+ return implementation.setTemperature(host, port, apiKey, component, temperature, options);
1798
2269
  }
1799
- toStructuredToolError(tool, message) {
2270
+ toStructuredToolError(tool, message, error) {
2271
+ // Slicer failures are local profile/slicer problems, never printer connectivity.
2272
+ if (error instanceof SlicerError) {
2273
+ return {
2274
+ status: "error",
2275
+ retryable: false,
2276
+ suggestion: error.suggestion,
2277
+ message,
2278
+ tool,
2279
+ slicer: { phase: error.phase, ...error.details },
2280
+ };
2281
+ }
2282
+ if (SLICING_TOOLS.has(tool)) {
2283
+ return {
2284
+ status: "error",
2285
+ retryable: false,
2286
+ suggestion: "Check the slicing arguments, template, and profile paths named in the error, then retry.",
2287
+ message,
2288
+ tool,
2289
+ };
2290
+ }
2291
+ if (isCredentialRejection(message)) {
2292
+ return { status: "error", retryable: false, suggestion: credentialSuggestion(), message, tool };
2293
+ }
2294
+ // Safety refusals stop before the printer is touched; retrying unchanged repeats the refusal.
2295
+ if (/nothing was (?:sent|uploaded)|no command was sent|print safety|hardware limit|material policy|declared material|confirmation was declined|are refused|is refused|refused because|refused until|refused before|does not match the job|is ambiguous|is missing or ambiguous|rejected the print command/i.test(message)) {
2296
+ return {
2297
+ status: "error",
2298
+ retryable: false,
2299
+ suggestion: "A safety check stopped this before anything reached the printer. Read the reason, fix the file, arguments, or printer state, and ask again.",
2300
+ message,
2301
+ tool,
2302
+ };
2303
+ }
1800
2304
  const isInputError = message.toLowerCase().includes("missing required") ||
1801
2305
  message.toLowerCase().includes("invalid") ||
1802
2306
  message.toLowerCase().includes("unsupported");