mcp-3d-printer-server 1.2.9 → 1.2.12
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/CHANGELOG.md +222 -0
- package/CONTRIBUTORS.md +56 -0
- package/README.md +528 -811
- package/dist/3mf_parser.js +8 -8
- package/dist/blender-mcp-bridge.d.ts +1 -0
- package/dist/blender-mcp-bridge.js +209 -21
- package/dist/fulu-orca.js +29 -5
- package/dist/index.js +614 -110
- package/dist/printers/bambu-node-guard.d.ts +2 -0
- package/dist/printers/bambu-node-guard.js +38 -0
- package/dist/printers/bambu.d.ts +53 -7
- package/dist/printers/bambu.js +420 -126
- package/dist/printers/creality.d.ts +15 -6
- package/dist/printers/creality.js +42 -9
- package/dist/printers/duet.d.ts +11 -6
- package/dist/printers/duet.js +57 -34
- package/dist/printers/generic-base.d.ts +69 -0
- package/dist/printers/generic-base.js +209 -0
- package/dist/printers/klipper.d.ts +17 -6
- package/dist/printers/klipper.js +63 -16
- package/dist/printers/octoprint.d.ts +18 -6
- package/dist/printers/octoprint.js +64 -9
- package/dist/printers/printer-factory.d.ts +11 -2
- package/dist/printers/printer-factory.js +9 -8
- package/dist/printers/prusa.d.ts +14 -6
- package/dist/printers/prusa.js +56 -9
- package/dist/printers/repetier.d.ts +18 -6
- package/dist/printers/repetier.js +48 -6
- package/dist/safety/archive.d.ts +11 -0
- package/dist/safety/archive.js +214 -0
- package/dist/safety/artifact.d.ts +6 -0
- package/dist/safety/artifact.js +66 -0
- package/dist/safety/confirmation.d.ts +22 -0
- package/dist/safety/confirmation.js +43 -0
- package/dist/safety/expected-peaks.d.ts +24 -0
- package/dist/safety/expected-peaks.js +24 -0
- package/dist/safety/gcode-thermal.d.ts +64 -0
- package/dist/safety/gcode-thermal.js +382 -0
- package/dist/safety/limits.d.ts +31 -0
- package/dist/safety/limits.js +105 -0
- package/dist/safety/print-file.d.ts +25 -0
- package/dist/safety/print-file.js +610 -0
- package/dist/safety/printer-state.d.ts +51 -0
- package/dist/safety/printer-state.js +371 -0
- package/dist/slicer/bambu-cli.d.ts +110 -0
- package/dist/slicer/bambu-cli.js +547 -0
- package/dist/slicer/profile-flatten.d.ts +104 -0
- package/dist/slicer/profile-flatten.js +676 -0
- package/dist/slicer/run-slicer.d.ts +18 -0
- package/dist/slicer/run-slicer.js +102 -0
- package/dist/slicer/slicer-error.d.ts +27 -0
- package/dist/slicer/slicer-error.js +53 -0
- package/dist/slicer/templates.d.ts +126 -0
- package/dist/slicer/templates.js +255 -0
- package/dist/stl/stl-manipulator.d.ts +25 -3
- package/dist/stl/stl-manipulator.js +214 -232
- package/package.json +16 -9
- package/server.json +56 -0
- package/src/3mf_parser.ts +8 -8
- package/src/blender-mcp-bridge.ts +200 -19
- package/src/fulu-orca.ts +36 -6
- package/src/index.ts +686 -137
- package/src/printers/bambu-node-guard.ts +44 -0
- package/src/printers/bambu.ts +458 -164
- package/src/printers/creality.ts +40 -9
- package/src/printers/duet.ts +64 -53
- package/src/printers/generic-base.ts +286 -0
- package/src/printers/klipper.ts +83 -28
- package/src/printers/octoprint.ts +74 -21
- package/src/printers/printer-factory.ts +21 -11
- package/src/printers/prusa.ts +53 -10
- package/src/printers/repetier.ts +55 -17
- package/src/safety/archive.ts +181 -0
- package/src/safety/artifact.ts +58 -0
- package/src/safety/confirmation.ts +56 -0
- package/src/safety/expected-peaks.ts +47 -0
- package/src/safety/gcode-thermal.ts +404 -0
- package/src/safety/limits.ts +98 -0
- package/src/safety/print-file.ts +429 -0
- package/src/safety/printer-state.ts +344 -0
- package/src/slicer/bambu-cli.ts +633 -0
- package/src/slicer/profile-flatten.ts +830 -0
- package/src/slicer/run-slicer.ts +126 -0
- package/src/slicer/slicer-error.ts +72 -0
- package/src/slicer/templates.ts +288 -0
- package/src/stl/stl-manipulator.ts +247 -275
package/src/index.ts
CHANGED
|
@@ -21,14 +21,23 @@ import { createServer as createHttpServer, type IncomingMessage, type Server as
|
|
|
21
21
|
import { randomUUID } from "node:crypto";
|
|
22
22
|
import * as THREE from 'three';
|
|
23
23
|
import { PrinterFactory } from "./printers/printer-factory.js";
|
|
24
|
-
import { STLManipulator } from "./stl/stl-manipulator.js";
|
|
24
|
+
import { STLManipulator, type BambuSliceOptions } from "./stl/stl-manipulator.js";
|
|
25
|
+
import { SlicerError } from "./slicer/slicer-error.js";
|
|
26
|
+
import {
|
|
27
|
+
inspectSliceSettings,
|
|
28
|
+
listTemplateRegistry,
|
|
29
|
+
resolveTemplatePath,
|
|
30
|
+
resolveTemplateSlicerProfilePath,
|
|
31
|
+
saveTemplate,
|
|
32
|
+
} from "./slicer/templates.js";
|
|
25
33
|
import { parse3MF, ThreeMFData } from './3mf_parser.js';
|
|
26
34
|
import { BambuImplementation } from "./printers/bambu.js";
|
|
27
35
|
import {
|
|
28
36
|
inspectFuluOrcaSetup,
|
|
29
37
|
invokeFuluBridgeRpc,
|
|
30
|
-
isFuluPrintRpcMethod,
|
|
31
38
|
} from "./fulu-orca.js";
|
|
39
|
+
import { CONFIRMATION_UNAVAILABLE, confirmationTimedOut, confirmationTimeoutMs } from "./safety/confirmation.js";
|
|
40
|
+
import { validateExpectedPeaks, type ExpectedPeakTemperatures, type PrintSafetyOptions } from "./safety/expected-peaks.js";
|
|
32
41
|
|
|
33
42
|
// Load environment variables from .env file
|
|
34
43
|
dotenv.config();
|
|
@@ -168,6 +177,211 @@ const BAMBU_MODEL_PRESETS: Record<string, (nozzle: string) => string> = {
|
|
|
168
177
|
h2d: (n) => `Bambu Lab H2D ${n} nozzle`,
|
|
169
178
|
};
|
|
170
179
|
|
|
180
|
+
// Models whose bundled machine presets the Bambu-compatible CLI slicing path
|
|
181
|
+
// can target. The installed slicer must still contain the exact
|
|
182
|
+
// "<model> <nozzle> nozzle" preset; a missing preset stops the slice.
|
|
183
|
+
const SLICE_BAMBU_MODELS = ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s", "h2c"] as const;
|
|
184
|
+
const SLICE_BAMBU_MODEL_PRESETS: Record<string, (nozzle: string) => string> = {
|
|
185
|
+
...BAMBU_MODEL_PRESETS,
|
|
186
|
+
p2s: (n) => `Bambu Lab P2S ${n} nozzle`,
|
|
187
|
+
h2s: (n) => `Bambu Lab H2S ${n} nozzle`,
|
|
188
|
+
h2c: (n) => `Bambu Lab H2C ${n} nozzle`,
|
|
189
|
+
};
|
|
190
|
+
const DEFAULT_TEMPLATE_3MF_PATH = process.env.BAMBU_TEMPLATE_3MF_PATH || "";
|
|
191
|
+
const VALID_NOZZLE_TYPES = ["stainless_steel", "hardened_steel", "tungsten_carbide", "brass"] as const;
|
|
192
|
+
|
|
193
|
+
/** The installed hotend material. Bambu machine presets assume the stock nozzle; the print gate compares this with the printer. */
|
|
194
|
+
function resolveNozzleType(value: unknown): string | undefined {
|
|
195
|
+
const raw = value === undefined || value === null || value === "" ? process.env.BAMBU_NOZZLE_TYPE?.trim() : value;
|
|
196
|
+
if (raw === undefined || raw === "") return undefined;
|
|
197
|
+
const normalized = String(raw).trim().toLowerCase().replace(/[\s-]+/g, "_");
|
|
198
|
+
if (!(VALID_NOZZLE_TYPES as readonly string[]).includes(normalized)) {
|
|
199
|
+
throw new Error(`Invalid nozzle type "${raw}". Valid types: ${VALID_NOZZLE_TYPES.join(", ")}`);
|
|
200
|
+
}
|
|
201
|
+
return normalized;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Shared slice_stl / slice_with_template argument schema.
|
|
205
|
+
const SLICE_TOOL_PROPERTIES = {
|
|
206
|
+
bambu_model: {
|
|
207
|
+
type: "string",
|
|
208
|
+
enum: [...SLICE_BAMBU_MODELS],
|
|
209
|
+
description:
|
|
210
|
+
"Bambu Lab printer model. Required for bambustudio and orcaslicer-bambulab (elicited or read from BAMBU_MODEL when omitted); " +
|
|
211
|
+
"passing it with orcaslicer selects the Bambu-compatible path. The installed slicer must contain the exact model/nozzle preset.",
|
|
212
|
+
},
|
|
213
|
+
nozzle_diameter: {
|
|
214
|
+
type: "string",
|
|
215
|
+
description: "Nozzle diameter in mm (default: NOZZLE_DIAMETER or 0.4). Selects the '<model> <diameter> nozzle' machine preset.",
|
|
216
|
+
},
|
|
217
|
+
slicer_type: {
|
|
218
|
+
type: "string",
|
|
219
|
+
enum: [...SLICER_TYPE_ENUM],
|
|
220
|
+
description:
|
|
221
|
+
"Type of slicer to use (prusaslicer, cura, slic3r, orcaslicer, orcaslicer-bambulab, bambustudio). Use orcaslicer-bambulab for the FULU fork. " +
|
|
222
|
+
"bambustudio and orcaslicer-bambulab (and orcaslicer with bambu_model) export a sliced 3MF.",
|
|
223
|
+
},
|
|
224
|
+
slicer_path: {
|
|
225
|
+
type: "string",
|
|
226
|
+
description: "Path to the slicer executable (default: value from env). Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1.",
|
|
227
|
+
},
|
|
228
|
+
slicer_profile: {
|
|
229
|
+
type: "string",
|
|
230
|
+
description:
|
|
231
|
+
"Profile to use for slicing (default: SLICER_PROFILE env). Bambu-compatible slicing: one process profile JSON (the machine preset comes from bambu_model). " +
|
|
232
|
+
"Generic OrcaSlicer: machine/process profiles separated with ';', optionally followed by '|filament.json'.",
|
|
233
|
+
},
|
|
234
|
+
filament_profile: {
|
|
235
|
+
type: "string",
|
|
236
|
+
description: "Filament profile path(s), ';'-separated in slot order, loaded with --load-filaments (default: FILAMENT_PROFILE/SLICER_FILAMENT_PROFILE env). Alias of load_filaments.",
|
|
237
|
+
},
|
|
238
|
+
load_filaments: {
|
|
239
|
+
type: "string",
|
|
240
|
+
description: "Bambu-compatible slicing: filament profile JSON paths in slot order, ';'-separated. One profile applies to every project slot; otherwise supply one per slot.",
|
|
241
|
+
},
|
|
242
|
+
load_filament_ids: {
|
|
243
|
+
type: "string",
|
|
244
|
+
description: "Bambu-compatible slicing: comma-separated filament IDs mapping load_filaments to objects, e.g. '1,2,3,1'.",
|
|
245
|
+
},
|
|
246
|
+
filament_colours: {
|
|
247
|
+
type: "string",
|
|
248
|
+
description: "Bambu-compatible slicing: one #RRGGBB per filament slot, ';'-separated. Defaults to the input 3MF's colours, then each profile's colour.",
|
|
249
|
+
},
|
|
250
|
+
bed_type: {
|
|
251
|
+
type: "string",
|
|
252
|
+
enum: [...VALID_BED_TYPES],
|
|
253
|
+
description: "Bambu-compatible slicing: build plate type (default: BED_TYPE or textured_plate).",
|
|
254
|
+
},
|
|
255
|
+
nozzle_type: {
|
|
256
|
+
type: "string",
|
|
257
|
+
enum: [...VALID_NOZZLE_TYPES],
|
|
258
|
+
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.",
|
|
259
|
+
},
|
|
260
|
+
template_3mf_path: {
|
|
261
|
+
type: "string",
|
|
262
|
+
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.",
|
|
263
|
+
},
|
|
264
|
+
template_name: {
|
|
265
|
+
type: "string",
|
|
266
|
+
description: "Named template from the local registry (see list_templates); resolves to its file.",
|
|
267
|
+
},
|
|
268
|
+
template_dir: {
|
|
269
|
+
type: "string",
|
|
270
|
+
description: "Template directory override when resolving template_name (default: BAMBU_TEMPLATE_DIR or ~/Sync/bambu/templates).",
|
|
271
|
+
},
|
|
272
|
+
uptodate: { type: "boolean", description: "Bambu-compatible slicing: refresh 3MF preset configs to the installed slicer version (--uptodate)." },
|
|
273
|
+
repetitions: { type: "number", description: "Bambu-compatible slicing: print N identical copies (--repetitions)." },
|
|
274
|
+
orient: { type: "boolean", description: "Bambu-compatible slicing: auto-orient for printability (--orient)." },
|
|
275
|
+
arrange: { type: "boolean", description: "Bambu-compatible slicing: auto-arrange objects on the plate (--arrange). Set false to keep an existing layout." },
|
|
276
|
+
ensure_on_bed: { type: "boolean", description: "Bambu-compatible slicing: lower floating models onto the bed (--ensure-on-bed)." },
|
|
277
|
+
clone_objects: { type: "string", description: "Bambu-compatible slicing: comma-separated clone counts per object index, e.g. '1,3,1,10'." },
|
|
278
|
+
skip_objects: { type: "string", description: "Bambu-compatible slicing: comma-separated object indices to skip, e.g. '3,5,10'." },
|
|
279
|
+
enable_timelapse: { type: "boolean", description: "Bambu-compatible slicing: insert timelapse parking moves (--enable-timelapse)." },
|
|
280
|
+
allow_mix_temp: { type: "boolean", description: "Bambu-compatible slicing: allow filaments with different temperature requirements on one plate." },
|
|
281
|
+
scale: { type: "number", description: "Bambu-compatible slicing: uniform scale factor applied before slicing (1.0 = original size)." },
|
|
282
|
+
rotate: { type: "number", description: "Bambu-compatible slicing: Z-axis rotation in degrees before slicing." },
|
|
283
|
+
rotate_x: { type: "number", description: "Bambu-compatible slicing: X-axis rotation in degrees before slicing." },
|
|
284
|
+
rotate_y: { type: "number", description: "Bambu-compatible slicing: Y-axis rotation in degrees before slicing." },
|
|
285
|
+
min_save: { type: "boolean", description: "Bambu-compatible slicing: write a smaller output 3MF (--min-save)." },
|
|
286
|
+
skip_modified_gcodes: { type: "boolean", description: "Bambu-compatible slicing: ignore custom G-code embedded in an input 3MF (--skip-modified-gcodes)." },
|
|
287
|
+
slice_plate: { type: "number", description: "Bambu-compatible slicing: plate number to slice; 0 slices all plates (default)." },
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
function validateSliceBambuModel(model: string): string {
|
|
291
|
+
const normalized = model.trim().toLowerCase();
|
|
292
|
+
if (!(SLICE_BAMBU_MODELS as readonly string[]).includes(normalized)) {
|
|
293
|
+
throw new Error(
|
|
294
|
+
`Invalid bambu_model "${model}" for slicing. Valid models: ${SLICE_BAMBU_MODELS.join(", ")}`
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
return normalized;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function normalizeSliceNozzleDiameter(raw: unknown): string {
|
|
301
|
+
const text = String(raw ?? "").trim() || DEFAULT_NOZZLE_DIAMETER;
|
|
302
|
+
const value = Number(text);
|
|
303
|
+
if (!/^\d+(\.\d+)?$/.test(text) || !Number.isFinite(value) || value <= 0 || value > 2) {
|
|
304
|
+
throw new Error(`Invalid nozzle_diameter "${text}". Use a diameter in mm such as 0.2, 0.4, 0.6, or 0.8.`);
|
|
305
|
+
}
|
|
306
|
+
return String(value);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Parse "#RRGGBB;#RRGGBB" (leading '#' optional) into positional slot colours. */
|
|
310
|
+
function parseFilamentColours(value: string): string[] {
|
|
311
|
+
const colours = value.split(";").map((c) => c.trim()).filter(Boolean)
|
|
312
|
+
.map((c) => (c.startsWith("#") ? c : `#${c}`).toUpperCase());
|
|
313
|
+
for (const c of colours) {
|
|
314
|
+
if (!/^#[0-9A-F]{6}([0-9A-F]{2})?$/.test(c)) {
|
|
315
|
+
throw new Error(`Invalid filament colour "${c}"; expected #RRGGBB.`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (colours.length === 0) throw new Error("Invalid filament_colours: list at least one colour.");
|
|
319
|
+
return colours;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function optionalFiniteNumber(args: Record<string, unknown> | undefined, key: string): number | undefined {
|
|
323
|
+
if (args?.[key] === undefined) return undefined;
|
|
324
|
+
const value = Number(args[key]);
|
|
325
|
+
if (!Number.isFinite(value)) throw new Error(`Invalid ${key}: expected a number.`);
|
|
326
|
+
return value;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function optionalBoolean(args: Record<string, unknown> | undefined, key: string): boolean | undefined {
|
|
330
|
+
const value = args?.[key];
|
|
331
|
+
if (value === undefined) return undefined;
|
|
332
|
+
if (typeof value !== "boolean") throw new Error(`Invalid ${key}: expected a boolean.`);
|
|
333
|
+
return value;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Map slice_stl/slice_with_template arguments to Bambu-compatible CLI options. */
|
|
337
|
+
function buildBambuSliceOptions(args: Record<string, unknown> | undefined): BambuSliceOptions {
|
|
338
|
+
const options: BambuSliceOptions = {};
|
|
339
|
+
const text = (key: string) => (args?.[key] !== undefined && String(args[key]).trim() ? String(args[key]).trim() : undefined);
|
|
340
|
+
options.uptodate = optionalBoolean(args, "uptodate");
|
|
341
|
+
options.orient = optionalBoolean(args, "orient");
|
|
342
|
+
options.arrange = optionalBoolean(args, "arrange");
|
|
343
|
+
options.ensureOnBed = optionalBoolean(args, "ensure_on_bed");
|
|
344
|
+
options.enableTimelapse = optionalBoolean(args, "enable_timelapse");
|
|
345
|
+
options.allowMixTemp = optionalBoolean(args, "allow_mix_temp");
|
|
346
|
+
options.minSave = optionalBoolean(args, "min_save");
|
|
347
|
+
options.skipModifiedGcodes = optionalBoolean(args, "skip_modified_gcodes");
|
|
348
|
+
options.repetitions = optionalFiniteNumber(args, "repetitions");
|
|
349
|
+
options.scale = optionalFiniteNumber(args, "scale");
|
|
350
|
+
options.rotate = optionalFiniteNumber(args, "rotate");
|
|
351
|
+
options.rotateX = optionalFiniteNumber(args, "rotate_x");
|
|
352
|
+
options.rotateY = optionalFiniteNumber(args, "rotate_y");
|
|
353
|
+
options.slicePlate = optionalFiniteNumber(args, "slice_plate");
|
|
354
|
+
if (options.repetitions !== undefined && (!Number.isInteger(options.repetitions) || options.repetitions < 1)) {
|
|
355
|
+
throw new Error("Invalid repetitions: expected a positive integer.");
|
|
356
|
+
}
|
|
357
|
+
if (options.slicePlate !== undefined && (!Number.isInteger(options.slicePlate) || options.slicePlate < 0)) {
|
|
358
|
+
throw new Error("Invalid slice_plate: expected 0 (all plates) or a plate number.");
|
|
359
|
+
}
|
|
360
|
+
if (options.scale !== undefined && options.scale <= 0) throw new Error("Invalid scale: expected a positive factor.");
|
|
361
|
+
options.cloneObjects = text("clone_objects");
|
|
362
|
+
options.skipObjects = text("skip_objects");
|
|
363
|
+
options.loadFilamentIds = text("load_filament_ids");
|
|
364
|
+
const loadFilaments = text("load_filaments");
|
|
365
|
+
const filamentAlias = text("filament_profile");
|
|
366
|
+
if (loadFilaments && filamentAlias && loadFilaments !== filamentAlias) {
|
|
367
|
+
throw new Error("Provide either load_filaments or filament_profile, not conflicting values.");
|
|
368
|
+
}
|
|
369
|
+
options.loadFilaments = loadFilaments ?? filamentAlias;
|
|
370
|
+
const colours = text("filament_colours");
|
|
371
|
+
if (colours) options.filamentColours = parseFilamentColours(colours);
|
|
372
|
+
options.bedType = resolveBedType(args?.bed_type as string | undefined);
|
|
373
|
+
options.nozzleType = resolveNozzleType(args?.nozzle_type);
|
|
374
|
+
return Object.fromEntries(Object.entries(options).filter(([, value]) => value !== undefined)) as BambuSliceOptions;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function defaultSlicerPathFor(slicerType: SlicerType): string {
|
|
378
|
+
if (process.env.SLICER_PATH) return process.env.SLICER_PATH;
|
|
379
|
+
if (slicerType === DEFAULT_SLICER_TYPE) return DEFAULT_SLICER_PATH;
|
|
380
|
+
if (slicerType === "bambustudio") return defaultBambuStudioPath();
|
|
381
|
+
if (slicerType === "orcaslicer-bambulab") return defaultFuluOrcaPath();
|
|
382
|
+
return DEFAULT_SLICER_PATH;
|
|
383
|
+
}
|
|
384
|
+
|
|
171
385
|
function validateBambuModel(model: string): BambuModel {
|
|
172
386
|
const normalized = model.trim().toLowerCase();
|
|
173
387
|
if (!(VALID_BAMBU_MODELS as readonly string[]).includes(normalized)) {
|
|
@@ -188,6 +402,35 @@ function resolveBedType(bedType: string | undefined): string {
|
|
|
188
402
|
return resolved;
|
|
189
403
|
}
|
|
190
404
|
|
|
405
|
+
const SUPPORTED_NOZZLE_DIAMETERS = [0.2, 0.4, 0.6, 0.8];
|
|
406
|
+
|
|
407
|
+
/** Optional declared material; blank means "not declared". */
|
|
408
|
+
function optionalMaterialArg(value: unknown): string | undefined {
|
|
409
|
+
if (value === undefined || value === null || value === "") return undefined;
|
|
410
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
411
|
+
throw new Error("material must be a non-empty string such as PLA, PETG, ABS, ASA, TPU, PA or PC.");
|
|
412
|
+
}
|
|
413
|
+
return value.trim();
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** Temperatures are validated as finite numbers before any printer connection. */
|
|
417
|
+
function optionalTemperatureArg(value: unknown, name: string): number | undefined {
|
|
418
|
+
if (value === undefined) return undefined;
|
|
419
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
420
|
+
throw new Error(`${name} must be a finite, non-negative number in °C.`);
|
|
421
|
+
}
|
|
422
|
+
return value;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function optionalNozzleDiameterArg(value: unknown): number | undefined {
|
|
426
|
+
if (value === undefined || value === null || value === "") return undefined;
|
|
427
|
+
const diameter = Number(value);
|
|
428
|
+
if ((typeof value !== "number" && typeof value !== "string") || !SUPPORTED_NOZZLE_DIAMETERS.includes(diameter)) {
|
|
429
|
+
throw new Error(`nozzle_diameter must be one of ${SUPPORTED_NOZZLE_DIAMETERS.join(", ")} mm.`);
|
|
430
|
+
}
|
|
431
|
+
return diameter;
|
|
432
|
+
}
|
|
433
|
+
|
|
191
434
|
type RuntimeConfig = {
|
|
192
435
|
transport: "stdio" | "streamable-http";
|
|
193
436
|
httpHost: string;
|
|
@@ -274,13 +517,29 @@ type StructuredToolError = {
|
|
|
274
517
|
suggestion: string;
|
|
275
518
|
message: string;
|
|
276
519
|
tool: string;
|
|
520
|
+
slicer?: Record<string, unknown>;
|
|
277
521
|
};
|
|
278
522
|
|
|
523
|
+
const SLICING_TOOLS = new Set(["slice_stl", "slice_with_template", "list_templates", "save_template", "get_slice_settings"]);
|
|
524
|
+
|
|
279
525
|
// Ensure temp directory exists
|
|
280
526
|
if (!fs.existsSync(TEMP_DIR)) {
|
|
281
527
|
fs.mkdirSync(TEMP_DIR, { recursive: true });
|
|
282
528
|
}
|
|
283
529
|
|
|
530
|
+
|
|
531
|
+
/** MQTT "Not authorized", FTP 530, and HTTP 401/403 all mean the printer rejected its credentials. */
|
|
532
|
+
function isCredentialRejection(message: string): boolean {
|
|
533
|
+
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);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function credentialSuggestion(): string {
|
|
537
|
+
return (process.env.PRINTER_TYPE ?? "").toLowerCase() === "bambu"
|
|
538
|
+
? "The printer rejected its LAN access code. Read the current Access Code on the printer (Settings > Network/WLAN; it changes " +
|
|
539
|
+
"when LAN mode is toggled and after some resets), update BAMBU_TOKEN, and check BAMBU_SERIAL. Retrying unchanged will not help."
|
|
540
|
+
: "The printer rejected its credentials. Check API_KEY (and PRINTER_HOST/PRINTER_PORT) for this printer system. Retrying unchanged will not help.";
|
|
541
|
+
}
|
|
542
|
+
|
|
284
543
|
class ThreeDPrinterMCPServer {
|
|
285
544
|
private server: Server;
|
|
286
545
|
private readonly blender = new BlenderMcpBridge();
|
|
@@ -304,13 +563,37 @@ class ThreeDPrinterMCPServer {
|
|
|
304
563
|
}
|
|
305
564
|
);
|
|
306
565
|
|
|
307
|
-
this.printerFactory = new PrinterFactory();
|
|
566
|
+
this.printerFactory = new PrinterFactory((message) => this.confirmHardware(message));
|
|
308
567
|
this.stlManipulator = new STLManipulator(TEMP_DIR);
|
|
309
568
|
|
|
310
569
|
this.setupHandlers();
|
|
311
570
|
this.setupErrorHandling();
|
|
312
571
|
}
|
|
313
572
|
|
|
573
|
+
/** Human MCP elicitation before a print start or positive heating command. */
|
|
574
|
+
private async confirmHardware(message: string): Promise<boolean> {
|
|
575
|
+
let response;
|
|
576
|
+
const timeout = confirmationTimeoutMs();
|
|
577
|
+
try {
|
|
578
|
+
response = await this.server.elicitInput({
|
|
579
|
+
mode: "form",
|
|
580
|
+
message,
|
|
581
|
+
requestedSchema: {
|
|
582
|
+
type: "object",
|
|
583
|
+
properties: {
|
|
584
|
+
confirmed: { type: "boolean", title: "I checked the printer and confirm this operation", default: false },
|
|
585
|
+
},
|
|
586
|
+
required: ["confirmed"],
|
|
587
|
+
},
|
|
588
|
+
}, { timeout });
|
|
589
|
+
} catch (error) {
|
|
590
|
+
// A person may take minutes to check the bed; a timeout is not a missing capability.
|
|
591
|
+
if ((error as { code?: unknown })?.code === ErrorCode.RequestTimeout) throw new Error(confirmationTimedOut(timeout));
|
|
592
|
+
throw new Error(CONFIRMATION_UNAVAILABLE);
|
|
593
|
+
}
|
|
594
|
+
return response.action === "accept" && response.content?.confirmed === true;
|
|
595
|
+
}
|
|
596
|
+
|
|
314
597
|
private async resolveBambuModel(argsModel: string | undefined): Promise<string> {
|
|
315
598
|
const fromArgs = (argsModel || DEFAULT_BAMBU_MODEL).trim().toLowerCase();
|
|
316
599
|
if (fromArgs) {
|
|
@@ -344,7 +627,7 @@ class ThreeDPrinterMCPServer {
|
|
|
344
627
|
},
|
|
345
628
|
required: ["bambu_model"],
|
|
346
629
|
},
|
|
347
|
-
});
|
|
630
|
+
}, { timeout: confirmationTimeoutMs() });
|
|
348
631
|
|
|
349
632
|
if (result.action === "accept" && result.content?.bambu_model) {
|
|
350
633
|
return validateBambuModel(String(result.content.bambu_model));
|
|
@@ -355,6 +638,12 @@ class ThreeDPrinterMCPServer {
|
|
|
355
638
|
);
|
|
356
639
|
} catch (elicitError: any) {
|
|
357
640
|
const msg = elicitError?.message || String(elicitError);
|
|
641
|
+
if (elicitError?.code === ErrorCode.RequestTimeout) {
|
|
642
|
+
throw new Error(
|
|
643
|
+
"No printer model was chosen in time; nothing was sent. Set BAMBU_MODEL or pass bambu_model, " +
|
|
644
|
+
`or answer the prompt within PRINT_CONFIRMATION_TIMEOUT_MS. Valid models: ${VALID_BAMBU_MODELS.join(", ")}`
|
|
645
|
+
);
|
|
646
|
+
}
|
|
358
647
|
if (
|
|
359
648
|
elicitError?.code === -32601 || elicitError?.code === -32600 ||
|
|
360
649
|
msg.includes("does not support") || msg.includes("elicitation")
|
|
@@ -369,6 +658,79 @@ class ThreeDPrinterMCPServer {
|
|
|
369
658
|
}
|
|
370
659
|
}
|
|
371
660
|
|
|
661
|
+
/** Slicing accepts every model with a Bambu-compatible CLI preset mapping. */
|
|
662
|
+
private async resolveSliceBambuModel(argsModel: string | undefined): Promise<string> {
|
|
663
|
+
const requested = (argsModel || DEFAULT_BAMBU_MODEL).trim().toLowerCase();
|
|
664
|
+
if (requested) {
|
|
665
|
+
return validateSliceBambuModel(requested);
|
|
666
|
+
}
|
|
667
|
+
return this.resolveBambuModel(undefined);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/** Shared slice_stl / slice_with_template implementation. */
|
|
671
|
+
private async runSliceTool(toolName: string, args: Record<string, unknown> | undefined): Promise<string> {
|
|
672
|
+
if (!args?.stl_path) {
|
|
673
|
+
throw new Error("Missing required parameter: stl_path");
|
|
674
|
+
}
|
|
675
|
+
const slicerType = normalizeSlicerType(String(args.slicer_type || DEFAULT_SLICER_TYPE));
|
|
676
|
+
const explicitModel =
|
|
677
|
+
typeof args.bambu_model === "string" && args.bambu_model.trim() ? args.bambu_model : undefined;
|
|
678
|
+
// Generic OrcaSlicer keeps its user-supplied machine profile unless the
|
|
679
|
+
// call explicitly targets a Bambu model.
|
|
680
|
+
const bambuCli = isBambuProjectSlicer(slicerType) || (slicerType === "orcaslicer" && explicitModel !== undefined);
|
|
681
|
+
|
|
682
|
+
let printerPreset: string | undefined;
|
|
683
|
+
let options: BambuSliceOptions | undefined;
|
|
684
|
+
if (bambuCli) {
|
|
685
|
+
const model = await this.resolveSliceBambuModel(explicitModel);
|
|
686
|
+
printerPreset = SLICE_BAMBU_MODEL_PRESETS[model](normalizeSliceNozzleDiameter(args.nozzle_diameter));
|
|
687
|
+
options = buildBambuSliceOptions(args);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Process profile precedence: explicit slicer_profile, then a template, then SLICER_PROFILE.
|
|
691
|
+
const explicitProfile =
|
|
692
|
+
typeof args.slicer_profile === "string" && args.slicer_profile.trim() ? args.slicer_profile.trim() : undefined;
|
|
693
|
+
const templateDir = typeof args.template_dir === "string" ? args.template_dir : undefined;
|
|
694
|
+
const requestedTemplate =
|
|
695
|
+
resolveTemplatePath(
|
|
696
|
+
typeof args.template_name === "string" ? args.template_name : undefined,
|
|
697
|
+
templateDir,
|
|
698
|
+
["json", "config", "3mf"]
|
|
699
|
+
) ||
|
|
700
|
+
(typeof args.template_3mf_path === "string" && args.template_3mf_path.trim() ? args.template_3mf_path.trim() : undefined);
|
|
701
|
+
if (requestedTemplate && !bambuCli) {
|
|
702
|
+
throw new Error(
|
|
703
|
+
"template_name/template_3mf_path apply to Bambu-compatible slicing (bambustudio, orcaslicer-bambulab, or orcaslicer with bambu_model)."
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
let slicerProfile = explicitProfile;
|
|
707
|
+
const templatePath = requestedTemplate || (bambuCli && DEFAULT_TEMPLATE_3MF_PATH ? DEFAULT_TEMPLATE_3MF_PATH : undefined);
|
|
708
|
+
if (!slicerProfile && templatePath) {
|
|
709
|
+
if (!fs.existsSync(templatePath)) {
|
|
710
|
+
throw new Error(`Template not found: ${templatePath}`);
|
|
711
|
+
}
|
|
712
|
+
slicerProfile = await resolveTemplateSlicerProfilePath(templatePath, path.join(TEMP_DIR, "templates"));
|
|
713
|
+
}
|
|
714
|
+
slicerProfile ??= DEFAULT_SLICER_PROFILE || undefined;
|
|
715
|
+
|
|
716
|
+
const explicitFilament =
|
|
717
|
+
typeof args.filament_profile === "string" && args.filament_profile.trim() ? args.filament_profile.trim() : undefined;
|
|
718
|
+
const filamentProfile = options?.loadFilaments ?? explicitFilament ?? (DEFAULT_FILAMENT_PROFILE || undefined);
|
|
719
|
+
const slicerPath =
|
|
720
|
+
this.resolveExecutableSelectorArg(args.slicer_path, toolName, "slicer_path") ?? defaultSlicerPathFor(slicerType);
|
|
721
|
+
|
|
722
|
+
return this.stlManipulator.sliceSTL(
|
|
723
|
+
String(args.stl_path),
|
|
724
|
+
slicerType,
|
|
725
|
+
slicerPath,
|
|
726
|
+
slicerProfile,
|
|
727
|
+
undefined, // progressCallback
|
|
728
|
+
printerPreset,
|
|
729
|
+
filamentProfile,
|
|
730
|
+
options
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
|
|
372
734
|
setupErrorHandling() {
|
|
373
735
|
this.server.onerror = (error) => {
|
|
374
736
|
console.error("[MCP Error]", error);
|
|
@@ -588,7 +950,7 @@ class ThreeDPrinterMCPServer {
|
|
|
588
950
|
},
|
|
589
951
|
{
|
|
590
952
|
name: "upload_gcode",
|
|
591
|
-
description: "Upload G-code content or a local G-code file path to the printer",
|
|
953
|
+
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.",
|
|
592
954
|
inputSchema: {
|
|
593
955
|
type: "object",
|
|
594
956
|
properties: {
|
|
@@ -635,14 +997,18 @@ class ThreeDPrinterMCPServer {
|
|
|
635
997
|
},
|
|
636
998
|
print: {
|
|
637
999
|
type: "boolean",
|
|
638
|
-
description: "Start printing after upload when the printer backend supports it."
|
|
1000
|
+
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."
|
|
1001
|
+
},
|
|
1002
|
+
material: {
|
|
1003
|
+
type: "string",
|
|
1004
|
+
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."
|
|
639
1005
|
}
|
|
640
1006
|
}
|
|
641
1007
|
}
|
|
642
1008
|
},
|
|
643
1009
|
{
|
|
644
1010
|
name: "start_print",
|
|
645
|
-
description: "Start printing a file
|
|
1011
|
+
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.",
|
|
646
1012
|
inputSchema: {
|
|
647
1013
|
type: "object",
|
|
648
1014
|
properties: {
|
|
@@ -678,6 +1044,10 @@ class ThreeDPrinterMCPServer {
|
|
|
678
1044
|
filename: {
|
|
679
1045
|
type: "string",
|
|
680
1046
|
description: "Name/path of the printer-side G-code file to start."
|
|
1047
|
+
},
|
|
1048
|
+
material: {
|
|
1049
|
+
type: "string",
|
|
1050
|
+
description: "Declared filament material when the G-code has no slicer filament_type metadata (non-Bambu printers). Must not contradict the file."
|
|
681
1051
|
}
|
|
682
1052
|
},
|
|
683
1053
|
required: ["filename"]
|
|
@@ -718,7 +1088,7 @@ class ThreeDPrinterMCPServer {
|
|
|
718
1088
|
},
|
|
719
1089
|
{
|
|
720
1090
|
name: "set_printer_temperature",
|
|
721
|
-
description: "Set the temperature of a printer component",
|
|
1091
|
+
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.",
|
|
722
1092
|
inputSchema: {
|
|
723
1093
|
type: "object",
|
|
724
1094
|
properties: {
|
|
@@ -752,7 +1122,20 @@ class ThreeDPrinterMCPServer {
|
|
|
752
1122
|
},
|
|
753
1123
|
temperature: {
|
|
754
1124
|
type: "number",
|
|
755
|
-
description: "Target temperature in Celsius."
|
|
1125
|
+
description: "Target temperature in Celsius: a finite number >= 0. 0 switches the heater off."
|
|
1126
|
+
},
|
|
1127
|
+
material: {
|
|
1128
|
+
type: "string",
|
|
1129
|
+
description: "Declared material at the nozzle (for example PLA, PETG, ABS). Required for positive nozzle heating, including non-RFID spools."
|
|
1130
|
+
},
|
|
1131
|
+
bambu_model: {
|
|
1132
|
+
type: "string",
|
|
1133
|
+
enum: [...VALID_BAMBU_MODELS],
|
|
1134
|
+
description: "Bambu printer model; required for positive Bambu heating unless BAMBU_MODEL is configured. Checked against the live printer."
|
|
1135
|
+
},
|
|
1136
|
+
nozzle_diameter: {
|
|
1137
|
+
type: "string",
|
|
1138
|
+
description: "Installed Bambu nozzle diameter in mm for nozzle heating (default: NOZZLE_DIAMETER or 0.4). Checked against the live printer."
|
|
756
1139
|
}
|
|
757
1140
|
},
|
|
758
1141
|
required: ["component", "temperature"]
|
|
@@ -779,7 +1162,11 @@ class ThreeDPrinterMCPServer {
|
|
|
779
1162
|
},
|
|
780
1163
|
{
|
|
781
1164
|
name: "slice_stl",
|
|
782
|
-
description:
|
|
1165
|
+
description:
|
|
1166
|
+
"Slice an STL or 3MF file to generate G-code or a sliced 3MF. For Bambu-compatible CLI slicing " +
|
|
1167
|
+
"(bambustudio, orcaslicer-bambulab, or orcaslicer with bambu_model), the exact bambu_model/nozzle machine " +
|
|
1168
|
+
"preset from the selected slicer installation is required, profile inheritance is resolved before the CLI " +
|
|
1169
|
+
"runs, and the result must contain plate G-code. Failures stop with the slicer's exit status and output.",
|
|
783
1170
|
inputSchema: {
|
|
784
1171
|
type: "object",
|
|
785
1172
|
properties: {
|
|
@@ -787,39 +1174,74 @@ class ThreeDPrinterMCPServer {
|
|
|
787
1174
|
type: "string",
|
|
788
1175
|
description: "Path to the STL or 3MF file to slice"
|
|
789
1176
|
},
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
},
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
description: "Path to the slicer executable (default: value from env). Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1."
|
|
807
|
-
},
|
|
1177
|
+
...SLICE_TOOL_PROPERTIES,
|
|
1178
|
+
},
|
|
1179
|
+
required: ["stl_path"]
|
|
1180
|
+
}
|
|
1181
|
+
},
|
|
1182
|
+
{
|
|
1183
|
+
name: "slice_with_template",
|
|
1184
|
+
description:
|
|
1185
|
+
"Slice an STL or 3MF with a named template from the local template registry (BAMBU_TEMPLATE_DIR). " +
|
|
1186
|
+
"The template supplies process settings; the machine preset still comes from bambu_model and nozzle_diameter.",
|
|
1187
|
+
inputSchema: {
|
|
1188
|
+
type: "object",
|
|
1189
|
+
properties: {
|
|
1190
|
+
stl_path: { type: "string", description: "Path to the STL or 3MF file to slice" },
|
|
1191
|
+
...SLICE_TOOL_PROPERTIES,
|
|
1192
|
+
template_name: { type: "string", description: "Named template from the local registry (required)." },
|
|
808
1193
|
slicer_profile: {
|
|
809
1194
|
type: "string",
|
|
810
|
-
description: "
|
|
1195
|
+
description: "Explicit process profile that overrides the named template only when provided in this call."
|
|
811
1196
|
},
|
|
812
|
-
|
|
1197
|
+
},
|
|
1198
|
+
required: ["stl_path", "template_name"]
|
|
1199
|
+
}
|
|
1200
|
+
},
|
|
1201
|
+
{
|
|
1202
|
+
name: "list_templates",
|
|
1203
|
+
description: "List saved slicing templates (.3mf, .json, .config) in the local template registry directory.",
|
|
1204
|
+
inputSchema: {
|
|
1205
|
+
type: "object",
|
|
1206
|
+
properties: {
|
|
1207
|
+
template_dir: {
|
|
813
1208
|
type: "string",
|
|
814
|
-
description: "
|
|
1209
|
+
description: "Template directory override. Defaults to BAMBU_TEMPLATE_DIR or ~/Sync/bambu/templates."
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
},
|
|
1214
|
+
{
|
|
1215
|
+
name: "save_template",
|
|
1216
|
+
description: "Copy a .3mf, .json, or .config file into the local template registry under a template name.",
|
|
1217
|
+
inputSchema: {
|
|
1218
|
+
type: "object",
|
|
1219
|
+
properties: {
|
|
1220
|
+
source_path: { type: "string", description: "Local .3mf, .json, or .config file to save as a template." },
|
|
1221
|
+
template_name: { type: "string", description: "Template name. Defaults to the source filename without extension." },
|
|
1222
|
+
template_dir: {
|
|
1223
|
+
type: "string",
|
|
1224
|
+
description: "Template directory override. Defaults to BAMBU_TEMPLATE_DIR or ~/Sync/bambu/templates."
|
|
815
1225
|
}
|
|
816
1226
|
},
|
|
817
|
-
required: ["
|
|
1227
|
+
required: ["source_path"]
|
|
1228
|
+
}
|
|
1229
|
+
},
|
|
1230
|
+
{
|
|
1231
|
+
name: "get_slice_settings",
|
|
1232
|
+
description: "Inspect slicer settings in a 3MF template or JSON/config profile without slicing (layer height, infill, walls, supports, brim, bed, printer, filaments).",
|
|
1233
|
+
inputSchema: {
|
|
1234
|
+
type: "object",
|
|
1235
|
+
properties: {
|
|
1236
|
+
source_path: { type: "string", description: "Path to a 3MF, extracted project_settings.config, or slicer profile JSON." },
|
|
1237
|
+
template_name: { type: "string", description: "Named template from the local registry; used when source_path is omitted." },
|
|
1238
|
+
template_dir: { type: "string", description: "Template directory override when resolving template_name." }
|
|
1239
|
+
}
|
|
818
1240
|
}
|
|
819
1241
|
},
|
|
820
1242
|
{
|
|
821
1243
|
name: "confirm_temperatures",
|
|
822
|
-
description: "
|
|
1244
|
+
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.",
|
|
823
1245
|
inputSchema: {
|
|
824
1246
|
type: "object",
|
|
825
1247
|
properties: {
|
|
@@ -829,11 +1251,11 @@ class ThreeDPrinterMCPServer {
|
|
|
829
1251
|
},
|
|
830
1252
|
extruder_temp: {
|
|
831
1253
|
type: "number",
|
|
832
|
-
description: "Expected
|
|
1254
|
+
description: "Expected highest nozzle target"
|
|
833
1255
|
},
|
|
834
1256
|
bed_temp: {
|
|
835
1257
|
type: "number",
|
|
836
|
-
description: "Expected bed
|
|
1258
|
+
description: "Expected highest bed target"
|
|
837
1259
|
}
|
|
838
1260
|
},
|
|
839
1261
|
required: ["gcode_path"]
|
|
@@ -841,7 +1263,7 @@ class ThreeDPrinterMCPServer {
|
|
|
841
1263
|
},
|
|
842
1264
|
{
|
|
843
1265
|
name: "process_and_print_stl",
|
|
844
|
-
description: "Process an STL file (extend base), slice it,
|
|
1266
|
+
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.",
|
|
845
1267
|
inputSchema: {
|
|
846
1268
|
type: "object",
|
|
847
1269
|
properties: {
|
|
@@ -855,11 +1277,15 @@ class ThreeDPrinterMCPServer {
|
|
|
855
1277
|
},
|
|
856
1278
|
extruder_temp: {
|
|
857
1279
|
type: "number",
|
|
858
|
-
description: "Expected
|
|
1280
|
+
description: "Expected highest nozzle target in the sliced G-code (S and R forms, every tool). Printing stops before upload if it differs."
|
|
859
1281
|
},
|
|
860
1282
|
bed_temp: {
|
|
861
1283
|
type: "number",
|
|
862
|
-
description: "Expected bed
|
|
1284
|
+
description: "Expected highest bed target in the sliced G-code (S and R forms). Printing stops before upload if it differs."
|
|
1285
|
+
},
|
|
1286
|
+
material: {
|
|
1287
|
+
type: "string",
|
|
1288
|
+
description: "Declared filament material when the sliced G-code has no filament_type metadata. Must not contradict the file."
|
|
863
1289
|
},
|
|
864
1290
|
host: {
|
|
865
1291
|
type: "string",
|
|
@@ -887,6 +1313,11 @@ class ThreeDPrinterMCPServer {
|
|
|
887
1313
|
enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate"],
|
|
888
1314
|
description: "Bed/plate type installed on the printer (default: textured_plate)."
|
|
889
1315
|
},
|
|
1316
|
+
nozzle_type: {
|
|
1317
|
+
type: "string",
|
|
1318
|
+
enum: [...VALID_NOZZLE_TYPES],
|
|
1319
|
+
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."
|
|
1320
|
+
},
|
|
890
1321
|
nozzle_diameter: {
|
|
891
1322
|
type: "string",
|
|
892
1323
|
description: "Nozzle diameter in mm (default: 0.4)."
|
|
@@ -1093,7 +1524,7 @@ class ThreeDPrinterMCPServer {
|
|
|
1093
1524
|
},
|
|
1094
1525
|
{
|
|
1095
1526
|
name: "print_3mf",
|
|
1096
|
-
description: "Print a 3MF file on a Bambu Lab printer,
|
|
1527
|
+
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.",
|
|
1097
1528
|
inputSchema: {
|
|
1098
1529
|
type: "object",
|
|
1099
1530
|
properties: {
|
|
@@ -1111,6 +1542,11 @@ class ThreeDPrinterMCPServer {
|
|
|
1111
1542
|
enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate"],
|
|
1112
1543
|
description: "Bed/plate type installed on the printer (default: textured_plate)."
|
|
1113
1544
|
},
|
|
1545
|
+
nozzle_type: {
|
|
1546
|
+
type: "string",
|
|
1547
|
+
enum: [...VALID_NOZZLE_TYPES],
|
|
1548
|
+
description: "Installed nozzle material, used when the 3MF must be auto-sliced (default: BAMBU_NOZZLE_TYPE, else the preset's stock nozzle)."
|
|
1549
|
+
},
|
|
1114
1550
|
nozzle_diameter: {
|
|
1115
1551
|
type: "string",
|
|
1116
1552
|
description: "Nozzle diameter in mm (default: 0.4)."
|
|
@@ -1234,7 +1670,7 @@ class ThreeDPrinterMCPServer {
|
|
|
1234
1670
|
{
|
|
1235
1671
|
name: "fulu_bambu_network_rpc",
|
|
1236
1672
|
description:
|
|
1237
|
-
"Advanced FULU bridge RPC for BambuNetwork diagnostics and development. Read-only methods are allowed by default
|
|
1673
|
+
"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.",
|
|
1238
1674
|
inputSchema: {
|
|
1239
1675
|
type: "object",
|
|
1240
1676
|
properties: {
|
|
@@ -1260,12 +1696,12 @@ class ThreeDPrinterMCPServer {
|
|
|
1260
1696
|
},
|
|
1261
1697
|
allow_mutating_method: {
|
|
1262
1698
|
type: "boolean",
|
|
1263
|
-
description: "Required for methods
|
|
1699
|
+
description: "Required for the allowlisted agent/session setup methods. It never enables print, printer-message, or unknown methods."
|
|
1264
1700
|
},
|
|
1265
1701
|
bambu_model: {
|
|
1266
1702
|
type: "string",
|
|
1267
1703
|
enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
|
|
1268
|
-
description: "
|
|
1704
|
+
description: "Informational only. Raw FULU print RPC methods are disabled; use print_3mf."
|
|
1269
1705
|
}
|
|
1270
1706
|
},
|
|
1271
1707
|
required: ["method"]
|
|
@@ -1319,11 +1755,13 @@ class ThreeDPrinterMCPServer {
|
|
|
1319
1755
|
},
|
|
1320
1756
|
{
|
|
1321
1757
|
name: "blender_mcp_status",
|
|
1322
|
-
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.",
|
|
1758
|
+
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.",
|
|
1323
1759
|
inputSchema: {
|
|
1324
1760
|
type: "object",
|
|
1325
1761
|
properties: {
|
|
1326
1762
|
connect: { type: "boolean", description: "Initialize the configured stdio MCP server and discover its tools (default false)." },
|
|
1763
|
+
tool_names: { type: "array", items: { type: "string" }, maxItems: 64, description: "Return full input schemas for these discovered tools, such as [\"execute_blender_code\"]." },
|
|
1764
|
+
include_schemas: { type: "boolean", description: "Return every discovered tool's full definition (large; default false)." },
|
|
1327
1765
|
timeout_ms: { type: "integer", minimum: 100, maximum: 300000, description: "Total connection and discovery deadline in milliseconds; defaults to BLENDER_MCP_TIMEOUT_MS or 120000." }
|
|
1328
1766
|
},
|
|
1329
1767
|
additionalProperties: false
|
|
@@ -1343,6 +1781,23 @@ class ThreeDPrinterMCPServer {
|
|
|
1343
1781
|
additionalProperties: false
|
|
1344
1782
|
}
|
|
1345
1783
|
},
|
|
1784
|
+
{
|
|
1785
|
+
name: "blender_mcp_export_stl",
|
|
1786
|
+
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.",
|
|
1787
|
+
inputSchema: {
|
|
1788
|
+
type: "object",
|
|
1789
|
+
properties: {
|
|
1790
|
+
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)." },
|
|
1791
|
+
output_path: { type: "string", description: "New local .stl path. Its parent must exist; existing files are never overwritten." },
|
|
1792
|
+
apply_modifiers: { type: "boolean", description: "Export the evaluated geometry with modifiers applied (default true)." },
|
|
1793
|
+
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." },
|
|
1794
|
+
user_prompt: { type: "string", description: "The user's own words, passed unchanged to Blender MCP." },
|
|
1795
|
+
timeout_ms: { type: "integer", minimum: 100, maximum: 300000, description: "Total Blender request deadline in milliseconds; defaults to BLENDER_MCP_TIMEOUT_MS or 120000." }
|
|
1796
|
+
},
|
|
1797
|
+
required: ["object_names", "output_path"],
|
|
1798
|
+
additionalProperties: false
|
|
1799
|
+
}
|
|
1800
|
+
},
|
|
1346
1801
|
{
|
|
1347
1802
|
name: "blender_mcp_edit_model",
|
|
1348
1803
|
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.",
|
|
@@ -1411,8 +1866,9 @@ class ThreeDPrinterMCPServer {
|
|
|
1411
1866
|
if (args.print !== undefined && typeof args.print !== "boolean") {
|
|
1412
1867
|
throw new Error("print must be a boolean.");
|
|
1413
1868
|
}
|
|
1869
|
+
const uploadOptions: PrintSafetyOptions = { material: optionalMaterialArg(args?.material) };
|
|
1414
1870
|
if (type.toLowerCase() === "bambu" && args.print === true) {
|
|
1415
|
-
await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
1871
|
+
uploadOptions.bambuModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
1416
1872
|
}
|
|
1417
1873
|
const uploadFilename = String(args?.filename || path.basename(String(args.gcode_path)));
|
|
1418
1874
|
const uploadGcodeSource = String(args?.gcode_path || args.gcode);
|
|
@@ -1420,7 +1876,8 @@ class ThreeDPrinterMCPServer {
|
|
|
1420
1876
|
host, port, type, apiKey, bambuSerial, bambuToken,
|
|
1421
1877
|
uploadFilename,
|
|
1422
1878
|
uploadGcodeSource,
|
|
1423
|
-
Boolean(args.print || false)
|
|
1879
|
+
Boolean(args.print || false),
|
|
1880
|
+
uploadOptions
|
|
1424
1881
|
);
|
|
1425
1882
|
break;
|
|
1426
1883
|
|
|
@@ -1428,10 +1885,11 @@ class ThreeDPrinterMCPServer {
|
|
|
1428
1885
|
if (!args?.filename) {
|
|
1429
1886
|
throw new Error("Missing required parameter: filename");
|
|
1430
1887
|
}
|
|
1888
|
+
const startOptions: PrintSafetyOptions = { material: optionalMaterialArg(args?.material) };
|
|
1431
1889
|
if (type.toLowerCase() === "bambu") {
|
|
1432
|
-
await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
1890
|
+
startOptions.bambuModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
1433
1891
|
}
|
|
1434
|
-
result = await this.startPrint(host, port, type, apiKey, bambuSerial, bambuToken, String(args.filename));
|
|
1892
|
+
result = await this.startPrint(host, port, type, apiKey, bambuSerial, bambuToken, String(args.filename), startOptions);
|
|
1435
1893
|
break;
|
|
1436
1894
|
|
|
1437
1895
|
case "cancel_print":
|
|
@@ -1442,10 +1900,19 @@ class ThreeDPrinterMCPServer {
|
|
|
1442
1900
|
if (!args?.component || args?.temperature === undefined) {
|
|
1443
1901
|
throw new Error("Missing required parameters: component and temperature");
|
|
1444
1902
|
}
|
|
1903
|
+
// Never coerce: "not-a-number" must not become NaN in a heater command.
|
|
1904
|
+
const requestedTemperature = optionalTemperatureArg(args.temperature, "temperature")!;
|
|
1905
|
+
const heatOptions: PrintSafetyOptions = { material: optionalMaterialArg(args.material) };
|
|
1906
|
+
if (type.toLowerCase() === "bambu" && requestedTemperature > 0) {
|
|
1907
|
+
heatOptions.bambuModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
1908
|
+
heatOptions.nozzleDiameter =
|
|
1909
|
+
optionalNozzleDiameterArg(args.nozzle_diameter) ?? Number(DEFAULT_NOZZLE_DIAMETER);
|
|
1910
|
+
}
|
|
1445
1911
|
result = await this.setPrinterTemperature(
|
|
1446
1912
|
host, port, type, apiKey, bambuSerial, bambuToken,
|
|
1447
|
-
String(args.component),
|
|
1448
|
-
|
|
1913
|
+
String(args.component),
|
|
1914
|
+
requestedTemperature,
|
|
1915
|
+
heatOptions
|
|
1449
1916
|
);
|
|
1450
1917
|
break;
|
|
1451
1918
|
|
|
@@ -1460,33 +1927,47 @@ class ThreeDPrinterMCPServer {
|
|
|
1460
1927
|
);
|
|
1461
1928
|
break;
|
|
1462
1929
|
|
|
1463
|
-
case "slice_stl":
|
|
1464
|
-
|
|
1465
|
-
|
|
1930
|
+
case "slice_stl":
|
|
1931
|
+
result = await this.runSliceTool("slice_stl", args as Record<string, unknown> | undefined);
|
|
1932
|
+
break;
|
|
1933
|
+
|
|
1934
|
+
case "slice_with_template":
|
|
1935
|
+
if (!args?.template_name || !String(args.template_name).trim()) {
|
|
1936
|
+
throw new Error("Missing required parameter: template_name");
|
|
1466
1937
|
}
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1938
|
+
result = await this.runSliceTool("slice_with_template", args as Record<string, unknown> | undefined);
|
|
1939
|
+
break;
|
|
1940
|
+
|
|
1941
|
+
case "list_templates":
|
|
1942
|
+
result = listTemplateRegistry(typeof args?.template_dir === "string" ? args.template_dir : undefined);
|
|
1943
|
+
break;
|
|
1944
|
+
|
|
1945
|
+
case "save_template":
|
|
1946
|
+
if (!args?.source_path) {
|
|
1947
|
+
throw new Error("Missing required parameter: source_path");
|
|
1473
1948
|
}
|
|
1474
|
-
result =
|
|
1475
|
-
String(args.
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
args?.slicer_path,
|
|
1479
|
-
"slice_stl",
|
|
1480
|
-
"slicer_path"
|
|
1481
|
-
) ?? DEFAULT_SLICER_PATH,
|
|
1482
|
-
slicerProfile || undefined,
|
|
1483
|
-
undefined, // progressCallback
|
|
1484
|
-
slicePreset,
|
|
1485
|
-
filamentProfile || undefined
|
|
1949
|
+
result = saveTemplate(
|
|
1950
|
+
String(args.source_path),
|
|
1951
|
+
typeof args?.template_name === "string" ? args.template_name : undefined,
|
|
1952
|
+
typeof args?.template_dir === "string" ? args.template_dir : undefined
|
|
1486
1953
|
);
|
|
1487
1954
|
break;
|
|
1955
|
+
|
|
1956
|
+
case "get_slice_settings": {
|
|
1957
|
+
if (!args?.source_path && !args?.template_name) {
|
|
1958
|
+
throw new Error("Missing required parameter: source_path or template_name");
|
|
1959
|
+
}
|
|
1960
|
+
const settingsSource = args?.source_path
|
|
1961
|
+
? String(args.source_path)
|
|
1962
|
+
: resolveTemplatePath(
|
|
1963
|
+
String(args.template_name),
|
|
1964
|
+
typeof args?.template_dir === "string" ? args.template_dir : undefined,
|
|
1965
|
+
["3mf", "json", "config"]
|
|
1966
|
+
)!;
|
|
1967
|
+
result = await inspectSliceSettings(settingsSource, TEMP_DIR);
|
|
1968
|
+
break;
|
|
1488
1969
|
}
|
|
1489
|
-
|
|
1970
|
+
|
|
1490
1971
|
case "confirm_temperatures":
|
|
1491
1972
|
if (!args?.gcode_path) {
|
|
1492
1973
|
throw new Error("Missing required parameter: gcode_path");
|
|
@@ -1505,26 +1986,36 @@ class ThreeDPrinterMCPServer {
|
|
|
1505
1986
|
throw new Error("Missing required parameters: stl_path and extension_inches");
|
|
1506
1987
|
}
|
|
1507
1988
|
|
|
1989
|
+
// Validate every safety input before extending, slicing or connecting.
|
|
1990
|
+
const expectedPeaks: ExpectedPeakTemperatures = {
|
|
1991
|
+
nozzle: optionalTemperatureArg(args.extruder_temp, "extruder_temp"),
|
|
1992
|
+
bed: optionalTemperatureArg(args.bed_temp, "bed_temp"),
|
|
1993
|
+
};
|
|
1994
|
+
validateExpectedPeaks(expectedPeaks);
|
|
1995
|
+
const processMaterial = optionalMaterialArg(args?.material);
|
|
1996
|
+
let processModel: string | undefined;
|
|
1997
|
+
let processNozzle = String(DEFAULT_NOZZLE_DIAMETER);
|
|
1998
|
+
if (type.toLowerCase() === 'bambu' || isBambuProjectSlicer(slicerType)) {
|
|
1999
|
+
processModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
2000
|
+
processNozzle = String(optionalNozzleDiameterArg(args?.nozzle_diameter) ?? DEFAULT_NOZZLE_DIAMETER);
|
|
2001
|
+
}
|
|
2002
|
+
|
|
1508
2003
|
// Define progress callback for UI updates
|
|
1509
2004
|
const processProgressCallback = (progress: number, message?: string) => {
|
|
1510
|
-
console.
|
|
2005
|
+
console.error(`Process progress: ${progress}% - ${message || ''}`);
|
|
1511
2006
|
};
|
|
1512
|
-
|
|
2007
|
+
|
|
1513
2008
|
// 1. Extend the base of the STL file
|
|
1514
2009
|
const extendedStlPath = await this.stlManipulator.extendBase(
|
|
1515
2010
|
String(args.stl_path),
|
|
1516
2011
|
Number(args.extension_inches),
|
|
1517
2012
|
processProgressCallback
|
|
1518
2013
|
);
|
|
1519
|
-
|
|
2014
|
+
|
|
1520
2015
|
// 2. Slice the extended STL file
|
|
1521
2016
|
let processPreset: string | undefined;
|
|
1522
|
-
if (
|
|
1523
|
-
|
|
1524
|
-
const processNozzle = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
1525
|
-
if (isBambuProjectSlicer(slicerType)) {
|
|
1526
|
-
processPreset = BAMBU_MODEL_PRESETS[processModel]?.(processNozzle);
|
|
1527
|
-
}
|
|
2017
|
+
if (processModel && isBambuProjectSlicer(slicerType)) {
|
|
2018
|
+
processPreset = BAMBU_MODEL_PRESETS[processModel]?.(processNozzle);
|
|
1528
2019
|
}
|
|
1529
2020
|
const gcodePath = await this.stlManipulator.sliceSTL(
|
|
1530
2021
|
extendedStlPath,
|
|
@@ -1537,7 +2028,14 @@ class ThreeDPrinterMCPServer {
|
|
|
1537
2028
|
slicerProfile || undefined,
|
|
1538
2029
|
processProgressCallback,
|
|
1539
2030
|
processPreset,
|
|
1540
|
-
filamentProfile || undefined
|
|
2031
|
+
filamentProfile || undefined,
|
|
2032
|
+
// Slice for the plate and nozzle the print gate will check against.
|
|
2033
|
+
processPreset
|
|
2034
|
+
? {
|
|
2035
|
+
bedType: resolveBedType(args?.bed_type as string | undefined),
|
|
2036
|
+
...(resolveNozzleType(args?.nozzle_type) ? { nozzleType: resolveNozzleType(args?.nozzle_type) } : {}),
|
|
2037
|
+
}
|
|
2038
|
+
: undefined
|
|
1541
2039
|
);
|
|
1542
2040
|
|
|
1543
2041
|
if (type.toLowerCase() === 'bambu' && gcodePath.toLowerCase().endsWith(".3mf")) {
|
|
@@ -1568,6 +2066,9 @@ class ThreeDPrinterMCPServer {
|
|
|
1568
2066
|
result = await factoryImplementation.print3mf(host, bambuSerial, bambuToken, {
|
|
1569
2067
|
projectName: path.basename(gcodePath).replace(/\.3mf$/i, ''),
|
|
1570
2068
|
filePath: gcodePath,
|
|
2069
|
+
bambuModel: processModel,
|
|
2070
|
+
nozzleDiameters: [Number(processNozzle)],
|
|
2071
|
+
expectedPeaks,
|
|
1571
2072
|
plateIndex: 0,
|
|
1572
2073
|
...amsOptions,
|
|
1573
2074
|
bedType: printBedType,
|
|
@@ -1590,38 +2091,26 @@ class ThreeDPrinterMCPServer {
|
|
|
1590
2091
|
);
|
|
1591
2092
|
}
|
|
1592
2093
|
|
|
1593
|
-
// 3.
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
extruder: args.extruder_temp !== undefined ? Number(args.extruder_temp) : undefined,
|
|
1599
|
-
bed: args.bed_temp !== undefined ? Number(args.bed_temp) : undefined
|
|
1600
|
-
},
|
|
1601
|
-
processProgressCallback
|
|
1602
|
-
);
|
|
1603
|
-
|
|
1604
|
-
if (!tempConfirmation.match) {
|
|
1605
|
-
console.warn("Temperature mismatch:", tempConfirmation);
|
|
1606
|
-
}
|
|
1607
|
-
}
|
|
1608
|
-
|
|
1609
|
-
// 4. Upload the G-code file to the printer
|
|
1610
|
-
const gcodeContent = await fs.promises.readFile(gcodePath, 'utf8');
|
|
2094
|
+
// 3. Upload and print through the adapter's hard safety gate. It
|
|
2095
|
+
// inspects the exact bytes sent (S and R targets, tool changes,
|
|
2096
|
+
// hardware and material ceilings), requires the expected peaks to
|
|
2097
|
+
// match, reads fresh printer state and asks for human confirmation.
|
|
2098
|
+
// A mismatch refuses before upload; nothing is only logged.
|
|
1611
2099
|
const filename = path.basename(gcodePath);
|
|
1612
|
-
|
|
1613
|
-
await this.uploadGcode(
|
|
2100
|
+
const printResult = await this.uploadGcode(
|
|
1614
2101
|
host, port, type, apiKey, bambuSerial, bambuToken,
|
|
1615
|
-
filename,
|
|
1616
|
-
|
|
1617
|
-
true // Start printing immediately
|
|
2102
|
+
filename,
|
|
2103
|
+
gcodePath,
|
|
2104
|
+
true, // Start printing immediately
|
|
2105
|
+
{ bambuModel: processModel, material: processMaterial, expectedPeaks }
|
|
1618
2106
|
);
|
|
1619
|
-
|
|
2107
|
+
|
|
1620
2108
|
result = {
|
|
1621
2109
|
extended_stl_path: extendedStlPath,
|
|
1622
2110
|
gcode_path: gcodePath,
|
|
1623
2111
|
filename,
|
|
1624
|
-
status: "
|
|
2112
|
+
status: "Checked print job started",
|
|
2113
|
+
print: printResult,
|
|
1625
2114
|
};
|
|
1626
2115
|
break;
|
|
1627
2116
|
|
|
@@ -1641,7 +2130,7 @@ class ThreeDPrinterMCPServer {
|
|
|
1641
2130
|
|
|
1642
2131
|
// Define progress callback for UI updates
|
|
1643
2132
|
const scaleProgressCallback = (progress: number, message?: string) => {
|
|
1644
|
-
console.
|
|
2133
|
+
console.error(`Scale progress: ${progress}% - ${message || ''}`);
|
|
1645
2134
|
};
|
|
1646
2135
|
|
|
1647
2136
|
let scaleFactors: number | [number, number, number];
|
|
@@ -1673,7 +2162,7 @@ class ThreeDPrinterMCPServer {
|
|
|
1673
2162
|
|
|
1674
2163
|
// Define progress callback for UI updates
|
|
1675
2164
|
const rotateProgressCallback = (progress: number, message?: string) => {
|
|
1676
|
-
console.
|
|
2165
|
+
console.error(`Rotate progress: ${progress}% - ${message || ''}`);
|
|
1677
2166
|
};
|
|
1678
2167
|
|
|
1679
2168
|
// Get rotation angles, defaulting to 0 for any undefined axis
|
|
@@ -1697,7 +2186,7 @@ class ThreeDPrinterMCPServer {
|
|
|
1697
2186
|
|
|
1698
2187
|
// Define progress callback for UI updates
|
|
1699
2188
|
const translateProgressCallback = (progress: number, message?: string) => {
|
|
1700
|
-
console.
|
|
2189
|
+
console.error(`Translate progress: ${progress}% - ${message || ''}`);
|
|
1701
2190
|
};
|
|
1702
2191
|
|
|
1703
2192
|
// Get translation values, defaulting to 0 for any undefined axis
|
|
@@ -1721,7 +2210,7 @@ class ThreeDPrinterMCPServer {
|
|
|
1721
2210
|
|
|
1722
2211
|
// Define progress callback for UI updates
|
|
1723
2212
|
const modifySectionProgressCallback = (progress: number, message?: string) => {
|
|
1724
|
-
console.
|
|
2213
|
+
console.error(`Modify section progress: ${progress}% - ${message || ''}`);
|
|
1725
2214
|
};
|
|
1726
2215
|
|
|
1727
2216
|
// Determine the section to modify
|
|
@@ -1798,7 +2287,7 @@ class ThreeDPrinterMCPServer {
|
|
|
1798
2287
|
|
|
1799
2288
|
// Define progress callback for UI updates
|
|
1800
2289
|
const visualizationProgressCallback = (progress: number, message?: string) => {
|
|
1801
|
-
console.
|
|
2290
|
+
console.error(`Visualization progress: ${progress}% - ${message || ''}`);
|
|
1802
2291
|
};
|
|
1803
2292
|
|
|
1804
2293
|
// Get width and height parameters, with defaults
|
|
@@ -1826,21 +2315,26 @@ class ThreeDPrinterMCPServer {
|
|
|
1826
2315
|
|
|
1827
2316
|
const printModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
1828
2317
|
const printBedType = resolveBedType(args?.bed_type as string | undefined);
|
|
1829
|
-
const
|
|
2318
|
+
const explicitPrintNozzle = optionalNozzleDiameterArg(args?.nozzle_diameter);
|
|
2319
|
+
const printNozzle = String(explicitPrintNozzle ?? DEFAULT_NOZZLE_DIAMETER);
|
|
1830
2320
|
const printPreset = BAMBU_MODEL_PRESETS[printModel]?.(printNozzle);
|
|
1831
2321
|
|
|
1832
2322
|
let threeMFPath = String(args.three_mf_path);
|
|
1833
2323
|
|
|
2324
|
+
// Only the selected plate's exact G-code entry counts as sliced output;
|
|
2325
|
+
// a stray .gcode entry elsewhere in the archive is not printable.
|
|
2326
|
+
const selectedPlateEntry = "Metadata/plate_1.gcode";
|
|
1834
2327
|
let shouldAutoSlice = false;
|
|
1835
2328
|
try {
|
|
1836
2329
|
const JSZip = (await import('jszip')).default;
|
|
1837
2330
|
const zipData = fs.readFileSync(threeMFPath);
|
|
1838
2331
|
const zip = await JSZip.loadAsync(zipData);
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
);
|
|
2332
|
+
const plateEntry = zip.file(selectedPlateEntry);
|
|
2333
|
+
shouldAutoSlice = !plateEntry || plateEntry.dir;
|
|
1842
2334
|
} catch (sliceCheckErr: any) {
|
|
1843
|
-
|
|
2335
|
+
throw new Error(
|
|
2336
|
+
`Cannot read 3MF archive ${threeMFPath}; nothing was uploaded or started: ${sliceCheckErr?.message ?? String(sliceCheckErr)}`
|
|
2337
|
+
);
|
|
1844
2338
|
}
|
|
1845
2339
|
|
|
1846
2340
|
// Only treat slicer_path as an executable selector when a slicer
|
|
@@ -1853,7 +2347,7 @@ class ThreeDPrinterMCPServer {
|
|
|
1853
2347
|
"slicer_path"
|
|
1854
2348
|
) ?? DEFAULT_SLICER_PATH;
|
|
1855
2349
|
try {
|
|
1856
|
-
console.
|
|
2350
|
+
console.error(`3MF has no gcode — auto-slicing with ${slicerType} for ${printModel}`);
|
|
1857
2351
|
threeMFPath = await this.stlManipulator.sliceSTL(
|
|
1858
2352
|
threeMFPath,
|
|
1859
2353
|
slicerType,
|
|
@@ -1861,13 +2355,26 @@ class ThreeDPrinterMCPServer {
|
|
|
1861
2355
|
slicerProfile || undefined,
|
|
1862
2356
|
undefined, // progressCallback
|
|
1863
2357
|
printPreset,
|
|
1864
|
-
filamentProfile || undefined
|
|
2358
|
+
filamentProfile || undefined,
|
|
2359
|
+
// Slice for the plate and nozzle the print will be checked against.
|
|
2360
|
+
{ bedType: printBedType, ...(resolveNozzleType(args?.nozzle_type) ? { nozzleType: resolveNozzleType(args?.nozzle_type) } : {}) }
|
|
1865
2361
|
);
|
|
1866
|
-
console.
|
|
2362
|
+
console.error("Auto-sliced to: " + threeMFPath);
|
|
1867
2363
|
} catch (sliceErr: any) {
|
|
1868
|
-
|
|
2364
|
+
// Never fall back to uploading the original unsliced project.
|
|
2365
|
+
// Rethrow the original instance (for example a SlicerError) so
|
|
2366
|
+
// structured slicer suggestions survive.
|
|
2367
|
+
const prefix = "Auto-slicing failed; nothing was uploaded or started: ";
|
|
2368
|
+
if (sliceErr instanceof Error) {
|
|
2369
|
+
if (!sliceErr.message.startsWith(prefix)) sliceErr.message = `${prefix}${sliceErr.message}`;
|
|
2370
|
+
throw sliceErr;
|
|
2371
|
+
}
|
|
2372
|
+
throw new Error(`${prefix}${String(sliceErr)}`);
|
|
1869
2373
|
}
|
|
1870
2374
|
}
|
|
2375
|
+
// Compare the sliced file with an explicit or auto-slice nozzle request.
|
|
2376
|
+
const requestedPrintNozzles =
|
|
2377
|
+
explicitPrintNozzle !== undefined || shouldAutoSlice ? [Number(printNozzle)] : undefined;
|
|
1871
2378
|
|
|
1872
2379
|
// Define variables needed outside the parse try block
|
|
1873
2380
|
let implementation: BambuImplementation;
|
|
@@ -1878,7 +2385,7 @@ class ThreeDPrinterMCPServer {
|
|
|
1878
2385
|
try {
|
|
1879
2386
|
// --- Parse 3MF ---
|
|
1880
2387
|
const parsed3MFData = await parse3MF(threeMFPath);
|
|
1881
|
-
console.
|
|
2388
|
+
console.error(`Successfully parsed 3MF file: ${threeMFPath}`);
|
|
1882
2389
|
const amsOptions = this.resolveAmsPrintOptions(
|
|
1883
2390
|
parsed3MFData,
|
|
1884
2391
|
args as Record<string, unknown> | undefined
|
|
@@ -1896,6 +2403,8 @@ class ThreeDPrinterMCPServer {
|
|
|
1896
2403
|
|
|
1897
2404
|
printOptions = { // Assign to outer scope variable
|
|
1898
2405
|
...amsOptions,
|
|
2406
|
+
bambuModel: printModel,
|
|
2407
|
+
nozzleDiameters: requestedPrintNozzles,
|
|
1899
2408
|
bedType: printBedType,
|
|
1900
2409
|
bedLeveling: args?.bed_leveling !== undefined ? Boolean(args.bed_leveling) : undefined,
|
|
1901
2410
|
flowCalibration: args?.flow_calibration !== undefined ? Boolean(args.flow_calibration) : undefined,
|
|
@@ -1918,7 +2427,7 @@ class ThreeDPrinterMCPServer {
|
|
|
1918
2427
|
plateIndex: 0,
|
|
1919
2428
|
...printOptions // Spread the final options
|
|
1920
2429
|
});
|
|
1921
|
-
|
|
2430
|
+
// Report what the printer did, not merely that a command was published.
|
|
1922
2431
|
} catch (printError) {
|
|
1923
2432
|
console.error(`Error starting 3MF print for ${threeMfFilename}:`, printError);
|
|
1924
2433
|
throw new Error(`Failed to start print: ${(printError as Error).message}`);
|
|
@@ -1971,11 +2480,11 @@ class ThreeDPrinterMCPServer {
|
|
|
1971
2480
|
throw new Error("Missing required parameter: method");
|
|
1972
2481
|
}
|
|
1973
2482
|
|
|
2483
|
+
// Raw print methods, printer messages and unknown methods are refused
|
|
2484
|
+
// inside invokeFuluBridgeRpc before any bridge process starts.
|
|
1974
2485
|
const method = String(args.method);
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
rpcBambuModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
1978
|
-
}
|
|
2486
|
+
const rpcBambuModel =
|
|
2487
|
+
typeof args?.bambu_model === "string" && args.bambu_model.trim() ? args.bambu_model.trim() : undefined;
|
|
1979
2488
|
|
|
1980
2489
|
const payload =
|
|
1981
2490
|
typeof args.payload === "object" && args.payload !== null && !Array.isArray(args.payload)
|
|
@@ -2030,6 +2539,10 @@ class ThreeDPrinterMCPServer {
|
|
|
2030
2539
|
case "blender_mcp_call":
|
|
2031
2540
|
return await this.blender.call(args ?? {}, extra.signal);
|
|
2032
2541
|
|
|
2542
|
+
case "blender_mcp_export_stl":
|
|
2543
|
+
result = await this.blender.exportStl(args ?? {}, extra.signal);
|
|
2544
|
+
break;
|
|
2545
|
+
|
|
2033
2546
|
case "blender_mcp_edit_model":
|
|
2034
2547
|
result = await this.blender.edit(
|
|
2035
2548
|
args ?? {},
|
|
@@ -2060,7 +2573,7 @@ class ThreeDPrinterMCPServer {
|
|
|
2060
2573
|
console.error(`Error calling tool ${name}:`, error);
|
|
2061
2574
|
|
|
2062
2575
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2063
|
-
const structuredError = this.toStructuredToolError(name, errorMessage);
|
|
2576
|
+
const structuredError = this.toStructuredToolError(name, errorMessage, error);
|
|
2064
2577
|
|
|
2065
2578
|
return {
|
|
2066
2579
|
content: [
|
|
@@ -2142,7 +2655,8 @@ class ThreeDPrinterMCPServer {
|
|
|
2142
2655
|
bambuToken: string,
|
|
2143
2656
|
filename: string,
|
|
2144
2657
|
gcode: string,
|
|
2145
|
-
print: boolean
|
|
2658
|
+
print: boolean,
|
|
2659
|
+
options: PrintSafetyOptions = {}
|
|
2146
2660
|
) {
|
|
2147
2661
|
const sourceIsFilePath = fs.existsSync(gcode) && fs.statSync(gcode).isFile();
|
|
2148
2662
|
// The remote filename must never select a local write/delete target.
|
|
@@ -2160,10 +2674,10 @@ class ThreeDPrinterMCPServer {
|
|
|
2160
2674
|
|
|
2161
2675
|
if (type.toLowerCase() === "bambu") {
|
|
2162
2676
|
const bambuApiKey = `${bambuSerial}:${bambuToken}`;
|
|
2163
|
-
return await implementation.uploadFile(host, port, bambuApiKey, tempFilePath, filename, print);
|
|
2677
|
+
return await implementation.uploadFile(host, port, bambuApiKey, tempFilePath, filename, print, options);
|
|
2164
2678
|
}
|
|
2165
2679
|
|
|
2166
|
-
return await implementation.uploadFile(host, port, apiKey, tempFilePath, filename, print);
|
|
2680
|
+
return await implementation.uploadFile(host, port, apiKey, tempFilePath, filename, print, options);
|
|
2167
2681
|
} finally {
|
|
2168
2682
|
if (scratchDir) fs.rmSync(scratchDir, { recursive: true, force: true });
|
|
2169
2683
|
}
|
|
@@ -2176,16 +2690,17 @@ class ThreeDPrinterMCPServer {
|
|
|
2176
2690
|
apiKey: string,
|
|
2177
2691
|
bambuSerial: string,
|
|
2178
2692
|
bambuToken: string,
|
|
2179
|
-
gcodeFilename: string
|
|
2693
|
+
gcodeFilename: string,
|
|
2694
|
+
options: PrintSafetyOptions = {}
|
|
2180
2695
|
) {
|
|
2181
2696
|
const implementation = this.printerFactory.getImplementation(type);
|
|
2182
2697
|
|
|
2183
2698
|
if (type.toLowerCase() === "bambu") {
|
|
2184
2699
|
const bambuApiKey = `${bambuSerial}:${bambuToken}`;
|
|
2185
|
-
return await implementation.startJob(host, port, bambuApiKey, gcodeFilename);
|
|
2700
|
+
return await implementation.startJob(host, port, bambuApiKey, gcodeFilename, options);
|
|
2186
2701
|
}
|
|
2187
2702
|
|
|
2188
|
-
return await implementation.startJob(host, port, apiKey, gcodeFilename);
|
|
2703
|
+
return await implementation.startJob(host, port, apiKey, gcodeFilename, options);
|
|
2189
2704
|
}
|
|
2190
2705
|
|
|
2191
2706
|
async cancelPrint(
|
|
@@ -2214,19 +2729,53 @@ class ThreeDPrinterMCPServer {
|
|
|
2214
2729
|
bambuSerial: string,
|
|
2215
2730
|
bambuToken: string,
|
|
2216
2731
|
component: string,
|
|
2217
|
-
temperature:
|
|
2732
|
+
temperature: unknown,
|
|
2733
|
+
options: PrintSafetyOptions = {}
|
|
2218
2734
|
) {
|
|
2219
2735
|
const implementation = this.printerFactory.getImplementation(type);
|
|
2220
2736
|
|
|
2221
2737
|
if (type.toLowerCase() === "bambu") {
|
|
2222
2738
|
const bambuApiKey = `${bambuSerial}:${bambuToken}`;
|
|
2223
|
-
return implementation.setTemperature(host, port, bambuApiKey, component, temperature);
|
|
2739
|
+
return implementation.setTemperature(host, port, bambuApiKey, component, temperature, options);
|
|
2224
2740
|
}
|
|
2225
2741
|
|
|
2226
|
-
return implementation.setTemperature(host, port, apiKey, component, temperature);
|
|
2742
|
+
return implementation.setTemperature(host, port, apiKey, component, temperature, options);
|
|
2227
2743
|
}
|
|
2228
2744
|
|
|
2229
|
-
private toStructuredToolError(tool: string, message: string): StructuredToolError {
|
|
2745
|
+
private toStructuredToolError(tool: string, message: string, error?: unknown): StructuredToolError {
|
|
2746
|
+
// Slicer failures are local profile/slicer problems, never printer connectivity.
|
|
2747
|
+
if (error instanceof SlicerError) {
|
|
2748
|
+
return {
|
|
2749
|
+
status: "error",
|
|
2750
|
+
retryable: false,
|
|
2751
|
+
suggestion: error.suggestion,
|
|
2752
|
+
message,
|
|
2753
|
+
tool,
|
|
2754
|
+
slicer: { phase: error.phase, ...error.details },
|
|
2755
|
+
};
|
|
2756
|
+
}
|
|
2757
|
+
if (SLICING_TOOLS.has(tool)) {
|
|
2758
|
+
return {
|
|
2759
|
+
status: "error",
|
|
2760
|
+
retryable: false,
|
|
2761
|
+
suggestion: "Check the slicing arguments, template, and profile paths named in the error, then retry.",
|
|
2762
|
+
message,
|
|
2763
|
+
tool,
|
|
2764
|
+
};
|
|
2765
|
+
}
|
|
2766
|
+
if (isCredentialRejection(message)) {
|
|
2767
|
+
return { status: "error", retryable: false, suggestion: credentialSuggestion(), message, tool };
|
|
2768
|
+
}
|
|
2769
|
+
// Safety refusals stop before the printer is touched; retrying unchanged repeats the refusal.
|
|
2770
|
+
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)) {
|
|
2771
|
+
return {
|
|
2772
|
+
status: "error",
|
|
2773
|
+
retryable: false,
|
|
2774
|
+
suggestion: "A safety check stopped this before anything reached the printer. Read the reason, fix the file, arguments, or printer state, and ask again.",
|
|
2775
|
+
message,
|
|
2776
|
+
tool,
|
|
2777
|
+
};
|
|
2778
|
+
}
|
|
2230
2779
|
const isInputError =
|
|
2231
2780
|
message.toLowerCase().includes("missing required") ||
|
|
2232
2781
|
message.toLowerCase().includes("invalid") ||
|