bambu-printer-mcp 1.1.13 → 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 +2 -2
- package/README.md +18 -4
- 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/index.js +457 -33
- package/dist/printers/bambu.d.ts +17 -14
- package/dist/printers/bambu.js +102 -56
- 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/index.ts +527 -66
- package/src/printers/bambu.ts +89 -53
package/dist/index.js
CHANGED
|
@@ -11,6 +11,8 @@ 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';
|
|
@@ -45,6 +47,7 @@ if (AUTOMATIC_TEMP_DIR) {
|
|
|
45
47
|
const DEFAULT_BAMBU_MODEL = process.env.BAMBU_PRINTER_MODEL?.trim().toLowerCase() ||
|
|
46
48
|
process.env.BAMBU_MODEL?.trim().toLowerCase() ||
|
|
47
49
|
"";
|
|
50
|
+
const DEFAULT_BAMBU_CONNECTION_MODE = process.env.BAMBU_DEFAULT_CONNECTION_MODE?.trim().toLowerCase() || "";
|
|
48
51
|
const DEFAULT_BED_TYPE = process.env.BED_TYPE?.trim().toLowerCase() || "textured_plate";
|
|
49
52
|
const DEFAULT_NOZZLE_DIAMETER = process.env.NOZZLE_DIAMETER?.trim() || "0.4";
|
|
50
53
|
function requestedPrintNozzles(args) {
|
|
@@ -69,7 +72,10 @@ function assertUniformCliNozzles(args) {
|
|
|
69
72
|
}
|
|
70
73
|
}
|
|
71
74
|
const VALID_BAMBU_MODELS = ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s", "h2c", "x2d"];
|
|
72
|
-
|
|
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"]);
|
|
73
79
|
const VALID_BED_TYPES = ["textured_plate", "cool_plate", "engineering_plate", "hot_plate", "supertack_plate"];
|
|
74
80
|
const VALID_BAMBUSTUDIO_CLI_BED_TYPES = ["textured_plate", "cool_plate", "engineering_plate", "hot_plate"];
|
|
75
81
|
// Map model IDs to BambuStudio --load-machine preset names
|
|
@@ -665,6 +671,19 @@ function stringifyBridgeJson(value) {
|
|
|
665
671
|
return value;
|
|
666
672
|
return JSON.stringify(value);
|
|
667
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
|
+
}
|
|
668
687
|
function redactPrintParams(params) {
|
|
669
688
|
return {
|
|
670
689
|
...params,
|
|
@@ -1014,11 +1033,14 @@ class BambuPrinterMCPServer {
|
|
|
1014
1033
|
useAMS = false;
|
|
1015
1034
|
}
|
|
1016
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
|
+
}
|
|
1017
1039
|
useAMS = false;
|
|
1018
1040
|
}
|
|
1019
1041
|
return { useAMS, finalAmsMapping, finalAmsSlots };
|
|
1020
1042
|
}
|
|
1021
|
-
async print3mfViaBambuNetwork(args, host, bambuSerial, bambuToken) {
|
|
1043
|
+
async print3mfViaBambuNetwork(args, host, bambuSerial, bambuToken, native) {
|
|
1022
1044
|
if (!args?.three_mf_path) {
|
|
1023
1045
|
throw new Error("Missing required parameter: three_mf_path");
|
|
1024
1046
|
}
|
|
@@ -1060,6 +1082,9 @@ class BambuPrinterMCPServer {
|
|
|
1060
1082
|
}
|
|
1061
1083
|
const { threeMFPath, autoSliced } = await this.ensurePrintableThreeMFPath(args, printModel, printPreset, printBedType);
|
|
1062
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
|
+
}
|
|
1063
1088
|
const threeMfFilename = path.basename(threeMFPath);
|
|
1064
1089
|
const projectName = String(args?.project_name || args?.task_name || threeMfFilename.replace(/\.3mf$/i, ''));
|
|
1065
1090
|
const presetName = String(args?.preset_name || `${projectName}_plate_${plateIndex + 1}`);
|
|
@@ -1133,6 +1158,31 @@ class BambuPrinterMCPServer {
|
|
|
1133
1158
|
try_emmc_print: args?.try_emmc_print !== undefined ? Boolean(args.try_emmc_print) : false,
|
|
1134
1159
|
};
|
|
1135
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
|
+
}
|
|
1136
1186
|
const bridgeResult = await this.bambuNetwork.callWithAgent(bridgeMethod, { client_job_id: clientJobId, params }, {
|
|
1137
1187
|
...this.bridgeOptionsFromArgs(args, "print_3mf_bambu_network"),
|
|
1138
1188
|
beforeDispatch: async (method) => {
|
|
@@ -1176,6 +1226,80 @@ class BambuPrinterMCPServer {
|
|
|
1176
1226
|
};
|
|
1177
1227
|
}));
|
|
1178
1228
|
}
|
|
1229
|
+
async handoff3mfViaBambuConnect(args) {
|
|
1230
|
+
if (!args?.three_mf_path) {
|
|
1231
|
+
throw new Error("Missing required parameter: three_mf_path");
|
|
1232
|
+
}
|
|
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.");
|
|
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
|
+
});
|
|
1249
|
+
return {
|
|
1250
|
+
...handoff,
|
|
1251
|
+
autoSliced,
|
|
1252
|
+
plateIndex,
|
|
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.",
|
|
1256
|
+
};
|
|
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
|
+
}
|
|
1179
1303
|
async getResolvedPrinterFilamentInventory(host, bambuSerial, bambuToken, bambuModel, nozzleDiameter) {
|
|
1180
1304
|
const status = await this.bambu.getStatus(host, bambuSerial, bambuToken);
|
|
1181
1305
|
let inventory = normalizePrinterFilamentInventory(status, bambuModel, nozzleDiameter);
|
|
@@ -1736,6 +1860,19 @@ class BambuPrinterMCPServer {
|
|
|
1736
1860
|
}
|
|
1737
1861
|
}
|
|
1738
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
|
+
},
|
|
1739
1876
|
{
|
|
1740
1877
|
name: "bambu_network_bridge_status",
|
|
1741
1878
|
description: "Inspect or probe the FULU OrcaSlicer-bambulab BambuNetwork bridge runtime used for cloud and restored BambuNetwork printing.",
|
|
@@ -1831,7 +1968,7 @@ class BambuPrinterMCPServer {
|
|
|
1831
1968
|
},
|
|
1832
1969
|
{
|
|
1833
1970
|
name: "camera_snapshot",
|
|
1834
|
-
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.",
|
|
1835
1972
|
inputSchema: {
|
|
1836
1973
|
type: "object",
|
|
1837
1974
|
properties: {
|
|
@@ -1839,7 +1976,7 @@ class BambuPrinterMCPServer {
|
|
|
1839
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." },
|
|
1840
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." },
|
|
1841
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." },
|
|
1842
|
-
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)." },
|
|
1843
1980
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
1844
1981
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
1845
1982
|
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
@@ -1893,11 +2030,20 @@ class BambuPrinterMCPServer {
|
|
|
1893
2030
|
file_path: { type: "string", description: "Local path to the file to upload" },
|
|
1894
2031
|
filename: { type: "string", description: "Name for the file on the printer" },
|
|
1895
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
|
+
},
|
|
1896
2038
|
bambu_model: {
|
|
1897
2039
|
type: "string",
|
|
1898
2040
|
enum: [...VALID_BAMBU_MODELS],
|
|
1899
2041
|
description: "Required for every printable .gcode or .3mf upload, including upload-only operations."
|
|
1900
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)." },
|
|
1901
2047
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
1902
2048
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
1903
2049
|
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
@@ -1979,6 +2125,22 @@ class BambuPrinterMCPServer {
|
|
|
1979
2125
|
}
|
|
1980
2126
|
}
|
|
1981
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
|
+
},
|
|
1982
2144
|
{
|
|
1983
2145
|
name: "clear_hms_errors",
|
|
1984
2146
|
description: "Clear HMS or print error state on the Bambu Lab printer using the clean_print_error MQTT command.",
|
|
@@ -2029,11 +2191,11 @@ class BambuPrinterMCPServer {
|
|
|
2029
2191
|
},
|
|
2030
2192
|
{
|
|
2031
2193
|
name: "reread_ams_rfid",
|
|
2032
|
-
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.",
|
|
2033
2195
|
inputSchema: {
|
|
2034
2196
|
type: "object",
|
|
2035
2197
|
properties: {
|
|
2036
|
-
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." },
|
|
2037
2199
|
slot_id: { type: "number", description: "Slot index within that AMS, from 0 to 3" },
|
|
2038
2200
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2039
2201
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
@@ -2062,15 +2224,16 @@ class BambuPrinterMCPServer {
|
|
|
2062
2224
|
},
|
|
2063
2225
|
{
|
|
2064
2226
|
name: "set_fan_speed",
|
|
2065
|
-
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.",
|
|
2066
2228
|
inputSchema: {
|
|
2067
2229
|
type: "object",
|
|
2068
2230
|
properties: {
|
|
2069
2231
|
fan: {
|
|
2070
2232
|
type: "string",
|
|
2071
|
-
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"
|
|
2072
2234
|
},
|
|
2073
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." },
|
|
2074
2237
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2075
2238
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
2076
2239
|
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
@@ -2120,7 +2283,7 @@ class BambuPrinterMCPServer {
|
|
|
2120
2283
|
},
|
|
2121
2284
|
{
|
|
2122
2285
|
name: "set_ams_drying",
|
|
2123
|
-
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.",
|
|
2124
2287
|
inputSchema: {
|
|
2125
2288
|
type: "object",
|
|
2126
2289
|
properties: {
|
|
@@ -2131,7 +2294,7 @@ class BambuPrinterMCPServer {
|
|
|
2131
2294
|
},
|
|
2132
2295
|
ams_id: {
|
|
2133
2296
|
type: "number",
|
|
2134
|
-
description: "AMS unit index from 0 to 3"
|
|
2297
|
+
description: "AMS unit index from 0 to 3, or 128 for an X2D AMS-HT."
|
|
2135
2298
|
},
|
|
2136
2299
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2137
2300
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
@@ -2154,8 +2317,8 @@ class BambuPrinterMCPServer {
|
|
|
2154
2317
|
},
|
|
2155
2318
|
connection_mode: {
|
|
2156
2319
|
type: "string",
|
|
2157
|
-
enum: ["lan_mqtt_ftps", "bambu_network"],
|
|
2158
|
-
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."
|
|
2159
2322
|
},
|
|
2160
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." },
|
|
2161
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." },
|
|
@@ -2332,9 +2495,36 @@ class BambuPrinterMCPServer {
|
|
|
2332
2495
|
});
|
|
2333
2496
|
this.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
2334
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
|
+
};
|
|
2335
2522
|
const host = String(args?.host || DEFAULT_HOST);
|
|
2336
2523
|
const bambuSerial = String(args?.bambu_serial || DEFAULT_BAMBU_SERIAL);
|
|
2337
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));
|
|
2338
2528
|
const requestedTemplateDir = typeof args?.template_dir === "string" && args.template_dir.trim().length > 0
|
|
2339
2529
|
? String(args.template_dir)
|
|
2340
2530
|
: undefined;
|
|
@@ -2392,6 +2582,16 @@ class BambuPrinterMCPServer {
|
|
|
2392
2582
|
case "list_printer_files":
|
|
2393
2583
|
result = await this.bambu.getFiles(host, bambuSerial, bambuToken);
|
|
2394
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;
|
|
2395
2595
|
case "bambu_network_bridge_status": {
|
|
2396
2596
|
const bridgeArgs = args;
|
|
2397
2597
|
const options = this.bridgeOptionsFromArgs(bridgeArgs, "bambu_network_bridge_status");
|
|
@@ -2466,12 +2666,17 @@ class BambuPrinterMCPServer {
|
|
|
2466
2666
|
if (!args?.file_path || !args?.filename) {
|
|
2467
2667
|
throw new Error("Missing required parameters: file_path and filename");
|
|
2468
2668
|
}
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
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
|
+
}
|
|
2475
2680
|
break;
|
|
2476
2681
|
}
|
|
2477
2682
|
case "start_print":
|
|
@@ -2485,34 +2690,131 @@ class BambuPrinterMCPServer {
|
|
|
2485
2690
|
break;
|
|
2486
2691
|
}
|
|
2487
2692
|
case "cancel_print":
|
|
2488
|
-
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);
|
|
2489
2697
|
break;
|
|
2490
2698
|
case "pause_print":
|
|
2491
|
-
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);
|
|
2492
2703
|
break;
|
|
2493
2704
|
case "resume_print":
|
|
2494
|
-
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);
|
|
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
|
+
});
|
|
2495
2726
|
break;
|
|
2727
|
+
}
|
|
2496
2728
|
case "clear_hms_errors":
|
|
2497
|
-
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);
|
|
2498
2733
|
break;
|
|
2499
2734
|
case "set_print_speed":
|
|
2500
2735
|
if (!args?.mode) {
|
|
2501
2736
|
throw new Error("Missing required parameter: mode");
|
|
2502
2737
|
}
|
|
2503
|
-
|
|
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
|
+
}
|
|
2504
2760
|
break;
|
|
2505
2761
|
case "set_airduct_mode":
|
|
2506
2762
|
if (!args?.mode) {
|
|
2507
2763
|
throw new Error("Missing required parameter: mode");
|
|
2508
2764
|
}
|
|
2509
|
-
|
|
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
|
+
}
|
|
2510
2787
|
break;
|
|
2511
2788
|
case "reread_ams_rfid":
|
|
2512
2789
|
if (args?.ams_id === undefined || args?.slot_id === undefined) {
|
|
2513
2790
|
throw new Error("Missing required parameters: ams_id and slot_id");
|
|
2514
2791
|
}
|
|
2515
|
-
|
|
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
|
+
}
|
|
2516
2818
|
break;
|
|
2517
2819
|
case "set_temperature": {
|
|
2518
2820
|
if (!args?.component || args?.temperature === undefined) {
|
|
@@ -2521,32 +2823,131 @@ class BambuPrinterMCPServer {
|
|
|
2521
2823
|
if (typeof args.temperature !== "number" || !Number.isFinite(args.temperature) || args.temperature < 0) {
|
|
2522
2824
|
throw new Error("Temperature must be a finite, non-negative number in °C.");
|
|
2523
2825
|
}
|
|
2524
|
-
|
|
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);
|
|
2525
2833
|
break;
|
|
2526
2834
|
}
|
|
2527
2835
|
case "set_fan_speed":
|
|
2528
2836
|
if (!args?.fan || args?.speed === undefined) {
|
|
2529
2837
|
throw new Error("Missing required parameters: fan and speed");
|
|
2530
2838
|
}
|
|
2531
|
-
|
|
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
|
+
}
|
|
2532
2862
|
break;
|
|
2533
2863
|
case "set_light":
|
|
2534
2864
|
if (!args?.light || !args?.mode) {
|
|
2535
2865
|
throw new Error("Missing required parameters: light and mode");
|
|
2536
2866
|
}
|
|
2537
|
-
|
|
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
|
+
}
|
|
2538
2897
|
break;
|
|
2539
2898
|
case "skip_objects":
|
|
2540
2899
|
if (!Array.isArray(args?.object_ids)) {
|
|
2541
2900
|
throw new Error("Missing required parameter: object_ids");
|
|
2542
2901
|
}
|
|
2543
|
-
|
|
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
|
+
}
|
|
2544
2920
|
break;
|
|
2545
2921
|
case "set_ams_drying":
|
|
2546
2922
|
if (!args?.action || args?.ams_id === undefined) {
|
|
2547
2923
|
throw new Error("Missing required parameters: action and ams_id");
|
|
2548
2924
|
}
|
|
2549
|
-
|
|
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
|
+
}
|
|
2550
2951
|
break;
|
|
2551
2952
|
case "extend_stl_base":
|
|
2552
2953
|
if (!args?.stl_path || args?.extension_height === undefined) {
|
|
@@ -2751,15 +3152,37 @@ class BambuPrinterMCPServer {
|
|
|
2751
3152
|
if (!args?.three_mf_path) {
|
|
2752
3153
|
throw new Error("Missing required parameter: three_mf_path");
|
|
2753
3154
|
}
|
|
2754
|
-
|
|
2755
|
-
|
|
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);
|
|
2756
3180
|
break;
|
|
2757
3181
|
}
|
|
2758
3182
|
if (!bambuSerial || !bambuToken) {
|
|
2759
3183
|
throw new Error("Bambu serial number and access token are required for print_3mf.");
|
|
2760
3184
|
}
|
|
2761
3185
|
const { slicerType, slicerPath, slicerProfile } = this.resolveSlicerConfigFromArgs(args, "print_3mf");
|
|
2762
|
-
const printModel = await this.resolveBambuModel(args?.bambu_model);
|
|
2763
3186
|
assertDirectPrintSupported(printModel, bambuSerial);
|
|
2764
3187
|
const printBedType = resolveBedType(args?.bed_type);
|
|
2765
3188
|
const printNozzles = requestedPrintNozzles(args);
|
|
@@ -2980,6 +3403,7 @@ class BambuPrinterMCPServer {
|
|
|
2980
3403
|
default:
|
|
2981
3404
|
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
|
2982
3405
|
}
|
|
3406
|
+
await progressNotifications;
|
|
2983
3407
|
const text = typeof result === "string" ? result : JSON.stringify(result, null, 2);
|
|
2984
3408
|
if (this.runtimeConfig.enableJsonResponse && typeof result === "object") {
|
|
2985
3409
|
return {
|