bambu-printer-mcp 1.1.12 → 1.1.14
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/CONTRIBUTORS.md +5 -3
- package/README.md +28 -8
- package/dist/bambu-connect.d.ts +7 -0
- package/dist/bambu-connect.js +77 -0
- package/dist/bambu-native.d.ts +66 -0
- package/dist/bambu-native.js +609 -0
- package/dist/bambu-network-bridge.d.ts +4 -0
- package/dist/bambu-network-bridge.js +3 -0
- package/dist/index.js +644 -109
- package/dist/printers/bambu.d.ts +51 -16
- package/dist/printers/bambu.js +392 -153
- package/dist/safety/archive.d.ts +11 -0
- package/dist/safety/archive.js +214 -0
- package/dist/safety/artifact.d.ts +6 -0
- package/dist/safety/artifact.js +66 -0
- package/dist/safety/limits.d.ts +29 -0
- package/dist/safety/limits.js +100 -0
- package/dist/safety/print-file.d.ts +25 -0
- package/dist/safety/print-file.js +514 -0
- package/dist/safety/printer-state.d.ts +43 -0
- package/dist/safety/printer-state.js +351 -0
- package/native/bambu-native-print.cpp +758 -0
- package/package.json +7 -3
- package/scripts/build-bambu-native.zsh +33 -0
- package/src/bambu-connect.ts +100 -0
- package/src/bambu-native.ts +722 -0
- package/src/bambu-network-bridge.ts +7 -0
- package/src/index.ts +720 -149
- package/src/printers/bambu.ts +374 -182
- package/src/safety/archive.ts +181 -0
- package/src/safety/artifact.ts +58 -0
- package/src/safety/limits.ts +93 -0
- package/src/safety/print-file.ts +352 -0
- package/src/safety/printer-state.ts +323 -0
package/src/index.ts
CHANGED
|
@@ -25,11 +25,25 @@ import {
|
|
|
25
25
|
type SlicerType,
|
|
26
26
|
} from "./stl/stl-manipulator.js";
|
|
27
27
|
import { BambuNetworkBridge, type BambuNetworkBridgeOptions } from "./bambu-network-bridge.js";
|
|
28
|
+
import {
|
|
29
|
+
assertBambuNativeAvailable,
|
|
30
|
+
validateBambuNativeMetadata,
|
|
31
|
+
buildBambuNativeFanCommand,
|
|
32
|
+
buildBambuNativeTemperatureCommand,
|
|
33
|
+
printWithBambuNative,
|
|
34
|
+
sendCommandWithBambuNative,
|
|
35
|
+
uploadWithBambuNative,
|
|
36
|
+
type BambuNativeUpdateCallback,
|
|
37
|
+
} from "./bambu-native.js";
|
|
38
|
+
import { importFileViaBambuConnect } from "./bambu-connect.js";
|
|
28
39
|
import { BlenderMcpBridge } from "./blender-mcp-bridge.js";
|
|
29
40
|
import { hasAmsMappingInput, normalizeAmsMappingObject, normalizeBridgeAmsTrayValue } from "./ams-mapping.js";
|
|
30
41
|
import { analyze3MFAmsRequirements, analyze3MFPlateObjects, analyzeCollarCharm3MF, extractBambuTemplateSettings, getCollarCharmRolePolicy, parse3MF } from './3mf_parser.js';
|
|
31
42
|
import type { ThreeMFAmsRequirements } from "./types.js";
|
|
32
43
|
import { BambuImplementation, assertDirectPrintSupported } from "./printers/bambu.js";
|
|
44
|
+
import { inspectPrintFile } from "./safety/print-file.js";
|
|
45
|
+
import { validatePrinterState } from "./safety/printer-state.js";
|
|
46
|
+
import { withPrinterOperation, withPrintSnapshot, uniquePrintName } from "./safety/artifact.js";
|
|
33
47
|
|
|
34
48
|
dotenv.config();
|
|
35
49
|
|
|
@@ -61,12 +75,40 @@ const DEFAULT_BAMBU_MODEL =
|
|
|
61
75
|
process.env.BAMBU_PRINTER_MODEL?.trim().toLowerCase() ||
|
|
62
76
|
process.env.BAMBU_MODEL?.trim().toLowerCase() ||
|
|
63
77
|
"";
|
|
78
|
+
const DEFAULT_BAMBU_CONNECTION_MODE =
|
|
79
|
+
process.env.BAMBU_DEFAULT_CONNECTION_MODE?.trim().toLowerCase() || "";
|
|
64
80
|
const DEFAULT_BED_TYPE = process.env.BED_TYPE?.trim().toLowerCase() || "textured_plate";
|
|
65
81
|
const DEFAULT_NOZZLE_DIAMETER = process.env.NOZZLE_DIAMETER?.trim() || "0.4";
|
|
66
82
|
|
|
83
|
+
function requestedPrintNozzles(args: Record<string, any>): number[] | undefined {
|
|
84
|
+
if (args.nozzle_diameters !== undefined && args.nozzle_diameter !== undefined) {
|
|
85
|
+
throw new Error("Supply nozzle_diameters or nozzle_diameter, not both.");
|
|
86
|
+
}
|
|
87
|
+
const raw = args.nozzle_diameters !== undefined ? args.nozzle_diameters :
|
|
88
|
+
args.nozzle_diameter !== undefined ? [args.nozzle_diameter] : undefined;
|
|
89
|
+
// A pre-sliced job already declares every nozzle; compare those with the
|
|
90
|
+
// printer rather than broadcasting the CLI's default diameter over them.
|
|
91
|
+
if (raw === undefined) return undefined;
|
|
92
|
+
if (!Array.isArray(raw) || !raw.length || raw.length > 2 || raw.some(value =>
|
|
93
|
+
(typeof value !== "number" && typeof value !== "string") || ![0.2, 0.4, 0.6, 0.8].includes(Number(value)))) {
|
|
94
|
+
throw new Error("Nozzle diameters must contain one or two supported sizes: 0.2, 0.4, 0.6, 0.8 mm.");
|
|
95
|
+
}
|
|
96
|
+
return raw.map(Number);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function assertUniformCliNozzles(args: Record<string, any>): void {
|
|
100
|
+
const nozzles = requestedPrintNozzles(args);
|
|
101
|
+
if (nozzles && new Set(nozzles).size > 1) {
|
|
102
|
+
throw new Error("Mixed nozzle diameters require a pre-sliced 3MF with complete per-nozzle metadata. Export it from the slicer GUI before printing.");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
67
106
|
const VALID_BAMBU_MODELS = ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s", "h2c", "x2d"] as const;
|
|
68
107
|
type BambuModel = typeof VALID_BAMBU_MODELS[number];
|
|
69
|
-
|
|
108
|
+
// X2D uses the same project_file/ams_mapping2 family of LAN print fields as
|
|
109
|
+
// the dual-nozzle H2 path. Keep it in this gate so pre-sliced multi-filament
|
|
110
|
+
// jobs take the correct branch instead of the legacy 5-slot mapping.
|
|
111
|
+
const H2_BAMBU_MODELS = new Set<string>(["h2d", "h2s", "h2c", "x2d"]);
|
|
70
112
|
|
|
71
113
|
const VALID_BED_TYPES = ["textured_plate", "cool_plate", "engineering_plate", "hot_plate", "supertack_plate"] as const;
|
|
72
114
|
const VALID_BAMBUSTUDIO_CLI_BED_TYPES = ["textured_plate", "cool_plate", "engineering_plate", "hot_plate"] as const;
|
|
@@ -882,6 +924,16 @@ function stringifyBridgeJson(value: unknown): string | undefined {
|
|
|
882
924
|
return JSON.stringify(value);
|
|
883
925
|
}
|
|
884
926
|
|
|
927
|
+
function nativeAmsMapping2(mapping: number[] | undefined): string | undefined {
|
|
928
|
+
if (!mapping || mapping.length === 0) return undefined;
|
|
929
|
+
return JSON.stringify(mapping.map((value) => {
|
|
930
|
+
if (value < 0 || value === 255) return { ams_id: 255, slot_id: 255 };
|
|
931
|
+
if (value === 254) return { ams_id: 254, slot_id: 254 };
|
|
932
|
+
if (value >= 128) return { ams_id: 128, slot_id: value - 128 };
|
|
933
|
+
return { ams_id: Math.floor(value / 4), slot_id: value % 4 };
|
|
934
|
+
}));
|
|
935
|
+
}
|
|
936
|
+
|
|
885
937
|
function redactPrintParams(params: Record<string, unknown>): Record<string, unknown> {
|
|
886
938
|
return {
|
|
887
939
|
...params,
|
|
@@ -1069,7 +1121,22 @@ class BambuPrinterMCPServer {
|
|
|
1069
1121
|
}
|
|
1070
1122
|
);
|
|
1071
1123
|
|
|
1072
|
-
this.bambu = new BambuImplementation(
|
|
1124
|
+
this.bambu = new BambuImplementation(async message => {
|
|
1125
|
+
try {
|
|
1126
|
+
const response = await this.server.elicitInput({
|
|
1127
|
+
mode: "form",
|
|
1128
|
+
message,
|
|
1129
|
+
requestedSchema: {
|
|
1130
|
+
type: "object",
|
|
1131
|
+
properties: { confirmed: { type: "boolean", title: "I checked the printer and confirm this operation", default: false } },
|
|
1132
|
+
required: ["confirmed"],
|
|
1133
|
+
},
|
|
1134
|
+
});
|
|
1135
|
+
return response.action === "accept" && response.content?.confirmed === true;
|
|
1136
|
+
} catch {
|
|
1137
|
+
throw new Error("Human hardware confirmation requires an MCP client with elicitation support. For deliberately headless operation, BAMBU_REQUIRE_CONFIRMATION=0 disables ordinary print/heat prompts; finished-bed clearance and hardware-error confirmation still require elicitation.");
|
|
1138
|
+
}
|
|
1139
|
+
});
|
|
1073
1140
|
this.bambuNetwork = new BambuNetworkBridge();
|
|
1074
1141
|
this.stlManipulator = new STLManipulator(TEMP_DIR);
|
|
1075
1142
|
|
|
@@ -1233,13 +1300,12 @@ class BambuPrinterMCPServer {
|
|
|
1233
1300
|
const JSZip = (await import('jszip')).default;
|
|
1234
1301
|
const zipData = fs.readFileSync(threeMFPath);
|
|
1235
1302
|
const zip = await JSZip.loadAsync(zipData);
|
|
1236
|
-
const hasGcode = Object.
|
|
1237
|
-
f => f.match(/Metadata\/plate_\d+\.gcode/i) || f.endsWith('.gcode')
|
|
1238
|
-
);
|
|
1303
|
+
const hasGcode = Object.values(zip.files).some(entry => !entry.dir && /\.gcode$/i.test(entry.name));
|
|
1239
1304
|
|
|
1240
1305
|
if (hasGcode) {
|
|
1241
1306
|
return { threeMFPath, autoSliced: false };
|
|
1242
1307
|
}
|
|
1308
|
+
assertUniformCliNozzles(args);
|
|
1243
1309
|
|
|
1244
1310
|
if (bedType === "supertack_plate") {
|
|
1245
1311
|
throw new Error(
|
|
@@ -1342,6 +1408,9 @@ class BambuPrinterMCPServer {
|
|
|
1342
1408
|
useAMS = false;
|
|
1343
1409
|
}
|
|
1344
1410
|
if ((!finalAmsMapping || finalAmsMapping.length === 0) && (!finalAmsSlots || finalAmsSlots.length === 0)) {
|
|
1411
|
+
if (args.use_ams === true || (args.use_ams !== false && (hasUserAmsMapping || hasUserAmsSlots))) {
|
|
1412
|
+
throw new Error("AMS printing requires a nonempty ams_mapping, ams_slots, or auto_match_ams result.");
|
|
1413
|
+
}
|
|
1345
1414
|
useAMS = false;
|
|
1346
1415
|
}
|
|
1347
1416
|
|
|
@@ -1352,7 +1421,8 @@ class BambuPrinterMCPServer {
|
|
|
1352
1421
|
args: Record<string, any>,
|
|
1353
1422
|
host: string,
|
|
1354
1423
|
bambuSerial: string,
|
|
1355
|
-
bambuToken: string
|
|
1424
|
+
bambuToken: string,
|
|
1425
|
+
native?: { onUpdate?: BambuNativeUpdateCallback; signal?: AbortSignal }
|
|
1356
1426
|
): Promise<Record<string, unknown>> {
|
|
1357
1427
|
if (!args?.three_mf_path) {
|
|
1358
1428
|
throw new Error("Missing required parameter: three_mf_path");
|
|
@@ -1360,7 +1430,8 @@ class BambuPrinterMCPServer {
|
|
|
1360
1430
|
|
|
1361
1431
|
const printModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
1362
1432
|
const printBedType = resolveBedType(args?.bed_type as string | undefined);
|
|
1363
|
-
const
|
|
1433
|
+
const printNozzles = requestedPrintNozzles(args);
|
|
1434
|
+
const printNozzle = String(printNozzles?.[0] ?? DEFAULT_NOZZLE_DIAMETER);
|
|
1364
1435
|
const printPreset = BAMBU_MODEL_PRESETS[printModel]?.(printNozzle);
|
|
1365
1436
|
const plateIndex = args?.plate_index !== undefined ? Number(args.plate_index) : 0;
|
|
1366
1437
|
|
|
@@ -1395,6 +1466,13 @@ class BambuPrinterMCPServer {
|
|
|
1395
1466
|
throw new Error("bambu_token/access code is required for FULU BambuNetwork LAN/local print methods.");
|
|
1396
1467
|
}
|
|
1397
1468
|
|
|
1469
|
+
if (!bambuSerial || devId !== bambuSerial || !devIp || devIp === "localhost" || !(password || bambuToken)) {
|
|
1470
|
+
throw new Error("Bridge printing requires a matching bambu_serial/dev_id, printer host, and LAN access token for fresh MQTT safety verification, including cloud dispatch.");
|
|
1471
|
+
}
|
|
1472
|
+
for (const field of ["config_filename", "ftp_file", "ftp_file_md5", "ams_mapping_bridge", "ams_mapping2", "ams_mapping_info", "nozzle_mapping", "nozzles_info", "extra_options"]) {
|
|
1473
|
+
if (args[field] !== undefined && args[field] !== "") throw new Error(`${field} cannot override a verified print. Use the structured plate, nozzle, and AMS arguments.`);
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1398
1476
|
const { threeMFPath, autoSliced } = await this.ensurePrintableThreeMFPath(
|
|
1399
1477
|
args,
|
|
1400
1478
|
printModel,
|
|
@@ -1410,92 +1488,253 @@ class BambuPrinterMCPServer {
|
|
|
1410
1488
|
printModel,
|
|
1411
1489
|
printNozzle
|
|
1412
1490
|
);
|
|
1491
|
+
if (native && args.use_ams !== false && !useAMS) {
|
|
1492
|
+
throw new Error("Native X2D printing requires ams_mapping, ams_slots, auto_match_ams, or explicit use_ams: false for an external spool.");
|
|
1493
|
+
}
|
|
1413
1494
|
const threeMfFilename = path.basename(threeMFPath);
|
|
1414
|
-
const projectName = String(args?.project_name || threeMfFilename.replace(/\.3mf$/i, ''));
|
|
1495
|
+
const projectName = String(args?.project_name || args?.task_name || threeMfFilename.replace(/\.3mf$/i, ''));
|
|
1415
1496
|
const presetName = String(args?.preset_name || `${projectName}_plate_${plateIndex + 1}`);
|
|
1416
1497
|
const clientJobId = args?.client_job_id !== undefined ? Number(args.client_job_id) : Date.now();
|
|
1417
1498
|
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1499
|
+
return withPrinterOperation(devIp, devId, assertActive => withPrintSnapshot(threeMFPath, async (snapshot, retainForDispatchedUpload) => {
|
|
1500
|
+
const inspection = await inspectPrintFile(snapshot, { model: printModel, nozzleDiameters: printNozzles, plateIndex, bedType: printBedType });
|
|
1501
|
+
let mapping = finalAmsMapping?.slice();
|
|
1502
|
+
if (finalAmsSlots) {
|
|
1503
|
+
if (finalAmsSlots.length !== inspection.usedFilamentPositions.length) throw new Error("ams_slots must supply exactly one physical tray per used filament.");
|
|
1504
|
+
mapping = Array(inspection.materials.length).fill(-1);
|
|
1505
|
+
inspection.usedFilamentPositions.forEach((position, index) => { mapping![position] = finalAmsSlots[index]; });
|
|
1506
|
+
}
|
|
1507
|
+
if (!useAMS) {
|
|
1508
|
+
mapping = Array(inspection.materials.length).fill(-1);
|
|
1509
|
+
inspection.usedFilamentPositions.forEach(position => { mapping![position] = 254; });
|
|
1510
|
+
}
|
|
1511
|
+
const requirements = { ...inspection, amsMapping: mapping, useAMS };
|
|
1512
|
+
const initialStatus = await this.bambu.getSafetyStatus(devIp, devId, password || bambuToken);
|
|
1513
|
+
validatePrinterState(initialStatus, requirements);
|
|
1514
|
+
const bedClearance = await this.bambu.confirmPrintPreflight(devId, initialStatus, inspection);
|
|
1515
|
+
const confirmedStatus = await this.bambu.getSafetyStatus(devIp, devId, password || bambuToken);
|
|
1516
|
+
validatePrinterState(confirmedStatus, requirements);
|
|
1517
|
+
this.bambu.assertBedClearance(confirmedStatus, bedClearance);
|
|
1518
|
+
const amsMapping = stringifyBridgeJson(mapping);
|
|
1519
|
+
const remoteFileName = uniquePrintName(threeMFPath);
|
|
1520
|
+
const params: Record<string, unknown> = {
|
|
1521
|
+
dev_id: devId,
|
|
1522
|
+
// Firmware can report the task name instead of the uploaded filename.
|
|
1523
|
+
// Bind either report to this inspected artifact for a later safe resume.
|
|
1524
|
+
task_name: remoteFileName.replace(/(?:\.gcode)?\.3mf$/i, ""),
|
|
1525
|
+
project_name: projectName,
|
|
1526
|
+
preset_name: presetName,
|
|
1527
|
+
filename: snapshot,
|
|
1528
|
+
config_filename: snapshot,
|
|
1529
|
+
plate_index: plateIndex + 1,
|
|
1530
|
+
ftp_folder: String(args?.ftp_folder || ""),
|
|
1531
|
+
ftp_file: String(args?.ftp_file || ""),
|
|
1532
|
+
ftp_file_md5: String(args?.ftp_file_md5 || ""),
|
|
1533
|
+
nozzle_mapping: stringifyBridgeJson(args?.nozzle_mapping) || "",
|
|
1534
|
+
ams_mapping: amsMapping || "",
|
|
1535
|
+
ams_mapping2: stringifyBridgeJson(args?.ams_mapping2) || "",
|
|
1536
|
+
ams_mapping_info: stringifyBridgeJson(args?.ams_mapping_info) || "",
|
|
1537
|
+
nozzles_info: stringifyBridgeJson(args?.nozzles_info) || "",
|
|
1538
|
+
connection_type: connectionType,
|
|
1539
|
+
comments: String(args?.comments || ""),
|
|
1540
|
+
origin_profile_id: args?.origin_profile_id !== undefined ? Number(args.origin_profile_id) : 0,
|
|
1541
|
+
stl_design_id: args?.stl_design_id !== undefined ? Number(args.stl_design_id) : 0,
|
|
1542
|
+
origin_model_id: String(args?.origin_model_id || ""),
|
|
1543
|
+
print_type: String(args?.print_type || "from_normal"),
|
|
1544
|
+
dst_file: remoteFileName,
|
|
1545
|
+
dev_name: String(args?.dev_name || ""),
|
|
1546
|
+
dev_ip: devIp,
|
|
1547
|
+
use_ssl_for_ftp: args?.use_ssl_for_ftp !== undefined ? Boolean(args.use_ssl_for_ftp) : true,
|
|
1548
|
+
use_ssl_for_mqtt: args?.use_ssl_for_mqtt !== undefined ? Boolean(args.use_ssl_for_mqtt) : true,
|
|
1549
|
+
username: String(args?.username || "bblp"),
|
|
1550
|
+
password,
|
|
1551
|
+
task_bed_leveling: args?.bed_leveling !== undefined ? Boolean(args.bed_leveling) : true,
|
|
1552
|
+
task_flow_cali: args?.flow_calibration !== undefined ? Boolean(args.flow_calibration) : true,
|
|
1553
|
+
task_vibration_cali: args?.vibration_calibration !== undefined ? Boolean(args.vibration_calibration) : true,
|
|
1554
|
+
task_layer_inspect: args?.layer_inspect !== undefined ? Boolean(args.layer_inspect) : false,
|
|
1555
|
+
task_record_timelapse: args?.timelapse !== undefined ? Boolean(args.timelapse) : false,
|
|
1556
|
+
task_use_ams: useAMS,
|
|
1557
|
+
task_bed_type: printBedType,
|
|
1558
|
+
extra_options: stringifyBridgeJson(args?.extra_options) || "",
|
|
1559
|
+
auto_bed_leveling: args?.auto_bed_leveling !== undefined ? Number(args.auto_bed_leveling) : 0,
|
|
1560
|
+
auto_flow_cali: args?.auto_flow_cali !== undefined ? Number(args.auto_flow_cali) : 0,
|
|
1561
|
+
auto_offset_cali: args?.auto_offset_cali !== undefined ? Number(args.auto_offset_cali) : 0,
|
|
1562
|
+
extruder_cali_manual_mode: args?.extruder_cali_manual_mode !== undefined ? Number(args.extruder_cali_manual_mode) : -1,
|
|
1563
|
+
task_ext_change_assist: args?.external_change_assist !== undefined ? Boolean(args.external_change_assist) : false,
|
|
1564
|
+
try_emmc_print: args?.try_emmc_print !== undefined ? Boolean(args.try_emmc_print) : false,
|
|
1565
|
+
};
|
|
1463
1566
|
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1567
|
+
assertActive();
|
|
1568
|
+
if (native) {
|
|
1569
|
+
// Release the telemetry client before the installed plug-in opens its session.
|
|
1570
|
+
await this.bambu.disconnectAll();
|
|
1571
|
+
assertActive();
|
|
1572
|
+
const nativeResult = await printWithBambuNative({
|
|
1573
|
+
host: devIp, serial: devId, token: password || bambuToken, filePath: snapshot,
|
|
1574
|
+
projectName: String(params.task_name), presetName, remoteName: remoteFileName,
|
|
1575
|
+
plateIndex, bedType: printBedType, useAMS,
|
|
1576
|
+
amsMapping, amsMapping2: nativeAmsMapping2(mapping),
|
|
1577
|
+
bedLeveling: params.task_bed_leveling as boolean,
|
|
1578
|
+
flowCalibration: params.task_flow_cali as boolean,
|
|
1579
|
+
vibrationCalibration: params.task_vibration_cali as boolean,
|
|
1580
|
+
layerInspect: params.task_layer_inspect as boolean,
|
|
1581
|
+
timelapse: params.task_record_timelapse as boolean,
|
|
1582
|
+
}, native.onUpdate, { signal: native.signal, assertActive, beforeDispatch: async () => {
|
|
1583
|
+
assertActive();
|
|
1584
|
+
const dispatchStatus = await this.bambu.getSafetyStatus(devIp, devId, password || bambuToken);
|
|
1585
|
+
validatePrinterState(dispatchStatus, requirements);
|
|
1586
|
+
this.bambu.assertBedClearance(dispatchStatus, bedClearance);
|
|
1587
|
+
assertActive();
|
|
1588
|
+
} });
|
|
1589
|
+
assertActive();
|
|
1590
|
+
this.bambu.recordCheckedJob(devIp, devId, remoteFileName, requirements);
|
|
1591
|
+
return { ...nativeResult, autoSliced, projectName, plateIndex, useAMS, amsMapping: mapping };
|
|
1592
|
+
}
|
|
1593
|
+
const bridgeResult = await this.bambuNetwork.callWithAgent(
|
|
1594
|
+
bridgeMethod,
|
|
1595
|
+
{ client_job_id: clientJobId, params },
|
|
1596
|
+
{
|
|
1597
|
+
...this.bridgeOptionsFromArgs(args, "print_3mf_bambu_network"),
|
|
1598
|
+
beforeDispatch: async method => {
|
|
1599
|
+
if (method !== bridgeMethod) return;
|
|
1600
|
+
assertActive();
|
|
1601
|
+
const dispatchStatus = await this.bambu.getSafetyStatus(devIp, devId, password || bambuToken);
|
|
1602
|
+
validatePrinterState(dispatchStatus, requirements);
|
|
1603
|
+
this.bambu.assertBedClearance(dispatchStatus, bedClearance);
|
|
1604
|
+
},
|
|
1605
|
+
assertDispatchAllowed: method => { if (method === bridgeMethod) assertActive(); },
|
|
1606
|
+
onDispatched: method => { if (method === bridgeMethod) retainForDispatchedUpload(); },
|
|
1607
|
+
}
|
|
1608
|
+
);
|
|
1609
|
+
|
|
1610
|
+
if (typeof bridgeResult === "object" && bridgeResult !== null && (bridgeResult as { ok?: unknown }).ok === false) {
|
|
1611
|
+
throw new Error(`FULU BambuNetwork bridge method ${bridgeMethod} failed: ${String((bridgeResult as { error?: unknown }).error || "unknown bridge error")}`);
|
|
1612
|
+
}
|
|
1613
|
+
if (
|
|
1614
|
+
typeof bridgeResult === "object" &&
|
|
1615
|
+
bridgeResult !== null &&
|
|
1616
|
+
typeof (bridgeResult as { value?: unknown }).value === "number" &&
|
|
1617
|
+
(bridgeResult as { value: number }).value !== 0
|
|
1618
|
+
) {
|
|
1619
|
+
const value = (bridgeResult as { value: number }).value;
|
|
1620
|
+
throw new Error(`FULU BambuNetwork bridge method ${bridgeMethod} returned non-zero result ${value}.`);
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
assertActive();
|
|
1624
|
+
this.bambu.recordCheckedJob(devIp, devId, remoteFileName, requirements);
|
|
1625
|
+
|
|
1626
|
+
return {
|
|
1627
|
+
status: "success",
|
|
1628
|
+
message: `FULU BambuNetwork ${bridgePrintMethod} command for ${threeMfFilename} sent successfully.`,
|
|
1629
|
+
bridgeMethod,
|
|
1630
|
+
bridgeResult,
|
|
1631
|
+
clientJobId,
|
|
1632
|
+
autoSliced,
|
|
1633
|
+
projectName,
|
|
1634
|
+
plateIndex,
|
|
1635
|
+
bridgePlateIndex: plateIndex + 1,
|
|
1636
|
+
useAMS,
|
|
1637
|
+
amsMapping: mapping,
|
|
1638
|
+
params: redactPrintParams(params),
|
|
1639
|
+
};
|
|
1640
|
+
}));
|
|
1641
|
+
}
|
|
1469
1642
|
|
|
1470
|
-
|
|
1471
|
-
|
|
1643
|
+
private async handoff3mfViaBambuConnect(
|
|
1644
|
+
args: Record<string, any>
|
|
1645
|
+
): Promise<Record<string, unknown>> {
|
|
1646
|
+
if (!args?.three_mf_path) {
|
|
1647
|
+
throw new Error("Missing required parameter: three_mf_path");
|
|
1472
1648
|
}
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
)
|
|
1479
|
-
|
|
1480
|
-
|
|
1649
|
+
|
|
1650
|
+
const printModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
1651
|
+
const printBedType = resolveBedType(args?.bed_type as string | undefined);
|
|
1652
|
+
const printNozzle = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
1653
|
+
const printPreset = BAMBU_MODEL_PRESETS[printModel]?.(printNozzle);
|
|
1654
|
+
const plateIndex = args?.plate_index !== undefined ? Number(args.plate_index) : 0;
|
|
1655
|
+
|
|
1656
|
+
if (!Number.isInteger(plateIndex) || plateIndex < 0) {
|
|
1657
|
+
throw new Error("plate_index must be a non-negative integer.");
|
|
1481
1658
|
}
|
|
1482
1659
|
|
|
1660
|
+
const { threeMFPath, autoSliced } = await this.ensurePrintableThreeMFPath(
|
|
1661
|
+
args,
|
|
1662
|
+
printModel,
|
|
1663
|
+
printPreset,
|
|
1664
|
+
printBedType
|
|
1665
|
+
);
|
|
1666
|
+
const threeMfFilename = path.basename(threeMFPath);
|
|
1667
|
+
const projectName = String(
|
|
1668
|
+
args?.project_name || threeMfFilename.replace(/\.3mf$/i, "")
|
|
1669
|
+
);
|
|
1670
|
+
const handoff = await importFileViaBambuConnect({
|
|
1671
|
+
filePath: threeMFPath,
|
|
1672
|
+
name: projectName,
|
|
1673
|
+
version: args?.version !== undefined ? String(args.version) : undefined,
|
|
1674
|
+
});
|
|
1675
|
+
|
|
1483
1676
|
return {
|
|
1484
|
-
|
|
1485
|
-
message: `FULU BambuNetwork ${bridgePrintMethod} command for ${threeMfFilename} sent successfully.`,
|
|
1486
|
-
bridgeMethod,
|
|
1487
|
-
bridgeResult,
|
|
1488
|
-
clientJobId,
|
|
1677
|
+
...handoff,
|
|
1489
1678
|
autoSliced,
|
|
1490
|
-
projectName,
|
|
1491
1679
|
plateIndex,
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
params: redactPrintParams(params),
|
|
1680
|
+
printer_model: printModel,
|
|
1681
|
+
bed_type: printBedType,
|
|
1682
|
+
note: "The printable file was handed to Bambu Connect. Check the selected printer and plate in Bambu Connect before starting; no print command was sent by this MCP route.",
|
|
1496
1683
|
};
|
|
1497
1684
|
}
|
|
1498
1685
|
|
|
1686
|
+
private async print3mfViaBambuNative(
|
|
1687
|
+
args: Record<string, any>, host: string, bambuSerial: string, bambuToken: string,
|
|
1688
|
+
onUpdate?: BambuNativeUpdateCallback, signal?: AbortSignal
|
|
1689
|
+
): Promise<Record<string, unknown>> {
|
|
1690
|
+
assertBambuNativeAvailable();
|
|
1691
|
+
const model = await this.resolveBambuModel(args.bambu_model);
|
|
1692
|
+
if (model !== "x2d") throw new Error("connection_mode=bambu_native is restricted to X2D.");
|
|
1693
|
+
return this.print3mfViaBambuNetwork(
|
|
1694
|
+
{ ...args, bambu_model: model, connection_type: "lan", bambu_network_method: "start_local_print" },
|
|
1695
|
+
host, bambuSerial, bambuToken, { onUpdate, signal }
|
|
1696
|
+
);
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
private async uploadFileViaBambuNative(
|
|
1700
|
+
args: Record<string, any>, host: string, bambuSerial: string, bambuToken: string,
|
|
1701
|
+
onUpdate?: BambuNativeUpdateCallback, signal?: AbortSignal
|
|
1702
|
+
): Promise<Record<string, unknown>> {
|
|
1703
|
+
assertBambuNativeAvailable();
|
|
1704
|
+
if (args.print) throw new Error("Native upload never starts printing; use print_3mf.");
|
|
1705
|
+
const model = await this.resolveBambuModel(args.bambu_model);
|
|
1706
|
+
if (model !== "x2d") throw new Error("connection_mode=bambu_native is restricted to X2D.");
|
|
1707
|
+
const unsupported = ["use_ams", "ams_mapping", "ams_mapping2", "ams_mapping_info", "nozzle_mapping", "nozzles_info",
|
|
1708
|
+
"bed_leveling", "flow_calibration", "vibration_calibration", "layer_inspect", "timelapse"];
|
|
1709
|
+
for (const key of unsupported) {
|
|
1710
|
+
if (args[key] !== undefined) throw new Error(`${key} is not supported for native upload-only requests; use print_3mf for checked print settings.`);
|
|
1711
|
+
}
|
|
1712
|
+
for (const key of ["project_name", "preset_name"]) {
|
|
1713
|
+
if (args[key] !== undefined && (typeof args[key] !== "string" || !args[key].trim())) {
|
|
1714
|
+
throw new Error(`${key} must be a non-empty string.`);
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
const plateIndex = args.plate_index ?? 0;
|
|
1718
|
+
if (!Number.isSafeInteger(plateIndex) || plateIndex < 0) throw new Error("plate_index must be a non-negative integer.");
|
|
1719
|
+
if (path.extname(String(args.file_path)).toLowerCase() !== ".3mf" && plateIndex !== 0) {
|
|
1720
|
+
throw new Error("plate_index is only supported for 3MF uploads.");
|
|
1721
|
+
}
|
|
1722
|
+
const bedType = resolveBedType(args.bed_type ?? "textured_plate");
|
|
1723
|
+
return this.bambu.uploadFile(host, bambuSerial, bambuToken, String(args.file_path), String(args.filename), false, model,
|
|
1724
|
+
async (snapshot, destination, assertActive) => {
|
|
1725
|
+
if (path.extname(snapshot).toLowerCase() === ".3mf") {
|
|
1726
|
+
await inspectPrintFile(snapshot, { model, plateIndex, bedType });
|
|
1727
|
+
}
|
|
1728
|
+
assertActive();
|
|
1729
|
+
await this.bambu.disconnectAll();
|
|
1730
|
+
await uploadWithBambuNative({
|
|
1731
|
+
host, serial: bambuSerial, token: bambuToken, filePath: snapshot,
|
|
1732
|
+
remoteName: destination, projectName: args.project_name ?? path.posix.basename(String(args.filename)),
|
|
1733
|
+
presetName: args.preset_name ?? "checked-upload", plateIndex, bedType, useAMS: false,
|
|
1734
|
+
}, onUpdate, { signal, assertActive });
|
|
1735
|
+
});
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1499
1738
|
private async getResolvedPrinterFilamentInventory(
|
|
1500
1739
|
host: string,
|
|
1501
1740
|
bambuSerial: string,
|
|
@@ -2142,6 +2381,19 @@ class BambuPrinterMCPServer {
|
|
|
2142
2381
|
}
|
|
2143
2382
|
}
|
|
2144
2383
|
},
|
|
2384
|
+
{
|
|
2385
|
+
name: "bambu_connect_import_file",
|
|
2386
|
+
description: "Open an existing G-code or sliced 3MF file in the signed-in Bambu Connect app through its official URL scheme. This is a cloud-mode handoff and does not start printing by itself.",
|
|
2387
|
+
inputSchema: {
|
|
2388
|
+
type: "object",
|
|
2389
|
+
properties: {
|
|
2390
|
+
file_path: { type: "string", description: "Readable local path to a .gcode, .3mf, or .gcode.3mf file" },
|
|
2391
|
+
name: { type: "string", description: "Optional project name shown by Bambu Connect" },
|
|
2392
|
+
version: { type: "string", description: "Optional integration version sent to Bambu Connect (default: 1.0.0)" }
|
|
2393
|
+
},
|
|
2394
|
+
required: ["file_path"]
|
|
2395
|
+
}
|
|
2396
|
+
},
|
|
2145
2397
|
{
|
|
2146
2398
|
name: "bambu_network_bridge_status",
|
|
2147
2399
|
description: "Inspect or probe the FULU OrcaSlicer-bambulab BambuNetwork bridge runtime used for cloud and restored BambuNetwork printing.",
|
|
@@ -2200,7 +2452,7 @@ class BambuPrinterMCPServer {
|
|
|
2200
2452
|
plate_index: { type: "number", description: "Zero-based plate index to print from the sliced 3MF; converted to FULU's one-based PrintParams plate_index." },
|
|
2201
2453
|
project_name: { type: "string", description: "Optional project name sent in FULU PrintParams; defaults to the 3MF filename without extension." },
|
|
2202
2454
|
preset_name: { type: "string", description: "Optional preset name sent in FULU PrintParams; defaults to project plus one-based plate index." },
|
|
2203
|
-
task_name: { type: "string", description: "Optional
|
|
2455
|
+
task_name: { type: "string", description: "Optional project label when project_name is omitted. The submitted task name uses a unique inspected-job identity for safe resume." },
|
|
2204
2456
|
config_filename: { type: "string", description: "Optional config 3MF path for cloud print; defaults to the same 3MF path." },
|
|
2205
2457
|
bridge_command: { type: "string", description: "Override command for the FULU bridge host or macOS/WSL wrapper; defaults to BAMBU_NETWORK_BRIDGE_COMMAND. Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1." },
|
|
2206
2458
|
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." },
|
|
@@ -2211,6 +2463,7 @@ class BambuPrinterMCPServer {
|
|
|
2211
2463
|
slicer_path: { type: "string", description: "Path to the slicer executable for auto-slicing; defaults to value from env or a platform default. Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1." },
|
|
2212
2464
|
slicer_profile: { type: "string", description: "Path to an optional slicer profile/config file for auto-slicing." },
|
|
2213
2465
|
nozzle_diameter: { type: "string", description: "Nozzle diameter in mm for auto-slicing (default: 0.4)." },
|
|
2466
|
+
nozzle_diameters: { type: "array", minItems: 1, maxItems: 2, items: { type: "number", enum: [0.2, 0.4, 0.6, 0.8] }, description: "Complete per-nozzle diameters for pre-sliced jobs, e.g. [0.4, 0.6]. Omit to verify file metadata against every reported nozzle; cannot combine with nozzle_diameter." },
|
|
2214
2467
|
use_ams: { type: "boolean", description: "Whether to use the AMS; defaults to auto-detect from the 3MF mapping." },
|
|
2215
2468
|
ams_mapping: { type: "array", description: "AMS slot mapping array used by both local MCP printing and FULU PrintParams.", items: { type: "number" } },
|
|
2216
2469
|
ams_slots: { type: "array", description: "Per-used-filament AMS slot list, matching the local LAN print path.", items: { type: "number" } },
|
|
@@ -2236,7 +2489,7 @@ class BambuPrinterMCPServer {
|
|
|
2236
2489
|
},
|
|
2237
2490
|
{
|
|
2238
2491
|
name: "camera_snapshot",
|
|
2239
|
-
description: "Capture a single JPEG frame from the printer's chamber camera. A1/P1 use TCP-on-6000; X1/P2S/H2 use RTSP via ffmpeg. Returns JPEG as base64; pass save_path to also write the bytes to disk.",
|
|
2492
|
+
description: "Capture a single JPEG frame from the printer's chamber camera. A1/P1 use TCP-on-6000; X1/P2S/H2/X2D use RTSP via ffmpeg. Returns JPEG as base64; pass save_path to also write the bytes to disk.",
|
|
2240
2493
|
inputSchema: {
|
|
2241
2494
|
type: "object",
|
|
2242
2495
|
properties: {
|
|
@@ -2244,7 +2497,7 @@ class BambuPrinterMCPServer {
|
|
|
2244
2497
|
timeout_ms: { type: "number", description: "Max ms to wait for a full frame (default 8000). Camera may take a few seconds on cold start." },
|
|
2245
2498
|
bambu_model: { type: "string", description: "Printer model. Used to route to the correct protocol or fail fast on unsupported models. Defaults to BAMBU_MODEL." },
|
|
2246
2499
|
experimental: { type: "boolean", description: "Deprecated and ignored. Earlier this flag let callers probe H2 series via the A1/P1 TCP-on-6000 path; live testing on an H2S confirmed H2 uses RTSP instead, so the flag has no effect now." },
|
|
2247
|
-
ffmpeg_path: { type: "string", description: "Override path to the ffmpeg binary used by the RTSP path. Defaults to FFMPEG_PATH or ffmpeg via $PATH. Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1. Required only for the RTSP transport (X1, P2S, H2 series)." },
|
|
2500
|
+
ffmpeg_path: { type: "string", description: "Override path to the ffmpeg binary used by the RTSP path. Defaults to FFMPEG_PATH or ffmpeg via $PATH. Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1. Required only for the RTSP transport (X1, P2S, H2 series, X2D)." },
|
|
2248
2501
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2249
2502
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
2250
2503
|
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
@@ -2274,13 +2527,14 @@ class BambuPrinterMCPServer {
|
|
|
2274
2527
|
},
|
|
2275
2528
|
{
|
|
2276
2529
|
name: "upload_gcode",
|
|
2277
|
-
description: "
|
|
2530
|
+
description: "Inspect a G-code file and upload a uniquely named copy without overwriting existing printer files.",
|
|
2278
2531
|
inputSchema: {
|
|
2279
2532
|
type: "object",
|
|
2280
2533
|
properties: {
|
|
2281
2534
|
filename: { type: "string", description: "Name for the file on the printer" },
|
|
2282
2535
|
gcode: { type: "string", description: "G-code content to upload, or a readable local .gcode path. Required unless gcode_path is provided. For large files, prefer gcode_path." },
|
|
2283
2536
|
gcode_path: { type: "string", description: "Local path to a .gcode file to upload. Required unless gcode is provided. This avoids sending large G-code bodies through the MCP request." },
|
|
2537
|
+
bambu_model: { type: "string", enum: [...VALID_BAMBU_MODELS], description: "Required printer model for inspecting the uploaded G-code, even when not starting it." },
|
|
2284
2538
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2285
2539
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
2286
2540
|
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
@@ -2297,11 +2551,20 @@ class BambuPrinterMCPServer {
|
|
|
2297
2551
|
file_path: { type: "string", description: "Local path to the file to upload" },
|
|
2298
2552
|
filename: { type: "string", description: "Name for the file on the printer" },
|
|
2299
2553
|
print: { type: "boolean", description: "Start printing after upload (default: false)" },
|
|
2554
|
+
connection_mode: {
|
|
2555
|
+
type: "string",
|
|
2556
|
+
enum: ["lan_mqtt_ftps", "bambu_native"],
|
|
2557
|
+
description: "Upload transport; bambu_native uses the installed Bambu networking plug-in for X2D and never starts printing."
|
|
2558
|
+
},
|
|
2300
2559
|
bambu_model: {
|
|
2301
2560
|
type: "string",
|
|
2302
2561
|
enum: [...VALID_BAMBU_MODELS],
|
|
2303
|
-
description: "Required
|
|
2562
|
+
description: "Required for every printable .gcode or .3mf upload, including upload-only operations."
|
|
2304
2563
|
},
|
|
2564
|
+
project_name: { type: "string", description: "Optional project name passed to the native X2D uploader; defaults to filename." },
|
|
2565
|
+
preset_name: { type: "string", description: "Optional printer preset name passed to the native X2D uploader." },
|
|
2566
|
+
bed_type: { type: "string", enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate", "supertack_plate"], description: "Bed plate type for the native X2D uploader (default: textured_plate)." },
|
|
2567
|
+
plate_index: { type: "integer", minimum: 0, description: "Zero-based plate index passed to the native X2D uploader (default: 0)." },
|
|
2305
2568
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2306
2569
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
2307
2570
|
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
@@ -2383,6 +2646,22 @@ class BambuPrinterMCPServer {
|
|
|
2383
2646
|
}
|
|
2384
2647
|
}
|
|
2385
2648
|
},
|
|
2649
|
+
{
|
|
2650
|
+
name: "x2d_native_control",
|
|
2651
|
+
description: "Read calibration results or update X2D AMS metadata/calibration selection through the installed plug-in. Raw motion, heating, task control, and safety-setting changes are rejected.",
|
|
2652
|
+
inputSchema: {
|
|
2653
|
+
type: "object",
|
|
2654
|
+
properties: {
|
|
2655
|
+
message_json: { type: "string", description: "JSON print envelope for ams_filament_setting, extrusion_cali_sel, extrusion_cali_get, extrusion_cali_get_result, or flowrate_get_result. A sequence_id is required; extra fields are rejected." },
|
|
2656
|
+
qos: { type: "number", description: "MQTT QoS used by Bambu Studio (0 or 1; default 0)." },
|
|
2657
|
+
flag: { type: "number", description: "Bambu networking plug-in command flag (0 or 1; default 0)." },
|
|
2658
|
+
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2659
|
+
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
2660
|
+
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
2661
|
+
},
|
|
2662
|
+
required: ["message_json"]
|
|
2663
|
+
}
|
|
2664
|
+
},
|
|
2386
2665
|
{
|
|
2387
2666
|
name: "clear_hms_errors",
|
|
2388
2667
|
description: "Clear HMS or print error state on the Bambu Lab printer using the clean_print_error MQTT command.",
|
|
@@ -2433,11 +2712,11 @@ class BambuPrinterMCPServer {
|
|
|
2433
2712
|
},
|
|
2434
2713
|
{
|
|
2435
2714
|
name: "reread_ams_rfid",
|
|
2436
|
-
description: "Trigger a Bambu AMS RFID re-read for one AMS slot. This can move AMS filament; use only when the printer is idle and unloaded.",
|
|
2715
|
+
description: "Trigger a Bambu AMS RFID re-read for one AMS slot. X2D on macOS uses the native official plug-in route. This can move AMS filament; use only when the printer is idle and unloaded.",
|
|
2437
2716
|
inputSchema: {
|
|
2438
2717
|
type: "object",
|
|
2439
2718
|
properties: {
|
|
2440
|
-
ams_id: { type: "number", description: "AMS unit index from 0 to 3" },
|
|
2719
|
+
ams_id: { type: "number", description: "AMS unit index from 0 to 3, or 128 for an X2D AMS-HT." },
|
|
2441
2720
|
slot_id: { type: "number", description: "Slot index within that AMS, from 0 to 3" },
|
|
2442
2721
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2443
2722
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
@@ -2448,12 +2727,15 @@ class BambuPrinterMCPServer {
|
|
|
2448
2727
|
},
|
|
2449
2728
|
{
|
|
2450
2729
|
name: "set_temperature",
|
|
2451
|
-
description: "Set
|
|
2730
|
+
description: "Set a checked bed or nozzle temperature. Positive heating requires matching fresh printer telemetry; nozzle heating also requires declared material. Zero turns the heater off.",
|
|
2452
2731
|
inputSchema: {
|
|
2453
2732
|
type: "object",
|
|
2454
2733
|
properties: {
|
|
2455
2734
|
component: { type: "string", description: "Component to heat: bed, nozzle, or extruder" },
|
|
2456
|
-
temperature: { type: "number", description: "
|
|
2735
|
+
temperature: { type: "number", minimum: 0, description: "Finite target temperature in °C, checked against model/component and declared material limits" },
|
|
2736
|
+
bambu_model: { type: "string", enum: [...VALID_BAMBU_MODELS], description: "Printer model, required for positive heating unless configured in the environment" },
|
|
2737
|
+
material: { type: "string", description: "Loaded material, for example PLA or PETG. Required for positive nozzle heating, including non-RFID spools." },
|
|
2738
|
+
nozzle_diameter: { type: "number", enum: [0.2, 0.4, 0.6, 0.8], description: "Installed nozzle diameter to compare with printer-reported configuration (default 0.4)" },
|
|
2457
2739
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2458
2740
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
2459
2741
|
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
@@ -2463,15 +2745,16 @@ class BambuPrinterMCPServer {
|
|
|
2463
2745
|
},
|
|
2464
2746
|
{
|
|
2465
2747
|
name: "set_fan_speed",
|
|
2466
|
-
description: "Set a Bambu printer fan speed percentage
|
|
2748
|
+
description: "Set a Bambu printer fan speed percentage. X2D on macOS uses the installed Bambu networking plug-in without UI automation; other models use MQTT.",
|
|
2467
2749
|
inputSchema: {
|
|
2468
2750
|
type: "object",
|
|
2469
2751
|
properties: {
|
|
2470
2752
|
fan: {
|
|
2471
2753
|
type: "string",
|
|
2472
|
-
description: "Fan to control: part, auxiliary, chamber, 1, 2, or
|
|
2754
|
+
description: "Fan to control: part, auxiliary, right_auxiliary, chamber, 1, 2, 3, or 10"
|
|
2473
2755
|
},
|
|
2474
2756
|
speed: { type: "number", description: "Fan speed percentage from 0 to 100" },
|
|
2757
|
+
confirm_during_print: { type: "boolean", description: "Required for X2D fan changes while a print is running." },
|
|
2475
2758
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2476
2759
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
2477
2760
|
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
@@ -2521,7 +2804,7 @@ class BambuPrinterMCPServer {
|
|
|
2521
2804
|
},
|
|
2522
2805
|
{
|
|
2523
2806
|
name: "set_ams_drying",
|
|
2524
|
-
description: "Start or stop the AMS filament drying cycle.
|
|
2807
|
+
description: "Start or stop the AMS filament drying cycle. X2D on macOS uses the native official plug-in route and supports AMS-HT id 128.",
|
|
2525
2808
|
inputSchema: {
|
|
2526
2809
|
type: "object",
|
|
2527
2810
|
properties: {
|
|
@@ -2532,7 +2815,7 @@ class BambuPrinterMCPServer {
|
|
|
2532
2815
|
},
|
|
2533
2816
|
ams_id: {
|
|
2534
2817
|
type: "number",
|
|
2535
|
-
description: "AMS unit index from 0 to 3"
|
|
2818
|
+
description: "AMS unit index from 0 to 3, or 128 for an X2D AMS-HT."
|
|
2536
2819
|
},
|
|
2537
2820
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2538
2821
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
@@ -2555,8 +2838,8 @@ class BambuPrinterMCPServer {
|
|
|
2555
2838
|
},
|
|
2556
2839
|
connection_mode: {
|
|
2557
2840
|
type: "string",
|
|
2558
|
-
enum: ["lan_mqtt_ftps", "bambu_network"],
|
|
2559
|
-
description: "Print path to
|
|
2841
|
+
enum: ["lan_mqtt_ftps", "bambu_network", "bambu_native", "bambu_connect"],
|
|
2842
|
+
description: "Print path: bambu_connect hands a printable file to the signed-in Bambu Connect app for cloud-mode review; X2D otherwise defaults to bambu_native (Bambu Studio's installed local networking plug-in and emmc tunnel); lan_mqtt_ftps is the legacy direct MQTT/FTPS path; bambu_network uses the FULU bridge."
|
|
2560
2843
|
},
|
|
2561
2844
|
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." },
|
|
2562
2845
|
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." },
|
|
@@ -2604,7 +2887,8 @@ class BambuPrinterMCPServer {
|
|
|
2604
2887
|
template_3mf_path: { type: "string", description: "Optional template 3MF whose embedded Bambu slicer settings should be reused when auto-slicing this print job." },
|
|
2605
2888
|
template_name: { type: "string", description: "Optional named template from the local registry. Resolves to template_3mf_path automatically." },
|
|
2606
2889
|
template_dir: { type: "string", description: "Optional template directory override when resolving template_name." },
|
|
2607
|
-
|
|
2890
|
+
nozzle_diameter: { type: "string", description: "Nozzle diameter in mm for auto-slicing (default: 0.4)" },
|
|
2891
|
+
nozzle_diameters: { type: "array", minItems: 1, maxItems: 2, items: { type: "number", enum: [0.2, 0.4, 0.6, 0.8] }, description: "Complete per-nozzle diameters for a pre-sliced job, e.g. [0.4, 0.6]. Omit to verify its declared diameters against live telemetry; cannot combine with nozzle_diameter." }
|
|
2608
2892
|
},
|
|
2609
2893
|
required: ["three_mf_path", "bambu_model"]
|
|
2610
2894
|
}
|
|
@@ -2733,10 +3017,41 @@ class BambuPrinterMCPServer {
|
|
|
2733
3017
|
|
|
2734
3018
|
this.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
2735
3019
|
const { name, arguments: args } = request.params;
|
|
3020
|
+
const sendNativeCommand = (
|
|
3021
|
+
options: Parameters<typeof sendCommandWithBambuNative>[0],
|
|
3022
|
+
execution: Parameters<typeof sendCommandWithBambuNative>[1] = {}
|
|
3023
|
+
) => sendCommandWithBambuNative(options, { signal: extra.signal, ...execution });
|
|
3024
|
+
|
|
3025
|
+
const progressToken = extra._meta?.progressToken;
|
|
3026
|
+
let progressNotifications = Promise.resolve();
|
|
3027
|
+
const reportNativeUpdate: BambuNativeUpdateCallback | undefined = progressToken === undefined
|
|
3028
|
+
? undefined
|
|
3029
|
+
: (line) => {
|
|
3030
|
+
const match = /^native_update status=(-?\d+) code=(-?\d+) msg=(.*)$/.exec(line);
|
|
3031
|
+
if (!match) return;
|
|
3032
|
+
|
|
3033
|
+
const stage = Number(match[1]);
|
|
3034
|
+
const code = Number(match[2]);
|
|
3035
|
+
const message = match[3];
|
|
3036
|
+
progressNotifications = progressNotifications
|
|
3037
|
+
.then(() => extra.sendNotification({
|
|
3038
|
+
method: "notifications/progress",
|
|
3039
|
+
params: {
|
|
3040
|
+
progressToken,
|
|
3041
|
+
progress: code,
|
|
3042
|
+
total: 100,
|
|
3043
|
+
message: `native_update status=${stage} code=${code} msg=${message}`,
|
|
3044
|
+
},
|
|
3045
|
+
}))
|
|
3046
|
+
.catch(() => {});
|
|
3047
|
+
};
|
|
2736
3048
|
|
|
2737
3049
|
const host = String(args?.host || DEFAULT_HOST);
|
|
2738
3050
|
const bambuSerial = String(args?.bambu_serial || DEFAULT_BAMBU_SERIAL);
|
|
2739
3051
|
const bambuToken = String(args?.bambu_token || DEFAULT_BAMBU_TOKEN);
|
|
3052
|
+
const nativeControls = process.platform === "darwin" &&
|
|
3053
|
+
(bambuSerial.trim().toUpperCase().startsWith("20P") ||
|
|
3054
|
+
(DEFAULT_BAMBU_MODEL === "x2d" && bambuSerial === DEFAULT_BAMBU_SERIAL));
|
|
2740
3055
|
const requestedTemplateDir =
|
|
2741
3056
|
typeof args?.template_dir === "string" && args.template_dir.trim().length > 0
|
|
2742
3057
|
? String(args.template_dir)
|
|
@@ -2828,6 +3143,17 @@ class BambuPrinterMCPServer {
|
|
|
2828
3143
|
result = await this.bambu.getFiles(host, bambuSerial, bambuToken);
|
|
2829
3144
|
break;
|
|
2830
3145
|
|
|
3146
|
+
case "bambu_connect_import_file":
|
|
3147
|
+
if (!args?.file_path) {
|
|
3148
|
+
throw new Error("Missing required parameter: file_path");
|
|
3149
|
+
}
|
|
3150
|
+
result = await importFileViaBambuConnect({
|
|
3151
|
+
filePath: String(args.file_path),
|
|
3152
|
+
name: args?.name !== undefined ? String(args.name) : undefined,
|
|
3153
|
+
version: args?.version !== undefined ? String(args.version) : undefined,
|
|
3154
|
+
});
|
|
3155
|
+
break;
|
|
3156
|
+
|
|
2831
3157
|
case "bambu_network_bridge_status": {
|
|
2832
3158
|
const bridgeArgs = args as Record<string, any> | undefined;
|
|
2833
3159
|
const options = this.bridgeOptionsFromArgs(
|
|
@@ -2852,6 +3178,10 @@ class BambuPrinterMCPServer {
|
|
|
2852
3178
|
throw new Error("Missing required parameter: method");
|
|
2853
3179
|
}
|
|
2854
3180
|
const bridgeArgs = args as Record<string, any>;
|
|
3181
|
+
const readMethods = new Set(["bridge.handshake", "net.is_user_login", "net.get_user_id", "net.get_user_name", "net.get_user_avatar", "net.get_user_nickanme"]);
|
|
3182
|
+
if (!readMethods.has(String(bridgeArgs.method))) {
|
|
3183
|
+
throw new Error("Raw bridge mutations and unknown methods are disabled. Use print_3mf_bambu_network for verified printing or a dedicated printer tool.");
|
|
3184
|
+
}
|
|
2855
3185
|
const payload = bridgeArgs.payload && typeof bridgeArgs.payload === "object"
|
|
2856
3186
|
? bridgeArgs.payload as Record<string, unknown>
|
|
2857
3187
|
: {};
|
|
@@ -2902,8 +3232,9 @@ class BambuPrinterMCPServer {
|
|
|
2902
3232
|
}
|
|
2903
3233
|
const uploadSource = resolveUploadGcodeSource(args as Record<string, unknown>);
|
|
2904
3234
|
try {
|
|
3235
|
+
const uploadModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
2905
3236
|
result = await this.bambu.uploadFile(
|
|
2906
|
-
host, bambuSerial, bambuToken, uploadSource.filePath, String(args.filename), false
|
|
3237
|
+
host, bambuSerial, bambuToken, uploadSource.filePath, String(args.filename), false, uploadModel
|
|
2907
3238
|
);
|
|
2908
3239
|
} finally {
|
|
2909
3240
|
if (uploadSource.cleanupDir) {
|
|
@@ -2917,14 +3248,21 @@ class BambuPrinterMCPServer {
|
|
|
2917
3248
|
if (!args?.file_path || !args?.filename) {
|
|
2918
3249
|
throw new Error("Missing required parameters: file_path and filename");
|
|
2919
3250
|
}
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
3251
|
+
if (String(args?.connection_mode || "").trim().toLowerCase() === "bambu_native") {
|
|
3252
|
+
result = await this.uploadFileViaBambuNative(
|
|
3253
|
+
args as Record<string, any>, host, bambuSerial, bambuToken, reportNativeUpdate, extra.signal
|
|
3254
|
+
);
|
|
3255
|
+
} else {
|
|
3256
|
+
const print = Boolean(args.print ?? false);
|
|
3257
|
+
const printableUpload = /\.(?:gcode|3mf)$/i.test(String(args.file_path)) || /\.(?:gcode|3mf)$/i.test(String(args.filename));
|
|
3258
|
+
const uploadModel = print || printableUpload ? await this.resolveBambuModel(args?.bambu_model as string | undefined) : undefined;
|
|
3259
|
+
if (print) assertDirectPrintSupported(uploadModel, bambuSerial);
|
|
3260
|
+
result = await this.bambu.uploadFile(
|
|
3261
|
+
host, bambuSerial, bambuToken,
|
|
3262
|
+
String(args.file_path), String(args.filename),
|
|
3263
|
+
print, uploadModel
|
|
3264
|
+
);
|
|
3265
|
+
}
|
|
2928
3266
|
break;
|
|
2929
3267
|
}
|
|
2930
3268
|
|
|
@@ -2940,107 +3278,309 @@ class BambuPrinterMCPServer {
|
|
|
2940
3278
|
}
|
|
2941
3279
|
|
|
2942
3280
|
case "cancel_print":
|
|
2943
|
-
result = await this.bambu.cancelJob(host, bambuSerial, bambuToken
|
|
3281
|
+
result = await this.bambu.cancelJob(host, bambuSerial, bambuToken,
|
|
3282
|
+
nativeControls
|
|
3283
|
+
? () => sendNativeCommand({ host, serial: bambuSerial, token: bambuToken,
|
|
3284
|
+
messageJson: JSON.stringify({ print: { command: "stop", param: "", sequence_id: String(Date.now()) } }), qos: 1 })
|
|
3285
|
+
: undefined);
|
|
2944
3286
|
break;
|
|
2945
3287
|
|
|
2946
3288
|
case "pause_print":
|
|
2947
|
-
result = await this.bambu.pauseJob(host, bambuSerial, bambuToken
|
|
3289
|
+
result = await this.bambu.pauseJob(host, bambuSerial, bambuToken,
|
|
3290
|
+
nativeControls
|
|
3291
|
+
? () => sendNativeCommand({ host, serial: bambuSerial, token: bambuToken,
|
|
3292
|
+
messageJson: JSON.stringify({ print: { command: "pause", param: "", sequence_id: String(Date.now()) } }), qos: 1 })
|
|
3293
|
+
: undefined);
|
|
2948
3294
|
break;
|
|
2949
3295
|
|
|
2950
3296
|
case "resume_print":
|
|
2951
|
-
result = await this.bambu.resumeJob(host, bambuSerial, bambuToken
|
|
3297
|
+
result = await this.bambu.resumeJob(host, bambuSerial, bambuToken,
|
|
3298
|
+
nativeControls
|
|
3299
|
+
? (assertActive, beforeDispatch) => sendNativeCommand({ host, serial: bambuSerial, token: bambuToken,
|
|
3300
|
+
messageJson: JSON.stringify({ print: { command: "resume", param: "", sequence_id: String(Date.now()) } }), qos: 1 }, { signal: extra.signal, assertActive, beforeDispatch })
|
|
3301
|
+
: undefined);
|
|
3302
|
+
break;
|
|
3303
|
+
|
|
3304
|
+
case "x2d_native_control": {
|
|
3305
|
+
if (!args?.message_json) {
|
|
3306
|
+
throw new Error("Missing required parameter: message_json");
|
|
3307
|
+
}
|
|
3308
|
+
if (!nativeControls) {
|
|
3309
|
+
throw new Error("x2d_native_control is restricted to a selected X2D printer on macOS.");
|
|
3310
|
+
}
|
|
3311
|
+
const messageJson = validateBambuNativeMetadata(String(args.message_json));
|
|
3312
|
+
result = await sendNativeCommand({
|
|
3313
|
+
host,
|
|
3314
|
+
serial: bambuSerial,
|
|
3315
|
+
token: bambuToken,
|
|
3316
|
+
messageJson,
|
|
3317
|
+
qos: args?.qos === undefined ? 0 : Number(args.qos),
|
|
3318
|
+
flag: args?.flag === undefined ? 0 : Number(args.flag),
|
|
3319
|
+
});
|
|
2952
3320
|
break;
|
|
3321
|
+
}
|
|
2953
3322
|
|
|
2954
3323
|
case "clear_hms_errors":
|
|
2955
|
-
result = await this.bambu.clearHmsErrors(host, bambuSerial, bambuToken
|
|
3324
|
+
result = await this.bambu.clearHmsErrors(host, bambuSerial, bambuToken,
|
|
3325
|
+
nativeControls
|
|
3326
|
+
? (assertActive, beforeDispatch) => sendNativeCommand({ host, serial: bambuSerial, token: bambuToken,
|
|
3327
|
+
messageJson: JSON.stringify({ print: { command: "clean_print_error", param: "", sequence_id: String(Date.now()) } }), qos: 1 }, { signal: extra.signal, assertActive, beforeDispatch })
|
|
3328
|
+
: undefined);
|
|
2956
3329
|
break;
|
|
2957
3330
|
|
|
2958
3331
|
case "set_print_speed":
|
|
2959
3332
|
if (!args?.mode) {
|
|
2960
3333
|
throw new Error("Missing required parameter: mode");
|
|
2961
3334
|
}
|
|
2962
|
-
|
|
3335
|
+
if (nativeControls) {
|
|
3336
|
+
const normalized = String(args.mode).trim().toLowerCase();
|
|
3337
|
+
const mode = normalized === "silent" ? 1
|
|
3338
|
+
: normalized === "standard" ? 2
|
|
3339
|
+
: normalized === "sport" ? 3
|
|
3340
|
+
: normalized === "ludicrous" ? 4
|
|
3341
|
+
: Number(normalized);
|
|
3342
|
+
if (!Number.isInteger(mode) || mode < 1 || mode > 4) {
|
|
3343
|
+
throw new Error("Print speed mode must be one of: silent, standard, sport, ludicrous, 1, 2, 3, 4.");
|
|
3344
|
+
}
|
|
3345
|
+
result = await sendNativeCommand({
|
|
3346
|
+
host,
|
|
3347
|
+
serial: bambuSerial,
|
|
3348
|
+
token: bambuToken,
|
|
3349
|
+
messageJson: JSON.stringify({
|
|
3350
|
+
print: { command: "print_speed", param: String(mode), sequence_id: String(Date.now()) },
|
|
3351
|
+
}),
|
|
3352
|
+
});
|
|
3353
|
+
} else {
|
|
3354
|
+
result = await this.bambu.setPrintSpeed(host, bambuSerial, bambuToken, String(args.mode));
|
|
3355
|
+
}
|
|
2963
3356
|
break;
|
|
2964
3357
|
|
|
2965
3358
|
case "set_airduct_mode":
|
|
2966
3359
|
if (!args?.mode) {
|
|
2967
3360
|
throw new Error("Missing required parameter: mode");
|
|
2968
3361
|
}
|
|
2969
|
-
|
|
3362
|
+
if (nativeControls) {
|
|
3363
|
+
const mode = String(args.mode).trim().toLowerCase();
|
|
3364
|
+
if (mode !== "cooling" && mode !== "heating") {
|
|
3365
|
+
throw new Error("Airduct mode must be one of: cooling, heating.");
|
|
3366
|
+
}
|
|
3367
|
+
result = await sendNativeCommand({
|
|
3368
|
+
host,
|
|
3369
|
+
serial: bambuSerial,
|
|
3370
|
+
token: bambuToken,
|
|
3371
|
+
messageJson: JSON.stringify({
|
|
3372
|
+
print: {
|
|
3373
|
+
command: "set_airduct",
|
|
3374
|
+
modeId: mode === "cooling" ? 0 : 1,
|
|
3375
|
+
submode: -1,
|
|
3376
|
+
sequence_id: String(Date.now()),
|
|
3377
|
+
},
|
|
3378
|
+
}),
|
|
3379
|
+
});
|
|
3380
|
+
} else {
|
|
3381
|
+
result = await this.bambu.setAirductMode(host, bambuSerial, bambuToken, String(args.mode));
|
|
3382
|
+
}
|
|
2970
3383
|
break;
|
|
2971
3384
|
|
|
2972
3385
|
case "reread_ams_rfid":
|
|
2973
3386
|
if (args?.ams_id === undefined || args?.slot_id === undefined) {
|
|
2974
3387
|
throw new Error("Missing required parameters: ams_id and slot_id");
|
|
2975
3388
|
}
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
3389
|
+
if (nativeControls) {
|
|
3390
|
+
const amsId = Number(args.ams_id);
|
|
3391
|
+
const slotId = Number(args.slot_id);
|
|
3392
|
+
if ((!Number.isInteger(amsId) || (amsId < 0 || amsId > 3)) && amsId !== 128) {
|
|
3393
|
+
throw new Error("X2D ams_id must be 0 to 3, or 128 for AMS-HT.");
|
|
3394
|
+
}
|
|
3395
|
+
if (!Number.isInteger(slotId) || slotId < 0 || slotId > 3 || (amsId === 128 && slotId !== 0)) {
|
|
3396
|
+
throw new Error("slot_id must be an integer from 0 to 3; AMS-HT supports only slot 0.");
|
|
3397
|
+
}
|
|
3398
|
+
result = await sendNativeCommand({
|
|
3399
|
+
host,
|
|
3400
|
+
serial: bambuSerial,
|
|
3401
|
+
token: bambuToken,
|
|
3402
|
+
messageJson: JSON.stringify({
|
|
3403
|
+
print: {
|
|
3404
|
+
command: "ams_get_rfid",
|
|
3405
|
+
sequence_id: String(Date.now()),
|
|
3406
|
+
ams_id: amsId,
|
|
3407
|
+
slot_id: slotId,
|
|
3408
|
+
},
|
|
3409
|
+
}),
|
|
3410
|
+
});
|
|
3411
|
+
} else {
|
|
3412
|
+
result = await this.bambu.rereadAmsRfid(
|
|
3413
|
+
host,
|
|
3414
|
+
bambuSerial,
|
|
3415
|
+
bambuToken,
|
|
3416
|
+
Number(args.ams_id),
|
|
3417
|
+
Number(args.slot_id)
|
|
3418
|
+
);
|
|
3419
|
+
}
|
|
2983
3420
|
break;
|
|
2984
3421
|
|
|
2985
|
-
case "set_temperature":
|
|
3422
|
+
case "set_temperature": {
|
|
2986
3423
|
if (!args?.component || args?.temperature === undefined) {
|
|
2987
3424
|
throw new Error("Missing required parameters: component and temperature");
|
|
2988
3425
|
}
|
|
3426
|
+
if (typeof args.temperature !== "number" || !Number.isFinite(args.temperature) || args.temperature < 0) {
|
|
3427
|
+
throw new Error("Temperature must be a finite, non-negative number in °C.");
|
|
3428
|
+
}
|
|
3429
|
+
const heaterModel = args.temperature === 0
|
|
3430
|
+
? String(args.bambu_model || DEFAULT_BAMBU_MODEL).trim().toLowerCase()
|
|
3431
|
+
: await this.resolveBambuModel(args.bambu_model as string | undefined);
|
|
2989
3432
|
result = await this.bambu.setTemperature(
|
|
2990
3433
|
host, bambuSerial, bambuToken,
|
|
2991
|
-
String(args.component),
|
|
3434
|
+
String(args.component), args.temperature,
|
|
3435
|
+
heaterModel || undefined,
|
|
3436
|
+
args?.material !== undefined ? String(args.material) : undefined,
|
|
3437
|
+
Number(args?.nozzle_diameter ?? DEFAULT_NOZZLE_DIAMETER),
|
|
3438
|
+
(args.temperature === 0 ? nativeControls : heaterModel === "x2d" && process.platform === "darwin")
|
|
3439
|
+
? (heater, target, assertActive, beforeDispatch) => sendNativeCommand({ host, serial: bambuSerial, token: bambuToken,
|
|
3440
|
+
messageJson: buildBambuNativeTemperatureCommand(heater, target).messageJson }, { signal: extra.signal, assertActive, beforeDispatch })
|
|
3441
|
+
: undefined
|
|
2992
3442
|
);
|
|
2993
3443
|
break;
|
|
3444
|
+
}
|
|
2994
3445
|
|
|
2995
3446
|
case "set_fan_speed":
|
|
2996
3447
|
if (!args?.fan || args?.speed === undefined) {
|
|
2997
3448
|
throw new Error("Missing required parameters: fan and speed");
|
|
2998
3449
|
}
|
|
2999
|
-
|
|
3000
|
-
host,
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3450
|
+
if (nativeControls) {
|
|
3451
|
+
const status = await this.bambu.getStatus(host, bambuSerial, bambuToken);
|
|
3452
|
+
const printerState = String(status?.raw?.gcode_state ?? status?.raw?.print?.gcode_state ?? status?.gcode_state ?? status?.status ?? "").toUpperCase();
|
|
3453
|
+
if (printerState === "RUNNING" && args?.confirm_during_print !== true) {
|
|
3454
|
+
throw new Error("The X2D is currently printing. Re-submit with confirm_during_print=true after explicit user confirmation.");
|
|
3455
|
+
}
|
|
3456
|
+
const fanCommand = buildBambuNativeFanCommand(String(args.fan), Number(args.speed));
|
|
3457
|
+
result = {
|
|
3458
|
+
...await sendNativeCommand({
|
|
3459
|
+
host,
|
|
3460
|
+
serial: bambuSerial,
|
|
3461
|
+
token: bambuToken,
|
|
3462
|
+
messageJson: fanCommand.messageJson,
|
|
3463
|
+
}),
|
|
3464
|
+
fan: fanCommand.fan,
|
|
3465
|
+
fan_index: fanCommand.fanIndex,
|
|
3466
|
+
requested_speed: fanCommand.requestedSpeed,
|
|
3467
|
+
speed: fanCommand.speed,
|
|
3468
|
+
};
|
|
3469
|
+
} else {
|
|
3470
|
+
result = await this.bambu.setFanSpeed(
|
|
3471
|
+
host,
|
|
3472
|
+
bambuSerial,
|
|
3473
|
+
bambuToken,
|
|
3474
|
+
String(args.fan),
|
|
3475
|
+
Number(args.speed)
|
|
3476
|
+
);
|
|
3477
|
+
}
|
|
3006
3478
|
break;
|
|
3007
3479
|
|
|
3008
3480
|
case "set_light":
|
|
3009
3481
|
if (!args?.light || !args?.mode) {
|
|
3010
3482
|
throw new Error("Missing required parameters: light and mode");
|
|
3011
3483
|
}
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3484
|
+
if (nativeControls) {
|
|
3485
|
+
const light = String(args.light).trim();
|
|
3486
|
+
const mode = String(args.mode).trim().toLowerCase();
|
|
3487
|
+
if (light !== "chamber_light" && light !== "chamber_light2") {
|
|
3488
|
+
throw new Error("X2D light must be chamber_light or chamber_light2.");
|
|
3489
|
+
}
|
|
3490
|
+
if (!["on", "off", "flashing"].includes(mode)) {
|
|
3491
|
+
throw new Error("Light mode must be one of: on, off, flashing.");
|
|
3492
|
+
}
|
|
3493
|
+
result = await sendNativeCommand({
|
|
3494
|
+
host,
|
|
3495
|
+
serial: bambuSerial,
|
|
3496
|
+
token: bambuToken,
|
|
3497
|
+
messageJson: JSON.stringify({
|
|
3498
|
+
system: {
|
|
3499
|
+
command: "ledctrl",
|
|
3500
|
+
led_node: light,
|
|
3501
|
+
led_mode: mode,
|
|
3502
|
+
led_on_time: 500,
|
|
3503
|
+
led_off_time: 500,
|
|
3504
|
+
loop_times: 0,
|
|
3505
|
+
interval_time: 0,
|
|
3506
|
+
sequence_id: String(Date.now()),
|
|
3507
|
+
},
|
|
3508
|
+
}),
|
|
3509
|
+
});
|
|
3510
|
+
} else {
|
|
3511
|
+
result = await this.bambu.setLight(
|
|
3512
|
+
host,
|
|
3513
|
+
bambuSerial,
|
|
3514
|
+
bambuToken,
|
|
3515
|
+
String(args.light),
|
|
3516
|
+
String(args.mode)
|
|
3517
|
+
);
|
|
3518
|
+
}
|
|
3019
3519
|
break;
|
|
3020
3520
|
|
|
3021
3521
|
case "skip_objects":
|
|
3022
3522
|
if (!Array.isArray(args?.object_ids)) {
|
|
3023
3523
|
throw new Error("Missing required parameter: object_ids");
|
|
3024
3524
|
}
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3525
|
+
if (nativeControls) {
|
|
3526
|
+
const objectIds = args.object_ids.map((id: unknown) => Number(id));
|
|
3527
|
+
if (objectIds.length === 0 || objectIds.some((id: number) => !Number.isInteger(id) || id < 0)) {
|
|
3528
|
+
throw new Error("object_ids must contain one or more non-negative integers.");
|
|
3529
|
+
}
|
|
3530
|
+
result = await sendNativeCommand({
|
|
3531
|
+
host,
|
|
3532
|
+
serial: bambuSerial,
|
|
3533
|
+
token: bambuToken,
|
|
3534
|
+
messageJson: JSON.stringify({
|
|
3535
|
+
print: { command: "skip_objects", obj_list: objectIds, sequence_id: String(Date.now()) },
|
|
3536
|
+
}),
|
|
3537
|
+
qos: 1,
|
|
3538
|
+
});
|
|
3539
|
+
} else {
|
|
3540
|
+
result = await this.bambu.skipObjects(
|
|
3541
|
+
host,
|
|
3542
|
+
bambuSerial,
|
|
3543
|
+
bambuToken,
|
|
3544
|
+
args.object_ids.map((id: unknown) => Number(id))
|
|
3545
|
+
);
|
|
3546
|
+
}
|
|
3031
3547
|
break;
|
|
3032
3548
|
|
|
3033
3549
|
case "set_ams_drying":
|
|
3034
3550
|
if (!args?.action || args?.ams_id === undefined) {
|
|
3035
3551
|
throw new Error("Missing required parameters: action and ams_id");
|
|
3036
3552
|
}
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3553
|
+
if (nativeControls) {
|
|
3554
|
+
const action = String(args.action).trim().toLowerCase();
|
|
3555
|
+
const amsId = Number(args.ams_id);
|
|
3556
|
+
if (action !== "start" && action !== "stop") {
|
|
3557
|
+
throw new Error("AMS drying action must be one of: start, stop.");
|
|
3558
|
+
}
|
|
3559
|
+
if ((!Number.isInteger(amsId) || (amsId < 0 || amsId > 3)) && amsId !== 128) {
|
|
3560
|
+
throw new Error("X2D ams_id must be 0 to 3, or 128 for AMS-HT.");
|
|
3561
|
+
}
|
|
3562
|
+
result = await sendNativeCommand({
|
|
3563
|
+
host,
|
|
3564
|
+
serial: bambuSerial,
|
|
3565
|
+
token: bambuToken,
|
|
3566
|
+
messageJson: JSON.stringify({
|
|
3567
|
+
print: {
|
|
3568
|
+
command: "ams_control",
|
|
3569
|
+
sequence_id: String(Date.now()),
|
|
3570
|
+
ams_id: amsId,
|
|
3571
|
+
param: action === "start" ? "start_drying" : "stop_drying",
|
|
3572
|
+
},
|
|
3573
|
+
}),
|
|
3574
|
+
});
|
|
3575
|
+
} else {
|
|
3576
|
+
result = await this.bambu.setAmsDrying(
|
|
3577
|
+
host,
|
|
3578
|
+
bambuSerial,
|
|
3579
|
+
bambuToken,
|
|
3580
|
+
String(args.action),
|
|
3581
|
+
Number(args.ams_id)
|
|
3582
|
+
);
|
|
3583
|
+
}
|
|
3044
3584
|
break;
|
|
3045
3585
|
|
|
3046
3586
|
case "extend_stl_base":
|
|
@@ -3275,8 +3815,34 @@ class BambuPrinterMCPServer {
|
|
|
3275
3815
|
if (!args?.three_mf_path) {
|
|
3276
3816
|
throw new Error("Missing required parameter: three_mf_path");
|
|
3277
3817
|
}
|
|
3278
|
-
|
|
3279
|
-
|
|
3818
|
+
const printModel = await this.resolveBambuModel(args.bambu_model as string | undefined);
|
|
3819
|
+
const resolvedArgs = { ...args, bambu_model: printModel };
|
|
3820
|
+
const requestedModel = printModel;
|
|
3821
|
+
const defaultConnectionMode =
|
|
3822
|
+
DEFAULT_BAMBU_CONNECTION_MODE ||
|
|
3823
|
+
(requestedModel === "x2d" ? "bambu_native" : "lan_mqtt_ftps");
|
|
3824
|
+
const connectionMode = String(args?.connection_mode || defaultConnectionMode).trim().toLowerCase();
|
|
3825
|
+
if (!["lan_mqtt_ftps", "bambu_native", "bambu_network", "bambu_connect"].includes(connectionMode)) throw new Error("Unsupported connection_mode.");
|
|
3826
|
+
// X2D firmware accepts the Bambu Studio :6000/eMMC route but
|
|
3827
|
+
// rejects the legacy direct FTPS STOR operation with 553. Keep
|
|
3828
|
+
// an explicit old-mode request from re-entering that known-bad
|
|
3829
|
+
// path; callers still get a fully scriptable local print.
|
|
3830
|
+
const effectiveConnectionMode =
|
|
3831
|
+
connectionMode === "lan_mqtt_ftps" && requestedModel === "x2d"
|
|
3832
|
+
? "bambu_native"
|
|
3833
|
+
: connectionMode;
|
|
3834
|
+
if (effectiveConnectionMode === "bambu_connect") {
|
|
3835
|
+
result = await this.handoff3mfViaBambuConnect(resolvedArgs);
|
|
3836
|
+
break;
|
|
3837
|
+
}
|
|
3838
|
+
if (effectiveConnectionMode === "bambu_network") {
|
|
3839
|
+
result = await this.print3mfViaBambuNetwork(resolvedArgs, host, bambuSerial, bambuToken);
|
|
3840
|
+
break;
|
|
3841
|
+
}
|
|
3842
|
+
if (effectiveConnectionMode === "bambu_native") {
|
|
3843
|
+
result = await this.print3mfViaBambuNative(
|
|
3844
|
+
resolvedArgs, host, bambuSerial, bambuToken, reportNativeUpdate, extra.signal
|
|
3845
|
+
);
|
|
3280
3846
|
break;
|
|
3281
3847
|
}
|
|
3282
3848
|
if (!bambuSerial || !bambuToken) {
|
|
@@ -3287,10 +3853,10 @@ class BambuPrinterMCPServer {
|
|
|
3287
3853
|
args as Record<string, unknown> | undefined,
|
|
3288
3854
|
"print_3mf"
|
|
3289
3855
|
);
|
|
3290
|
-
const printModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
3291
3856
|
assertDirectPrintSupported(printModel, bambuSerial);
|
|
3292
3857
|
const printBedType = resolveBedType(args?.bed_type as string | undefined);
|
|
3293
|
-
const
|
|
3858
|
+
const printNozzles = requestedPrintNozzles(args as Record<string, any>);
|
|
3859
|
+
const printNozzle = String(printNozzles?.[0] ?? DEFAULT_NOZZLE_DIAMETER);
|
|
3294
3860
|
const activeSlicerProfile = await resolveTemplateFirstSlicerProfilePath(
|
|
3295
3861
|
args,
|
|
3296
3862
|
slicerProfile || undefined,
|
|
@@ -3315,6 +3881,7 @@ class BambuPrinterMCPServer {
|
|
|
3315
3881
|
entry => !entry.dir && /\.gcode$/i.test(entry.name)
|
|
3316
3882
|
);
|
|
3317
3883
|
if (!hasGcode) {
|
|
3884
|
+
assertUniformCliNozzles(args as Record<string, any>);
|
|
3318
3885
|
if (printBedType === "supertack_plate") {
|
|
3319
3886
|
throw new Error(
|
|
3320
3887
|
'BambuStudio CLI SuperTack bed type is not verified; use a pre-sliced 3MF for SuperTack or choose textured_plate, cool_plate, engineering_plate, or hot_plate.'
|
|
@@ -3414,6 +3981,7 @@ class BambuPrinterMCPServer {
|
|
|
3414
3981
|
}
|
|
3415
3982
|
if (
|
|
3416
3983
|
isH2Print &&
|
|
3984
|
+
args?.use_ams !== false &&
|
|
3417
3985
|
!hasUserAmsMapping &&
|
|
3418
3986
|
!hasUserAmsSlots &&
|
|
3419
3987
|
args?.auto_match_ams !== true &&
|
|
@@ -3444,6 +4012,7 @@ class BambuPrinterMCPServer {
|
|
|
3444
4012
|
projectName,
|
|
3445
4013
|
filePath: threeMFPath,
|
|
3446
4014
|
bambuModel: printModel,
|
|
4015
|
+
nozzleDiameters: printNozzles,
|
|
3447
4016
|
plateIndex,
|
|
3448
4017
|
useAMS: useAMS,
|
|
3449
4018
|
amsMapping: finalAmsMapping,
|
|
@@ -3512,6 +4081,7 @@ class BambuPrinterMCPServer {
|
|
|
3512
4081
|
projectName,
|
|
3513
4082
|
filePath: preparedThreeMFPath,
|
|
3514
4083
|
bambuModel: printModel,
|
|
4084
|
+
nozzleDiameters: [Number(printNozzle)],
|
|
3515
4085
|
plateIndex: collarAnalysis.plateIndex,
|
|
3516
4086
|
useAMS: true,
|
|
3517
4087
|
amsSlots: collarAnalysis.amsSlots,
|
|
@@ -3592,6 +4162,7 @@ class BambuPrinterMCPServer {
|
|
|
3592
4162
|
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
|
3593
4163
|
}
|
|
3594
4164
|
|
|
4165
|
+
await progressNotifications;
|
|
3595
4166
|
const text = typeof result === "string" ? result : JSON.stringify(result, null, 2);
|
|
3596
4167
|
|
|
3597
4168
|
if (this.runtimeConfig.enableJsonResponse && typeof result === "object") {
|