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/dist/index.js
CHANGED
|
@@ -11,10 +11,15 @@ import { createServer as createHttpServer } from "node:http";
|
|
|
11
11
|
import { randomUUID } from "node:crypto";
|
|
12
12
|
import { STLManipulator, SLICER_TYPES, normalizeSlicerType, } from "./stl/stl-manipulator.js";
|
|
13
13
|
import { BambuNetworkBridge } from "./bambu-network-bridge.js";
|
|
14
|
+
import { assertBambuNativeAvailable, validateBambuNativeMetadata, buildBambuNativeFanCommand, buildBambuNativeTemperatureCommand, printWithBambuNative, sendCommandWithBambuNative, uploadWithBambuNative, } from "./bambu-native.js";
|
|
15
|
+
import { importFileViaBambuConnect } from "./bambu-connect.js";
|
|
14
16
|
import { BlenderMcpBridge } from "./blender-mcp-bridge.js";
|
|
15
17
|
import { hasAmsMappingInput, normalizeAmsMappingObject, normalizeBridgeAmsTrayValue } from "./ams-mapping.js";
|
|
16
18
|
import { analyze3MFAmsRequirements, analyze3MFPlateObjects, analyzeCollarCharm3MF, extractBambuTemplateSettings, getCollarCharmRolePolicy, parse3MF } from './3mf_parser.js';
|
|
17
19
|
import { BambuImplementation, assertDirectPrintSupported } from "./printers/bambu.js";
|
|
20
|
+
import { inspectPrintFile } from "./safety/print-file.js";
|
|
21
|
+
import { validatePrinterState } from "./safety/printer-state.js";
|
|
22
|
+
import { withPrinterOperation, withPrintSnapshot, uniquePrintName } from "./safety/artifact.js";
|
|
18
23
|
dotenv.config();
|
|
19
24
|
const DEFAULT_HOST = process.env.BAMBU_PRINTER_HOST || process.env.PRINTER_HOST || "localhost";
|
|
20
25
|
const DEFAULT_BAMBU_SERIAL = process.env.BAMBU_PRINTER_SERIAL || process.env.BAMBU_SERIAL || "";
|
|
@@ -42,10 +47,35 @@ if (AUTOMATIC_TEMP_DIR) {
|
|
|
42
47
|
const DEFAULT_BAMBU_MODEL = process.env.BAMBU_PRINTER_MODEL?.trim().toLowerCase() ||
|
|
43
48
|
process.env.BAMBU_MODEL?.trim().toLowerCase() ||
|
|
44
49
|
"";
|
|
50
|
+
const DEFAULT_BAMBU_CONNECTION_MODE = process.env.BAMBU_DEFAULT_CONNECTION_MODE?.trim().toLowerCase() || "";
|
|
45
51
|
const DEFAULT_BED_TYPE = process.env.BED_TYPE?.trim().toLowerCase() || "textured_plate";
|
|
46
52
|
const DEFAULT_NOZZLE_DIAMETER = process.env.NOZZLE_DIAMETER?.trim() || "0.4";
|
|
53
|
+
function requestedPrintNozzles(args) {
|
|
54
|
+
if (args.nozzle_diameters !== undefined && args.nozzle_diameter !== undefined) {
|
|
55
|
+
throw new Error("Supply nozzle_diameters or nozzle_diameter, not both.");
|
|
56
|
+
}
|
|
57
|
+
const raw = args.nozzle_diameters !== undefined ? args.nozzle_diameters :
|
|
58
|
+
args.nozzle_diameter !== undefined ? [args.nozzle_diameter] : undefined;
|
|
59
|
+
// A pre-sliced job already declares every nozzle; compare those with the
|
|
60
|
+
// printer rather than broadcasting the CLI's default diameter over them.
|
|
61
|
+
if (raw === undefined)
|
|
62
|
+
return undefined;
|
|
63
|
+
if (!Array.isArray(raw) || !raw.length || raw.length > 2 || raw.some(value => (typeof value !== "number" && typeof value !== "string") || ![0.2, 0.4, 0.6, 0.8].includes(Number(value)))) {
|
|
64
|
+
throw new Error("Nozzle diameters must contain one or two supported sizes: 0.2, 0.4, 0.6, 0.8 mm.");
|
|
65
|
+
}
|
|
66
|
+
return raw.map(Number);
|
|
67
|
+
}
|
|
68
|
+
function assertUniformCliNozzles(args) {
|
|
69
|
+
const nozzles = requestedPrintNozzles(args);
|
|
70
|
+
if (nozzles && new Set(nozzles).size > 1) {
|
|
71
|
+
throw new Error("Mixed nozzle diameters require a pre-sliced 3MF with complete per-nozzle metadata. Export it from the slicer GUI before printing.");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
47
74
|
const VALID_BAMBU_MODELS = ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s", "h2c", "x2d"];
|
|
48
|
-
|
|
75
|
+
// X2D uses the same project_file/ams_mapping2 family of LAN print fields as
|
|
76
|
+
// the dual-nozzle H2 path. Keep it in this gate so pre-sliced multi-filament
|
|
77
|
+
// jobs take the correct branch instead of the legacy 5-slot mapping.
|
|
78
|
+
const H2_BAMBU_MODELS = new Set(["h2d", "h2s", "h2c", "x2d"]);
|
|
49
79
|
const VALID_BED_TYPES = ["textured_plate", "cool_plate", "engineering_plate", "hot_plate", "supertack_plate"];
|
|
50
80
|
const VALID_BAMBUSTUDIO_CLI_BED_TYPES = ["textured_plate", "cool_plate", "engineering_plate", "hot_plate"];
|
|
51
81
|
// Map model IDs to BambuStudio --load-machine preset names
|
|
@@ -641,6 +671,19 @@ function stringifyBridgeJson(value) {
|
|
|
641
671
|
return value;
|
|
642
672
|
return JSON.stringify(value);
|
|
643
673
|
}
|
|
674
|
+
function nativeAmsMapping2(mapping) {
|
|
675
|
+
if (!mapping || mapping.length === 0)
|
|
676
|
+
return undefined;
|
|
677
|
+
return JSON.stringify(mapping.map((value) => {
|
|
678
|
+
if (value < 0 || value === 255)
|
|
679
|
+
return { ams_id: 255, slot_id: 255 };
|
|
680
|
+
if (value === 254)
|
|
681
|
+
return { ams_id: 254, slot_id: 254 };
|
|
682
|
+
if (value >= 128)
|
|
683
|
+
return { ams_id: 128, slot_id: value - 128 };
|
|
684
|
+
return { ams_id: Math.floor(value / 4), slot_id: value % 4 };
|
|
685
|
+
}));
|
|
686
|
+
}
|
|
644
687
|
function redactPrintParams(params) {
|
|
645
688
|
return {
|
|
646
689
|
...params,
|
|
@@ -789,7 +832,23 @@ class BambuPrinterMCPServer {
|
|
|
789
832
|
tools: {}
|
|
790
833
|
}
|
|
791
834
|
});
|
|
792
|
-
this.bambu = new BambuImplementation()
|
|
835
|
+
this.bambu = new BambuImplementation(async (message) => {
|
|
836
|
+
try {
|
|
837
|
+
const response = await this.server.elicitInput({
|
|
838
|
+
mode: "form",
|
|
839
|
+
message,
|
|
840
|
+
requestedSchema: {
|
|
841
|
+
type: "object",
|
|
842
|
+
properties: { confirmed: { type: "boolean", title: "I checked the printer and confirm this operation", default: false } },
|
|
843
|
+
required: ["confirmed"],
|
|
844
|
+
},
|
|
845
|
+
});
|
|
846
|
+
return response.action === "accept" && response.content?.confirmed === true;
|
|
847
|
+
}
|
|
848
|
+
catch {
|
|
849
|
+
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.");
|
|
850
|
+
}
|
|
851
|
+
});
|
|
793
852
|
this.bambuNetwork = new BambuNetworkBridge();
|
|
794
853
|
this.stlManipulator = new STLManipulator(TEMP_DIR);
|
|
795
854
|
this.setupHandlers();
|
|
@@ -903,10 +962,11 @@ class BambuPrinterMCPServer {
|
|
|
903
962
|
const JSZip = (await import('jszip')).default;
|
|
904
963
|
const zipData = fs.readFileSync(threeMFPath);
|
|
905
964
|
const zip = await JSZip.loadAsync(zipData);
|
|
906
|
-
const hasGcode = Object.
|
|
965
|
+
const hasGcode = Object.values(zip.files).some(entry => !entry.dir && /\.gcode$/i.test(entry.name));
|
|
907
966
|
if (hasGcode) {
|
|
908
967
|
return { threeMFPath, autoSliced: false };
|
|
909
968
|
}
|
|
969
|
+
assertUniformCliNozzles(args);
|
|
910
970
|
if (bedType === "supertack_plate") {
|
|
911
971
|
throw new Error('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.');
|
|
912
972
|
}
|
|
@@ -973,17 +1033,21 @@ class BambuPrinterMCPServer {
|
|
|
973
1033
|
useAMS = false;
|
|
974
1034
|
}
|
|
975
1035
|
if ((!finalAmsMapping || finalAmsMapping.length === 0) && (!finalAmsSlots || finalAmsSlots.length === 0)) {
|
|
1036
|
+
if (args.use_ams === true || (args.use_ams !== false && (hasUserAmsMapping || hasUserAmsSlots))) {
|
|
1037
|
+
throw new Error("AMS printing requires a nonempty ams_mapping, ams_slots, or auto_match_ams result.");
|
|
1038
|
+
}
|
|
976
1039
|
useAMS = false;
|
|
977
1040
|
}
|
|
978
1041
|
return { useAMS, finalAmsMapping, finalAmsSlots };
|
|
979
1042
|
}
|
|
980
|
-
async print3mfViaBambuNetwork(args, host, bambuSerial, bambuToken) {
|
|
1043
|
+
async print3mfViaBambuNetwork(args, host, bambuSerial, bambuToken, native) {
|
|
981
1044
|
if (!args?.three_mf_path) {
|
|
982
1045
|
throw new Error("Missing required parameter: three_mf_path");
|
|
983
1046
|
}
|
|
984
1047
|
const printModel = await this.resolveBambuModel(args?.bambu_model);
|
|
985
1048
|
const printBedType = resolveBedType(args?.bed_type);
|
|
986
|
-
const
|
|
1049
|
+
const printNozzles = requestedPrintNozzles(args);
|
|
1050
|
+
const printNozzle = String(printNozzles?.[0] ?? DEFAULT_NOZZLE_DIAMETER);
|
|
987
1051
|
const printPreset = BAMBU_MODEL_PRESETS[printModel]?.(printNozzle);
|
|
988
1052
|
const plateIndex = args?.plate_index !== undefined ? Number(args.plate_index) : 0;
|
|
989
1053
|
if (!Number.isInteger(plateIndex) || plateIndex < 0) {
|
|
@@ -1009,83 +1073,233 @@ class BambuPrinterMCPServer {
|
|
|
1009
1073
|
if (isLocalBridgePrint && !password) {
|
|
1010
1074
|
throw new Error("bambu_token/access code is required for FULU BambuNetwork LAN/local print methods.");
|
|
1011
1075
|
}
|
|
1076
|
+
if (!bambuSerial || devId !== bambuSerial || !devIp || devIp === "localhost" || !(password || bambuToken)) {
|
|
1077
|
+
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.");
|
|
1078
|
+
}
|
|
1079
|
+
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"]) {
|
|
1080
|
+
if (args[field] !== undefined && args[field] !== "")
|
|
1081
|
+
throw new Error(`${field} cannot override a verified print. Use the structured plate, nozzle, and AMS arguments.`);
|
|
1082
|
+
}
|
|
1012
1083
|
const { threeMFPath, autoSliced } = await this.ensurePrintableThreeMFPath(args, printModel, printPreset, printBedType);
|
|
1013
1084
|
const { useAMS, finalAmsMapping, finalAmsSlots } = await this.resolveAmsPrintSettings(threeMFPath, args, host, bambuSerial, bambuToken, printModel, printNozzle);
|
|
1085
|
+
if (native && args.use_ams !== false && !useAMS) {
|
|
1086
|
+
throw new Error("Native X2D printing requires ams_mapping, ams_slots, auto_match_ams, or explicit use_ams: false for an external spool.");
|
|
1087
|
+
}
|
|
1014
1088
|
const threeMfFilename = path.basename(threeMFPath);
|
|
1015
|
-
const projectName = String(args?.project_name || threeMfFilename.replace(/\.3mf$/i, ''));
|
|
1089
|
+
const projectName = String(args?.project_name || args?.task_name || threeMfFilename.replace(/\.3mf$/i, ''));
|
|
1016
1090
|
const presetName = String(args?.preset_name || `${projectName}_plate_${plateIndex + 1}`);
|
|
1017
1091
|
const clientJobId = args?.client_job_id !== undefined ? Number(args.client_job_id) : Date.now();
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1092
|
+
return withPrinterOperation(devIp, devId, assertActive => withPrintSnapshot(threeMFPath, async (snapshot, retainForDispatchedUpload) => {
|
|
1093
|
+
const inspection = await inspectPrintFile(snapshot, { model: printModel, nozzleDiameters: printNozzles, plateIndex, bedType: printBedType });
|
|
1094
|
+
let mapping = finalAmsMapping?.slice();
|
|
1095
|
+
if (finalAmsSlots) {
|
|
1096
|
+
if (finalAmsSlots.length !== inspection.usedFilamentPositions.length)
|
|
1097
|
+
throw new Error("ams_slots must supply exactly one physical tray per used filament.");
|
|
1098
|
+
mapping = Array(inspection.materials.length).fill(-1);
|
|
1099
|
+
inspection.usedFilamentPositions.forEach((position, index) => { mapping[position] = finalAmsSlots[index]; });
|
|
1100
|
+
}
|
|
1101
|
+
if (!useAMS) {
|
|
1102
|
+
mapping = Array(inspection.materials.length).fill(-1);
|
|
1103
|
+
inspection.usedFilamentPositions.forEach(position => { mapping[position] = 254; });
|
|
1104
|
+
}
|
|
1105
|
+
const requirements = { ...inspection, amsMapping: mapping, useAMS };
|
|
1106
|
+
const initialStatus = await this.bambu.getSafetyStatus(devIp, devId, password || bambuToken);
|
|
1107
|
+
validatePrinterState(initialStatus, requirements);
|
|
1108
|
+
const bedClearance = await this.bambu.confirmPrintPreflight(devId, initialStatus, inspection);
|
|
1109
|
+
const confirmedStatus = await this.bambu.getSafetyStatus(devIp, devId, password || bambuToken);
|
|
1110
|
+
validatePrinterState(confirmedStatus, requirements);
|
|
1111
|
+
this.bambu.assertBedClearance(confirmedStatus, bedClearance);
|
|
1112
|
+
const amsMapping = stringifyBridgeJson(mapping);
|
|
1113
|
+
const remoteFileName = uniquePrintName(threeMFPath);
|
|
1114
|
+
const params = {
|
|
1115
|
+
dev_id: devId,
|
|
1116
|
+
// Firmware can report the task name instead of the uploaded filename.
|
|
1117
|
+
// Bind either report to this inspected artifact for a later safe resume.
|
|
1118
|
+
task_name: remoteFileName.replace(/(?:\.gcode)?\.3mf$/i, ""),
|
|
1119
|
+
project_name: projectName,
|
|
1120
|
+
preset_name: presetName,
|
|
1121
|
+
filename: snapshot,
|
|
1122
|
+
config_filename: snapshot,
|
|
1123
|
+
plate_index: plateIndex + 1,
|
|
1124
|
+
ftp_folder: String(args?.ftp_folder || ""),
|
|
1125
|
+
ftp_file: String(args?.ftp_file || ""),
|
|
1126
|
+
ftp_file_md5: String(args?.ftp_file_md5 || ""),
|
|
1127
|
+
nozzle_mapping: stringifyBridgeJson(args?.nozzle_mapping) || "",
|
|
1128
|
+
ams_mapping: amsMapping || "",
|
|
1129
|
+
ams_mapping2: stringifyBridgeJson(args?.ams_mapping2) || "",
|
|
1130
|
+
ams_mapping_info: stringifyBridgeJson(args?.ams_mapping_info) || "",
|
|
1131
|
+
nozzles_info: stringifyBridgeJson(args?.nozzles_info) || "",
|
|
1132
|
+
connection_type: connectionType,
|
|
1133
|
+
comments: String(args?.comments || ""),
|
|
1134
|
+
origin_profile_id: args?.origin_profile_id !== undefined ? Number(args.origin_profile_id) : 0,
|
|
1135
|
+
stl_design_id: args?.stl_design_id !== undefined ? Number(args.stl_design_id) : 0,
|
|
1136
|
+
origin_model_id: String(args?.origin_model_id || ""),
|
|
1137
|
+
print_type: String(args?.print_type || "from_normal"),
|
|
1138
|
+
dst_file: remoteFileName,
|
|
1139
|
+
dev_name: String(args?.dev_name || ""),
|
|
1140
|
+
dev_ip: devIp,
|
|
1141
|
+
use_ssl_for_ftp: args?.use_ssl_for_ftp !== undefined ? Boolean(args.use_ssl_for_ftp) : true,
|
|
1142
|
+
use_ssl_for_mqtt: args?.use_ssl_for_mqtt !== undefined ? Boolean(args.use_ssl_for_mqtt) : true,
|
|
1143
|
+
username: String(args?.username || "bblp"),
|
|
1144
|
+
password,
|
|
1145
|
+
task_bed_leveling: args?.bed_leveling !== undefined ? Boolean(args.bed_leveling) : true,
|
|
1146
|
+
task_flow_cali: args?.flow_calibration !== undefined ? Boolean(args.flow_calibration) : true,
|
|
1147
|
+
task_vibration_cali: args?.vibration_calibration !== undefined ? Boolean(args.vibration_calibration) : true,
|
|
1148
|
+
task_layer_inspect: args?.layer_inspect !== undefined ? Boolean(args.layer_inspect) : false,
|
|
1149
|
+
task_record_timelapse: args?.timelapse !== undefined ? Boolean(args.timelapse) : false,
|
|
1150
|
+
task_use_ams: useAMS,
|
|
1151
|
+
task_bed_type: printBedType,
|
|
1152
|
+
extra_options: stringifyBridgeJson(args?.extra_options) || "",
|
|
1153
|
+
auto_bed_leveling: args?.auto_bed_leveling !== undefined ? Number(args.auto_bed_leveling) : 0,
|
|
1154
|
+
auto_flow_cali: args?.auto_flow_cali !== undefined ? Number(args.auto_flow_cali) : 0,
|
|
1155
|
+
auto_offset_cali: args?.auto_offset_cali !== undefined ? Number(args.auto_offset_cali) : 0,
|
|
1156
|
+
extruder_cali_manual_mode: args?.extruder_cali_manual_mode !== undefined ? Number(args.extruder_cali_manual_mode) : -1,
|
|
1157
|
+
task_ext_change_assist: args?.external_change_assist !== undefined ? Boolean(args.external_change_assist) : false,
|
|
1158
|
+
try_emmc_print: args?.try_emmc_print !== undefined ? Boolean(args.try_emmc_print) : false,
|
|
1159
|
+
};
|
|
1160
|
+
assertActive();
|
|
1161
|
+
if (native) {
|
|
1162
|
+
// Release the telemetry client before the installed plug-in opens its session.
|
|
1163
|
+
await this.bambu.disconnectAll();
|
|
1164
|
+
assertActive();
|
|
1165
|
+
const nativeResult = await printWithBambuNative({
|
|
1166
|
+
host: devIp, serial: devId, token: password || bambuToken, filePath: snapshot,
|
|
1167
|
+
projectName: String(params.task_name), presetName, remoteName: remoteFileName,
|
|
1168
|
+
plateIndex, bedType: printBedType, useAMS,
|
|
1169
|
+
amsMapping, amsMapping2: nativeAmsMapping2(mapping),
|
|
1170
|
+
bedLeveling: params.task_bed_leveling,
|
|
1171
|
+
flowCalibration: params.task_flow_cali,
|
|
1172
|
+
vibrationCalibration: params.task_vibration_cali,
|
|
1173
|
+
layerInspect: params.task_layer_inspect,
|
|
1174
|
+
timelapse: params.task_record_timelapse,
|
|
1175
|
+
}, native.onUpdate, { signal: native.signal, assertActive, beforeDispatch: async () => {
|
|
1176
|
+
assertActive();
|
|
1177
|
+
const dispatchStatus = await this.bambu.getSafetyStatus(devIp, devId, password || bambuToken);
|
|
1178
|
+
validatePrinterState(dispatchStatus, requirements);
|
|
1179
|
+
this.bambu.assertBedClearance(dispatchStatus, bedClearance);
|
|
1180
|
+
assertActive();
|
|
1181
|
+
} });
|
|
1182
|
+
assertActive();
|
|
1183
|
+
this.bambu.recordCheckedJob(devIp, devId, remoteFileName, requirements);
|
|
1184
|
+
return { ...nativeResult, autoSliced, projectName, plateIndex, useAMS, amsMapping: mapping };
|
|
1185
|
+
}
|
|
1186
|
+
const bridgeResult = await this.bambuNetwork.callWithAgent(bridgeMethod, { client_job_id: clientJobId, params }, {
|
|
1187
|
+
...this.bridgeOptionsFromArgs(args, "print_3mf_bambu_network"),
|
|
1188
|
+
beforeDispatch: async (method) => {
|
|
1189
|
+
if (method !== bridgeMethod)
|
|
1190
|
+
return;
|
|
1191
|
+
assertActive();
|
|
1192
|
+
const dispatchStatus = await this.bambu.getSafetyStatus(devIp, devId, password || bambuToken);
|
|
1193
|
+
validatePrinterState(dispatchStatus, requirements);
|
|
1194
|
+
this.bambu.assertBedClearance(dispatchStatus, bedClearance);
|
|
1195
|
+
},
|
|
1196
|
+
assertDispatchAllowed: method => { if (method === bridgeMethod)
|
|
1197
|
+
assertActive(); },
|
|
1198
|
+
onDispatched: method => { if (method === bridgeMethod)
|
|
1199
|
+
retainForDispatchedUpload(); },
|
|
1200
|
+
});
|
|
1201
|
+
if (typeof bridgeResult === "object" && bridgeResult !== null && bridgeResult.ok === false) {
|
|
1202
|
+
throw new Error(`FULU BambuNetwork bridge method ${bridgeMethod} failed: ${String(bridgeResult.error || "unknown bridge error")}`);
|
|
1203
|
+
}
|
|
1204
|
+
if (typeof bridgeResult === "object" &&
|
|
1205
|
+
bridgeResult !== null &&
|
|
1206
|
+
typeof bridgeResult.value === "number" &&
|
|
1207
|
+
bridgeResult.value !== 0) {
|
|
1208
|
+
const value = bridgeResult.value;
|
|
1209
|
+
throw new Error(`FULU BambuNetwork bridge method ${bridgeMethod} returned non-zero result ${value}.`);
|
|
1210
|
+
}
|
|
1211
|
+
assertActive();
|
|
1212
|
+
this.bambu.recordCheckedJob(devIp, devId, remoteFileName, requirements);
|
|
1213
|
+
return {
|
|
1214
|
+
status: "success",
|
|
1215
|
+
message: `FULU BambuNetwork ${bridgePrintMethod} command for ${threeMfFilename} sent successfully.`,
|
|
1216
|
+
bridgeMethod,
|
|
1217
|
+
bridgeResult,
|
|
1218
|
+
clientJobId,
|
|
1219
|
+
autoSliced,
|
|
1220
|
+
projectName,
|
|
1221
|
+
plateIndex,
|
|
1222
|
+
bridgePlateIndex: plateIndex + 1,
|
|
1223
|
+
useAMS,
|
|
1224
|
+
amsMapping: mapping,
|
|
1225
|
+
params: redactPrintParams(params),
|
|
1226
|
+
};
|
|
1227
|
+
}));
|
|
1228
|
+
}
|
|
1229
|
+
async handoff3mfViaBambuConnect(args) {
|
|
1230
|
+
if (!args?.three_mf_path) {
|
|
1231
|
+
throw new Error("Missing required parameter: three_mf_path");
|
|
1066
1232
|
}
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1233
|
+
const printModel = await this.resolveBambuModel(args?.bambu_model);
|
|
1234
|
+
const printBedType = resolveBedType(args?.bed_type);
|
|
1235
|
+
const printNozzle = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
1236
|
+
const printPreset = BAMBU_MODEL_PRESETS[printModel]?.(printNozzle);
|
|
1237
|
+
const plateIndex = args?.plate_index !== undefined ? Number(args.plate_index) : 0;
|
|
1238
|
+
if (!Number.isInteger(plateIndex) || plateIndex < 0) {
|
|
1239
|
+
throw new Error("plate_index must be a non-negative integer.");
|
|
1073
1240
|
}
|
|
1241
|
+
const { threeMFPath, autoSliced } = await this.ensurePrintableThreeMFPath(args, printModel, printPreset, printBedType);
|
|
1242
|
+
const threeMfFilename = path.basename(threeMFPath);
|
|
1243
|
+
const projectName = String(args?.project_name || threeMfFilename.replace(/\.3mf$/i, ""));
|
|
1244
|
+
const handoff = await importFileViaBambuConnect({
|
|
1245
|
+
filePath: threeMFPath,
|
|
1246
|
+
name: projectName,
|
|
1247
|
+
version: args?.version !== undefined ? String(args.version) : undefined,
|
|
1248
|
+
});
|
|
1074
1249
|
return {
|
|
1075
|
-
|
|
1076
|
-
message: `FULU BambuNetwork ${bridgePrintMethod} command for ${threeMfFilename} sent successfully.`,
|
|
1077
|
-
bridgeMethod,
|
|
1078
|
-
bridgeResult,
|
|
1079
|
-
clientJobId,
|
|
1250
|
+
...handoff,
|
|
1080
1251
|
autoSliced,
|
|
1081
|
-
projectName,
|
|
1082
1252
|
plateIndex,
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
params: redactPrintParams(params),
|
|
1253
|
+
printer_model: printModel,
|
|
1254
|
+
bed_type: printBedType,
|
|
1255
|
+
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.",
|
|
1087
1256
|
};
|
|
1088
1257
|
}
|
|
1258
|
+
async print3mfViaBambuNative(args, host, bambuSerial, bambuToken, onUpdate, signal) {
|
|
1259
|
+
assertBambuNativeAvailable();
|
|
1260
|
+
const model = await this.resolveBambuModel(args.bambu_model);
|
|
1261
|
+
if (model !== "x2d")
|
|
1262
|
+
throw new Error("connection_mode=bambu_native is restricted to X2D.");
|
|
1263
|
+
return this.print3mfViaBambuNetwork({ ...args, bambu_model: model, connection_type: "lan", bambu_network_method: "start_local_print" }, host, bambuSerial, bambuToken, { onUpdate, signal });
|
|
1264
|
+
}
|
|
1265
|
+
async uploadFileViaBambuNative(args, host, bambuSerial, bambuToken, onUpdate, signal) {
|
|
1266
|
+
assertBambuNativeAvailable();
|
|
1267
|
+
if (args.print)
|
|
1268
|
+
throw new Error("Native upload never starts printing; use print_3mf.");
|
|
1269
|
+
const model = await this.resolveBambuModel(args.bambu_model);
|
|
1270
|
+
if (model !== "x2d")
|
|
1271
|
+
throw new Error("connection_mode=bambu_native is restricted to X2D.");
|
|
1272
|
+
const unsupported = ["use_ams", "ams_mapping", "ams_mapping2", "ams_mapping_info", "nozzle_mapping", "nozzles_info",
|
|
1273
|
+
"bed_leveling", "flow_calibration", "vibration_calibration", "layer_inspect", "timelapse"];
|
|
1274
|
+
for (const key of unsupported) {
|
|
1275
|
+
if (args[key] !== undefined)
|
|
1276
|
+
throw new Error(`${key} is not supported for native upload-only requests; use print_3mf for checked print settings.`);
|
|
1277
|
+
}
|
|
1278
|
+
for (const key of ["project_name", "preset_name"]) {
|
|
1279
|
+
if (args[key] !== undefined && (typeof args[key] !== "string" || !args[key].trim())) {
|
|
1280
|
+
throw new Error(`${key} must be a non-empty string.`);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
const plateIndex = args.plate_index ?? 0;
|
|
1284
|
+
if (!Number.isSafeInteger(plateIndex) || plateIndex < 0)
|
|
1285
|
+
throw new Error("plate_index must be a non-negative integer.");
|
|
1286
|
+
if (path.extname(String(args.file_path)).toLowerCase() !== ".3mf" && plateIndex !== 0) {
|
|
1287
|
+
throw new Error("plate_index is only supported for 3MF uploads.");
|
|
1288
|
+
}
|
|
1289
|
+
const bedType = resolveBedType(args.bed_type ?? "textured_plate");
|
|
1290
|
+
return this.bambu.uploadFile(host, bambuSerial, bambuToken, String(args.file_path), String(args.filename), false, model, async (snapshot, destination, assertActive) => {
|
|
1291
|
+
if (path.extname(snapshot).toLowerCase() === ".3mf") {
|
|
1292
|
+
await inspectPrintFile(snapshot, { model, plateIndex, bedType });
|
|
1293
|
+
}
|
|
1294
|
+
assertActive();
|
|
1295
|
+
await this.bambu.disconnectAll();
|
|
1296
|
+
await uploadWithBambuNative({
|
|
1297
|
+
host, serial: bambuSerial, token: bambuToken, filePath: snapshot,
|
|
1298
|
+
remoteName: destination, projectName: args.project_name ?? path.posix.basename(String(args.filename)),
|
|
1299
|
+
presetName: args.preset_name ?? "checked-upload", plateIndex, bedType, useAMS: false,
|
|
1300
|
+
}, onUpdate, { signal, assertActive });
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1089
1303
|
async getResolvedPrinterFilamentInventory(host, bambuSerial, bambuToken, bambuModel, nozzleDiameter) {
|
|
1090
1304
|
const status = await this.bambu.getStatus(host, bambuSerial, bambuToken);
|
|
1091
1305
|
let inventory = normalizePrinterFilamentInventory(status, bambuModel, nozzleDiameter);
|
|
@@ -1646,6 +1860,19 @@ class BambuPrinterMCPServer {
|
|
|
1646
1860
|
}
|
|
1647
1861
|
}
|
|
1648
1862
|
},
|
|
1863
|
+
{
|
|
1864
|
+
name: "bambu_connect_import_file",
|
|
1865
|
+
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.",
|
|
1866
|
+
inputSchema: {
|
|
1867
|
+
type: "object",
|
|
1868
|
+
properties: {
|
|
1869
|
+
file_path: { type: "string", description: "Readable local path to a .gcode, .3mf, or .gcode.3mf file" },
|
|
1870
|
+
name: { type: "string", description: "Optional project name shown by Bambu Connect" },
|
|
1871
|
+
version: { type: "string", description: "Optional integration version sent to Bambu Connect (default: 1.0.0)" }
|
|
1872
|
+
},
|
|
1873
|
+
required: ["file_path"]
|
|
1874
|
+
}
|
|
1875
|
+
},
|
|
1649
1876
|
{
|
|
1650
1877
|
name: "bambu_network_bridge_status",
|
|
1651
1878
|
description: "Inspect or probe the FULU OrcaSlicer-bambulab BambuNetwork bridge runtime used for cloud and restored BambuNetwork printing.",
|
|
@@ -1704,7 +1931,7 @@ class BambuPrinterMCPServer {
|
|
|
1704
1931
|
plate_index: { type: "number", description: "Zero-based plate index to print from the sliced 3MF; converted to FULU's one-based PrintParams plate_index." },
|
|
1705
1932
|
project_name: { type: "string", description: "Optional project name sent in FULU PrintParams; defaults to the 3MF filename without extension." },
|
|
1706
1933
|
preset_name: { type: "string", description: "Optional preset name sent in FULU PrintParams; defaults to project plus one-based plate index." },
|
|
1707
|
-
task_name: { type: "string", description: "Optional
|
|
1934
|
+
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." },
|
|
1708
1935
|
config_filename: { type: "string", description: "Optional config 3MF path for cloud print; defaults to the same 3MF path." },
|
|
1709
1936
|
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." },
|
|
1710
1937
|
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." },
|
|
@@ -1715,6 +1942,7 @@ class BambuPrinterMCPServer {
|
|
|
1715
1942
|
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." },
|
|
1716
1943
|
slicer_profile: { type: "string", description: "Path to an optional slicer profile/config file for auto-slicing." },
|
|
1717
1944
|
nozzle_diameter: { type: "string", description: "Nozzle diameter in mm for auto-slicing (default: 0.4)." },
|
|
1945
|
+
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." },
|
|
1718
1946
|
use_ams: { type: "boolean", description: "Whether to use the AMS; defaults to auto-detect from the 3MF mapping." },
|
|
1719
1947
|
ams_mapping: { type: "array", description: "AMS slot mapping array used by both local MCP printing and FULU PrintParams.", items: { type: "number" } },
|
|
1720
1948
|
ams_slots: { type: "array", description: "Per-used-filament AMS slot list, matching the local LAN print path.", items: { type: "number" } },
|
|
@@ -1740,7 +1968,7 @@ class BambuPrinterMCPServer {
|
|
|
1740
1968
|
},
|
|
1741
1969
|
{
|
|
1742
1970
|
name: "camera_snapshot",
|
|
1743
|
-
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.",
|
|
1971
|
+
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.",
|
|
1744
1972
|
inputSchema: {
|
|
1745
1973
|
type: "object",
|
|
1746
1974
|
properties: {
|
|
@@ -1748,7 +1976,7 @@ class BambuPrinterMCPServer {
|
|
|
1748
1976
|
timeout_ms: { type: "number", description: "Max ms to wait for a full frame (default 8000). Camera may take a few seconds on cold start." },
|
|
1749
1977
|
bambu_model: { type: "string", description: "Printer model. Used to route to the correct protocol or fail fast on unsupported models. Defaults to BAMBU_MODEL." },
|
|
1750
1978
|
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." },
|
|
1751
|
-
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)." },
|
|
1979
|
+
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)." },
|
|
1752
1980
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
1753
1981
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
1754
1982
|
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
@@ -1778,13 +2006,14 @@ class BambuPrinterMCPServer {
|
|
|
1778
2006
|
},
|
|
1779
2007
|
{
|
|
1780
2008
|
name: "upload_gcode",
|
|
1781
|
-
description: "
|
|
2009
|
+
description: "Inspect a G-code file and upload a uniquely named copy without overwriting existing printer files.",
|
|
1782
2010
|
inputSchema: {
|
|
1783
2011
|
type: "object",
|
|
1784
2012
|
properties: {
|
|
1785
2013
|
filename: { type: "string", description: "Name for the file on the printer" },
|
|
1786
2014
|
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." },
|
|
1787
2015
|
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." },
|
|
2016
|
+
bambu_model: { type: "string", enum: [...VALID_BAMBU_MODELS], description: "Required printer model for inspecting the uploaded G-code, even when not starting it." },
|
|
1788
2017
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
1789
2018
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
1790
2019
|
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
@@ -1801,11 +2030,20 @@ class BambuPrinterMCPServer {
|
|
|
1801
2030
|
file_path: { type: "string", description: "Local path to the file to upload" },
|
|
1802
2031
|
filename: { type: "string", description: "Name for the file on the printer" },
|
|
1803
2032
|
print: { type: "boolean", description: "Start printing after upload (default: false)" },
|
|
2033
|
+
connection_mode: {
|
|
2034
|
+
type: "string",
|
|
2035
|
+
enum: ["lan_mqtt_ftps", "bambu_native"],
|
|
2036
|
+
description: "Upload transport; bambu_native uses the installed Bambu networking plug-in for X2D and never starts printing."
|
|
2037
|
+
},
|
|
1804
2038
|
bambu_model: {
|
|
1805
2039
|
type: "string",
|
|
1806
2040
|
enum: [...VALID_BAMBU_MODELS],
|
|
1807
|
-
description: "Required
|
|
2041
|
+
description: "Required for every printable .gcode or .3mf upload, including upload-only operations."
|
|
1808
2042
|
},
|
|
2043
|
+
project_name: { type: "string", description: "Optional project name passed to the native X2D uploader; defaults to filename." },
|
|
2044
|
+
preset_name: { type: "string", description: "Optional printer preset name passed to the native X2D uploader." },
|
|
2045
|
+
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)." },
|
|
2046
|
+
plate_index: { type: "integer", minimum: 0, description: "Zero-based plate index passed to the native X2D uploader (default: 0)." },
|
|
1809
2047
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
1810
2048
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
1811
2049
|
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
@@ -1887,6 +2125,22 @@ class BambuPrinterMCPServer {
|
|
|
1887
2125
|
}
|
|
1888
2126
|
}
|
|
1889
2127
|
},
|
|
2128
|
+
{
|
|
2129
|
+
name: "x2d_native_control",
|
|
2130
|
+
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.",
|
|
2131
|
+
inputSchema: {
|
|
2132
|
+
type: "object",
|
|
2133
|
+
properties: {
|
|
2134
|
+
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." },
|
|
2135
|
+
qos: { type: "number", description: "MQTT QoS used by Bambu Studio (0 or 1; default 0)." },
|
|
2136
|
+
flag: { type: "number", description: "Bambu networking plug-in command flag (0 or 1; default 0)." },
|
|
2137
|
+
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2138
|
+
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
2139
|
+
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
2140
|
+
},
|
|
2141
|
+
required: ["message_json"]
|
|
2142
|
+
}
|
|
2143
|
+
},
|
|
1890
2144
|
{
|
|
1891
2145
|
name: "clear_hms_errors",
|
|
1892
2146
|
description: "Clear HMS or print error state on the Bambu Lab printer using the clean_print_error MQTT command.",
|
|
@@ -1937,11 +2191,11 @@ class BambuPrinterMCPServer {
|
|
|
1937
2191
|
},
|
|
1938
2192
|
{
|
|
1939
2193
|
name: "reread_ams_rfid",
|
|
1940
|
-
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.",
|
|
2194
|
+
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.",
|
|
1941
2195
|
inputSchema: {
|
|
1942
2196
|
type: "object",
|
|
1943
2197
|
properties: {
|
|
1944
|
-
ams_id: { type: "number", description: "AMS unit index from 0 to 3" },
|
|
2198
|
+
ams_id: { type: "number", description: "AMS unit index from 0 to 3, or 128 for an X2D AMS-HT." },
|
|
1945
2199
|
slot_id: { type: "number", description: "Slot index within that AMS, from 0 to 3" },
|
|
1946
2200
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
1947
2201
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
@@ -1952,12 +2206,15 @@ class BambuPrinterMCPServer {
|
|
|
1952
2206
|
},
|
|
1953
2207
|
{
|
|
1954
2208
|
name: "set_temperature",
|
|
1955
|
-
description: "Set
|
|
2209
|
+
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.",
|
|
1956
2210
|
inputSchema: {
|
|
1957
2211
|
type: "object",
|
|
1958
2212
|
properties: {
|
|
1959
2213
|
component: { type: "string", description: "Component to heat: bed, nozzle, or extruder" },
|
|
1960
|
-
temperature: { type: "number", description: "
|
|
2214
|
+
temperature: { type: "number", minimum: 0, description: "Finite target temperature in °C, checked against model/component and declared material limits" },
|
|
2215
|
+
bambu_model: { type: "string", enum: [...VALID_BAMBU_MODELS], description: "Printer model, required for positive heating unless configured in the environment" },
|
|
2216
|
+
material: { type: "string", description: "Loaded material, for example PLA or PETG. Required for positive nozzle heating, including non-RFID spools." },
|
|
2217
|
+
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)" },
|
|
1961
2218
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
1962
2219
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
1963
2220
|
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
@@ -1967,15 +2224,16 @@ class BambuPrinterMCPServer {
|
|
|
1967
2224
|
},
|
|
1968
2225
|
{
|
|
1969
2226
|
name: "set_fan_speed",
|
|
1970
|
-
description: "Set a Bambu printer fan speed percentage
|
|
2227
|
+
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.",
|
|
1971
2228
|
inputSchema: {
|
|
1972
2229
|
type: "object",
|
|
1973
2230
|
properties: {
|
|
1974
2231
|
fan: {
|
|
1975
2232
|
type: "string",
|
|
1976
|
-
description: "Fan to control: part, auxiliary, chamber, 1, 2, or
|
|
2233
|
+
description: "Fan to control: part, auxiliary, right_auxiliary, chamber, 1, 2, 3, or 10"
|
|
1977
2234
|
},
|
|
1978
2235
|
speed: { type: "number", description: "Fan speed percentage from 0 to 100" },
|
|
2236
|
+
confirm_during_print: { type: "boolean", description: "Required for X2D fan changes while a print is running." },
|
|
1979
2237
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
1980
2238
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
1981
2239
|
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
@@ -2025,7 +2283,7 @@ class BambuPrinterMCPServer {
|
|
|
2025
2283
|
},
|
|
2026
2284
|
{
|
|
2027
2285
|
name: "set_ams_drying",
|
|
2028
|
-
description: "Start or stop the AMS filament drying cycle.
|
|
2286
|
+
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.",
|
|
2029
2287
|
inputSchema: {
|
|
2030
2288
|
type: "object",
|
|
2031
2289
|
properties: {
|
|
@@ -2036,7 +2294,7 @@ class BambuPrinterMCPServer {
|
|
|
2036
2294
|
},
|
|
2037
2295
|
ams_id: {
|
|
2038
2296
|
type: "number",
|
|
2039
|
-
description: "AMS unit index from 0 to 3"
|
|
2297
|
+
description: "AMS unit index from 0 to 3, or 128 for an X2D AMS-HT."
|
|
2040
2298
|
},
|
|
2041
2299
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2042
2300
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
@@ -2059,8 +2317,8 @@ class BambuPrinterMCPServer {
|
|
|
2059
2317
|
},
|
|
2060
2318
|
connection_mode: {
|
|
2061
2319
|
type: "string",
|
|
2062
|
-
enum: ["lan_mqtt_ftps", "bambu_network"],
|
|
2063
|
-
description: "Print path to
|
|
2320
|
+
enum: ["lan_mqtt_ftps", "bambu_network", "bambu_native", "bambu_connect"],
|
|
2321
|
+
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."
|
|
2064
2322
|
},
|
|
2065
2323
|
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." },
|
|
2066
2324
|
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." },
|
|
@@ -2108,7 +2366,8 @@ class BambuPrinterMCPServer {
|
|
|
2108
2366
|
template_3mf_path: { type: "string", description: "Optional template 3MF whose embedded Bambu slicer settings should be reused when auto-slicing this print job." },
|
|
2109
2367
|
template_name: { type: "string", description: "Optional named template from the local registry. Resolves to template_3mf_path automatically." },
|
|
2110
2368
|
template_dir: { type: "string", description: "Optional template directory override when resolving template_name." },
|
|
2111
|
-
nozzle_diameter: { type: "string", description: "Nozzle diameter in mm for auto-slicing (default: 0.4)" }
|
|
2369
|
+
nozzle_diameter: { type: "string", description: "Nozzle diameter in mm for auto-slicing (default: 0.4)" },
|
|
2370
|
+
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." }
|
|
2112
2371
|
},
|
|
2113
2372
|
required: ["three_mf_path", "bambu_model"]
|
|
2114
2373
|
}
|
|
@@ -2236,9 +2495,36 @@ class BambuPrinterMCPServer {
|
|
|
2236
2495
|
});
|
|
2237
2496
|
this.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
2238
2497
|
const { name, arguments: args } = request.params;
|
|
2498
|
+
const sendNativeCommand = (options, execution = {}) => sendCommandWithBambuNative(options, { signal: extra.signal, ...execution });
|
|
2499
|
+
const progressToken = extra._meta?.progressToken;
|
|
2500
|
+
let progressNotifications = Promise.resolve();
|
|
2501
|
+
const reportNativeUpdate = progressToken === undefined
|
|
2502
|
+
? undefined
|
|
2503
|
+
: (line) => {
|
|
2504
|
+
const match = /^native_update status=(-?\d+) code=(-?\d+) msg=(.*)$/.exec(line);
|
|
2505
|
+
if (!match)
|
|
2506
|
+
return;
|
|
2507
|
+
const stage = Number(match[1]);
|
|
2508
|
+
const code = Number(match[2]);
|
|
2509
|
+
const message = match[3];
|
|
2510
|
+
progressNotifications = progressNotifications
|
|
2511
|
+
.then(() => extra.sendNotification({
|
|
2512
|
+
method: "notifications/progress",
|
|
2513
|
+
params: {
|
|
2514
|
+
progressToken,
|
|
2515
|
+
progress: code,
|
|
2516
|
+
total: 100,
|
|
2517
|
+
message: `native_update status=${stage} code=${code} msg=${message}`,
|
|
2518
|
+
},
|
|
2519
|
+
}))
|
|
2520
|
+
.catch(() => { });
|
|
2521
|
+
};
|
|
2239
2522
|
const host = String(args?.host || DEFAULT_HOST);
|
|
2240
2523
|
const bambuSerial = String(args?.bambu_serial || DEFAULT_BAMBU_SERIAL);
|
|
2241
2524
|
const bambuToken = String(args?.bambu_token || DEFAULT_BAMBU_TOKEN);
|
|
2525
|
+
const nativeControls = process.platform === "darwin" &&
|
|
2526
|
+
(bambuSerial.trim().toUpperCase().startsWith("20P") ||
|
|
2527
|
+
(DEFAULT_BAMBU_MODEL === "x2d" && bambuSerial === DEFAULT_BAMBU_SERIAL));
|
|
2242
2528
|
const requestedTemplateDir = typeof args?.template_dir === "string" && args.template_dir.trim().length > 0
|
|
2243
2529
|
? String(args.template_dir)
|
|
2244
2530
|
: undefined;
|
|
@@ -2296,6 +2582,16 @@ class BambuPrinterMCPServer {
|
|
|
2296
2582
|
case "list_printer_files":
|
|
2297
2583
|
result = await this.bambu.getFiles(host, bambuSerial, bambuToken);
|
|
2298
2584
|
break;
|
|
2585
|
+
case "bambu_connect_import_file":
|
|
2586
|
+
if (!args?.file_path) {
|
|
2587
|
+
throw new Error("Missing required parameter: file_path");
|
|
2588
|
+
}
|
|
2589
|
+
result = await importFileViaBambuConnect({
|
|
2590
|
+
filePath: String(args.file_path),
|
|
2591
|
+
name: args?.name !== undefined ? String(args.name) : undefined,
|
|
2592
|
+
version: args?.version !== undefined ? String(args.version) : undefined,
|
|
2593
|
+
});
|
|
2594
|
+
break;
|
|
2299
2595
|
case "bambu_network_bridge_status": {
|
|
2300
2596
|
const bridgeArgs = args;
|
|
2301
2597
|
const options = this.bridgeOptionsFromArgs(bridgeArgs, "bambu_network_bridge_status");
|
|
@@ -2316,6 +2612,10 @@ class BambuPrinterMCPServer {
|
|
|
2316
2612
|
throw new Error("Missing required parameter: method");
|
|
2317
2613
|
}
|
|
2318
2614
|
const bridgeArgs = args;
|
|
2615
|
+
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"]);
|
|
2616
|
+
if (!readMethods.has(String(bridgeArgs.method))) {
|
|
2617
|
+
throw new Error("Raw bridge mutations and unknown methods are disabled. Use print_3mf_bambu_network for verified printing or a dedicated printer tool.");
|
|
2618
|
+
}
|
|
2319
2619
|
const payload = bridgeArgs.payload && typeof bridgeArgs.payload === "object"
|
|
2320
2620
|
? bridgeArgs.payload
|
|
2321
2621
|
: {};
|
|
@@ -2352,7 +2652,8 @@ class BambuPrinterMCPServer {
|
|
|
2352
2652
|
}
|
|
2353
2653
|
const uploadSource = resolveUploadGcodeSource(args);
|
|
2354
2654
|
try {
|
|
2355
|
-
|
|
2655
|
+
const uploadModel = await this.resolveBambuModel(args?.bambu_model);
|
|
2656
|
+
result = await this.bambu.uploadFile(host, bambuSerial, bambuToken, uploadSource.filePath, String(args.filename), false, uploadModel);
|
|
2356
2657
|
}
|
|
2357
2658
|
finally {
|
|
2358
2659
|
if (uploadSource.cleanupDir) {
|
|
@@ -2365,11 +2666,17 @@ class BambuPrinterMCPServer {
|
|
|
2365
2666
|
if (!args?.file_path || !args?.filename) {
|
|
2366
2667
|
throw new Error("Missing required parameters: file_path and filename");
|
|
2367
2668
|
}
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2669
|
+
if (String(args?.connection_mode || "").trim().toLowerCase() === "bambu_native") {
|
|
2670
|
+
result = await this.uploadFileViaBambuNative(args, host, bambuSerial, bambuToken, reportNativeUpdate, extra.signal);
|
|
2671
|
+
}
|
|
2672
|
+
else {
|
|
2673
|
+
const print = Boolean(args.print ?? false);
|
|
2674
|
+
const printableUpload = /\.(?:gcode|3mf)$/i.test(String(args.file_path)) || /\.(?:gcode|3mf)$/i.test(String(args.filename));
|
|
2675
|
+
const uploadModel = print || printableUpload ? await this.resolveBambuModel(args?.bambu_model) : undefined;
|
|
2676
|
+
if (print)
|
|
2677
|
+
assertDirectPrintSupported(uploadModel, bambuSerial);
|
|
2678
|
+
result = await this.bambu.uploadFile(host, bambuSerial, bambuToken, String(args.file_path), String(args.filename), print, uploadModel);
|
|
2679
|
+
}
|
|
2373
2680
|
break;
|
|
2374
2681
|
}
|
|
2375
2682
|
case "start_print":
|
|
@@ -2383,64 +2690,264 @@ class BambuPrinterMCPServer {
|
|
|
2383
2690
|
break;
|
|
2384
2691
|
}
|
|
2385
2692
|
case "cancel_print":
|
|
2386
|
-
result = await this.bambu.cancelJob(host, bambuSerial, bambuToken
|
|
2693
|
+
result = await this.bambu.cancelJob(host, bambuSerial, bambuToken, nativeControls
|
|
2694
|
+
? () => sendNativeCommand({ host, serial: bambuSerial, token: bambuToken,
|
|
2695
|
+
messageJson: JSON.stringify({ print: { command: "stop", param: "", sequence_id: String(Date.now()) } }), qos: 1 })
|
|
2696
|
+
: undefined);
|
|
2387
2697
|
break;
|
|
2388
2698
|
case "pause_print":
|
|
2389
|
-
result = await this.bambu.pauseJob(host, bambuSerial, bambuToken
|
|
2699
|
+
result = await this.bambu.pauseJob(host, bambuSerial, bambuToken, nativeControls
|
|
2700
|
+
? () => sendNativeCommand({ host, serial: bambuSerial, token: bambuToken,
|
|
2701
|
+
messageJson: JSON.stringify({ print: { command: "pause", param: "", sequence_id: String(Date.now()) } }), qos: 1 })
|
|
2702
|
+
: undefined);
|
|
2390
2703
|
break;
|
|
2391
2704
|
case "resume_print":
|
|
2392
|
-
result = await this.bambu.resumeJob(host, bambuSerial, bambuToken
|
|
2705
|
+
result = await this.bambu.resumeJob(host, bambuSerial, bambuToken, nativeControls
|
|
2706
|
+
? (assertActive, beforeDispatch) => sendNativeCommand({ host, serial: bambuSerial, token: bambuToken,
|
|
2707
|
+
messageJson: JSON.stringify({ print: { command: "resume", param: "", sequence_id: String(Date.now()) } }), qos: 1 }, { signal: extra.signal, assertActive, beforeDispatch })
|
|
2708
|
+
: undefined);
|
|
2393
2709
|
break;
|
|
2710
|
+
case "x2d_native_control": {
|
|
2711
|
+
if (!args?.message_json) {
|
|
2712
|
+
throw new Error("Missing required parameter: message_json");
|
|
2713
|
+
}
|
|
2714
|
+
if (!nativeControls) {
|
|
2715
|
+
throw new Error("x2d_native_control is restricted to a selected X2D printer on macOS.");
|
|
2716
|
+
}
|
|
2717
|
+
const messageJson = validateBambuNativeMetadata(String(args.message_json));
|
|
2718
|
+
result = await sendNativeCommand({
|
|
2719
|
+
host,
|
|
2720
|
+
serial: bambuSerial,
|
|
2721
|
+
token: bambuToken,
|
|
2722
|
+
messageJson,
|
|
2723
|
+
qos: args?.qos === undefined ? 0 : Number(args.qos),
|
|
2724
|
+
flag: args?.flag === undefined ? 0 : Number(args.flag),
|
|
2725
|
+
});
|
|
2726
|
+
break;
|
|
2727
|
+
}
|
|
2394
2728
|
case "clear_hms_errors":
|
|
2395
|
-
result = await this.bambu.clearHmsErrors(host, bambuSerial, bambuToken
|
|
2729
|
+
result = await this.bambu.clearHmsErrors(host, bambuSerial, bambuToken, nativeControls
|
|
2730
|
+
? (assertActive, beforeDispatch) => sendNativeCommand({ host, serial: bambuSerial, token: bambuToken,
|
|
2731
|
+
messageJson: JSON.stringify({ print: { command: "clean_print_error", param: "", sequence_id: String(Date.now()) } }), qos: 1 }, { signal: extra.signal, assertActive, beforeDispatch })
|
|
2732
|
+
: undefined);
|
|
2396
2733
|
break;
|
|
2397
2734
|
case "set_print_speed":
|
|
2398
2735
|
if (!args?.mode) {
|
|
2399
2736
|
throw new Error("Missing required parameter: mode");
|
|
2400
2737
|
}
|
|
2401
|
-
|
|
2738
|
+
if (nativeControls) {
|
|
2739
|
+
const normalized = String(args.mode).trim().toLowerCase();
|
|
2740
|
+
const mode = normalized === "silent" ? 1
|
|
2741
|
+
: normalized === "standard" ? 2
|
|
2742
|
+
: normalized === "sport" ? 3
|
|
2743
|
+
: normalized === "ludicrous" ? 4
|
|
2744
|
+
: Number(normalized);
|
|
2745
|
+
if (!Number.isInteger(mode) || mode < 1 || mode > 4) {
|
|
2746
|
+
throw new Error("Print speed mode must be one of: silent, standard, sport, ludicrous, 1, 2, 3, 4.");
|
|
2747
|
+
}
|
|
2748
|
+
result = await sendNativeCommand({
|
|
2749
|
+
host,
|
|
2750
|
+
serial: bambuSerial,
|
|
2751
|
+
token: bambuToken,
|
|
2752
|
+
messageJson: JSON.stringify({
|
|
2753
|
+
print: { command: "print_speed", param: String(mode), sequence_id: String(Date.now()) },
|
|
2754
|
+
}),
|
|
2755
|
+
});
|
|
2756
|
+
}
|
|
2757
|
+
else {
|
|
2758
|
+
result = await this.bambu.setPrintSpeed(host, bambuSerial, bambuToken, String(args.mode));
|
|
2759
|
+
}
|
|
2402
2760
|
break;
|
|
2403
2761
|
case "set_airduct_mode":
|
|
2404
2762
|
if (!args?.mode) {
|
|
2405
2763
|
throw new Error("Missing required parameter: mode");
|
|
2406
2764
|
}
|
|
2407
|
-
|
|
2765
|
+
if (nativeControls) {
|
|
2766
|
+
const mode = String(args.mode).trim().toLowerCase();
|
|
2767
|
+
if (mode !== "cooling" && mode !== "heating") {
|
|
2768
|
+
throw new Error("Airduct mode must be one of: cooling, heating.");
|
|
2769
|
+
}
|
|
2770
|
+
result = await sendNativeCommand({
|
|
2771
|
+
host,
|
|
2772
|
+
serial: bambuSerial,
|
|
2773
|
+
token: bambuToken,
|
|
2774
|
+
messageJson: JSON.stringify({
|
|
2775
|
+
print: {
|
|
2776
|
+
command: "set_airduct",
|
|
2777
|
+
modeId: mode === "cooling" ? 0 : 1,
|
|
2778
|
+
submode: -1,
|
|
2779
|
+
sequence_id: String(Date.now()),
|
|
2780
|
+
},
|
|
2781
|
+
}),
|
|
2782
|
+
});
|
|
2783
|
+
}
|
|
2784
|
+
else {
|
|
2785
|
+
result = await this.bambu.setAirductMode(host, bambuSerial, bambuToken, String(args.mode));
|
|
2786
|
+
}
|
|
2408
2787
|
break;
|
|
2409
2788
|
case "reread_ams_rfid":
|
|
2410
2789
|
if (args?.ams_id === undefined || args?.slot_id === undefined) {
|
|
2411
2790
|
throw new Error("Missing required parameters: ams_id and slot_id");
|
|
2412
2791
|
}
|
|
2413
|
-
|
|
2792
|
+
if (nativeControls) {
|
|
2793
|
+
const amsId = Number(args.ams_id);
|
|
2794
|
+
const slotId = Number(args.slot_id);
|
|
2795
|
+
if ((!Number.isInteger(amsId) || (amsId < 0 || amsId > 3)) && amsId !== 128) {
|
|
2796
|
+
throw new Error("X2D ams_id must be 0 to 3, or 128 for AMS-HT.");
|
|
2797
|
+
}
|
|
2798
|
+
if (!Number.isInteger(slotId) || slotId < 0 || slotId > 3 || (amsId === 128 && slotId !== 0)) {
|
|
2799
|
+
throw new Error("slot_id must be an integer from 0 to 3; AMS-HT supports only slot 0.");
|
|
2800
|
+
}
|
|
2801
|
+
result = await sendNativeCommand({
|
|
2802
|
+
host,
|
|
2803
|
+
serial: bambuSerial,
|
|
2804
|
+
token: bambuToken,
|
|
2805
|
+
messageJson: JSON.stringify({
|
|
2806
|
+
print: {
|
|
2807
|
+
command: "ams_get_rfid",
|
|
2808
|
+
sequence_id: String(Date.now()),
|
|
2809
|
+
ams_id: amsId,
|
|
2810
|
+
slot_id: slotId,
|
|
2811
|
+
},
|
|
2812
|
+
}),
|
|
2813
|
+
});
|
|
2814
|
+
}
|
|
2815
|
+
else {
|
|
2816
|
+
result = await this.bambu.rereadAmsRfid(host, bambuSerial, bambuToken, Number(args.ams_id), Number(args.slot_id));
|
|
2817
|
+
}
|
|
2414
2818
|
break;
|
|
2415
|
-
case "set_temperature":
|
|
2819
|
+
case "set_temperature": {
|
|
2416
2820
|
if (!args?.component || args?.temperature === undefined) {
|
|
2417
2821
|
throw new Error("Missing required parameters: component and temperature");
|
|
2418
2822
|
}
|
|
2419
|
-
|
|
2823
|
+
if (typeof args.temperature !== "number" || !Number.isFinite(args.temperature) || args.temperature < 0) {
|
|
2824
|
+
throw new Error("Temperature must be a finite, non-negative number in °C.");
|
|
2825
|
+
}
|
|
2826
|
+
const heaterModel = args.temperature === 0
|
|
2827
|
+
? String(args.bambu_model || DEFAULT_BAMBU_MODEL).trim().toLowerCase()
|
|
2828
|
+
: await this.resolveBambuModel(args.bambu_model);
|
|
2829
|
+
result = await this.bambu.setTemperature(host, bambuSerial, bambuToken, String(args.component), args.temperature, heaterModel || undefined, args?.material !== undefined ? String(args.material) : undefined, Number(args?.nozzle_diameter ?? DEFAULT_NOZZLE_DIAMETER), (args.temperature === 0 ? nativeControls : heaterModel === "x2d" && process.platform === "darwin")
|
|
2830
|
+
? (heater, target, assertActive, beforeDispatch) => sendNativeCommand({ host, serial: bambuSerial, token: bambuToken,
|
|
2831
|
+
messageJson: buildBambuNativeTemperatureCommand(heater, target).messageJson }, { signal: extra.signal, assertActive, beforeDispatch })
|
|
2832
|
+
: undefined);
|
|
2420
2833
|
break;
|
|
2834
|
+
}
|
|
2421
2835
|
case "set_fan_speed":
|
|
2422
2836
|
if (!args?.fan || args?.speed === undefined) {
|
|
2423
2837
|
throw new Error("Missing required parameters: fan and speed");
|
|
2424
2838
|
}
|
|
2425
|
-
|
|
2839
|
+
if (nativeControls) {
|
|
2840
|
+
const status = await this.bambu.getStatus(host, bambuSerial, bambuToken);
|
|
2841
|
+
const printerState = String(status?.raw?.gcode_state ?? status?.raw?.print?.gcode_state ?? status?.gcode_state ?? status?.status ?? "").toUpperCase();
|
|
2842
|
+
if (printerState === "RUNNING" && args?.confirm_during_print !== true) {
|
|
2843
|
+
throw new Error("The X2D is currently printing. Re-submit with confirm_during_print=true after explicit user confirmation.");
|
|
2844
|
+
}
|
|
2845
|
+
const fanCommand = buildBambuNativeFanCommand(String(args.fan), Number(args.speed));
|
|
2846
|
+
result = {
|
|
2847
|
+
...await sendNativeCommand({
|
|
2848
|
+
host,
|
|
2849
|
+
serial: bambuSerial,
|
|
2850
|
+
token: bambuToken,
|
|
2851
|
+
messageJson: fanCommand.messageJson,
|
|
2852
|
+
}),
|
|
2853
|
+
fan: fanCommand.fan,
|
|
2854
|
+
fan_index: fanCommand.fanIndex,
|
|
2855
|
+
requested_speed: fanCommand.requestedSpeed,
|
|
2856
|
+
speed: fanCommand.speed,
|
|
2857
|
+
};
|
|
2858
|
+
}
|
|
2859
|
+
else {
|
|
2860
|
+
result = await this.bambu.setFanSpeed(host, bambuSerial, bambuToken, String(args.fan), Number(args.speed));
|
|
2861
|
+
}
|
|
2426
2862
|
break;
|
|
2427
2863
|
case "set_light":
|
|
2428
2864
|
if (!args?.light || !args?.mode) {
|
|
2429
2865
|
throw new Error("Missing required parameters: light and mode");
|
|
2430
2866
|
}
|
|
2431
|
-
|
|
2867
|
+
if (nativeControls) {
|
|
2868
|
+
const light = String(args.light).trim();
|
|
2869
|
+
const mode = String(args.mode).trim().toLowerCase();
|
|
2870
|
+
if (light !== "chamber_light" && light !== "chamber_light2") {
|
|
2871
|
+
throw new Error("X2D light must be chamber_light or chamber_light2.");
|
|
2872
|
+
}
|
|
2873
|
+
if (!["on", "off", "flashing"].includes(mode)) {
|
|
2874
|
+
throw new Error("Light mode must be one of: on, off, flashing.");
|
|
2875
|
+
}
|
|
2876
|
+
result = await sendNativeCommand({
|
|
2877
|
+
host,
|
|
2878
|
+
serial: bambuSerial,
|
|
2879
|
+
token: bambuToken,
|
|
2880
|
+
messageJson: JSON.stringify({
|
|
2881
|
+
system: {
|
|
2882
|
+
command: "ledctrl",
|
|
2883
|
+
led_node: light,
|
|
2884
|
+
led_mode: mode,
|
|
2885
|
+
led_on_time: 500,
|
|
2886
|
+
led_off_time: 500,
|
|
2887
|
+
loop_times: 0,
|
|
2888
|
+
interval_time: 0,
|
|
2889
|
+
sequence_id: String(Date.now()),
|
|
2890
|
+
},
|
|
2891
|
+
}),
|
|
2892
|
+
});
|
|
2893
|
+
}
|
|
2894
|
+
else {
|
|
2895
|
+
result = await this.bambu.setLight(host, bambuSerial, bambuToken, String(args.light), String(args.mode));
|
|
2896
|
+
}
|
|
2432
2897
|
break;
|
|
2433
2898
|
case "skip_objects":
|
|
2434
2899
|
if (!Array.isArray(args?.object_ids)) {
|
|
2435
2900
|
throw new Error("Missing required parameter: object_ids");
|
|
2436
2901
|
}
|
|
2437
|
-
|
|
2902
|
+
if (nativeControls) {
|
|
2903
|
+
const objectIds = args.object_ids.map((id) => Number(id));
|
|
2904
|
+
if (objectIds.length === 0 || objectIds.some((id) => !Number.isInteger(id) || id < 0)) {
|
|
2905
|
+
throw new Error("object_ids must contain one or more non-negative integers.");
|
|
2906
|
+
}
|
|
2907
|
+
result = await sendNativeCommand({
|
|
2908
|
+
host,
|
|
2909
|
+
serial: bambuSerial,
|
|
2910
|
+
token: bambuToken,
|
|
2911
|
+
messageJson: JSON.stringify({
|
|
2912
|
+
print: { command: "skip_objects", obj_list: objectIds, sequence_id: String(Date.now()) },
|
|
2913
|
+
}),
|
|
2914
|
+
qos: 1,
|
|
2915
|
+
});
|
|
2916
|
+
}
|
|
2917
|
+
else {
|
|
2918
|
+
result = await this.bambu.skipObjects(host, bambuSerial, bambuToken, args.object_ids.map((id) => Number(id)));
|
|
2919
|
+
}
|
|
2438
2920
|
break;
|
|
2439
2921
|
case "set_ams_drying":
|
|
2440
2922
|
if (!args?.action || args?.ams_id === undefined) {
|
|
2441
2923
|
throw new Error("Missing required parameters: action and ams_id");
|
|
2442
2924
|
}
|
|
2443
|
-
|
|
2925
|
+
if (nativeControls) {
|
|
2926
|
+
const action = String(args.action).trim().toLowerCase();
|
|
2927
|
+
const amsId = Number(args.ams_id);
|
|
2928
|
+
if (action !== "start" && action !== "stop") {
|
|
2929
|
+
throw new Error("AMS drying action must be one of: start, stop.");
|
|
2930
|
+
}
|
|
2931
|
+
if ((!Number.isInteger(amsId) || (amsId < 0 || amsId > 3)) && amsId !== 128) {
|
|
2932
|
+
throw new Error("X2D ams_id must be 0 to 3, or 128 for AMS-HT.");
|
|
2933
|
+
}
|
|
2934
|
+
result = await sendNativeCommand({
|
|
2935
|
+
host,
|
|
2936
|
+
serial: bambuSerial,
|
|
2937
|
+
token: bambuToken,
|
|
2938
|
+
messageJson: JSON.stringify({
|
|
2939
|
+
print: {
|
|
2940
|
+
command: "ams_control",
|
|
2941
|
+
sequence_id: String(Date.now()),
|
|
2942
|
+
ams_id: amsId,
|
|
2943
|
+
param: action === "start" ? "start_drying" : "stop_drying",
|
|
2944
|
+
},
|
|
2945
|
+
}),
|
|
2946
|
+
});
|
|
2947
|
+
}
|
|
2948
|
+
else {
|
|
2949
|
+
result = await this.bambu.setAmsDrying(host, bambuSerial, bambuToken, String(args.action), Number(args.ams_id));
|
|
2950
|
+
}
|
|
2444
2951
|
break;
|
|
2445
2952
|
case "extend_stl_base":
|
|
2446
2953
|
if (!args?.stl_path || args?.extension_height === undefined) {
|
|
@@ -2645,18 +3152,41 @@ class BambuPrinterMCPServer {
|
|
|
2645
3152
|
if (!args?.three_mf_path) {
|
|
2646
3153
|
throw new Error("Missing required parameter: three_mf_path");
|
|
2647
3154
|
}
|
|
2648
|
-
|
|
2649
|
-
|
|
3155
|
+
const printModel = await this.resolveBambuModel(args.bambu_model);
|
|
3156
|
+
const resolvedArgs = { ...args, bambu_model: printModel };
|
|
3157
|
+
const requestedModel = printModel;
|
|
3158
|
+
const defaultConnectionMode = DEFAULT_BAMBU_CONNECTION_MODE ||
|
|
3159
|
+
(requestedModel === "x2d" ? "bambu_native" : "lan_mqtt_ftps");
|
|
3160
|
+
const connectionMode = String(args?.connection_mode || defaultConnectionMode).trim().toLowerCase();
|
|
3161
|
+
if (!["lan_mqtt_ftps", "bambu_native", "bambu_network", "bambu_connect"].includes(connectionMode))
|
|
3162
|
+
throw new Error("Unsupported connection_mode.");
|
|
3163
|
+
// X2D firmware accepts the Bambu Studio :6000/eMMC route but
|
|
3164
|
+
// rejects the legacy direct FTPS STOR operation with 553. Keep
|
|
3165
|
+
// an explicit old-mode request from re-entering that known-bad
|
|
3166
|
+
// path; callers still get a fully scriptable local print.
|
|
3167
|
+
const effectiveConnectionMode = connectionMode === "lan_mqtt_ftps" && requestedModel === "x2d"
|
|
3168
|
+
? "bambu_native"
|
|
3169
|
+
: connectionMode;
|
|
3170
|
+
if (effectiveConnectionMode === "bambu_connect") {
|
|
3171
|
+
result = await this.handoff3mfViaBambuConnect(resolvedArgs);
|
|
3172
|
+
break;
|
|
3173
|
+
}
|
|
3174
|
+
if (effectiveConnectionMode === "bambu_network") {
|
|
3175
|
+
result = await this.print3mfViaBambuNetwork(resolvedArgs, host, bambuSerial, bambuToken);
|
|
3176
|
+
break;
|
|
3177
|
+
}
|
|
3178
|
+
if (effectiveConnectionMode === "bambu_native") {
|
|
3179
|
+
result = await this.print3mfViaBambuNative(resolvedArgs, host, bambuSerial, bambuToken, reportNativeUpdate, extra.signal);
|
|
2650
3180
|
break;
|
|
2651
3181
|
}
|
|
2652
3182
|
if (!bambuSerial || !bambuToken) {
|
|
2653
3183
|
throw new Error("Bambu serial number and access token are required for print_3mf.");
|
|
2654
3184
|
}
|
|
2655
3185
|
const { slicerType, slicerPath, slicerProfile } = this.resolveSlicerConfigFromArgs(args, "print_3mf");
|
|
2656
|
-
const printModel = await this.resolveBambuModel(args?.bambu_model);
|
|
2657
3186
|
assertDirectPrintSupported(printModel, bambuSerial);
|
|
2658
3187
|
const printBedType = resolveBedType(args?.bed_type);
|
|
2659
|
-
const
|
|
3188
|
+
const printNozzles = requestedPrintNozzles(args);
|
|
3189
|
+
const printNozzle = String(printNozzles?.[0] ?? DEFAULT_NOZZLE_DIAMETER);
|
|
2660
3190
|
const activeSlicerProfile = await resolveTemplateFirstSlicerProfilePath(args, slicerProfile || undefined, resolveTemplatePathFromName(["json", "config", "3mf"]) || explicitTemplatePath || undefined, TEMP_DIR);
|
|
2661
3191
|
const explicitSlicerProfile = hasExplicitSlicerProfile(args);
|
|
2662
3192
|
const printPreset = BAMBU_MODEL_PRESETS[printModel]?.(printNozzle);
|
|
@@ -2671,6 +3201,7 @@ class BambuPrinterMCPServer {
|
|
|
2671
3201
|
const zip = await JSZip.loadAsync(zipData);
|
|
2672
3202
|
const hasGcode = Object.values(zip.files).some(entry => !entry.dir && /\.gcode$/i.test(entry.name));
|
|
2673
3203
|
if (!hasGcode) {
|
|
3204
|
+
assertUniformCliNozzles(args);
|
|
2674
3205
|
if (printBedType === "supertack_plate") {
|
|
2675
3206
|
throw new Error('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.');
|
|
2676
3207
|
}
|
|
@@ -2746,6 +3277,7 @@ class BambuPrinterMCPServer {
|
|
|
2746
3277
|
useAMS = finalAmsSlots.length > 0;
|
|
2747
3278
|
}
|
|
2748
3279
|
if (isH2Print &&
|
|
3280
|
+
args?.use_ams !== false &&
|
|
2749
3281
|
!hasUserAmsMapping &&
|
|
2750
3282
|
!hasUserAmsSlots &&
|
|
2751
3283
|
args?.auto_match_ams !== true &&
|
|
@@ -2770,6 +3302,7 @@ class BambuPrinterMCPServer {
|
|
|
2770
3302
|
projectName,
|
|
2771
3303
|
filePath: threeMFPath,
|
|
2772
3304
|
bambuModel: printModel,
|
|
3305
|
+
nozzleDiameters: printNozzles,
|
|
2773
3306
|
plateIndex,
|
|
2774
3307
|
useAMS: useAMS,
|
|
2775
3308
|
amsMapping: finalAmsMapping,
|
|
@@ -2807,6 +3340,7 @@ class BambuPrinterMCPServer {
|
|
|
2807
3340
|
projectName,
|
|
2808
3341
|
filePath: preparedThreeMFPath,
|
|
2809
3342
|
bambuModel: printModel,
|
|
3343
|
+
nozzleDiameters: [Number(printNozzle)],
|
|
2810
3344
|
plateIndex: collarAnalysis.plateIndex,
|
|
2811
3345
|
useAMS: true,
|
|
2812
3346
|
amsSlots: collarAnalysis.amsSlots,
|
|
@@ -2869,6 +3403,7 @@ class BambuPrinterMCPServer {
|
|
|
2869
3403
|
default:
|
|
2870
3404
|
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
|
2871
3405
|
}
|
|
3406
|
+
await progressNotifications;
|
|
2872
3407
|
const text = typeof result === "string" ? result : JSON.stringify(result, null, 2);
|
|
2873
3408
|
if (this.runtimeConfig.enableJsonResponse && typeof result === "object") {
|
|
2874
3409
|
return {
|