bambu-printer-mcp 1.0.5 → 1.0.8
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/README.md +485 -26
- package/dist/bambu-network-bridge.d.ts +63 -0
- package/dist/bambu-network-bridge.js +393 -0
- package/dist/index.js +562 -23
- package/dist/printers/bambu.js +6 -8
- package/dist/stl/stl-manipulator.d.ts +8 -4
- package/dist/stl/stl-manipulator.js +119 -30
- package/package.json +1 -1
- package/src/bambu-network-bridge.ts +526 -0
- package/src/index.ts +671 -26
- package/src/printers/bambu.ts +6 -8
- package/src/stl/stl-manipulator.ts +143 -33
package/dist/index.js
CHANGED
|
@@ -8,13 +8,15 @@ import fs from "fs";
|
|
|
8
8
|
import path from "path";
|
|
9
9
|
import { createServer as createHttpServer } from "node:http";
|
|
10
10
|
import { randomUUID } from "node:crypto";
|
|
11
|
-
import { STLManipulator } from "./stl/stl-manipulator.js";
|
|
11
|
+
import { STLManipulator, SLICER_TYPES, normalizeSlicerType, } from "./stl/stl-manipulator.js";
|
|
12
|
+
import { BambuNetworkBridge } from "./bambu-network-bridge.js";
|
|
12
13
|
import { parse3MF } from './3mf_parser.js';
|
|
13
14
|
import { BambuImplementation } from "./printers/bambu.js";
|
|
14
15
|
dotenv.config();
|
|
15
16
|
const DEFAULT_HOST = process.env.PRINTER_HOST || "localhost";
|
|
16
17
|
const DEFAULT_BAMBU_SERIAL = process.env.BAMBU_SERIAL || "";
|
|
17
18
|
const DEFAULT_BAMBU_TOKEN = process.env.BAMBU_TOKEN || "";
|
|
19
|
+
const DEFAULT_BAMBU_DEV_ID = process.env.BAMBU_DEV_ID || DEFAULT_BAMBU_SERIAL;
|
|
18
20
|
const TEMP_DIR = process.env.TEMP_DIR || path.join(process.cwd(), "temp");
|
|
19
21
|
// Printer model and bed type
|
|
20
22
|
const DEFAULT_BAMBU_MODEL = process.env.BAMBU_MODEL?.trim().toLowerCase() || "";
|
|
@@ -48,8 +50,48 @@ function resolveBedType(argsBedType) {
|
|
|
48
50
|
}
|
|
49
51
|
// Slicer configuration (defaults to bambustudio)
|
|
50
52
|
const DEFAULT_SLICER_TYPE = process.env.SLICER_TYPE || "bambustudio";
|
|
51
|
-
const DEFAULT_SLICER_PATH = process.env.SLICER_PATH || "/Applications/BambuStudio.app/Contents/MacOS/BambuStudio";
|
|
52
53
|
const DEFAULT_SLICER_PROFILE = process.env.SLICER_PROFILE || "";
|
|
54
|
+
const SLICER_SCHEMA_VALUES = [
|
|
55
|
+
...SLICER_TYPES,
|
|
56
|
+
"fulu-orca",
|
|
57
|
+
"fulu-orcaslicer",
|
|
58
|
+
"orca-studio",
|
|
59
|
+
"orca-bambulab",
|
|
60
|
+
];
|
|
61
|
+
function firstExistingPath(paths, fallback) {
|
|
62
|
+
return paths.find((candidate) => fs.existsSync(candidate)) || fallback;
|
|
63
|
+
}
|
|
64
|
+
function defaultSlicerPathFor(slicerType) {
|
|
65
|
+
if (slicerType === "orcaslicer" || slicerType === "orcaslicer-bambulab") {
|
|
66
|
+
if (process.platform === "darwin") {
|
|
67
|
+
return firstExistingPath([
|
|
68
|
+
"/Applications/OrcaSlicer.app/Contents/MacOS/OrcaSlicer",
|
|
69
|
+
"/Applications/Orca Studio.app/Contents/MacOS/Orca Studio",
|
|
70
|
+
"/Applications/OrcaSlicer-BMCU.app/Contents/MacOS/OrcaSlicer",
|
|
71
|
+
], "/Applications/OrcaSlicer.app/Contents/MacOS/OrcaSlicer");
|
|
72
|
+
}
|
|
73
|
+
if (process.platform === "win32") {
|
|
74
|
+
return "C:\\Program Files\\OrcaSlicer\\OrcaSlicer.exe";
|
|
75
|
+
}
|
|
76
|
+
return "OrcaSlicer";
|
|
77
|
+
}
|
|
78
|
+
if (slicerType === "bambustudio") {
|
|
79
|
+
if (process.platform === "darwin") {
|
|
80
|
+
return "/Applications/BambuStudio.app/Contents/MacOS/BambuStudio";
|
|
81
|
+
}
|
|
82
|
+
if (process.platform === "win32") {
|
|
83
|
+
return "C:\\Program Files\\Bambu Studio\\bambu-studio.exe";
|
|
84
|
+
}
|
|
85
|
+
return "BambuStudio";
|
|
86
|
+
}
|
|
87
|
+
return slicerType;
|
|
88
|
+
}
|
|
89
|
+
function resolveSlicerConfig(args) {
|
|
90
|
+
const slicerType = normalizeSlicerType(String(args?.slicer_type || DEFAULT_SLICER_TYPE));
|
|
91
|
+
const slicerPath = String(args?.slicer_path || process.env.SLICER_PATH || defaultSlicerPathFor(slicerType));
|
|
92
|
+
const slicerProfile = String(args?.slicer_profile || DEFAULT_SLICER_PROFILE);
|
|
93
|
+
return { slicerType, slicerPath, slicerProfile };
|
|
94
|
+
}
|
|
53
95
|
function parseBooleanEnv(rawValue, fallback) {
|
|
54
96
|
if (rawValue === undefined)
|
|
55
97
|
return fallback;
|
|
@@ -96,6 +138,116 @@ function readRuntimeConfig() {
|
|
|
96
138
|
blenderBridgeCommand: process.env.BLENDER_MCP_BRIDGE_COMMAND?.trim() || undefined,
|
|
97
139
|
};
|
|
98
140
|
}
|
|
141
|
+
function expandUserPath(rawPath) {
|
|
142
|
+
const trimmed = rawPath.trim();
|
|
143
|
+
if (trimmed === "~") {
|
|
144
|
+
return process.env.HOME || trimmed;
|
|
145
|
+
}
|
|
146
|
+
if (trimmed.startsWith("~/")) {
|
|
147
|
+
return path.join(process.env.HOME || "", trimmed.slice(2));
|
|
148
|
+
}
|
|
149
|
+
return path.resolve(trimmed);
|
|
150
|
+
}
|
|
151
|
+
function readableFilePathFromString(value) {
|
|
152
|
+
if (!value.trim() || value.includes("\n") || value.includes("\r")) {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
const candidate = expandUserPath(value);
|
|
156
|
+
try {
|
|
157
|
+
return fs.statSync(candidate).isFile() ? candidate : undefined;
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function looksLikeGcodeFilePath(value) {
|
|
164
|
+
const trimmed = value.trim();
|
|
165
|
+
if (!trimmed || trimmed.includes("\n") || trimmed.includes("\r")) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
return (trimmed.startsWith("/") ||
|
|
169
|
+
trimmed.startsWith("./") ||
|
|
170
|
+
trimmed.startsWith("../") ||
|
|
171
|
+
trimmed.startsWith("~/") ||
|
|
172
|
+
trimmed.includes("\\") ||
|
|
173
|
+
/\.(gcode|gco|gc)$/i.test(trimmed));
|
|
174
|
+
}
|
|
175
|
+
function requireReadableFilePath(rawPath, label) {
|
|
176
|
+
const candidate = expandUserPath(rawPath);
|
|
177
|
+
try {
|
|
178
|
+
if (!fs.statSync(candidate).isFile()) {
|
|
179
|
+
throw new Error(`${label} is not a file: ${candidate}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
if (error.message.startsWith(`${label} is not a file:`)) {
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
throw new Error(`${label} does not exist or is not readable: ${candidate}`);
|
|
187
|
+
}
|
|
188
|
+
return candidate;
|
|
189
|
+
}
|
|
190
|
+
function writeGcodeContentToTempFile(filename, gcode) {
|
|
191
|
+
fs.mkdirSync(TEMP_DIR, { recursive: true });
|
|
192
|
+
const cleanupDir = fs.mkdtempSync(path.join(TEMP_DIR, "upload-gcode-"));
|
|
193
|
+
const safeName = path.basename(filename.replace(/^\/+/, "")) || "upload.gcode";
|
|
194
|
+
const filePath = path.join(cleanupDir, safeName);
|
|
195
|
+
fs.writeFileSync(filePath, gcode);
|
|
196
|
+
return { filePath, cleanupDir };
|
|
197
|
+
}
|
|
198
|
+
function resolveUploadGcodeSource(args) {
|
|
199
|
+
const gcodePath = args.gcode_path !== undefined ? String(args.gcode_path) : "";
|
|
200
|
+
const gcode = args.gcode !== undefined ? String(args.gcode) : "";
|
|
201
|
+
if (gcodePath && gcode) {
|
|
202
|
+
throw new Error("Provide either gcode_path or gcode, not both.");
|
|
203
|
+
}
|
|
204
|
+
if (gcodePath) {
|
|
205
|
+
return { filePath: requireReadableFilePath(gcodePath, "gcode_path") };
|
|
206
|
+
}
|
|
207
|
+
if (!gcode) {
|
|
208
|
+
throw new Error("Missing required parameter: gcode or gcode_path");
|
|
209
|
+
}
|
|
210
|
+
const detectedPath = readableFilePathFromString(gcode);
|
|
211
|
+
if (detectedPath) {
|
|
212
|
+
return { filePath: detectedPath };
|
|
213
|
+
}
|
|
214
|
+
if (looksLikeGcodeFilePath(gcode)) {
|
|
215
|
+
throw new Error("gcode looks like a local G-code path, but the file is not readable. " +
|
|
216
|
+
"Pass readable gcode_path or literal G-code content.");
|
|
217
|
+
}
|
|
218
|
+
return writeGcodeContentToTempFile(String(args.filename), gcode);
|
|
219
|
+
}
|
|
220
|
+
const BAMBU_NETWORK_PRINT_METHODS = [
|
|
221
|
+
"start_print",
|
|
222
|
+
"start_local_print",
|
|
223
|
+
"start_local_print_with_record",
|
|
224
|
+
"start_send_gcode_to_sdcard",
|
|
225
|
+
"start_sdcard_print",
|
|
226
|
+
];
|
|
227
|
+
function resolveBambuNetworkPrintMethod(rawMethod, connectionType) {
|
|
228
|
+
const defaultMethod = connectionType === "lan" ? "start_local_print" : "start_print";
|
|
229
|
+
const method = (rawMethod || defaultMethod).trim();
|
|
230
|
+
if (!BAMBU_NETWORK_PRINT_METHODS.includes(method)) {
|
|
231
|
+
throw new Error(`Invalid bambu_network_method: "${method}". Valid methods: ${BAMBU_NETWORK_PRINT_METHODS.join(", ")}`);
|
|
232
|
+
}
|
|
233
|
+
return method;
|
|
234
|
+
}
|
|
235
|
+
function toBridgeMethod(method) {
|
|
236
|
+
return `net.${method}`;
|
|
237
|
+
}
|
|
238
|
+
function stringifyBridgeJson(value) {
|
|
239
|
+
if (value === undefined || value === null)
|
|
240
|
+
return undefined;
|
|
241
|
+
if (typeof value === "string")
|
|
242
|
+
return value;
|
|
243
|
+
return JSON.stringify(value);
|
|
244
|
+
}
|
|
245
|
+
function redactPrintParams(params) {
|
|
246
|
+
return {
|
|
247
|
+
...params,
|
|
248
|
+
password: params.password ? "[redacted]" : "",
|
|
249
|
+
};
|
|
250
|
+
}
|
|
99
251
|
if (!fs.existsSync(TEMP_DIR)) {
|
|
100
252
|
fs.mkdirSync(TEMP_DIR, { recursive: true });
|
|
101
253
|
}
|
|
@@ -112,6 +264,7 @@ class BambuPrinterMCPServer {
|
|
|
112
264
|
}
|
|
113
265
|
});
|
|
114
266
|
this.bambu = new BambuImplementation();
|
|
267
|
+
this.bambuNetwork = new BambuNetworkBridge();
|
|
115
268
|
this.stlManipulator = new STLManipulator(TEMP_DIR);
|
|
116
269
|
this.setupHandlers();
|
|
117
270
|
this.setupErrorHandling();
|
|
@@ -134,7 +287,7 @@ class BambuPrinterMCPServer {
|
|
|
134
287
|
if (fromArgs) {
|
|
135
288
|
return validateBambuModel(fromArgs);
|
|
136
289
|
}
|
|
137
|
-
// No model from args or env
|
|
290
|
+
// No model from args or env - ask the user via elicitation
|
|
138
291
|
try {
|
|
139
292
|
const result = await this.server.elicitInput({
|
|
140
293
|
mode: "form",
|
|
@@ -167,7 +320,7 @@ class BambuPrinterMCPServer {
|
|
|
167
320
|
throw new Error("Printer model selection was cancelled. Cannot proceed without knowing the printer model.");
|
|
168
321
|
}
|
|
169
322
|
catch (elicitError) {
|
|
170
|
-
// Elicitation not supported by this client
|
|
323
|
+
// Elicitation not supported by this client - fall back to a clear error
|
|
171
324
|
const msg = elicitError?.message || String(elicitError);
|
|
172
325
|
if (elicitError?.code === -32601 || elicitError?.code === -32600 ||
|
|
173
326
|
msg.includes("does not support") || msg.includes("elicitation")) {
|
|
@@ -178,6 +331,182 @@ class BambuPrinterMCPServer {
|
|
|
178
331
|
throw elicitError;
|
|
179
332
|
}
|
|
180
333
|
}
|
|
334
|
+
bridgeOptionsFromArgs(args) {
|
|
335
|
+
return {
|
|
336
|
+
bridgeCommand: args?.bridge_command !== undefined ? String(args.bridge_command) : undefined,
|
|
337
|
+
configDir: args?.bambu_network_config_dir !== undefined ? String(args.bambu_network_config_dir) : undefined,
|
|
338
|
+
countryCode: args?.country_code !== undefined ? String(args.country_code) : undefined,
|
|
339
|
+
userInfo: args?.user_info !== undefined ? String(args.user_info) : undefined,
|
|
340
|
+
timeoutMs: args?.timeout_ms !== undefined ? Number(args.timeout_ms) : undefined,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
async ensurePrintableThreeMFPath(args, printModel, printPreset) {
|
|
344
|
+
const { slicerType, slicerPath, slicerProfile } = resolveSlicerConfig(args);
|
|
345
|
+
let threeMFPath = String(args.three_mf_path);
|
|
346
|
+
const JSZip = (await import('jszip')).default;
|
|
347
|
+
const zipData = fs.readFileSync(threeMFPath);
|
|
348
|
+
const zip = await JSZip.loadAsync(zipData);
|
|
349
|
+
const hasGcode = Object.keys(zip.files).some(f => f.match(/Metadata\/plate_\d+\.gcode/i) || f.endsWith('.gcode'));
|
|
350
|
+
if (hasGcode) {
|
|
351
|
+
return { threeMFPath, autoSliced: false };
|
|
352
|
+
}
|
|
353
|
+
console.log(`3MF has no gcode - auto-slicing with ${slicerType} for ${printModel}`);
|
|
354
|
+
const autoSliceOptions = {
|
|
355
|
+
uptodate: true,
|
|
356
|
+
ensureOnBed: true,
|
|
357
|
+
minSave: true,
|
|
358
|
+
skipModifiedGcodes: true,
|
|
359
|
+
};
|
|
360
|
+
threeMFPath = await this.stlManipulator.sliceSTL(threeMFPath, slicerType, slicerPath, slicerProfile || undefined, undefined, printPreset, autoSliceOptions);
|
|
361
|
+
console.log("Auto-sliced to: " + threeMFPath);
|
|
362
|
+
return { threeMFPath, autoSliced: true };
|
|
363
|
+
}
|
|
364
|
+
async resolveAmsPrintSettings(threeMFPath, args) {
|
|
365
|
+
const parsed3MFData = await parse3MF(threeMFPath);
|
|
366
|
+
let parsedAmsMapping;
|
|
367
|
+
if (parsed3MFData.slicerConfig?.ams_mapping) {
|
|
368
|
+
const slots = Object.values(parsed3MFData.slicerConfig.ams_mapping)
|
|
369
|
+
.filter(v => typeof v === 'number');
|
|
370
|
+
if (slots.length > 0) {
|
|
371
|
+
parsedAmsMapping = slots.sort((a, b) => a - b);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
let finalAmsMapping = parsedAmsMapping;
|
|
375
|
+
const explicitUseAMS = args?.use_ams !== undefined;
|
|
376
|
+
let useAMS = explicitUseAMS ? Boolean(args.use_ams) : (!!finalAmsMapping && finalAmsMapping.length > 0);
|
|
377
|
+
if (args?.ams_mapping) {
|
|
378
|
+
let userMappingOverride;
|
|
379
|
+
if (Array.isArray(args.ams_mapping)) {
|
|
380
|
+
userMappingOverride = args.ams_mapping.filter((v) => typeof v === 'number');
|
|
381
|
+
}
|
|
382
|
+
else if (typeof args.ams_mapping === 'object') {
|
|
383
|
+
userMappingOverride = Object.values(args.ams_mapping)
|
|
384
|
+
.filter((v) => typeof v === 'number')
|
|
385
|
+
.sort((a, b) => a - b);
|
|
386
|
+
}
|
|
387
|
+
if (userMappingOverride && userMappingOverride.length > 0) {
|
|
388
|
+
finalAmsMapping = userMappingOverride;
|
|
389
|
+
useAMS = true;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (args?.use_ams === false) {
|
|
393
|
+
finalAmsMapping = undefined;
|
|
394
|
+
useAMS = false;
|
|
395
|
+
}
|
|
396
|
+
if ((!finalAmsMapping || finalAmsMapping.length === 0) && !explicitUseAMS) {
|
|
397
|
+
useAMS = false;
|
|
398
|
+
}
|
|
399
|
+
return { useAMS, finalAmsMapping };
|
|
400
|
+
}
|
|
401
|
+
async print3mfViaBambuNetwork(args, host, bambuSerial, bambuToken) {
|
|
402
|
+
if (!args?.three_mf_path) {
|
|
403
|
+
throw new Error("Missing required parameter: three_mf_path");
|
|
404
|
+
}
|
|
405
|
+
const printModel = await this.resolveBambuModel(args?.bambu_model);
|
|
406
|
+
const printBedType = resolveBedType(args?.bed_type);
|
|
407
|
+
const printNozzle = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
408
|
+
const printPreset = BAMBU_MODEL_PRESETS[printModel]?.(printNozzle);
|
|
409
|
+
const plateIndex = args?.plate_index !== undefined ? Number(args.plate_index) : 0;
|
|
410
|
+
if (!Number.isInteger(plateIndex) || plateIndex < 0) {
|
|
411
|
+
throw new Error("plate_index must be a non-negative integer.");
|
|
412
|
+
}
|
|
413
|
+
const connectionType = String(args?.connection_type || "cloud").trim().toLowerCase();
|
|
414
|
+
if (!["cloud", "lan"].includes(connectionType)) {
|
|
415
|
+
throw new Error('connection_type must be "cloud" or "lan".');
|
|
416
|
+
}
|
|
417
|
+
const bridgePrintMethod = resolveBambuNetworkPrintMethod(args?.bambu_network_method !== undefined ? String(args.bambu_network_method) : undefined, connectionType);
|
|
418
|
+
const bridgeMethod = toBridgeMethod(bridgePrintMethod);
|
|
419
|
+
const isLocalBridgePrint = bridgePrintMethod !== "start_print";
|
|
420
|
+
const devId = String(args?.dev_id || bambuSerial || DEFAULT_BAMBU_DEV_ID).trim();
|
|
421
|
+
if (!devId) {
|
|
422
|
+
throw new Error("dev_id is required for FULU BambuNetwork printing. Pass dev_id or set BAMBU_DEV_ID/BAMBU_SERIAL.");
|
|
423
|
+
}
|
|
424
|
+
const devIp = String(args?.dev_ip || args?.host || host || "").trim();
|
|
425
|
+
const explicitPassword = String(args?.password || args?.bambu_token || "").trim();
|
|
426
|
+
const password = isLocalBridgePrint ? (explicitPassword || String(bambuToken || "").trim()) : explicitPassword;
|
|
427
|
+
if (isLocalBridgePrint && (!devIp || devIp === "localhost")) {
|
|
428
|
+
throw new Error("dev_ip or host is required for FULU BambuNetwork LAN/local print methods.");
|
|
429
|
+
}
|
|
430
|
+
if (isLocalBridgePrint && !password) {
|
|
431
|
+
throw new Error("bambu_token/access code is required for FULU BambuNetwork LAN/local print methods.");
|
|
432
|
+
}
|
|
433
|
+
const { threeMFPath, autoSliced } = await this.ensurePrintableThreeMFPath(args, printModel, printPreset);
|
|
434
|
+
const { useAMS, finalAmsMapping } = await this.resolveAmsPrintSettings(threeMFPath, args);
|
|
435
|
+
const threeMfFilename = path.basename(threeMFPath);
|
|
436
|
+
const projectName = String(args?.project_name || threeMfFilename.replace(/\.3mf$/i, ''));
|
|
437
|
+
const presetName = String(args?.preset_name || `${projectName}_plate_${plateIndex + 1}`);
|
|
438
|
+
const clientJobId = args?.client_job_id !== undefined ? Number(args.client_job_id) : Date.now();
|
|
439
|
+
const amsMapping = stringifyBridgeJson(args?.ams_mapping_bridge ?? finalAmsMapping);
|
|
440
|
+
const params = {
|
|
441
|
+
dev_id: devId,
|
|
442
|
+
task_name: String(args?.task_name || projectName),
|
|
443
|
+
project_name: projectName,
|
|
444
|
+
preset_name: presetName,
|
|
445
|
+
filename: threeMFPath,
|
|
446
|
+
config_filename: String(args?.config_filename || threeMFPath),
|
|
447
|
+
plate_index: plateIndex + 1,
|
|
448
|
+
ftp_folder: String(args?.ftp_folder || ""),
|
|
449
|
+
ftp_file: String(args?.ftp_file || ""),
|
|
450
|
+
ftp_file_md5: String(args?.ftp_file_md5 || ""),
|
|
451
|
+
nozzle_mapping: stringifyBridgeJson(args?.nozzle_mapping) || "",
|
|
452
|
+
ams_mapping: amsMapping || "",
|
|
453
|
+
ams_mapping2: stringifyBridgeJson(args?.ams_mapping2) || "",
|
|
454
|
+
ams_mapping_info: stringifyBridgeJson(args?.ams_mapping_info) || "",
|
|
455
|
+
nozzles_info: stringifyBridgeJson(args?.nozzles_info) || "",
|
|
456
|
+
connection_type: connectionType,
|
|
457
|
+
comments: String(args?.comments || ""),
|
|
458
|
+
origin_profile_id: args?.origin_profile_id !== undefined ? Number(args.origin_profile_id) : 0,
|
|
459
|
+
stl_design_id: args?.stl_design_id !== undefined ? Number(args.stl_design_id) : 0,
|
|
460
|
+
origin_model_id: String(args?.origin_model_id || ""),
|
|
461
|
+
print_type: String(args?.print_type || "from_normal"),
|
|
462
|
+
dst_file: String(args?.dst_file || ""),
|
|
463
|
+
dev_name: String(args?.dev_name || ""),
|
|
464
|
+
dev_ip: devIp,
|
|
465
|
+
use_ssl_for_ftp: args?.use_ssl_for_ftp !== undefined ? Boolean(args.use_ssl_for_ftp) : true,
|
|
466
|
+
use_ssl_for_mqtt: args?.use_ssl_for_mqtt !== undefined ? Boolean(args.use_ssl_for_mqtt) : true,
|
|
467
|
+
username: String(args?.username || "bblp"),
|
|
468
|
+
password,
|
|
469
|
+
task_bed_leveling: args?.bed_leveling !== undefined ? Boolean(args.bed_leveling) : true,
|
|
470
|
+
task_flow_cali: args?.flow_calibration !== undefined ? Boolean(args.flow_calibration) : true,
|
|
471
|
+
task_vibration_cali: args?.vibration_calibration !== undefined ? Boolean(args.vibration_calibration) : true,
|
|
472
|
+
task_layer_inspect: args?.layer_inspect !== undefined ? Boolean(args.layer_inspect) : false,
|
|
473
|
+
task_record_timelapse: args?.timelapse !== undefined ? Boolean(args.timelapse) : false,
|
|
474
|
+
task_use_ams: useAMS,
|
|
475
|
+
task_bed_type: printBedType,
|
|
476
|
+
extra_options: stringifyBridgeJson(args?.extra_options) || "",
|
|
477
|
+
auto_bed_leveling: args?.auto_bed_leveling !== undefined ? Number(args.auto_bed_leveling) : 0,
|
|
478
|
+
auto_flow_cali: args?.auto_flow_cali !== undefined ? Number(args.auto_flow_cali) : 0,
|
|
479
|
+
auto_offset_cali: args?.auto_offset_cali !== undefined ? Number(args.auto_offset_cali) : 0,
|
|
480
|
+
extruder_cali_manual_mode: args?.extruder_cali_manual_mode !== undefined ? Number(args.extruder_cali_manual_mode) : -1,
|
|
481
|
+
task_ext_change_assist: args?.external_change_assist !== undefined ? Boolean(args.external_change_assist) : false,
|
|
482
|
+
try_emmc_print: args?.try_emmc_print !== undefined ? Boolean(args.try_emmc_print) : false,
|
|
483
|
+
};
|
|
484
|
+
const bridgeResult = await this.bambuNetwork.callWithAgent(bridgeMethod, { client_job_id: clientJobId, params }, this.bridgeOptionsFromArgs(args));
|
|
485
|
+
if (typeof bridgeResult === "object" && bridgeResult !== null && bridgeResult.ok === false) {
|
|
486
|
+
throw new Error(`FULU BambuNetwork bridge method ${bridgeMethod} failed: ${String(bridgeResult.error || "unknown bridge error")}`);
|
|
487
|
+
}
|
|
488
|
+
if (typeof bridgeResult === "object" &&
|
|
489
|
+
bridgeResult !== null &&
|
|
490
|
+
typeof bridgeResult.value === "number" &&
|
|
491
|
+
bridgeResult.value !== 0) {
|
|
492
|
+
const value = bridgeResult.value;
|
|
493
|
+
throw new Error(`FULU BambuNetwork bridge method ${bridgeMethod} returned non-zero result ${value}.`);
|
|
494
|
+
}
|
|
495
|
+
return {
|
|
496
|
+
status: "success",
|
|
497
|
+
message: `FULU BambuNetwork ${bridgePrintMethod} command for ${threeMfFilename} sent successfully.`,
|
|
498
|
+
bridgeMethod,
|
|
499
|
+
bridgeResult,
|
|
500
|
+
clientJobId,
|
|
501
|
+
autoSliced,
|
|
502
|
+
projectName,
|
|
503
|
+
plateIndex,
|
|
504
|
+
bridgePlateIndex: plateIndex + 1,
|
|
505
|
+
useAMS,
|
|
506
|
+
amsMapping: finalAmsMapping,
|
|
507
|
+
params: redactPrintParams(params),
|
|
508
|
+
};
|
|
509
|
+
}
|
|
181
510
|
setupResourceHandlers() {
|
|
182
511
|
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
183
512
|
return {
|
|
@@ -329,12 +658,13 @@ class BambuPrinterMCPServer {
|
|
|
329
658
|
},
|
|
330
659
|
slicer_type: {
|
|
331
660
|
type: "string",
|
|
332
|
-
|
|
661
|
+
enum: SLICER_SCHEMA_VALUES,
|
|
662
|
+
description: "Type of slicer to use. Bambu-compatible choices (bambustudio, orcaslicer, orcaslicer-bambulab) export sliced 3MF; aliases such as fulu-orca and orca-studio are accepted."
|
|
333
663
|
},
|
|
334
|
-
slicer_path: { type: "string", description: "Path to the slicer executable (default: value from env)" },
|
|
664
|
+
slicer_path: { type: "string", description: "Path to the slicer executable (default: value from env or a platform default)" },
|
|
335
665
|
slicer_profile: { type: "string", description: "Path to the slicer profile/config file (optional, overrides bambu_model preset)" },
|
|
336
666
|
nozzle_diameter: { type: "string", description: "Nozzle diameter in mm (default: 0.4)" },
|
|
337
|
-
uptodate: { type: "boolean", description: "Refresh 3MF preset configs to match the latest
|
|
667
|
+
uptodate: { type: "boolean", description: "Refresh 3MF preset configs to match the latest Bambu-compatible slicer presets. Use when slicing downloaded or older 3MF files to prevent stale-config failures." },
|
|
338
668
|
repetitions: { type: "number", description: "Print N identical copies of the model. Each copy gets its own plate placement. Example: 3 prints three copies." },
|
|
339
669
|
orient: { type: "boolean", description: "Auto-orient the model for optimal printability (minimize supports, maximize bed adhesion). Recommended for raw STL imports that lack a pre-set orientation." },
|
|
340
670
|
arrange: { type: "boolean", description: "Auto-arrange all objects on the build plate with optimal spacing. Recommended when importing STLs or adding multiple objects. Set false to preserve existing plate layout." },
|
|
@@ -342,6 +672,7 @@ class BambuPrinterMCPServer {
|
|
|
342
672
|
clone_objects: { type: "string", description: "Duplicate specific objects on the plate. Comma-separated clone counts per object index, e.g. '1,3,1,10' clones object 0 once, object 1 three times, etc." },
|
|
343
673
|
skip_objects: { type: "string", description: "Skip specific objects during slicing by index. Comma-separated, e.g. '3,5,10'. Useful for multi-object 3MFs where you only want to print some parts." },
|
|
344
674
|
load_filaments: { type: "string", description: "Override filament profiles. Semicolon-separated paths to filament JSON configs, e.g. 'pla_basic.json;petg_cf.json'." },
|
|
675
|
+
filament_profile: { type: "string", description: "Compatibility alias for load_filaments. Semicolon-separated Orca/Bambu filament profile JSON paths; flat self-contained profiles are safest because Orca does not resolve every inherited system filament setting from arbitrary file paths." },
|
|
345
676
|
load_filament_ids: { type: "string", description: "Map filaments to objects/parts. Comma-separated IDs matching load_filaments order, e.g. '1,2,3,1' assigns filament 1 to objects 0 and 3." },
|
|
346
677
|
enable_timelapse: { type: "boolean", description: "Insert timelapse parking moves into gcode. The toolhead parks at a fixed position each layer for camera capture. Adds ~10% print time." },
|
|
347
678
|
allow_mix_temp: { type: "boolean", description: "Allow filaments with different temperature requirements on the same plate. Required for multi-material prints mixing e.g. PLA and PETG." },
|
|
@@ -368,6 +699,97 @@ class BambuPrinterMCPServer {
|
|
|
368
699
|
}
|
|
369
700
|
}
|
|
370
701
|
},
|
|
702
|
+
{
|
|
703
|
+
name: "bambu_network_bridge_status",
|
|
704
|
+
description: "Inspect or probe the FULU OrcaSlicer-bambulab BambuNetwork bridge runtime used for cloud and restored BambuNetwork printing.",
|
|
705
|
+
inputSchema: {
|
|
706
|
+
type: "object",
|
|
707
|
+
properties: {
|
|
708
|
+
connect: { type: "boolean", description: "When true, start the bridge command and run a handshake plus agent initialization probe." },
|
|
709
|
+
bridge_command: { type: "string", description: "Override command for the FULU bridge host or macOS/WSL wrapper; defaults to BAMBU_NETWORK_BRIDGE_COMMAND." },
|
|
710
|
+
bambu_network_config_dir: { type: "string", description: "Config/log directory used by the BambuNetwork agent; defaults to BAMBU_NETWORK_CONFIG_DIR or a user config directory." },
|
|
711
|
+
country_code: { type: "string", description: "BambuNetwork country code, such as US, used by the agent during startup." },
|
|
712
|
+
user_info: { type: "string", description: "Optional BambuNetwork user_info JSON string to pass to net.change_user after the agent starts." },
|
|
713
|
+
timeout_ms: { type: "number", description: "Bridge request timeout in milliseconds for the connect probe." }
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
},
|
|
717
|
+
{
|
|
718
|
+
name: "bambu_network_call",
|
|
719
|
+
description: "Call a raw FULU OrcaSlicer-bambulab BambuNetwork bridge method, optionally with an initialized network agent injected into the payload.",
|
|
720
|
+
inputSchema: {
|
|
721
|
+
type: "object",
|
|
722
|
+
properties: {
|
|
723
|
+
method: { type: "string", description: "FULU bridge method name, for example bridge.handshake, net.is_user_login, or net.get_user_selected_machine." },
|
|
724
|
+
payload: { type: "object", description: "JSON payload passed to the bridge method." },
|
|
725
|
+
with_agent: { type: "boolean", description: "When true, initialize a BambuNetwork agent and add its agent id to the payload before calling the method." },
|
|
726
|
+
bridge_command: { type: "string", description: "Override command for the FULU bridge host or macOS/WSL wrapper; defaults to BAMBU_NETWORK_BRIDGE_COMMAND." },
|
|
727
|
+
bambu_network_config_dir: { type: "string", description: "Config/log directory used by the BambuNetwork agent; defaults to BAMBU_NETWORK_CONFIG_DIR or a user config directory." },
|
|
728
|
+
country_code: { type: "string", description: "BambuNetwork country code, such as US, used by the agent during startup." },
|
|
729
|
+
user_info: { type: "string", description: "Optional BambuNetwork user_info JSON string to pass to net.change_user after the agent starts." },
|
|
730
|
+
timeout_ms: { type: "number", description: "Bridge request timeout in milliseconds." }
|
|
731
|
+
},
|
|
732
|
+
required: ["method"]
|
|
733
|
+
}
|
|
734
|
+
},
|
|
735
|
+
{
|
|
736
|
+
name: "print_3mf_bambu_network",
|
|
737
|
+
description: "Print a 3MF through FULU OrcaSlicer-bambulab's restored BambuNetwork path instead of the MCP LAN MQTT/FTPS path.",
|
|
738
|
+
inputSchema: {
|
|
739
|
+
type: "object",
|
|
740
|
+
properties: {
|
|
741
|
+
three_mf_path: { type: "string", description: "Path to the 3MF file to print; unsliced 3MFs are auto-sliced before sending." },
|
|
742
|
+
bambu_model: {
|
|
743
|
+
type: "string",
|
|
744
|
+
enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
|
|
745
|
+
description: "REQUIRED: Bambu Lab printer model. Ask the user if not known. Using the wrong model can damage the printer."
|
|
746
|
+
},
|
|
747
|
+
connection_type: { type: "string", enum: ["cloud", "lan"], description: "BambuNetwork connection type to put in FULU PrintParams; cloud uses restored internet printing, lan uses local bridge printing." },
|
|
748
|
+
bambu_network_method: { type: "string", enum: BAMBU_NETWORK_PRINT_METHODS, description: "FULU print method to invoke; defaults to start_print for cloud and start_local_print for lan." },
|
|
749
|
+
dev_id: { type: "string", description: "Bambu device id used by BambuNetwork; defaults to BAMBU_DEV_ID or BAMBU_SERIAL." },
|
|
750
|
+
dev_ip: { type: "string", description: "Printer IP address for LAN/local bridge methods; defaults to host when provided." },
|
|
751
|
+
host: { type: "string", description: "Printer host or IP address, used as dev_ip for LAN/local bridge methods." },
|
|
752
|
+
bambu_serial: { type: "string", description: "Fallback Bambu device id when dev_id is not supplied." },
|
|
753
|
+
bambu_token: { type: "string", description: "Printer access code/password for LAN/local bridge methods." },
|
|
754
|
+
username: { type: "string", description: "Printer username for LAN/local bridge methods; defaults to bblp." },
|
|
755
|
+
password: { type: "string", description: "Printer password/access code override for LAN/local bridge methods." },
|
|
756
|
+
bed_type: { type: "string", enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate"], description: "Bed plate type currently installed (default: textured_plate)." },
|
|
757
|
+
plate_index: { type: "number", description: "Zero-based plate index to print from the sliced 3MF; converted to FULU's one-based PrintParams plate_index." },
|
|
758
|
+
project_name: { type: "string", description: "Optional project name sent in FULU PrintParams; defaults to the 3MF filename without extension." },
|
|
759
|
+
preset_name: { type: "string", description: "Optional preset name sent in FULU PrintParams; defaults to project plus one-based plate index." },
|
|
760
|
+
task_name: { type: "string", description: "Optional BambuNetwork task name; defaults to the project name." },
|
|
761
|
+
config_filename: { type: "string", description: "Optional config 3MF path for cloud print; defaults to the same 3MF path." },
|
|
762
|
+
bridge_command: { type: "string", description: "Override command for the FULU bridge host or macOS/WSL wrapper; defaults to BAMBU_NETWORK_BRIDGE_COMMAND." },
|
|
763
|
+
bambu_network_config_dir: { type: "string", description: "Config/log directory used by the BambuNetwork agent; defaults to BAMBU_NETWORK_CONFIG_DIR or a user config directory." },
|
|
764
|
+
country_code: { type: "string", description: "BambuNetwork country code, such as US, used by the agent during startup." },
|
|
765
|
+
user_info: { type: "string", description: "Optional BambuNetwork user_info JSON string to pass to net.change_user after the agent starts." },
|
|
766
|
+
timeout_ms: { type: "number", description: "Bridge request timeout in milliseconds." },
|
|
767
|
+
slicer_type: { type: "string", enum: SLICER_SCHEMA_VALUES, description: "Slicer to use only if auto-slicing an unsliced 3MF; use orcaslicer-bambulab for FULU's fork." },
|
|
768
|
+
slicer_path: { type: "string", description: "Path to the slicer executable for auto-slicing; defaults to value from env or a platform default." },
|
|
769
|
+
slicer_profile: { type: "string", description: "Path to an optional slicer profile/config file for auto-slicing." },
|
|
770
|
+
nozzle_diameter: { type: "string", description: "Nozzle diameter in mm for auto-slicing (default: 0.4)." },
|
|
771
|
+
use_ams: { type: "boolean", description: "Whether to use the AMS; defaults to auto-detect from the 3MF mapping." },
|
|
772
|
+
ams_mapping: { type: "array", description: "AMS slot mapping array used by both local MCP printing and FULU PrintParams.", items: { type: "number" } },
|
|
773
|
+
ams_mapping_bridge: { type: "string", description: "Raw JSON string override for FULU PrintParams ams_mapping when the automatic array is not enough." },
|
|
774
|
+
ams_mapping2: { type: "string", description: "Raw JSON string for FULU PrintParams ams_mapping2, matching OrcaSlicer-bambulab's v1 AMS mapping field." },
|
|
775
|
+
ams_mapping_info: { type: "string", description: "Raw JSON string for FULU PrintParams ams_mapping_info, matching OrcaSlicer-bambulab's detailed AMS mapping field." },
|
|
776
|
+
nozzle_mapping: { type: "string", description: "Raw JSON string for FULU PrintParams nozzle_mapping." },
|
|
777
|
+
nozzles_info: { type: "string", description: "Raw JSON string for FULU PrintParams nozzles_info." },
|
|
778
|
+
bed_leveling: { type: "boolean", description: "Enable auto bed leveling in FULU PrintParams (default: true)." },
|
|
779
|
+
flow_calibration: { type: "boolean", description: "Enable flow calibration in FULU PrintParams (default: true)." },
|
|
780
|
+
vibration_calibration: { type: "boolean", description: "Enable vibration calibration in FULU PrintParams (default: true)." },
|
|
781
|
+
layer_inspect: { type: "boolean", description: "Enable first-layer inspection where supported (default: false for BambuNetwork bridge)." },
|
|
782
|
+
timelapse: { type: "boolean", description: "Enable timelapse recording in FULU PrintParams (default: false)." },
|
|
783
|
+
use_ssl_for_ftp: { type: "boolean", description: "Whether FULU local print should use SSL for FTP (default: true)." },
|
|
784
|
+
use_ssl_for_mqtt: { type: "boolean", description: "Whether FULU local print should use SSL for MQTT (default: true)." },
|
|
785
|
+
external_change_assist: { type: "boolean", description: "Enable FULU PrintParams task_ext_change_assist for external filament change assistance." },
|
|
786
|
+
try_emmc_print: { type: "boolean", description: "Enable FULU PrintParams try_emmc_print for printers that support internal storage printing." },
|
|
787
|
+
extra_options: { type: "string", description: "Raw JSON string or text for FULU PrintParams extra_options." },
|
|
788
|
+
client_job_id: { type: "number", description: "Optional client job id sent to the bridge; defaults to the current timestamp." }
|
|
789
|
+
},
|
|
790
|
+
required: ["three_mf_path", "bambu_model"]
|
|
791
|
+
}
|
|
792
|
+
},
|
|
371
793
|
{
|
|
372
794
|
name: "upload_gcode",
|
|
373
795
|
description: "Upload a G-code file to the Bambu Lab printer",
|
|
@@ -375,12 +797,17 @@ class BambuPrinterMCPServer {
|
|
|
375
797
|
type: "object",
|
|
376
798
|
properties: {
|
|
377
799
|
filename: { type: "string", description: "Name for the file on the printer" },
|
|
378
|
-
gcode: { type: "string", description: "G-code content to upload" },
|
|
800
|
+
gcode: { type: "string", description: "G-code content to upload, or a readable local .gcode path. For large files, prefer gcode_path." },
|
|
801
|
+
gcode_path: { type: "string", description: "Local path to a .gcode file to upload. This avoids sending large G-code bodies through the MCP request." },
|
|
379
802
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
380
803
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
381
804
|
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
382
805
|
},
|
|
383
|
-
required: ["filename",
|
|
806
|
+
required: ["filename"],
|
|
807
|
+
anyOf: [
|
|
808
|
+
{ required: ["gcode"] },
|
|
809
|
+
{ required: ["gcode_path"] }
|
|
810
|
+
]
|
|
384
811
|
}
|
|
385
812
|
},
|
|
386
813
|
{
|
|
@@ -392,6 +819,11 @@ class BambuPrinterMCPServer {
|
|
|
392
819
|
file_path: { type: "string", description: "Local path to the file to upload" },
|
|
393
820
|
filename: { type: "string", description: "Name for the file on the printer" },
|
|
394
821
|
print: { type: "boolean", description: "Start printing after upload (default: false)" },
|
|
822
|
+
bambu_model: {
|
|
823
|
+
type: "string",
|
|
824
|
+
enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
|
|
825
|
+
description: "Required when print is true. Bambu Lab printer model used as a safety confirmation before starting the uploaded file."
|
|
826
|
+
},
|
|
395
827
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
396
828
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
397
829
|
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
@@ -399,6 +831,25 @@ class BambuPrinterMCPServer {
|
|
|
399
831
|
required: ["file_path", "filename"]
|
|
400
832
|
}
|
|
401
833
|
},
|
|
834
|
+
{
|
|
835
|
+
name: "start_print",
|
|
836
|
+
description: "Start printing a G-code file already on the Bambu Lab printer. Alias of start_print_job for upstream MCP compatibility.",
|
|
837
|
+
inputSchema: {
|
|
838
|
+
type: "object",
|
|
839
|
+
properties: {
|
|
840
|
+
filename: { type: "string", description: "Name of the file to print" },
|
|
841
|
+
bambu_model: {
|
|
842
|
+
type: "string",
|
|
843
|
+
enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
|
|
844
|
+
description: "REQUIRED: Bambu Lab printer model. Ask the user if not known. Starting G-code for the wrong model can damage the printer."
|
|
845
|
+
},
|
|
846
|
+
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
847
|
+
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
848
|
+
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
849
|
+
},
|
|
850
|
+
required: ["filename", "bambu_model"]
|
|
851
|
+
}
|
|
852
|
+
},
|
|
402
853
|
{
|
|
403
854
|
name: "start_print_job",
|
|
404
855
|
description: "Start printing a G-code file already on the Bambu Lab printer",
|
|
@@ -406,11 +857,16 @@ class BambuPrinterMCPServer {
|
|
|
406
857
|
type: "object",
|
|
407
858
|
properties: {
|
|
408
859
|
filename: { type: "string", description: "Name of the file to print" },
|
|
860
|
+
bambu_model: {
|
|
861
|
+
type: "string",
|
|
862
|
+
enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
|
|
863
|
+
description: "REQUIRED: Bambu Lab printer model. Ask the user if not known. Starting G-code for the wrong model can damage the printer."
|
|
864
|
+
},
|
|
409
865
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
410
866
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
411
867
|
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
412
868
|
},
|
|
413
|
-
required: ["filename"]
|
|
869
|
+
required: ["filename", "bambu_model"]
|
|
414
870
|
}
|
|
415
871
|
},
|
|
416
872
|
{
|
|
@@ -452,14 +908,35 @@ class BambuPrinterMCPServer {
|
|
|
452
908
|
enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
|
|
453
909
|
description: "REQUIRED: Bambu Lab printer model. Ask the user if not known. Using the wrong model can damage the printer."
|
|
454
910
|
},
|
|
911
|
+
connection_mode: {
|
|
912
|
+
type: "string",
|
|
913
|
+
enum: ["lan_mqtt_ftps", "bambu_network"],
|
|
914
|
+
description: "Print path to use: lan_mqtt_ftps uses this MCP's direct local MQTT/FTPS path; bambu_network uses FULU OrcaSlicer-bambulab's restored BambuNetwork bridge."
|
|
915
|
+
},
|
|
916
|
+
connection_type: { type: "string", enum: ["cloud", "lan"], description: "BambuNetwork connection type when connection_mode is bambu_network; cloud uses restored internet printing, lan uses local bridge printing." },
|
|
917
|
+
bambu_network_method: { type: "string", enum: BAMBU_NETWORK_PRINT_METHODS, description: "FULU print method when connection_mode is bambu_network; defaults to start_print for cloud and start_local_print for lan." },
|
|
918
|
+
dev_id: { type: "string", description: "Bambu device id for FULU BambuNetwork printing; defaults to BAMBU_DEV_ID or BAMBU_SERIAL." },
|
|
919
|
+
dev_ip: { type: "string", description: "Printer IP address for FULU BambuNetwork LAN/local print methods; defaults to host when provided." },
|
|
920
|
+
bridge_command: { type: "string", description: "Override command for the FULU bridge host or macOS/WSL wrapper; defaults to BAMBU_NETWORK_BRIDGE_COMMAND." },
|
|
921
|
+
bambu_network_config_dir: { type: "string", description: "Config/log directory used by the FULU BambuNetwork agent." },
|
|
922
|
+
country_code: { type: "string", description: "BambuNetwork country code, such as US, used by the FULU bridge agent." },
|
|
923
|
+
user_info: { type: "string", description: "Optional BambuNetwork user_info JSON string passed to net.change_user for the FULU bridge." },
|
|
455
924
|
bed_type: {
|
|
456
925
|
type: "string",
|
|
457
926
|
enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate"],
|
|
458
927
|
description: "Bed plate type currently installed (default: textured_plate)"
|
|
459
928
|
},
|
|
929
|
+
plate_index: { type: "number", description: "Zero-based plate index to print from the sliced 3MF (default: 0)" },
|
|
460
930
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
461
931
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
462
932
|
bambu_token: { type: "string", description: "Access token (default: value from env)" },
|
|
933
|
+
slicer_type: {
|
|
934
|
+
type: "string",
|
|
935
|
+
enum: SLICER_SCHEMA_VALUES,
|
|
936
|
+
description: "Slicer to use only if auto-slicing an unsliced 3MF. Use orcaslicer-bambulab for FULU's OrcaSlicer-bambulab fork; aliases such as fulu-orca and orca-studio are accepted."
|
|
937
|
+
},
|
|
938
|
+
slicer_path: { type: "string", description: "Path to the slicer executable for auto-slicing (default: value from env or a platform default)" },
|
|
939
|
+
slicer_profile: { type: "string", description: "Path to an optional slicer profile/config file for auto-slicing" },
|
|
463
940
|
use_ams: { type: "boolean", description: "Whether to use the AMS (default: auto-detect from 3MF)" },
|
|
464
941
|
ams_mapping: {
|
|
465
942
|
type: "array",
|
|
@@ -469,6 +946,7 @@ class BambuPrinterMCPServer {
|
|
|
469
946
|
bed_leveling: { type: "boolean", description: "Enable auto bed leveling (default: true)" },
|
|
470
947
|
flow_calibration: { type: "boolean", description: "Enable flow calibration (default: true)" },
|
|
471
948
|
vibration_calibration: { type: "boolean", description: "Enable vibration calibration (default: true)" },
|
|
949
|
+
layer_inspect: { type: "boolean", description: "Enable first-layer inspection where supported (default: printer/profile behavior)" },
|
|
472
950
|
timelapse: { type: "boolean", description: "Enable timelapse recording (default: false)" },
|
|
473
951
|
nozzle_diameter: { type: "string", description: "Nozzle diameter in mm for auto-slicing (default: 0.4)" }
|
|
474
952
|
},
|
|
@@ -535,9 +1013,6 @@ class BambuPrinterMCPServer {
|
|
|
535
1013
|
const host = String(args?.host || DEFAULT_HOST);
|
|
536
1014
|
const bambuSerial = String(args?.bambu_serial || DEFAULT_BAMBU_SERIAL);
|
|
537
1015
|
const bambuToken = String(args?.bambu_token || DEFAULT_BAMBU_TOKEN);
|
|
538
|
-
const slicerType = String(args?.slicer_type || DEFAULT_SLICER_TYPE);
|
|
539
|
-
const slicerPath = String(args?.slicer_path || DEFAULT_SLICER_PATH);
|
|
540
|
-
const slicerProfile = String(args?.slicer_profile || DEFAULT_SLICER_PROFILE);
|
|
541
1016
|
try {
|
|
542
1017
|
let result;
|
|
543
1018
|
switch (name) {
|
|
@@ -547,25 +1022,69 @@ class BambuPrinterMCPServer {
|
|
|
547
1022
|
case "list_printer_files":
|
|
548
1023
|
result = await this.bambu.getFiles(host, bambuSerial, bambuToken);
|
|
549
1024
|
break;
|
|
1025
|
+
case "bambu_network_bridge_status": {
|
|
1026
|
+
const bridgeArgs = args;
|
|
1027
|
+
const options = this.bridgeOptionsFromArgs(bridgeArgs);
|
|
1028
|
+
result = this.bambuNetwork.getStatus(options);
|
|
1029
|
+
if (Boolean(bridgeArgs?.connect)) {
|
|
1030
|
+
const probe = await this.bambuNetwork.ensureAgent(options);
|
|
1031
|
+
result = {
|
|
1032
|
+
...this.bambuNetwork.getStatus(options),
|
|
1033
|
+
connected: true,
|
|
1034
|
+
agent: probe.agent,
|
|
1035
|
+
handshake: probe.handshake,
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
break;
|
|
1039
|
+
}
|
|
1040
|
+
case "bambu_network_call": {
|
|
1041
|
+
if (!args?.method) {
|
|
1042
|
+
throw new Error("Missing required parameter: method");
|
|
1043
|
+
}
|
|
1044
|
+
const bridgeArgs = args;
|
|
1045
|
+
const payload = bridgeArgs.payload && typeof bridgeArgs.payload === "object"
|
|
1046
|
+
? bridgeArgs.payload
|
|
1047
|
+
: {};
|
|
1048
|
+
const options = this.bridgeOptionsFromArgs(bridgeArgs);
|
|
1049
|
+
result = bridgeArgs.with_agent === false
|
|
1050
|
+
? await this.bambuNetwork.request(String(bridgeArgs.method), payload, options)
|
|
1051
|
+
: await this.bambuNetwork.callWithAgent(String(bridgeArgs.method), payload, options);
|
|
1052
|
+
break;
|
|
1053
|
+
}
|
|
1054
|
+
case "print_3mf_bambu_network": {
|
|
1055
|
+
result = await this.print3mfViaBambuNetwork(args, host, bambuSerial, bambuToken);
|
|
1056
|
+
break;
|
|
1057
|
+
}
|
|
550
1058
|
case "upload_gcode": {
|
|
551
|
-
if (!args?.filename
|
|
552
|
-
throw new Error("Missing required
|
|
1059
|
+
if (!args?.filename) {
|
|
1060
|
+
throw new Error("Missing required parameter: filename");
|
|
1061
|
+
}
|
|
1062
|
+
const uploadSource = resolveUploadGcodeSource(args);
|
|
1063
|
+
try {
|
|
1064
|
+
result = await this.bambu.uploadFile(host, bambuSerial, bambuToken, uploadSource.filePath, String(args.filename), false);
|
|
1065
|
+
}
|
|
1066
|
+
finally {
|
|
1067
|
+
if (uploadSource.cleanupDir) {
|
|
1068
|
+
fs.rmSync(uploadSource.cleanupDir, { recursive: true, force: true });
|
|
1069
|
+
}
|
|
553
1070
|
}
|
|
554
|
-
const tmpPath = path.join(TEMP_DIR, String(args.filename));
|
|
555
|
-
fs.writeFileSync(tmpPath, String(args.gcode));
|
|
556
|
-
result = await this.bambu.uploadFile(host, bambuSerial, bambuToken, tmpPath, String(args.filename), false);
|
|
557
1071
|
break;
|
|
558
1072
|
}
|
|
559
1073
|
case "upload_file":
|
|
560
1074
|
if (!args?.file_path || !args?.filename) {
|
|
561
1075
|
throw new Error("Missing required parameters: file_path and filename");
|
|
562
1076
|
}
|
|
1077
|
+
if (Boolean(args.print ?? false)) {
|
|
1078
|
+
await this.resolveBambuModel(args?.bambu_model);
|
|
1079
|
+
}
|
|
563
1080
|
result = await this.bambu.uploadFile(host, bambuSerial, bambuToken, String(args.file_path), String(args.filename), Boolean(args.print ?? false));
|
|
564
1081
|
break;
|
|
1082
|
+
case "start_print":
|
|
565
1083
|
case "start_print_job":
|
|
566
1084
|
if (!args?.filename) {
|
|
567
1085
|
throw new Error("Missing required parameter: filename");
|
|
568
1086
|
}
|
|
1087
|
+
await this.resolveBambuModel(args?.bambu_model);
|
|
569
1088
|
result = await this.bambu.startJob(host, bambuSerial, bambuToken, String(args.filename));
|
|
570
1089
|
break;
|
|
571
1090
|
case "cancel_print":
|
|
@@ -613,6 +1132,7 @@ class BambuPrinterMCPServer {
|
|
|
613
1132
|
if (!args?.stl_path) {
|
|
614
1133
|
throw new Error("Missing required parameter: stl_path");
|
|
615
1134
|
}
|
|
1135
|
+
const { slicerType, slicerPath, slicerProfile } = resolveSlicerConfig(args);
|
|
616
1136
|
const sliceModel = await this.resolveBambuModel(args?.bambu_model);
|
|
617
1137
|
const nozzleDiam = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
618
1138
|
// Resolve printer preset for BambuStudio slicer
|
|
@@ -632,8 +1152,17 @@ class BambuPrinterMCPServer {
|
|
|
632
1152
|
sliceBambuOptions.cloneObjects = String(args.clone_objects);
|
|
633
1153
|
if (args?.skip_objects !== undefined)
|
|
634
1154
|
sliceBambuOptions.skipObjects = String(args.skip_objects);
|
|
635
|
-
if (args?.load_filaments !== undefined
|
|
1155
|
+
if (args?.load_filaments !== undefined &&
|
|
1156
|
+
args?.filament_profile !== undefined &&
|
|
1157
|
+
String(args.load_filaments) !== String(args.filament_profile)) {
|
|
1158
|
+
throw new Error("Provide either load_filaments or filament_profile, not conflicting values.");
|
|
1159
|
+
}
|
|
1160
|
+
if (args?.load_filaments !== undefined) {
|
|
636
1161
|
sliceBambuOptions.loadFilaments = String(args.load_filaments);
|
|
1162
|
+
}
|
|
1163
|
+
else if (args?.filament_profile !== undefined) {
|
|
1164
|
+
sliceBambuOptions.loadFilaments = String(args.filament_profile);
|
|
1165
|
+
}
|
|
637
1166
|
if (args?.load_filament_ids !== undefined)
|
|
638
1167
|
sliceBambuOptions.loadFilamentIds = String(args.load_filament_ids);
|
|
639
1168
|
if (args?.enable_timelapse !== undefined)
|
|
@@ -662,13 +1191,22 @@ class BambuPrinterMCPServer {
|
|
|
662
1191
|
if (!args?.three_mf_path) {
|
|
663
1192
|
throw new Error("Missing required parameter: three_mf_path");
|
|
664
1193
|
}
|
|
1194
|
+
if (String(args?.connection_mode || "lan_mqtt_ftps") === "bambu_network") {
|
|
1195
|
+
result = await this.print3mfViaBambuNetwork(args, host, bambuSerial, bambuToken);
|
|
1196
|
+
break;
|
|
1197
|
+
}
|
|
665
1198
|
if (!bambuSerial || !bambuToken) {
|
|
666
1199
|
throw new Error("Bambu serial number and access token are required for print_3mf.");
|
|
667
1200
|
}
|
|
1201
|
+
const { slicerType, slicerPath, slicerProfile } = resolveSlicerConfig(args);
|
|
668
1202
|
const printModel = await this.resolveBambuModel(args?.bambu_model);
|
|
669
1203
|
const printBedType = resolveBedType(args?.bed_type);
|
|
670
1204
|
const printNozzle = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
671
1205
|
const printPreset = BAMBU_MODEL_PRESETS[printModel]?.(printNozzle);
|
|
1206
|
+
const plateIndex = args?.plate_index !== undefined ? Number(args.plate_index) : 0;
|
|
1207
|
+
if (!Number.isInteger(plateIndex) || plateIndex < 0) {
|
|
1208
|
+
throw new Error("plate_index must be a non-negative integer.");
|
|
1209
|
+
}
|
|
672
1210
|
let threeMFPath = String(args.three_mf_path);
|
|
673
1211
|
// Auto-slice if 3MF has no gcode
|
|
674
1212
|
try {
|
|
@@ -677,7 +1215,7 @@ class BambuPrinterMCPServer {
|
|
|
677
1215
|
const zip = await JSZip.loadAsync(zipData);
|
|
678
1216
|
const hasGcode = Object.keys(zip.files).some(f => f.match(/Metadata\/plate_\d+\.gcode/i) || f.endsWith('.gcode'));
|
|
679
1217
|
if (!hasGcode) {
|
|
680
|
-
console.log(`3MF has no gcode
|
|
1218
|
+
console.log(`3MF has no gcode - auto-slicing with ${slicerType} for ${printModel}`);
|
|
681
1219
|
const autoSliceOptions = {
|
|
682
1220
|
uptodate: true,
|
|
683
1221
|
ensureOnBed: true,
|
|
@@ -702,7 +1240,8 @@ class BambuPrinterMCPServer {
|
|
|
702
1240
|
}
|
|
703
1241
|
}
|
|
704
1242
|
let finalAmsMapping = parsedAmsMapping;
|
|
705
|
-
|
|
1243
|
+
const explicitUseAMS = args?.use_ams !== undefined;
|
|
1244
|
+
let useAMS = explicitUseAMS ? Boolean(args.use_ams) : (!!finalAmsMapping && finalAmsMapping.length > 0);
|
|
706
1245
|
if (args?.ams_mapping) {
|
|
707
1246
|
let userMappingOverride;
|
|
708
1247
|
if (Array.isArray(args.ams_mapping)) {
|
|
@@ -722,7 +1261,7 @@ class BambuPrinterMCPServer {
|
|
|
722
1261
|
finalAmsMapping = undefined;
|
|
723
1262
|
useAMS = false;
|
|
724
1263
|
}
|
|
725
|
-
if (!finalAmsMapping || finalAmsMapping.length === 0) {
|
|
1264
|
+
if ((!finalAmsMapping || finalAmsMapping.length === 0) && !explicitUseAMS) {
|
|
726
1265
|
useAMS = false;
|
|
727
1266
|
}
|
|
728
1267
|
const threeMfFilename = path.basename(threeMFPath);
|
|
@@ -730,7 +1269,7 @@ class BambuPrinterMCPServer {
|
|
|
730
1269
|
result = await this.bambu.print3mf(host, bambuSerial, bambuToken, {
|
|
731
1270
|
projectName,
|
|
732
1271
|
filePath: threeMFPath,
|
|
733
|
-
plateIndex
|
|
1272
|
+
plateIndex,
|
|
734
1273
|
useAMS: useAMS,
|
|
735
1274
|
amsMapping: finalAmsMapping,
|
|
736
1275
|
bedType: printBedType,
|