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/src/index.ts
CHANGED
|
@@ -25,6 +25,17 @@ import {
|
|
|
25
25
|
type SlicerType,
|
|
26
26
|
} from "./stl/stl-manipulator.js";
|
|
27
27
|
import { BambuNetworkBridge, type BambuNetworkBridgeOptions } from "./bambu-network-bridge.js";
|
|
28
|
+
import {
|
|
29
|
+
assertBambuNativeAvailable,
|
|
30
|
+
validateBambuNativeMetadata,
|
|
31
|
+
buildBambuNativeFanCommand,
|
|
32
|
+
buildBambuNativeTemperatureCommand,
|
|
33
|
+
printWithBambuNative,
|
|
34
|
+
sendCommandWithBambuNative,
|
|
35
|
+
uploadWithBambuNative,
|
|
36
|
+
type BambuNativeUpdateCallback,
|
|
37
|
+
} from "./bambu-native.js";
|
|
38
|
+
import { importFileViaBambuConnect } from "./bambu-connect.js";
|
|
28
39
|
import { BlenderMcpBridge } from "./blender-mcp-bridge.js";
|
|
29
40
|
import { hasAmsMappingInput, normalizeAmsMappingObject, normalizeBridgeAmsTrayValue } from "./ams-mapping.js";
|
|
30
41
|
import { analyze3MFAmsRequirements, analyze3MFPlateObjects, analyzeCollarCharm3MF, extractBambuTemplateSettings, getCollarCharmRolePolicy, parse3MF } from './3mf_parser.js';
|
|
@@ -64,6 +75,8 @@ const DEFAULT_BAMBU_MODEL =
|
|
|
64
75
|
process.env.BAMBU_PRINTER_MODEL?.trim().toLowerCase() ||
|
|
65
76
|
process.env.BAMBU_MODEL?.trim().toLowerCase() ||
|
|
66
77
|
"";
|
|
78
|
+
const DEFAULT_BAMBU_CONNECTION_MODE =
|
|
79
|
+
process.env.BAMBU_DEFAULT_CONNECTION_MODE?.trim().toLowerCase() || "";
|
|
67
80
|
const DEFAULT_BED_TYPE = process.env.BED_TYPE?.trim().toLowerCase() || "textured_plate";
|
|
68
81
|
const DEFAULT_NOZZLE_DIAMETER = process.env.NOZZLE_DIAMETER?.trim() || "0.4";
|
|
69
82
|
|
|
@@ -92,7 +105,10 @@ function assertUniformCliNozzles(args: Record<string, any>): void {
|
|
|
92
105
|
|
|
93
106
|
const VALID_BAMBU_MODELS = ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s", "h2c", "x2d"] as const;
|
|
94
107
|
type BambuModel = typeof VALID_BAMBU_MODELS[number];
|
|
95
|
-
|
|
108
|
+
// X2D uses the same project_file/ams_mapping2 family of LAN print fields as
|
|
109
|
+
// the dual-nozzle H2 path. Keep it in this gate so pre-sliced multi-filament
|
|
110
|
+
// jobs take the correct branch instead of the legacy 5-slot mapping.
|
|
111
|
+
const H2_BAMBU_MODELS = new Set<string>(["h2d", "h2s", "h2c", "x2d"]);
|
|
96
112
|
|
|
97
113
|
const VALID_BED_TYPES = ["textured_plate", "cool_plate", "engineering_plate", "hot_plate", "supertack_plate"] as const;
|
|
98
114
|
const VALID_BAMBUSTUDIO_CLI_BED_TYPES = ["textured_plate", "cool_plate", "engineering_plate", "hot_plate"] as const;
|
|
@@ -908,6 +924,16 @@ function stringifyBridgeJson(value: unknown): string | undefined {
|
|
|
908
924
|
return JSON.stringify(value);
|
|
909
925
|
}
|
|
910
926
|
|
|
927
|
+
function nativeAmsMapping2(mapping: number[] | undefined): string | undefined {
|
|
928
|
+
if (!mapping || mapping.length === 0) return undefined;
|
|
929
|
+
return JSON.stringify(mapping.map((value) => {
|
|
930
|
+
if (value < 0 || value === 255) return { ams_id: 255, slot_id: 255 };
|
|
931
|
+
if (value === 254) return { ams_id: 254, slot_id: 254 };
|
|
932
|
+
if (value >= 128) return { ams_id: 128, slot_id: value - 128 };
|
|
933
|
+
return { ams_id: Math.floor(value / 4), slot_id: value % 4 };
|
|
934
|
+
}));
|
|
935
|
+
}
|
|
936
|
+
|
|
911
937
|
function redactPrintParams(params: Record<string, unknown>): Record<string, unknown> {
|
|
912
938
|
return {
|
|
913
939
|
...params,
|
|
@@ -1382,6 +1408,9 @@ class BambuPrinterMCPServer {
|
|
|
1382
1408
|
useAMS = false;
|
|
1383
1409
|
}
|
|
1384
1410
|
if ((!finalAmsMapping || finalAmsMapping.length === 0) && (!finalAmsSlots || finalAmsSlots.length === 0)) {
|
|
1411
|
+
if (args.use_ams === true || (args.use_ams !== false && (hasUserAmsMapping || hasUserAmsSlots))) {
|
|
1412
|
+
throw new Error("AMS printing requires a nonempty ams_mapping, ams_slots, or auto_match_ams result.");
|
|
1413
|
+
}
|
|
1385
1414
|
useAMS = false;
|
|
1386
1415
|
}
|
|
1387
1416
|
|
|
@@ -1392,7 +1421,8 @@ class BambuPrinterMCPServer {
|
|
|
1392
1421
|
args: Record<string, any>,
|
|
1393
1422
|
host: string,
|
|
1394
1423
|
bambuSerial: string,
|
|
1395
|
-
bambuToken: string
|
|
1424
|
+
bambuToken: string,
|
|
1425
|
+
native?: { onUpdate?: BambuNativeUpdateCallback; signal?: AbortSignal }
|
|
1396
1426
|
): Promise<Record<string, unknown>> {
|
|
1397
1427
|
if (!args?.three_mf_path) {
|
|
1398
1428
|
throw new Error("Missing required parameter: three_mf_path");
|
|
@@ -1458,6 +1488,9 @@ class BambuPrinterMCPServer {
|
|
|
1458
1488
|
printModel,
|
|
1459
1489
|
printNozzle
|
|
1460
1490
|
);
|
|
1491
|
+
if (native && args.use_ams !== false && !useAMS) {
|
|
1492
|
+
throw new Error("Native X2D printing requires ams_mapping, ams_slots, auto_match_ams, or explicit use_ams: false for an external spool.");
|
|
1493
|
+
}
|
|
1461
1494
|
const threeMfFilename = path.basename(threeMFPath);
|
|
1462
1495
|
const projectName = String(args?.project_name || args?.task_name || threeMfFilename.replace(/\.3mf$/i, ''));
|
|
1463
1496
|
const presetName = String(args?.preset_name || `${projectName}_plate_${plateIndex + 1}`);
|
|
@@ -1532,6 +1565,31 @@ class BambuPrinterMCPServer {
|
|
|
1532
1565
|
};
|
|
1533
1566
|
|
|
1534
1567
|
assertActive();
|
|
1568
|
+
if (native) {
|
|
1569
|
+
// Release the telemetry client before the installed plug-in opens its session.
|
|
1570
|
+
await this.bambu.disconnectAll();
|
|
1571
|
+
assertActive();
|
|
1572
|
+
const nativeResult = await printWithBambuNative({
|
|
1573
|
+
host: devIp, serial: devId, token: password || bambuToken, filePath: snapshot,
|
|
1574
|
+
projectName: String(params.task_name), presetName, remoteName: remoteFileName,
|
|
1575
|
+
plateIndex, bedType: printBedType, useAMS,
|
|
1576
|
+
amsMapping, amsMapping2: nativeAmsMapping2(mapping),
|
|
1577
|
+
bedLeveling: params.task_bed_leveling as boolean,
|
|
1578
|
+
flowCalibration: params.task_flow_cali as boolean,
|
|
1579
|
+
vibrationCalibration: params.task_vibration_cali as boolean,
|
|
1580
|
+
layerInspect: params.task_layer_inspect as boolean,
|
|
1581
|
+
timelapse: params.task_record_timelapse as boolean,
|
|
1582
|
+
}, native.onUpdate, { signal: native.signal, assertActive, beforeDispatch: async () => {
|
|
1583
|
+
assertActive();
|
|
1584
|
+
const dispatchStatus = await this.bambu.getSafetyStatus(devIp, devId, password || bambuToken);
|
|
1585
|
+
validatePrinterState(dispatchStatus, requirements);
|
|
1586
|
+
this.bambu.assertBedClearance(dispatchStatus, bedClearance);
|
|
1587
|
+
assertActive();
|
|
1588
|
+
} });
|
|
1589
|
+
assertActive();
|
|
1590
|
+
this.bambu.recordCheckedJob(devIp, devId, remoteFileName, requirements);
|
|
1591
|
+
return { ...nativeResult, autoSliced, projectName, plateIndex, useAMS, amsMapping: mapping };
|
|
1592
|
+
}
|
|
1535
1593
|
const bridgeResult = await this.bambuNetwork.callWithAgent(
|
|
1536
1594
|
bridgeMethod,
|
|
1537
1595
|
{ client_job_id: clientJobId, params },
|
|
@@ -1582,6 +1640,101 @@ class BambuPrinterMCPServer {
|
|
|
1582
1640
|
}));
|
|
1583
1641
|
}
|
|
1584
1642
|
|
|
1643
|
+
private async handoff3mfViaBambuConnect(
|
|
1644
|
+
args: Record<string, any>
|
|
1645
|
+
): Promise<Record<string, unknown>> {
|
|
1646
|
+
if (!args?.three_mf_path) {
|
|
1647
|
+
throw new Error("Missing required parameter: three_mf_path");
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
const printModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
1651
|
+
const printBedType = resolveBedType(args?.bed_type as string | undefined);
|
|
1652
|
+
const printNozzle = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
1653
|
+
const printPreset = BAMBU_MODEL_PRESETS[printModel]?.(printNozzle);
|
|
1654
|
+
const plateIndex = args?.plate_index !== undefined ? Number(args.plate_index) : 0;
|
|
1655
|
+
|
|
1656
|
+
if (!Number.isInteger(plateIndex) || plateIndex < 0) {
|
|
1657
|
+
throw new Error("plate_index must be a non-negative integer.");
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
const { threeMFPath, autoSliced } = await this.ensurePrintableThreeMFPath(
|
|
1661
|
+
args,
|
|
1662
|
+
printModel,
|
|
1663
|
+
printPreset,
|
|
1664
|
+
printBedType
|
|
1665
|
+
);
|
|
1666
|
+
const threeMfFilename = path.basename(threeMFPath);
|
|
1667
|
+
const projectName = String(
|
|
1668
|
+
args?.project_name || threeMfFilename.replace(/\.3mf$/i, "")
|
|
1669
|
+
);
|
|
1670
|
+
const handoff = await importFileViaBambuConnect({
|
|
1671
|
+
filePath: threeMFPath,
|
|
1672
|
+
name: projectName,
|
|
1673
|
+
version: args?.version !== undefined ? String(args.version) : undefined,
|
|
1674
|
+
});
|
|
1675
|
+
|
|
1676
|
+
return {
|
|
1677
|
+
...handoff,
|
|
1678
|
+
autoSliced,
|
|
1679
|
+
plateIndex,
|
|
1680
|
+
printer_model: printModel,
|
|
1681
|
+
bed_type: printBedType,
|
|
1682
|
+
note: "The printable file was handed to Bambu Connect. Check the selected printer and plate in Bambu Connect before starting; no print command was sent by this MCP route.",
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
private async print3mfViaBambuNative(
|
|
1687
|
+
args: Record<string, any>, host: string, bambuSerial: string, bambuToken: string,
|
|
1688
|
+
onUpdate?: BambuNativeUpdateCallback, signal?: AbortSignal
|
|
1689
|
+
): Promise<Record<string, unknown>> {
|
|
1690
|
+
assertBambuNativeAvailable();
|
|
1691
|
+
const model = await this.resolveBambuModel(args.bambu_model);
|
|
1692
|
+
if (model !== "x2d") throw new Error("connection_mode=bambu_native is restricted to X2D.");
|
|
1693
|
+
return this.print3mfViaBambuNetwork(
|
|
1694
|
+
{ ...args, bambu_model: model, connection_type: "lan", bambu_network_method: "start_local_print" },
|
|
1695
|
+
host, bambuSerial, bambuToken, { onUpdate, signal }
|
|
1696
|
+
);
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
private async uploadFileViaBambuNative(
|
|
1700
|
+
args: Record<string, any>, host: string, bambuSerial: string, bambuToken: string,
|
|
1701
|
+
onUpdate?: BambuNativeUpdateCallback, signal?: AbortSignal
|
|
1702
|
+
): Promise<Record<string, unknown>> {
|
|
1703
|
+
assertBambuNativeAvailable();
|
|
1704
|
+
if (args.print) throw new Error("Native upload never starts printing; use print_3mf.");
|
|
1705
|
+
const model = await this.resolveBambuModel(args.bambu_model);
|
|
1706
|
+
if (model !== "x2d") throw new Error("connection_mode=bambu_native is restricted to X2D.");
|
|
1707
|
+
const unsupported = ["use_ams", "ams_mapping", "ams_mapping2", "ams_mapping_info", "nozzle_mapping", "nozzles_info",
|
|
1708
|
+
"bed_leveling", "flow_calibration", "vibration_calibration", "layer_inspect", "timelapse"];
|
|
1709
|
+
for (const key of unsupported) {
|
|
1710
|
+
if (args[key] !== undefined) throw new Error(`${key} is not supported for native upload-only requests; use print_3mf for checked print settings.`);
|
|
1711
|
+
}
|
|
1712
|
+
for (const key of ["project_name", "preset_name"]) {
|
|
1713
|
+
if (args[key] !== undefined && (typeof args[key] !== "string" || !args[key].trim())) {
|
|
1714
|
+
throw new Error(`${key} must be a non-empty string.`);
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
const plateIndex = args.plate_index ?? 0;
|
|
1718
|
+
if (!Number.isSafeInteger(plateIndex) || plateIndex < 0) throw new Error("plate_index must be a non-negative integer.");
|
|
1719
|
+
if (path.extname(String(args.file_path)).toLowerCase() !== ".3mf" && plateIndex !== 0) {
|
|
1720
|
+
throw new Error("plate_index is only supported for 3MF uploads.");
|
|
1721
|
+
}
|
|
1722
|
+
const bedType = resolveBedType(args.bed_type ?? "textured_plate");
|
|
1723
|
+
return this.bambu.uploadFile(host, bambuSerial, bambuToken, String(args.file_path), String(args.filename), false, model,
|
|
1724
|
+
async (snapshot, destination, assertActive) => {
|
|
1725
|
+
if (path.extname(snapshot).toLowerCase() === ".3mf") {
|
|
1726
|
+
await inspectPrintFile(snapshot, { model, plateIndex, bedType });
|
|
1727
|
+
}
|
|
1728
|
+
assertActive();
|
|
1729
|
+
await this.bambu.disconnectAll();
|
|
1730
|
+
await uploadWithBambuNative({
|
|
1731
|
+
host, serial: bambuSerial, token: bambuToken, filePath: snapshot,
|
|
1732
|
+
remoteName: destination, projectName: args.project_name ?? path.posix.basename(String(args.filename)),
|
|
1733
|
+
presetName: args.preset_name ?? "checked-upload", plateIndex, bedType, useAMS: false,
|
|
1734
|
+
}, onUpdate, { signal, assertActive });
|
|
1735
|
+
});
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1585
1738
|
private async getResolvedPrinterFilamentInventory(
|
|
1586
1739
|
host: string,
|
|
1587
1740
|
bambuSerial: string,
|
|
@@ -2228,6 +2381,19 @@ class BambuPrinterMCPServer {
|
|
|
2228
2381
|
}
|
|
2229
2382
|
}
|
|
2230
2383
|
},
|
|
2384
|
+
{
|
|
2385
|
+
name: "bambu_connect_import_file",
|
|
2386
|
+
description: "Open an existing G-code or sliced 3MF file in the signed-in Bambu Connect app through its official URL scheme. This is a cloud-mode handoff and does not start printing by itself.",
|
|
2387
|
+
inputSchema: {
|
|
2388
|
+
type: "object",
|
|
2389
|
+
properties: {
|
|
2390
|
+
file_path: { type: "string", description: "Readable local path to a .gcode, .3mf, or .gcode.3mf file" },
|
|
2391
|
+
name: { type: "string", description: "Optional project name shown by Bambu Connect" },
|
|
2392
|
+
version: { type: "string", description: "Optional integration version sent to Bambu Connect (default: 1.0.0)" }
|
|
2393
|
+
},
|
|
2394
|
+
required: ["file_path"]
|
|
2395
|
+
}
|
|
2396
|
+
},
|
|
2231
2397
|
{
|
|
2232
2398
|
name: "bambu_network_bridge_status",
|
|
2233
2399
|
description: "Inspect or probe the FULU OrcaSlicer-bambulab BambuNetwork bridge runtime used for cloud and restored BambuNetwork printing.",
|
|
@@ -2323,7 +2489,7 @@ class BambuPrinterMCPServer {
|
|
|
2323
2489
|
},
|
|
2324
2490
|
{
|
|
2325
2491
|
name: "camera_snapshot",
|
|
2326
|
-
description: "Capture a single JPEG frame from the printer's chamber camera. A1/P1 use TCP-on-6000; X1/P2S/H2 use RTSP via ffmpeg. Returns JPEG as base64; pass save_path to also write the bytes to disk.",
|
|
2492
|
+
description: "Capture a single JPEG frame from the printer's chamber camera. A1/P1 use TCP-on-6000; X1/P2S/H2/X2D use RTSP via ffmpeg. Returns JPEG as base64; pass save_path to also write the bytes to disk.",
|
|
2327
2493
|
inputSchema: {
|
|
2328
2494
|
type: "object",
|
|
2329
2495
|
properties: {
|
|
@@ -2331,7 +2497,7 @@ class BambuPrinterMCPServer {
|
|
|
2331
2497
|
timeout_ms: { type: "number", description: "Max ms to wait for a full frame (default 8000). Camera may take a few seconds on cold start." },
|
|
2332
2498
|
bambu_model: { type: "string", description: "Printer model. Used to route to the correct protocol or fail fast on unsupported models. Defaults to BAMBU_MODEL." },
|
|
2333
2499
|
experimental: { type: "boolean", description: "Deprecated and ignored. Earlier this flag let callers probe H2 series via the A1/P1 TCP-on-6000 path; live testing on an H2S confirmed H2 uses RTSP instead, so the flag has no effect now." },
|
|
2334
|
-
ffmpeg_path: { type: "string", description: "Override path to the ffmpeg binary used by the RTSP path. Defaults to FFMPEG_PATH or ffmpeg via $PATH. Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1. Required only for the RTSP transport (X1, P2S, H2 series)." },
|
|
2500
|
+
ffmpeg_path: { type: "string", description: "Override path to the ffmpeg binary used by the RTSP path. Defaults to FFMPEG_PATH or ffmpeg via $PATH. Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1. Required only for the RTSP transport (X1, P2S, H2 series, X2D)." },
|
|
2335
2501
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2336
2502
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
2337
2503
|
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
@@ -2385,11 +2551,20 @@ class BambuPrinterMCPServer {
|
|
|
2385
2551
|
file_path: { type: "string", description: "Local path to the file to upload" },
|
|
2386
2552
|
filename: { type: "string", description: "Name for the file on the printer" },
|
|
2387
2553
|
print: { type: "boolean", description: "Start printing after upload (default: false)" },
|
|
2554
|
+
connection_mode: {
|
|
2555
|
+
type: "string",
|
|
2556
|
+
enum: ["lan_mqtt_ftps", "bambu_native"],
|
|
2557
|
+
description: "Upload transport; bambu_native uses the installed Bambu networking plug-in for X2D and never starts printing."
|
|
2558
|
+
},
|
|
2388
2559
|
bambu_model: {
|
|
2389
2560
|
type: "string",
|
|
2390
2561
|
enum: [...VALID_BAMBU_MODELS],
|
|
2391
2562
|
description: "Required for every printable .gcode or .3mf upload, including upload-only operations."
|
|
2392
2563
|
},
|
|
2564
|
+
project_name: { type: "string", description: "Optional project name passed to the native X2D uploader; defaults to filename." },
|
|
2565
|
+
preset_name: { type: "string", description: "Optional printer preset name passed to the native X2D uploader." },
|
|
2566
|
+
bed_type: { type: "string", enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate", "supertack_plate"], description: "Bed plate type for the native X2D uploader (default: textured_plate)." },
|
|
2567
|
+
plate_index: { type: "integer", minimum: 0, description: "Zero-based plate index passed to the native X2D uploader (default: 0)." },
|
|
2393
2568
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2394
2569
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
2395
2570
|
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
@@ -2471,6 +2646,22 @@ class BambuPrinterMCPServer {
|
|
|
2471
2646
|
}
|
|
2472
2647
|
}
|
|
2473
2648
|
},
|
|
2649
|
+
{
|
|
2650
|
+
name: "x2d_native_control",
|
|
2651
|
+
description: "Read calibration results or update X2D AMS metadata/calibration selection through the installed plug-in. Raw motion, heating, task control, and safety-setting changes are rejected.",
|
|
2652
|
+
inputSchema: {
|
|
2653
|
+
type: "object",
|
|
2654
|
+
properties: {
|
|
2655
|
+
message_json: { type: "string", description: "JSON print envelope for ams_filament_setting, extrusion_cali_sel, extrusion_cali_get, extrusion_cali_get_result, or flowrate_get_result. A sequence_id is required; extra fields are rejected." },
|
|
2656
|
+
qos: { type: "number", description: "MQTT QoS used by Bambu Studio (0 or 1; default 0)." },
|
|
2657
|
+
flag: { type: "number", description: "Bambu networking plug-in command flag (0 or 1; default 0)." },
|
|
2658
|
+
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2659
|
+
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
2660
|
+
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
2661
|
+
},
|
|
2662
|
+
required: ["message_json"]
|
|
2663
|
+
}
|
|
2664
|
+
},
|
|
2474
2665
|
{
|
|
2475
2666
|
name: "clear_hms_errors",
|
|
2476
2667
|
description: "Clear HMS or print error state on the Bambu Lab printer using the clean_print_error MQTT command.",
|
|
@@ -2521,11 +2712,11 @@ class BambuPrinterMCPServer {
|
|
|
2521
2712
|
},
|
|
2522
2713
|
{
|
|
2523
2714
|
name: "reread_ams_rfid",
|
|
2524
|
-
description: "Trigger a Bambu AMS RFID re-read for one AMS slot. This can move AMS filament; use only when the printer is idle and unloaded.",
|
|
2715
|
+
description: "Trigger a Bambu AMS RFID re-read for one AMS slot. X2D on macOS uses the native official plug-in route. This can move AMS filament; use only when the printer is idle and unloaded.",
|
|
2525
2716
|
inputSchema: {
|
|
2526
2717
|
type: "object",
|
|
2527
2718
|
properties: {
|
|
2528
|
-
ams_id: { type: "number", description: "AMS unit index from 0 to 3" },
|
|
2719
|
+
ams_id: { type: "number", description: "AMS unit index from 0 to 3, or 128 for an X2D AMS-HT." },
|
|
2529
2720
|
slot_id: { type: "number", description: "Slot index within that AMS, from 0 to 3" },
|
|
2530
2721
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2531
2722
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
@@ -2554,15 +2745,16 @@ class BambuPrinterMCPServer {
|
|
|
2554
2745
|
},
|
|
2555
2746
|
{
|
|
2556
2747
|
name: "set_fan_speed",
|
|
2557
|
-
description: "Set a Bambu printer fan speed percentage
|
|
2748
|
+
description: "Set a Bambu printer fan speed percentage. X2D on macOS uses the installed Bambu networking plug-in without UI automation; other models use MQTT.",
|
|
2558
2749
|
inputSchema: {
|
|
2559
2750
|
type: "object",
|
|
2560
2751
|
properties: {
|
|
2561
2752
|
fan: {
|
|
2562
2753
|
type: "string",
|
|
2563
|
-
description: "Fan to control: part, auxiliary, chamber, 1, 2, or
|
|
2754
|
+
description: "Fan to control: part, auxiliary, right_auxiliary, chamber, 1, 2, 3, or 10"
|
|
2564
2755
|
},
|
|
2565
2756
|
speed: { type: "number", description: "Fan speed percentage from 0 to 100" },
|
|
2757
|
+
confirm_during_print: { type: "boolean", description: "Required for X2D fan changes while a print is running." },
|
|
2566
2758
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2567
2759
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
2568
2760
|
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
@@ -2612,7 +2804,7 @@ class BambuPrinterMCPServer {
|
|
|
2612
2804
|
},
|
|
2613
2805
|
{
|
|
2614
2806
|
name: "set_ams_drying",
|
|
2615
|
-
description: "Start or stop the AMS filament drying cycle.
|
|
2807
|
+
description: "Start or stop the AMS filament drying cycle. X2D on macOS uses the native official plug-in route and supports AMS-HT id 128.",
|
|
2616
2808
|
inputSchema: {
|
|
2617
2809
|
type: "object",
|
|
2618
2810
|
properties: {
|
|
@@ -2623,7 +2815,7 @@ class BambuPrinterMCPServer {
|
|
|
2623
2815
|
},
|
|
2624
2816
|
ams_id: {
|
|
2625
2817
|
type: "number",
|
|
2626
|
-
description: "AMS unit index from 0 to 3"
|
|
2818
|
+
description: "AMS unit index from 0 to 3, or 128 for an X2D AMS-HT."
|
|
2627
2819
|
},
|
|
2628
2820
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2629
2821
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
@@ -2646,8 +2838,8 @@ class BambuPrinterMCPServer {
|
|
|
2646
2838
|
},
|
|
2647
2839
|
connection_mode: {
|
|
2648
2840
|
type: "string",
|
|
2649
|
-
enum: ["lan_mqtt_ftps", "bambu_network"],
|
|
2650
|
-
description: "Print path to
|
|
2841
|
+
enum: ["lan_mqtt_ftps", "bambu_network", "bambu_native", "bambu_connect"],
|
|
2842
|
+
description: "Print path: bambu_connect hands a printable file to the signed-in Bambu Connect app for cloud-mode review; X2D otherwise defaults to bambu_native (Bambu Studio's installed local networking plug-in and emmc tunnel); lan_mqtt_ftps is the legacy direct MQTT/FTPS path; bambu_network uses the FULU bridge."
|
|
2651
2843
|
},
|
|
2652
2844
|
connection_type: { type: "string", enum: ["cloud", "lan"], description: "BambuNetwork connection type when connection_mode is bambu_network; cloud uses restored internet printing, lan uses local bridge printing." },
|
|
2653
2845
|
bambu_network_method: { type: "string", enum: BAMBU_NETWORK_PRINT_METHODS, description: "FULU print method when connection_mode is bambu_network; defaults to start_print for cloud and start_local_print for lan." },
|
|
@@ -2825,10 +3017,41 @@ class BambuPrinterMCPServer {
|
|
|
2825
3017
|
|
|
2826
3018
|
this.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
2827
3019
|
const { name, arguments: args } = request.params;
|
|
3020
|
+
const sendNativeCommand = (
|
|
3021
|
+
options: Parameters<typeof sendCommandWithBambuNative>[0],
|
|
3022
|
+
execution: Parameters<typeof sendCommandWithBambuNative>[1] = {}
|
|
3023
|
+
) => sendCommandWithBambuNative(options, { signal: extra.signal, ...execution });
|
|
3024
|
+
|
|
3025
|
+
const progressToken = extra._meta?.progressToken;
|
|
3026
|
+
let progressNotifications = Promise.resolve();
|
|
3027
|
+
const reportNativeUpdate: BambuNativeUpdateCallback | undefined = progressToken === undefined
|
|
3028
|
+
? undefined
|
|
3029
|
+
: (line) => {
|
|
3030
|
+
const match = /^native_update status=(-?\d+) code=(-?\d+) msg=(.*)$/.exec(line);
|
|
3031
|
+
if (!match) return;
|
|
3032
|
+
|
|
3033
|
+
const stage = Number(match[1]);
|
|
3034
|
+
const code = Number(match[2]);
|
|
3035
|
+
const message = match[3];
|
|
3036
|
+
progressNotifications = progressNotifications
|
|
3037
|
+
.then(() => extra.sendNotification({
|
|
3038
|
+
method: "notifications/progress",
|
|
3039
|
+
params: {
|
|
3040
|
+
progressToken,
|
|
3041
|
+
progress: code,
|
|
3042
|
+
total: 100,
|
|
3043
|
+
message: `native_update status=${stage} code=${code} msg=${message}`,
|
|
3044
|
+
},
|
|
3045
|
+
}))
|
|
3046
|
+
.catch(() => {});
|
|
3047
|
+
};
|
|
2828
3048
|
|
|
2829
3049
|
const host = String(args?.host || DEFAULT_HOST);
|
|
2830
3050
|
const bambuSerial = String(args?.bambu_serial || DEFAULT_BAMBU_SERIAL);
|
|
2831
3051
|
const bambuToken = String(args?.bambu_token || DEFAULT_BAMBU_TOKEN);
|
|
3052
|
+
const nativeControls = process.platform === "darwin" &&
|
|
3053
|
+
(bambuSerial.trim().toUpperCase().startsWith("20P") ||
|
|
3054
|
+
(DEFAULT_BAMBU_MODEL === "x2d" && bambuSerial === DEFAULT_BAMBU_SERIAL));
|
|
2832
3055
|
const requestedTemplateDir =
|
|
2833
3056
|
typeof args?.template_dir === "string" && args.template_dir.trim().length > 0
|
|
2834
3057
|
? String(args.template_dir)
|
|
@@ -2920,6 +3143,17 @@ class BambuPrinterMCPServer {
|
|
|
2920
3143
|
result = await this.bambu.getFiles(host, bambuSerial, bambuToken);
|
|
2921
3144
|
break;
|
|
2922
3145
|
|
|
3146
|
+
case "bambu_connect_import_file":
|
|
3147
|
+
if (!args?.file_path) {
|
|
3148
|
+
throw new Error("Missing required parameter: file_path");
|
|
3149
|
+
}
|
|
3150
|
+
result = await importFileViaBambuConnect({
|
|
3151
|
+
filePath: String(args.file_path),
|
|
3152
|
+
name: args?.name !== undefined ? String(args.name) : undefined,
|
|
3153
|
+
version: args?.version !== undefined ? String(args.version) : undefined,
|
|
3154
|
+
});
|
|
3155
|
+
break;
|
|
3156
|
+
|
|
2923
3157
|
case "bambu_network_bridge_status": {
|
|
2924
3158
|
const bridgeArgs = args as Record<string, any> | undefined;
|
|
2925
3159
|
const options = this.bridgeOptionsFromArgs(
|
|
@@ -3014,15 +3248,21 @@ class BambuPrinterMCPServer {
|
|
|
3014
3248
|
if (!args?.file_path || !args?.filename) {
|
|
3015
3249
|
throw new Error("Missing required parameters: file_path and filename");
|
|
3016
3250
|
}
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
String(args.file_path)
|
|
3024
|
-
print
|
|
3025
|
-
|
|
3251
|
+
if (String(args?.connection_mode || "").trim().toLowerCase() === "bambu_native") {
|
|
3252
|
+
result = await this.uploadFileViaBambuNative(
|
|
3253
|
+
args as Record<string, any>, host, bambuSerial, bambuToken, reportNativeUpdate, extra.signal
|
|
3254
|
+
);
|
|
3255
|
+
} else {
|
|
3256
|
+
const print = Boolean(args.print ?? false);
|
|
3257
|
+
const printableUpload = /\.(?:gcode|3mf)$/i.test(String(args.file_path)) || /\.(?:gcode|3mf)$/i.test(String(args.filename));
|
|
3258
|
+
const uploadModel = print || printableUpload ? await this.resolveBambuModel(args?.bambu_model as string | undefined) : undefined;
|
|
3259
|
+
if (print) assertDirectPrintSupported(uploadModel, bambuSerial);
|
|
3260
|
+
result = await this.bambu.uploadFile(
|
|
3261
|
+
host, bambuSerial, bambuToken,
|
|
3262
|
+
String(args.file_path), String(args.filename),
|
|
3263
|
+
print, uploadModel
|
|
3264
|
+
);
|
|
3265
|
+
}
|
|
3026
3266
|
break;
|
|
3027
3267
|
}
|
|
3028
3268
|
|
|
@@ -3038,46 +3278,145 @@ class BambuPrinterMCPServer {
|
|
|
3038
3278
|
}
|
|
3039
3279
|
|
|
3040
3280
|
case "cancel_print":
|
|
3041
|
-
result = await this.bambu.cancelJob(host, bambuSerial, bambuToken
|
|
3281
|
+
result = await this.bambu.cancelJob(host, bambuSerial, bambuToken,
|
|
3282
|
+
nativeControls
|
|
3283
|
+
? () => sendNativeCommand({ host, serial: bambuSerial, token: bambuToken,
|
|
3284
|
+
messageJson: JSON.stringify({ print: { command: "stop", param: "", sequence_id: String(Date.now()) } }), qos: 1 })
|
|
3285
|
+
: undefined);
|
|
3042
3286
|
break;
|
|
3043
3287
|
|
|
3044
3288
|
case "pause_print":
|
|
3045
|
-
result = await this.bambu.pauseJob(host, bambuSerial, bambuToken
|
|
3289
|
+
result = await this.bambu.pauseJob(host, bambuSerial, bambuToken,
|
|
3290
|
+
nativeControls
|
|
3291
|
+
? () => sendNativeCommand({ host, serial: bambuSerial, token: bambuToken,
|
|
3292
|
+
messageJson: JSON.stringify({ print: { command: "pause", param: "", sequence_id: String(Date.now()) } }), qos: 1 })
|
|
3293
|
+
: undefined);
|
|
3046
3294
|
break;
|
|
3047
3295
|
|
|
3048
3296
|
case "resume_print":
|
|
3049
|
-
result = await this.bambu.resumeJob(host, bambuSerial, bambuToken
|
|
3297
|
+
result = await this.bambu.resumeJob(host, bambuSerial, bambuToken,
|
|
3298
|
+
nativeControls
|
|
3299
|
+
? (assertActive, beforeDispatch) => sendNativeCommand({ host, serial: bambuSerial, token: bambuToken,
|
|
3300
|
+
messageJson: JSON.stringify({ print: { command: "resume", param: "", sequence_id: String(Date.now()) } }), qos: 1 }, { signal: extra.signal, assertActive, beforeDispatch })
|
|
3301
|
+
: undefined);
|
|
3302
|
+
break;
|
|
3303
|
+
|
|
3304
|
+
case "x2d_native_control": {
|
|
3305
|
+
if (!args?.message_json) {
|
|
3306
|
+
throw new Error("Missing required parameter: message_json");
|
|
3307
|
+
}
|
|
3308
|
+
if (!nativeControls) {
|
|
3309
|
+
throw new Error("x2d_native_control is restricted to a selected X2D printer on macOS.");
|
|
3310
|
+
}
|
|
3311
|
+
const messageJson = validateBambuNativeMetadata(String(args.message_json));
|
|
3312
|
+
result = await sendNativeCommand({
|
|
3313
|
+
host,
|
|
3314
|
+
serial: bambuSerial,
|
|
3315
|
+
token: bambuToken,
|
|
3316
|
+
messageJson,
|
|
3317
|
+
qos: args?.qos === undefined ? 0 : Number(args.qos),
|
|
3318
|
+
flag: args?.flag === undefined ? 0 : Number(args.flag),
|
|
3319
|
+
});
|
|
3050
3320
|
break;
|
|
3321
|
+
}
|
|
3051
3322
|
|
|
3052
3323
|
case "clear_hms_errors":
|
|
3053
|
-
result = await this.bambu.clearHmsErrors(host, bambuSerial, bambuToken
|
|
3324
|
+
result = await this.bambu.clearHmsErrors(host, bambuSerial, bambuToken,
|
|
3325
|
+
nativeControls
|
|
3326
|
+
? (assertActive, beforeDispatch) => sendNativeCommand({ host, serial: bambuSerial, token: bambuToken,
|
|
3327
|
+
messageJson: JSON.stringify({ print: { command: "clean_print_error", param: "", sequence_id: String(Date.now()) } }), qos: 1 }, { signal: extra.signal, assertActive, beforeDispatch })
|
|
3328
|
+
: undefined);
|
|
3054
3329
|
break;
|
|
3055
3330
|
|
|
3056
3331
|
case "set_print_speed":
|
|
3057
3332
|
if (!args?.mode) {
|
|
3058
3333
|
throw new Error("Missing required parameter: mode");
|
|
3059
3334
|
}
|
|
3060
|
-
|
|
3335
|
+
if (nativeControls) {
|
|
3336
|
+
const normalized = String(args.mode).trim().toLowerCase();
|
|
3337
|
+
const mode = normalized === "silent" ? 1
|
|
3338
|
+
: normalized === "standard" ? 2
|
|
3339
|
+
: normalized === "sport" ? 3
|
|
3340
|
+
: normalized === "ludicrous" ? 4
|
|
3341
|
+
: Number(normalized);
|
|
3342
|
+
if (!Number.isInteger(mode) || mode < 1 || mode > 4) {
|
|
3343
|
+
throw new Error("Print speed mode must be one of: silent, standard, sport, ludicrous, 1, 2, 3, 4.");
|
|
3344
|
+
}
|
|
3345
|
+
result = await sendNativeCommand({
|
|
3346
|
+
host,
|
|
3347
|
+
serial: bambuSerial,
|
|
3348
|
+
token: bambuToken,
|
|
3349
|
+
messageJson: JSON.stringify({
|
|
3350
|
+
print: { command: "print_speed", param: String(mode), sequence_id: String(Date.now()) },
|
|
3351
|
+
}),
|
|
3352
|
+
});
|
|
3353
|
+
} else {
|
|
3354
|
+
result = await this.bambu.setPrintSpeed(host, bambuSerial, bambuToken, String(args.mode));
|
|
3355
|
+
}
|
|
3061
3356
|
break;
|
|
3062
3357
|
|
|
3063
3358
|
case "set_airduct_mode":
|
|
3064
3359
|
if (!args?.mode) {
|
|
3065
3360
|
throw new Error("Missing required parameter: mode");
|
|
3066
3361
|
}
|
|
3067
|
-
|
|
3362
|
+
if (nativeControls) {
|
|
3363
|
+
const mode = String(args.mode).trim().toLowerCase();
|
|
3364
|
+
if (mode !== "cooling" && mode !== "heating") {
|
|
3365
|
+
throw new Error("Airduct mode must be one of: cooling, heating.");
|
|
3366
|
+
}
|
|
3367
|
+
result = await sendNativeCommand({
|
|
3368
|
+
host,
|
|
3369
|
+
serial: bambuSerial,
|
|
3370
|
+
token: bambuToken,
|
|
3371
|
+
messageJson: JSON.stringify({
|
|
3372
|
+
print: {
|
|
3373
|
+
command: "set_airduct",
|
|
3374
|
+
modeId: mode === "cooling" ? 0 : 1,
|
|
3375
|
+
submode: -1,
|
|
3376
|
+
sequence_id: String(Date.now()),
|
|
3377
|
+
},
|
|
3378
|
+
}),
|
|
3379
|
+
});
|
|
3380
|
+
} else {
|
|
3381
|
+
result = await this.bambu.setAirductMode(host, bambuSerial, bambuToken, String(args.mode));
|
|
3382
|
+
}
|
|
3068
3383
|
break;
|
|
3069
3384
|
|
|
3070
3385
|
case "reread_ams_rfid":
|
|
3071
3386
|
if (args?.ams_id === undefined || args?.slot_id === undefined) {
|
|
3072
3387
|
throw new Error("Missing required parameters: ams_id and slot_id");
|
|
3073
3388
|
}
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3389
|
+
if (nativeControls) {
|
|
3390
|
+
const amsId = Number(args.ams_id);
|
|
3391
|
+
const slotId = Number(args.slot_id);
|
|
3392
|
+
if ((!Number.isInteger(amsId) || (amsId < 0 || amsId > 3)) && amsId !== 128) {
|
|
3393
|
+
throw new Error("X2D ams_id must be 0 to 3, or 128 for AMS-HT.");
|
|
3394
|
+
}
|
|
3395
|
+
if (!Number.isInteger(slotId) || slotId < 0 || slotId > 3 || (amsId === 128 && slotId !== 0)) {
|
|
3396
|
+
throw new Error("slot_id must be an integer from 0 to 3; AMS-HT supports only slot 0.");
|
|
3397
|
+
}
|
|
3398
|
+
result = await sendNativeCommand({
|
|
3399
|
+
host,
|
|
3400
|
+
serial: bambuSerial,
|
|
3401
|
+
token: bambuToken,
|
|
3402
|
+
messageJson: JSON.stringify({
|
|
3403
|
+
print: {
|
|
3404
|
+
command: "ams_get_rfid",
|
|
3405
|
+
sequence_id: String(Date.now()),
|
|
3406
|
+
ams_id: amsId,
|
|
3407
|
+
slot_id: slotId,
|
|
3408
|
+
},
|
|
3409
|
+
}),
|
|
3410
|
+
});
|
|
3411
|
+
} else {
|
|
3412
|
+
result = await this.bambu.rereadAmsRfid(
|
|
3413
|
+
host,
|
|
3414
|
+
bambuSerial,
|
|
3415
|
+
bambuToken,
|
|
3416
|
+
Number(args.ams_id),
|
|
3417
|
+
Number(args.slot_id)
|
|
3418
|
+
);
|
|
3419
|
+
}
|
|
3081
3420
|
break;
|
|
3082
3421
|
|
|
3083
3422
|
case "set_temperature": {
|
|
@@ -3087,12 +3426,19 @@ class BambuPrinterMCPServer {
|
|
|
3087
3426
|
if (typeof args.temperature !== "number" || !Number.isFinite(args.temperature) || args.temperature < 0) {
|
|
3088
3427
|
throw new Error("Temperature must be a finite, non-negative number in °C.");
|
|
3089
3428
|
}
|
|
3429
|
+
const heaterModel = args.temperature === 0
|
|
3430
|
+
? String(args.bambu_model || DEFAULT_BAMBU_MODEL).trim().toLowerCase()
|
|
3431
|
+
: await this.resolveBambuModel(args.bambu_model as string | undefined);
|
|
3090
3432
|
result = await this.bambu.setTemperature(
|
|
3091
3433
|
host, bambuSerial, bambuToken,
|
|
3092
3434
|
String(args.component), args.temperature,
|
|
3093
|
-
|
|
3435
|
+
heaterModel || undefined,
|
|
3094
3436
|
args?.material !== undefined ? String(args.material) : undefined,
|
|
3095
|
-
Number(args?.nozzle_diameter ?? DEFAULT_NOZZLE_DIAMETER)
|
|
3437
|
+
Number(args?.nozzle_diameter ?? DEFAULT_NOZZLE_DIAMETER),
|
|
3438
|
+
(args.temperature === 0 ? nativeControls : heaterModel === "x2d" && process.platform === "darwin")
|
|
3439
|
+
? (heater, target, assertActive, beforeDispatch) => sendNativeCommand({ host, serial: bambuSerial, token: bambuToken,
|
|
3440
|
+
messageJson: buildBambuNativeTemperatureCommand(heater, target).messageJson }, { signal: extra.signal, assertActive, beforeDispatch })
|
|
3441
|
+
: undefined
|
|
3096
3442
|
);
|
|
3097
3443
|
break;
|
|
3098
3444
|
}
|
|
@@ -3101,51 +3447,140 @@ class BambuPrinterMCPServer {
|
|
|
3101
3447
|
if (!args?.fan || args?.speed === undefined) {
|
|
3102
3448
|
throw new Error("Missing required parameters: fan and speed");
|
|
3103
3449
|
}
|
|
3104
|
-
|
|
3105
|
-
host,
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3450
|
+
if (nativeControls) {
|
|
3451
|
+
const status = await this.bambu.getStatus(host, bambuSerial, bambuToken);
|
|
3452
|
+
const printerState = String(status?.raw?.gcode_state ?? status?.raw?.print?.gcode_state ?? status?.gcode_state ?? status?.status ?? "").toUpperCase();
|
|
3453
|
+
if (printerState === "RUNNING" && args?.confirm_during_print !== true) {
|
|
3454
|
+
throw new Error("The X2D is currently printing. Re-submit with confirm_during_print=true after explicit user confirmation.");
|
|
3455
|
+
}
|
|
3456
|
+
const fanCommand = buildBambuNativeFanCommand(String(args.fan), Number(args.speed));
|
|
3457
|
+
result = {
|
|
3458
|
+
...await sendNativeCommand({
|
|
3459
|
+
host,
|
|
3460
|
+
serial: bambuSerial,
|
|
3461
|
+
token: bambuToken,
|
|
3462
|
+
messageJson: fanCommand.messageJson,
|
|
3463
|
+
}),
|
|
3464
|
+
fan: fanCommand.fan,
|
|
3465
|
+
fan_index: fanCommand.fanIndex,
|
|
3466
|
+
requested_speed: fanCommand.requestedSpeed,
|
|
3467
|
+
speed: fanCommand.speed,
|
|
3468
|
+
};
|
|
3469
|
+
} else {
|
|
3470
|
+
result = await this.bambu.setFanSpeed(
|
|
3471
|
+
host,
|
|
3472
|
+
bambuSerial,
|
|
3473
|
+
bambuToken,
|
|
3474
|
+
String(args.fan),
|
|
3475
|
+
Number(args.speed)
|
|
3476
|
+
);
|
|
3477
|
+
}
|
|
3111
3478
|
break;
|
|
3112
3479
|
|
|
3113
3480
|
case "set_light":
|
|
3114
3481
|
if (!args?.light || !args?.mode) {
|
|
3115
3482
|
throw new Error("Missing required parameters: light and mode");
|
|
3116
3483
|
}
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3484
|
+
if (nativeControls) {
|
|
3485
|
+
const light = String(args.light).trim();
|
|
3486
|
+
const mode = String(args.mode).trim().toLowerCase();
|
|
3487
|
+
if (light !== "chamber_light" && light !== "chamber_light2") {
|
|
3488
|
+
throw new Error("X2D light must be chamber_light or chamber_light2.");
|
|
3489
|
+
}
|
|
3490
|
+
if (!["on", "off", "flashing"].includes(mode)) {
|
|
3491
|
+
throw new Error("Light mode must be one of: on, off, flashing.");
|
|
3492
|
+
}
|
|
3493
|
+
result = await sendNativeCommand({
|
|
3494
|
+
host,
|
|
3495
|
+
serial: bambuSerial,
|
|
3496
|
+
token: bambuToken,
|
|
3497
|
+
messageJson: JSON.stringify({
|
|
3498
|
+
system: {
|
|
3499
|
+
command: "ledctrl",
|
|
3500
|
+
led_node: light,
|
|
3501
|
+
led_mode: mode,
|
|
3502
|
+
led_on_time: 500,
|
|
3503
|
+
led_off_time: 500,
|
|
3504
|
+
loop_times: 0,
|
|
3505
|
+
interval_time: 0,
|
|
3506
|
+
sequence_id: String(Date.now()),
|
|
3507
|
+
},
|
|
3508
|
+
}),
|
|
3509
|
+
});
|
|
3510
|
+
} else {
|
|
3511
|
+
result = await this.bambu.setLight(
|
|
3512
|
+
host,
|
|
3513
|
+
bambuSerial,
|
|
3514
|
+
bambuToken,
|
|
3515
|
+
String(args.light),
|
|
3516
|
+
String(args.mode)
|
|
3517
|
+
);
|
|
3518
|
+
}
|
|
3124
3519
|
break;
|
|
3125
3520
|
|
|
3126
3521
|
case "skip_objects":
|
|
3127
3522
|
if (!Array.isArray(args?.object_ids)) {
|
|
3128
3523
|
throw new Error("Missing required parameter: object_ids");
|
|
3129
3524
|
}
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3525
|
+
if (nativeControls) {
|
|
3526
|
+
const objectIds = args.object_ids.map((id: unknown) => Number(id));
|
|
3527
|
+
if (objectIds.length === 0 || objectIds.some((id: number) => !Number.isInteger(id) || id < 0)) {
|
|
3528
|
+
throw new Error("object_ids must contain one or more non-negative integers.");
|
|
3529
|
+
}
|
|
3530
|
+
result = await sendNativeCommand({
|
|
3531
|
+
host,
|
|
3532
|
+
serial: bambuSerial,
|
|
3533
|
+
token: bambuToken,
|
|
3534
|
+
messageJson: JSON.stringify({
|
|
3535
|
+
print: { command: "skip_objects", obj_list: objectIds, sequence_id: String(Date.now()) },
|
|
3536
|
+
}),
|
|
3537
|
+
qos: 1,
|
|
3538
|
+
});
|
|
3539
|
+
} else {
|
|
3540
|
+
result = await this.bambu.skipObjects(
|
|
3541
|
+
host,
|
|
3542
|
+
bambuSerial,
|
|
3543
|
+
bambuToken,
|
|
3544
|
+
args.object_ids.map((id: unknown) => Number(id))
|
|
3545
|
+
);
|
|
3546
|
+
}
|
|
3136
3547
|
break;
|
|
3137
3548
|
|
|
3138
3549
|
case "set_ams_drying":
|
|
3139
3550
|
if (!args?.action || args?.ams_id === undefined) {
|
|
3140
3551
|
throw new Error("Missing required parameters: action and ams_id");
|
|
3141
3552
|
}
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3553
|
+
if (nativeControls) {
|
|
3554
|
+
const action = String(args.action).trim().toLowerCase();
|
|
3555
|
+
const amsId = Number(args.ams_id);
|
|
3556
|
+
if (action !== "start" && action !== "stop") {
|
|
3557
|
+
throw new Error("AMS drying action must be one of: start, stop.");
|
|
3558
|
+
}
|
|
3559
|
+
if ((!Number.isInteger(amsId) || (amsId < 0 || amsId > 3)) && amsId !== 128) {
|
|
3560
|
+
throw new Error("X2D ams_id must be 0 to 3, or 128 for AMS-HT.");
|
|
3561
|
+
}
|
|
3562
|
+
result = await sendNativeCommand({
|
|
3563
|
+
host,
|
|
3564
|
+
serial: bambuSerial,
|
|
3565
|
+
token: bambuToken,
|
|
3566
|
+
messageJson: JSON.stringify({
|
|
3567
|
+
print: {
|
|
3568
|
+
command: "ams_control",
|
|
3569
|
+
sequence_id: String(Date.now()),
|
|
3570
|
+
ams_id: amsId,
|
|
3571
|
+
param: action === "start" ? "start_drying" : "stop_drying",
|
|
3572
|
+
},
|
|
3573
|
+
}),
|
|
3574
|
+
});
|
|
3575
|
+
} else {
|
|
3576
|
+
result = await this.bambu.setAmsDrying(
|
|
3577
|
+
host,
|
|
3578
|
+
bambuSerial,
|
|
3579
|
+
bambuToken,
|
|
3580
|
+
String(args.action),
|
|
3581
|
+
Number(args.ams_id)
|
|
3582
|
+
);
|
|
3583
|
+
}
|
|
3149
3584
|
break;
|
|
3150
3585
|
|
|
3151
3586
|
case "extend_stl_base":
|
|
@@ -3380,8 +3815,34 @@ class BambuPrinterMCPServer {
|
|
|
3380
3815
|
if (!args?.three_mf_path) {
|
|
3381
3816
|
throw new Error("Missing required parameter: three_mf_path");
|
|
3382
3817
|
}
|
|
3383
|
-
|
|
3384
|
-
|
|
3818
|
+
const printModel = await this.resolveBambuModel(args.bambu_model as string | undefined);
|
|
3819
|
+
const resolvedArgs = { ...args, bambu_model: printModel };
|
|
3820
|
+
const requestedModel = printModel;
|
|
3821
|
+
const defaultConnectionMode =
|
|
3822
|
+
DEFAULT_BAMBU_CONNECTION_MODE ||
|
|
3823
|
+
(requestedModel === "x2d" ? "bambu_native" : "lan_mqtt_ftps");
|
|
3824
|
+
const connectionMode = String(args?.connection_mode || defaultConnectionMode).trim().toLowerCase();
|
|
3825
|
+
if (!["lan_mqtt_ftps", "bambu_native", "bambu_network", "bambu_connect"].includes(connectionMode)) throw new Error("Unsupported connection_mode.");
|
|
3826
|
+
// X2D firmware accepts the Bambu Studio :6000/eMMC route but
|
|
3827
|
+
// rejects the legacy direct FTPS STOR operation with 553. Keep
|
|
3828
|
+
// an explicit old-mode request from re-entering that known-bad
|
|
3829
|
+
// path; callers still get a fully scriptable local print.
|
|
3830
|
+
const effectiveConnectionMode =
|
|
3831
|
+
connectionMode === "lan_mqtt_ftps" && requestedModel === "x2d"
|
|
3832
|
+
? "bambu_native"
|
|
3833
|
+
: connectionMode;
|
|
3834
|
+
if (effectiveConnectionMode === "bambu_connect") {
|
|
3835
|
+
result = await this.handoff3mfViaBambuConnect(resolvedArgs);
|
|
3836
|
+
break;
|
|
3837
|
+
}
|
|
3838
|
+
if (effectiveConnectionMode === "bambu_network") {
|
|
3839
|
+
result = await this.print3mfViaBambuNetwork(resolvedArgs, host, bambuSerial, bambuToken);
|
|
3840
|
+
break;
|
|
3841
|
+
}
|
|
3842
|
+
if (effectiveConnectionMode === "bambu_native") {
|
|
3843
|
+
result = await this.print3mfViaBambuNative(
|
|
3844
|
+
resolvedArgs, host, bambuSerial, bambuToken, reportNativeUpdate, extra.signal
|
|
3845
|
+
);
|
|
3385
3846
|
break;
|
|
3386
3847
|
}
|
|
3387
3848
|
if (!bambuSerial || !bambuToken) {
|
|
@@ -3392,7 +3853,6 @@ class BambuPrinterMCPServer {
|
|
|
3392
3853
|
args as Record<string, unknown> | undefined,
|
|
3393
3854
|
"print_3mf"
|
|
3394
3855
|
);
|
|
3395
|
-
const printModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
3396
3856
|
assertDirectPrintSupported(printModel, bambuSerial);
|
|
3397
3857
|
const printBedType = resolveBedType(args?.bed_type as string | undefined);
|
|
3398
3858
|
const printNozzles = requestedPrintNozzles(args as Record<string, any>);
|
|
@@ -3702,6 +4162,7 @@ class BambuPrinterMCPServer {
|
|
|
3702
4162
|
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
|
3703
4163
|
}
|
|
3704
4164
|
|
|
4165
|
+
await progressNotifications;
|
|
3705
4166
|
const text = typeof result === "string" ? result : JSON.stringify(result, null, 2);
|
|
3706
4167
|
|
|
3707
4168
|
if (this.runtimeConfig.enableJsonResponse && typeof result === "object") {
|