bambu-printer-mcp 1.1.23 → 1.1.25
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 -0
- package/README.md +3 -1
- package/dist/3mf_parser.js +8 -8
- package/dist/index.js +25 -8
- package/dist/printers/bambu.js +6 -1
- package/dist/safety/print-file.js +19 -2
- package/dist/stl/stl-manipulator.js +8 -8
- package/package.json +5 -5
- package/src/3mf_parser.ts +8 -8
- package/src/index.ts +23 -9
- package/src/printers/bambu.ts +8 -2
- package/src/safety/print-file.ts +15 -2
- package/src/stl/stl-manipulator.ts +8 -8
package/CONTRIBUTORS.md
CHANGED
|
@@ -6,6 +6,9 @@ Thank you to everyone who builds, tests, reports problems, and shares real print
|
|
|
6
6
|
|
|
7
7
|
| Contributor | Contribution |
|
|
8
8
|
| --- | --- |
|
|
9
|
+
| [Aleksei Popovich (alexpapay)](https://github.com/alexpapay) | Fixes cross-platform AMS profile resolution and packed P2S chamber status in [#43](https://github.com/DMontgomery40/bambu-printer-mcp/pull/43), contributes regressions, and supplies read-only Windows P2S/AMS 2 Pro status and local slicing evidence. No physical print is claimed for this contribution. |
|
|
10
|
+
| [travismcashan](https://github.com/travismcashan) | Reports stock X2D material-switch and runout-purge rejection, misleading unchecked confirmation, and stdout diagnostics in [#44](https://github.com/DMontgomery40/bambu-printer-mcp/issues/44). Supplies exact preset syntax and firmware evidence, then reports a clean physical X2D cube print with a local workaround; maintainer validation remains isolated and mocked. |
|
|
11
|
+
| [nitpreet22](https://github.com/nitpreet22) | Fixes rejection of the stock A1/A1 mini `M109 H` startup waits in [#41](https://github.com/DMontgomery40/bambu-printer-mcp/pull/41), contributes temperature and model regressions, and supplies Windows Bambu Studio profile and real sliced A1 job inspection evidence. Their physical print used Bambu Studio, not this MCP change. |
|
|
9
12
|
| [Steavie (steavie)](https://github.com/steavie) | Reports the reversed A1/A1 mini serial prefixes in [#39](https://github.com/DMontgomery40/bambu-printer-mcp/issues/39), with A1 status and configuration evidence that identifies the status and fresh-identity regression. |
|
|
10
13
|
| [JaviOFC](https://github.com/JaviOFC) | Reports the X2D identity rejection in [#36](https://github.com/DMontgomery40/bambu-printer-mcp/issues/36) and supplies current firmware, connection-mode, status-field, serial-prefix, and cloud-job evidence showing that `model_id` can contain a print-job identifier while `model` correctly identifies the printer. |
|
|
11
14
|
| [Izzy Mansurov (sapoepsilon)](https://github.com/sapoepsilon) | Fixes Bambu Studio 02.08 P2S extruder-variant metadata and airduct-command inspection in [#33](https://github.com/DMontgomery40/bambu-printer-mcp/pull/33). Supplies regression tests and offline inspection evidence from four real P2S 0.6 mm PLA/PETG jobs and unsafe mutated copies; this contribution does not claim a physical print. |
|
package/README.md
CHANGED
|
@@ -191,7 +191,7 @@ The optional FULU **BambuNetwork bridge** exposes `bambu_network_bridge_status`,
|
|
|
191
191
|
## Features
|
|
192
192
|
|
|
193
193
|
- Get detailed printer status: temperatures (nozzle, bed, chamber), print progress, current layer, time remaining, and live AMS slot data
|
|
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).
|
|
194
|
+
- Query live AMS inventory with resolved Bambu/Orca filament profile paths via `get_printer_filaments`. Profile suggestions use the selected slicer installation on Windows, macOS and Linux, including an explicit `BAMBU_PROFILES_ROOT`; unavailable profiles leave live inventory readable. 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
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).
|
|
@@ -568,6 +568,8 @@ Retrieve current printer state including temperatures, print progress, layer cou
|
|
|
568
568
|
}
|
|
569
569
|
```
|
|
570
570
|
|
|
571
|
+
Chamber status decodes the current temperature from newer firmware's packed CTC reports, including P2S, while retaining legacy chamber/frame fields.
|
|
572
|
+
|
|
571
573
|
Returns a structured object with fields including `status` (gcode_state string), `temperatures.nozzle`, `temperatures.bed`, `temperatures.chamber`, `print.progress`, `print.currentLayer`, `print.totalLayers`, `print.timeRemaining`, and `ams` (raw AMS data from the printer).
|
|
572
574
|
|
|
573
575
|
#### get_printer_filaments
|
package/dist/3mf_parser.js
CHANGED
|
@@ -90,7 +90,7 @@ async function parseBambuConfig(zip) {
|
|
|
90
90
|
if (file) {
|
|
91
91
|
configFile = file;
|
|
92
92
|
configContent = await file.async('string');
|
|
93
|
-
console.
|
|
93
|
+
console.error(`Found Bambu config file: ${configFile.name}`);
|
|
94
94
|
break; // Use the first one found (project usually has the most)
|
|
95
95
|
}
|
|
96
96
|
}
|
|
@@ -99,7 +99,7 @@ async function parseBambuConfig(zip) {
|
|
|
99
99
|
// Attempt to parse as JSON
|
|
100
100
|
const jsonData = JSON.parse(configContent);
|
|
101
101
|
const parsedConfig = parseBambuJSONConfig(jsonData);
|
|
102
|
-
console.
|
|
102
|
+
console.error('Bambu config parsed successfully as JSON.');
|
|
103
103
|
return parsedConfig;
|
|
104
104
|
}
|
|
105
105
|
catch (jsonError) {
|
|
@@ -107,7 +107,7 @@ async function parseBambuConfig(zip) {
|
|
|
107
107
|
// Fallback: Try parsing as INI if JSON fails (though unlikely based on sample)
|
|
108
108
|
try {
|
|
109
109
|
const parsedConfig = parseFallbackINIConfig(configContent); // Keep a simple INI parser as fallback
|
|
110
|
-
console.
|
|
110
|
+
console.error('Bambu config parsed successfully as INI (fallback).');
|
|
111
111
|
return parsedConfig;
|
|
112
112
|
}
|
|
113
113
|
catch (iniError) {
|
|
@@ -117,7 +117,7 @@ async function parseBambuConfig(zip) {
|
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
119
|
else {
|
|
120
|
-
console.
|
|
120
|
+
console.error('No Bambu-specific config file found in Metadata directory.');
|
|
121
121
|
return {};
|
|
122
122
|
}
|
|
123
123
|
}
|
|
@@ -186,7 +186,7 @@ async function parse3DModelConfig(zip) {
|
|
|
186
186
|
});
|
|
187
187
|
});
|
|
188
188
|
}
|
|
189
|
-
console.
|
|
189
|
+
console.error('3dmodel.model parsed successfully.');
|
|
190
190
|
return { metadata, objects, build: { items: buildItems } };
|
|
191
191
|
}
|
|
192
192
|
catch (error) {
|
|
@@ -195,13 +195,13 @@ async function parse3DModelConfig(zip) {
|
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
197
|
export async function parse3MF(filePath) {
|
|
198
|
-
console.
|
|
198
|
+
console.error(`Parsing 3MF file: ${filePath}`);
|
|
199
199
|
try {
|
|
200
200
|
const data = await fs.readFile(filePath);
|
|
201
201
|
const zip = await JSZip.loadAsync(data);
|
|
202
202
|
// List files for debugging
|
|
203
203
|
// const fileList = Object.keys(zip.files);
|
|
204
|
-
// console.
|
|
204
|
+
// console.error('Files in 3MF:', fileList);
|
|
205
205
|
const { metadata, objects, build } = await parse3DModelConfig(zip);
|
|
206
206
|
const bambuConfig = await parseBambuConfig(zip);
|
|
207
207
|
// Combine data into the final structure
|
|
@@ -211,7 +211,7 @@ export async function parse3MF(filePath) {
|
|
|
211
211
|
build,
|
|
212
212
|
slicerConfig: bambuConfig
|
|
213
213
|
};
|
|
214
|
-
console.
|
|
214
|
+
console.error('3MF parsing completed.');
|
|
215
215
|
return combinedData;
|
|
216
216
|
}
|
|
217
217
|
catch (error) {
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,7 @@ import { BambuImplementation, assertDirectPrintSupported } from "./printers/bamb
|
|
|
20
20
|
import { inspectPrintFile } from "./safety/print-file.js";
|
|
21
21
|
import { validatePrinterState } from "./safety/printer-state.js";
|
|
22
22
|
import { withPrinterOperation, withPrintSnapshot, uniquePrintName } from "./safety/artifact.js";
|
|
23
|
+
import { detectProfilesRoot } from "./slicer/profile-flatten.js";
|
|
23
24
|
dotenv.config();
|
|
24
25
|
const DEFAULT_HOST = process.env.BAMBU_PRINTER_HOST || process.env.PRINTER_HOST || "localhost";
|
|
25
26
|
const DEFAULT_BAMBU_SERIAL = process.env.BAMBU_PRINTER_SERIAL || process.env.BAMBU_SERIAL || "";
|
|
@@ -104,7 +105,6 @@ const BAMBU_MODEL_PRESETS = {
|
|
|
104
105
|
h2c: (n) => `Bambu Lab H2C ${n} nozzle`,
|
|
105
106
|
x2d: (n) => `Bambu Lab X2D ${n} nozzle`,
|
|
106
107
|
};
|
|
107
|
-
const FILAMENT_PROFILE_DIR = "/Applications/BambuStudio.app/Contents/Resources/profiles/BBL/filament";
|
|
108
108
|
const FILAMENT_MODEL_CODES = {
|
|
109
109
|
p1s: "P1S",
|
|
110
110
|
p1p: "P1P",
|
|
@@ -123,13 +123,27 @@ let filamentProfileIndexCache = null;
|
|
|
123
123
|
function buildFilamentProfileIndex() {
|
|
124
124
|
const byName = new Map();
|
|
125
125
|
const baseNameByFilamentId = new Map();
|
|
126
|
-
|
|
126
|
+
let filamentProfileDir;
|
|
127
|
+
try {
|
|
128
|
+
const { slicerType, slicerPath } = resolveSlicerConfig(undefined);
|
|
129
|
+
filamentProfileDir = path.join(detectProfilesRoot(slicerPath, slicerType), "BBL", "filament");
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// Profile suggestions are optional; invalid slicer configuration must not hide AMS inventory.
|
|
127
133
|
return { byName, baseNameByFilamentId };
|
|
128
134
|
}
|
|
129
|
-
|
|
135
|
+
let profileEntries;
|
|
136
|
+
try {
|
|
137
|
+
profileEntries = fs.readdirSync(filamentProfileDir);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// Ignore unavailable profile suggestions while keeping live inventory readable.
|
|
141
|
+
return { byName, baseNameByFilamentId };
|
|
142
|
+
}
|
|
143
|
+
for (const entry of profileEntries) {
|
|
130
144
|
if (!entry.endsWith(".json"))
|
|
131
145
|
continue;
|
|
132
|
-
const filePath = path.join(
|
|
146
|
+
const filePath = path.join(filamentProfileDir, entry);
|
|
133
147
|
try {
|
|
134
148
|
const raw = fs.readFileSync(filePath, "utf8");
|
|
135
149
|
const parsed = JSON.parse(raw);
|
|
@@ -880,6 +894,9 @@ class BambuPrinterMCPServer {
|
|
|
880
894
|
required: ["confirmed"],
|
|
881
895
|
},
|
|
882
896
|
}, { timeout: this.confirmationTimeoutMs });
|
|
897
|
+
if (response.action === "accept" && response.content?.confirmed !== true) {
|
|
898
|
+
throw new Error("Hardware confirmation checkbox was not checked. Check it only after inspecting the printer, or cancel. No command was sent.");
|
|
899
|
+
}
|
|
883
900
|
return response.action === "accept" && response.content?.confirmed === true;
|
|
884
901
|
}
|
|
885
902
|
catch (error) {
|
|
@@ -1015,7 +1032,7 @@ class BambuPrinterMCPServer {
|
|
|
1015
1032
|
if (bedType === "supertack_plate") {
|
|
1016
1033
|
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.');
|
|
1017
1034
|
}
|
|
1018
|
-
console.
|
|
1035
|
+
console.error(`3MF has no gcode - auto-slicing with ${slicerType} for ${printModel}`);
|
|
1019
1036
|
const autoSliceOptions = {
|
|
1020
1037
|
uptodate: true,
|
|
1021
1038
|
ensureOnBed: true,
|
|
@@ -1025,7 +1042,7 @@ class BambuPrinterMCPServer {
|
|
|
1025
1042
|
nozzleType: resolveNozzleType(args?.nozzle_type),
|
|
1026
1043
|
};
|
|
1027
1044
|
threeMFPath = await this.stlManipulator.sliceSTL(threeMFPath, slicerType, slicerPath, slicerProfile || undefined, undefined, printPreset, autoSliceOptions);
|
|
1028
|
-
console.
|
|
1045
|
+
console.error("Auto-sliced to: " + threeMFPath);
|
|
1029
1046
|
return { threeMFPath, autoSliced: true };
|
|
1030
1047
|
}
|
|
1031
1048
|
async resolveAmsPrintSettings(threeMFPath, args, host, bambuSerial, bambuToken, printModel, printNozzle) {
|
|
@@ -3269,7 +3286,7 @@ class BambuPrinterMCPServer {
|
|
|
3269
3286
|
if (printBedType === "supertack_plate") {
|
|
3270
3287
|
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.');
|
|
3271
3288
|
}
|
|
3272
|
-
console.
|
|
3289
|
+
console.error(`3MF has no gcode — auto-slicing with ${slicerType} for ${printModel}`);
|
|
3273
3290
|
const autoSliceOptions = {
|
|
3274
3291
|
uptodate: true,
|
|
3275
3292
|
ensureOnBed: true,
|
|
@@ -3291,7 +3308,7 @@ class BambuPrinterMCPServer {
|
|
|
3291
3308
|
}
|
|
3292
3309
|
threeMFPath = await this.stlManipulator.sliceSTL(threeMFPath, slicerType, slicerPath, activeSlicerProfile, undefined, // progressCallback
|
|
3293
3310
|
printPreset, autoSliceOptions);
|
|
3294
|
-
console.
|
|
3311
|
+
console.error("Auto-sliced to: " + threeMFPath);
|
|
3295
3312
|
}
|
|
3296
3313
|
const parsed3MFData = await parse3MF(threeMFPath);
|
|
3297
3314
|
const isH2Print = H2_BAMBU_MODELS.has(printModel);
|
package/dist/printers/bambu.js
CHANGED
|
@@ -504,6 +504,11 @@ export class BambuImplementation {
|
|
|
504
504
|
const data = cachedData && Object.keys(cachedData).length > 0
|
|
505
505
|
? cachedData
|
|
506
506
|
: printer.data;
|
|
507
|
+
const ctcTemperature = data.device?.ctc?.info?.temp;
|
|
508
|
+
// CTC packs the current temperature in the low 16 bits and the target in the high 16 bits.
|
|
509
|
+
const chamberTemperature = typeof ctcTemperature === "number" && Number.isInteger(ctcTemperature) && ctcTemperature >= 0 && ctcTemperature <= 0xffffffff
|
|
510
|
+
? ctcTemperature & 0xffff
|
|
511
|
+
: data.chamber_temper || data.frame_temper || 0;
|
|
507
512
|
return {
|
|
508
513
|
status: data.gcode_state || "UNKNOWN",
|
|
509
514
|
connected: true,
|
|
@@ -516,7 +521,7 @@ export class BambuImplementation {
|
|
|
516
521
|
actual: data.bed_temper || 0,
|
|
517
522
|
target: data.bed_target_temper || 0,
|
|
518
523
|
},
|
|
519
|
-
chamber:
|
|
524
|
+
chamber: chamberTemperature,
|
|
520
525
|
},
|
|
521
526
|
print: {
|
|
522
527
|
filename: data.subtask_name || data.gcode_file || "None",
|
|
@@ -405,9 +405,15 @@ export async function inspectPrintFile(filePath, options) {
|
|
|
405
405
|
}
|
|
406
406
|
if (['M104', 'M109', 'M140', 'M190', 'M141', 'M191'].includes(code)) {
|
|
407
407
|
const args = parameters(argumentsText, 'A');
|
|
408
|
+
// Official A1 and A1 mini start G-code waits with M109 S<target> H<limit>
|
|
409
|
+
// (S25 H140, S100 H170, S220 H300). H is not a heater target: S stays the
|
|
410
|
+
// checked target, so H is accepted only within those vendor bounds.
|
|
411
|
+
const vendorWait = code === 'M109' && ['a1', 'a1mini'].includes(model);
|
|
408
412
|
for (const key of args.keys())
|
|
409
|
-
if (!'SRTA'.includes(key))
|
|
413
|
+
if (!'SRTA'.includes(key) && !(key === 'H' && vendorWait))
|
|
410
414
|
fail(`unsupported ${code} temperature parameter ${key}`);
|
|
415
|
+
if (args.has('H') && (!Number.isFinite(args.get('H')) || args.get('H') < 0 || args.get('H') > 300))
|
|
416
|
+
fail(`unsupported ${code} wait parameter H`);
|
|
411
417
|
if (!args.has('S') && !args.has('R'))
|
|
412
418
|
fail(`missing ${code} temperature target`);
|
|
413
419
|
const component = ['M104', 'M109'].includes(code) ? 'nozzle' : ['M140', 'M190'].includes(code) ? 'bed' : 'chamber';
|
|
@@ -424,7 +430,10 @@ export async function inspectPrintFile(filePath, options) {
|
|
|
424
430
|
heat(component, args.get(key), position, args.has('T') || args.has('A'), commonFlush);
|
|
425
431
|
}
|
|
426
432
|
else if (code === 'M620' || code === 'M621') {
|
|
427
|
-
const args = parameters(argumentsText, 'MA');
|
|
433
|
+
const args = parameters(argumentsText, model === 'x2d' ? 'MAB' : 'MA');
|
|
434
|
+
// X2D templates use B as a bare material-switch flag, never a target.
|
|
435
|
+
if (args.has('B') && (model !== 'x2d' || !Number.isNaN(args.get('B'))))
|
|
436
|
+
fail('unsupported material-switch B flag');
|
|
428
437
|
if (args.has('S')) {
|
|
429
438
|
const position = args.get('S');
|
|
430
439
|
if ([254, 255, 65279, 65535].includes(position)) {
|
|
@@ -448,6 +457,14 @@ export async function inspectPrintFile(filePath, options) {
|
|
|
448
457
|
}
|
|
449
458
|
}
|
|
450
459
|
}
|
|
460
|
+
else if (code === 'M620.22') {
|
|
461
|
+
// Official X2D runout auto-purge enables the declared project filament.
|
|
462
|
+
// It supplies no heater target; all explicit heat still uses the shared gate.
|
|
463
|
+
const args = parameters(argumentsText);
|
|
464
|
+
if (model !== 'x2d' || args.size !== 2 || !args.has('I') || args.get('P') !== 1)
|
|
465
|
+
fail('unsupported M620.22 runout purge parameters');
|
|
466
|
+
requirePosition(args.get('I'));
|
|
467
|
+
}
|
|
451
468
|
else if (code === 'M620.1' || code === 'M620.10') {
|
|
452
469
|
const args = parameters(argumentsText, 'E');
|
|
453
470
|
const position = code === 'M620.10' && args.get('A') === 1 ? pending ?? active : code === 'M620.10' && args.get('A') === 0 ? active : undefined;
|
|
@@ -1144,7 +1144,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
1144
1144
|
try {
|
|
1145
1145
|
if (progressCallback)
|
|
1146
1146
|
progressCallback(0, "Starting base extension operation...");
|
|
1147
|
-
console.
|
|
1147
|
+
console.error(`Extending base of ${stlFilePath} by ${extensionInches} inches`);
|
|
1148
1148
|
// Load the STL file
|
|
1149
1149
|
const { geometry, boundingBox } = await this.loadSTL(stlFilePath, progressCallback);
|
|
1150
1150
|
if (!this.activeOperations.get(operationId)) {
|
|
@@ -1197,7 +1197,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
1197
1197
|
success: true,
|
|
1198
1198
|
output: outputFilePath
|
|
1199
1199
|
});
|
|
1200
|
-
console.
|
|
1200
|
+
console.error(`Modified STL saved to ${outputFilePath}`);
|
|
1201
1201
|
return outputFilePath;
|
|
1202
1202
|
}
|
|
1203
1203
|
catch (error) {
|
|
@@ -1364,7 +1364,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
1364
1364
|
}
|
|
1365
1365
|
if (progressCallback)
|
|
1366
1366
|
progressCallback(20, `Executing slicer: ${slicerPath} ${args.join(' ')}`);
|
|
1367
|
-
console.
|
|
1367
|
+
console.error(`Executing: ${slicerPath} ${args.join(' ')}`);
|
|
1368
1368
|
// Execute the slicer
|
|
1369
1369
|
await new Promise((resolve, reject) => {
|
|
1370
1370
|
const process = execFile(slicerPath, args, (error, stdout, stderr) => {
|
|
@@ -1374,7 +1374,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
1374
1374
|
reject(new Error(`Slicer failed: ${error.message}. Stderr: ${stderr}`));
|
|
1375
1375
|
}
|
|
1376
1376
|
else {
|
|
1377
|
-
console.
|
|
1377
|
+
console.error(`Slicer Stdout: ${stdout}`);
|
|
1378
1378
|
if (stderr) {
|
|
1379
1379
|
console.warn(`Slicer Stderr: ${stderr}`); // Log stderr even on success
|
|
1380
1380
|
}
|
|
@@ -1382,8 +1382,8 @@ export class STLManipulator extends EventEmitter {
|
|
|
1382
1382
|
}
|
|
1383
1383
|
});
|
|
1384
1384
|
// Optional: Add listeners for stdout/stderr for real-time progress if slicer provides it
|
|
1385
|
-
// process.stdout?.on('data', (data) => { console.
|
|
1386
|
-
// process.stderr?.on('data', (data) => { console.
|
|
1385
|
+
// process.stdout?.on('data', (data) => { console.error(`Slicer stdout: ${data}`); });
|
|
1386
|
+
// process.stderr?.on('data', (data) => { console.error(`Slicer stderr: ${data}`); });
|
|
1387
1387
|
// Check for cancellation periodically
|
|
1388
1388
|
const checkCancel = setInterval(() => {
|
|
1389
1389
|
if (!this.activeOperations.get(operationId)) {
|
|
@@ -1534,7 +1534,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
1534
1534
|
const originalVertexCount = geometry.attributes.position.count;
|
|
1535
1535
|
const mergedGeometry = BufferGeometryUtils.mergeVertices(geometry, tolerance);
|
|
1536
1536
|
const newVertexCount = mergedGeometry.attributes.position.count;
|
|
1537
|
-
console.
|
|
1537
|
+
console.error(`Merged vertices: ${originalVertexCount} -> ${newVertexCount} (Tolerance: ${tolerance}mm)`);
|
|
1538
1538
|
if (progressCallback)
|
|
1539
1539
|
progressCallback(70, `Vertices merged: ${originalVertexCount} -> ${newVertexCount}`);
|
|
1540
1540
|
if (!this.activeOperations.get(operationId))
|
|
@@ -1572,7 +1572,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
1572
1572
|
if (center.lengthSq() < 0.0001) { // Already centered (or very close)
|
|
1573
1573
|
if (progressCallback)
|
|
1574
1574
|
progressCallback(100, "Model is already centered.");
|
|
1575
|
-
console.
|
|
1575
|
+
console.error("Model already centered. No changes made.");
|
|
1576
1576
|
this.emit('operationComplete', { operationId, type: 'centerModel', success: true, output: stlFilePath, message: "Model already centered." });
|
|
1577
1577
|
return stlFilePath; // Return original path
|
|
1578
1578
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bambu-printer-mcp",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.25",
|
|
4
4
|
"description": "MCP server for Bambu Lab 3D printers with STL manipulation, slicing, and print management",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"repository": {
|
|
@@ -46,13 +46,13 @@
|
|
|
46
46
|
"author": "David Montgomery",
|
|
47
47
|
"license": "GPL-2.0",
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
49
|
+
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
50
50
|
"bambu-js": "^2.2.2",
|
|
51
51
|
"bambu-node": "3.22.21",
|
|
52
|
-
"basic-ftp": "^
|
|
52
|
+
"basic-ftp": "^6.2.2",
|
|
53
53
|
"dotenv": "^16.3.1",
|
|
54
|
-
"jszip": "^3.10.
|
|
55
|
-
"mqtt": "^5.
|
|
54
|
+
"jszip": "^3.10.2",
|
|
55
|
+
"mqtt": "^5.16.0",
|
|
56
56
|
"patch-package": "8.0.1",
|
|
57
57
|
"three": "^0.173.0",
|
|
58
58
|
"xml2js": "^0.6.2"
|
package/src/3mf_parser.ts
CHANGED
|
@@ -86,7 +86,7 @@ async function parseBambuConfig(zip: JSZip): Promise<Partial<BambuSlicerConfig>>
|
|
|
86
86
|
if (file) {
|
|
87
87
|
configFile = file;
|
|
88
88
|
configContent = await file.async('string');
|
|
89
|
-
console.
|
|
89
|
+
console.error(`Found Bambu config file: ${configFile.name}`);
|
|
90
90
|
break; // Use the first one found (project usually has the most)
|
|
91
91
|
}
|
|
92
92
|
}
|
|
@@ -96,14 +96,14 @@ async function parseBambuConfig(zip: JSZip): Promise<Partial<BambuSlicerConfig>>
|
|
|
96
96
|
// Attempt to parse as JSON
|
|
97
97
|
const jsonData = JSON.parse(configContent);
|
|
98
98
|
const parsedConfig = parseBambuJSONConfig(jsonData);
|
|
99
|
-
console.
|
|
99
|
+
console.error('Bambu config parsed successfully as JSON.');
|
|
100
100
|
return parsedConfig;
|
|
101
101
|
} catch (jsonError: any) {
|
|
102
102
|
console.warn(`Failed to parse ${configFile.name} as JSON: ${jsonError.message}. Attempting INI parse as fallback...`);
|
|
103
103
|
// Fallback: Try parsing as INI if JSON fails (though unlikely based on sample)
|
|
104
104
|
try {
|
|
105
105
|
const parsedConfig = parseFallbackINIConfig(configContent); // Keep a simple INI parser as fallback
|
|
106
|
-
console.
|
|
106
|
+
console.error('Bambu config parsed successfully as INI (fallback).');
|
|
107
107
|
return parsedConfig;
|
|
108
108
|
} catch (iniError: any) {
|
|
109
109
|
console.error(`Error parsing Bambu config ${configFile.name} as INI (fallback):`, iniError);
|
|
@@ -111,7 +111,7 @@ async function parseBambuConfig(zip: JSZip): Promise<Partial<BambuSlicerConfig>>
|
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
113
|
} else {
|
|
114
|
-
console.
|
|
114
|
+
console.error('No Bambu-specific config file found in Metadata directory.');
|
|
115
115
|
return {};
|
|
116
116
|
}
|
|
117
117
|
}
|
|
@@ -186,7 +186,7 @@ async function parse3DModelConfig(zip: JSZip): Promise<{ metadata: ThreeMFMetada
|
|
|
186
186
|
});
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
console.
|
|
189
|
+
console.error('3dmodel.model parsed successfully.');
|
|
190
190
|
return { metadata, objects, build: { items: buildItems } };
|
|
191
191
|
|
|
192
192
|
} catch (error: any) {
|
|
@@ -198,14 +198,14 @@ async function parse3DModelConfig(zip: JSZip): Promise<{ metadata: ThreeMFMetada
|
|
|
198
198
|
export { ThreeMFData };
|
|
199
199
|
|
|
200
200
|
export async function parse3MF(filePath: string): Promise<ThreeMFData> {
|
|
201
|
-
console.
|
|
201
|
+
console.error(`Parsing 3MF file: ${filePath}`);
|
|
202
202
|
try {
|
|
203
203
|
const data = await fs.readFile(filePath);
|
|
204
204
|
const zip = await JSZip.loadAsync(data);
|
|
205
205
|
|
|
206
206
|
// List files for debugging
|
|
207
207
|
// const fileList = Object.keys(zip.files);
|
|
208
|
-
// console.
|
|
208
|
+
// console.error('Files in 3MF:', fileList);
|
|
209
209
|
|
|
210
210
|
const { metadata, objects, build } = await parse3DModelConfig(zip);
|
|
211
211
|
const bambuConfig = await parseBambuConfig(zip);
|
|
@@ -218,7 +218,7 @@ export async function parse3MF(filePath: string): Promise<ThreeMFData> {
|
|
|
218
218
|
slicerConfig: bambuConfig
|
|
219
219
|
};
|
|
220
220
|
|
|
221
|
-
console.
|
|
221
|
+
console.error('3MF parsing completed.');
|
|
222
222
|
return combinedData;
|
|
223
223
|
|
|
224
224
|
} catch (error: any) {
|
package/src/index.ts
CHANGED
|
@@ -44,6 +44,7 @@ import { BambuImplementation, assertDirectPrintSupported } from "./printers/bamb
|
|
|
44
44
|
import { inspectPrintFile } from "./safety/print-file.js";
|
|
45
45
|
import { validatePrinterState } from "./safety/printer-state.js";
|
|
46
46
|
import { withPrinterOperation, withPrintSnapshot, uniquePrintName } from "./safety/artifact.js";
|
|
47
|
+
import { detectProfilesRoot } from "./slicer/profile-flatten.js";
|
|
47
48
|
|
|
48
49
|
dotenv.config();
|
|
49
50
|
|
|
@@ -140,8 +141,6 @@ const BAMBU_MODEL_PRESETS: Record<string, (nozzle: string) => string> = {
|
|
|
140
141
|
x2d: (n) => `Bambu Lab X2D ${n} nozzle`,
|
|
141
142
|
};
|
|
142
143
|
|
|
143
|
-
const FILAMENT_PROFILE_DIR =
|
|
144
|
-
"/Applications/BambuStudio.app/Contents/Resources/profiles/BBL/filament";
|
|
145
144
|
const FILAMENT_MODEL_CODES: Record<string, string> = {
|
|
146
145
|
p1s: "P1S",
|
|
147
146
|
p1p: "P1P",
|
|
@@ -212,15 +211,27 @@ let filamentProfileIndexCache: FilamentProfileIndex | null = null;
|
|
|
212
211
|
function buildFilamentProfileIndex(): FilamentProfileIndex {
|
|
213
212
|
const byName = new Map<string, string>();
|
|
214
213
|
const baseNameByFilamentId = new Map<string, string>();
|
|
214
|
+
let filamentProfileDir: string;
|
|
215
|
+
try {
|
|
216
|
+
const { slicerType, slicerPath } = resolveSlicerConfig(undefined);
|
|
217
|
+
filamentProfileDir = path.join(detectProfilesRoot(slicerPath, slicerType), "BBL", "filament");
|
|
218
|
+
} catch {
|
|
219
|
+
// Profile suggestions are optional; invalid slicer configuration must not hide AMS inventory.
|
|
220
|
+
return { byName, baseNameByFilamentId };
|
|
221
|
+
}
|
|
215
222
|
|
|
216
|
-
|
|
223
|
+
let profileEntries: string[];
|
|
224
|
+
try {
|
|
225
|
+
profileEntries = fs.readdirSync(filamentProfileDir);
|
|
226
|
+
} catch {
|
|
227
|
+
// Ignore unavailable profile suggestions while keeping live inventory readable.
|
|
217
228
|
return { byName, baseNameByFilamentId };
|
|
218
229
|
}
|
|
219
230
|
|
|
220
|
-
for (const entry of
|
|
231
|
+
for (const entry of profileEntries) {
|
|
221
232
|
if (!entry.endsWith(".json")) continue;
|
|
222
233
|
|
|
223
|
-
const filePath = path.join(
|
|
234
|
+
const filePath = path.join(filamentProfileDir, entry);
|
|
224
235
|
|
|
225
236
|
try {
|
|
226
237
|
const raw = fs.readFileSync(filePath, "utf8");
|
|
@@ -1174,6 +1185,9 @@ class BambuPrinterMCPServer {
|
|
|
1174
1185
|
required: ["confirmed"],
|
|
1175
1186
|
},
|
|
1176
1187
|
}, { timeout: this.confirmationTimeoutMs });
|
|
1188
|
+
if (response.action === "accept" && response.content?.confirmed !== true) {
|
|
1189
|
+
throw new Error("Hardware confirmation checkbox was not checked. Check it only after inspecting the printer, or cancel. No command was sent.");
|
|
1190
|
+
}
|
|
1177
1191
|
return response.action === "accept" && response.content?.confirmed === true;
|
|
1178
1192
|
} catch (error: any) {
|
|
1179
1193
|
if (isElicitationTimeout(error)) {
|
|
@@ -1363,7 +1377,7 @@ class BambuPrinterMCPServer {
|
|
|
1363
1377
|
);
|
|
1364
1378
|
}
|
|
1365
1379
|
|
|
1366
|
-
console.
|
|
1380
|
+
console.error(`3MF has no gcode - auto-slicing with ${slicerType} for ${printModel}`);
|
|
1367
1381
|
const autoSliceOptions: BambuSliceOptions = {
|
|
1368
1382
|
uptodate: true,
|
|
1369
1383
|
ensureOnBed: true,
|
|
@@ -1378,7 +1392,7 @@ class BambuPrinterMCPServer {
|
|
|
1378
1392
|
printPreset,
|
|
1379
1393
|
autoSliceOptions
|
|
1380
1394
|
);
|
|
1381
|
-
console.
|
|
1395
|
+
console.error("Auto-sliced to: " + threeMFPath);
|
|
1382
1396
|
return { threeMFPath, autoSliced: true };
|
|
1383
1397
|
}
|
|
1384
1398
|
|
|
@@ -3956,7 +3970,7 @@ class BambuPrinterMCPServer {
|
|
|
3956
3970
|
'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.'
|
|
3957
3971
|
);
|
|
3958
3972
|
}
|
|
3959
|
-
console.
|
|
3973
|
+
console.error(`3MF has no gcode — auto-slicing with ${slicerType} for ${printModel}`);
|
|
3960
3974
|
const autoSliceOptions: BambuSliceOptions = {
|
|
3961
3975
|
uptodate: true,
|
|
3962
3976
|
ensureOnBed: true,
|
|
@@ -3987,7 +4001,7 @@ class BambuPrinterMCPServer {
|
|
|
3987
4001
|
printPreset,
|
|
3988
4002
|
autoSliceOptions
|
|
3989
4003
|
);
|
|
3990
|
-
console.
|
|
4004
|
+
console.error("Auto-sliced to: " + threeMFPath);
|
|
3991
4005
|
}
|
|
3992
4006
|
|
|
3993
4007
|
const parsed3MFData = await parse3MF(threeMFPath);
|
package/src/printers/bambu.ts
CHANGED
|
@@ -606,10 +606,16 @@ export class BambuImplementation {
|
|
|
606
606
|
}
|
|
607
607
|
|
|
608
608
|
const cachedData = await this.printerStore.waitForInitialReport(host, serial, token);
|
|
609
|
-
const data =
|
|
609
|
+
const data: Record<string, any> =
|
|
610
610
|
cachedData && Object.keys(cachedData).length > 0
|
|
611
611
|
? cachedData
|
|
612
612
|
: printer.data;
|
|
613
|
+
const ctcTemperature = data.device?.ctc?.info?.temp;
|
|
614
|
+
// CTC packs the current temperature in the low 16 bits and the target in the high 16 bits.
|
|
615
|
+
const chamberTemperature =
|
|
616
|
+
typeof ctcTemperature === "number" && Number.isInteger(ctcTemperature) && ctcTemperature >= 0 && ctcTemperature <= 0xffffffff
|
|
617
|
+
? ctcTemperature & 0xffff
|
|
618
|
+
: data.chamber_temper || data.frame_temper || 0;
|
|
613
619
|
|
|
614
620
|
return {
|
|
615
621
|
status: data.gcode_state || "UNKNOWN",
|
|
@@ -623,7 +629,7 @@ export class BambuImplementation {
|
|
|
623
629
|
actual: data.bed_temper || 0,
|
|
624
630
|
target: data.bed_target_temper || 0,
|
|
625
631
|
},
|
|
626
|
-
chamber:
|
|
632
|
+
chamber: chamberTemperature,
|
|
627
633
|
},
|
|
628
634
|
print: {
|
|
629
635
|
filename: data.subtask_name || data.gcode_file || "None",
|
package/src/safety/print-file.ts
CHANGED
|
@@ -296,7 +296,12 @@ export async function inspectPrintFile(filePath: string, options: {model:string;
|
|
|
296
296
|
}
|
|
297
297
|
if(['M104','M109','M140','M190','M141','M191'].includes(code)) {
|
|
298
298
|
const args=parameters(argumentsText,'A');
|
|
299
|
-
|
|
299
|
+
// Official A1 and A1 mini start G-code waits with M109 S<target> H<limit>
|
|
300
|
+
// (S25 H140, S100 H170, S220 H300). H is not a heater target: S stays the
|
|
301
|
+
// checked target, so H is accepted only within those vendor bounds.
|
|
302
|
+
const vendorWait=code==='M109' && ['a1','a1mini'].includes(model);
|
|
303
|
+
for(const key of args.keys()) if(!'SRTA'.includes(key) && !(key==='H' && vendorWait)) fail(`unsupported ${code} temperature parameter ${key}`);
|
|
304
|
+
if(args.has('H') && (!Number.isFinite(args.get('H')) || args.get('H')!<0 || args.get('H')!>300)) fail(`unsupported ${code} wait parameter H`);
|
|
300
305
|
if(!args.has('S')&&!args.has('R')) fail(`missing ${code} temperature target`);
|
|
301
306
|
const component= ['M104','M109'].includes(code)?'nozzle': ['M140','M190'].includes(code)?'bed':'chamber';
|
|
302
307
|
// Heater T is a physical nozzle, unlike standalone remapped T filament selection.
|
|
@@ -307,12 +312,20 @@ export async function inspectPrintFile(filePath: string, options: {model:string;
|
|
|
307
312
|
if(commonFlush) commonFlushUsed=true;
|
|
308
313
|
for(const key of ['S','R']) if(args.has(key)) heat(component,args.get(key)!,position,args.has('T') || args.has('A'),commonFlush);
|
|
309
314
|
} else if(code==='M620' || code==='M621') {
|
|
310
|
-
const args=parameters(argumentsText,'MA');
|
|
315
|
+
const args=parameters(argumentsText,model==='x2d'?'MAB':'MA');
|
|
316
|
+
// X2D templates use B as a bare material-switch flag, never a target.
|
|
317
|
+
if(args.has('B') && (model!=='x2d' || !Number.isNaN(args.get('B')!))) fail('unsupported material-switch B flag');
|
|
311
318
|
if(args.has('S')) {
|
|
312
319
|
const position=args.get('S')!;
|
|
313
320
|
if([254,255,65279,65535].includes(position)) {if(code==='M621') {previous=active;active=undefined;}pending=undefined;}
|
|
314
321
|
else {selectsAms=true;requirePosition(position);if(code==='M620') pending=position;else {validateTemperature('nozzle',nozzleTarget,model,[materials[position]]);previous=active;active=position;pending=undefined;}}
|
|
315
322
|
}
|
|
323
|
+
} else if(code==='M620.22') {
|
|
324
|
+
// Official X2D runout auto-purge enables the declared project filament.
|
|
325
|
+
// It supplies no heater target; all explicit heat still uses the shared gate.
|
|
326
|
+
const args=parameters(argumentsText);
|
|
327
|
+
if(model!=='x2d' || args.size!==2 || !args.has('I') || args.get('P')!==1) fail('unsupported M620.22 runout purge parameters');
|
|
328
|
+
requirePosition(args.get('I')!);
|
|
316
329
|
} else if(code==='M620.1' || code==='M620.10') {
|
|
317
330
|
const args=parameters(argumentsText,'E');
|
|
318
331
|
const position=code==='M620.10' && args.get('A')===1 ? pending ?? active : code==='M620.10' && args.get('A')===0 ? active : undefined;
|
|
@@ -1477,7 +1477,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
1477
1477
|
|
|
1478
1478
|
try {
|
|
1479
1479
|
if (progressCallback) progressCallback(0, "Starting base extension operation...");
|
|
1480
|
-
console.
|
|
1480
|
+
console.error(`Extending base of ${stlFilePath} by ${extensionInches} inches`);
|
|
1481
1481
|
|
|
1482
1482
|
// Load the STL file
|
|
1483
1483
|
const { geometry, boundingBox } = await this.loadSTL(stlFilePath, progressCallback);
|
|
@@ -1554,7 +1554,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
1554
1554
|
output: outputFilePath
|
|
1555
1555
|
});
|
|
1556
1556
|
|
|
1557
|
-
console.
|
|
1557
|
+
console.error(`Modified STL saved to ${outputFilePath}`);
|
|
1558
1558
|
return outputFilePath;
|
|
1559
1559
|
} catch (error) {
|
|
1560
1560
|
console.error("Error extending STL base:", error);
|
|
@@ -1733,7 +1733,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
1733
1733
|
}
|
|
1734
1734
|
|
|
1735
1735
|
if (progressCallback) progressCallback(20, `Executing slicer: ${slicerPath} ${args.join(' ')}`);
|
|
1736
|
-
console.
|
|
1736
|
+
console.error(`Executing: ${slicerPath} ${args.join(' ')}`);
|
|
1737
1737
|
|
|
1738
1738
|
// Execute the slicer
|
|
1739
1739
|
await new Promise<void>((resolve, reject) => {
|
|
@@ -1743,7 +1743,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
1743
1743
|
console.error(`Slicer Stderr: ${stderr}`);
|
|
1744
1744
|
reject(new Error(`Slicer failed: ${error.message}. Stderr: ${stderr}`));
|
|
1745
1745
|
} else {
|
|
1746
|
-
console.
|
|
1746
|
+
console.error(`Slicer Stdout: ${stdout}`);
|
|
1747
1747
|
if (stderr) {
|
|
1748
1748
|
console.warn(`Slicer Stderr: ${stderr}`); // Log stderr even on success
|
|
1749
1749
|
}
|
|
@@ -1752,8 +1752,8 @@ export class STLManipulator extends EventEmitter {
|
|
|
1752
1752
|
});
|
|
1753
1753
|
|
|
1754
1754
|
// Optional: Add listeners for stdout/stderr for real-time progress if slicer provides it
|
|
1755
|
-
// process.stdout?.on('data', (data) => { console.
|
|
1756
|
-
// process.stderr?.on('data', (data) => { console.
|
|
1755
|
+
// process.stdout?.on('data', (data) => { console.error(`Slicer stdout: ${data}`); });
|
|
1756
|
+
// process.stderr?.on('data', (data) => { console.error(`Slicer stderr: ${data}`); });
|
|
1757
1757
|
|
|
1758
1758
|
// Check for cancellation periodically
|
|
1759
1759
|
const checkCancel = setInterval(() => {
|
|
@@ -1938,7 +1938,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
1938
1938
|
const mergedGeometry = BufferGeometryUtils.mergeVertices(geometry, tolerance);
|
|
1939
1939
|
const newVertexCount = mergedGeometry.attributes.position.count;
|
|
1940
1940
|
|
|
1941
|
-
console.
|
|
1941
|
+
console.error(`Merged vertices: ${originalVertexCount} -> ${newVertexCount} (Tolerance: ${tolerance}mm)`);
|
|
1942
1942
|
if (progressCallback) progressCallback(70, `Vertices merged: ${originalVertexCount} -> ${newVertexCount}`);
|
|
1943
1943
|
|
|
1944
1944
|
if (!this.activeOperations.get(operationId)) throw new Error("Operation cancelled");
|
|
@@ -1982,7 +1982,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
1982
1982
|
|
|
1983
1983
|
if (center.lengthSq() < 0.0001) { // Already centered (or very close)
|
|
1984
1984
|
if (progressCallback) progressCallback(100, "Model is already centered.");
|
|
1985
|
-
console.
|
|
1985
|
+
console.error("Model already centered. No changes made.");
|
|
1986
1986
|
this.emit('operationComplete', { operationId, type: 'centerModel', success: true, output: stlFilePath, message: "Model already centered." });
|
|
1987
1987
|
return stlFilePath; // Return original path
|
|
1988
1988
|
}
|