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/dist/index.js
CHANGED
|
@@ -7,14 +7,20 @@ import axios from "axios";
|
|
|
7
7
|
import dotenv from "dotenv";
|
|
8
8
|
import fs from "fs";
|
|
9
9
|
import path from "path";
|
|
10
|
+
import os from "node:os";
|
|
11
|
+
import { BlenderMcpBridge } from "./blender-mcp-bridge.js";
|
|
10
12
|
import { createServer as createHttpServer } from "node:http";
|
|
11
13
|
import { randomUUID } from "node:crypto";
|
|
12
14
|
import * as THREE from 'three';
|
|
13
15
|
import { PrinterFactory } from "./printers/printer-factory.js";
|
|
14
16
|
import { STLManipulator } from "./stl/stl-manipulator.js";
|
|
17
|
+
import { SlicerError } from "./slicer/slicer-error.js";
|
|
18
|
+
import { inspectSliceSettings, listTemplateRegistry, resolveTemplatePath, resolveTemplateSlicerProfilePath, saveTemplate, } from "./slicer/templates.js";
|
|
15
19
|
import { parse3MF } from './3mf_parser.js';
|
|
16
20
|
import { BambuImplementation } from "./printers/bambu.js";
|
|
17
|
-
import { inspectFuluOrcaSetup, invokeFuluBridgeRpc,
|
|
21
|
+
import { inspectFuluOrcaSetup, invokeFuluBridgeRpc, } from "./fulu-orca.js";
|
|
22
|
+
import { CONFIRMATION_UNAVAILABLE, confirmationTimedOut, confirmationTimeoutMs } from "./safety/confirmation.js";
|
|
23
|
+
import { validateExpectedPeaks } from "./safety/expected-peaks.js";
|
|
18
24
|
// Load environment variables from .env file
|
|
19
25
|
dotenv.config();
|
|
20
26
|
// Default values
|
|
@@ -22,7 +28,7 @@ const DEFAULT_HOST = process.env.PRINTER_HOST || "localhost";
|
|
|
22
28
|
const DEFAULT_PORT = process.env.PRINTER_PORT || "80";
|
|
23
29
|
const DEFAULT_API_KEY = process.env.API_KEY || "";
|
|
24
30
|
const DEFAULT_TYPE = process.env.PRINTER_TYPE || "octoprint"; // Default to OctoPrint
|
|
25
|
-
const TEMP_DIR = process.env.TEMP_DIR || path.join(
|
|
31
|
+
const TEMP_DIR = process.env.TEMP_DIR || fs.mkdtempSync(path.join(os.tmpdir(), "mcp-3d-printer-server-"));
|
|
26
32
|
// Slicer configuration
|
|
27
33
|
const CANONICAL_SLICER_TYPES = [
|
|
28
34
|
"prusaslicer",
|
|
@@ -125,6 +131,208 @@ const BAMBU_MODEL_PRESETS = {
|
|
|
125
131
|
a1mini: (n) => `Bambu Lab A1 mini ${n} nozzle`,
|
|
126
132
|
h2d: (n) => `Bambu Lab H2D ${n} nozzle`,
|
|
127
133
|
};
|
|
134
|
+
// Models whose bundled machine presets the Bambu-compatible CLI slicing path
|
|
135
|
+
// can target. The installed slicer must still contain the exact
|
|
136
|
+
// "<model> <nozzle> nozzle" preset; a missing preset stops the slice.
|
|
137
|
+
const SLICE_BAMBU_MODELS = ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s", "h2c"];
|
|
138
|
+
const SLICE_BAMBU_MODEL_PRESETS = {
|
|
139
|
+
...BAMBU_MODEL_PRESETS,
|
|
140
|
+
p2s: (n) => `Bambu Lab P2S ${n} nozzle`,
|
|
141
|
+
h2s: (n) => `Bambu Lab H2S ${n} nozzle`,
|
|
142
|
+
h2c: (n) => `Bambu Lab H2C ${n} nozzle`,
|
|
143
|
+
};
|
|
144
|
+
const DEFAULT_TEMPLATE_3MF_PATH = process.env.BAMBU_TEMPLATE_3MF_PATH || "";
|
|
145
|
+
const VALID_NOZZLE_TYPES = ["stainless_steel", "hardened_steel", "tungsten_carbide", "brass"];
|
|
146
|
+
/** The installed hotend material. Bambu machine presets assume the stock nozzle; the print gate compares this with the printer. */
|
|
147
|
+
function resolveNozzleType(value) {
|
|
148
|
+
const raw = value === undefined || value === null || value === "" ? process.env.BAMBU_NOZZLE_TYPE?.trim() : value;
|
|
149
|
+
if (raw === undefined || raw === "")
|
|
150
|
+
return undefined;
|
|
151
|
+
const normalized = String(raw).trim().toLowerCase().replace(/[\s-]+/g, "_");
|
|
152
|
+
if (!VALID_NOZZLE_TYPES.includes(normalized)) {
|
|
153
|
+
throw new Error(`Invalid nozzle type "${raw}". Valid types: ${VALID_NOZZLE_TYPES.join(", ")}`);
|
|
154
|
+
}
|
|
155
|
+
return normalized;
|
|
156
|
+
}
|
|
157
|
+
// Shared slice_stl / slice_with_template argument schema.
|
|
158
|
+
const SLICE_TOOL_PROPERTIES = {
|
|
159
|
+
bambu_model: {
|
|
160
|
+
type: "string",
|
|
161
|
+
enum: [...SLICE_BAMBU_MODELS],
|
|
162
|
+
description: "Bambu Lab printer model. Required for bambustudio and orcaslicer-bambulab (elicited or read from BAMBU_MODEL when omitted); " +
|
|
163
|
+
"passing it with orcaslicer selects the Bambu-compatible path. The installed slicer must contain the exact model/nozzle preset.",
|
|
164
|
+
},
|
|
165
|
+
nozzle_diameter: {
|
|
166
|
+
type: "string",
|
|
167
|
+
description: "Nozzle diameter in mm (default: NOZZLE_DIAMETER or 0.4). Selects the '<model> <diameter> nozzle' machine preset.",
|
|
168
|
+
},
|
|
169
|
+
slicer_type: {
|
|
170
|
+
type: "string",
|
|
171
|
+
enum: [...SLICER_TYPE_ENUM],
|
|
172
|
+
description: "Type of slicer to use (prusaslicer, cura, slic3r, orcaslicer, orcaslicer-bambulab, bambustudio). Use orcaslicer-bambulab for the FULU fork. " +
|
|
173
|
+
"bambustudio and orcaslicer-bambulab (and orcaslicer with bambu_model) export a sliced 3MF.",
|
|
174
|
+
},
|
|
175
|
+
slicer_path: {
|
|
176
|
+
type: "string",
|
|
177
|
+
description: "Path to the slicer executable (default: value from env). Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1.",
|
|
178
|
+
},
|
|
179
|
+
slicer_profile: {
|
|
180
|
+
type: "string",
|
|
181
|
+
description: "Profile to use for slicing (default: SLICER_PROFILE env). Bambu-compatible slicing: one process profile JSON (the machine preset comes from bambu_model). " +
|
|
182
|
+
"Generic OrcaSlicer: machine/process profiles separated with ';', optionally followed by '|filament.json'.",
|
|
183
|
+
},
|
|
184
|
+
filament_profile: {
|
|
185
|
+
type: "string",
|
|
186
|
+
description: "Filament profile path(s), ';'-separated in slot order, loaded with --load-filaments (default: FILAMENT_PROFILE/SLICER_FILAMENT_PROFILE env). Alias of load_filaments.",
|
|
187
|
+
},
|
|
188
|
+
load_filaments: {
|
|
189
|
+
type: "string",
|
|
190
|
+
description: "Bambu-compatible slicing: filament profile JSON paths in slot order, ';'-separated. One profile applies to every project slot; otherwise supply one per slot.",
|
|
191
|
+
},
|
|
192
|
+
load_filament_ids: {
|
|
193
|
+
type: "string",
|
|
194
|
+
description: "Bambu-compatible slicing: comma-separated filament IDs mapping load_filaments to objects, e.g. '1,2,3,1'.",
|
|
195
|
+
},
|
|
196
|
+
filament_colours: {
|
|
197
|
+
type: "string",
|
|
198
|
+
description: "Bambu-compatible slicing: one #RRGGBB per filament slot, ';'-separated. Defaults to the input 3MF's colours, then each profile's colour.",
|
|
199
|
+
},
|
|
200
|
+
bed_type: {
|
|
201
|
+
type: "string",
|
|
202
|
+
enum: [...VALID_BED_TYPES],
|
|
203
|
+
description: "Bambu-compatible slicing: build plate type (default: BED_TYPE or textured_plate).",
|
|
204
|
+
},
|
|
205
|
+
nozzle_type: {
|
|
206
|
+
type: "string",
|
|
207
|
+
enum: [...VALID_NOZZLE_TYPES],
|
|
208
|
+
description: "Bambu-compatible slicing: the hotend nozzle material installed on the printer (default: BAMBU_NOZZLE_TYPE, else the model preset's stock nozzle, usually stainless_steel; X1C/X1E presets use hardened_steel). Printing compares it with the printer's reported nozzle.",
|
|
209
|
+
},
|
|
210
|
+
template_3mf_path: {
|
|
211
|
+
type: "string",
|
|
212
|
+
description: "Bambu-compatible slicing: 3MF or profile whose embedded slicer settings are reused as the process profile (default: BAMBU_TEMPLATE_3MF_PATH). An explicit slicer_profile takes precedence.",
|
|
213
|
+
},
|
|
214
|
+
template_name: {
|
|
215
|
+
type: "string",
|
|
216
|
+
description: "Named template from the local registry (see list_templates); resolves to its file.",
|
|
217
|
+
},
|
|
218
|
+
template_dir: {
|
|
219
|
+
type: "string",
|
|
220
|
+
description: "Template directory override when resolving template_name (default: BAMBU_TEMPLATE_DIR or ~/Sync/bambu/templates).",
|
|
221
|
+
},
|
|
222
|
+
uptodate: { type: "boolean", description: "Bambu-compatible slicing: refresh 3MF preset configs to the installed slicer version (--uptodate)." },
|
|
223
|
+
repetitions: { type: "number", description: "Bambu-compatible slicing: print N identical copies (--repetitions)." },
|
|
224
|
+
orient: { type: "boolean", description: "Bambu-compatible slicing: auto-orient for printability (--orient)." },
|
|
225
|
+
arrange: { type: "boolean", description: "Bambu-compatible slicing: auto-arrange objects on the plate (--arrange). Set false to keep an existing layout." },
|
|
226
|
+
ensure_on_bed: { type: "boolean", description: "Bambu-compatible slicing: lower floating models onto the bed (--ensure-on-bed)." },
|
|
227
|
+
clone_objects: { type: "string", description: "Bambu-compatible slicing: comma-separated clone counts per object index, e.g. '1,3,1,10'." },
|
|
228
|
+
skip_objects: { type: "string", description: "Bambu-compatible slicing: comma-separated object indices to skip, e.g. '3,5,10'." },
|
|
229
|
+
enable_timelapse: { type: "boolean", description: "Bambu-compatible slicing: insert timelapse parking moves (--enable-timelapse)." },
|
|
230
|
+
allow_mix_temp: { type: "boolean", description: "Bambu-compatible slicing: allow filaments with different temperature requirements on one plate." },
|
|
231
|
+
scale: { type: "number", description: "Bambu-compatible slicing: uniform scale factor applied before slicing (1.0 = original size)." },
|
|
232
|
+
rotate: { type: "number", description: "Bambu-compatible slicing: Z-axis rotation in degrees before slicing." },
|
|
233
|
+
rotate_x: { type: "number", description: "Bambu-compatible slicing: X-axis rotation in degrees before slicing." },
|
|
234
|
+
rotate_y: { type: "number", description: "Bambu-compatible slicing: Y-axis rotation in degrees before slicing." },
|
|
235
|
+
min_save: { type: "boolean", description: "Bambu-compatible slicing: write a smaller output 3MF (--min-save)." },
|
|
236
|
+
skip_modified_gcodes: { type: "boolean", description: "Bambu-compatible slicing: ignore custom G-code embedded in an input 3MF (--skip-modified-gcodes)." },
|
|
237
|
+
slice_plate: { type: "number", description: "Bambu-compatible slicing: plate number to slice; 0 slices all plates (default)." },
|
|
238
|
+
};
|
|
239
|
+
function validateSliceBambuModel(model) {
|
|
240
|
+
const normalized = model.trim().toLowerCase();
|
|
241
|
+
if (!SLICE_BAMBU_MODELS.includes(normalized)) {
|
|
242
|
+
throw new Error(`Invalid bambu_model "${model}" for slicing. Valid models: ${SLICE_BAMBU_MODELS.join(", ")}`);
|
|
243
|
+
}
|
|
244
|
+
return normalized;
|
|
245
|
+
}
|
|
246
|
+
function normalizeSliceNozzleDiameter(raw) {
|
|
247
|
+
const text = String(raw ?? "").trim() || DEFAULT_NOZZLE_DIAMETER;
|
|
248
|
+
const value = Number(text);
|
|
249
|
+
if (!/^\d+(\.\d+)?$/.test(text) || !Number.isFinite(value) || value <= 0 || value > 2) {
|
|
250
|
+
throw new Error(`Invalid nozzle_diameter "${text}". Use a diameter in mm such as 0.2, 0.4, 0.6, or 0.8.`);
|
|
251
|
+
}
|
|
252
|
+
return String(value);
|
|
253
|
+
}
|
|
254
|
+
/** Parse "#RRGGBB;#RRGGBB" (leading '#' optional) into positional slot colours. */
|
|
255
|
+
function parseFilamentColours(value) {
|
|
256
|
+
const colours = value.split(";").map((c) => c.trim()).filter(Boolean)
|
|
257
|
+
.map((c) => (c.startsWith("#") ? c : `#${c}`).toUpperCase());
|
|
258
|
+
for (const c of colours) {
|
|
259
|
+
if (!/^#[0-9A-F]{6}([0-9A-F]{2})?$/.test(c)) {
|
|
260
|
+
throw new Error(`Invalid filament colour "${c}"; expected #RRGGBB.`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (colours.length === 0)
|
|
264
|
+
throw new Error("Invalid filament_colours: list at least one colour.");
|
|
265
|
+
return colours;
|
|
266
|
+
}
|
|
267
|
+
function optionalFiniteNumber(args, key) {
|
|
268
|
+
if (args?.[key] === undefined)
|
|
269
|
+
return undefined;
|
|
270
|
+
const value = Number(args[key]);
|
|
271
|
+
if (!Number.isFinite(value))
|
|
272
|
+
throw new Error(`Invalid ${key}: expected a number.`);
|
|
273
|
+
return value;
|
|
274
|
+
}
|
|
275
|
+
function optionalBoolean(args, key) {
|
|
276
|
+
const value = args?.[key];
|
|
277
|
+
if (value === undefined)
|
|
278
|
+
return undefined;
|
|
279
|
+
if (typeof value !== "boolean")
|
|
280
|
+
throw new Error(`Invalid ${key}: expected a boolean.`);
|
|
281
|
+
return value;
|
|
282
|
+
}
|
|
283
|
+
/** Map slice_stl/slice_with_template arguments to Bambu-compatible CLI options. */
|
|
284
|
+
function buildBambuSliceOptions(args) {
|
|
285
|
+
const options = {};
|
|
286
|
+
const text = (key) => (args?.[key] !== undefined && String(args[key]).trim() ? String(args[key]).trim() : undefined);
|
|
287
|
+
options.uptodate = optionalBoolean(args, "uptodate");
|
|
288
|
+
options.orient = optionalBoolean(args, "orient");
|
|
289
|
+
options.arrange = optionalBoolean(args, "arrange");
|
|
290
|
+
options.ensureOnBed = optionalBoolean(args, "ensure_on_bed");
|
|
291
|
+
options.enableTimelapse = optionalBoolean(args, "enable_timelapse");
|
|
292
|
+
options.allowMixTemp = optionalBoolean(args, "allow_mix_temp");
|
|
293
|
+
options.minSave = optionalBoolean(args, "min_save");
|
|
294
|
+
options.skipModifiedGcodes = optionalBoolean(args, "skip_modified_gcodes");
|
|
295
|
+
options.repetitions = optionalFiniteNumber(args, "repetitions");
|
|
296
|
+
options.scale = optionalFiniteNumber(args, "scale");
|
|
297
|
+
options.rotate = optionalFiniteNumber(args, "rotate");
|
|
298
|
+
options.rotateX = optionalFiniteNumber(args, "rotate_x");
|
|
299
|
+
options.rotateY = optionalFiniteNumber(args, "rotate_y");
|
|
300
|
+
options.slicePlate = optionalFiniteNumber(args, "slice_plate");
|
|
301
|
+
if (options.repetitions !== undefined && (!Number.isInteger(options.repetitions) || options.repetitions < 1)) {
|
|
302
|
+
throw new Error("Invalid repetitions: expected a positive integer.");
|
|
303
|
+
}
|
|
304
|
+
if (options.slicePlate !== undefined && (!Number.isInteger(options.slicePlate) || options.slicePlate < 0)) {
|
|
305
|
+
throw new Error("Invalid slice_plate: expected 0 (all plates) or a plate number.");
|
|
306
|
+
}
|
|
307
|
+
if (options.scale !== undefined && options.scale <= 0)
|
|
308
|
+
throw new Error("Invalid scale: expected a positive factor.");
|
|
309
|
+
options.cloneObjects = text("clone_objects");
|
|
310
|
+
options.skipObjects = text("skip_objects");
|
|
311
|
+
options.loadFilamentIds = text("load_filament_ids");
|
|
312
|
+
const loadFilaments = text("load_filaments");
|
|
313
|
+
const filamentAlias = text("filament_profile");
|
|
314
|
+
if (loadFilaments && filamentAlias && loadFilaments !== filamentAlias) {
|
|
315
|
+
throw new Error("Provide either load_filaments or filament_profile, not conflicting values.");
|
|
316
|
+
}
|
|
317
|
+
options.loadFilaments = loadFilaments ?? filamentAlias;
|
|
318
|
+
const colours = text("filament_colours");
|
|
319
|
+
if (colours)
|
|
320
|
+
options.filamentColours = parseFilamentColours(colours);
|
|
321
|
+
options.bedType = resolveBedType(args?.bed_type);
|
|
322
|
+
options.nozzleType = resolveNozzleType(args?.nozzle_type);
|
|
323
|
+
return Object.fromEntries(Object.entries(options).filter(([, value]) => value !== undefined));
|
|
324
|
+
}
|
|
325
|
+
function defaultSlicerPathFor(slicerType) {
|
|
326
|
+
if (process.env.SLICER_PATH)
|
|
327
|
+
return process.env.SLICER_PATH;
|
|
328
|
+
if (slicerType === DEFAULT_SLICER_TYPE)
|
|
329
|
+
return DEFAULT_SLICER_PATH;
|
|
330
|
+
if (slicerType === "bambustudio")
|
|
331
|
+
return defaultBambuStudioPath();
|
|
332
|
+
if (slicerType === "orcaslicer-bambulab")
|
|
333
|
+
return defaultFuluOrcaPath();
|
|
334
|
+
return DEFAULT_SLICER_PATH;
|
|
335
|
+
}
|
|
128
336
|
function validateBambuModel(model) {
|
|
129
337
|
const normalized = model.trim().toLowerCase();
|
|
130
338
|
if (!VALID_BAMBU_MODELS.includes(normalized)) {
|
|
@@ -139,6 +347,34 @@ function resolveBedType(bedType) {
|
|
|
139
347
|
}
|
|
140
348
|
return resolved;
|
|
141
349
|
}
|
|
350
|
+
const SUPPORTED_NOZZLE_DIAMETERS = [0.2, 0.4, 0.6, 0.8];
|
|
351
|
+
/** Optional declared material; blank means "not declared". */
|
|
352
|
+
function optionalMaterialArg(value) {
|
|
353
|
+
if (value === undefined || value === null || value === "")
|
|
354
|
+
return undefined;
|
|
355
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
356
|
+
throw new Error("material must be a non-empty string such as PLA, PETG, ABS, ASA, TPU, PA or PC.");
|
|
357
|
+
}
|
|
358
|
+
return value.trim();
|
|
359
|
+
}
|
|
360
|
+
/** Temperatures are validated as finite numbers before any printer connection. */
|
|
361
|
+
function optionalTemperatureArg(value, name) {
|
|
362
|
+
if (value === undefined)
|
|
363
|
+
return undefined;
|
|
364
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
365
|
+
throw new Error(`${name} must be a finite, non-negative number in °C.`);
|
|
366
|
+
}
|
|
367
|
+
return value;
|
|
368
|
+
}
|
|
369
|
+
function optionalNozzleDiameterArg(value) {
|
|
370
|
+
if (value === undefined || value === null || value === "")
|
|
371
|
+
return undefined;
|
|
372
|
+
const diameter = Number(value);
|
|
373
|
+
if ((typeof value !== "number" && typeof value !== "string") || !SUPPORTED_NOZZLE_DIAMETERS.includes(diameter)) {
|
|
374
|
+
throw new Error(`nozzle_diameter must be one of ${SUPPORTED_NOZZLE_DIAMETERS.join(", ")} mm.`);
|
|
375
|
+
}
|
|
376
|
+
return diameter;
|
|
377
|
+
}
|
|
142
378
|
function parseBooleanEnv(rawValue, fallback) {
|
|
143
379
|
if (rawValue === undefined) {
|
|
144
380
|
return fallback;
|
|
@@ -193,29 +429,68 @@ function readRuntimeConfig() {
|
|
|
193
429
|
enableJsonResponse: parseBooleanEnv(process.env.MCP_HTTP_JSON_RESPONSE, true),
|
|
194
430
|
allowedOrigins: parseCsvEnv(process.env.MCP_HTTP_ALLOWED_ORIGINS),
|
|
195
431
|
blenderBridgeCommand: process.env.BLENDER_MCP_BRIDGE_COMMAND?.trim() || undefined,
|
|
432
|
+
allowExecutableArg: parseBooleanEnv(process.env.MCP_ALLOW_EXECUTABLE_ARG, false),
|
|
433
|
+
allowBridgeCommandArg: parseBooleanEnv(process.env.MCP_ALLOW_BRIDGE_COMMAND_ARG, false),
|
|
196
434
|
};
|
|
197
435
|
}
|
|
436
|
+
const SLICING_TOOLS = new Set(["slice_stl", "slice_with_template", "list_templates", "save_template", "get_slice_settings"]);
|
|
198
437
|
// Ensure temp directory exists
|
|
199
438
|
if (!fs.existsSync(TEMP_DIR)) {
|
|
200
439
|
fs.mkdirSync(TEMP_DIR, { recursive: true });
|
|
201
440
|
}
|
|
441
|
+
/** MQTT "Not authorized", FTP 530, and HTTP 401/403 all mean the printer rejected its credentials. */
|
|
442
|
+
function isCredentialRejection(message) {
|
|
443
|
+
return /not authori[sz]ed|\b530\b|login incorrect|\b401\b|\b403\b|unauthori[sz]ed|forbidden|bad user name or password/i.test(message);
|
|
444
|
+
}
|
|
445
|
+
function credentialSuggestion() {
|
|
446
|
+
return (process.env.PRINTER_TYPE ?? "").toLowerCase() === "bambu"
|
|
447
|
+
? "The printer rejected its LAN access code. Read the current Access Code on the printer (Settings > Network/WLAN; it changes " +
|
|
448
|
+
"when LAN mode is toggled and after some resets), update BAMBU_TOKEN, and check BAMBU_SERIAL. Retrying unchanged will not help."
|
|
449
|
+
: "The printer rejected its credentials. Check API_KEY (and PRINTER_HOST/PRINTER_PORT) for this printer system. Retrying unchanged will not help.";
|
|
450
|
+
}
|
|
202
451
|
class ThreeDPrinterMCPServer {
|
|
203
452
|
constructor() {
|
|
453
|
+
this.blender = new BlenderMcpBridge();
|
|
204
454
|
this.runtimeConfig = readRuntimeConfig();
|
|
205
455
|
this.server = new Server({
|
|
206
456
|
name: "mcp-3d-printer-server",
|
|
207
|
-
version: "
|
|
457
|
+
version: JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")).version
|
|
208
458
|
}, {
|
|
209
459
|
capabilities: {
|
|
210
460
|
resources: {},
|
|
211
461
|
tools: {}
|
|
212
462
|
}
|
|
213
463
|
});
|
|
214
|
-
this.printerFactory = new PrinterFactory();
|
|
464
|
+
this.printerFactory = new PrinterFactory((message) => this.confirmHardware(message));
|
|
215
465
|
this.stlManipulator = new STLManipulator(TEMP_DIR);
|
|
216
466
|
this.setupHandlers();
|
|
217
467
|
this.setupErrorHandling();
|
|
218
468
|
}
|
|
469
|
+
/** Human MCP elicitation before a print start or positive heating command. */
|
|
470
|
+
async confirmHardware(message) {
|
|
471
|
+
let response;
|
|
472
|
+
const timeout = confirmationTimeoutMs();
|
|
473
|
+
try {
|
|
474
|
+
response = await this.server.elicitInput({
|
|
475
|
+
mode: "form",
|
|
476
|
+
message,
|
|
477
|
+
requestedSchema: {
|
|
478
|
+
type: "object",
|
|
479
|
+
properties: {
|
|
480
|
+
confirmed: { type: "boolean", title: "I checked the printer and confirm this operation", default: false },
|
|
481
|
+
},
|
|
482
|
+
required: ["confirmed"],
|
|
483
|
+
},
|
|
484
|
+
}, { timeout });
|
|
485
|
+
}
|
|
486
|
+
catch (error) {
|
|
487
|
+
// A person may take minutes to check the bed; a timeout is not a missing capability.
|
|
488
|
+
if (error?.code === ErrorCode.RequestTimeout)
|
|
489
|
+
throw new Error(confirmationTimedOut(timeout));
|
|
490
|
+
throw new Error(CONFIRMATION_UNAVAILABLE);
|
|
491
|
+
}
|
|
492
|
+
return response.action === "accept" && response.content?.confirmed === true;
|
|
493
|
+
}
|
|
219
494
|
async resolveBambuModel(argsModel) {
|
|
220
495
|
const fromArgs = (argsModel || DEFAULT_BAMBU_MODEL).trim().toLowerCase();
|
|
221
496
|
if (fromArgs) {
|
|
@@ -247,7 +522,7 @@ class ThreeDPrinterMCPServer {
|
|
|
247
522
|
},
|
|
248
523
|
required: ["bambu_model"],
|
|
249
524
|
},
|
|
250
|
-
});
|
|
525
|
+
}, { timeout: confirmationTimeoutMs() });
|
|
251
526
|
if (result.action === "accept" && result.content?.bambu_model) {
|
|
252
527
|
return validateBambuModel(String(result.content.bambu_model));
|
|
253
528
|
}
|
|
@@ -255,6 +530,10 @@ class ThreeDPrinterMCPServer {
|
|
|
255
530
|
}
|
|
256
531
|
catch (elicitError) {
|
|
257
532
|
const msg = elicitError?.message || String(elicitError);
|
|
533
|
+
if (elicitError?.code === ErrorCode.RequestTimeout) {
|
|
534
|
+
throw new Error("No printer model was chosen in time; nothing was sent. Set BAMBU_MODEL or pass bambu_model, " +
|
|
535
|
+
`or answer the prompt within PRINT_CONFIRMATION_TIMEOUT_MS. Valid models: ${VALID_BAMBU_MODELS.join(", ")}`);
|
|
536
|
+
}
|
|
258
537
|
if (elicitError?.code === -32601 || elicitError?.code === -32600 ||
|
|
259
538
|
msg.includes("does not support") || msg.includes("elicitation")) {
|
|
260
539
|
throw new Error("bambu_model is required but your MCP client does not support elicitation. " +
|
|
@@ -264,6 +543,54 @@ class ThreeDPrinterMCPServer {
|
|
|
264
543
|
throw elicitError;
|
|
265
544
|
}
|
|
266
545
|
}
|
|
546
|
+
/** Slicing accepts every model with a Bambu-compatible CLI preset mapping. */
|
|
547
|
+
async resolveSliceBambuModel(argsModel) {
|
|
548
|
+
const requested = (argsModel || DEFAULT_BAMBU_MODEL).trim().toLowerCase();
|
|
549
|
+
if (requested) {
|
|
550
|
+
return validateSliceBambuModel(requested);
|
|
551
|
+
}
|
|
552
|
+
return this.resolveBambuModel(undefined);
|
|
553
|
+
}
|
|
554
|
+
/** Shared slice_stl / slice_with_template implementation. */
|
|
555
|
+
async runSliceTool(toolName, args) {
|
|
556
|
+
if (!args?.stl_path) {
|
|
557
|
+
throw new Error("Missing required parameter: stl_path");
|
|
558
|
+
}
|
|
559
|
+
const slicerType = normalizeSlicerType(String(args.slicer_type || DEFAULT_SLICER_TYPE));
|
|
560
|
+
const explicitModel = typeof args.bambu_model === "string" && args.bambu_model.trim() ? args.bambu_model : undefined;
|
|
561
|
+
// Generic OrcaSlicer keeps its user-supplied machine profile unless the
|
|
562
|
+
// call explicitly targets a Bambu model.
|
|
563
|
+
const bambuCli = isBambuProjectSlicer(slicerType) || (slicerType === "orcaslicer" && explicitModel !== undefined);
|
|
564
|
+
let printerPreset;
|
|
565
|
+
let options;
|
|
566
|
+
if (bambuCli) {
|
|
567
|
+
const model = await this.resolveSliceBambuModel(explicitModel);
|
|
568
|
+
printerPreset = SLICE_BAMBU_MODEL_PRESETS[model](normalizeSliceNozzleDiameter(args.nozzle_diameter));
|
|
569
|
+
options = buildBambuSliceOptions(args);
|
|
570
|
+
}
|
|
571
|
+
// Process profile precedence: explicit slicer_profile, then a template, then SLICER_PROFILE.
|
|
572
|
+
const explicitProfile = typeof args.slicer_profile === "string" && args.slicer_profile.trim() ? args.slicer_profile.trim() : undefined;
|
|
573
|
+
const templateDir = typeof args.template_dir === "string" ? args.template_dir : undefined;
|
|
574
|
+
const requestedTemplate = resolveTemplatePath(typeof args.template_name === "string" ? args.template_name : undefined, templateDir, ["json", "config", "3mf"]) ||
|
|
575
|
+
(typeof args.template_3mf_path === "string" && args.template_3mf_path.trim() ? args.template_3mf_path.trim() : undefined);
|
|
576
|
+
if (requestedTemplate && !bambuCli) {
|
|
577
|
+
throw new Error("template_name/template_3mf_path apply to Bambu-compatible slicing (bambustudio, orcaslicer-bambulab, or orcaslicer with bambu_model).");
|
|
578
|
+
}
|
|
579
|
+
let slicerProfile = explicitProfile;
|
|
580
|
+
const templatePath = requestedTemplate || (bambuCli && DEFAULT_TEMPLATE_3MF_PATH ? DEFAULT_TEMPLATE_3MF_PATH : undefined);
|
|
581
|
+
if (!slicerProfile && templatePath) {
|
|
582
|
+
if (!fs.existsSync(templatePath)) {
|
|
583
|
+
throw new Error(`Template not found: ${templatePath}`);
|
|
584
|
+
}
|
|
585
|
+
slicerProfile = await resolveTemplateSlicerProfilePath(templatePath, path.join(TEMP_DIR, "templates"));
|
|
586
|
+
}
|
|
587
|
+
slicerProfile ?? (slicerProfile = DEFAULT_SLICER_PROFILE || undefined);
|
|
588
|
+
const explicitFilament = typeof args.filament_profile === "string" && args.filament_profile.trim() ? args.filament_profile.trim() : undefined;
|
|
589
|
+
const filamentProfile = options?.loadFilaments ?? explicitFilament ?? (DEFAULT_FILAMENT_PROFILE || undefined);
|
|
590
|
+
const slicerPath = this.resolveExecutableSelectorArg(args.slicer_path, toolName, "slicer_path") ?? defaultSlicerPathFor(slicerType);
|
|
591
|
+
return this.stlManipulator.sliceSTL(String(args.stl_path), slicerType, slicerPath, slicerProfile, undefined, // progressCallback
|
|
592
|
+
printerPreset, filamentProfile, options);
|
|
593
|
+
}
|
|
267
594
|
setupErrorHandling() {
|
|
268
595
|
this.server.onerror = (error) => {
|
|
269
596
|
console.error("[MCP Error]", error);
|
|
@@ -461,7 +788,7 @@ class ThreeDPrinterMCPServer {
|
|
|
461
788
|
},
|
|
462
789
|
{
|
|
463
790
|
name: "upload_gcode",
|
|
464
|
-
description: "Upload G-code content or a local G-code file path to the printer",
|
|
791
|
+
description: "Upload G-code content or a local G-code file path to the printer. With print=true the exact uploaded bytes are inspected first (every S/R heater target, tool changes, hardware and material ceilings), printer state is checked, and a human confirmation is requested before the print starts.",
|
|
465
792
|
inputSchema: {
|
|
466
793
|
type: "object",
|
|
467
794
|
properties: {
|
|
@@ -489,6 +816,11 @@ class ThreeDPrinterMCPServer {
|
|
|
489
816
|
type: "string",
|
|
490
817
|
description: "Access token for Bambu Lab printers (default: value from env)"
|
|
491
818
|
},
|
|
819
|
+
bambu_model: {
|
|
820
|
+
type: "string",
|
|
821
|
+
enum: [...VALID_BAMBU_MODELS],
|
|
822
|
+
description: "Required for Bambu print operations unless BAMBU_MODEL is configured. Must match the printer and pre-sliced G-code."
|
|
823
|
+
},
|
|
492
824
|
filename: {
|
|
493
825
|
type: "string",
|
|
494
826
|
description: "Filename to use on the printer. Defaults to the basename of gcode_path when omitted."
|
|
@@ -503,18 +835,18 @@ class ThreeDPrinterMCPServer {
|
|
|
503
835
|
},
|
|
504
836
|
print: {
|
|
505
837
|
type: "boolean",
|
|
506
|
-
description: "Start printing after upload when the printer backend supports it."
|
|
838
|
+
description: "Start printing after upload when the printer backend supports it. Printing requires a declared material from slicer metadata (; filament_type = PLA) or the material argument."
|
|
839
|
+
},
|
|
840
|
+
material: {
|
|
841
|
+
type: "string",
|
|
842
|
+
description: "Declared filament material (for example PLA, PETG, ABS, ASA, TPU, PA, PC) when the G-code has no slicer filament_type metadata. Must not contradict the file. Material ceilings limit nozzle targets."
|
|
507
843
|
}
|
|
508
|
-
}
|
|
509
|
-
anyOf: [
|
|
510
|
-
{ required: ["filename", "gcode"] },
|
|
511
|
-
{ required: ["gcode_path"] }
|
|
512
|
-
]
|
|
844
|
+
}
|
|
513
845
|
}
|
|
514
846
|
},
|
|
515
847
|
{
|
|
516
848
|
name: "start_print",
|
|
517
|
-
description: "Start printing a file
|
|
849
|
+
description: "Start printing a G-code file already stored on the printer. The server downloads and inspects the exact file, then starts a uniquely named checked copy after printer-state checks and human confirmation. Printers whose API cannot download files (Repetier, Prusa, Creality) refuse; use upload_gcode with print=true instead.",
|
|
518
850
|
inputSchema: {
|
|
519
851
|
type: "object",
|
|
520
852
|
properties: {
|
|
@@ -542,9 +874,18 @@ class ThreeDPrinterMCPServer {
|
|
|
542
874
|
type: "string",
|
|
543
875
|
description: "Access token for Bambu Lab printers (default: value from env)"
|
|
544
876
|
},
|
|
877
|
+
bambu_model: {
|
|
878
|
+
type: "string",
|
|
879
|
+
enum: [...VALID_BAMBU_MODELS],
|
|
880
|
+
description: "Required for Bambu print operations unless BAMBU_MODEL is configured. Must match the printer and pre-sliced G-code."
|
|
881
|
+
},
|
|
545
882
|
filename: {
|
|
546
883
|
type: "string",
|
|
547
884
|
description: "Name/path of the printer-side G-code file to start."
|
|
885
|
+
},
|
|
886
|
+
material: {
|
|
887
|
+
type: "string",
|
|
888
|
+
description: "Declared filament material when the G-code has no slicer filament_type metadata (non-Bambu printers). Must not contradict the file."
|
|
548
889
|
}
|
|
549
890
|
},
|
|
550
891
|
required: ["filename"]
|
|
@@ -585,7 +926,7 @@ class ThreeDPrinterMCPServer {
|
|
|
585
926
|
},
|
|
586
927
|
{
|
|
587
928
|
name: "set_printer_temperature",
|
|
588
|
-
description: "Set the temperature of a printer component",
|
|
929
|
+
description: "Set the temperature of a printer component. Temperature 0 switches a heater off and is never gated. Positive targets are validated before connecting, limited by independent hardware and material ceilings, require a ready printer and a human confirmation.",
|
|
589
930
|
inputSchema: {
|
|
590
931
|
type: "object",
|
|
591
932
|
properties: {
|
|
@@ -619,7 +960,20 @@ class ThreeDPrinterMCPServer {
|
|
|
619
960
|
},
|
|
620
961
|
temperature: {
|
|
621
962
|
type: "number",
|
|
622
|
-
description: "Target temperature in Celsius."
|
|
963
|
+
description: "Target temperature in Celsius: a finite number >= 0. 0 switches the heater off."
|
|
964
|
+
},
|
|
965
|
+
material: {
|
|
966
|
+
type: "string",
|
|
967
|
+
description: "Declared material at the nozzle (for example PLA, PETG, ABS). Required for positive nozzle heating, including non-RFID spools."
|
|
968
|
+
},
|
|
969
|
+
bambu_model: {
|
|
970
|
+
type: "string",
|
|
971
|
+
enum: [...VALID_BAMBU_MODELS],
|
|
972
|
+
description: "Bambu printer model; required for positive Bambu heating unless BAMBU_MODEL is configured. Checked against the live printer."
|
|
973
|
+
},
|
|
974
|
+
nozzle_diameter: {
|
|
975
|
+
type: "string",
|
|
976
|
+
description: "Installed Bambu nozzle diameter in mm for nozzle heating (default: NOZZLE_DIAMETER or 0.4). Checked against the live printer."
|
|
623
977
|
}
|
|
624
978
|
},
|
|
625
979
|
required: ["component", "temperature"]
|
|
@@ -646,7 +1000,10 @@ class ThreeDPrinterMCPServer {
|
|
|
646
1000
|
},
|
|
647
1001
|
{
|
|
648
1002
|
name: "slice_stl",
|
|
649
|
-
description: "Slice an STL or 3MF file to generate G-code or sliced 3MF"
|
|
1003
|
+
description: "Slice an STL or 3MF file to generate G-code or a sliced 3MF. For Bambu-compatible CLI slicing " +
|
|
1004
|
+
"(bambustudio, orcaslicer-bambulab, or orcaslicer with bambu_model), the exact bambu_model/nozzle machine " +
|
|
1005
|
+
"preset from the selected slicer installation is required, profile inheritance is resolved before the CLI " +
|
|
1006
|
+
"runs, and the result must contain plate G-code. Failures stop with the slicer's exit status and output.",
|
|
650
1007
|
inputSchema: {
|
|
651
1008
|
type: "object",
|
|
652
1009
|
properties: {
|
|
@@ -654,39 +1011,73 @@ class ThreeDPrinterMCPServer {
|
|
|
654
1011
|
type: "string",
|
|
655
1012
|
description: "Path to the STL or 3MF file to slice"
|
|
656
1013
|
},
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
type: "string",
|
|
673
|
-
description: "Path to the slicer executable (default: value from env)"
|
|
674
|
-
},
|
|
1014
|
+
...SLICE_TOOL_PROPERTIES,
|
|
1015
|
+
},
|
|
1016
|
+
required: ["stl_path"]
|
|
1017
|
+
}
|
|
1018
|
+
},
|
|
1019
|
+
{
|
|
1020
|
+
name: "slice_with_template",
|
|
1021
|
+
description: "Slice an STL or 3MF with a named template from the local template registry (BAMBU_TEMPLATE_DIR). " +
|
|
1022
|
+
"The template supplies process settings; the machine preset still comes from bambu_model and nozzle_diameter.",
|
|
1023
|
+
inputSchema: {
|
|
1024
|
+
type: "object",
|
|
1025
|
+
properties: {
|
|
1026
|
+
stl_path: { type: "string", description: "Path to the STL or 3MF file to slice" },
|
|
1027
|
+
...SLICE_TOOL_PROPERTIES,
|
|
1028
|
+
template_name: { type: "string", description: "Named template from the local registry (required)." },
|
|
675
1029
|
slicer_profile: {
|
|
676
1030
|
type: "string",
|
|
677
|
-
description: "
|
|
1031
|
+
description: "Explicit process profile that overrides the named template only when provided in this call."
|
|
678
1032
|
},
|
|
679
|
-
|
|
1033
|
+
},
|
|
1034
|
+
required: ["stl_path", "template_name"]
|
|
1035
|
+
}
|
|
1036
|
+
},
|
|
1037
|
+
{
|
|
1038
|
+
name: "list_templates",
|
|
1039
|
+
description: "List saved slicing templates (.3mf, .json, .config) in the local template registry directory.",
|
|
1040
|
+
inputSchema: {
|
|
1041
|
+
type: "object",
|
|
1042
|
+
properties: {
|
|
1043
|
+
template_dir: {
|
|
680
1044
|
type: "string",
|
|
681
|
-
description: "
|
|
1045
|
+
description: "Template directory override. Defaults to BAMBU_TEMPLATE_DIR or ~/Sync/bambu/templates."
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
},
|
|
1050
|
+
{
|
|
1051
|
+
name: "save_template",
|
|
1052
|
+
description: "Copy a .3mf, .json, or .config file into the local template registry under a template name.",
|
|
1053
|
+
inputSchema: {
|
|
1054
|
+
type: "object",
|
|
1055
|
+
properties: {
|
|
1056
|
+
source_path: { type: "string", description: "Local .3mf, .json, or .config file to save as a template." },
|
|
1057
|
+
template_name: { type: "string", description: "Template name. Defaults to the source filename without extension." },
|
|
1058
|
+
template_dir: {
|
|
1059
|
+
type: "string",
|
|
1060
|
+
description: "Template directory override. Defaults to BAMBU_TEMPLATE_DIR or ~/Sync/bambu/templates."
|
|
682
1061
|
}
|
|
683
1062
|
},
|
|
684
|
-
required: ["
|
|
1063
|
+
required: ["source_path"]
|
|
1064
|
+
}
|
|
1065
|
+
},
|
|
1066
|
+
{
|
|
1067
|
+
name: "get_slice_settings",
|
|
1068
|
+
description: "Inspect slicer settings in a 3MF template or JSON/config profile without slicing (layer height, infill, walls, supports, brim, bed, printer, filaments).",
|
|
1069
|
+
inputSchema: {
|
|
1070
|
+
type: "object",
|
|
1071
|
+
properties: {
|
|
1072
|
+
source_path: { type: "string", description: "Path to a 3MF, extracted project_settings.config, or slicer profile JSON." },
|
|
1073
|
+
template_name: { type: "string", description: "Named template from the local registry; used when source_path is omitted." },
|
|
1074
|
+
template_dir: { type: "string", description: "Template directory override when resolving template_name." }
|
|
1075
|
+
}
|
|
685
1076
|
}
|
|
686
1077
|
},
|
|
687
1078
|
{
|
|
688
1079
|
name: "confirm_temperatures",
|
|
689
|
-
description: "
|
|
1080
|
+
description: "Report every heater target in a G-code file (S and R forms, tool-addressed, RepRapFirmware G10/M568 and Klipper SET_HEATER_TEMPERATURE). An expected temperature matches only when it equals the file's highest target. Read-only; printing tools enforce their own safety gate.",
|
|
690
1081
|
inputSchema: {
|
|
691
1082
|
type: "object",
|
|
692
1083
|
properties: {
|
|
@@ -696,11 +1087,11 @@ class ThreeDPrinterMCPServer {
|
|
|
696
1087
|
},
|
|
697
1088
|
extruder_temp: {
|
|
698
1089
|
type: "number",
|
|
699
|
-
description: "Expected
|
|
1090
|
+
description: "Expected highest nozzle target"
|
|
700
1091
|
},
|
|
701
1092
|
bed_temp: {
|
|
702
1093
|
type: "number",
|
|
703
|
-
description: "Expected bed
|
|
1094
|
+
description: "Expected highest bed target"
|
|
704
1095
|
}
|
|
705
1096
|
},
|
|
706
1097
|
required: ["gcode_path"]
|
|
@@ -708,7 +1099,7 @@ class ThreeDPrinterMCPServer {
|
|
|
708
1099
|
},
|
|
709
1100
|
{
|
|
710
1101
|
name: "process_and_print_stl",
|
|
711
|
-
description: "Process an STL file (extend base), slice it,
|
|
1102
|
+
description: "Process an STL file (extend base), slice it, and start printing through the same checked print gate as upload_gcode/print_3mf. Expected temperatures are enforced: a mismatch refuses before upload.",
|
|
712
1103
|
inputSchema: {
|
|
713
1104
|
type: "object",
|
|
714
1105
|
properties: {
|
|
@@ -722,11 +1113,15 @@ class ThreeDPrinterMCPServer {
|
|
|
722
1113
|
},
|
|
723
1114
|
extruder_temp: {
|
|
724
1115
|
type: "number",
|
|
725
|
-
description: "Expected
|
|
1116
|
+
description: "Expected highest nozzle target in the sliced G-code (S and R forms, every tool). Printing stops before upload if it differs."
|
|
726
1117
|
},
|
|
727
1118
|
bed_temp: {
|
|
728
1119
|
type: "number",
|
|
729
|
-
description: "Expected bed
|
|
1120
|
+
description: "Expected highest bed target in the sliced G-code (S and R forms). Printing stops before upload if it differs."
|
|
1121
|
+
},
|
|
1122
|
+
material: {
|
|
1123
|
+
type: "string",
|
|
1124
|
+
description: "Declared filament material when the sliced G-code has no filament_type metadata. Must not contradict the file."
|
|
730
1125
|
},
|
|
731
1126
|
host: {
|
|
732
1127
|
type: "string",
|
|
@@ -754,6 +1149,11 @@ class ThreeDPrinterMCPServer {
|
|
|
754
1149
|
enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate"],
|
|
755
1150
|
description: "Bed/plate type installed on the printer (default: textured_plate)."
|
|
756
1151
|
},
|
|
1152
|
+
nozzle_type: {
|
|
1153
|
+
type: "string",
|
|
1154
|
+
enum: [...VALID_NOZZLE_TYPES],
|
|
1155
|
+
description: "Installed Bambu nozzle material used when slicing (default: BAMBU_NOZZLE_TYPE, else the preset's stock nozzle). The print gate compares it with the printer's report."
|
|
1156
|
+
},
|
|
757
1157
|
nozzle_diameter: {
|
|
758
1158
|
type: "string",
|
|
759
1159
|
description: "Nozzle diameter in mm (default: 0.4)."
|
|
@@ -765,7 +1165,7 @@ class ThreeDPrinterMCPServer {
|
|
|
765
1165
|
},
|
|
766
1166
|
slicer_path: {
|
|
767
1167
|
type: "string",
|
|
768
|
-
description: "Path to the slicer executable (default: value from env)."
|
|
1168
|
+
description: "Path to the slicer executable (default: value from env). Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1."
|
|
769
1169
|
},
|
|
770
1170
|
slicer_profile: {
|
|
771
1171
|
type: "string",
|
|
@@ -960,7 +1360,7 @@ class ThreeDPrinterMCPServer {
|
|
|
960
1360
|
},
|
|
961
1361
|
{
|
|
962
1362
|
name: "print_3mf",
|
|
963
|
-
description: "Print a 3MF file on a Bambu Lab printer,
|
|
1363
|
+
description: "Print a 3MF file on a Bambu Lab printer. The exact selected plate is inspected (model, nozzle, bed type, materials, every heater target) and checked against a fresh MQTT report of the printer's identity, nozzle, state, errors and loaded filament, then a human confirmation is requested before upload and start.",
|
|
964
1364
|
inputSchema: {
|
|
965
1365
|
type: "object",
|
|
966
1366
|
properties: {
|
|
@@ -978,6 +1378,11 @@ class ThreeDPrinterMCPServer {
|
|
|
978
1378
|
enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate"],
|
|
979
1379
|
description: "Bed/plate type installed on the printer (default: textured_plate)."
|
|
980
1380
|
},
|
|
1381
|
+
nozzle_type: {
|
|
1382
|
+
type: "string",
|
|
1383
|
+
enum: [...VALID_NOZZLE_TYPES],
|
|
1384
|
+
description: "Installed nozzle material, used when the 3MF must be auto-sliced (default: BAMBU_NOZZLE_TYPE, else the preset's stock nozzle)."
|
|
1385
|
+
},
|
|
981
1386
|
nozzle_diameter: {
|
|
982
1387
|
type: "string",
|
|
983
1388
|
description: "Nozzle diameter in mm (default: 0.4)."
|
|
@@ -1010,7 +1415,7 @@ class ThreeDPrinterMCPServer {
|
|
|
1010
1415
|
},
|
|
1011
1416
|
slicer_path: {
|
|
1012
1417
|
type: "string",
|
|
1013
|
-
description: "Path to the slicer executable if auto-slicing is needed."
|
|
1418
|
+
description: "Path to the slicer executable if auto-slicing is needed. Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1."
|
|
1014
1419
|
},
|
|
1015
1420
|
slicer_profile: {
|
|
1016
1421
|
type: "string",
|
|
@@ -1056,15 +1461,20 @@ class ThreeDPrinterMCPServer {
|
|
|
1056
1461
|
properties: {
|
|
1057
1462
|
slicer_path: {
|
|
1058
1463
|
type: "string",
|
|
1059
|
-
description: "Path to the FULU OrcaSlicer executable. Defaults from SLICER_PATH/FULU_ORCA_PATH."
|
|
1464
|
+
description: "Path to the FULU OrcaSlicer executable. Defaults from SLICER_PATH/FULU_ORCA_PATH. " +
|
|
1465
|
+
"When run_bridge_probe=true, requires MCP_ALLOW_EXECUTABLE_ARG=1 to be accepted here."
|
|
1060
1466
|
},
|
|
1061
1467
|
plugin_dir: {
|
|
1062
1468
|
type: "string",
|
|
1063
|
-
description: "Directory containing the FULU Bambu runtime payload; on macOS this is usually
|
|
1469
|
+
description: "Directory containing the FULU Bambu runtime payload; on macOS this is usually " +
|
|
1470
|
+
"OrcaSlicer.app/Contents/MacOS. When run_bridge_probe=true, requires " +
|
|
1471
|
+
"MCP_ALLOW_EXECUTABLE_ARG=1 to be accepted here."
|
|
1064
1472
|
},
|
|
1065
1473
|
runtime_dir: {
|
|
1066
1474
|
type: "string",
|
|
1067
|
-
description: "Installed runtime directory. On macOS this defaults to ~/Library/Application
|
|
1475
|
+
description: "Installed runtime directory. On macOS this defaults to ~/Library/Application " +
|
|
1476
|
+
"Support/OrcaSlicer/macos-bridge/runtime. When run_bridge_probe=true, requires " +
|
|
1477
|
+
"MCP_ALLOW_EXECUTABLE_ARG=1 to be accepted here."
|
|
1068
1478
|
},
|
|
1069
1479
|
platform: {
|
|
1070
1480
|
type: "string",
|
|
@@ -1073,7 +1483,9 @@ class ThreeDPrinterMCPServer {
|
|
|
1073
1483
|
},
|
|
1074
1484
|
bridge_command: {
|
|
1075
1485
|
type: "string",
|
|
1076
|
-
description: "Command that starts the FULU BambuNetwork bridge host for probing."
|
|
1486
|
+
description: "Command that starts the FULU BambuNetwork bridge host for probing. Read from " +
|
|
1487
|
+
"FULU_BAMBU_BRIDGE_COMMAND by default; requires MCP_ALLOW_EXECUTABLE_ARG=1 " +
|
|
1488
|
+
"to be accepted here."
|
|
1077
1489
|
},
|
|
1078
1490
|
run_bridge_probe: {
|
|
1079
1491
|
type: "boolean",
|
|
@@ -1088,13 +1500,15 @@ class ThreeDPrinterMCPServer {
|
|
|
1088
1500
|
},
|
|
1089
1501
|
{
|
|
1090
1502
|
name: "fulu_bambu_network_rpc",
|
|
1091
|
-
description: "Advanced FULU bridge RPC for BambuNetwork diagnostics and development. Read-only methods are allowed by default
|
|
1503
|
+
description: "Advanced FULU bridge RPC for BambuNetwork diagnostics and development. Read-only methods are allowed by default. Agent/session setup methods require allow_mutating_method=true. Raw print methods, printer messages, file transfers and unknown methods are refused because they would bypass the print safety gate; use print_3mf for checked printing.",
|
|
1092
1504
|
inputSchema: {
|
|
1093
1505
|
type: "object",
|
|
1094
1506
|
properties: {
|
|
1095
1507
|
bridge_command: {
|
|
1096
1508
|
type: "string",
|
|
1097
|
-
description: "Command that starts the FULU BambuNetwork bridge host. Defaults to
|
|
1509
|
+
description: "Command that starts the FULU BambuNetwork bridge host. Defaults to " +
|
|
1510
|
+
"FULU_BAMBU_BRIDGE_COMMAND; requires MCP_ALLOW_EXECUTABLE_ARG=1 to be " +
|
|
1511
|
+
"accepted here."
|
|
1098
1512
|
},
|
|
1099
1513
|
method: {
|
|
1100
1514
|
type: "string",
|
|
@@ -1111,12 +1525,12 @@ class ThreeDPrinterMCPServer {
|
|
|
1111
1525
|
},
|
|
1112
1526
|
allow_mutating_method: {
|
|
1113
1527
|
type: "boolean",
|
|
1114
|
-
description: "Required for methods
|
|
1528
|
+
description: "Required for the allowlisted agent/session setup methods. It never enables print, printer-message, or unknown methods."
|
|
1115
1529
|
},
|
|
1116
1530
|
bambu_model: {
|
|
1117
1531
|
type: "string",
|
|
1118
1532
|
enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
|
|
1119
|
-
description: "
|
|
1533
|
+
description: "Informational only. Raw FULU print RPC methods are disabled; use print_3mf."
|
|
1120
1534
|
}
|
|
1121
1535
|
},
|
|
1122
1536
|
required: ["method"]
|
|
@@ -1168,38 +1582,80 @@ class ThreeDPrinterMCPServer {
|
|
|
1168
1582
|
required: ["stl_path"]
|
|
1169
1583
|
}
|
|
1170
1584
|
},
|
|
1585
|
+
{
|
|
1586
|
+
name: "blender_mcp_status",
|
|
1587
|
+
description: "Inspect Blender MCP configuration or connect and discover the remote server's tools. Lists tool names and summaries; pass tool_names for the full input schemas of the tools you will call. Connecting does not edit the scene; use get_scene_info through blender_mcp_call to check the Blender addon.",
|
|
1588
|
+
inputSchema: {
|
|
1589
|
+
type: "object",
|
|
1590
|
+
properties: {
|
|
1591
|
+
connect: { type: "boolean", description: "Initialize the configured stdio MCP server and discover its tools (default false)." },
|
|
1592
|
+
tool_names: { type: "array", items: { type: "string" }, maxItems: 64, description: "Return full input schemas for these discovered tools, such as [\"execute_blender_code\"]." },
|
|
1593
|
+
include_schemas: { type: "boolean", description: "Return every discovered tool's full definition (large; default false)." },
|
|
1594
|
+
timeout_ms: { type: "integer", minimum: 100, maximum: 300000, description: "Total connection and discovery deadline in milliseconds; defaults to BLENDER_MCP_TIMEOUT_MS or 120000." }
|
|
1595
|
+
},
|
|
1596
|
+
additionalProperties: false
|
|
1597
|
+
}
|
|
1598
|
+
},
|
|
1599
|
+
{
|
|
1600
|
+
name: "blender_mcp_call",
|
|
1601
|
+
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.",
|
|
1602
|
+
inputSchema: {
|
|
1603
|
+
type: "object",
|
|
1604
|
+
properties: {
|
|
1605
|
+
tool_name: { type: "string", description: "Exact name advertised by Blender MCP, such as get_scene_info or execute_blender_code." },
|
|
1606
|
+
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." },
|
|
1607
|
+
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." }
|
|
1608
|
+
},
|
|
1609
|
+
required: ["tool_name"],
|
|
1610
|
+
additionalProperties: false
|
|
1611
|
+
}
|
|
1612
|
+
},
|
|
1613
|
+
{
|
|
1614
|
+
name: "blender_mcp_export_stl",
|
|
1615
|
+
description: "Export named objects from the live Blender scene to a new, verified STL for slicing. Writes world-space geometry with modifiers applied, without changing the scene, selection, or mode, and reports triangle count and bounding-box dimensions from the written file. Use this after editing or modelling through blender_mcp_call; Blender MCP's own export_scene writes GLB/FBX only. Requires standard Blender MCP and a shared local filesystem.",
|
|
1616
|
+
inputSchema: {
|
|
1617
|
+
type: "object",
|
|
1618
|
+
properties: {
|
|
1619
|
+
object_names: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 64, description: "Blender object names to export together as one STL (mesh, curve, surface, metaball, or text objects)." },
|
|
1620
|
+
output_path: { type: "string", description: "New local .stl path. Its parent must exist; existing files are never overwritten." },
|
|
1621
|
+
apply_modifiers: { type: "boolean", description: "Export the evaluated geometry with modifiers applied (default true)." },
|
|
1622
|
+
scale: { type: "number", exclusiveMinimum: 0, description: "Multiply coordinates before writing (default 1). Slicers read STL units as millimetres, so use 1000 for a scene modelled in metres." },
|
|
1623
|
+
user_prompt: { type: "string", description: "The user's own words, passed unchanged to Blender MCP." },
|
|
1624
|
+
timeout_ms: { type: "integer", minimum: 100, maximum: 300000, description: "Total Blender request deadline in milliseconds; defaults to BLENDER_MCP_TIMEOUT_MS or 120000." }
|
|
1625
|
+
},
|
|
1626
|
+
required: ["object_names", "output_path"],
|
|
1627
|
+
additionalProperties: false
|
|
1628
|
+
}
|
|
1629
|
+
},
|
|
1171
1630
|
{
|
|
1172
1631
|
name: "blender_mcp_edit_model",
|
|
1173
|
-
description: "
|
|
1632
|
+
description: "Import, edit, and export a local STL through standard Blender MCP with verified output and existing scene objects preserved. Requires a shared local filesystem and Blender Object Mode. Also supports a separately configured legacy executable bridge.",
|
|
1174
1633
|
inputSchema: {
|
|
1175
1634
|
type: "object",
|
|
1176
1635
|
properties: {
|
|
1177
|
-
stl_path: {
|
|
1178
|
-
type: "string",
|
|
1179
|
-
description: "Path to the local STL file."
|
|
1180
|
-
},
|
|
1636
|
+
stl_path: { type: "string", description: "Path to the local STL file" },
|
|
1181
1637
|
operations: {
|
|
1182
1638
|
type: "array",
|
|
1183
|
-
description: "Ordered
|
|
1639
|
+
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.",
|
|
1640
|
+
minItems: 1,
|
|
1641
|
+
maxItems: 64,
|
|
1184
1642
|
items: { type: "string" }
|
|
1185
1643
|
},
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
},
|
|
1190
|
-
execute: {
|
|
1191
|
-
type: "boolean",
|
|
1192
|
-
description: "When true, attempts to execute bridge command; otherwise returns a prepared payload only."
|
|
1193
|
-
}
|
|
1644
|
+
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." },
|
|
1645
|
+
user_prompt: { type: "string", description: "The user's own words describing the edit, passed unchanged to Blender MCP." },
|
|
1646
|
+
timeout_ms: { type: "integer", minimum: 100, maximum: 300000, description: "Total Blender request deadline in milliseconds; defaults to BLENDER_MCP_TIMEOUT_MS or 120000." },
|
|
1647
|
+
bridge_command: { type: "string", description: "Legacy custom bridge executable override, not a standard MCP command. Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1." },
|
|
1648
|
+
execute: { type: "boolean", description: "Apply edits and export (true) or validate and return the prepared request without connecting (false, default)." }
|
|
1194
1649
|
},
|
|
1195
|
-
required: ["stl_path", "operations"]
|
|
1650
|
+
required: ["stl_path", "operations"],
|
|
1651
|
+
additionalProperties: false
|
|
1196
1652
|
}
|
|
1197
1653
|
}
|
|
1198
1654
|
]
|
|
1199
1655
|
};
|
|
1200
1656
|
});
|
|
1201
1657
|
// Handle tool calls
|
|
1202
|
-
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1658
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
1203
1659
|
const { name, arguments: args } = request.params;
|
|
1204
1660
|
// Set default values for common parameters
|
|
1205
1661
|
const host = String(args?.host || DEFAULT_HOST);
|
|
@@ -1209,7 +1665,6 @@ class ThreeDPrinterMCPServer {
|
|
|
1209
1665
|
const bambuSerial = String(args?.bambu_serial || DEFAULT_BAMBU_SERIAL);
|
|
1210
1666
|
const bambuToken = String(args?.bambu_token || DEFAULT_BAMBU_TOKEN);
|
|
1211
1667
|
const slicerType = normalizeSlicerType(String(args?.slicer_type || DEFAULT_SLICER_TYPE));
|
|
1212
|
-
const slicerPath = String(args?.slicer_path || DEFAULT_SLICER_PATH);
|
|
1213
1668
|
const slicerProfile = String(args?.slicer_profile || DEFAULT_SLICER_PROFILE);
|
|
1214
1669
|
const filamentProfile = String(args?.filament_profile || DEFAULT_FILAMENT_PROFILE);
|
|
1215
1670
|
try {
|
|
@@ -1231,15 +1686,26 @@ class ThreeDPrinterMCPServer {
|
|
|
1231
1686
|
if (!args?.filename && !args?.gcode_path) {
|
|
1232
1687
|
throw new Error("Missing required parameter: filename");
|
|
1233
1688
|
}
|
|
1689
|
+
if (args.print !== undefined && typeof args.print !== "boolean") {
|
|
1690
|
+
throw new Error("print must be a boolean.");
|
|
1691
|
+
}
|
|
1692
|
+
const uploadOptions = { material: optionalMaterialArg(args?.material) };
|
|
1693
|
+
if (type.toLowerCase() === "bambu" && args.print === true) {
|
|
1694
|
+
uploadOptions.bambuModel = await this.resolveBambuModel(args?.bambu_model);
|
|
1695
|
+
}
|
|
1234
1696
|
const uploadFilename = String(args?.filename || path.basename(String(args.gcode_path)));
|
|
1235
1697
|
const uploadGcodeSource = String(args?.gcode_path || args.gcode);
|
|
1236
|
-
result = await this.uploadGcode(host, port, type, apiKey, bambuSerial, bambuToken, uploadFilename, uploadGcodeSource, Boolean(args.print || false));
|
|
1698
|
+
result = await this.uploadGcode(host, port, type, apiKey, bambuSerial, bambuToken, uploadFilename, uploadGcodeSource, Boolean(args.print || false), uploadOptions);
|
|
1237
1699
|
break;
|
|
1238
1700
|
case "start_print":
|
|
1239
1701
|
if (!args?.filename) {
|
|
1240
1702
|
throw new Error("Missing required parameter: filename");
|
|
1241
1703
|
}
|
|
1242
|
-
|
|
1704
|
+
const startOptions = { material: optionalMaterialArg(args?.material) };
|
|
1705
|
+
if (type.toLowerCase() === "bambu") {
|
|
1706
|
+
startOptions.bambuModel = await this.resolveBambuModel(args?.bambu_model);
|
|
1707
|
+
}
|
|
1708
|
+
result = await this.startPrint(host, port, type, apiKey, bambuSerial, bambuToken, String(args.filename), startOptions);
|
|
1243
1709
|
break;
|
|
1244
1710
|
case "cancel_print":
|
|
1245
1711
|
result = await this.cancelPrint(host, port, type, apiKey, bambuSerial, bambuToken);
|
|
@@ -1248,7 +1714,15 @@ class ThreeDPrinterMCPServer {
|
|
|
1248
1714
|
if (!args?.component || args?.temperature === undefined) {
|
|
1249
1715
|
throw new Error("Missing required parameters: component and temperature");
|
|
1250
1716
|
}
|
|
1251
|
-
|
|
1717
|
+
// Never coerce: "not-a-number" must not become NaN in a heater command.
|
|
1718
|
+
const requestedTemperature = optionalTemperatureArg(args.temperature, "temperature");
|
|
1719
|
+
const heatOptions = { material: optionalMaterialArg(args.material) };
|
|
1720
|
+
if (type.toLowerCase() === "bambu" && requestedTemperature > 0) {
|
|
1721
|
+
heatOptions.bambuModel = await this.resolveBambuModel(args?.bambu_model);
|
|
1722
|
+
heatOptions.nozzleDiameter =
|
|
1723
|
+
optionalNozzleDiameterArg(args.nozzle_diameter) ?? Number(DEFAULT_NOZZLE_DIAMETER);
|
|
1724
|
+
}
|
|
1725
|
+
result = await this.setPrinterTemperature(host, port, type, apiKey, bambuSerial, bambuToken, String(args.component), requestedTemperature, heatOptions);
|
|
1252
1726
|
break;
|
|
1253
1727
|
// New STL manipulation tools
|
|
1254
1728
|
case "extend_stl_base":
|
|
@@ -1257,19 +1731,32 @@ class ThreeDPrinterMCPServer {
|
|
|
1257
1731
|
}
|
|
1258
1732
|
result = await this.stlManipulator.extendBase(String(args.stl_path), Number(args.extension_inches));
|
|
1259
1733
|
break;
|
|
1260
|
-
case "slice_stl":
|
|
1261
|
-
|
|
1262
|
-
|
|
1734
|
+
case "slice_stl":
|
|
1735
|
+
result = await this.runSliceTool("slice_stl", args);
|
|
1736
|
+
break;
|
|
1737
|
+
case "slice_with_template":
|
|
1738
|
+
if (!args?.template_name || !String(args.template_name).trim()) {
|
|
1739
|
+
throw new Error("Missing required parameter: template_name");
|
|
1263
1740
|
}
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1741
|
+
result = await this.runSliceTool("slice_with_template", args);
|
|
1742
|
+
break;
|
|
1743
|
+
case "list_templates":
|
|
1744
|
+
result = listTemplateRegistry(typeof args?.template_dir === "string" ? args.template_dir : undefined);
|
|
1745
|
+
break;
|
|
1746
|
+
case "save_template":
|
|
1747
|
+
if (!args?.source_path) {
|
|
1748
|
+
throw new Error("Missing required parameter: source_path");
|
|
1270
1749
|
}
|
|
1271
|
-
result =
|
|
1272
|
-
|
|
1750
|
+
result = saveTemplate(String(args.source_path), typeof args?.template_name === "string" ? args.template_name : undefined, typeof args?.template_dir === "string" ? args.template_dir : undefined);
|
|
1751
|
+
break;
|
|
1752
|
+
case "get_slice_settings": {
|
|
1753
|
+
if (!args?.source_path && !args?.template_name) {
|
|
1754
|
+
throw new Error("Missing required parameter: source_path or template_name");
|
|
1755
|
+
}
|
|
1756
|
+
const settingsSource = args?.source_path
|
|
1757
|
+
? String(args.source_path)
|
|
1758
|
+
: resolveTemplatePath(String(args.template_name), typeof args?.template_dir === "string" ? args.template_dir : undefined, ["3mf", "json", "config"]);
|
|
1759
|
+
result = await inspectSliceSettings(settingsSource, TEMP_DIR);
|
|
1273
1760
|
break;
|
|
1274
1761
|
}
|
|
1275
1762
|
case "confirm_temperatures":
|
|
@@ -1285,22 +1772,38 @@ class ThreeDPrinterMCPServer {
|
|
|
1285
1772
|
if (!args?.stl_path || args?.extension_inches === undefined) {
|
|
1286
1773
|
throw new Error("Missing required parameters: stl_path and extension_inches");
|
|
1287
1774
|
}
|
|
1775
|
+
// Validate every safety input before extending, slicing or connecting.
|
|
1776
|
+
const expectedPeaks = {
|
|
1777
|
+
nozzle: optionalTemperatureArg(args.extruder_temp, "extruder_temp"),
|
|
1778
|
+
bed: optionalTemperatureArg(args.bed_temp, "bed_temp"),
|
|
1779
|
+
};
|
|
1780
|
+
validateExpectedPeaks(expectedPeaks);
|
|
1781
|
+
const processMaterial = optionalMaterialArg(args?.material);
|
|
1782
|
+
let processModel;
|
|
1783
|
+
let processNozzle = String(DEFAULT_NOZZLE_DIAMETER);
|
|
1784
|
+
if (type.toLowerCase() === 'bambu' || isBambuProjectSlicer(slicerType)) {
|
|
1785
|
+
processModel = await this.resolveBambuModel(args?.bambu_model);
|
|
1786
|
+
processNozzle = String(optionalNozzleDiameterArg(args?.nozzle_diameter) ?? DEFAULT_NOZZLE_DIAMETER);
|
|
1787
|
+
}
|
|
1288
1788
|
// Define progress callback for UI updates
|
|
1289
1789
|
const processProgressCallback = (progress, message) => {
|
|
1290
|
-
console.
|
|
1790
|
+
console.error(`Process progress: ${progress}% - ${message || ''}`);
|
|
1291
1791
|
};
|
|
1292
1792
|
// 1. Extend the base of the STL file
|
|
1293
1793
|
const extendedStlPath = await this.stlManipulator.extendBase(String(args.stl_path), Number(args.extension_inches), processProgressCallback);
|
|
1294
1794
|
// 2. Slice the extended STL file
|
|
1295
1795
|
let processPreset;
|
|
1296
|
-
if (
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1796
|
+
if (processModel && isBambuProjectSlicer(slicerType)) {
|
|
1797
|
+
processPreset = BAMBU_MODEL_PRESETS[processModel]?.(processNozzle);
|
|
1798
|
+
}
|
|
1799
|
+
const gcodePath = await this.stlManipulator.sliceSTL(extendedStlPath, slicerType, this.resolveExecutableSelectorArg(args?.slicer_path, "process_and_print_stl", "slicer_path") ?? DEFAULT_SLICER_PATH, slicerProfile || undefined, processProgressCallback, processPreset, filamentProfile || undefined,
|
|
1800
|
+
// Slice for the plate and nozzle the print gate will check against.
|
|
1801
|
+
processPreset
|
|
1802
|
+
? {
|
|
1803
|
+
bedType: resolveBedType(args?.bed_type),
|
|
1804
|
+
...(resolveNozzleType(args?.nozzle_type) ? { nozzleType: resolveNozzleType(args?.nozzle_type) } : {}),
|
|
1301
1805
|
}
|
|
1302
|
-
|
|
1303
|
-
const gcodePath = await this.stlManipulator.sliceSTL(extendedStlPath, slicerType, slicerPath, slicerProfile || undefined, processProgressCallback, processPreset, filamentProfile || undefined);
|
|
1806
|
+
: undefined);
|
|
1304
1807
|
if (type.toLowerCase() === 'bambu' && gcodePath.toLowerCase().endsWith(".3mf")) {
|
|
1305
1808
|
if (!bambuSerial || !bambuToken) {
|
|
1306
1809
|
throw new Error("Bambu serial number and access token are required for Bambu 3MF printing.");
|
|
@@ -1321,6 +1824,9 @@ class ThreeDPrinterMCPServer {
|
|
|
1321
1824
|
result = await factoryImplementation.print3mf(host, bambuSerial, bambuToken, {
|
|
1322
1825
|
projectName: path.basename(gcodePath).replace(/\.3mf$/i, ''),
|
|
1323
1826
|
filePath: gcodePath,
|
|
1827
|
+
bambuModel: processModel,
|
|
1828
|
+
nozzleDiameters: [Number(processNozzle)],
|
|
1829
|
+
expectedPeaks,
|
|
1324
1830
|
plateIndex: 0,
|
|
1325
1831
|
...amsOptions,
|
|
1326
1832
|
bedType: printBedType,
|
|
@@ -1337,26 +1843,20 @@ class ThreeDPrinterMCPServer {
|
|
|
1337
1843
|
if (gcodePath.toLowerCase().endsWith(".3mf")) {
|
|
1338
1844
|
throw new Error("process_and_print_stl produced a sliced 3MF. Direct process-and-print for sliced 3MF is only supported for Bambu printers via print_3mf.");
|
|
1339
1845
|
}
|
|
1340
|
-
// 3.
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
}, processProgressCallback);
|
|
1346
|
-
if (!tempConfirmation.match) {
|
|
1347
|
-
console.warn("Temperature mismatch:", tempConfirmation);
|
|
1348
|
-
}
|
|
1349
|
-
}
|
|
1350
|
-
// 4. Upload the G-code file to the printer
|
|
1351
|
-
const gcodeContent = await fs.promises.readFile(gcodePath, 'utf8');
|
|
1846
|
+
// 3. Upload and print through the adapter's hard safety gate. It
|
|
1847
|
+
// inspects the exact bytes sent (S and R targets, tool changes,
|
|
1848
|
+
// hardware and material ceilings), requires the expected peaks to
|
|
1849
|
+
// match, reads fresh printer state and asks for human confirmation.
|
|
1850
|
+
// A mismatch refuses before upload; nothing is only logged.
|
|
1352
1851
|
const filename = path.basename(gcodePath);
|
|
1353
|
-
await this.uploadGcode(host, port, type, apiKey, bambuSerial, bambuToken, filename,
|
|
1354
|
-
);
|
|
1852
|
+
const printResult = await this.uploadGcode(host, port, type, apiKey, bambuSerial, bambuToken, filename, gcodePath, true, // Start printing immediately
|
|
1853
|
+
{ bambuModel: processModel, material: processMaterial, expectedPeaks });
|
|
1355
1854
|
result = {
|
|
1356
1855
|
extended_stl_path: extendedStlPath,
|
|
1357
1856
|
gcode_path: gcodePath,
|
|
1358
1857
|
filename,
|
|
1359
|
-
status: "
|
|
1858
|
+
status: "Checked print job started",
|
|
1859
|
+
print: printResult,
|
|
1360
1860
|
};
|
|
1361
1861
|
break;
|
|
1362
1862
|
// New STL manipulation tool handlers
|
|
@@ -1372,7 +1872,7 @@ class ThreeDPrinterMCPServer {
|
|
|
1372
1872
|
}
|
|
1373
1873
|
// Define progress callback for UI updates
|
|
1374
1874
|
const scaleProgressCallback = (progress, message) => {
|
|
1375
|
-
console.
|
|
1875
|
+
console.error(`Scale progress: ${progress}% - ${message || ''}`);
|
|
1376
1876
|
};
|
|
1377
1877
|
let scaleFactors;
|
|
1378
1878
|
// Check if we have individual axis scaling factors
|
|
@@ -1396,7 +1896,7 @@ class ThreeDPrinterMCPServer {
|
|
|
1396
1896
|
}
|
|
1397
1897
|
// Define progress callback for UI updates
|
|
1398
1898
|
const rotateProgressCallback = (progress, message) => {
|
|
1399
|
-
console.
|
|
1899
|
+
console.error(`Rotate progress: ${progress}% - ${message || ''}`);
|
|
1400
1900
|
};
|
|
1401
1901
|
// Get rotation angles, defaulting to 0 for any undefined axis
|
|
1402
1902
|
const rotationAngles = [
|
|
@@ -1412,7 +1912,7 @@ class ThreeDPrinterMCPServer {
|
|
|
1412
1912
|
}
|
|
1413
1913
|
// Define progress callback for UI updates
|
|
1414
1914
|
const translateProgressCallback = (progress, message) => {
|
|
1415
|
-
console.
|
|
1915
|
+
console.error(`Translate progress: ${progress}% - ${message || ''}`);
|
|
1416
1916
|
};
|
|
1417
1917
|
// Get translation values, defaulting to 0 for any undefined axis
|
|
1418
1918
|
const translationValues = [
|
|
@@ -1428,7 +1928,7 @@ class ThreeDPrinterMCPServer {
|
|
|
1428
1928
|
}
|
|
1429
1929
|
// Define progress callback for UI updates
|
|
1430
1930
|
const modifySectionProgressCallback = (progress, message) => {
|
|
1431
|
-
console.
|
|
1931
|
+
console.error(`Modify section progress: ${progress}% - ${message || ''}`);
|
|
1432
1932
|
};
|
|
1433
1933
|
// Determine the section to modify
|
|
1434
1934
|
let sectionBox;
|
|
@@ -1485,7 +1985,7 @@ class ThreeDPrinterMCPServer {
|
|
|
1485
1985
|
}
|
|
1486
1986
|
// Define progress callback for UI updates
|
|
1487
1987
|
const visualizationProgressCallback = (progress, message) => {
|
|
1488
|
-
console.
|
|
1988
|
+
console.error(`Visualization progress: ${progress}% - ${message || ''}`);
|
|
1489
1989
|
};
|
|
1490
1990
|
// Get width and height parameters, with defaults
|
|
1491
1991
|
const width = args.width !== undefined ? Number(args.width) : 300;
|
|
@@ -1504,25 +2004,51 @@ class ThreeDPrinterMCPServer {
|
|
|
1504
2004
|
}
|
|
1505
2005
|
const printModel = await this.resolveBambuModel(args?.bambu_model);
|
|
1506
2006
|
const printBedType = resolveBedType(args?.bed_type);
|
|
1507
|
-
const
|
|
2007
|
+
const explicitPrintNozzle = optionalNozzleDiameterArg(args?.nozzle_diameter);
|
|
2008
|
+
const printNozzle = String(explicitPrintNozzle ?? DEFAULT_NOZZLE_DIAMETER);
|
|
1508
2009
|
const printPreset = BAMBU_MODEL_PRESETS[printModel]?.(printNozzle);
|
|
1509
2010
|
let threeMFPath = String(args.three_mf_path);
|
|
1510
|
-
//
|
|
2011
|
+
// Only the selected plate's exact G-code entry counts as sliced output;
|
|
2012
|
+
// a stray .gcode entry elsewhere in the archive is not printable.
|
|
2013
|
+
const selectedPlateEntry = "Metadata/plate_1.gcode";
|
|
2014
|
+
let shouldAutoSlice = false;
|
|
1511
2015
|
try {
|
|
1512
2016
|
const JSZip = (await import('jszip')).default;
|
|
1513
2017
|
const zipData = fs.readFileSync(threeMFPath);
|
|
1514
2018
|
const zip = await JSZip.loadAsync(zipData);
|
|
1515
|
-
const
|
|
1516
|
-
|
|
1517
|
-
console.log(`3MF has no gcode — auto-slicing with ${slicerType} for ${printModel}`);
|
|
1518
|
-
threeMFPath = await this.stlManipulator.sliceSTL(threeMFPath, slicerType, slicerPath, slicerProfile || undefined, undefined, // progressCallback
|
|
1519
|
-
printPreset, filamentProfile || undefined);
|
|
1520
|
-
console.log("Auto-sliced to: " + threeMFPath);
|
|
1521
|
-
}
|
|
2019
|
+
const plateEntry = zip.file(selectedPlateEntry);
|
|
2020
|
+
shouldAutoSlice = !plateEntry || plateEntry.dir;
|
|
1522
2021
|
}
|
|
1523
2022
|
catch (sliceCheckErr) {
|
|
1524
|
-
|
|
2023
|
+
throw new Error(`Cannot read 3MF archive ${threeMFPath}; nothing was uploaded or started: ${sliceCheckErr?.message ?? String(sliceCheckErr)}`);
|
|
1525
2024
|
}
|
|
2025
|
+
// Only treat slicer_path as an executable selector when a slicer
|
|
2026
|
+
// will actually launch. Ready-to-print archives ignore it.
|
|
2027
|
+
if (shouldAutoSlice) {
|
|
2028
|
+
const printSlicerPath = this.resolveExecutableSelectorArg(args?.slicer_path, "print_3mf", "slicer_path") ?? DEFAULT_SLICER_PATH;
|
|
2029
|
+
try {
|
|
2030
|
+
console.error(`3MF has no gcode — auto-slicing with ${slicerType} for ${printModel}`);
|
|
2031
|
+
threeMFPath = await this.stlManipulator.sliceSTL(threeMFPath, slicerType, printSlicerPath, slicerProfile || undefined, undefined, // progressCallback
|
|
2032
|
+
printPreset, filamentProfile || undefined,
|
|
2033
|
+
// Slice for the plate and nozzle the print will be checked against.
|
|
2034
|
+
{ bedType: printBedType, ...(resolveNozzleType(args?.nozzle_type) ? { nozzleType: resolveNozzleType(args?.nozzle_type) } : {}) });
|
|
2035
|
+
console.error("Auto-sliced to: " + threeMFPath);
|
|
2036
|
+
}
|
|
2037
|
+
catch (sliceErr) {
|
|
2038
|
+
// Never fall back to uploading the original unsliced project.
|
|
2039
|
+
// Rethrow the original instance (for example a SlicerError) so
|
|
2040
|
+
// structured slicer suggestions survive.
|
|
2041
|
+
const prefix = "Auto-slicing failed; nothing was uploaded or started: ";
|
|
2042
|
+
if (sliceErr instanceof Error) {
|
|
2043
|
+
if (!sliceErr.message.startsWith(prefix))
|
|
2044
|
+
sliceErr.message = `${prefix}${sliceErr.message}`;
|
|
2045
|
+
throw sliceErr;
|
|
2046
|
+
}
|
|
2047
|
+
throw new Error(`${prefix}${String(sliceErr)}`);
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
// Compare the sliced file with an explicit or auto-slice nozzle request.
|
|
2051
|
+
const requestedPrintNozzles = explicitPrintNozzle !== undefined || shouldAutoSlice ? [Number(printNozzle)] : undefined;
|
|
1526
2052
|
// Define variables needed outside the parse try block
|
|
1527
2053
|
let implementation;
|
|
1528
2054
|
let threeMfFilename;
|
|
@@ -1531,7 +2057,7 @@ class ThreeDPrinterMCPServer {
|
|
|
1531
2057
|
try {
|
|
1532
2058
|
// --- Parse 3MF ---
|
|
1533
2059
|
const parsed3MFData = await parse3MF(threeMFPath);
|
|
1534
|
-
console.
|
|
2060
|
+
console.error(`Successfully parsed 3MF file: ${threeMFPath}`);
|
|
1535
2061
|
const amsOptions = this.resolveAmsPrintOptions(parsed3MFData, args);
|
|
1536
2062
|
// --- Prepare Implementation and Print Options ---
|
|
1537
2063
|
const factoryImplementation = this.printerFactory.getImplementation('bambu');
|
|
@@ -1543,6 +2069,8 @@ class ThreeDPrinterMCPServer {
|
|
|
1543
2069
|
projectName = threeMfFilename.replace(/\.3mf$/i, ''); // Assign to outer scope variable
|
|
1544
2070
|
printOptions = {
|
|
1545
2071
|
...amsOptions,
|
|
2072
|
+
bambuModel: printModel,
|
|
2073
|
+
nozzleDiameters: requestedPrintNozzles,
|
|
1546
2074
|
bedType: printBedType,
|
|
1547
2075
|
bedLeveling: args?.bed_leveling !== undefined ? Boolean(args.bed_leveling) : undefined,
|
|
1548
2076
|
flowCalibration: args?.flow_calibration !== undefined ? Boolean(args.flow_calibration) : undefined,
|
|
@@ -1564,7 +2092,7 @@ class ThreeDPrinterMCPServer {
|
|
|
1564
2092
|
plateIndex: 0,
|
|
1565
2093
|
...printOptions // Spread the final options
|
|
1566
2094
|
});
|
|
1567
|
-
|
|
2095
|
+
// Report what the printer did, not merely that a command was published.
|
|
1568
2096
|
}
|
|
1569
2097
|
catch (printError) {
|
|
1570
2098
|
console.error(`Error starting 3MF print for ${threeMfFilename}:`, printError);
|
|
@@ -1573,13 +2101,14 @@ class ThreeDPrinterMCPServer {
|
|
|
1573
2101
|
break;
|
|
1574
2102
|
}
|
|
1575
2103
|
case "check_fulu_orca_setup": {
|
|
2104
|
+
const runBridgeProbe = Boolean(args?.run_bridge_probe ?? false);
|
|
1576
2105
|
result = await inspectFuluOrcaSetup({
|
|
1577
|
-
slicerPath: args?.slicer_path
|
|
1578
|
-
pluginDir: args?.plugin_dir
|
|
1579
|
-
runtimeDir: args?.runtime_dir
|
|
2106
|
+
slicerPath: this.resolveExecutableSelectorArg(args?.slicer_path, "check_fulu_orca_setup", "slicer_path", runBridgeProbe, true),
|
|
2107
|
+
pluginDir: this.resolveExecutableSelectorArg(args?.plugin_dir, "check_fulu_orca_setup", "plugin_dir", runBridgeProbe, true),
|
|
2108
|
+
runtimeDir: this.resolveExecutableSelectorArg(args?.runtime_dir, "check_fulu_orca_setup", "runtime_dir", runBridgeProbe, true),
|
|
1580
2109
|
platform: args?.platform !== undefined ? String(args.platform) : undefined,
|
|
1581
|
-
bridgeCommand: args?.bridge_command
|
|
1582
|
-
runBridgeProbe
|
|
2110
|
+
bridgeCommand: this.resolveExecutableSelectorArg(args?.bridge_command, "check_fulu_orca_setup", "bridge_command", true, true),
|
|
2111
|
+
runBridgeProbe,
|
|
1583
2112
|
probeTimeoutMs: args?.probe_timeout_ms !== undefined ? Number(args.probe_timeout_ms) : undefined,
|
|
1584
2113
|
});
|
|
1585
2114
|
break;
|
|
@@ -1588,16 +2117,15 @@ class ThreeDPrinterMCPServer {
|
|
|
1588
2117
|
if (!args?.method) {
|
|
1589
2118
|
throw new Error("Missing required parameter: method");
|
|
1590
2119
|
}
|
|
2120
|
+
// Raw print methods, printer messages and unknown methods are refused
|
|
2121
|
+
// inside invokeFuluBridgeRpc before any bridge process starts.
|
|
1591
2122
|
const method = String(args.method);
|
|
1592
|
-
|
|
1593
|
-
if (isFuluPrintRpcMethod(method)) {
|
|
1594
|
-
rpcBambuModel = await this.resolveBambuModel(args?.bambu_model);
|
|
1595
|
-
}
|
|
2123
|
+
const rpcBambuModel = typeof args?.bambu_model === "string" && args.bambu_model.trim() ? args.bambu_model.trim() : undefined;
|
|
1596
2124
|
const payload = typeof args.payload === "object" && args.payload !== null && !Array.isArray(args.payload)
|
|
1597
2125
|
? args.payload
|
|
1598
2126
|
: undefined;
|
|
1599
2127
|
result = await invokeFuluBridgeRpc({
|
|
1600
|
-
bridgeCommand: args?.bridge_command
|
|
2128
|
+
bridgeCommand: this.resolveExecutableSelectorArg(args?.bridge_command, "fulu_bambu_network_rpc", "bridge_command", true, true),
|
|
1601
2129
|
method,
|
|
1602
2130
|
payload,
|
|
1603
2131
|
timeoutMs: args?.timeout_ms !== undefined ? Number(args.timeout_ms) : undefined,
|
|
@@ -1625,16 +2153,16 @@ class ThreeDPrinterMCPServer {
|
|
|
1625
2153
|
}
|
|
1626
2154
|
result = await this.stlManipulator.layFlat(String(args.stl_path));
|
|
1627
2155
|
break;
|
|
2156
|
+
case "blender_mcp_status":
|
|
2157
|
+
result = await this.blender.status(args ?? {}, extra.signal);
|
|
2158
|
+
break;
|
|
2159
|
+
case "blender_mcp_call":
|
|
2160
|
+
return await this.blender.call(args ?? {}, extra.signal);
|
|
2161
|
+
case "blender_mcp_export_stl":
|
|
2162
|
+
result = await this.blender.exportStl(args ?? {}, extra.signal);
|
|
2163
|
+
break;
|
|
1628
2164
|
case "blender_mcp_edit_model":
|
|
1629
|
-
|
|
1630
|
-
throw new Error("Missing required parameters: stl_path and operations");
|
|
1631
|
-
}
|
|
1632
|
-
result = await this.invokeBlenderBridge({
|
|
1633
|
-
stlPath: String(args.stl_path),
|
|
1634
|
-
operations: args.operations.map((entry) => String(entry)),
|
|
1635
|
-
execute: Boolean(args.execute ?? false),
|
|
1636
|
-
bridgeCommand: args.bridge_command !== undefined ? String(args.bridge_command) : undefined,
|
|
1637
|
-
});
|
|
2165
|
+
result = await this.blender.edit(args ?? {}, this.resolveExecutableSelectorArg(args?.bridge_command, "blender_mcp_edit_model", "bridge_command", true, true) ?? (!process.env.BLENDER_MCP_COMMAND?.trim() ? this.runtimeConfig.blenderBridgeCommand : undefined), extra.signal);
|
|
1638
2166
|
break;
|
|
1639
2167
|
default:
|
|
1640
2168
|
throw new Error(`Unknown tool: ${name}`);
|
|
@@ -1651,7 +2179,7 @@ class ThreeDPrinterMCPServer {
|
|
|
1651
2179
|
catch (error) {
|
|
1652
2180
|
console.error(`Error calling tool ${name}:`, error);
|
|
1653
2181
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1654
|
-
const structuredError = this.toStructuredToolError(name, errorMessage);
|
|
2182
|
+
const structuredError = this.toStructuredToolError(name, errorMessage, error);
|
|
1655
2183
|
return {
|
|
1656
2184
|
content: [
|
|
1657
2185
|
{
|
|
@@ -1690,34 +2218,38 @@ class ThreeDPrinterMCPServer {
|
|
|
1690
2218
|
}
|
|
1691
2219
|
return implementation.getFile(host, port, apiKey, filename);
|
|
1692
2220
|
}
|
|
1693
|
-
async uploadGcode(host, port, type, apiKey, bambuSerial, bambuToken, filename, gcode, print) {
|
|
2221
|
+
async uploadGcode(host, port, type, apiKey, bambuSerial, bambuToken, filename, gcode, print, options = {}) {
|
|
1694
2222
|
const sourceIsFilePath = fs.existsSync(gcode) && fs.statSync(gcode).isFile();
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
2223
|
+
// The remote filename must never select a local write/delete target.
|
|
2224
|
+
const localFilename = path.basename(filename);
|
|
2225
|
+
if (!sourceIsFilePath && (!localFilename || localFilename === "." || localFilename === "..")) {
|
|
2226
|
+
throw new Error("filename must include a file basename.");
|
|
1699
2227
|
}
|
|
2228
|
+
const scratchDir = sourceIsFilePath ? undefined : fs.mkdtempSync(path.join(TEMP_DIR, "upload-"));
|
|
2229
|
+
// Multipart adapters infer their file-part name from this safe basename.
|
|
2230
|
+
const tempFilePath = scratchDir ? path.join(scratchDir, localFilename) : gcode;
|
|
1700
2231
|
try {
|
|
2232
|
+
if (scratchDir)
|
|
2233
|
+
fs.writeFileSync(tempFilePath, gcode);
|
|
1701
2234
|
const implementation = this.printerFactory.getImplementation(type);
|
|
1702
2235
|
if (type.toLowerCase() === "bambu") {
|
|
1703
2236
|
const bambuApiKey = `${bambuSerial}:${bambuToken}`;
|
|
1704
|
-
return await implementation.uploadFile(host, port, bambuApiKey, tempFilePath, filename, print);
|
|
2237
|
+
return await implementation.uploadFile(host, port, bambuApiKey, tempFilePath, filename, print, options);
|
|
1705
2238
|
}
|
|
1706
|
-
return await implementation.uploadFile(host, port, apiKey, tempFilePath, filename, print);
|
|
2239
|
+
return await implementation.uploadFile(host, port, apiKey, tempFilePath, filename, print, options);
|
|
1707
2240
|
}
|
|
1708
2241
|
finally {
|
|
1709
|
-
if (
|
|
1710
|
-
fs.
|
|
1711
|
-
}
|
|
2242
|
+
if (scratchDir)
|
|
2243
|
+
fs.rmSync(scratchDir, { recursive: true, force: true });
|
|
1712
2244
|
}
|
|
1713
2245
|
}
|
|
1714
|
-
async startPrint(host, port, type, apiKey, bambuSerial, bambuToken, gcodeFilename) {
|
|
2246
|
+
async startPrint(host, port, type, apiKey, bambuSerial, bambuToken, gcodeFilename, options = {}) {
|
|
1715
2247
|
const implementation = this.printerFactory.getImplementation(type);
|
|
1716
2248
|
if (type.toLowerCase() === "bambu") {
|
|
1717
2249
|
const bambuApiKey = `${bambuSerial}:${bambuToken}`;
|
|
1718
|
-
return await implementation.startJob(host, port, bambuApiKey, gcodeFilename);
|
|
2250
|
+
return await implementation.startJob(host, port, bambuApiKey, gcodeFilename, options);
|
|
1719
2251
|
}
|
|
1720
|
-
return await implementation.startJob(host, port, apiKey, gcodeFilename);
|
|
2252
|
+
return await implementation.startJob(host, port, apiKey, gcodeFilename, options);
|
|
1721
2253
|
}
|
|
1722
2254
|
async cancelPrint(host, port, type, apiKey, bambuSerial, bambuToken) {
|
|
1723
2255
|
const implementation = this.printerFactory.getImplementation(type);
|
|
@@ -1727,15 +2259,48 @@ class ThreeDPrinterMCPServer {
|
|
|
1727
2259
|
}
|
|
1728
2260
|
return await implementation.cancelJob(host, port, apiKey);
|
|
1729
2261
|
}
|
|
1730
|
-
async setPrinterTemperature(host, port, type, apiKey, bambuSerial, bambuToken, component, temperature) {
|
|
2262
|
+
async setPrinterTemperature(host, port, type, apiKey, bambuSerial, bambuToken, component, temperature, options = {}) {
|
|
1731
2263
|
const implementation = this.printerFactory.getImplementation(type);
|
|
1732
2264
|
if (type.toLowerCase() === "bambu") {
|
|
1733
2265
|
const bambuApiKey = `${bambuSerial}:${bambuToken}`;
|
|
1734
|
-
return implementation.setTemperature(host, port, bambuApiKey, component, temperature);
|
|
2266
|
+
return implementation.setTemperature(host, port, bambuApiKey, component, temperature, options);
|
|
1735
2267
|
}
|
|
1736
|
-
return implementation.setTemperature(host, port, apiKey, component, temperature);
|
|
2268
|
+
return implementation.setTemperature(host, port, apiKey, component, temperature, options);
|
|
1737
2269
|
}
|
|
1738
|
-
toStructuredToolError(tool, message) {
|
|
2270
|
+
toStructuredToolError(tool, message, error) {
|
|
2271
|
+
// Slicer failures are local profile/slicer problems, never printer connectivity.
|
|
2272
|
+
if (error instanceof SlicerError) {
|
|
2273
|
+
return {
|
|
2274
|
+
status: "error",
|
|
2275
|
+
retryable: false,
|
|
2276
|
+
suggestion: error.suggestion,
|
|
2277
|
+
message,
|
|
2278
|
+
tool,
|
|
2279
|
+
slicer: { phase: error.phase, ...error.details },
|
|
2280
|
+
};
|
|
2281
|
+
}
|
|
2282
|
+
if (SLICING_TOOLS.has(tool)) {
|
|
2283
|
+
return {
|
|
2284
|
+
status: "error",
|
|
2285
|
+
retryable: false,
|
|
2286
|
+
suggestion: "Check the slicing arguments, template, and profile paths named in the error, then retry.",
|
|
2287
|
+
message,
|
|
2288
|
+
tool,
|
|
2289
|
+
};
|
|
2290
|
+
}
|
|
2291
|
+
if (isCredentialRejection(message)) {
|
|
2292
|
+
return { status: "error", retryable: false, suggestion: credentialSuggestion(), message, tool };
|
|
2293
|
+
}
|
|
2294
|
+
// Safety refusals stop before the printer is touched; retrying unchanged repeats the refusal.
|
|
2295
|
+
if (/nothing was (?:sent|uploaded)|no command was sent|print safety|hardware limit|material policy|declared material|confirmation was declined|are refused|is refused|refused because|refused until|refused before|does not match the job|is ambiguous|is missing or ambiguous|rejected the print command/i.test(message)) {
|
|
2296
|
+
return {
|
|
2297
|
+
status: "error",
|
|
2298
|
+
retryable: false,
|
|
2299
|
+
suggestion: "A safety check stopped this before anything reached the printer. Read the reason, fix the file, arguments, or printer state, and ask again.",
|
|
2300
|
+
message,
|
|
2301
|
+
tool,
|
|
2302
|
+
};
|
|
2303
|
+
}
|
|
1739
2304
|
const isInputError = message.toLowerCase().includes("missing required") ||
|
|
1740
2305
|
message.toLowerCase().includes("invalid") ||
|
|
1741
2306
|
message.toLowerCase().includes("unsupported");
|
|
@@ -1847,57 +2412,32 @@ class ThreeDPrinterMCPServer {
|
|
|
1847
2412
|
this.httpRuntime = { transport, httpServer };
|
|
1848
2413
|
console.error(`3D Printer MCP server running on streamable-http at http://${this.runtimeConfig.httpHost}:${this.runtimeConfig.httpPort}${this.runtimeConfig.httpPath}`);
|
|
1849
2414
|
}
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
};
|
|
2415
|
+
/**
|
|
2416
|
+
* Bridge commands and path-derived bridge executables are machine-local
|
|
2417
|
+
* settings. Accepting their selectors as tool arguments means anything that
|
|
2418
|
+
* can steer the model — a model description, a README in a downloaded
|
|
2419
|
+
* archive, 3MF metadata — can choose the program this server runs. They are
|
|
2420
|
+
* read from environment configuration by default and only accepted for
|
|
2421
|
+
* execution when MCP_ALLOW_EXECUTABLE_ARG is set. The older
|
|
2422
|
+
* MCP_ALLOW_BRIDGE_COMMAND_ARG name remains a compatibility alias only for
|
|
2423
|
+
* bridge commands and path selectors used to derive a bridge executable.
|
|
2424
|
+
*/
|
|
2425
|
+
resolveExecutableSelectorArg(rawValue, toolName, argumentName, executionRequested = true, allowLegacyBridgeArg = false) {
|
|
2426
|
+
if (rawValue === undefined) {
|
|
2427
|
+
return undefined;
|
|
1864
2428
|
}
|
|
1865
|
-
|
|
1866
|
-
|
|
2429
|
+
const value = String(rawValue);
|
|
2430
|
+
if (value.length === 0) {
|
|
2431
|
+
return undefined;
|
|
1867
2432
|
}
|
|
1868
|
-
const
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
child.stdout.setEncoding("utf8");
|
|
1877
|
-
child.stderr.setEncoding("utf8");
|
|
1878
|
-
child.stdout.on("data", (chunk) => {
|
|
1879
|
-
stdout += chunk;
|
|
1880
|
-
});
|
|
1881
|
-
child.stderr.on("data", (chunk) => {
|
|
1882
|
-
stderr += chunk;
|
|
1883
|
-
});
|
|
1884
|
-
child.on("error", reject);
|
|
1885
|
-
child.on("close", (code) => {
|
|
1886
|
-
if (code === 0) {
|
|
1887
|
-
resolve({ stdout, stderr });
|
|
1888
|
-
return;
|
|
1889
|
-
}
|
|
1890
|
-
reject(new Error(`Blender bridge command exited with code ${code ?? "unknown"}${stderr.trim() ? `: ${stderr.trim()}` : ""}`));
|
|
1891
|
-
});
|
|
1892
|
-
child.stdin.write(JSON.stringify(payload));
|
|
1893
|
-
child.stdin.end();
|
|
1894
|
-
});
|
|
1895
|
-
return {
|
|
1896
|
-
status: "executed",
|
|
1897
|
-
bridgeCommand: command,
|
|
1898
|
-
stdout: result.stdout?.toString() ?? "",
|
|
1899
|
-
stderr: result.stderr?.toString() ?? ""
|
|
1900
|
-
};
|
|
2433
|
+
const isAllowed = this.runtimeConfig.allowExecutableArg ||
|
|
2434
|
+
(allowLegacyBridgeArg && this.runtimeConfig.allowBridgeCommandArg);
|
|
2435
|
+
if (executionRequested && !isAllowed) {
|
|
2436
|
+
throw new Error(`${toolName}: the ${argumentName} argument cannot select an executable by default. ` +
|
|
2437
|
+
"Executable paths and commands are read from server environment configuration. Set " +
|
|
2438
|
+
`MCP_ALLOW_EXECUTABLE_ARG=1 to accept ${argumentName} for process execution.`);
|
|
2439
|
+
}
|
|
2440
|
+
return value;
|
|
1901
2441
|
}
|
|
1902
2442
|
async close() {
|
|
1903
2443
|
await this.printerFactory.disconnectAll();
|