mcp-3d-printer-server 1.2.5 → 1.2.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +228 -0
- package/CONTRIBUTORS.md +49 -0
- package/README.md +529 -739
- package/dist/3mf_parser.js +8 -8
- package/dist/blender-mcp-bridge.d.ts +11 -0
- package/dist/blender-mcp-bridge.js +661 -0
- package/dist/fulu-orca.js +29 -5
- package/dist/index.js +752 -212
- 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 -120
- 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 +517 -0
- package/dist/safety/printer-state.d.ts +51 -0
- package/dist/safety/printer-state.js +365 -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 +30 -14
- package/server.json +29 -0
- package/src/3mf_parser.ts +8 -8
- package/src/blender-mcp-bridge.ts +611 -0
- package/src/fulu-orca.ts +36 -6
- package/src/index.ts +888 -255
- package/src/printers/bambu-node-guard.ts +44 -0
- package/src/printers/bambu.ts +458 -158
- 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 +354 -0
- package/src/safety/printer-state.ts +339 -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.map +0 -1
package/src/index.ts
CHANGED
|
@@ -15,18 +15,29 @@ import axios from "axios";
|
|
|
15
15
|
import dotenv from "dotenv";
|
|
16
16
|
import fs from "fs";
|
|
17
17
|
import path from "path";
|
|
18
|
+
import os from "node:os";
|
|
19
|
+
import { BlenderMcpBridge } from "./blender-mcp-bridge.js";
|
|
18
20
|
import { createServer as createHttpServer, type IncomingMessage, type Server as HttpServer, type ServerResponse } from "node:http";
|
|
19
21
|
import { randomUUID } from "node:crypto";
|
|
20
22
|
import * as THREE from 'three';
|
|
21
23
|
import { PrinterFactory } from "./printers/printer-factory.js";
|
|
22
|
-
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";
|
|
23
33
|
import { parse3MF, ThreeMFData } from './3mf_parser.js';
|
|
24
34
|
import { BambuImplementation } from "./printers/bambu.js";
|
|
25
35
|
import {
|
|
26
36
|
inspectFuluOrcaSetup,
|
|
27
37
|
invokeFuluBridgeRpc,
|
|
28
|
-
isFuluPrintRpcMethod,
|
|
29
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";
|
|
30
41
|
|
|
31
42
|
// Load environment variables from .env file
|
|
32
43
|
dotenv.config();
|
|
@@ -36,7 +47,7 @@ const DEFAULT_HOST = process.env.PRINTER_HOST || "localhost";
|
|
|
36
47
|
const DEFAULT_PORT = process.env.PRINTER_PORT || "80";
|
|
37
48
|
const DEFAULT_API_KEY = process.env.API_KEY || "";
|
|
38
49
|
const DEFAULT_TYPE = process.env.PRINTER_TYPE || "octoprint"; // Default to OctoPrint
|
|
39
|
-
const TEMP_DIR = process.env.TEMP_DIR || path.join(
|
|
50
|
+
const TEMP_DIR = process.env.TEMP_DIR || fs.mkdtempSync(path.join(os.tmpdir(), "mcp-3d-printer-server-"));
|
|
40
51
|
|
|
41
52
|
// Slicer configuration
|
|
42
53
|
const CANONICAL_SLICER_TYPES = [
|
|
@@ -166,6 +177,211 @@ const BAMBU_MODEL_PRESETS: Record<string, (nozzle: string) => string> = {
|
|
|
166
177
|
h2d: (n) => `Bambu Lab H2D ${n} nozzle`,
|
|
167
178
|
};
|
|
168
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
|
+
|
|
169
385
|
function validateBambuModel(model: string): BambuModel {
|
|
170
386
|
const normalized = model.trim().toLowerCase();
|
|
171
387
|
if (!(VALID_BAMBU_MODELS as readonly string[]).includes(normalized)) {
|
|
@@ -186,6 +402,35 @@ function resolveBedType(bedType: string | undefined): string {
|
|
|
186
402
|
return resolved;
|
|
187
403
|
}
|
|
188
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
|
+
|
|
189
434
|
type RuntimeConfig = {
|
|
190
435
|
transport: "stdio" | "streamable-http";
|
|
191
436
|
httpHost: string;
|
|
@@ -195,6 +440,8 @@ type RuntimeConfig = {
|
|
|
195
440
|
enableJsonResponse: boolean;
|
|
196
441
|
allowedOrigins: Set<string>;
|
|
197
442
|
blenderBridgeCommand?: string;
|
|
443
|
+
allowExecutableArg: boolean;
|
|
444
|
+
allowBridgeCommandArg: boolean;
|
|
198
445
|
};
|
|
199
446
|
|
|
200
447
|
function parseBooleanEnv(rawValue: string | undefined, fallback: boolean): boolean {
|
|
@@ -259,6 +506,8 @@ function readRuntimeConfig(): RuntimeConfig {
|
|
|
259
506
|
enableJsonResponse: parseBooleanEnv(process.env.MCP_HTTP_JSON_RESPONSE, true),
|
|
260
507
|
allowedOrigins: parseCsvEnv(process.env.MCP_HTTP_ALLOWED_ORIGINS),
|
|
261
508
|
blenderBridgeCommand: process.env.BLENDER_MCP_BRIDGE_COMMAND?.trim() || undefined,
|
|
509
|
+
allowExecutableArg: parseBooleanEnv(process.env.MCP_ALLOW_EXECUTABLE_ARG, false),
|
|
510
|
+
allowBridgeCommandArg: parseBooleanEnv(process.env.MCP_ALLOW_BRIDGE_COMMAND_ARG, false),
|
|
262
511
|
};
|
|
263
512
|
}
|
|
264
513
|
|
|
@@ -268,15 +517,32 @@ type StructuredToolError = {
|
|
|
268
517
|
suggestion: string;
|
|
269
518
|
message: string;
|
|
270
519
|
tool: string;
|
|
520
|
+
slicer?: Record<string, unknown>;
|
|
271
521
|
};
|
|
272
522
|
|
|
523
|
+
const SLICING_TOOLS = new Set(["slice_stl", "slice_with_template", "list_templates", "save_template", "get_slice_settings"]);
|
|
524
|
+
|
|
273
525
|
// Ensure temp directory exists
|
|
274
526
|
if (!fs.existsSync(TEMP_DIR)) {
|
|
275
527
|
fs.mkdirSync(TEMP_DIR, { recursive: true });
|
|
276
528
|
}
|
|
277
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
|
+
|
|
278
543
|
class ThreeDPrinterMCPServer {
|
|
279
544
|
private server: Server;
|
|
545
|
+
private readonly blender = new BlenderMcpBridge();
|
|
280
546
|
private printerFactory: PrinterFactory;
|
|
281
547
|
private stlManipulator: STLManipulator;
|
|
282
548
|
private readonly runtimeConfig: RuntimeConfig;
|
|
@@ -287,7 +553,7 @@ class ThreeDPrinterMCPServer {
|
|
|
287
553
|
this.server = new Server(
|
|
288
554
|
{
|
|
289
555
|
name: "mcp-3d-printer-server",
|
|
290
|
-
version: "
|
|
556
|
+
version: JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")).version
|
|
291
557
|
},
|
|
292
558
|
{
|
|
293
559
|
capabilities: {
|
|
@@ -297,13 +563,37 @@ class ThreeDPrinterMCPServer {
|
|
|
297
563
|
}
|
|
298
564
|
);
|
|
299
565
|
|
|
300
|
-
this.printerFactory = new PrinterFactory();
|
|
566
|
+
this.printerFactory = new PrinterFactory((message) => this.confirmHardware(message));
|
|
301
567
|
this.stlManipulator = new STLManipulator(TEMP_DIR);
|
|
302
568
|
|
|
303
569
|
this.setupHandlers();
|
|
304
570
|
this.setupErrorHandling();
|
|
305
571
|
}
|
|
306
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
|
+
|
|
307
597
|
private async resolveBambuModel(argsModel: string | undefined): Promise<string> {
|
|
308
598
|
const fromArgs = (argsModel || DEFAULT_BAMBU_MODEL).trim().toLowerCase();
|
|
309
599
|
if (fromArgs) {
|
|
@@ -337,7 +627,7 @@ class ThreeDPrinterMCPServer {
|
|
|
337
627
|
},
|
|
338
628
|
required: ["bambu_model"],
|
|
339
629
|
},
|
|
340
|
-
});
|
|
630
|
+
}, { timeout: confirmationTimeoutMs() });
|
|
341
631
|
|
|
342
632
|
if (result.action === "accept" && result.content?.bambu_model) {
|
|
343
633
|
return validateBambuModel(String(result.content.bambu_model));
|
|
@@ -348,6 +638,12 @@ class ThreeDPrinterMCPServer {
|
|
|
348
638
|
);
|
|
349
639
|
} catch (elicitError: any) {
|
|
350
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
|
+
}
|
|
351
647
|
if (
|
|
352
648
|
elicitError?.code === -32601 || elicitError?.code === -32600 ||
|
|
353
649
|
msg.includes("does not support") || msg.includes("elicitation")
|
|
@@ -362,6 +658,79 @@ class ThreeDPrinterMCPServer {
|
|
|
362
658
|
}
|
|
363
659
|
}
|
|
364
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
|
+
|
|
365
734
|
setupErrorHandling() {
|
|
366
735
|
this.server.onerror = (error) => {
|
|
367
736
|
console.error("[MCP Error]", error);
|
|
@@ -581,7 +950,7 @@ class ThreeDPrinterMCPServer {
|
|
|
581
950
|
},
|
|
582
951
|
{
|
|
583
952
|
name: "upload_gcode",
|
|
584
|
-
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.",
|
|
585
954
|
inputSchema: {
|
|
586
955
|
type: "object",
|
|
587
956
|
properties: {
|
|
@@ -609,6 +978,11 @@ class ThreeDPrinterMCPServer {
|
|
|
609
978
|
type: "string",
|
|
610
979
|
description: "Access token for Bambu Lab printers (default: value from env)"
|
|
611
980
|
},
|
|
981
|
+
bambu_model: {
|
|
982
|
+
type: "string",
|
|
983
|
+
enum: [...VALID_BAMBU_MODELS],
|
|
984
|
+
description: "Required for Bambu print operations unless BAMBU_MODEL is configured. Must match the printer and pre-sliced G-code."
|
|
985
|
+
},
|
|
612
986
|
filename: {
|
|
613
987
|
type: "string",
|
|
614
988
|
description: "Filename to use on the printer. Defaults to the basename of gcode_path when omitted."
|
|
@@ -623,18 +997,18 @@ class ThreeDPrinterMCPServer {
|
|
|
623
997
|
},
|
|
624
998
|
print: {
|
|
625
999
|
type: "boolean",
|
|
626
|
-
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."
|
|
627
1005
|
}
|
|
628
|
-
}
|
|
629
|
-
anyOf: [
|
|
630
|
-
{ required: ["filename", "gcode"] },
|
|
631
|
-
{ required: ["gcode_path"] }
|
|
632
|
-
]
|
|
1006
|
+
}
|
|
633
1007
|
}
|
|
634
1008
|
},
|
|
635
1009
|
{
|
|
636
1010
|
name: "start_print",
|
|
637
|
-
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.",
|
|
638
1012
|
inputSchema: {
|
|
639
1013
|
type: "object",
|
|
640
1014
|
properties: {
|
|
@@ -662,9 +1036,18 @@ class ThreeDPrinterMCPServer {
|
|
|
662
1036
|
type: "string",
|
|
663
1037
|
description: "Access token for Bambu Lab printers (default: value from env)"
|
|
664
1038
|
},
|
|
1039
|
+
bambu_model: {
|
|
1040
|
+
type: "string",
|
|
1041
|
+
enum: [...VALID_BAMBU_MODELS],
|
|
1042
|
+
description: "Required for Bambu print operations unless BAMBU_MODEL is configured. Must match the printer and pre-sliced G-code."
|
|
1043
|
+
},
|
|
665
1044
|
filename: {
|
|
666
1045
|
type: "string",
|
|
667
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."
|
|
668
1051
|
}
|
|
669
1052
|
},
|
|
670
1053
|
required: ["filename"]
|
|
@@ -705,7 +1088,7 @@ class ThreeDPrinterMCPServer {
|
|
|
705
1088
|
},
|
|
706
1089
|
{
|
|
707
1090
|
name: "set_printer_temperature",
|
|
708
|
-
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.",
|
|
709
1092
|
inputSchema: {
|
|
710
1093
|
type: "object",
|
|
711
1094
|
properties: {
|
|
@@ -739,7 +1122,20 @@ class ThreeDPrinterMCPServer {
|
|
|
739
1122
|
},
|
|
740
1123
|
temperature: {
|
|
741
1124
|
type: "number",
|
|
742
|
-
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."
|
|
743
1139
|
}
|
|
744
1140
|
},
|
|
745
1141
|
required: ["component", "temperature"]
|
|
@@ -766,7 +1162,11 @@ class ThreeDPrinterMCPServer {
|
|
|
766
1162
|
},
|
|
767
1163
|
{
|
|
768
1164
|
name: "slice_stl",
|
|
769
|
-
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.",
|
|
770
1170
|
inputSchema: {
|
|
771
1171
|
type: "object",
|
|
772
1172
|
properties: {
|
|
@@ -774,39 +1174,74 @@ class ThreeDPrinterMCPServer {
|
|
|
774
1174
|
type: "string",
|
|
775
1175
|
description: "Path to the STL or 3MF file to slice"
|
|
776
1176
|
},
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
},
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
description: "Path to the slicer executable (default: value from env)"
|
|
794
|
-
},
|
|
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)." },
|
|
795
1193
|
slicer_profile: {
|
|
796
1194
|
type: "string",
|
|
797
|
-
description: "
|
|
1195
|
+
description: "Explicit process profile that overrides the named template only when provided in this call."
|
|
798
1196
|
},
|
|
799
|
-
|
|
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: {
|
|
800
1208
|
type: "string",
|
|
801
|
-
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."
|
|
802
1225
|
}
|
|
803
1226
|
},
|
|
804
|
-
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
|
+
}
|
|
805
1240
|
}
|
|
806
1241
|
},
|
|
807
1242
|
{
|
|
808
1243
|
name: "confirm_temperatures",
|
|
809
|
-
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.",
|
|
810
1245
|
inputSchema: {
|
|
811
1246
|
type: "object",
|
|
812
1247
|
properties: {
|
|
@@ -816,11 +1251,11 @@ class ThreeDPrinterMCPServer {
|
|
|
816
1251
|
},
|
|
817
1252
|
extruder_temp: {
|
|
818
1253
|
type: "number",
|
|
819
|
-
description: "Expected
|
|
1254
|
+
description: "Expected highest nozzle target"
|
|
820
1255
|
},
|
|
821
1256
|
bed_temp: {
|
|
822
1257
|
type: "number",
|
|
823
|
-
description: "Expected bed
|
|
1258
|
+
description: "Expected highest bed target"
|
|
824
1259
|
}
|
|
825
1260
|
},
|
|
826
1261
|
required: ["gcode_path"]
|
|
@@ -828,7 +1263,7 @@ class ThreeDPrinterMCPServer {
|
|
|
828
1263
|
},
|
|
829
1264
|
{
|
|
830
1265
|
name: "process_and_print_stl",
|
|
831
|
-
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.",
|
|
832
1267
|
inputSchema: {
|
|
833
1268
|
type: "object",
|
|
834
1269
|
properties: {
|
|
@@ -842,11 +1277,15 @@ class ThreeDPrinterMCPServer {
|
|
|
842
1277
|
},
|
|
843
1278
|
extruder_temp: {
|
|
844
1279
|
type: "number",
|
|
845
|
-
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."
|
|
846
1281
|
},
|
|
847
1282
|
bed_temp: {
|
|
848
1283
|
type: "number",
|
|
849
|
-
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."
|
|
850
1289
|
},
|
|
851
1290
|
host: {
|
|
852
1291
|
type: "string",
|
|
@@ -874,6 +1313,11 @@ class ThreeDPrinterMCPServer {
|
|
|
874
1313
|
enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate"],
|
|
875
1314
|
description: "Bed/plate type installed on the printer (default: textured_plate)."
|
|
876
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
|
+
},
|
|
877
1321
|
nozzle_diameter: {
|
|
878
1322
|
type: "string",
|
|
879
1323
|
description: "Nozzle diameter in mm (default: 0.4)."
|
|
@@ -885,7 +1329,7 @@ class ThreeDPrinterMCPServer {
|
|
|
885
1329
|
},
|
|
886
1330
|
slicer_path: {
|
|
887
1331
|
type: "string",
|
|
888
|
-
description: "Path to the slicer executable (default: value from env)."
|
|
1332
|
+
description: "Path to the slicer executable (default: value from env). Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1."
|
|
889
1333
|
},
|
|
890
1334
|
slicer_profile: {
|
|
891
1335
|
type: "string",
|
|
@@ -1080,7 +1524,7 @@ class ThreeDPrinterMCPServer {
|
|
|
1080
1524
|
},
|
|
1081
1525
|
{
|
|
1082
1526
|
name: "print_3mf",
|
|
1083
|
-
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.",
|
|
1084
1528
|
inputSchema: {
|
|
1085
1529
|
type: "object",
|
|
1086
1530
|
properties: {
|
|
@@ -1098,6 +1542,11 @@ class ThreeDPrinterMCPServer {
|
|
|
1098
1542
|
enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate"],
|
|
1099
1543
|
description: "Bed/plate type installed on the printer (default: textured_plate)."
|
|
1100
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
|
+
},
|
|
1101
1550
|
nozzle_diameter: {
|
|
1102
1551
|
type: "string",
|
|
1103
1552
|
description: "Nozzle diameter in mm (default: 0.4)."
|
|
@@ -1130,7 +1579,7 @@ class ThreeDPrinterMCPServer {
|
|
|
1130
1579
|
},
|
|
1131
1580
|
slicer_path: {
|
|
1132
1581
|
type: "string",
|
|
1133
|
-
description: "Path to the slicer executable if auto-slicing is needed."
|
|
1582
|
+
description: "Path to the slicer executable if auto-slicing is needed. Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1."
|
|
1134
1583
|
},
|
|
1135
1584
|
slicer_profile: {
|
|
1136
1585
|
type: "string",
|
|
@@ -1177,15 +1626,23 @@ class ThreeDPrinterMCPServer {
|
|
|
1177
1626
|
properties: {
|
|
1178
1627
|
slicer_path: {
|
|
1179
1628
|
type: "string",
|
|
1180
|
-
description:
|
|
1629
|
+
description:
|
|
1630
|
+
"Path to the FULU OrcaSlicer executable. Defaults from SLICER_PATH/FULU_ORCA_PATH. " +
|
|
1631
|
+
"When run_bridge_probe=true, requires MCP_ALLOW_EXECUTABLE_ARG=1 to be accepted here."
|
|
1181
1632
|
},
|
|
1182
1633
|
plugin_dir: {
|
|
1183
1634
|
type: "string",
|
|
1184
|
-
description:
|
|
1635
|
+
description:
|
|
1636
|
+
"Directory containing the FULU Bambu runtime payload; on macOS this is usually " +
|
|
1637
|
+
"OrcaSlicer.app/Contents/MacOS. When run_bridge_probe=true, requires " +
|
|
1638
|
+
"MCP_ALLOW_EXECUTABLE_ARG=1 to be accepted here."
|
|
1185
1639
|
},
|
|
1186
1640
|
runtime_dir: {
|
|
1187
1641
|
type: "string",
|
|
1188
|
-
description:
|
|
1642
|
+
description:
|
|
1643
|
+
"Installed runtime directory. On macOS this defaults to ~/Library/Application " +
|
|
1644
|
+
"Support/OrcaSlicer/macos-bridge/runtime. When run_bridge_probe=true, requires " +
|
|
1645
|
+
"MCP_ALLOW_EXECUTABLE_ARG=1 to be accepted here."
|
|
1189
1646
|
},
|
|
1190
1647
|
platform: {
|
|
1191
1648
|
type: "string",
|
|
@@ -1194,7 +1651,10 @@ class ThreeDPrinterMCPServer {
|
|
|
1194
1651
|
},
|
|
1195
1652
|
bridge_command: {
|
|
1196
1653
|
type: "string",
|
|
1197
|
-
description:
|
|
1654
|
+
description:
|
|
1655
|
+
"Command that starts the FULU BambuNetwork bridge host for probing. Read from " +
|
|
1656
|
+
"FULU_BAMBU_BRIDGE_COMMAND by default; requires MCP_ALLOW_EXECUTABLE_ARG=1 " +
|
|
1657
|
+
"to be accepted here."
|
|
1198
1658
|
},
|
|
1199
1659
|
run_bridge_probe: {
|
|
1200
1660
|
type: "boolean",
|
|
@@ -1210,13 +1670,16 @@ class ThreeDPrinterMCPServer {
|
|
|
1210
1670
|
{
|
|
1211
1671
|
name: "fulu_bambu_network_rpc",
|
|
1212
1672
|
description:
|
|
1213
|
-
"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.",
|
|
1214
1674
|
inputSchema: {
|
|
1215
1675
|
type: "object",
|
|
1216
1676
|
properties: {
|
|
1217
1677
|
bridge_command: {
|
|
1218
1678
|
type: "string",
|
|
1219
|
-
description:
|
|
1679
|
+
description:
|
|
1680
|
+
"Command that starts the FULU BambuNetwork bridge host. Defaults to " +
|
|
1681
|
+
"FULU_BAMBU_BRIDGE_COMMAND; requires MCP_ALLOW_EXECUTABLE_ARG=1 to be " +
|
|
1682
|
+
"accepted here."
|
|
1220
1683
|
},
|
|
1221
1684
|
method: {
|
|
1222
1685
|
type: "string",
|
|
@@ -1233,12 +1696,12 @@ class ThreeDPrinterMCPServer {
|
|
|
1233
1696
|
},
|
|
1234
1697
|
allow_mutating_method: {
|
|
1235
1698
|
type: "boolean",
|
|
1236
|
-
description: "Required for methods
|
|
1699
|
+
description: "Required for the allowlisted agent/session setup methods. It never enables print, printer-message, or unknown methods."
|
|
1237
1700
|
},
|
|
1238
1701
|
bambu_model: {
|
|
1239
1702
|
type: "string",
|
|
1240
1703
|
enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
|
|
1241
|
-
description: "
|
|
1704
|
+
description: "Informational only. Raw FULU print RPC methods are disabled; use print_3mf."
|
|
1242
1705
|
}
|
|
1243
1706
|
},
|
|
1244
1707
|
required: ["method"]
|
|
@@ -1290,31 +1753,73 @@ class ThreeDPrinterMCPServer {
|
|
|
1290
1753
|
required: ["stl_path"]
|
|
1291
1754
|
}
|
|
1292
1755
|
},
|
|
1756
|
+
{
|
|
1757
|
+
name: "blender_mcp_status",
|
|
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.",
|
|
1759
|
+
inputSchema: {
|
|
1760
|
+
type: "object",
|
|
1761
|
+
properties: {
|
|
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)." },
|
|
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." }
|
|
1766
|
+
},
|
|
1767
|
+
additionalProperties: false
|
|
1768
|
+
}
|
|
1769
|
+
},
|
|
1770
|
+
{
|
|
1771
|
+
name: "blender_mcp_call",
|
|
1772
|
+
description: "Call a discovered tool on the configured Blender MCP server, preserving its full MCP content and errors. Discover tool schemas with blender_mcp_status first; execute_blender_code accepts Python code and user_prompt. Calls can modify the active Blender scene and are never automatically retried.",
|
|
1773
|
+
inputSchema: {
|
|
1774
|
+
type: "object",
|
|
1775
|
+
properties: {
|
|
1776
|
+
tool_name: { type: "string", description: "Exact name advertised by Blender MCP, such as get_scene_info or execute_blender_code." },
|
|
1777
|
+
arguments: { type: "object", description: "Arguments matching the remote tool's discovered input schema. Preserve the user's own words in user_prompt when the remote tool requests it." },
|
|
1778
|
+
timeout_ms: { type: "integer", minimum: 100, maximum: 300000, description: "Total connection, discovery, and tool deadline in milliseconds; defaults to BLENDER_MCP_TIMEOUT_MS or 120000." }
|
|
1779
|
+
},
|
|
1780
|
+
required: ["tool_name"],
|
|
1781
|
+
additionalProperties: false
|
|
1782
|
+
}
|
|
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
|
+
},
|
|
1293
1801
|
{
|
|
1294
1802
|
name: "blender_mcp_edit_model",
|
|
1295
|
-
description: "
|
|
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.",
|
|
1296
1804
|
inputSchema: {
|
|
1297
1805
|
type: "object",
|
|
1298
1806
|
properties: {
|
|
1299
|
-
stl_path: {
|
|
1300
|
-
type: "string",
|
|
1301
|
-
description: "Path to the local STL file."
|
|
1302
|
-
},
|
|
1807
|
+
stl_path: { type: "string", description: "Path to the local STL file" },
|
|
1303
1808
|
operations: {
|
|
1304
1809
|
type: "array",
|
|
1305
|
-
description: "Ordered
|
|
1810
|
+
description: "Ordered operations: decimate:<ratio greater than 0 and at most 1>, remesh:<positive voxel size in STL units>, boolean_union:<STL path>. Legacy custom bridges define their own operations.",
|
|
1811
|
+
minItems: 1,
|
|
1812
|
+
maxItems: 64,
|
|
1306
1813
|
items: { type: "string" }
|
|
1307
1814
|
},
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
},
|
|
1312
|
-
execute: {
|
|
1313
|
-
type: "boolean",
|
|
1314
|
-
description: "When true, attempts to execute bridge command; otherwise returns a prepared payload only."
|
|
1315
|
-
}
|
|
1815
|
+
output_path: { type: "string", description: "New local STL output path for standard MCP editing; defaults to a unique model-edited-<id>.stl beside the input. Its parent must exist and existing files are never overwritten. Reuse the preview's output_path when executing that plan." },
|
|
1816
|
+
user_prompt: { type: "string", description: "The user's own words describing the edit, passed unchanged to Blender MCP." },
|
|
1817
|
+
timeout_ms: { type: "integer", minimum: 100, maximum: 300000, description: "Total Blender request deadline in milliseconds; defaults to BLENDER_MCP_TIMEOUT_MS or 120000." },
|
|
1818
|
+
bridge_command: { type: "string", description: "Legacy custom bridge executable override, not a standard MCP command. Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1." },
|
|
1819
|
+
execute: { type: "boolean", description: "Apply edits and export (true) or validate and return the prepared request without connecting (false, default)." }
|
|
1316
1820
|
},
|
|
1317
|
-
required: ["stl_path", "operations"]
|
|
1821
|
+
required: ["stl_path", "operations"],
|
|
1822
|
+
additionalProperties: false
|
|
1318
1823
|
}
|
|
1319
1824
|
}
|
|
1320
1825
|
]
|
|
@@ -1322,7 +1827,7 @@ class ThreeDPrinterMCPServer {
|
|
|
1322
1827
|
});
|
|
1323
1828
|
|
|
1324
1829
|
// Handle tool calls
|
|
1325
|
-
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1830
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
1326
1831
|
const { name, arguments: args } = request.params;
|
|
1327
1832
|
|
|
1328
1833
|
// Set default values for common parameters
|
|
@@ -1333,7 +1838,6 @@ class ThreeDPrinterMCPServer {
|
|
|
1333
1838
|
const bambuSerial = String(args?.bambu_serial || DEFAULT_BAMBU_SERIAL);
|
|
1334
1839
|
const bambuToken = String(args?.bambu_token || DEFAULT_BAMBU_TOKEN);
|
|
1335
1840
|
const slicerType = normalizeSlicerType(String(args?.slicer_type || DEFAULT_SLICER_TYPE));
|
|
1336
|
-
const slicerPath = String(args?.slicer_path || DEFAULT_SLICER_PATH);
|
|
1337
1841
|
const slicerProfile = String(args?.slicer_profile || DEFAULT_SLICER_PROFILE);
|
|
1338
1842
|
const filamentProfile = String(args?.filament_profile || DEFAULT_FILAMENT_PROFILE);
|
|
1339
1843
|
|
|
@@ -1359,13 +1863,21 @@ class ThreeDPrinterMCPServer {
|
|
|
1359
1863
|
if (!args?.filename && !args?.gcode_path) {
|
|
1360
1864
|
throw new Error("Missing required parameter: filename");
|
|
1361
1865
|
}
|
|
1866
|
+
if (args.print !== undefined && typeof args.print !== "boolean") {
|
|
1867
|
+
throw new Error("print must be a boolean.");
|
|
1868
|
+
}
|
|
1869
|
+
const uploadOptions: PrintSafetyOptions = { material: optionalMaterialArg(args?.material) };
|
|
1870
|
+
if (type.toLowerCase() === "bambu" && args.print === true) {
|
|
1871
|
+
uploadOptions.bambuModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
1872
|
+
}
|
|
1362
1873
|
const uploadFilename = String(args?.filename || path.basename(String(args.gcode_path)));
|
|
1363
1874
|
const uploadGcodeSource = String(args?.gcode_path || args.gcode);
|
|
1364
1875
|
result = await this.uploadGcode(
|
|
1365
1876
|
host, port, type, apiKey, bambuSerial, bambuToken,
|
|
1366
1877
|
uploadFilename,
|
|
1367
1878
|
uploadGcodeSource,
|
|
1368
|
-
Boolean(args.print || false)
|
|
1879
|
+
Boolean(args.print || false),
|
|
1880
|
+
uploadOptions
|
|
1369
1881
|
);
|
|
1370
1882
|
break;
|
|
1371
1883
|
|
|
@@ -1373,7 +1885,11 @@ class ThreeDPrinterMCPServer {
|
|
|
1373
1885
|
if (!args?.filename) {
|
|
1374
1886
|
throw new Error("Missing required parameter: filename");
|
|
1375
1887
|
}
|
|
1376
|
-
|
|
1888
|
+
const startOptions: PrintSafetyOptions = { material: optionalMaterialArg(args?.material) };
|
|
1889
|
+
if (type.toLowerCase() === "bambu") {
|
|
1890
|
+
startOptions.bambuModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
1891
|
+
}
|
|
1892
|
+
result = await this.startPrint(host, port, type, apiKey, bambuSerial, bambuToken, String(args.filename), startOptions);
|
|
1377
1893
|
break;
|
|
1378
1894
|
|
|
1379
1895
|
case "cancel_print":
|
|
@@ -1384,10 +1900,19 @@ class ThreeDPrinterMCPServer {
|
|
|
1384
1900
|
if (!args?.component || args?.temperature === undefined) {
|
|
1385
1901
|
throw new Error("Missing required parameters: component and temperature");
|
|
1386
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
|
+
}
|
|
1387
1911
|
result = await this.setPrinterTemperature(
|
|
1388
1912
|
host, port, type, apiKey, bambuSerial, bambuToken,
|
|
1389
|
-
String(args.component),
|
|
1390
|
-
|
|
1913
|
+
String(args.component),
|
|
1914
|
+
requestedTemperature,
|
|
1915
|
+
heatOptions
|
|
1391
1916
|
);
|
|
1392
1917
|
break;
|
|
1393
1918
|
|
|
@@ -1402,29 +1927,47 @@ class ThreeDPrinterMCPServer {
|
|
|
1402
1927
|
);
|
|
1403
1928
|
break;
|
|
1404
1929
|
|
|
1405
|
-
case "slice_stl":
|
|
1406
|
-
|
|
1407
|
-
|
|
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");
|
|
1408
1937
|
}
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
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");
|
|
1415
1948
|
}
|
|
1416
|
-
result =
|
|
1417
|
-
String(args.
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
slicerProfile || undefined,
|
|
1421
|
-
undefined, // progressCallback
|
|
1422
|
-
slicePreset,
|
|
1423
|
-
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
|
|
1424
1953
|
);
|
|
1425
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;
|
|
1426
1969
|
}
|
|
1427
|
-
|
|
1970
|
+
|
|
1428
1971
|
case "confirm_temperatures":
|
|
1429
1972
|
if (!args?.gcode_path) {
|
|
1430
1973
|
throw new Error("Missing required parameter: gcode_path");
|
|
@@ -1443,35 +1986,56 @@ class ThreeDPrinterMCPServer {
|
|
|
1443
1986
|
throw new Error("Missing required parameters: stl_path and extension_inches");
|
|
1444
1987
|
}
|
|
1445
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
|
+
|
|
1446
2003
|
// Define progress callback for UI updates
|
|
1447
2004
|
const processProgressCallback = (progress: number, message?: string) => {
|
|
1448
|
-
console.
|
|
2005
|
+
console.error(`Process progress: ${progress}% - ${message || ''}`);
|
|
1449
2006
|
};
|
|
1450
|
-
|
|
2007
|
+
|
|
1451
2008
|
// 1. Extend the base of the STL file
|
|
1452
2009
|
const extendedStlPath = await this.stlManipulator.extendBase(
|
|
1453
2010
|
String(args.stl_path),
|
|
1454
2011
|
Number(args.extension_inches),
|
|
1455
2012
|
processProgressCallback
|
|
1456
2013
|
);
|
|
1457
|
-
|
|
2014
|
+
|
|
1458
2015
|
// 2. Slice the extended STL file
|
|
1459
2016
|
let processPreset: string | undefined;
|
|
1460
|
-
if (
|
|
1461
|
-
|
|
1462
|
-
const processNozzle = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
1463
|
-
if (isBambuProjectSlicer(slicerType)) {
|
|
1464
|
-
processPreset = BAMBU_MODEL_PRESETS[processModel]?.(processNozzle);
|
|
1465
|
-
}
|
|
2017
|
+
if (processModel && isBambuProjectSlicer(slicerType)) {
|
|
2018
|
+
processPreset = BAMBU_MODEL_PRESETS[processModel]?.(processNozzle);
|
|
1466
2019
|
}
|
|
1467
2020
|
const gcodePath = await this.stlManipulator.sliceSTL(
|
|
1468
2021
|
extendedStlPath,
|
|
1469
2022
|
slicerType,
|
|
1470
|
-
|
|
2023
|
+
this.resolveExecutableSelectorArg(
|
|
2024
|
+
args?.slicer_path,
|
|
2025
|
+
"process_and_print_stl",
|
|
2026
|
+
"slicer_path"
|
|
2027
|
+
) ?? DEFAULT_SLICER_PATH,
|
|
1471
2028
|
slicerProfile || undefined,
|
|
1472
2029
|
processProgressCallback,
|
|
1473
2030
|
processPreset,
|
|
1474
|
-
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
|
|
1475
2039
|
);
|
|
1476
2040
|
|
|
1477
2041
|
if (type.toLowerCase() === 'bambu' && gcodePath.toLowerCase().endsWith(".3mf")) {
|
|
@@ -1502,6 +2066,9 @@ class ThreeDPrinterMCPServer {
|
|
|
1502
2066
|
result = await factoryImplementation.print3mf(host, bambuSerial, bambuToken, {
|
|
1503
2067
|
projectName: path.basename(gcodePath).replace(/\.3mf$/i, ''),
|
|
1504
2068
|
filePath: gcodePath,
|
|
2069
|
+
bambuModel: processModel,
|
|
2070
|
+
nozzleDiameters: [Number(processNozzle)],
|
|
2071
|
+
expectedPeaks,
|
|
1505
2072
|
plateIndex: 0,
|
|
1506
2073
|
...amsOptions,
|
|
1507
2074
|
bedType: printBedType,
|
|
@@ -1524,38 +2091,26 @@ class ThreeDPrinterMCPServer {
|
|
|
1524
2091
|
);
|
|
1525
2092
|
}
|
|
1526
2093
|
|
|
1527
|
-
// 3.
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
extruder: args.extruder_temp !== undefined ? Number(args.extruder_temp) : undefined,
|
|
1533
|
-
bed: args.bed_temp !== undefined ? Number(args.bed_temp) : undefined
|
|
1534
|
-
},
|
|
1535
|
-
processProgressCallback
|
|
1536
|
-
);
|
|
1537
|
-
|
|
1538
|
-
if (!tempConfirmation.match) {
|
|
1539
|
-
console.warn("Temperature mismatch:", tempConfirmation);
|
|
1540
|
-
}
|
|
1541
|
-
}
|
|
1542
|
-
|
|
1543
|
-
// 4. Upload the G-code file to the printer
|
|
1544
|
-
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.
|
|
1545
2099
|
const filename = path.basename(gcodePath);
|
|
1546
|
-
|
|
1547
|
-
await this.uploadGcode(
|
|
2100
|
+
const printResult = await this.uploadGcode(
|
|
1548
2101
|
host, port, type, apiKey, bambuSerial, bambuToken,
|
|
1549
|
-
filename,
|
|
1550
|
-
|
|
1551
|
-
true // Start printing immediately
|
|
2102
|
+
filename,
|
|
2103
|
+
gcodePath,
|
|
2104
|
+
true, // Start printing immediately
|
|
2105
|
+
{ bambuModel: processModel, material: processMaterial, expectedPeaks }
|
|
1552
2106
|
);
|
|
1553
|
-
|
|
2107
|
+
|
|
1554
2108
|
result = {
|
|
1555
2109
|
extended_stl_path: extendedStlPath,
|
|
1556
2110
|
gcode_path: gcodePath,
|
|
1557
2111
|
filename,
|
|
1558
|
-
status: "
|
|
2112
|
+
status: "Checked print job started",
|
|
2113
|
+
print: printResult,
|
|
1559
2114
|
};
|
|
1560
2115
|
break;
|
|
1561
2116
|
|
|
@@ -1575,7 +2130,7 @@ class ThreeDPrinterMCPServer {
|
|
|
1575
2130
|
|
|
1576
2131
|
// Define progress callback for UI updates
|
|
1577
2132
|
const scaleProgressCallback = (progress: number, message?: string) => {
|
|
1578
|
-
console.
|
|
2133
|
+
console.error(`Scale progress: ${progress}% - ${message || ''}`);
|
|
1579
2134
|
};
|
|
1580
2135
|
|
|
1581
2136
|
let scaleFactors: number | [number, number, number];
|
|
@@ -1607,7 +2162,7 @@ class ThreeDPrinterMCPServer {
|
|
|
1607
2162
|
|
|
1608
2163
|
// Define progress callback for UI updates
|
|
1609
2164
|
const rotateProgressCallback = (progress: number, message?: string) => {
|
|
1610
|
-
console.
|
|
2165
|
+
console.error(`Rotate progress: ${progress}% - ${message || ''}`);
|
|
1611
2166
|
};
|
|
1612
2167
|
|
|
1613
2168
|
// Get rotation angles, defaulting to 0 for any undefined axis
|
|
@@ -1631,7 +2186,7 @@ class ThreeDPrinterMCPServer {
|
|
|
1631
2186
|
|
|
1632
2187
|
// Define progress callback for UI updates
|
|
1633
2188
|
const translateProgressCallback = (progress: number, message?: string) => {
|
|
1634
|
-
console.
|
|
2189
|
+
console.error(`Translate progress: ${progress}% - ${message || ''}`);
|
|
1635
2190
|
};
|
|
1636
2191
|
|
|
1637
2192
|
// Get translation values, defaulting to 0 for any undefined axis
|
|
@@ -1655,7 +2210,7 @@ class ThreeDPrinterMCPServer {
|
|
|
1655
2210
|
|
|
1656
2211
|
// Define progress callback for UI updates
|
|
1657
2212
|
const modifySectionProgressCallback = (progress: number, message?: string) => {
|
|
1658
|
-
console.
|
|
2213
|
+
console.error(`Modify section progress: ${progress}% - ${message || ''}`);
|
|
1659
2214
|
};
|
|
1660
2215
|
|
|
1661
2216
|
// Determine the section to modify
|
|
@@ -1732,7 +2287,7 @@ class ThreeDPrinterMCPServer {
|
|
|
1732
2287
|
|
|
1733
2288
|
// Define progress callback for UI updates
|
|
1734
2289
|
const visualizationProgressCallback = (progress: number, message?: string) => {
|
|
1735
|
-
console.
|
|
2290
|
+
console.error(`Visualization progress: ${progress}% - ${message || ''}`);
|
|
1736
2291
|
};
|
|
1737
2292
|
|
|
1738
2293
|
// Get width and height parameters, with defaults
|
|
@@ -1760,35 +2315,66 @@ class ThreeDPrinterMCPServer {
|
|
|
1760
2315
|
|
|
1761
2316
|
const printModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
1762
2317
|
const printBedType = resolveBedType(args?.bed_type as string | undefined);
|
|
1763
|
-
const
|
|
2318
|
+
const explicitPrintNozzle = optionalNozzleDiameterArg(args?.nozzle_diameter);
|
|
2319
|
+
const printNozzle = String(explicitPrintNozzle ?? DEFAULT_NOZZLE_DIAMETER);
|
|
1764
2320
|
const printPreset = BAMBU_MODEL_PRESETS[printModel]?.(printNozzle);
|
|
1765
2321
|
|
|
1766
2322
|
let threeMFPath = String(args.three_mf_path);
|
|
1767
2323
|
|
|
1768
|
-
//
|
|
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";
|
|
2327
|
+
let shouldAutoSlice = false;
|
|
1769
2328
|
try {
|
|
1770
2329
|
const JSZip = (await import('jszip')).default;
|
|
1771
2330
|
const zipData = fs.readFileSync(threeMFPath);
|
|
1772
2331
|
const zip = await JSZip.loadAsync(zipData);
|
|
1773
|
-
const
|
|
1774
|
-
|
|
2332
|
+
const plateEntry = zip.file(selectedPlateEntry);
|
|
2333
|
+
shouldAutoSlice = !plateEntry || plateEntry.dir;
|
|
2334
|
+
} catch (sliceCheckErr: any) {
|
|
2335
|
+
throw new Error(
|
|
2336
|
+
`Cannot read 3MF archive ${threeMFPath}; nothing was uploaded or started: ${sliceCheckErr?.message ?? String(sliceCheckErr)}`
|
|
1775
2337
|
);
|
|
1776
|
-
|
|
1777
|
-
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
// Only treat slicer_path as an executable selector when a slicer
|
|
2341
|
+
// will actually launch. Ready-to-print archives ignore it.
|
|
2342
|
+
if (shouldAutoSlice) {
|
|
2343
|
+
const printSlicerPath =
|
|
2344
|
+
this.resolveExecutableSelectorArg(
|
|
2345
|
+
args?.slicer_path,
|
|
2346
|
+
"print_3mf",
|
|
2347
|
+
"slicer_path"
|
|
2348
|
+
) ?? DEFAULT_SLICER_PATH;
|
|
2349
|
+
try {
|
|
2350
|
+
console.error(`3MF has no gcode — auto-slicing with ${slicerType} for ${printModel}`);
|
|
1778
2351
|
threeMFPath = await this.stlManipulator.sliceSTL(
|
|
1779
2352
|
threeMFPath,
|
|
1780
2353
|
slicerType,
|
|
1781
|
-
|
|
2354
|
+
printSlicerPath,
|
|
1782
2355
|
slicerProfile || undefined,
|
|
1783
2356
|
undefined, // progressCallback
|
|
1784
2357
|
printPreset,
|
|
1785
|
-
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) } : {}) }
|
|
1786
2361
|
);
|
|
1787
|
-
console.
|
|
2362
|
+
console.error("Auto-sliced to: " + threeMFPath);
|
|
2363
|
+
} catch (sliceErr: any) {
|
|
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)}`);
|
|
1788
2373
|
}
|
|
1789
|
-
} catch (sliceCheckErr: any) {
|
|
1790
|
-
console.warn("Could not check/slice 3MF, proceeding with original:", sliceCheckErr.message);
|
|
1791
2374
|
}
|
|
2375
|
+
// Compare the sliced file with an explicit or auto-slice nozzle request.
|
|
2376
|
+
const requestedPrintNozzles =
|
|
2377
|
+
explicitPrintNozzle !== undefined || shouldAutoSlice ? [Number(printNozzle)] : undefined;
|
|
1792
2378
|
|
|
1793
2379
|
// Define variables needed outside the parse try block
|
|
1794
2380
|
let implementation: BambuImplementation;
|
|
@@ -1799,7 +2385,7 @@ class ThreeDPrinterMCPServer {
|
|
|
1799
2385
|
try {
|
|
1800
2386
|
// --- Parse 3MF ---
|
|
1801
2387
|
const parsed3MFData = await parse3MF(threeMFPath);
|
|
1802
|
-
console.
|
|
2388
|
+
console.error(`Successfully parsed 3MF file: ${threeMFPath}`);
|
|
1803
2389
|
const amsOptions = this.resolveAmsPrintOptions(
|
|
1804
2390
|
parsed3MFData,
|
|
1805
2391
|
args as Record<string, unknown> | undefined
|
|
@@ -1817,6 +2403,8 @@ class ThreeDPrinterMCPServer {
|
|
|
1817
2403
|
|
|
1818
2404
|
printOptions = { // Assign to outer scope variable
|
|
1819
2405
|
...amsOptions,
|
|
2406
|
+
bambuModel: printModel,
|
|
2407
|
+
nozzleDiameters: requestedPrintNozzles,
|
|
1820
2408
|
bedType: printBedType,
|
|
1821
2409
|
bedLeveling: args?.bed_leveling !== undefined ? Boolean(args.bed_leveling) : undefined,
|
|
1822
2410
|
flowCalibration: args?.flow_calibration !== undefined ? Boolean(args.flow_calibration) : undefined,
|
|
@@ -1839,7 +2427,7 @@ class ThreeDPrinterMCPServer {
|
|
|
1839
2427
|
plateIndex: 0,
|
|
1840
2428
|
...printOptions // Spread the final options
|
|
1841
2429
|
});
|
|
1842
|
-
|
|
2430
|
+
// Report what the printer did, not merely that a command was published.
|
|
1843
2431
|
} catch (printError) {
|
|
1844
2432
|
console.error(`Error starting 3MF print for ${threeMfFilename}:`, printError);
|
|
1845
2433
|
throw new Error(`Failed to start print: ${(printError as Error).message}`);
|
|
@@ -1849,13 +2437,38 @@ class ThreeDPrinterMCPServer {
|
|
|
1849
2437
|
}
|
|
1850
2438
|
|
|
1851
2439
|
case "check_fulu_orca_setup": {
|
|
2440
|
+
const runBridgeProbe = Boolean(args?.run_bridge_probe ?? false);
|
|
1852
2441
|
result = await inspectFuluOrcaSetup({
|
|
1853
|
-
slicerPath:
|
|
1854
|
-
|
|
1855
|
-
|
|
2442
|
+
slicerPath: this.resolveExecutableSelectorArg(
|
|
2443
|
+
args?.slicer_path,
|
|
2444
|
+
"check_fulu_orca_setup",
|
|
2445
|
+
"slicer_path",
|
|
2446
|
+
runBridgeProbe,
|
|
2447
|
+
true
|
|
2448
|
+
),
|
|
2449
|
+
pluginDir: this.resolveExecutableSelectorArg(
|
|
2450
|
+
args?.plugin_dir,
|
|
2451
|
+
"check_fulu_orca_setup",
|
|
2452
|
+
"plugin_dir",
|
|
2453
|
+
runBridgeProbe,
|
|
2454
|
+
true
|
|
2455
|
+
),
|
|
2456
|
+
runtimeDir: this.resolveExecutableSelectorArg(
|
|
2457
|
+
args?.runtime_dir,
|
|
2458
|
+
"check_fulu_orca_setup",
|
|
2459
|
+
"runtime_dir",
|
|
2460
|
+
runBridgeProbe,
|
|
2461
|
+
true
|
|
2462
|
+
),
|
|
1856
2463
|
platform: args?.platform !== undefined ? String(args.platform) : undefined,
|
|
1857
|
-
bridgeCommand:
|
|
1858
|
-
|
|
2464
|
+
bridgeCommand: this.resolveExecutableSelectorArg(
|
|
2465
|
+
args?.bridge_command,
|
|
2466
|
+
"check_fulu_orca_setup",
|
|
2467
|
+
"bridge_command",
|
|
2468
|
+
true,
|
|
2469
|
+
true
|
|
2470
|
+
),
|
|
2471
|
+
runBridgeProbe,
|
|
1859
2472
|
probeTimeoutMs:
|
|
1860
2473
|
args?.probe_timeout_ms !== undefined ? Number(args.probe_timeout_ms) : undefined,
|
|
1861
2474
|
});
|
|
@@ -1867,11 +2480,11 @@ class ThreeDPrinterMCPServer {
|
|
|
1867
2480
|
throw new Error("Missing required parameter: method");
|
|
1868
2481
|
}
|
|
1869
2482
|
|
|
2483
|
+
// Raw print methods, printer messages and unknown methods are refused
|
|
2484
|
+
// inside invokeFuluBridgeRpc before any bridge process starts.
|
|
1870
2485
|
const method = String(args.method);
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
rpcBambuModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
1874
|
-
}
|
|
2486
|
+
const rpcBambuModel =
|
|
2487
|
+
typeof args?.bambu_model === "string" && args.bambu_model.trim() ? args.bambu_model.trim() : undefined;
|
|
1875
2488
|
|
|
1876
2489
|
const payload =
|
|
1877
2490
|
typeof args.payload === "object" && args.payload !== null && !Array.isArray(args.payload)
|
|
@@ -1879,8 +2492,13 @@ class ThreeDPrinterMCPServer {
|
|
|
1879
2492
|
: undefined;
|
|
1880
2493
|
|
|
1881
2494
|
result = await invokeFuluBridgeRpc({
|
|
1882
|
-
bridgeCommand:
|
|
1883
|
-
args?.bridge_command
|
|
2495
|
+
bridgeCommand: this.resolveExecutableSelectorArg(
|
|
2496
|
+
args?.bridge_command,
|
|
2497
|
+
"fulu_bambu_network_rpc",
|
|
2498
|
+
"bridge_command",
|
|
2499
|
+
true,
|
|
2500
|
+
true
|
|
2501
|
+
),
|
|
1884
2502
|
method,
|
|
1885
2503
|
payload,
|
|
1886
2504
|
timeoutMs: args?.timeout_ms !== undefined ? Number(args.timeout_ms) : undefined,
|
|
@@ -1914,19 +2532,31 @@ class ThreeDPrinterMCPServer {
|
|
|
1914
2532
|
result = await this.stlManipulator.layFlat(String(args.stl_path));
|
|
1915
2533
|
break;
|
|
1916
2534
|
|
|
2535
|
+
case "blender_mcp_status":
|
|
2536
|
+
result = await this.blender.status(args ?? {}, extra.signal);
|
|
2537
|
+
break;
|
|
2538
|
+
|
|
2539
|
+
case "blender_mcp_call":
|
|
2540
|
+
return await this.blender.call(args ?? {}, extra.signal);
|
|
2541
|
+
|
|
2542
|
+
case "blender_mcp_export_stl":
|
|
2543
|
+
result = await this.blender.exportStl(args ?? {}, extra.signal);
|
|
2544
|
+
break;
|
|
2545
|
+
|
|
1917
2546
|
case "blender_mcp_edit_model":
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
2547
|
+
result = await this.blender.edit(
|
|
2548
|
+
args ?? {},
|
|
2549
|
+
this.resolveExecutableSelectorArg(
|
|
2550
|
+
args?.bridge_command,
|
|
2551
|
+
"blender_mcp_edit_model",
|
|
2552
|
+
"bridge_command",
|
|
2553
|
+
true,
|
|
2554
|
+
true
|
|
2555
|
+
) ?? (!process.env.BLENDER_MCP_COMMAND?.trim() ? this.runtimeConfig.blenderBridgeCommand : undefined),
|
|
2556
|
+
extra.signal
|
|
2557
|
+
);
|
|
1928
2558
|
break;
|
|
1929
|
-
|
|
2559
|
+
|
|
1930
2560
|
default:
|
|
1931
2561
|
throw new Error(`Unknown tool: ${name}`);
|
|
1932
2562
|
}
|
|
@@ -1943,7 +2573,7 @@ class ThreeDPrinterMCPServer {
|
|
|
1943
2573
|
console.error(`Error calling tool ${name}:`, error);
|
|
1944
2574
|
|
|
1945
2575
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1946
|
-
const structuredError = this.toStructuredToolError(name, errorMessage);
|
|
2576
|
+
const structuredError = this.toStructuredToolError(name, errorMessage, error);
|
|
1947
2577
|
|
|
1948
2578
|
return {
|
|
1949
2579
|
content: [
|
|
@@ -2025,29 +2655,31 @@ class ThreeDPrinterMCPServer {
|
|
|
2025
2655
|
bambuToken: string,
|
|
2026
2656
|
filename: string,
|
|
2027
2657
|
gcode: string,
|
|
2028
|
-
print: boolean
|
|
2658
|
+
print: boolean,
|
|
2659
|
+
options: PrintSafetyOptions = {}
|
|
2029
2660
|
) {
|
|
2030
2661
|
const sourceIsFilePath = fs.existsSync(gcode) && fs.statSync(gcode).isFile();
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
if (!sourceIsFilePath) {
|
|
2034
|
-
|
|
2035
|
-
fs.writeFileSync(tempFilePath, gcode);
|
|
2662
|
+
// The remote filename must never select a local write/delete target.
|
|
2663
|
+
const localFilename = path.basename(filename);
|
|
2664
|
+
if (!sourceIsFilePath && (!localFilename || localFilename === "." || localFilename === "..")) {
|
|
2665
|
+
throw new Error("filename must include a file basename.");
|
|
2036
2666
|
}
|
|
2667
|
+
const scratchDir = sourceIsFilePath ? undefined : fs.mkdtempSync(path.join(TEMP_DIR, "upload-"));
|
|
2668
|
+
// Multipart adapters infer their file-part name from this safe basename.
|
|
2669
|
+
const tempFilePath = scratchDir ? path.join(scratchDir, localFilename) : gcode;
|
|
2037
2670
|
|
|
2038
2671
|
try {
|
|
2672
|
+
if (scratchDir) fs.writeFileSync(tempFilePath, gcode);
|
|
2039
2673
|
const implementation = this.printerFactory.getImplementation(type);
|
|
2040
2674
|
|
|
2041
2675
|
if (type.toLowerCase() === "bambu") {
|
|
2042
2676
|
const bambuApiKey = `${bambuSerial}:${bambuToken}`;
|
|
2043
|
-
return await implementation.uploadFile(host, port, bambuApiKey, tempFilePath, filename, print);
|
|
2677
|
+
return await implementation.uploadFile(host, port, bambuApiKey, tempFilePath, filename, print, options);
|
|
2044
2678
|
}
|
|
2045
2679
|
|
|
2046
|
-
return await implementation.uploadFile(host, port, apiKey, tempFilePath, filename, print);
|
|
2680
|
+
return await implementation.uploadFile(host, port, apiKey, tempFilePath, filename, print, options);
|
|
2047
2681
|
} finally {
|
|
2048
|
-
if (
|
|
2049
|
-
fs.unlinkSync(tempFilePath);
|
|
2050
|
-
}
|
|
2682
|
+
if (scratchDir) fs.rmSync(scratchDir, { recursive: true, force: true });
|
|
2051
2683
|
}
|
|
2052
2684
|
}
|
|
2053
2685
|
|
|
@@ -2058,16 +2690,17 @@ class ThreeDPrinterMCPServer {
|
|
|
2058
2690
|
apiKey: string,
|
|
2059
2691
|
bambuSerial: string,
|
|
2060
2692
|
bambuToken: string,
|
|
2061
|
-
gcodeFilename: string
|
|
2693
|
+
gcodeFilename: string,
|
|
2694
|
+
options: PrintSafetyOptions = {}
|
|
2062
2695
|
) {
|
|
2063
2696
|
const implementation = this.printerFactory.getImplementation(type);
|
|
2064
2697
|
|
|
2065
2698
|
if (type.toLowerCase() === "bambu") {
|
|
2066
2699
|
const bambuApiKey = `${bambuSerial}:${bambuToken}`;
|
|
2067
|
-
return await implementation.startJob(host, port, bambuApiKey, gcodeFilename);
|
|
2700
|
+
return await implementation.startJob(host, port, bambuApiKey, gcodeFilename, options);
|
|
2068
2701
|
}
|
|
2069
2702
|
|
|
2070
|
-
return await implementation.startJob(host, port, apiKey, gcodeFilename);
|
|
2703
|
+
return await implementation.startJob(host, port, apiKey, gcodeFilename, options);
|
|
2071
2704
|
}
|
|
2072
2705
|
|
|
2073
2706
|
async cancelPrint(
|
|
@@ -2096,19 +2729,53 @@ class ThreeDPrinterMCPServer {
|
|
|
2096
2729
|
bambuSerial: string,
|
|
2097
2730
|
bambuToken: string,
|
|
2098
2731
|
component: string,
|
|
2099
|
-
temperature:
|
|
2732
|
+
temperature: unknown,
|
|
2733
|
+
options: PrintSafetyOptions = {}
|
|
2100
2734
|
) {
|
|
2101
2735
|
const implementation = this.printerFactory.getImplementation(type);
|
|
2102
2736
|
|
|
2103
2737
|
if (type.toLowerCase() === "bambu") {
|
|
2104
2738
|
const bambuApiKey = `${bambuSerial}:${bambuToken}`;
|
|
2105
|
-
return implementation.setTemperature(host, port, bambuApiKey, component, temperature);
|
|
2739
|
+
return implementation.setTemperature(host, port, bambuApiKey, component, temperature, options);
|
|
2106
2740
|
}
|
|
2107
2741
|
|
|
2108
|
-
return implementation.setTemperature(host, port, apiKey, component, temperature);
|
|
2742
|
+
return implementation.setTemperature(host, port, apiKey, component, temperature, options);
|
|
2109
2743
|
}
|
|
2110
2744
|
|
|
2111
|
-
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
|
+
}
|
|
2112
2779
|
const isInputError =
|
|
2113
2780
|
message.toLowerCase().includes("missing required") ||
|
|
2114
2781
|
message.toLowerCase().includes("invalid") ||
|
|
@@ -2244,78 +2911,44 @@ class ThreeDPrinterMCPServer {
|
|
|
2244
2911
|
);
|
|
2245
2912
|
}
|
|
2246
2913
|
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2914
|
+
/**
|
|
2915
|
+
* Bridge commands and path-derived bridge executables are machine-local
|
|
2916
|
+
* settings. Accepting their selectors as tool arguments means anything that
|
|
2917
|
+
* can steer the model — a model description, a README in a downloaded
|
|
2918
|
+
* archive, 3MF metadata — can choose the program this server runs. They are
|
|
2919
|
+
* read from environment configuration by default and only accepted for
|
|
2920
|
+
* execution when MCP_ALLOW_EXECUTABLE_ARG is set. The older
|
|
2921
|
+
* MCP_ALLOW_BRIDGE_COMMAND_ARG name remains a compatibility alias only for
|
|
2922
|
+
* bridge commands and path selectors used to derive a bridge executable.
|
|
2923
|
+
*/
|
|
2924
|
+
private resolveExecutableSelectorArg(
|
|
2925
|
+
rawValue: unknown,
|
|
2926
|
+
toolName: string,
|
|
2927
|
+
argumentName: string,
|
|
2928
|
+
executionRequested = true,
|
|
2929
|
+
allowLegacyBridgeArg = false
|
|
2930
|
+
): string | undefined {
|
|
2931
|
+
if (rawValue === undefined) {
|
|
2932
|
+
return undefined;
|
|
2933
|
+
}
|
|
2259
2934
|
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
message: "Prepared Blender MCP payload. Set execute=true to run bridge command.",
|
|
2264
|
-
bridgeCommand: command ?? null,
|
|
2265
|
-
payload
|
|
2266
|
-
};
|
|
2935
|
+
const value = String(rawValue);
|
|
2936
|
+
if (value.length === 0) {
|
|
2937
|
+
return undefined;
|
|
2267
2938
|
}
|
|
2268
2939
|
|
|
2269
|
-
|
|
2940
|
+
const isAllowed =
|
|
2941
|
+
this.runtimeConfig.allowExecutableArg ||
|
|
2942
|
+
(allowLegacyBridgeArg && this.runtimeConfig.allowBridgeCommandArg);
|
|
2943
|
+
if (executionRequested && !isAllowed) {
|
|
2270
2944
|
throw new Error(
|
|
2271
|
-
|
|
2945
|
+
`${toolName}: the ${argumentName} argument cannot select an executable by default. ` +
|
|
2946
|
+
"Executable paths and commands are read from server environment configuration. Set " +
|
|
2947
|
+
`MCP_ALLOW_EXECUTABLE_ARG=1 to accept ${argumentName} for process execution.`
|
|
2272
2948
|
);
|
|
2273
2949
|
}
|
|
2274
2950
|
|
|
2275
|
-
|
|
2276
|
-
const result = await new Promise<{ stdout: string; stderr: string }>((resolve, reject) => {
|
|
2277
|
-
const child = spawn(command, {
|
|
2278
|
-
shell: true,
|
|
2279
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
2280
|
-
});
|
|
2281
|
-
|
|
2282
|
-
let stdout = "";
|
|
2283
|
-
let stderr = "";
|
|
2284
|
-
|
|
2285
|
-
child.stdout.setEncoding("utf8");
|
|
2286
|
-
child.stderr.setEncoding("utf8");
|
|
2287
|
-
child.stdout.on("data", (chunk: string) => {
|
|
2288
|
-
stdout += chunk;
|
|
2289
|
-
});
|
|
2290
|
-
child.stderr.on("data", (chunk: string) => {
|
|
2291
|
-
stderr += chunk;
|
|
2292
|
-
});
|
|
2293
|
-
child.on("error", reject);
|
|
2294
|
-
child.on("close", (code) => {
|
|
2295
|
-
if (code === 0) {
|
|
2296
|
-
resolve({ stdout, stderr });
|
|
2297
|
-
return;
|
|
2298
|
-
}
|
|
2299
|
-
|
|
2300
|
-
reject(
|
|
2301
|
-
new Error(
|
|
2302
|
-
`Blender bridge command exited with code ${code ?? "unknown"}${
|
|
2303
|
-
stderr.trim() ? `: ${stderr.trim()}` : ""
|
|
2304
|
-
}`
|
|
2305
|
-
)
|
|
2306
|
-
);
|
|
2307
|
-
});
|
|
2308
|
-
|
|
2309
|
-
child.stdin.write(JSON.stringify(payload));
|
|
2310
|
-
child.stdin.end();
|
|
2311
|
-
});
|
|
2312
|
-
|
|
2313
|
-
return {
|
|
2314
|
-
status: "executed",
|
|
2315
|
-
bridgeCommand: command,
|
|
2316
|
-
stdout: result.stdout?.toString() ?? "",
|
|
2317
|
-
stderr: result.stderr?.toString() ?? ""
|
|
2318
|
-
};
|
|
2951
|
+
return value;
|
|
2319
2952
|
}
|
|
2320
2953
|
|
|
2321
2954
|
async close(): Promise<void> {
|