bambu-printer-mcp 1.0.4 → 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 +6 -3
- 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/src/index.ts
CHANGED
|
@@ -16,7 +16,14 @@ import fs from "fs";
|
|
|
16
16
|
import path from "path";
|
|
17
17
|
import { createServer as createHttpServer, type IncomingMessage, type Server as HttpServer, type ServerResponse } from "node:http";
|
|
18
18
|
import { randomUUID } from "node:crypto";
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
STLManipulator,
|
|
21
|
+
SLICER_TYPES,
|
|
22
|
+
normalizeSlicerType,
|
|
23
|
+
type BambuSliceOptions,
|
|
24
|
+
type SlicerType,
|
|
25
|
+
} from "./stl/stl-manipulator.js";
|
|
26
|
+
import { BambuNetworkBridge, type BambuNetworkBridgeOptions } from "./bambu-network-bridge.js";
|
|
20
27
|
import { parse3MF } from './3mf_parser.js';
|
|
21
28
|
import { BambuImplementation } from "./printers/bambu.js";
|
|
22
29
|
|
|
@@ -25,6 +32,7 @@ dotenv.config();
|
|
|
25
32
|
const DEFAULT_HOST = process.env.PRINTER_HOST || "localhost";
|
|
26
33
|
const DEFAULT_BAMBU_SERIAL = process.env.BAMBU_SERIAL || "";
|
|
27
34
|
const DEFAULT_BAMBU_TOKEN = process.env.BAMBU_TOKEN || "";
|
|
35
|
+
const DEFAULT_BAMBU_DEV_ID = process.env.BAMBU_DEV_ID || DEFAULT_BAMBU_SERIAL;
|
|
28
36
|
const TEMP_DIR = process.env.TEMP_DIR || path.join(process.cwd(), "temp");
|
|
29
37
|
|
|
30
38
|
// Printer model and bed type
|
|
@@ -70,8 +78,60 @@ function resolveBedType(argsBedType: string | undefined): string {
|
|
|
70
78
|
|
|
71
79
|
// Slicer configuration (defaults to bambustudio)
|
|
72
80
|
const DEFAULT_SLICER_TYPE = process.env.SLICER_TYPE || "bambustudio";
|
|
73
|
-
const DEFAULT_SLICER_PATH = process.env.SLICER_PATH || "/Applications/BambuStudio.app/Contents/MacOS/BambuStudio";
|
|
74
81
|
const DEFAULT_SLICER_PROFILE = process.env.SLICER_PROFILE || "";
|
|
82
|
+
const SLICER_SCHEMA_VALUES = [
|
|
83
|
+
...SLICER_TYPES,
|
|
84
|
+
"fulu-orca",
|
|
85
|
+
"fulu-orcaslicer",
|
|
86
|
+
"orca-studio",
|
|
87
|
+
"orca-bambulab",
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
function firstExistingPath(paths: string[], fallback: string): string {
|
|
91
|
+
return paths.find((candidate) => fs.existsSync(candidate)) || fallback;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function defaultSlicerPathFor(slicerType: SlicerType): string {
|
|
95
|
+
if (slicerType === "orcaslicer" || slicerType === "orcaslicer-bambulab") {
|
|
96
|
+
if (process.platform === "darwin") {
|
|
97
|
+
return firstExistingPath(
|
|
98
|
+
[
|
|
99
|
+
"/Applications/OrcaSlicer.app/Contents/MacOS/OrcaSlicer",
|
|
100
|
+
"/Applications/Orca Studio.app/Contents/MacOS/Orca Studio",
|
|
101
|
+
"/Applications/OrcaSlicer-BMCU.app/Contents/MacOS/OrcaSlicer",
|
|
102
|
+
],
|
|
103
|
+
"/Applications/OrcaSlicer.app/Contents/MacOS/OrcaSlicer"
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
if (process.platform === "win32") {
|
|
107
|
+
return "C:\\Program Files\\OrcaSlicer\\OrcaSlicer.exe";
|
|
108
|
+
}
|
|
109
|
+
return "OrcaSlicer";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (slicerType === "bambustudio") {
|
|
113
|
+
if (process.platform === "darwin") {
|
|
114
|
+
return "/Applications/BambuStudio.app/Contents/MacOS/BambuStudio";
|
|
115
|
+
}
|
|
116
|
+
if (process.platform === "win32") {
|
|
117
|
+
return "C:\\Program Files\\Bambu Studio\\bambu-studio.exe";
|
|
118
|
+
}
|
|
119
|
+
return "BambuStudio";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return slicerType;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function resolveSlicerConfig(args: Record<string, unknown> | undefined): {
|
|
126
|
+
slicerType: SlicerType;
|
|
127
|
+
slicerPath: string;
|
|
128
|
+
slicerProfile: string;
|
|
129
|
+
} {
|
|
130
|
+
const slicerType = normalizeSlicerType(String(args?.slicer_type || DEFAULT_SLICER_TYPE));
|
|
131
|
+
const slicerPath = String(args?.slicer_path || process.env.SLICER_PATH || defaultSlicerPathFor(slicerType));
|
|
132
|
+
const slicerProfile = String(args?.slicer_profile || DEFAULT_SLICER_PROFILE);
|
|
133
|
+
return { slicerType, slicerPath, slicerProfile };
|
|
134
|
+
}
|
|
75
135
|
|
|
76
136
|
type RuntimeConfig = {
|
|
77
137
|
transport: "stdio" | "streamable-http";
|
|
@@ -139,6 +199,147 @@ type StructuredToolError = {
|
|
|
139
199
|
tool: string;
|
|
140
200
|
};
|
|
141
201
|
|
|
202
|
+
type UploadGcodeSource = {
|
|
203
|
+
filePath: string;
|
|
204
|
+
cleanupDir?: string;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
function expandUserPath(rawPath: string): string {
|
|
208
|
+
const trimmed = rawPath.trim();
|
|
209
|
+
if (trimmed === "~") {
|
|
210
|
+
return process.env.HOME || trimmed;
|
|
211
|
+
}
|
|
212
|
+
if (trimmed.startsWith("~/")) {
|
|
213
|
+
return path.join(process.env.HOME || "", trimmed.slice(2));
|
|
214
|
+
}
|
|
215
|
+
return path.resolve(trimmed);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function readableFilePathFromString(value: string): string | undefined {
|
|
219
|
+
if (!value.trim() || value.includes("\n") || value.includes("\r")) {
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const candidate = expandUserPath(value);
|
|
224
|
+
try {
|
|
225
|
+
return fs.statSync(candidate).isFile() ? candidate : undefined;
|
|
226
|
+
} catch {
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function looksLikeGcodeFilePath(value: string): boolean {
|
|
232
|
+
const trimmed = value.trim();
|
|
233
|
+
if (!trimmed || trimmed.includes("\n") || trimmed.includes("\r")) {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
return (
|
|
237
|
+
trimmed.startsWith("/") ||
|
|
238
|
+
trimmed.startsWith("./") ||
|
|
239
|
+
trimmed.startsWith("../") ||
|
|
240
|
+
trimmed.startsWith("~/") ||
|
|
241
|
+
trimmed.includes("\\") ||
|
|
242
|
+
/\.(gcode|gco|gc)$/i.test(trimmed)
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function requireReadableFilePath(rawPath: string, label: string): string {
|
|
247
|
+
const candidate = expandUserPath(rawPath);
|
|
248
|
+
try {
|
|
249
|
+
if (!fs.statSync(candidate).isFile()) {
|
|
250
|
+
throw new Error(`${label} is not a file: ${candidate}`);
|
|
251
|
+
}
|
|
252
|
+
} catch (error) {
|
|
253
|
+
if ((error as Error).message.startsWith(`${label} is not a file:`)) {
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
throw new Error(`${label} does not exist or is not readable: ${candidate}`);
|
|
257
|
+
}
|
|
258
|
+
return candidate;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function writeGcodeContentToTempFile(filename: string, gcode: string): UploadGcodeSource {
|
|
262
|
+
fs.mkdirSync(TEMP_DIR, { recursive: true });
|
|
263
|
+
const cleanupDir = fs.mkdtempSync(path.join(TEMP_DIR, "upload-gcode-"));
|
|
264
|
+
const safeName = path.basename(filename.replace(/^\/+/, "")) || "upload.gcode";
|
|
265
|
+
const filePath = path.join(cleanupDir, safeName);
|
|
266
|
+
fs.writeFileSync(filePath, gcode);
|
|
267
|
+
return { filePath, cleanupDir };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function resolveUploadGcodeSource(args: Record<string, unknown>): UploadGcodeSource {
|
|
271
|
+
const gcodePath = args.gcode_path !== undefined ? String(args.gcode_path) : "";
|
|
272
|
+
const gcode = args.gcode !== undefined ? String(args.gcode) : "";
|
|
273
|
+
|
|
274
|
+
if (gcodePath && gcode) {
|
|
275
|
+
throw new Error("Provide either gcode_path or gcode, not both.");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (gcodePath) {
|
|
279
|
+
return { filePath: requireReadableFilePath(gcodePath, "gcode_path") };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (!gcode) {
|
|
283
|
+
throw new Error("Missing required parameter: gcode or gcode_path");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const detectedPath = readableFilePathFromString(gcode);
|
|
287
|
+
if (detectedPath) {
|
|
288
|
+
return { filePath: detectedPath };
|
|
289
|
+
}
|
|
290
|
+
if (looksLikeGcodeFilePath(gcode)) {
|
|
291
|
+
throw new Error(
|
|
292
|
+
"gcode looks like a local G-code path, but the file is not readable. " +
|
|
293
|
+
"Pass readable gcode_path or literal G-code content."
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return writeGcodeContentToTempFile(String(args.filename), gcode);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const BAMBU_NETWORK_PRINT_METHODS = [
|
|
301
|
+
"start_print",
|
|
302
|
+
"start_local_print",
|
|
303
|
+
"start_local_print_with_record",
|
|
304
|
+
"start_send_gcode_to_sdcard",
|
|
305
|
+
"start_sdcard_print",
|
|
306
|
+
] as const;
|
|
307
|
+
|
|
308
|
+
type BambuNetworkPrintMethod = typeof BAMBU_NETWORK_PRINT_METHODS[number];
|
|
309
|
+
|
|
310
|
+
function resolveBambuNetworkPrintMethod(
|
|
311
|
+
rawMethod: string | undefined,
|
|
312
|
+
connectionType: string
|
|
313
|
+
): BambuNetworkPrintMethod {
|
|
314
|
+
const defaultMethod = connectionType === "lan" ? "start_local_print" : "start_print";
|
|
315
|
+
const method = (rawMethod || defaultMethod).trim();
|
|
316
|
+
|
|
317
|
+
if (!(BAMBU_NETWORK_PRINT_METHODS as readonly string[]).includes(method)) {
|
|
318
|
+
throw new Error(
|
|
319
|
+
`Invalid bambu_network_method: "${method}". Valid methods: ${BAMBU_NETWORK_PRINT_METHODS.join(", ")}`
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return method as BambuNetworkPrintMethod;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function toBridgeMethod(method: BambuNetworkPrintMethod): string {
|
|
327
|
+
return `net.${method}`;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function stringifyBridgeJson(value: unknown): string | undefined {
|
|
331
|
+
if (value === undefined || value === null) return undefined;
|
|
332
|
+
if (typeof value === "string") return value;
|
|
333
|
+
return JSON.stringify(value);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function redactPrintParams(params: Record<string, unknown>): Record<string, unknown> {
|
|
337
|
+
return {
|
|
338
|
+
...params,
|
|
339
|
+
password: params.password ? "[redacted]" : "",
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
142
343
|
if (!fs.existsSync(TEMP_DIR)) {
|
|
143
344
|
fs.mkdirSync(TEMP_DIR, { recursive: true });
|
|
144
345
|
}
|
|
@@ -146,6 +347,7 @@ if (!fs.existsSync(TEMP_DIR)) {
|
|
|
146
347
|
class BambuPrinterMCPServer {
|
|
147
348
|
private server: Server;
|
|
148
349
|
private bambu: BambuImplementation;
|
|
350
|
+
private bambuNetwork: BambuNetworkBridge;
|
|
149
351
|
private stlManipulator: STLManipulator;
|
|
150
352
|
private readonly runtimeConfig: RuntimeConfig;
|
|
151
353
|
private httpRuntime?: { transport: StreamableHTTPServerTransport; httpServer: HttpServer };
|
|
@@ -166,6 +368,7 @@ class BambuPrinterMCPServer {
|
|
|
166
368
|
);
|
|
167
369
|
|
|
168
370
|
this.bambu = new BambuImplementation();
|
|
371
|
+
this.bambuNetwork = new BambuNetworkBridge();
|
|
169
372
|
this.stlManipulator = new STLManipulator(TEMP_DIR);
|
|
170
373
|
|
|
171
374
|
this.setupHandlers();
|
|
@@ -193,7 +396,7 @@ class BambuPrinterMCPServer {
|
|
|
193
396
|
return validateBambuModel(fromArgs);
|
|
194
397
|
}
|
|
195
398
|
|
|
196
|
-
// No model from args or env
|
|
399
|
+
// No model from args or env - ask the user via elicitation
|
|
197
400
|
try {
|
|
198
401
|
const result = await this.server.elicitInput({
|
|
199
402
|
mode: "form" as const,
|
|
@@ -230,7 +433,7 @@ class BambuPrinterMCPServer {
|
|
|
230
433
|
"Printer model selection was cancelled. Cannot proceed without knowing the printer model."
|
|
231
434
|
);
|
|
232
435
|
} catch (elicitError: any) {
|
|
233
|
-
// Elicitation not supported by this client
|
|
436
|
+
// Elicitation not supported by this client - fall back to a clear error
|
|
234
437
|
const msg = elicitError?.message || String(elicitError);
|
|
235
438
|
if (
|
|
236
439
|
elicitError?.code === -32601 || elicitError?.code === -32600 ||
|
|
@@ -246,6 +449,232 @@ class BambuPrinterMCPServer {
|
|
|
246
449
|
}
|
|
247
450
|
}
|
|
248
451
|
|
|
452
|
+
private bridgeOptionsFromArgs(args: Record<string, any> | undefined): BambuNetworkBridgeOptions {
|
|
453
|
+
return {
|
|
454
|
+
bridgeCommand: args?.bridge_command !== undefined ? String(args.bridge_command) : undefined,
|
|
455
|
+
configDir: args?.bambu_network_config_dir !== undefined ? String(args.bambu_network_config_dir) : undefined,
|
|
456
|
+
countryCode: args?.country_code !== undefined ? String(args.country_code) : undefined,
|
|
457
|
+
userInfo: args?.user_info !== undefined ? String(args.user_info) : undefined,
|
|
458
|
+
timeoutMs: args?.timeout_ms !== undefined ? Number(args.timeout_ms) : undefined,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
private async ensurePrintableThreeMFPath(
|
|
463
|
+
args: Record<string, any>,
|
|
464
|
+
printModel: string,
|
|
465
|
+
printPreset: string | undefined
|
|
466
|
+
): Promise<{ threeMFPath: string; autoSliced: boolean }> {
|
|
467
|
+
const { slicerType, slicerPath, slicerProfile } = resolveSlicerConfig(args);
|
|
468
|
+
let threeMFPath = String(args.three_mf_path);
|
|
469
|
+
|
|
470
|
+
const JSZip = (await import('jszip')).default;
|
|
471
|
+
const zipData = fs.readFileSync(threeMFPath);
|
|
472
|
+
const zip = await JSZip.loadAsync(zipData);
|
|
473
|
+
const hasGcode = Object.keys(zip.files).some(
|
|
474
|
+
f => f.match(/Metadata\/plate_\d+\.gcode/i) || f.endsWith('.gcode')
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
if (hasGcode) {
|
|
478
|
+
return { threeMFPath, autoSliced: false };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
console.log(`3MF has no gcode - auto-slicing with ${slicerType} for ${printModel}`);
|
|
482
|
+
const autoSliceOptions: BambuSliceOptions = {
|
|
483
|
+
uptodate: true,
|
|
484
|
+
ensureOnBed: true,
|
|
485
|
+
minSave: true,
|
|
486
|
+
skipModifiedGcodes: true,
|
|
487
|
+
};
|
|
488
|
+
threeMFPath = await this.stlManipulator.sliceSTL(
|
|
489
|
+
threeMFPath, slicerType, slicerPath, slicerProfile || undefined,
|
|
490
|
+
undefined,
|
|
491
|
+
printPreset,
|
|
492
|
+
autoSliceOptions
|
|
493
|
+
);
|
|
494
|
+
console.log("Auto-sliced to: " + threeMFPath);
|
|
495
|
+
return { threeMFPath, autoSliced: true };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
private async resolveAmsPrintSettings(
|
|
499
|
+
threeMFPath: string,
|
|
500
|
+
args: Record<string, any>
|
|
501
|
+
): Promise<{ useAMS: boolean; finalAmsMapping?: number[] }> {
|
|
502
|
+
const parsed3MFData = await parse3MF(threeMFPath);
|
|
503
|
+
let parsedAmsMapping: number[] | undefined;
|
|
504
|
+
|
|
505
|
+
if (parsed3MFData.slicerConfig?.ams_mapping) {
|
|
506
|
+
const slots = Object.values(parsed3MFData.slicerConfig.ams_mapping)
|
|
507
|
+
.filter(v => typeof v === 'number') as number[];
|
|
508
|
+
if (slots.length > 0) {
|
|
509
|
+
parsedAmsMapping = slots.sort((a, b) => a - b);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
let finalAmsMapping = parsedAmsMapping;
|
|
514
|
+
const explicitUseAMS = args?.use_ams !== undefined;
|
|
515
|
+
let useAMS = explicitUseAMS ? Boolean(args.use_ams) : (!!finalAmsMapping && finalAmsMapping.length > 0);
|
|
516
|
+
|
|
517
|
+
if (args?.ams_mapping) {
|
|
518
|
+
let userMappingOverride: number[] | undefined;
|
|
519
|
+
if (Array.isArray(args.ams_mapping)) {
|
|
520
|
+
userMappingOverride = args.ams_mapping.filter((v: any) => typeof v === 'number');
|
|
521
|
+
} else if (typeof args.ams_mapping === 'object') {
|
|
522
|
+
userMappingOverride = Object.values(args.ams_mapping)
|
|
523
|
+
.filter((v: any) => typeof v === 'number')
|
|
524
|
+
.sort((a: any, b: any) => a - b) as number[];
|
|
525
|
+
}
|
|
526
|
+
if (userMappingOverride && userMappingOverride.length > 0) {
|
|
527
|
+
finalAmsMapping = userMappingOverride;
|
|
528
|
+
useAMS = true;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (args?.use_ams === false) {
|
|
533
|
+
finalAmsMapping = undefined;
|
|
534
|
+
useAMS = false;
|
|
535
|
+
}
|
|
536
|
+
if ((!finalAmsMapping || finalAmsMapping.length === 0) && !explicitUseAMS) {
|
|
537
|
+
useAMS = false;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return { useAMS, finalAmsMapping };
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
private async print3mfViaBambuNetwork(
|
|
544
|
+
args: Record<string, any>,
|
|
545
|
+
host: string,
|
|
546
|
+
bambuSerial: string,
|
|
547
|
+
bambuToken: string
|
|
548
|
+
): Promise<Record<string, unknown>> {
|
|
549
|
+
if (!args?.three_mf_path) {
|
|
550
|
+
throw new Error("Missing required parameter: three_mf_path");
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const printModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
554
|
+
const printBedType = resolveBedType(args?.bed_type as string | undefined);
|
|
555
|
+
const printNozzle = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
556
|
+
const printPreset = BAMBU_MODEL_PRESETS[printModel]?.(printNozzle);
|
|
557
|
+
const plateIndex = args?.plate_index !== undefined ? Number(args.plate_index) : 0;
|
|
558
|
+
|
|
559
|
+
if (!Number.isInteger(plateIndex) || plateIndex < 0) {
|
|
560
|
+
throw new Error("plate_index must be a non-negative integer.");
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const connectionType = String(args?.connection_type || "cloud").trim().toLowerCase();
|
|
564
|
+
if (!["cloud", "lan"].includes(connectionType)) {
|
|
565
|
+
throw new Error('connection_type must be "cloud" or "lan".');
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const bridgePrintMethod = resolveBambuNetworkPrintMethod(
|
|
569
|
+
args?.bambu_network_method !== undefined ? String(args.bambu_network_method) : undefined,
|
|
570
|
+
connectionType
|
|
571
|
+
);
|
|
572
|
+
const bridgeMethod = toBridgeMethod(bridgePrintMethod);
|
|
573
|
+
const isLocalBridgePrint = bridgePrintMethod !== "start_print";
|
|
574
|
+
|
|
575
|
+
const devId = String(args?.dev_id || bambuSerial || DEFAULT_BAMBU_DEV_ID).trim();
|
|
576
|
+
if (!devId) {
|
|
577
|
+
throw new Error("dev_id is required for FULU BambuNetwork printing. Pass dev_id or set BAMBU_DEV_ID/BAMBU_SERIAL.");
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const devIp = String(args?.dev_ip || args?.host || host || "").trim();
|
|
581
|
+
const explicitPassword = String(args?.password || args?.bambu_token || "").trim();
|
|
582
|
+
const password = isLocalBridgePrint ? (explicitPassword || String(bambuToken || "").trim()) : explicitPassword;
|
|
583
|
+
if (isLocalBridgePrint && (!devIp || devIp === "localhost")) {
|
|
584
|
+
throw new Error("dev_ip or host is required for FULU BambuNetwork LAN/local print methods.");
|
|
585
|
+
}
|
|
586
|
+
if (isLocalBridgePrint && !password) {
|
|
587
|
+
throw new Error("bambu_token/access code is required for FULU BambuNetwork LAN/local print methods.");
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const { threeMFPath, autoSliced } = await this.ensurePrintableThreeMFPath(args, printModel, printPreset);
|
|
591
|
+
const { useAMS, finalAmsMapping } = await this.resolveAmsPrintSettings(threeMFPath, args);
|
|
592
|
+
const threeMfFilename = path.basename(threeMFPath);
|
|
593
|
+
const projectName = String(args?.project_name || threeMfFilename.replace(/\.3mf$/i, ''));
|
|
594
|
+
const presetName = String(args?.preset_name || `${projectName}_plate_${plateIndex + 1}`);
|
|
595
|
+
const clientJobId = args?.client_job_id !== undefined ? Number(args.client_job_id) : Date.now();
|
|
596
|
+
|
|
597
|
+
const amsMapping = stringifyBridgeJson(args?.ams_mapping_bridge ?? finalAmsMapping);
|
|
598
|
+
const params: Record<string, unknown> = {
|
|
599
|
+
dev_id: devId,
|
|
600
|
+
task_name: String(args?.task_name || projectName),
|
|
601
|
+
project_name: projectName,
|
|
602
|
+
preset_name: presetName,
|
|
603
|
+
filename: threeMFPath,
|
|
604
|
+
config_filename: String(args?.config_filename || threeMFPath),
|
|
605
|
+
plate_index: plateIndex + 1,
|
|
606
|
+
ftp_folder: String(args?.ftp_folder || ""),
|
|
607
|
+
ftp_file: String(args?.ftp_file || ""),
|
|
608
|
+
ftp_file_md5: String(args?.ftp_file_md5 || ""),
|
|
609
|
+
nozzle_mapping: stringifyBridgeJson(args?.nozzle_mapping) || "",
|
|
610
|
+
ams_mapping: amsMapping || "",
|
|
611
|
+
ams_mapping2: stringifyBridgeJson(args?.ams_mapping2) || "",
|
|
612
|
+
ams_mapping_info: stringifyBridgeJson(args?.ams_mapping_info) || "",
|
|
613
|
+
nozzles_info: stringifyBridgeJson(args?.nozzles_info) || "",
|
|
614
|
+
connection_type: connectionType,
|
|
615
|
+
comments: String(args?.comments || ""),
|
|
616
|
+
origin_profile_id: args?.origin_profile_id !== undefined ? Number(args.origin_profile_id) : 0,
|
|
617
|
+
stl_design_id: args?.stl_design_id !== undefined ? Number(args.stl_design_id) : 0,
|
|
618
|
+
origin_model_id: String(args?.origin_model_id || ""),
|
|
619
|
+
print_type: String(args?.print_type || "from_normal"),
|
|
620
|
+
dst_file: String(args?.dst_file || ""),
|
|
621
|
+
dev_name: String(args?.dev_name || ""),
|
|
622
|
+
dev_ip: devIp,
|
|
623
|
+
use_ssl_for_ftp: args?.use_ssl_for_ftp !== undefined ? Boolean(args.use_ssl_for_ftp) : true,
|
|
624
|
+
use_ssl_for_mqtt: args?.use_ssl_for_mqtt !== undefined ? Boolean(args.use_ssl_for_mqtt) : true,
|
|
625
|
+
username: String(args?.username || "bblp"),
|
|
626
|
+
password,
|
|
627
|
+
task_bed_leveling: args?.bed_leveling !== undefined ? Boolean(args.bed_leveling) : true,
|
|
628
|
+
task_flow_cali: args?.flow_calibration !== undefined ? Boolean(args.flow_calibration) : true,
|
|
629
|
+
task_vibration_cali: args?.vibration_calibration !== undefined ? Boolean(args.vibration_calibration) : true,
|
|
630
|
+
task_layer_inspect: args?.layer_inspect !== undefined ? Boolean(args.layer_inspect) : false,
|
|
631
|
+
task_record_timelapse: args?.timelapse !== undefined ? Boolean(args.timelapse) : false,
|
|
632
|
+
task_use_ams: useAMS,
|
|
633
|
+
task_bed_type: printBedType,
|
|
634
|
+
extra_options: stringifyBridgeJson(args?.extra_options) || "",
|
|
635
|
+
auto_bed_leveling: args?.auto_bed_leveling !== undefined ? Number(args.auto_bed_leveling) : 0,
|
|
636
|
+
auto_flow_cali: args?.auto_flow_cali !== undefined ? Number(args.auto_flow_cali) : 0,
|
|
637
|
+
auto_offset_cali: args?.auto_offset_cali !== undefined ? Number(args.auto_offset_cali) : 0,
|
|
638
|
+
extruder_cali_manual_mode: args?.extruder_cali_manual_mode !== undefined ? Number(args.extruder_cali_manual_mode) : -1,
|
|
639
|
+
task_ext_change_assist: args?.external_change_assist !== undefined ? Boolean(args.external_change_assist) : false,
|
|
640
|
+
try_emmc_print: args?.try_emmc_print !== undefined ? Boolean(args.try_emmc_print) : false,
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
const bridgeResult = await this.bambuNetwork.callWithAgent(
|
|
644
|
+
bridgeMethod,
|
|
645
|
+
{ client_job_id: clientJobId, params },
|
|
646
|
+
this.bridgeOptionsFromArgs(args)
|
|
647
|
+
);
|
|
648
|
+
|
|
649
|
+
if (typeof bridgeResult === "object" && bridgeResult !== null && (bridgeResult as { ok?: unknown }).ok === false) {
|
|
650
|
+
throw new Error(`FULU BambuNetwork bridge method ${bridgeMethod} failed: ${String((bridgeResult as { error?: unknown }).error || "unknown bridge error")}`);
|
|
651
|
+
}
|
|
652
|
+
if (
|
|
653
|
+
typeof bridgeResult === "object" &&
|
|
654
|
+
bridgeResult !== null &&
|
|
655
|
+
typeof (bridgeResult as { value?: unknown }).value === "number" &&
|
|
656
|
+
(bridgeResult as { value: number }).value !== 0
|
|
657
|
+
) {
|
|
658
|
+
const value = (bridgeResult as { value: number }).value;
|
|
659
|
+
throw new Error(`FULU BambuNetwork bridge method ${bridgeMethod} returned non-zero result ${value}.`);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return {
|
|
663
|
+
status: "success",
|
|
664
|
+
message: `FULU BambuNetwork ${bridgePrintMethod} command for ${threeMfFilename} sent successfully.`,
|
|
665
|
+
bridgeMethod,
|
|
666
|
+
bridgeResult,
|
|
667
|
+
clientJobId,
|
|
668
|
+
autoSliced,
|
|
669
|
+
projectName,
|
|
670
|
+
plateIndex,
|
|
671
|
+
bridgePlateIndex: plateIndex + 1,
|
|
672
|
+
useAMS,
|
|
673
|
+
amsMapping: finalAmsMapping,
|
|
674
|
+
params: redactPrintParams(params),
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
|
|
249
678
|
setupResourceHandlers() {
|
|
250
679
|
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
251
680
|
return {
|
|
@@ -401,12 +830,13 @@ class BambuPrinterMCPServer {
|
|
|
401
830
|
},
|
|
402
831
|
slicer_type: {
|
|
403
832
|
type: "string",
|
|
404
|
-
|
|
833
|
+
enum: SLICER_SCHEMA_VALUES,
|
|
834
|
+
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."
|
|
405
835
|
},
|
|
406
|
-
slicer_path: { type: "string", description: "Path to the slicer executable (default: value from env)" },
|
|
836
|
+
slicer_path: { type: "string", description: "Path to the slicer executable (default: value from env or a platform default)" },
|
|
407
837
|
slicer_profile: { type: "string", description: "Path to the slicer profile/config file (optional, overrides bambu_model preset)" },
|
|
408
838
|
nozzle_diameter: { type: "string", description: "Nozzle diameter in mm (default: 0.4)" },
|
|
409
|
-
uptodate: { type: "boolean", description: "Refresh 3MF preset configs to match the latest
|
|
839
|
+
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." },
|
|
410
840
|
repetitions: { type: "number", description: "Print N identical copies of the model. Each copy gets its own plate placement. Example: 3 prints three copies." },
|
|
411
841
|
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." },
|
|
412
842
|
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." },
|
|
@@ -414,6 +844,7 @@ class BambuPrinterMCPServer {
|
|
|
414
844
|
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." },
|
|
415
845
|
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." },
|
|
416
846
|
load_filaments: { type: "string", description: "Override filament profiles. Semicolon-separated paths to filament JSON configs, e.g. 'pla_basic.json;petg_cf.json'." },
|
|
847
|
+
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." },
|
|
417
848
|
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." },
|
|
418
849
|
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." },
|
|
419
850
|
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." },
|
|
@@ -440,6 +871,97 @@ class BambuPrinterMCPServer {
|
|
|
440
871
|
}
|
|
441
872
|
}
|
|
442
873
|
},
|
|
874
|
+
{
|
|
875
|
+
name: "bambu_network_bridge_status",
|
|
876
|
+
description: "Inspect or probe the FULU OrcaSlicer-bambulab BambuNetwork bridge runtime used for cloud and restored BambuNetwork printing.",
|
|
877
|
+
inputSchema: {
|
|
878
|
+
type: "object",
|
|
879
|
+
properties: {
|
|
880
|
+
connect: { type: "boolean", description: "When true, start the bridge command and run a handshake plus agent initialization probe." },
|
|
881
|
+
bridge_command: { type: "string", description: "Override command for the FULU bridge host or macOS/WSL wrapper; defaults to BAMBU_NETWORK_BRIDGE_COMMAND." },
|
|
882
|
+
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." },
|
|
883
|
+
country_code: { type: "string", description: "BambuNetwork country code, such as US, used by the agent during startup." },
|
|
884
|
+
user_info: { type: "string", description: "Optional BambuNetwork user_info JSON string to pass to net.change_user after the agent starts." },
|
|
885
|
+
timeout_ms: { type: "number", description: "Bridge request timeout in milliseconds for the connect probe." }
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
},
|
|
889
|
+
{
|
|
890
|
+
name: "bambu_network_call",
|
|
891
|
+
description: "Call a raw FULU OrcaSlicer-bambulab BambuNetwork bridge method, optionally with an initialized network agent injected into the payload.",
|
|
892
|
+
inputSchema: {
|
|
893
|
+
type: "object",
|
|
894
|
+
properties: {
|
|
895
|
+
method: { type: "string", description: "FULU bridge method name, for example bridge.handshake, net.is_user_login, or net.get_user_selected_machine." },
|
|
896
|
+
payload: { type: "object", description: "JSON payload passed to the bridge method." },
|
|
897
|
+
with_agent: { type: "boolean", description: "When true, initialize a BambuNetwork agent and add its agent id to the payload before calling the method." },
|
|
898
|
+
bridge_command: { type: "string", description: "Override command for the FULU bridge host or macOS/WSL wrapper; defaults to BAMBU_NETWORK_BRIDGE_COMMAND." },
|
|
899
|
+
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." },
|
|
900
|
+
country_code: { type: "string", description: "BambuNetwork country code, such as US, used by the agent during startup." },
|
|
901
|
+
user_info: { type: "string", description: "Optional BambuNetwork user_info JSON string to pass to net.change_user after the agent starts." },
|
|
902
|
+
timeout_ms: { type: "number", description: "Bridge request timeout in milliseconds." }
|
|
903
|
+
},
|
|
904
|
+
required: ["method"]
|
|
905
|
+
}
|
|
906
|
+
},
|
|
907
|
+
{
|
|
908
|
+
name: "print_3mf_bambu_network",
|
|
909
|
+
description: "Print a 3MF through FULU OrcaSlicer-bambulab's restored BambuNetwork path instead of the MCP LAN MQTT/FTPS path.",
|
|
910
|
+
inputSchema: {
|
|
911
|
+
type: "object",
|
|
912
|
+
properties: {
|
|
913
|
+
three_mf_path: { type: "string", description: "Path to the 3MF file to print; unsliced 3MFs are auto-sliced before sending." },
|
|
914
|
+
bambu_model: {
|
|
915
|
+
type: "string",
|
|
916
|
+
enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
|
|
917
|
+
description: "REQUIRED: Bambu Lab printer model. Ask the user if not known. Using the wrong model can damage the printer."
|
|
918
|
+
},
|
|
919
|
+
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." },
|
|
920
|
+
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." },
|
|
921
|
+
dev_id: { type: "string", description: "Bambu device id used by BambuNetwork; defaults to BAMBU_DEV_ID or BAMBU_SERIAL." },
|
|
922
|
+
dev_ip: { type: "string", description: "Printer IP address for LAN/local bridge methods; defaults to host when provided." },
|
|
923
|
+
host: { type: "string", description: "Printer host or IP address, used as dev_ip for LAN/local bridge methods." },
|
|
924
|
+
bambu_serial: { type: "string", description: "Fallback Bambu device id when dev_id is not supplied." },
|
|
925
|
+
bambu_token: { type: "string", description: "Printer access code/password for LAN/local bridge methods." },
|
|
926
|
+
username: { type: "string", description: "Printer username for LAN/local bridge methods; defaults to bblp." },
|
|
927
|
+
password: { type: "string", description: "Printer password/access code override for LAN/local bridge methods." },
|
|
928
|
+
bed_type: { type: "string", enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate"], description: "Bed plate type currently installed (default: textured_plate)." },
|
|
929
|
+
plate_index: { type: "number", description: "Zero-based plate index to print from the sliced 3MF; converted to FULU's one-based PrintParams plate_index." },
|
|
930
|
+
project_name: { type: "string", description: "Optional project name sent in FULU PrintParams; defaults to the 3MF filename without extension." },
|
|
931
|
+
preset_name: { type: "string", description: "Optional preset name sent in FULU PrintParams; defaults to project plus one-based plate index." },
|
|
932
|
+
task_name: { type: "string", description: "Optional BambuNetwork task name; defaults to the project name." },
|
|
933
|
+
config_filename: { type: "string", description: "Optional config 3MF path for cloud print; defaults to the same 3MF path." },
|
|
934
|
+
bridge_command: { type: "string", description: "Override command for the FULU bridge host or macOS/WSL wrapper; defaults to BAMBU_NETWORK_BRIDGE_COMMAND." },
|
|
935
|
+
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." },
|
|
936
|
+
country_code: { type: "string", description: "BambuNetwork country code, such as US, used by the agent during startup." },
|
|
937
|
+
user_info: { type: "string", description: "Optional BambuNetwork user_info JSON string to pass to net.change_user after the agent starts." },
|
|
938
|
+
timeout_ms: { type: "number", description: "Bridge request timeout in milliseconds." },
|
|
939
|
+
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." },
|
|
940
|
+
slicer_path: { type: "string", description: "Path to the slicer executable for auto-slicing; defaults to value from env or a platform default." },
|
|
941
|
+
slicer_profile: { type: "string", description: "Path to an optional slicer profile/config file for auto-slicing." },
|
|
942
|
+
nozzle_diameter: { type: "string", description: "Nozzle diameter in mm for auto-slicing (default: 0.4)." },
|
|
943
|
+
use_ams: { type: "boolean", description: "Whether to use the AMS; defaults to auto-detect from the 3MF mapping." },
|
|
944
|
+
ams_mapping: { type: "array", description: "AMS slot mapping array used by both local MCP printing and FULU PrintParams.", items: { type: "number" } },
|
|
945
|
+
ams_mapping_bridge: { type: "string", description: "Raw JSON string override for FULU PrintParams ams_mapping when the automatic array is not enough." },
|
|
946
|
+
ams_mapping2: { type: "string", description: "Raw JSON string for FULU PrintParams ams_mapping2, matching OrcaSlicer-bambulab's v1 AMS mapping field." },
|
|
947
|
+
ams_mapping_info: { type: "string", description: "Raw JSON string for FULU PrintParams ams_mapping_info, matching OrcaSlicer-bambulab's detailed AMS mapping field." },
|
|
948
|
+
nozzle_mapping: { type: "string", description: "Raw JSON string for FULU PrintParams nozzle_mapping." },
|
|
949
|
+
nozzles_info: { type: "string", description: "Raw JSON string for FULU PrintParams nozzles_info." },
|
|
950
|
+
bed_leveling: { type: "boolean", description: "Enable auto bed leveling in FULU PrintParams (default: true)." },
|
|
951
|
+
flow_calibration: { type: "boolean", description: "Enable flow calibration in FULU PrintParams (default: true)." },
|
|
952
|
+
vibration_calibration: { type: "boolean", description: "Enable vibration calibration in FULU PrintParams (default: true)." },
|
|
953
|
+
layer_inspect: { type: "boolean", description: "Enable first-layer inspection where supported (default: false for BambuNetwork bridge)." },
|
|
954
|
+
timelapse: { type: "boolean", description: "Enable timelapse recording in FULU PrintParams (default: false)." },
|
|
955
|
+
use_ssl_for_ftp: { type: "boolean", description: "Whether FULU local print should use SSL for FTP (default: true)." },
|
|
956
|
+
use_ssl_for_mqtt: { type: "boolean", description: "Whether FULU local print should use SSL for MQTT (default: true)." },
|
|
957
|
+
external_change_assist: { type: "boolean", description: "Enable FULU PrintParams task_ext_change_assist for external filament change assistance." },
|
|
958
|
+
try_emmc_print: { type: "boolean", description: "Enable FULU PrintParams try_emmc_print for printers that support internal storage printing." },
|
|
959
|
+
extra_options: { type: "string", description: "Raw JSON string or text for FULU PrintParams extra_options." },
|
|
960
|
+
client_job_id: { type: "number", description: "Optional client job id sent to the bridge; defaults to the current timestamp." }
|
|
961
|
+
},
|
|
962
|
+
required: ["three_mf_path", "bambu_model"]
|
|
963
|
+
}
|
|
964
|
+
},
|
|
443
965
|
{
|
|
444
966
|
name: "upload_gcode",
|
|
445
967
|
description: "Upload a G-code file to the Bambu Lab printer",
|
|
@@ -447,12 +969,17 @@ class BambuPrinterMCPServer {
|
|
|
447
969
|
type: "object",
|
|
448
970
|
properties: {
|
|
449
971
|
filename: { type: "string", description: "Name for the file on the printer" },
|
|
450
|
-
gcode: { type: "string", description: "G-code content to upload" },
|
|
972
|
+
gcode: { type: "string", description: "G-code content to upload, or a readable local .gcode path. For large files, prefer gcode_path." },
|
|
973
|
+
gcode_path: { type: "string", description: "Local path to a .gcode file to upload. This avoids sending large G-code bodies through the MCP request." },
|
|
451
974
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
452
975
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
453
976
|
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
454
977
|
},
|
|
455
|
-
required: ["filename",
|
|
978
|
+
required: ["filename"],
|
|
979
|
+
anyOf: [
|
|
980
|
+
{ required: ["gcode"] },
|
|
981
|
+
{ required: ["gcode_path"] }
|
|
982
|
+
]
|
|
456
983
|
}
|
|
457
984
|
},
|
|
458
985
|
{
|
|
@@ -464,6 +991,11 @@ class BambuPrinterMCPServer {
|
|
|
464
991
|
file_path: { type: "string", description: "Local path to the file to upload" },
|
|
465
992
|
filename: { type: "string", description: "Name for the file on the printer" },
|
|
466
993
|
print: { type: "boolean", description: "Start printing after upload (default: false)" },
|
|
994
|
+
bambu_model: {
|
|
995
|
+
type: "string",
|
|
996
|
+
enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
|
|
997
|
+
description: "Required when print is true. Bambu Lab printer model used as a safety confirmation before starting the uploaded file."
|
|
998
|
+
},
|
|
467
999
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
468
1000
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
469
1001
|
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
@@ -471,6 +1003,25 @@ class BambuPrinterMCPServer {
|
|
|
471
1003
|
required: ["file_path", "filename"]
|
|
472
1004
|
}
|
|
473
1005
|
},
|
|
1006
|
+
{
|
|
1007
|
+
name: "start_print",
|
|
1008
|
+
description: "Start printing a G-code file already on the Bambu Lab printer. Alias of start_print_job for upstream MCP compatibility.",
|
|
1009
|
+
inputSchema: {
|
|
1010
|
+
type: "object",
|
|
1011
|
+
properties: {
|
|
1012
|
+
filename: { type: "string", description: "Name of the file to print" },
|
|
1013
|
+
bambu_model: {
|
|
1014
|
+
type: "string",
|
|
1015
|
+
enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
|
|
1016
|
+
description: "REQUIRED: Bambu Lab printer model. Ask the user if not known. Starting G-code for the wrong model can damage the printer."
|
|
1017
|
+
},
|
|
1018
|
+
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
1019
|
+
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
1020
|
+
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
1021
|
+
},
|
|
1022
|
+
required: ["filename", "bambu_model"]
|
|
1023
|
+
}
|
|
1024
|
+
},
|
|
474
1025
|
{
|
|
475
1026
|
name: "start_print_job",
|
|
476
1027
|
description: "Start printing a G-code file already on the Bambu Lab printer",
|
|
@@ -478,11 +1029,16 @@ class BambuPrinterMCPServer {
|
|
|
478
1029
|
type: "object",
|
|
479
1030
|
properties: {
|
|
480
1031
|
filename: { type: "string", description: "Name of the file to print" },
|
|
1032
|
+
bambu_model: {
|
|
1033
|
+
type: "string",
|
|
1034
|
+
enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
|
|
1035
|
+
description: "REQUIRED: Bambu Lab printer model. Ask the user if not known. Starting G-code for the wrong model can damage the printer."
|
|
1036
|
+
},
|
|
481
1037
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
482
1038
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
483
1039
|
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
484
1040
|
},
|
|
485
|
-
required: ["filename"]
|
|
1041
|
+
required: ["filename", "bambu_model"]
|
|
486
1042
|
}
|
|
487
1043
|
},
|
|
488
1044
|
{
|
|
@@ -524,14 +1080,35 @@ class BambuPrinterMCPServer {
|
|
|
524
1080
|
enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
|
|
525
1081
|
description: "REQUIRED: Bambu Lab printer model. Ask the user if not known. Using the wrong model can damage the printer."
|
|
526
1082
|
},
|
|
1083
|
+
connection_mode: {
|
|
1084
|
+
type: "string",
|
|
1085
|
+
enum: ["lan_mqtt_ftps", "bambu_network"],
|
|
1086
|
+
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."
|
|
1087
|
+
},
|
|
1088
|
+
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." },
|
|
1089
|
+
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." },
|
|
1090
|
+
dev_id: { type: "string", description: "Bambu device id for FULU BambuNetwork printing; defaults to BAMBU_DEV_ID or BAMBU_SERIAL." },
|
|
1091
|
+
dev_ip: { type: "string", description: "Printer IP address for FULU BambuNetwork LAN/local print methods; defaults to host when provided." },
|
|
1092
|
+
bridge_command: { type: "string", description: "Override command for the FULU bridge host or macOS/WSL wrapper; defaults to BAMBU_NETWORK_BRIDGE_COMMAND." },
|
|
1093
|
+
bambu_network_config_dir: { type: "string", description: "Config/log directory used by the FULU BambuNetwork agent." },
|
|
1094
|
+
country_code: { type: "string", description: "BambuNetwork country code, such as US, used by the FULU bridge agent." },
|
|
1095
|
+
user_info: { type: "string", description: "Optional BambuNetwork user_info JSON string passed to net.change_user for the FULU bridge." },
|
|
527
1096
|
bed_type: {
|
|
528
1097
|
type: "string",
|
|
529
1098
|
enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate"],
|
|
530
1099
|
description: "Bed plate type currently installed (default: textured_plate)"
|
|
531
1100
|
},
|
|
1101
|
+
plate_index: { type: "number", description: "Zero-based plate index to print from the sliced 3MF (default: 0)" },
|
|
532
1102
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
533
1103
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
534
1104
|
bambu_token: { type: "string", description: "Access token (default: value from env)" },
|
|
1105
|
+
slicer_type: {
|
|
1106
|
+
type: "string",
|
|
1107
|
+
enum: SLICER_SCHEMA_VALUES,
|
|
1108
|
+
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."
|
|
1109
|
+
},
|
|
1110
|
+
slicer_path: { type: "string", description: "Path to the slicer executable for auto-slicing (default: value from env or a platform default)" },
|
|
1111
|
+
slicer_profile: { type: "string", description: "Path to an optional slicer profile/config file for auto-slicing" },
|
|
535
1112
|
use_ams: { type: "boolean", description: "Whether to use the AMS (default: auto-detect from 3MF)" },
|
|
536
1113
|
ams_mapping: {
|
|
537
1114
|
type: "array",
|
|
@@ -541,6 +1118,7 @@ class BambuPrinterMCPServer {
|
|
|
541
1118
|
bed_leveling: { type: "boolean", description: "Enable auto bed leveling (default: true)" },
|
|
542
1119
|
flow_calibration: { type: "boolean", description: "Enable flow calibration (default: true)" },
|
|
543
1120
|
vibration_calibration: { type: "boolean", description: "Enable vibration calibration (default: true)" },
|
|
1121
|
+
layer_inspect: { type: "boolean", description: "Enable first-layer inspection where supported (default: printer/profile behavior)" },
|
|
544
1122
|
timelapse: { type: "boolean", description: "Enable timelapse recording (default: false)" },
|
|
545
1123
|
nozzle_diameter: { type: "string", description: "Nozzle diameter in mm for auto-slicing (default: 0.4)" }
|
|
546
1124
|
},
|
|
@@ -609,10 +1187,6 @@ class BambuPrinterMCPServer {
|
|
|
609
1187
|
const host = String(args?.host || DEFAULT_HOST);
|
|
610
1188
|
const bambuSerial = String(args?.bambu_serial || DEFAULT_BAMBU_SERIAL);
|
|
611
1189
|
const bambuToken = String(args?.bambu_token || DEFAULT_BAMBU_TOKEN);
|
|
612
|
-
const slicerType = String(args?.slicer_type || DEFAULT_SLICER_TYPE) as 'prusaslicer' | 'cura' | 'slic3r' | 'orcaslicer' | 'bambustudio';
|
|
613
|
-
const slicerPath = String(args?.slicer_path || DEFAULT_SLICER_PATH);
|
|
614
|
-
const slicerProfile = String(args?.slicer_profile || DEFAULT_SLICER_PROFILE);
|
|
615
|
-
|
|
616
1190
|
try {
|
|
617
1191
|
let result;
|
|
618
1192
|
|
|
@@ -625,15 +1199,56 @@ class BambuPrinterMCPServer {
|
|
|
625
1199
|
result = await this.bambu.getFiles(host, bambuSerial, bambuToken);
|
|
626
1200
|
break;
|
|
627
1201
|
|
|
1202
|
+
case "bambu_network_bridge_status": {
|
|
1203
|
+
const bridgeArgs = args as Record<string, any> | undefined;
|
|
1204
|
+
const options = this.bridgeOptionsFromArgs(bridgeArgs);
|
|
1205
|
+
result = this.bambuNetwork.getStatus(options);
|
|
1206
|
+
if (Boolean(bridgeArgs?.connect)) {
|
|
1207
|
+
const probe = await this.bambuNetwork.ensureAgent(options);
|
|
1208
|
+
result = {
|
|
1209
|
+
...this.bambuNetwork.getStatus(options),
|
|
1210
|
+
connected: true,
|
|
1211
|
+
agent: probe.agent,
|
|
1212
|
+
handshake: probe.handshake,
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
break;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
case "bambu_network_call": {
|
|
1219
|
+
if (!args?.method) {
|
|
1220
|
+
throw new Error("Missing required parameter: method");
|
|
1221
|
+
}
|
|
1222
|
+
const bridgeArgs = args as Record<string, any>;
|
|
1223
|
+
const payload = bridgeArgs.payload && typeof bridgeArgs.payload === "object"
|
|
1224
|
+
? bridgeArgs.payload as Record<string, unknown>
|
|
1225
|
+
: {};
|
|
1226
|
+
const options = this.bridgeOptionsFromArgs(bridgeArgs);
|
|
1227
|
+
result = bridgeArgs.with_agent === false
|
|
1228
|
+
? await this.bambuNetwork.request(String(bridgeArgs.method), payload, options)
|
|
1229
|
+
: await this.bambuNetwork.callWithAgent(String(bridgeArgs.method), payload, options);
|
|
1230
|
+
break;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
case "print_3mf_bambu_network": {
|
|
1234
|
+
result = await this.print3mfViaBambuNetwork(args as Record<string, any>, host, bambuSerial, bambuToken);
|
|
1235
|
+
break;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
628
1238
|
case "upload_gcode": {
|
|
629
|
-
if (!args?.filename
|
|
630
|
-
throw new Error("Missing required
|
|
1239
|
+
if (!args?.filename) {
|
|
1240
|
+
throw new Error("Missing required parameter: filename");
|
|
1241
|
+
}
|
|
1242
|
+
const uploadSource = resolveUploadGcodeSource(args as Record<string, unknown>);
|
|
1243
|
+
try {
|
|
1244
|
+
result = await this.bambu.uploadFile(
|
|
1245
|
+
host, bambuSerial, bambuToken, uploadSource.filePath, String(args.filename), false
|
|
1246
|
+
);
|
|
1247
|
+
} finally {
|
|
1248
|
+
if (uploadSource.cleanupDir) {
|
|
1249
|
+
fs.rmSync(uploadSource.cleanupDir, { recursive: true, force: true });
|
|
1250
|
+
}
|
|
631
1251
|
}
|
|
632
|
-
const tmpPath = path.join(TEMP_DIR, String(args.filename));
|
|
633
|
-
fs.writeFileSync(tmpPath, String(args.gcode));
|
|
634
|
-
result = await this.bambu.uploadFile(
|
|
635
|
-
host, bambuSerial, bambuToken, tmpPath, String(args.filename), false
|
|
636
|
-
);
|
|
637
1252
|
break;
|
|
638
1253
|
}
|
|
639
1254
|
|
|
@@ -641,6 +1256,9 @@ class BambuPrinterMCPServer {
|
|
|
641
1256
|
if (!args?.file_path || !args?.filename) {
|
|
642
1257
|
throw new Error("Missing required parameters: file_path and filename");
|
|
643
1258
|
}
|
|
1259
|
+
if (Boolean(args.print ?? false)) {
|
|
1260
|
+
await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
1261
|
+
}
|
|
644
1262
|
result = await this.bambu.uploadFile(
|
|
645
1263
|
host, bambuSerial, bambuToken,
|
|
646
1264
|
String(args.file_path), String(args.filename),
|
|
@@ -648,10 +1266,12 @@ class BambuPrinterMCPServer {
|
|
|
648
1266
|
);
|
|
649
1267
|
break;
|
|
650
1268
|
|
|
1269
|
+
case "start_print":
|
|
651
1270
|
case "start_print_job":
|
|
652
1271
|
if (!args?.filename) {
|
|
653
1272
|
throw new Error("Missing required parameter: filename");
|
|
654
1273
|
}
|
|
1274
|
+
await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
655
1275
|
result = await this.bambu.startJob(host, bambuSerial, bambuToken, String(args.filename));
|
|
656
1276
|
break;
|
|
657
1277
|
|
|
@@ -717,6 +1337,7 @@ class BambuPrinterMCPServer {
|
|
|
717
1337
|
if (!args?.stl_path) {
|
|
718
1338
|
throw new Error("Missing required parameter: stl_path");
|
|
719
1339
|
}
|
|
1340
|
+
const { slicerType, slicerPath, slicerProfile } = resolveSlicerConfig(args);
|
|
720
1341
|
const sliceModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
721
1342
|
const nozzleDiam = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
722
1343
|
// Resolve printer preset for BambuStudio slicer
|
|
@@ -730,7 +1351,18 @@ class BambuPrinterMCPServer {
|
|
|
730
1351
|
if (args?.ensure_on_bed !== undefined) sliceBambuOptions.ensureOnBed = Boolean(args.ensure_on_bed);
|
|
731
1352
|
if (args?.clone_objects !== undefined) sliceBambuOptions.cloneObjects = String(args.clone_objects);
|
|
732
1353
|
if (args?.skip_objects !== undefined) sliceBambuOptions.skipObjects = String(args.skip_objects);
|
|
733
|
-
if (
|
|
1354
|
+
if (
|
|
1355
|
+
args?.load_filaments !== undefined &&
|
|
1356
|
+
args?.filament_profile !== undefined &&
|
|
1357
|
+
String(args.load_filaments) !== String(args.filament_profile)
|
|
1358
|
+
) {
|
|
1359
|
+
throw new Error("Provide either load_filaments or filament_profile, not conflicting values.");
|
|
1360
|
+
}
|
|
1361
|
+
if (args?.load_filaments !== undefined) {
|
|
1362
|
+
sliceBambuOptions.loadFilaments = String(args.load_filaments);
|
|
1363
|
+
} else if (args?.filament_profile !== undefined) {
|
|
1364
|
+
sliceBambuOptions.loadFilaments = String(args.filament_profile);
|
|
1365
|
+
}
|
|
734
1366
|
if (args?.load_filament_ids !== undefined) sliceBambuOptions.loadFilamentIds = String(args.load_filament_ids);
|
|
735
1367
|
if (args?.enable_timelapse !== undefined) sliceBambuOptions.enableTimelapse = Boolean(args.enable_timelapse);
|
|
736
1368
|
if (args?.allow_mix_temp !== undefined) sliceBambuOptions.allowMixTemp = Boolean(args.allow_mix_temp);
|
|
@@ -756,14 +1388,26 @@ class BambuPrinterMCPServer {
|
|
|
756
1388
|
if (!args?.three_mf_path) {
|
|
757
1389
|
throw new Error("Missing required parameter: three_mf_path");
|
|
758
1390
|
}
|
|
1391
|
+
|
|
1392
|
+
if (String(args?.connection_mode || "lan_mqtt_ftps") === "bambu_network") {
|
|
1393
|
+
result = await this.print3mfViaBambuNetwork(args as Record<string, any>, host, bambuSerial, bambuToken);
|
|
1394
|
+
break;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
759
1397
|
if (!bambuSerial || !bambuToken) {
|
|
760
1398
|
throw new Error("Bambu serial number and access token are required for print_3mf.");
|
|
761
1399
|
}
|
|
762
1400
|
|
|
1401
|
+
const { slicerType, slicerPath, slicerProfile } = resolveSlicerConfig(args);
|
|
763
1402
|
const printModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
764
1403
|
const printBedType = resolveBedType(args?.bed_type as string | undefined);
|
|
765
1404
|
const printNozzle = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
766
1405
|
const printPreset = BAMBU_MODEL_PRESETS[printModel]?.(printNozzle);
|
|
1406
|
+
const plateIndex = args?.plate_index !== undefined ? Number(args.plate_index) : 0;
|
|
1407
|
+
|
|
1408
|
+
if (!Number.isInteger(plateIndex) || plateIndex < 0) {
|
|
1409
|
+
throw new Error("plate_index must be a non-negative integer.");
|
|
1410
|
+
}
|
|
767
1411
|
|
|
768
1412
|
let threeMFPath = String(args.three_mf_path);
|
|
769
1413
|
|
|
@@ -776,7 +1420,7 @@ class BambuPrinterMCPServer {
|
|
|
776
1420
|
f => f.match(/Metadata\/plate_\d+\.gcode/i) || f.endsWith('.gcode')
|
|
777
1421
|
);
|
|
778
1422
|
if (!hasGcode) {
|
|
779
|
-
console.log(`3MF has no gcode
|
|
1423
|
+
console.log(`3MF has no gcode - auto-slicing with ${slicerType} for ${printModel}`);
|
|
780
1424
|
const autoSliceOptions: BambuSliceOptions = {
|
|
781
1425
|
uptodate: true,
|
|
782
1426
|
ensureOnBed: true,
|
|
@@ -806,7 +1450,8 @@ class BambuPrinterMCPServer {
|
|
|
806
1450
|
}
|
|
807
1451
|
|
|
808
1452
|
let finalAmsMapping = parsedAmsMapping;
|
|
809
|
-
|
|
1453
|
+
const explicitUseAMS = args?.use_ams !== undefined;
|
|
1454
|
+
let useAMS = explicitUseAMS ? Boolean(args.use_ams) : (!!finalAmsMapping && finalAmsMapping.length > 0);
|
|
810
1455
|
|
|
811
1456
|
if (args?.ams_mapping) {
|
|
812
1457
|
let userMappingOverride: number[] | undefined;
|
|
@@ -827,7 +1472,7 @@ class BambuPrinterMCPServer {
|
|
|
827
1472
|
finalAmsMapping = undefined;
|
|
828
1473
|
useAMS = false;
|
|
829
1474
|
}
|
|
830
|
-
if (!finalAmsMapping || finalAmsMapping.length === 0) {
|
|
1475
|
+
if ((!finalAmsMapping || finalAmsMapping.length === 0) && !explicitUseAMS) {
|
|
831
1476
|
useAMS = false;
|
|
832
1477
|
}
|
|
833
1478
|
|
|
@@ -837,7 +1482,7 @@ class BambuPrinterMCPServer {
|
|
|
837
1482
|
result = await this.bambu.print3mf(host, bambuSerial, bambuToken, {
|
|
838
1483
|
projectName,
|
|
839
1484
|
filePath: threeMFPath,
|
|
840
|
-
plateIndex
|
|
1485
|
+
plateIndex,
|
|
841
1486
|
useAMS: useAMS,
|
|
842
1487
|
amsMapping: finalAmsMapping,
|
|
843
1488
|
bedType: printBedType,
|