bambu-printer-mcp 1.1.12 → 1.1.13
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 +3 -1
- package/README.md +10 -4
- package/dist/bambu-network-bridge.d.ts +4 -0
- package/dist/bambu-network-bridge.js +3 -0
- package/dist/index.js +196 -85
- package/dist/printers/bambu.d.ts +36 -4
- package/dist/printers/bambu.js +315 -122
- 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/package.json +1 -1
- package/src/bambu-network-bridge.ts +7 -0
- package/src/index.ts +203 -93
- package/src/printers/bambu.ts +308 -152
- 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/CONTRIBUTORS.md
CHANGED
|
@@ -6,6 +6,8 @@ Thank you to everyone who builds, tests, reports problems, and shares real print
|
|
|
6
6
|
|
|
7
7
|
| Contributor | Contribution |
|
|
8
8
|
| --- | --- |
|
|
9
|
+
| [Boardy (@boardyai)](https://x.com/boardyai) | Raises the nozzle-verification question that informs the added printer safety checks. |
|
|
10
|
+
| Fable red-team review, shared by David Montgomery | Supplies archive-ambiguity reproductions and hardware-safety review findings used for plate binding, upload checks, temperature policy, and human preflight. Network/security suggestions remain outside this hardware-safety release. |
|
|
9
11
|
| [Sebastian (sebas1986)](https://github.com/sebas1986) | Isolates the multi-filament CLI crash with real BambuStudio bisection, contributes per-slot colours and multi-nozzle prime-tower placement, and verifies X2D identification, status, and slicing in [#18](https://github.com/DMontgomery40/bambu-printer-mcp/pull/18). Direct X2D printing remains deferred pending the native transport. |
|
|
10
12
|
| [Stenslaen](https://github.com/Stenslaen) | Traces the delayed H2 crash to OTA model detection, documents a multi-day workaround, and reports unexpected state-transition crashes in [#7](https://github.com/DMontgomery40/bambu-printer-mcp/issues/7). |
|
|
11
13
|
| [Alejandro Oñate (alexol91)](https://github.com/alexol91) | Reports the missing machine-template G-code and multi-filament override problems in [#12](https://github.com/DMontgomery40/bambu-printer-mcp/issues/12), with measured output and a proposed fix in [#13](https://github.com/DMontgomery40/bambu-printer-mcp/pull/13). |
|
|
@@ -19,7 +21,7 @@ Thank you to everyone who builds, tests, reports problems, and shares real print
|
|
|
19
21
|
|
|
20
22
|
Thank you also to the existing contributors whose work the project builds on:
|
|
21
23
|
|
|
22
|
-
- [David Montgomery (DMontgomery40)](https://github.com/DMontgomery40) — project maintainer;
|
|
24
|
+
- [David Montgomery (DMontgomery40)](https://github.com/DMontgomery40) — project maintainer; contributes the printer-profile, temperature, and hardware-safety reports informing the shared slicing and print checks.
|
|
23
25
|
- [rowbotik](https://github.com/rowbotik) — printer, AMS, slicing, and operational work across the existing release history.
|
|
24
26
|
- [len-foss](https://github.com/len-foss) — project code contribution.
|
|
25
27
|
- [thebitrock](https://github.com/thebitrock) — project code contribution.
|
package/README.md
CHANGED
|
@@ -194,7 +194,7 @@ The optional FULU **BambuNetwork bridge** exposes `bambu_network_bridge_status`,
|
|
|
194
194
|
- Query live AMS inventory with resolved Bambu/Orca filament profile paths via `get_printer_filaments`. Includes per-tray display names, match confidence (`high`/`medium`/`low`/`none`), resolution tier (`exact-model-nozzle`/`model`/`generic`/`unresolved`), and a summary with recommended auto-slice filament. Retries automatically when AMS data hasn't arrived yet (common on first MQTT push from idle printers).
|
|
195
195
|
- List, upload, and delete files on the printer's SD card via FTPS
|
|
196
196
|
- Capture a JPEG snapshot from the chamber camera. Supports A1, A1 mini, P1S, P1P (TCP-on-6000), and X1, X1C, X1E, P2S, H2, H2S, H2D, H2C, H2D Pro, X2D (RTSP via ffmpeg). Requires ffmpeg in PATH for the RTSP path.
|
|
197
|
-
- Upload and print pre-sliced `.
|
|
197
|
+
- Upload and print pre-sliced `.3mf` projects with checked plate selection and calibration flags. Legacy `.gcode.3mf` routes require a single external-spool-only plate; see the [slicing guide](https://github.com/DMontgomery40/bambu-printer-mcp/blob/main/docs/SLICING.md).
|
|
198
198
|
- Slice through BambuStudio CLI with automatic BBL inheritance/include resolution, per-slot filament colours, and fallback prime-tower placement for multi-nozzle printers. Missing dependencies stop the slice; custom settings and saved project tower positions are preserved. Multi-colour slicing is verified by the contributor on BambuStudio 02.08.02.60 for Windows; older CLI versions have separate limitations. See [slicing guide](https://github.com/DMontgomery40/bambu-printer-mcp/blob/main/docs/SLICING.md).
|
|
199
199
|
- Recognize X2D status and slice with its own installed BambuStudio preset (`BAMBU_MODEL=x2d`). **Direct X2D printing is not supported yet**: the internal eMMC transport is pending. These print requests stop before slicing, uploading, or issuing printer commands. Print exported projects through a supported slicer instead.
|
|
200
200
|
- Parse AMS mapping from the 3MF's embedded slicer metadata (`Metadata/plate_<n>.json` + gcode filament header) and send it correctly formatted per the OpenBambuAPI spec, with correct H2S/H2D/H2C `ams_mapping2` parallel array format
|
|
@@ -204,6 +204,7 @@ The optional FULU **BambuNetwork bridge** exposes `bambu_network_bridge_status`,
|
|
|
204
204
|
- Set print speed mode (`silent`/`standard`/`sport`/`ludicrous`), clear HMS/print errors, trigger AMS RFID re-read, and control H2/P2 airduct mode (`cooling`/`heating`) via MQTT
|
|
205
205
|
- **Start/stop AMS filament drying** (`set_ams_drying`) on heated AMS units (AMS Pro / AMS-HT). Sends `print.ams_control` MQTT command.
|
|
206
206
|
- Set nozzle and bed temperature via G-code dispatch over MQTT
|
|
207
|
+
- Confirm print starts and positive manual heating through a human preflight prompt. Finished-bed clearance and hardware-error clearing require explicit human acknowledgment; see [hardware safety and headless configuration](https://github.com/DMontgomery40/bambu-printer-mcp/blob/main/docs/SLICING.md).
|
|
207
208
|
- Set fan speed (part, auxiliary, chamber) and chamber light mode (on/off/flashing) via MQTT
|
|
208
209
|
- Read HMS (Health Management System) diagnostics as an MCP resource at `printer://{host}/hms` — read-only error summary from the printer, with automatic settle retry
|
|
209
210
|
- Start G-code files already stored on the printer
|
|
@@ -792,12 +793,17 @@ Trigger a Bambu AMS RFID re-read for one AMS slot. This can move AMS filament; u
|
|
|
792
793
|
|
|
793
794
|
#### set_temperature
|
|
794
795
|
|
|
795
|
-
Set
|
|
796
|
+
Set a checked target temperature for the bed or nozzle through MQTT. Positive targets require the printer model and fresh matching printer telemetry; nozzle heating also requires the declared loaded `material` and matching `nozzle_diameter` (default 0.4). Independent model/component and material ceilings apply. A target of zero turns the heater off without requiring material or nozzle metadata. Accepted `component` values are `bed`, `nozzle`, `extruder`, `tool`, and `tool0`.
|
|
797
|
+
|
|
798
|
+
Manual nozzle heating checks the reported currently loaded AMS tray or external spool. It refuses ambiguous active-nozzle/material selection on multi-nozzle printers; use a checked sliced job or the printer's own controls there. Stop and heater-off commands cancel pending server print/heating operations. Resuming through MCP requires the same paused job inspected and started by this server instance, with fresh matching telemetry; other paused jobs remain controllable at the printer.
|
|
796
799
|
|
|
797
800
|
```json
|
|
798
801
|
{
|
|
799
802
|
"component": "nozzle",
|
|
800
803
|
"temperature": 220,
|
|
804
|
+
"bambu_model": "p1s",
|
|
805
|
+
"material": "PLA",
|
|
806
|
+
"nozzle_diameter": 0.4,
|
|
801
807
|
"host": "192.168.1.100",
|
|
802
808
|
"bambu_serial": "01P00A123456789",
|
|
803
809
|
"bambu_token": "your_access_token"
|
|
@@ -912,11 +918,11 @@ Inspect the configured FULU bridge without starting it using `{}`. Use `{"connec
|
|
|
912
918
|
|
|
913
919
|
#### bambu_network_call
|
|
914
920
|
|
|
915
|
-
Call
|
|
921
|
+
Call an allowed read-only probe such as `{"method": "net.is_user_login", "payload": {}}`. The default injects the initialized agent; use `with_agent: false` for `bridge.handshake`. Raw printer mutations and unknown methods are refused; use the dedicated checked print or printer tools.
|
|
916
922
|
|
|
917
923
|
#### print_3mf_bambu_network
|
|
918
924
|
|
|
919
|
-
Submit a sliced project through FULU's separately configured networking runtime. `connection_type` defaults to `cloud
|
|
925
|
+
Submit a sliced project through FULU's separately configured networking runtime. `connection_type` defaults to `cloud`. Both cloud and LAN bridge jobs require a printer IP, LAN access code, matching serial/device ID, and fresh MQTT safety telemetry. File, raw AMS/nozzle mapping, and extra-option overrides cannot replace checked parameters. See [print examples and AMS requirements](https://github.com/DMontgomery40/bambu-printer-mcp/blob/main/docs/FULU.md#print-through-the-bridge). A zero bridge return code confirms submission, not a physical print or direct X2D support.
|
|
920
926
|
|
|
921
927
|
#### resolve_3mf_ams_slots
|
|
922
928
|
|
|
@@ -5,6 +5,10 @@ export type BambuNetworkBridgeOptions = {
|
|
|
5
5
|
countryCode?: string;
|
|
6
6
|
userInfo?: string;
|
|
7
7
|
timeoutMs?: number;
|
|
8
|
+
/** Trusted in-process preflight; never populated from tool arguments. */
|
|
9
|
+
beforeDispatch?: (method: string) => Promise<void>;
|
|
10
|
+
assertDispatchAllowed?: (method: string) => void;
|
|
11
|
+
onDispatched?: (method: string) => void;
|
|
8
12
|
};
|
|
9
13
|
export type BambuNetworkBridgeStatus = {
|
|
10
14
|
configured: boolean;
|
|
@@ -161,6 +161,7 @@ export class BambuNetworkBridge {
|
|
|
161
161
|
throw new Error("BambuNetwork bridge method is required.");
|
|
162
162
|
}
|
|
163
163
|
await this.ensureStarted(options.bridgeCommand);
|
|
164
|
+
await options.beforeDispatch?.(method);
|
|
164
165
|
const id = this.nextRequestId++;
|
|
165
166
|
const requestPayload = Buffer.from(JSON.stringify({ method, payload }), "utf8");
|
|
166
167
|
const frame = Buffer.alloc(FRAME_HEADER_SIZE + requestPayload.length);
|
|
@@ -176,6 +177,7 @@ export class BambuNetworkBridge {
|
|
|
176
177
|
}, options.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS);
|
|
177
178
|
this.pending.set(id, { method, resolve, reject, timer });
|
|
178
179
|
try {
|
|
180
|
+
options.assertDispatchAllowed?.(method);
|
|
179
181
|
this.child.stdin.write(frame, (error) => {
|
|
180
182
|
if (error) {
|
|
181
183
|
const pending = this.pending.get(id);
|
|
@@ -186,6 +188,7 @@ export class BambuNetworkBridge {
|
|
|
186
188
|
}
|
|
187
189
|
}
|
|
188
190
|
});
|
|
191
|
+
options.onDispatched?.(method);
|
|
189
192
|
}
|
|
190
193
|
catch (error) {
|
|
191
194
|
clearTimeout(timer);
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,9 @@ import { BlenderMcpBridge } from "./blender-mcp-bridge.js";
|
|
|
15
15
|
import { hasAmsMappingInput, normalizeAmsMappingObject, normalizeBridgeAmsTrayValue } from "./ams-mapping.js";
|
|
16
16
|
import { analyze3MFAmsRequirements, analyze3MFPlateObjects, analyzeCollarCharm3MF, extractBambuTemplateSettings, getCollarCharmRolePolicy, parse3MF } from './3mf_parser.js';
|
|
17
17
|
import { BambuImplementation, assertDirectPrintSupported } from "./printers/bambu.js";
|
|
18
|
+
import { inspectPrintFile } from "./safety/print-file.js";
|
|
19
|
+
import { validatePrinterState } from "./safety/printer-state.js";
|
|
20
|
+
import { withPrinterOperation, withPrintSnapshot, uniquePrintName } from "./safety/artifact.js";
|
|
18
21
|
dotenv.config();
|
|
19
22
|
const DEFAULT_HOST = process.env.BAMBU_PRINTER_HOST || process.env.PRINTER_HOST || "localhost";
|
|
20
23
|
const DEFAULT_BAMBU_SERIAL = process.env.BAMBU_PRINTER_SERIAL || process.env.BAMBU_SERIAL || "";
|
|
@@ -44,6 +47,27 @@ const DEFAULT_BAMBU_MODEL = process.env.BAMBU_PRINTER_MODEL?.trim().toLowerCase(
|
|
|
44
47
|
"";
|
|
45
48
|
const DEFAULT_BED_TYPE = process.env.BED_TYPE?.trim().toLowerCase() || "textured_plate";
|
|
46
49
|
const DEFAULT_NOZZLE_DIAMETER = process.env.NOZZLE_DIAMETER?.trim() || "0.4";
|
|
50
|
+
function requestedPrintNozzles(args) {
|
|
51
|
+
if (args.nozzle_diameters !== undefined && args.nozzle_diameter !== undefined) {
|
|
52
|
+
throw new Error("Supply nozzle_diameters or nozzle_diameter, not both.");
|
|
53
|
+
}
|
|
54
|
+
const raw = args.nozzle_diameters !== undefined ? args.nozzle_diameters :
|
|
55
|
+
args.nozzle_diameter !== undefined ? [args.nozzle_diameter] : undefined;
|
|
56
|
+
// A pre-sliced job already declares every nozzle; compare those with the
|
|
57
|
+
// printer rather than broadcasting the CLI's default diameter over them.
|
|
58
|
+
if (raw === undefined)
|
|
59
|
+
return undefined;
|
|
60
|
+
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)))) {
|
|
61
|
+
throw new Error("Nozzle diameters must contain one or two supported sizes: 0.2, 0.4, 0.6, 0.8 mm.");
|
|
62
|
+
}
|
|
63
|
+
return raw.map(Number);
|
|
64
|
+
}
|
|
65
|
+
function assertUniformCliNozzles(args) {
|
|
66
|
+
const nozzles = requestedPrintNozzles(args);
|
|
67
|
+
if (nozzles && new Set(nozzles).size > 1) {
|
|
68
|
+
throw new Error("Mixed nozzle diameters require a pre-sliced 3MF with complete per-nozzle metadata. Export it from the slicer GUI before printing.");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
47
71
|
const VALID_BAMBU_MODELS = ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s", "h2c", "x2d"];
|
|
48
72
|
const H2_BAMBU_MODELS = new Set(["h2d", "h2s", "h2c"]);
|
|
49
73
|
const VALID_BED_TYPES = ["textured_plate", "cool_plate", "engineering_plate", "hot_plate", "supertack_plate"];
|
|
@@ -789,7 +813,23 @@ class BambuPrinterMCPServer {
|
|
|
789
813
|
tools: {}
|
|
790
814
|
}
|
|
791
815
|
});
|
|
792
|
-
this.bambu = new BambuImplementation()
|
|
816
|
+
this.bambu = new BambuImplementation(async (message) => {
|
|
817
|
+
try {
|
|
818
|
+
const response = await this.server.elicitInput({
|
|
819
|
+
mode: "form",
|
|
820
|
+
message,
|
|
821
|
+
requestedSchema: {
|
|
822
|
+
type: "object",
|
|
823
|
+
properties: { confirmed: { type: "boolean", title: "I checked the printer and confirm this operation", default: false } },
|
|
824
|
+
required: ["confirmed"],
|
|
825
|
+
},
|
|
826
|
+
});
|
|
827
|
+
return response.action === "accept" && response.content?.confirmed === true;
|
|
828
|
+
}
|
|
829
|
+
catch {
|
|
830
|
+
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.");
|
|
831
|
+
}
|
|
832
|
+
});
|
|
793
833
|
this.bambuNetwork = new BambuNetworkBridge();
|
|
794
834
|
this.stlManipulator = new STLManipulator(TEMP_DIR);
|
|
795
835
|
this.setupHandlers();
|
|
@@ -903,10 +943,11 @@ class BambuPrinterMCPServer {
|
|
|
903
943
|
const JSZip = (await import('jszip')).default;
|
|
904
944
|
const zipData = fs.readFileSync(threeMFPath);
|
|
905
945
|
const zip = await JSZip.loadAsync(zipData);
|
|
906
|
-
const hasGcode = Object.
|
|
946
|
+
const hasGcode = Object.values(zip.files).some(entry => !entry.dir && /\.gcode$/i.test(entry.name));
|
|
907
947
|
if (hasGcode) {
|
|
908
948
|
return { threeMFPath, autoSliced: false };
|
|
909
949
|
}
|
|
950
|
+
assertUniformCliNozzles(args);
|
|
910
951
|
if (bedType === "supertack_plate") {
|
|
911
952
|
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
953
|
}
|
|
@@ -983,7 +1024,8 @@ class BambuPrinterMCPServer {
|
|
|
983
1024
|
}
|
|
984
1025
|
const printModel = await this.resolveBambuModel(args?.bambu_model);
|
|
985
1026
|
const printBedType = resolveBedType(args?.bed_type);
|
|
986
|
-
const
|
|
1027
|
+
const printNozzles = requestedPrintNozzles(args);
|
|
1028
|
+
const printNozzle = String(printNozzles?.[0] ?? DEFAULT_NOZZLE_DIAMETER);
|
|
987
1029
|
const printPreset = BAMBU_MODEL_PRESETS[printModel]?.(printNozzle);
|
|
988
1030
|
const plateIndex = args?.plate_index !== undefined ? Number(args.plate_index) : 0;
|
|
989
1031
|
if (!Number.isInteger(plateIndex) || plateIndex < 0) {
|
|
@@ -1009,82 +1051,130 @@ class BambuPrinterMCPServer {
|
|
|
1009
1051
|
if (isLocalBridgePrint && !password) {
|
|
1010
1052
|
throw new Error("bambu_token/access code is required for FULU BambuNetwork LAN/local print methods.");
|
|
1011
1053
|
}
|
|
1054
|
+
if (!bambuSerial || devId !== bambuSerial || !devIp || devIp === "localhost" || !(password || bambuToken)) {
|
|
1055
|
+
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.");
|
|
1056
|
+
}
|
|
1057
|
+
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"]) {
|
|
1058
|
+
if (args[field] !== undefined && args[field] !== "")
|
|
1059
|
+
throw new Error(`${field} cannot override a verified print. Use the structured plate, nozzle, and AMS arguments.`);
|
|
1060
|
+
}
|
|
1012
1061
|
const { threeMFPath, autoSliced } = await this.ensurePrintableThreeMFPath(args, printModel, printPreset, printBedType);
|
|
1013
1062
|
const { useAMS, finalAmsMapping, finalAmsSlots } = await this.resolveAmsPrintSettings(threeMFPath, args, host, bambuSerial, bambuToken, printModel, printNozzle);
|
|
1014
1063
|
const threeMfFilename = path.basename(threeMFPath);
|
|
1015
|
-
const projectName = String(args?.project_name || threeMfFilename.replace(/\.3mf$/i, ''));
|
|
1064
|
+
const projectName = String(args?.project_name || args?.task_name || threeMfFilename.replace(/\.3mf$/i, ''));
|
|
1016
1065
|
const presetName = String(args?.preset_name || `${projectName}_plate_${plateIndex + 1}`);
|
|
1017
1066
|
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
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1067
|
+
return withPrinterOperation(devIp, devId, assertActive => withPrintSnapshot(threeMFPath, async (snapshot, retainForDispatchedUpload) => {
|
|
1068
|
+
const inspection = await inspectPrintFile(snapshot, { model: printModel, nozzleDiameters: printNozzles, plateIndex, bedType: printBedType });
|
|
1069
|
+
let mapping = finalAmsMapping?.slice();
|
|
1070
|
+
if (finalAmsSlots) {
|
|
1071
|
+
if (finalAmsSlots.length !== inspection.usedFilamentPositions.length)
|
|
1072
|
+
throw new Error("ams_slots must supply exactly one physical tray per used filament.");
|
|
1073
|
+
mapping = Array(inspection.materials.length).fill(-1);
|
|
1074
|
+
inspection.usedFilamentPositions.forEach((position, index) => { mapping[position] = finalAmsSlots[index]; });
|
|
1075
|
+
}
|
|
1076
|
+
if (!useAMS) {
|
|
1077
|
+
mapping = Array(inspection.materials.length).fill(-1);
|
|
1078
|
+
inspection.usedFilamentPositions.forEach(position => { mapping[position] = 254; });
|
|
1079
|
+
}
|
|
1080
|
+
const requirements = { ...inspection, amsMapping: mapping, useAMS };
|
|
1081
|
+
const initialStatus = await this.bambu.getSafetyStatus(devIp, devId, password || bambuToken);
|
|
1082
|
+
validatePrinterState(initialStatus, requirements);
|
|
1083
|
+
const bedClearance = await this.bambu.confirmPrintPreflight(devId, initialStatus, inspection);
|
|
1084
|
+
const confirmedStatus = await this.bambu.getSafetyStatus(devIp, devId, password || bambuToken);
|
|
1085
|
+
validatePrinterState(confirmedStatus, requirements);
|
|
1086
|
+
this.bambu.assertBedClearance(confirmedStatus, bedClearance);
|
|
1087
|
+
const amsMapping = stringifyBridgeJson(mapping);
|
|
1088
|
+
const remoteFileName = uniquePrintName(threeMFPath);
|
|
1089
|
+
const params = {
|
|
1090
|
+
dev_id: devId,
|
|
1091
|
+
// Firmware can report the task name instead of the uploaded filename.
|
|
1092
|
+
// Bind either report to this inspected artifact for a later safe resume.
|
|
1093
|
+
task_name: remoteFileName.replace(/(?:\.gcode)?\.3mf$/i, ""),
|
|
1094
|
+
project_name: projectName,
|
|
1095
|
+
preset_name: presetName,
|
|
1096
|
+
filename: snapshot,
|
|
1097
|
+
config_filename: snapshot,
|
|
1098
|
+
plate_index: plateIndex + 1,
|
|
1099
|
+
ftp_folder: String(args?.ftp_folder || ""),
|
|
1100
|
+
ftp_file: String(args?.ftp_file || ""),
|
|
1101
|
+
ftp_file_md5: String(args?.ftp_file_md5 || ""),
|
|
1102
|
+
nozzle_mapping: stringifyBridgeJson(args?.nozzle_mapping) || "",
|
|
1103
|
+
ams_mapping: amsMapping || "",
|
|
1104
|
+
ams_mapping2: stringifyBridgeJson(args?.ams_mapping2) || "",
|
|
1105
|
+
ams_mapping_info: stringifyBridgeJson(args?.ams_mapping_info) || "",
|
|
1106
|
+
nozzles_info: stringifyBridgeJson(args?.nozzles_info) || "",
|
|
1107
|
+
connection_type: connectionType,
|
|
1108
|
+
comments: String(args?.comments || ""),
|
|
1109
|
+
origin_profile_id: args?.origin_profile_id !== undefined ? Number(args.origin_profile_id) : 0,
|
|
1110
|
+
stl_design_id: args?.stl_design_id !== undefined ? Number(args.stl_design_id) : 0,
|
|
1111
|
+
origin_model_id: String(args?.origin_model_id || ""),
|
|
1112
|
+
print_type: String(args?.print_type || "from_normal"),
|
|
1113
|
+
dst_file: remoteFileName,
|
|
1114
|
+
dev_name: String(args?.dev_name || ""),
|
|
1115
|
+
dev_ip: devIp,
|
|
1116
|
+
use_ssl_for_ftp: args?.use_ssl_for_ftp !== undefined ? Boolean(args.use_ssl_for_ftp) : true,
|
|
1117
|
+
use_ssl_for_mqtt: args?.use_ssl_for_mqtt !== undefined ? Boolean(args.use_ssl_for_mqtt) : true,
|
|
1118
|
+
username: String(args?.username || "bblp"),
|
|
1119
|
+
password,
|
|
1120
|
+
task_bed_leveling: args?.bed_leveling !== undefined ? Boolean(args.bed_leveling) : true,
|
|
1121
|
+
task_flow_cali: args?.flow_calibration !== undefined ? Boolean(args.flow_calibration) : true,
|
|
1122
|
+
task_vibration_cali: args?.vibration_calibration !== undefined ? Boolean(args.vibration_calibration) : true,
|
|
1123
|
+
task_layer_inspect: args?.layer_inspect !== undefined ? Boolean(args.layer_inspect) : false,
|
|
1124
|
+
task_record_timelapse: args?.timelapse !== undefined ? Boolean(args.timelapse) : false,
|
|
1125
|
+
task_use_ams: useAMS,
|
|
1126
|
+
task_bed_type: printBedType,
|
|
1127
|
+
extra_options: stringifyBridgeJson(args?.extra_options) || "",
|
|
1128
|
+
auto_bed_leveling: args?.auto_bed_leveling !== undefined ? Number(args.auto_bed_leveling) : 0,
|
|
1129
|
+
auto_flow_cali: args?.auto_flow_cali !== undefined ? Number(args.auto_flow_cali) : 0,
|
|
1130
|
+
auto_offset_cali: args?.auto_offset_cali !== undefined ? Number(args.auto_offset_cali) : 0,
|
|
1131
|
+
extruder_cali_manual_mode: args?.extruder_cali_manual_mode !== undefined ? Number(args.extruder_cali_manual_mode) : -1,
|
|
1132
|
+
task_ext_change_assist: args?.external_change_assist !== undefined ? Boolean(args.external_change_assist) : false,
|
|
1133
|
+
try_emmc_print: args?.try_emmc_print !== undefined ? Boolean(args.try_emmc_print) : false,
|
|
1134
|
+
};
|
|
1135
|
+
assertActive();
|
|
1136
|
+
const bridgeResult = await this.bambuNetwork.callWithAgent(bridgeMethod, { client_job_id: clientJobId, params }, {
|
|
1137
|
+
...this.bridgeOptionsFromArgs(args, "print_3mf_bambu_network"),
|
|
1138
|
+
beforeDispatch: async (method) => {
|
|
1139
|
+
if (method !== bridgeMethod)
|
|
1140
|
+
return;
|
|
1141
|
+
assertActive();
|
|
1142
|
+
const dispatchStatus = await this.bambu.getSafetyStatus(devIp, devId, password || bambuToken);
|
|
1143
|
+
validatePrinterState(dispatchStatus, requirements);
|
|
1144
|
+
this.bambu.assertBedClearance(dispatchStatus, bedClearance);
|
|
1145
|
+
},
|
|
1146
|
+
assertDispatchAllowed: method => { if (method === bridgeMethod)
|
|
1147
|
+
assertActive(); },
|
|
1148
|
+
onDispatched: method => { if (method === bridgeMethod)
|
|
1149
|
+
retainForDispatchedUpload(); },
|
|
1150
|
+
});
|
|
1151
|
+
if (typeof bridgeResult === "object" && bridgeResult !== null && bridgeResult.ok === false) {
|
|
1152
|
+
throw new Error(`FULU BambuNetwork bridge method ${bridgeMethod} failed: ${String(bridgeResult.error || "unknown bridge error")}`);
|
|
1153
|
+
}
|
|
1154
|
+
if (typeof bridgeResult === "object" &&
|
|
1155
|
+
bridgeResult !== null &&
|
|
1156
|
+
typeof bridgeResult.value === "number" &&
|
|
1157
|
+
bridgeResult.value !== 0) {
|
|
1158
|
+
const value = bridgeResult.value;
|
|
1159
|
+
throw new Error(`FULU BambuNetwork bridge method ${bridgeMethod} returned non-zero result ${value}.`);
|
|
1160
|
+
}
|
|
1161
|
+
assertActive();
|
|
1162
|
+
this.bambu.recordCheckedJob(devIp, devId, remoteFileName, requirements);
|
|
1163
|
+
return {
|
|
1164
|
+
status: "success",
|
|
1165
|
+
message: `FULU BambuNetwork ${bridgePrintMethod} command for ${threeMfFilename} sent successfully.`,
|
|
1166
|
+
bridgeMethod,
|
|
1167
|
+
bridgeResult,
|
|
1168
|
+
clientJobId,
|
|
1169
|
+
autoSliced,
|
|
1170
|
+
projectName,
|
|
1171
|
+
plateIndex,
|
|
1172
|
+
bridgePlateIndex: plateIndex + 1,
|
|
1173
|
+
useAMS,
|
|
1174
|
+
amsMapping: mapping,
|
|
1175
|
+
params: redactPrintParams(params),
|
|
1176
|
+
};
|
|
1177
|
+
}));
|
|
1088
1178
|
}
|
|
1089
1179
|
async getResolvedPrinterFilamentInventory(host, bambuSerial, bambuToken, bambuModel, nozzleDiameter) {
|
|
1090
1180
|
const status = await this.bambu.getStatus(host, bambuSerial, bambuToken);
|
|
@@ -1704,7 +1794,7 @@ class BambuPrinterMCPServer {
|
|
|
1704
1794
|
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
1795
|
project_name: { type: "string", description: "Optional project name sent in FULU PrintParams; defaults to the 3MF filename without extension." },
|
|
1706
1796
|
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
|
|
1797
|
+
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
1798
|
config_filename: { type: "string", description: "Optional config 3MF path for cloud print; defaults to the same 3MF path." },
|
|
1709
1799
|
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
1800
|
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 +1805,7 @@ class BambuPrinterMCPServer {
|
|
|
1715
1805
|
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
1806
|
slicer_profile: { type: "string", description: "Path to an optional slicer profile/config file for auto-slicing." },
|
|
1717
1807
|
nozzle_diameter: { type: "string", description: "Nozzle diameter in mm for auto-slicing (default: 0.4)." },
|
|
1808
|
+
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
1809
|
use_ams: { type: "boolean", description: "Whether to use the AMS; defaults to auto-detect from the 3MF mapping." },
|
|
1719
1810
|
ams_mapping: { type: "array", description: "AMS slot mapping array used by both local MCP printing and FULU PrintParams.", items: { type: "number" } },
|
|
1720
1811
|
ams_slots: { type: "array", description: "Per-used-filament AMS slot list, matching the local LAN print path.", items: { type: "number" } },
|
|
@@ -1778,13 +1869,14 @@ class BambuPrinterMCPServer {
|
|
|
1778
1869
|
},
|
|
1779
1870
|
{
|
|
1780
1871
|
name: "upload_gcode",
|
|
1781
|
-
description: "
|
|
1872
|
+
description: "Inspect a G-code file and upload a uniquely named copy without overwriting existing printer files.",
|
|
1782
1873
|
inputSchema: {
|
|
1783
1874
|
type: "object",
|
|
1784
1875
|
properties: {
|
|
1785
1876
|
filename: { type: "string", description: "Name for the file on the printer" },
|
|
1786
1877
|
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
1878
|
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." },
|
|
1879
|
+
bambu_model: { type: "string", enum: [...VALID_BAMBU_MODELS], description: "Required printer model for inspecting the uploaded G-code, even when not starting it." },
|
|
1788
1880
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
1789
1881
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
1790
1882
|
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
@@ -1804,7 +1896,7 @@ class BambuPrinterMCPServer {
|
|
|
1804
1896
|
bambu_model: {
|
|
1805
1897
|
type: "string",
|
|
1806
1898
|
enum: [...VALID_BAMBU_MODELS],
|
|
1807
|
-
description: "Required
|
|
1899
|
+
description: "Required for every printable .gcode or .3mf upload, including upload-only operations."
|
|
1808
1900
|
},
|
|
1809
1901
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
1810
1902
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
@@ -1952,12 +2044,15 @@ class BambuPrinterMCPServer {
|
|
|
1952
2044
|
},
|
|
1953
2045
|
{
|
|
1954
2046
|
name: "set_temperature",
|
|
1955
|
-
description: "Set
|
|
2047
|
+
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
2048
|
inputSchema: {
|
|
1957
2049
|
type: "object",
|
|
1958
2050
|
properties: {
|
|
1959
2051
|
component: { type: "string", description: "Component to heat: bed, nozzle, or extruder" },
|
|
1960
|
-
temperature: { type: "number", description: "
|
|
2052
|
+
temperature: { type: "number", minimum: 0, description: "Finite target temperature in °C, checked against model/component and declared material limits" },
|
|
2053
|
+
bambu_model: { type: "string", enum: [...VALID_BAMBU_MODELS], description: "Printer model, required for positive heating unless configured in the environment" },
|
|
2054
|
+
material: { type: "string", description: "Loaded material, for example PLA or PETG. Required for positive nozzle heating, including non-RFID spools." },
|
|
2055
|
+
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
2056
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
1962
2057
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
1963
2058
|
bambu_token: { type: "string", description: "Access token (default: value from env)" }
|
|
@@ -2108,7 +2203,8 @@ class BambuPrinterMCPServer {
|
|
|
2108
2203
|
template_3mf_path: { type: "string", description: "Optional template 3MF whose embedded Bambu slicer settings should be reused when auto-slicing this print job." },
|
|
2109
2204
|
template_name: { type: "string", description: "Optional named template from the local registry. Resolves to template_3mf_path automatically." },
|
|
2110
2205
|
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)" }
|
|
2206
|
+
nozzle_diameter: { type: "string", description: "Nozzle diameter in mm for auto-slicing (default: 0.4)" },
|
|
2207
|
+
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
2208
|
},
|
|
2113
2209
|
required: ["three_mf_path", "bambu_model"]
|
|
2114
2210
|
}
|
|
@@ -2316,6 +2412,10 @@ class BambuPrinterMCPServer {
|
|
|
2316
2412
|
throw new Error("Missing required parameter: method");
|
|
2317
2413
|
}
|
|
2318
2414
|
const bridgeArgs = args;
|
|
2415
|
+
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"]);
|
|
2416
|
+
if (!readMethods.has(String(bridgeArgs.method))) {
|
|
2417
|
+
throw new Error("Raw bridge mutations and unknown methods are disabled. Use print_3mf_bambu_network for verified printing or a dedicated printer tool.");
|
|
2418
|
+
}
|
|
2319
2419
|
const payload = bridgeArgs.payload && typeof bridgeArgs.payload === "object"
|
|
2320
2420
|
? bridgeArgs.payload
|
|
2321
2421
|
: {};
|
|
@@ -2352,7 +2452,8 @@ class BambuPrinterMCPServer {
|
|
|
2352
2452
|
}
|
|
2353
2453
|
const uploadSource = resolveUploadGcodeSource(args);
|
|
2354
2454
|
try {
|
|
2355
|
-
|
|
2455
|
+
const uploadModel = await this.resolveBambuModel(args?.bambu_model);
|
|
2456
|
+
result = await this.bambu.uploadFile(host, bambuSerial, bambuToken, uploadSource.filePath, String(args.filename), false, uploadModel);
|
|
2356
2457
|
}
|
|
2357
2458
|
finally {
|
|
2358
2459
|
if (uploadSource.cleanupDir) {
|
|
@@ -2366,7 +2467,8 @@ class BambuPrinterMCPServer {
|
|
|
2366
2467
|
throw new Error("Missing required parameters: file_path and filename");
|
|
2367
2468
|
}
|
|
2368
2469
|
const print = Boolean(args.print ?? false);
|
|
2369
|
-
const
|
|
2470
|
+
const printableUpload = /\.(?:gcode|3mf)$/i.test(String(args.file_path)) || /\.(?:gcode|3mf)$/i.test(String(args.filename));
|
|
2471
|
+
const uploadModel = print || printableUpload ? await this.resolveBambuModel(args?.bambu_model) : undefined;
|
|
2370
2472
|
if (print)
|
|
2371
2473
|
assertDirectPrintSupported(uploadModel, bambuSerial);
|
|
2372
2474
|
result = await this.bambu.uploadFile(host, bambuSerial, bambuToken, String(args.file_path), String(args.filename), print, uploadModel);
|
|
@@ -2412,12 +2514,16 @@ class BambuPrinterMCPServer {
|
|
|
2412
2514
|
}
|
|
2413
2515
|
result = await this.bambu.rereadAmsRfid(host, bambuSerial, bambuToken, Number(args.ams_id), Number(args.slot_id));
|
|
2414
2516
|
break;
|
|
2415
|
-
case "set_temperature":
|
|
2517
|
+
case "set_temperature": {
|
|
2416
2518
|
if (!args?.component || args?.temperature === undefined) {
|
|
2417
2519
|
throw new Error("Missing required parameters: component and temperature");
|
|
2418
2520
|
}
|
|
2419
|
-
|
|
2521
|
+
if (typeof args.temperature !== "number" || !Number.isFinite(args.temperature) || args.temperature < 0) {
|
|
2522
|
+
throw new Error("Temperature must be a finite, non-negative number in °C.");
|
|
2523
|
+
}
|
|
2524
|
+
result = await this.bambu.setTemperature(host, bambuSerial, bambuToken, String(args.component), args.temperature, args.temperature === 0 ? undefined : await this.resolveBambuModel(args?.bambu_model), args?.material !== undefined ? String(args.material) : undefined, Number(args?.nozzle_diameter ?? DEFAULT_NOZZLE_DIAMETER));
|
|
2420
2525
|
break;
|
|
2526
|
+
}
|
|
2421
2527
|
case "set_fan_speed":
|
|
2422
2528
|
if (!args?.fan || args?.speed === undefined) {
|
|
2423
2529
|
throw new Error("Missing required parameters: fan and speed");
|
|
@@ -2656,7 +2762,8 @@ class BambuPrinterMCPServer {
|
|
|
2656
2762
|
const printModel = await this.resolveBambuModel(args?.bambu_model);
|
|
2657
2763
|
assertDirectPrintSupported(printModel, bambuSerial);
|
|
2658
2764
|
const printBedType = resolveBedType(args?.bed_type);
|
|
2659
|
-
const
|
|
2765
|
+
const printNozzles = requestedPrintNozzles(args);
|
|
2766
|
+
const printNozzle = String(printNozzles?.[0] ?? DEFAULT_NOZZLE_DIAMETER);
|
|
2660
2767
|
const activeSlicerProfile = await resolveTemplateFirstSlicerProfilePath(args, slicerProfile || undefined, resolveTemplatePathFromName(["json", "config", "3mf"]) || explicitTemplatePath || undefined, TEMP_DIR);
|
|
2661
2768
|
const explicitSlicerProfile = hasExplicitSlicerProfile(args);
|
|
2662
2769
|
const printPreset = BAMBU_MODEL_PRESETS[printModel]?.(printNozzle);
|
|
@@ -2671,6 +2778,7 @@ class BambuPrinterMCPServer {
|
|
|
2671
2778
|
const zip = await JSZip.loadAsync(zipData);
|
|
2672
2779
|
const hasGcode = Object.values(zip.files).some(entry => !entry.dir && /\.gcode$/i.test(entry.name));
|
|
2673
2780
|
if (!hasGcode) {
|
|
2781
|
+
assertUniformCliNozzles(args);
|
|
2674
2782
|
if (printBedType === "supertack_plate") {
|
|
2675
2783
|
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
2784
|
}
|
|
@@ -2746,6 +2854,7 @@ class BambuPrinterMCPServer {
|
|
|
2746
2854
|
useAMS = finalAmsSlots.length > 0;
|
|
2747
2855
|
}
|
|
2748
2856
|
if (isH2Print &&
|
|
2857
|
+
args?.use_ams !== false &&
|
|
2749
2858
|
!hasUserAmsMapping &&
|
|
2750
2859
|
!hasUserAmsSlots &&
|
|
2751
2860
|
args?.auto_match_ams !== true &&
|
|
@@ -2770,6 +2879,7 @@ class BambuPrinterMCPServer {
|
|
|
2770
2879
|
projectName,
|
|
2771
2880
|
filePath: threeMFPath,
|
|
2772
2881
|
bambuModel: printModel,
|
|
2882
|
+
nozzleDiameters: printNozzles,
|
|
2773
2883
|
plateIndex,
|
|
2774
2884
|
useAMS: useAMS,
|
|
2775
2885
|
amsMapping: finalAmsMapping,
|
|
@@ -2807,6 +2917,7 @@ class BambuPrinterMCPServer {
|
|
|
2807
2917
|
projectName,
|
|
2808
2918
|
filePath: preparedThreeMFPath,
|
|
2809
2919
|
bambuModel: printModel,
|
|
2920
|
+
nozzleDiameters: [Number(printNozzle)],
|
|
2810
2921
|
plateIndex: collarAnalysis.plateIndex,
|
|
2811
2922
|
useAMS: true,
|
|
2812
2923
|
amsSlots: collarAnalysis.amsSlots,
|