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/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,
|
|
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
|
|
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
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
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: "
|
|
1031
|
+
description: "Explicit process profile that overrides the named template only when provided in this call."
|
|
689
1032
|
},
|
|
690
|
-
|
|
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: "
|
|
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: ["
|
|
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: "
|
|
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
|
|
1090
|
+
description: "Expected highest nozzle target"
|
|
711
1091
|
},
|
|
712
1092
|
bed_temp: {
|
|
713
1093
|
type: "number",
|
|
714
|
-
description: "Expected bed
|
|
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,
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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: "
|
|
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
|
-
|
|
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
|
-
|
|
1313
|
-
|
|
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
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
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 =
|
|
1323
|
-
|
|
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.
|
|
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 (
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
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.
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
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,
|
|
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: "
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
2019
|
+
const plateEntry = zip.file(selectedPlateEntry);
|
|
2020
|
+
shouldAutoSlice = !plateEntry || plateEntry.dir;
|
|
1567
2021
|
}
|
|
1568
2022
|
catch (sliceCheckErr) {
|
|
1569
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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");
|