bambu-printer-mcp 1.1.6 → 1.1.9
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 +27 -0
- package/README.md +103 -28
- package/dist/blender-mcp-bridge.d.ts +10 -0
- package/dist/blender-mcp-bridge.js +389 -0
- package/dist/index.js +155 -91
- package/dist/printers/bambu.d.ts +3 -2
- package/dist/printers/bambu.js +49 -18
- package/dist/slicer/profile-flatten.d.ts +29 -4
- package/dist/slicer/profile-flatten.js +269 -74
- package/dist/stl/stl-manipulator.d.ts +4 -17
- package/dist/stl/stl-manipulator.js +161 -85
- package/package.json +8 -4
- package/patches/bambu-node+3.22.21.patch +18 -2
- package/scripts/install-patches.mjs +64 -0
- package/src/blender-mcp-bridge.ts +344 -0
- package/src/index.ts +169 -114
- package/src/printers/bambu.ts +49 -18
- package/src/slicer/profile-flatten.ts +307 -82
- package/src/stl/stl-manipulator.ts +184 -98
package/dist/index.js
CHANGED
|
@@ -5,27 +5,46 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
|
|
|
5
5
|
import { ListResourcesRequestSchema, ReadResourceRequestSchema, ListToolsRequestSchema, CallToolRequestSchema, ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
|
|
6
6
|
import dotenv from "dotenv";
|
|
7
7
|
import fs from "fs";
|
|
8
|
+
import os from "node:os";
|
|
8
9
|
import path from "path";
|
|
9
10
|
import { createServer as createHttpServer } from "node:http";
|
|
10
11
|
import { randomUUID } from "node:crypto";
|
|
11
12
|
import { STLManipulator, SLICER_TYPES, normalizeSlicerType, } from "./stl/stl-manipulator.js";
|
|
12
13
|
import { BambuNetworkBridge } from "./bambu-network-bridge.js";
|
|
14
|
+
import { BlenderMcpBridge } from "./blender-mcp-bridge.js";
|
|
13
15
|
import { hasAmsMappingInput, normalizeAmsMappingObject, normalizeBridgeAmsTrayValue } from "./ams-mapping.js";
|
|
14
16
|
import { analyze3MFAmsRequirements, analyze3MFPlateObjects, analyzeCollarCharm3MF, extractBambuTemplateSettings, getCollarCharmRolePolicy, parse3MF } from './3mf_parser.js';
|
|
15
|
-
import { BambuImplementation } from "./printers/bambu.js";
|
|
17
|
+
import { BambuImplementation, assertDirectPrintSupported } from "./printers/bambu.js";
|
|
16
18
|
dotenv.config();
|
|
17
19
|
const DEFAULT_HOST = process.env.BAMBU_PRINTER_HOST || process.env.PRINTER_HOST || "localhost";
|
|
18
20
|
const DEFAULT_BAMBU_SERIAL = process.env.BAMBU_PRINTER_SERIAL || process.env.BAMBU_SERIAL || "";
|
|
19
21
|
const DEFAULT_BAMBU_TOKEN = process.env.BAMBU_PRINTER_ACCESS_TOKEN || process.env.BAMBU_TOKEN || "";
|
|
20
22
|
const DEFAULT_BAMBU_DEV_ID = process.env.BAMBU_DEV_ID || DEFAULT_BAMBU_SERIAL;
|
|
21
|
-
|
|
23
|
+
// os.tmpdir(), not process.cwd() — a packaged Claude Desktop extension is
|
|
24
|
+
// spawned with a cwd we don't control (and may not be writable), so a
|
|
25
|
+
// cwd-relative default here can throw at module load via the mkdirSync
|
|
26
|
+
// below and kill the server before it ever opens the stdio transport.
|
|
27
|
+
const AUTOMATIC_TEMP_DIR = process.env.TEMP_DIR ? undefined : fs.mkdtempSync(path.join(os.tmpdir(), "bambu-printer-mcp-"));
|
|
28
|
+
const TEMP_DIR = process.env.TEMP_DIR || AUTOMATIC_TEMP_DIR;
|
|
29
|
+
if (AUTOMATIC_TEMP_DIR) {
|
|
30
|
+
// The exit event also covers startup failures and normal event-loop exit.
|
|
31
|
+
// Only the directory created above belongs to us; explicit TEMP_DIR is retained.
|
|
32
|
+
process.once("exit", () => {
|
|
33
|
+
try {
|
|
34
|
+
fs.rmSync(AUTOMATIC_TEMP_DIR, { recursive: true, force: true });
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
console.error("Unable to remove the server's temporary directory:", error);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
22
41
|
// Printer model and bed type
|
|
23
42
|
const DEFAULT_BAMBU_MODEL = process.env.BAMBU_PRINTER_MODEL?.trim().toLowerCase() ||
|
|
24
43
|
process.env.BAMBU_MODEL?.trim().toLowerCase() ||
|
|
25
44
|
"";
|
|
26
45
|
const DEFAULT_BED_TYPE = process.env.BED_TYPE?.trim().toLowerCase() || "textured_plate";
|
|
27
46
|
const DEFAULT_NOZZLE_DIAMETER = process.env.NOZZLE_DIAMETER?.trim() || "0.4";
|
|
28
|
-
const VALID_BAMBU_MODELS = ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s", "h2c"];
|
|
47
|
+
const VALID_BAMBU_MODELS = ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s", "h2c", "x2d"];
|
|
29
48
|
const H2_BAMBU_MODELS = new Set(["h2d", "h2s", "h2c"]);
|
|
30
49
|
const VALID_BED_TYPES = ["textured_plate", "cool_plate", "engineering_plate", "hot_plate", "supertack_plate"];
|
|
31
50
|
const VALID_BAMBUSTUDIO_CLI_BED_TYPES = ["textured_plate", "cool_plate", "engineering_plate", "hot_plate"];
|
|
@@ -41,6 +60,7 @@ const BAMBU_MODEL_PRESETS = {
|
|
|
41
60
|
h2d: (n) => `Bambu Lab H2D ${n} nozzle`,
|
|
42
61
|
h2s: (n) => `Bambu Lab H2S ${n} nozzle`,
|
|
43
62
|
h2c: (n) => `Bambu Lab H2C ${n} nozzle`,
|
|
63
|
+
x2d: (n) => `Bambu Lab X2D ${n} nozzle`,
|
|
44
64
|
};
|
|
45
65
|
const FILAMENT_PROFILE_DIR = "/Applications/BambuStudio.app/Contents/Resources/profiles/BBL/filament";
|
|
46
66
|
const FILAMENT_MODEL_CODES = {
|
|
@@ -54,6 +74,7 @@ const FILAMENT_MODEL_CODES = {
|
|
|
54
74
|
h2d: "H2D",
|
|
55
75
|
h2s: "H2S",
|
|
56
76
|
h2c: "H2C",
|
|
77
|
+
x2d: "X2D",
|
|
57
78
|
};
|
|
58
79
|
const COLLAR_CHARM_POLICY = getCollarCharmRolePolicy();
|
|
59
80
|
let filamentProfileIndexCache = null;
|
|
@@ -352,6 +373,19 @@ function extractPrinterDiagnostics(status) {
|
|
|
352
373
|
diagnostic_fields: diagnosticFields,
|
|
353
374
|
};
|
|
354
375
|
}
|
|
376
|
+
/** Parse "#RRGGBB;#RRGGBB" (leading '#' optional) into positional slot colours. */
|
|
377
|
+
function parseFilamentColours(value) {
|
|
378
|
+
const colours = value.split(";").map((c) => c.trim()).filter(Boolean)
|
|
379
|
+
.map((c) => (c.startsWith("#") ? c : `#${c}`).toUpperCase());
|
|
380
|
+
for (const c of colours) {
|
|
381
|
+
if (!/^#[0-9A-F]{6}([0-9A-F]{2})?$/.test(c)) {
|
|
382
|
+
throw new Error(`Invalid filament colour "${c}"; expected #RRGGBB.`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (colours.length === 0)
|
|
386
|
+
throw new Error("filament_colours must list at least one colour.");
|
|
387
|
+
return colours;
|
|
388
|
+
}
|
|
355
389
|
function validateBambuModel(model) {
|
|
356
390
|
const normalized = model.trim().toLowerCase();
|
|
357
391
|
if (!VALID_BAMBU_MODELS.includes(normalized)) {
|
|
@@ -743,10 +777,12 @@ if (!fs.existsSync(TEMP_DIR)) {
|
|
|
743
777
|
}
|
|
744
778
|
class BambuPrinterMCPServer {
|
|
745
779
|
constructor() {
|
|
780
|
+
this.blender = new BlenderMcpBridge();
|
|
781
|
+
this.shuttingDown = false;
|
|
746
782
|
this.runtimeConfig = readRuntimeConfig();
|
|
747
783
|
this.server = new Server({
|
|
748
784
|
name: "bambu-printer-mcp",
|
|
749
|
-
version: "
|
|
785
|
+
version: JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")).version
|
|
750
786
|
}, {
|
|
751
787
|
capabilities: {
|
|
752
788
|
resources: {},
|
|
@@ -801,6 +837,7 @@ class BambuPrinterMCPServer {
|
|
|
801
837
|
{ const: "h2d", title: "H2D" },
|
|
802
838
|
{ const: "h2s", title: "H2S" },
|
|
803
839
|
{ const: "h2c", title: "H2C" },
|
|
840
|
+
{ const: "x2d", title: "X2D" },
|
|
804
841
|
],
|
|
805
842
|
},
|
|
806
843
|
},
|
|
@@ -1528,6 +1565,7 @@ class BambuPrinterMCPServer {
|
|
|
1528
1565
|
bambu_token: { type: "string", description: "Access token (default: value from env)" },
|
|
1529
1566
|
load_filaments: { type: "string", description: "Override filament profiles. Semicolon-separated paths to filament JSON configs." },
|
|
1530
1567
|
load_filament_ids: { type: "string", description: "Optional filament-to-object mapping string." },
|
|
1568
|
+
filament_colours: { type: "string", description: "Optional slot colours, one #RRGGBB per filament slot in order, separated by ';' (e.g. '#161616;#FFFFFF'). Defaults to the input 3MF's project colours, else the BambuStudio default." },
|
|
1531
1569
|
ensure_on_bed: { type: "boolean", description: "Lift floating models onto the bed." },
|
|
1532
1570
|
arrange: { type: "boolean", description: "Auto-arrange objects on the build plate." },
|
|
1533
1571
|
orient: { type: "boolean", description: "Auto-orient model for optimal printability." },
|
|
@@ -1582,6 +1620,7 @@ class BambuPrinterMCPServer {
|
|
|
1582
1620
|
load_filaments: { type: "string", description: "Override filament profiles. Semicolon-separated paths to filament JSON configs, e.g. 'pla_basic.json;petg_cf.json'." },
|
|
1583
1621
|
filament_profile: { type: "string", description: "Compatibility alias for load_filaments. Semicolon-separated Orca/Bambu filament profile JSON paths." },
|
|
1584
1622
|
load_filament_ids: { type: "string", description: "Map filaments to objects/parts. Comma-separated IDs matching load_filaments order, e.g. '1,2,3,1' assigns filament 1 to objects 0 and 3." },
|
|
1623
|
+
filament_colours: { type: "string", description: "Optional slot colours, one #RRGGBB per filament slot in order, separated by ';' (e.g. '#161616;#FFFFFF'). Defaults to the input 3MF's project colours, else the BambuStudio default." },
|
|
1585
1624
|
enable_timelapse: { type: "boolean", description: "Insert timelapse parking moves into gcode. The toolhead parks at a fixed position each layer for camera capture. Adds ~10% print time." },
|
|
1586
1625
|
allow_mix_temp: { type: "boolean", description: "Allow filaments with different temperature requirements on the same plate. Required for multi-material prints mixing e.g. PLA and PETG." },
|
|
1587
1626
|
scale: { type: "number", description: "Uniform scale factor applied to all axes. 1.0 = original size, 2.0 = double, 0.5 = half. Applied before slicing." },
|
|
@@ -2086,7 +2125,7 @@ class BambuPrinterMCPServer {
|
|
|
2086
2125
|
bambu_model: {
|
|
2087
2126
|
type: "string",
|
|
2088
2127
|
enum: [...VALID_BAMBU_MODELS],
|
|
2089
|
-
description: "REQUIRED: Bambu Lab printer model. H2D, H2S, and H2C are the primary intended paths."
|
|
2128
|
+
description: "REQUIRED: Bambu Lab printer model. H2D, H2S, and H2C are the primary intended paths. X2D direct printing is not supported."
|
|
2090
2129
|
},
|
|
2091
2130
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2092
2131
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
@@ -2140,28 +2179,62 @@ class BambuPrinterMCPServer {
|
|
|
2140
2179
|
required: ["stl_path"]
|
|
2141
2180
|
}
|
|
2142
2181
|
},
|
|
2182
|
+
{
|
|
2183
|
+
name: "blender_mcp_status",
|
|
2184
|
+
description: "Inspect Blender MCP configuration or connect and discover the remote server's tools and schemas. Connecting does not edit the scene; use get_scene_info through blender_mcp_call to check the Blender addon.",
|
|
2185
|
+
inputSchema: {
|
|
2186
|
+
type: "object",
|
|
2187
|
+
properties: {
|
|
2188
|
+
connect: { type: "boolean", description: "Initialize the configured stdio MCP server and discover its tools (default false)." },
|
|
2189
|
+
timeout_ms: { type: "integer", minimum: 100, maximum: 300000, description: "Total connection and discovery deadline in milliseconds; defaults to BLENDER_MCP_TIMEOUT_MS or 120000." }
|
|
2190
|
+
},
|
|
2191
|
+
additionalProperties: false
|
|
2192
|
+
}
|
|
2193
|
+
},
|
|
2194
|
+
{
|
|
2195
|
+
name: "blender_mcp_call",
|
|
2196
|
+
description: "Call a discovered tool on the configured Blender MCP server, preserving its full MCP content and errors. Discover tool schemas with blender_mcp_status first; execute_blender_code accepts Python code and user_prompt. Calls can modify the active Blender scene and are never automatically retried.",
|
|
2197
|
+
inputSchema: {
|
|
2198
|
+
type: "object",
|
|
2199
|
+
properties: {
|
|
2200
|
+
tool_name: { type: "string", description: "Exact name advertised by Blender MCP, such as get_scene_info or execute_blender_code." },
|
|
2201
|
+
arguments: { type: "object", description: "Arguments matching the remote tool's discovered input schema. Preserve the user's own words in user_prompt when the remote tool requests it." },
|
|
2202
|
+
timeout_ms: { type: "integer", minimum: 100, maximum: 300000, description: "Total connection, discovery, and tool deadline in milliseconds; defaults to BLENDER_MCP_TIMEOUT_MS or 120000." }
|
|
2203
|
+
},
|
|
2204
|
+
required: ["tool_name"],
|
|
2205
|
+
additionalProperties: false
|
|
2206
|
+
}
|
|
2207
|
+
},
|
|
2143
2208
|
{
|
|
2144
2209
|
name: "blender_mcp_edit_model",
|
|
2145
|
-
description: "
|
|
2210
|
+
description: "Import, edit, and export a local STL through standard Blender MCP with verified output and existing scene objects preserved. Requires a shared local filesystem and Blender Object Mode. Also supports a separately configured legacy executable bridge.",
|
|
2146
2211
|
inputSchema: {
|
|
2147
2212
|
type: "object",
|
|
2148
2213
|
properties: {
|
|
2149
2214
|
stl_path: { type: "string", description: "Path to the local STL file" },
|
|
2150
2215
|
operations: {
|
|
2151
2216
|
type: "array",
|
|
2152
|
-
description: "Ordered
|
|
2217
|
+
description: "Ordered operations: decimate:<ratio greater than 0 and at most 1>, remesh:<positive voxel size in STL units>, boolean_union:<STL path>. Legacy custom bridges define their own operations.",
|
|
2218
|
+
minItems: 1,
|
|
2219
|
+
maxItems: 64,
|
|
2153
2220
|
items: { type: "string" }
|
|
2154
2221
|
},
|
|
2155
|
-
|
|
2156
|
-
|
|
2222
|
+
output_path: { type: "string", description: "Required for standard MCP previews and execution: new local STL output path whose parent exists. Existing files are never overwritten. Optional for legacy-only bridge configuration." },
|
|
2223
|
+
user_prompt: { type: "string", description: "The user's own words describing the edit, passed unchanged to Blender MCP." },
|
|
2224
|
+
timeout_ms: { type: "integer", minimum: 100, maximum: 300000, description: "Total Blender request deadline in milliseconds; defaults to BLENDER_MCP_TIMEOUT_MS or 120000." },
|
|
2225
|
+
bridge_command: { type: "string", description: "Legacy custom bridge executable override, not a standard MCP command. Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1." },
|
|
2226
|
+
execute: { type: "boolean", description: "Apply edits and export (true) or validate and return the prepared request without connecting (false, default)." }
|
|
2157
2227
|
},
|
|
2158
|
-
required:
|
|
2228
|
+
required: process.env.BLENDER_MCP_COMMAND?.trim()
|
|
2229
|
+
? ["stl_path", "operations", "output_path"]
|
|
2230
|
+
: ["stl_path", "operations"],
|
|
2231
|
+
additionalProperties: false
|
|
2159
2232
|
}
|
|
2160
2233
|
}
|
|
2161
2234
|
]
|
|
2162
2235
|
};
|
|
2163
2236
|
});
|
|
2164
|
-
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
2237
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
2165
2238
|
const { name, arguments: args } = request.params;
|
|
2166
2239
|
const host = String(args?.host || DEFAULT_HOST);
|
|
2167
2240
|
const bambuSerial = String(args?.bambu_serial || DEFAULT_BAMBU_SERIAL);
|
|
@@ -2288,23 +2361,27 @@ class BambuPrinterMCPServer {
|
|
|
2288
2361
|
}
|
|
2289
2362
|
break;
|
|
2290
2363
|
}
|
|
2291
|
-
case "upload_file":
|
|
2364
|
+
case "upload_file": {
|
|
2292
2365
|
if (!args?.file_path || !args?.filename) {
|
|
2293
2366
|
throw new Error("Missing required parameters: file_path and filename");
|
|
2294
2367
|
}
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2368
|
+
const print = Boolean(args.print ?? false);
|
|
2369
|
+
const uploadModel = print ? await this.resolveBambuModel(args?.bambu_model) : undefined;
|
|
2370
|
+
if (print)
|
|
2371
|
+
assertDirectPrintSupported(uploadModel, bambuSerial);
|
|
2372
|
+
result = await this.bambu.uploadFile(host, bambuSerial, bambuToken, String(args.file_path), String(args.filename), print, uploadModel);
|
|
2299
2373
|
break;
|
|
2374
|
+
}
|
|
2300
2375
|
case "start_print":
|
|
2301
|
-
case "start_print_job":
|
|
2376
|
+
case "start_print_job": {
|
|
2302
2377
|
if (!args?.filename) {
|
|
2303
2378
|
throw new Error("Missing required parameter: filename");
|
|
2304
2379
|
}
|
|
2305
|
-
await this.resolveBambuModel(args?.bambu_model);
|
|
2306
|
-
|
|
2380
|
+
const startModel = await this.resolveBambuModel(args?.bambu_model);
|
|
2381
|
+
assertDirectPrintSupported(startModel, bambuSerial);
|
|
2382
|
+
result = await this.bambu.startJob(host, bambuSerial, bambuToken, String(args.filename), startModel);
|
|
2307
2383
|
break;
|
|
2384
|
+
}
|
|
2308
2385
|
case "cancel_print":
|
|
2309
2386
|
result = await this.bambu.cancelJob(host, bambuSerial, bambuToken);
|
|
2310
2387
|
break;
|
|
@@ -2444,6 +2521,8 @@ class BambuPrinterMCPServer {
|
|
|
2444
2521
|
}
|
|
2445
2522
|
if (args?.load_filament_ids !== undefined)
|
|
2446
2523
|
sliceBambuOptions.loadFilamentIds = String(args.load_filament_ids);
|
|
2524
|
+
if (args?.filament_colours !== undefined)
|
|
2525
|
+
sliceBambuOptions.filamentColours = parseFilamentColours(String(args.filament_colours));
|
|
2447
2526
|
sliceBambuOptions.bedType = resolveBambuStudioCliBedType(args?.bed_type);
|
|
2448
2527
|
if (args?.enable_timelapse !== undefined)
|
|
2449
2528
|
sliceBambuOptions.enableTimelapse = Boolean(args.enable_timelapse);
|
|
@@ -2521,6 +2600,8 @@ class BambuPrinterMCPServer {
|
|
|
2521
2600
|
}
|
|
2522
2601
|
if (args?.load_filament_ids !== undefined)
|
|
2523
2602
|
sliceBambuOptions.loadFilamentIds = String(args.load_filament_ids);
|
|
2603
|
+
if (args?.filament_colours !== undefined)
|
|
2604
|
+
sliceBambuOptions.filamentColours = parseFilamentColours(String(args.filament_colours));
|
|
2524
2605
|
sliceBambuOptions.bedType = resolveBambuStudioCliBedType(args?.bed_type);
|
|
2525
2606
|
if (args?.enable_timelapse !== undefined)
|
|
2526
2607
|
sliceBambuOptions.enableTimelapse = Boolean(args.enable_timelapse);
|
|
@@ -2573,6 +2654,7 @@ class BambuPrinterMCPServer {
|
|
|
2573
2654
|
}
|
|
2574
2655
|
const { slicerType, slicerPath, slicerProfile } = this.resolveSlicerConfigFromArgs(args, "print_3mf");
|
|
2575
2656
|
const printModel = await this.resolveBambuModel(args?.bambu_model);
|
|
2657
|
+
assertDirectPrintSupported(printModel, bambuSerial);
|
|
2576
2658
|
const printBedType = resolveBedType(args?.bed_type);
|
|
2577
2659
|
const printNozzle = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
2578
2660
|
const activeSlicerProfile = await resolveTemplateFirstSlicerProfilePath(args, slicerProfile || undefined, resolveTemplatePathFromName(["json", "config", "3mf"]) || explicitTemplatePath || undefined, TEMP_DIR);
|
|
@@ -2583,45 +2665,37 @@ class BambuPrinterMCPServer {
|
|
|
2583
2665
|
throw new Error("plate_index must be a non-negative integer.");
|
|
2584
2666
|
}
|
|
2585
2667
|
let threeMFPath = String(args.three_mf_path);
|
|
2586
|
-
//
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
if (
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
autoSliceOptions.loadFilaments = liveFilaments.recommended.load_filaments;
|
|
2609
|
-
}
|
|
2610
|
-
}
|
|
2611
|
-
catch (filamentError) {
|
|
2612
|
-
console.warn("Could not resolve live printer filaments for auto-slicing:", filamentError);
|
|
2668
|
+
// Inspect and auto-slice before upload. Preserve any failure instead of sending an unsliced project.
|
|
2669
|
+
const JSZip = (await import('jszip')).default;
|
|
2670
|
+
const zipData = fs.readFileSync(threeMFPath);
|
|
2671
|
+
const zip = await JSZip.loadAsync(zipData);
|
|
2672
|
+
const hasGcode = Object.values(zip.files).some(entry => !entry.dir && /\.gcode$/i.test(entry.name));
|
|
2673
|
+
if (!hasGcode) {
|
|
2674
|
+
if (printBedType === "supertack_plate") {
|
|
2675
|
+
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
|
+
}
|
|
2677
|
+
console.log(`3MF has no gcode — auto-slicing with ${slicerType} for ${printModel}`);
|
|
2678
|
+
const autoSliceOptions = {
|
|
2679
|
+
uptodate: true,
|
|
2680
|
+
ensureOnBed: true,
|
|
2681
|
+
minSave: true,
|
|
2682
|
+
skipModifiedGcodes: true,
|
|
2683
|
+
bedType: printBedType,
|
|
2684
|
+
};
|
|
2685
|
+
if (!explicitSlicerProfile) {
|
|
2686
|
+
try {
|
|
2687
|
+
const liveFilaments = await this.getResolvedPrinterFilamentInventory(host, bambuSerial, bambuToken, printModel, printNozzle);
|
|
2688
|
+
if (liveFilaments.recommended?.load_filaments) {
|
|
2689
|
+
autoSliceOptions.loadFilaments = liveFilaments.recommended.load_filaments;
|
|
2613
2690
|
}
|
|
2614
2691
|
}
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
}
|
|
2619
|
-
}
|
|
2620
|
-
catch (sliceCheckErr) {
|
|
2621
|
-
if (String(sliceCheckErr?.message || "").includes("SuperTack")) {
|
|
2622
|
-
throw sliceCheckErr;
|
|
2692
|
+
catch (filamentError) {
|
|
2693
|
+
console.warn("Could not resolve live printer filaments for auto-slicing:", filamentError);
|
|
2694
|
+
}
|
|
2623
2695
|
}
|
|
2624
|
-
|
|
2696
|
+
threeMFPath = await this.stlManipulator.sliceSTL(threeMFPath, slicerType, slicerPath, activeSlicerProfile, undefined, // progressCallback
|
|
2697
|
+
printPreset, autoSliceOptions);
|
|
2698
|
+
console.log("Auto-sliced to: " + threeMFPath);
|
|
2625
2699
|
}
|
|
2626
2700
|
const parsed3MFData = await parse3MF(threeMFPath);
|
|
2627
2701
|
const isH2Print = H2_BAMBU_MODELS.has(printModel);
|
|
@@ -2721,6 +2795,7 @@ class BambuPrinterMCPServer {
|
|
|
2721
2795
|
}
|
|
2722
2796
|
const { slicerType, slicerPath, slicerProfile } = this.resolveSlicerConfigFromArgs(args, "print_collar_charm");
|
|
2723
2797
|
const printModel = await this.resolveBambuModel(args?.bambu_model);
|
|
2798
|
+
assertDirectPrintSupported(printModel, bambuSerial);
|
|
2724
2799
|
const printBedType = resolveBedType(args?.bed_type);
|
|
2725
2800
|
const printNozzle = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
2726
2801
|
const activeSlicerProfile = await resolveTemplateFirstSlicerProfilePath(args, slicerProfile || undefined, resolvedTemplateSourcePath || explicitTemplatePath || undefined, TEMP_DIR);
|
|
@@ -2783,16 +2858,13 @@ class BambuPrinterMCPServer {
|
|
|
2783
2858
|
throw new Error("Missing required parameter: stl_path");
|
|
2784
2859
|
result = await this.stlManipulator.layFlat(String(args.stl_path));
|
|
2785
2860
|
break;
|
|
2861
|
+
case "blender_mcp_status":
|
|
2862
|
+
result = await this.blender.status(args ?? {}, extra.signal);
|
|
2863
|
+
break;
|
|
2864
|
+
case "blender_mcp_call":
|
|
2865
|
+
return await this.blender.call(args ?? {}, extra.signal);
|
|
2786
2866
|
case "blender_mcp_edit_model":
|
|
2787
|
-
|
|
2788
|
-
throw new Error("Missing required parameters: stl_path and operations");
|
|
2789
|
-
}
|
|
2790
|
-
result = await this.invokeBlenderBridge({
|
|
2791
|
-
stlPath: String(args.stl_path),
|
|
2792
|
-
operations: args.operations.map((entry) => String(entry)),
|
|
2793
|
-
execute: Boolean(args.execute ?? false),
|
|
2794
|
-
bridgeCommand: this.resolveExecutableSelectorArg(args.bridge_command, "blender_mcp_edit_model", "bridge_command", true) ?? this.runtimeConfig.blenderBridgeCommand,
|
|
2795
|
-
});
|
|
2867
|
+
result = await this.blender.edit(args ?? {}, this.resolveExecutableSelectorArg(args?.bridge_command, "blender_mcp_edit_model", "bridge_command", true) ?? (!process.env.BLENDER_MCP_COMMAND?.trim() ? this.runtimeConfig.blenderBridgeCommand : undefined), extra.signal);
|
|
2796
2868
|
break;
|
|
2797
2869
|
default:
|
|
2798
2870
|
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
|
@@ -2823,38 +2895,28 @@ class BambuPrinterMCPServer {
|
|
|
2823
2895
|
}
|
|
2824
2896
|
});
|
|
2825
2897
|
}
|
|
2826
|
-
async invokeBlenderBridge(params) {
|
|
2827
|
-
const payload = {
|
|
2828
|
-
stlPath: params.stlPath,
|
|
2829
|
-
operations: params.operations,
|
|
2830
|
-
};
|
|
2831
|
-
if (!params.execute || !params.bridgeCommand) {
|
|
2832
|
-
return {
|
|
2833
|
-
status: "prepared",
|
|
2834
|
-
payload,
|
|
2835
|
-
note: params.bridgeCommand
|
|
2836
|
-
? "Set execute=true to run the Blender bridge command."
|
|
2837
|
-
: "No BLENDER_MCP_BRIDGE_COMMAND configured. Set the env var or pass bridge_command.",
|
|
2838
|
-
};
|
|
2839
|
-
}
|
|
2840
|
-
const { execFile } = await import("node:child_process");
|
|
2841
|
-
const { promisify } = await import("node:util");
|
|
2842
|
-
const execFileAsync = promisify(execFile);
|
|
2843
|
-
const { stdout, stderr } = await execFileAsync(params.bridgeCommand, [], {
|
|
2844
|
-
env: { ...process.env, MCP_BLENDER_PAYLOAD: JSON.stringify(payload) },
|
|
2845
|
-
timeout: 120000,
|
|
2846
|
-
});
|
|
2847
|
-
return {
|
|
2848
|
-
status: "executed",
|
|
2849
|
-
stdout: stdout.trim(),
|
|
2850
|
-
stderr: stderr.trim(),
|
|
2851
|
-
};
|
|
2852
|
-
}
|
|
2853
2898
|
async startStdio() {
|
|
2854
2899
|
const transport = new StdioServerTransport();
|
|
2855
2900
|
await this.server.connect(transport);
|
|
2901
|
+
process.stdin.once("end", () => { void this.shutdown(); });
|
|
2856
2902
|
console.error("Bambu Printer MCP server running on stdio");
|
|
2857
2903
|
}
|
|
2904
|
+
async shutdown(exitCode = 0) {
|
|
2905
|
+
if (this.shuttingDown)
|
|
2906
|
+
return;
|
|
2907
|
+
this.shuttingDown = true;
|
|
2908
|
+
try {
|
|
2909
|
+
// Closing the MCP transport aborts its active requests before exit cleanup.
|
|
2910
|
+
await this.server.close();
|
|
2911
|
+
this.httpRuntime?.httpServer.close();
|
|
2912
|
+
}
|
|
2913
|
+
catch (error) {
|
|
2914
|
+
console.error("[MCP Shutdown]", error);
|
|
2915
|
+
}
|
|
2916
|
+
finally {
|
|
2917
|
+
process.exit(exitCode);
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2858
2920
|
async startHttp() {
|
|
2859
2921
|
const { httpHost, httpPort, httpPath, statefulSession, enableJsonResponse, allowedOrigins } = this.runtimeConfig;
|
|
2860
2922
|
const transport = new StreamableHTTPServerTransport({
|
|
@@ -2894,4 +2956,6 @@ class BambuPrinterMCPServer {
|
|
|
2894
2956
|
}
|
|
2895
2957
|
}
|
|
2896
2958
|
const server = new BambuPrinterMCPServer();
|
|
2959
|
+
process.once("SIGINT", () => { void server.shutdown(130); });
|
|
2960
|
+
process.once("SIGTERM", () => { void server.shutdown(143); });
|
|
2897
2961
|
server.run().catch(console.error);
|
package/dist/printers/bambu.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export declare function assertDirectPrintSupported(model: string | undefined, serial?: string): void;
|
|
1
2
|
interface BambuPrintOptionsInternal {
|
|
2
3
|
projectName: string;
|
|
3
4
|
filePath: string;
|
|
@@ -67,8 +68,8 @@ export declare class BambuImplementation {
|
|
|
67
68
|
name: string;
|
|
68
69
|
exists: boolean;
|
|
69
70
|
}>;
|
|
70
|
-
uploadFile(host: string, serial: string, token: string, filePath: string, filename: string, print: boolean): Promise<Record<string, unknown>>;
|
|
71
|
-
startJob(host: string, serial: string, token: string, filename: string): Promise<{
|
|
71
|
+
uploadFile(host: string, serial: string, token: string, filePath: string, filename: string, print: boolean, bambuModel?: string): Promise<Record<string, unknown>>;
|
|
72
|
+
startJob(host: string, serial: string, token: string, filename: string, bambuModel?: string): Promise<{
|
|
72
73
|
status: string;
|
|
73
74
|
message: string;
|
|
74
75
|
file: string;
|
package/dist/printers/bambu.js
CHANGED
|
@@ -37,6 +37,7 @@ const MODEL_ID_TO_NAME = {
|
|
|
37
37
|
O1D: "H2D",
|
|
38
38
|
O1E: "H2D Pro",
|
|
39
39
|
O1S: "H2S",
|
|
40
|
+
N6: "X2D",
|
|
40
41
|
N2S: "A1",
|
|
41
42
|
A1M: "A1 Mini",
|
|
42
43
|
C11: "P1P",
|
|
@@ -46,9 +47,18 @@ const MODEL_ID_TO_NAME = {
|
|
|
46
47
|
C13: "X1E",
|
|
47
48
|
};
|
|
48
49
|
const H2_MODEL_NAMES = new Set(["h2", "h2c", "h2d", "h2dpro", "h2d pro", "h2s"]);
|
|
50
|
+
export function assertDirectPrintSupported(model, serial) {
|
|
51
|
+
if (model?.trim().toLowerCase() === "x2d" || serial?.trim().toUpperCase().startsWith("20P")) {
|
|
52
|
+
throw new Error("X2D direct printing is not supported pending native eMMC transport support. " +
|
|
53
|
+
"X2D status and slicing remain available; use a supported slicer to print.");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
49
56
|
function isH2ModelName(model) {
|
|
50
57
|
return H2_MODEL_NAMES.has(String(model ?? "").trim().toLowerCase().replace(/\s+/g, " "));
|
|
51
58
|
}
|
|
59
|
+
function isP2SModelName(model) {
|
|
60
|
+
return String(model ?? "").trim().toLowerCase() === "p2s";
|
|
61
|
+
}
|
|
52
62
|
function sleep(ms) {
|
|
53
63
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
54
64
|
}
|
|
@@ -88,6 +98,12 @@ class TolerantBambuClient extends BambuClient {
|
|
|
88
98
|
return "H2S";
|
|
89
99
|
if (sn.startsWith("094"))
|
|
90
100
|
return "H2D";
|
|
101
|
+
if (sn.startsWith("239"))
|
|
102
|
+
return "H2C";
|
|
103
|
+
if (sn.startsWith("31B"))
|
|
104
|
+
return "H2DPRO";
|
|
105
|
+
if (sn.startsWith("20P"))
|
|
106
|
+
return "X2D";
|
|
91
107
|
if (sn.startsWith("00M"))
|
|
92
108
|
return "X1C";
|
|
93
109
|
if (sn.startsWith("00W"))
|
|
@@ -98,6 +114,8 @@ class TolerantBambuClient extends BambuClient {
|
|
|
98
114
|
return "P1P";
|
|
99
115
|
if (sn.startsWith("01P"))
|
|
100
116
|
return "P1S";
|
|
117
|
+
if (sn.startsWith("22E"))
|
|
118
|
+
return "P2S";
|
|
101
119
|
if (sn.startsWith("030"))
|
|
102
120
|
return "A1";
|
|
103
121
|
if (sn.startsWith("039"))
|
|
@@ -448,6 +466,7 @@ export class BambuImplementation {
|
|
|
448
466
|
}
|
|
449
467
|
}
|
|
450
468
|
async print3mf(host, serial, token, options) {
|
|
469
|
+
assertDirectPrintSupported(options.bambuModel, serial);
|
|
451
470
|
if (!options.filePath.toLowerCase().endsWith(".3mf")) {
|
|
452
471
|
throw new Error("print3mf requires a .3mf input file.");
|
|
453
472
|
}
|
|
@@ -457,24 +476,33 @@ export class BambuImplementation {
|
|
|
457
476
|
let remoteFileName = path.basename(options.filePath);
|
|
458
477
|
remoteFileName = remoteFileName.replace(/\.gcode\.3mf\.gcode\.3mf$/i, ".gcode.3mf");
|
|
459
478
|
// H2-series printers land files at the FTP root and reference them via ftp:///<name>.
|
|
460
|
-
//
|
|
479
|
+
// Full-size A1 uses SD root/project_file (reported on firmware 01.08.01.00).
|
|
480
|
+
// P1/X1/A1 mini retain their legacy cache/gcode_file route.
|
|
481
|
+
// P2S keeps the /cache/<name> upload but only accepts the H2-style
|
|
482
|
+
// project_file command, referenced via ftp:///cache/<name> (verified on
|
|
483
|
+
// P2S firmware 01.02.00.00; file:///sdcard/... fails with ERROR STATE).
|
|
461
484
|
const isH2 = serial.startsWith("093") ||
|
|
462
485
|
serial.startsWith("094") ||
|
|
463
486
|
isH2ModelName(options.bambuModel);
|
|
464
|
-
const
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
|
|
487
|
+
const isP2S = serial.startsWith("22E") || isP2SModelName(options.bambuModel);
|
|
488
|
+
const isA1 = String(options.bambuModel ?? "").trim().toLowerCase() === "a1" ||
|
|
489
|
+
(!options.bambuModel && serial.startsWith("030"));
|
|
490
|
+
const usesH2ProjectFile = isH2 || isP2S;
|
|
491
|
+
const remoteProjectPath = isH2 || isA1 ? remoteFileName : `cache/${remoteFileName}`;
|
|
492
|
+
const remoteUploadPath = `/${remoteProjectPath}`;
|
|
493
|
+
const projectUrl = usesH2ProjectFile
|
|
494
|
+
? `ftp:///${remoteProjectPath}`
|
|
468
495
|
: `file:///sdcard/${remoteProjectPath}`;
|
|
469
496
|
// Upload via basic-ftp directly (bypasses bambu-js double-path bug)
|
|
470
497
|
await this.ftpUpload(host, token, options.filePath, remoteUploadPath);
|
|
471
498
|
// Pre-sliced .gcode.3mf files: routing depends on firmware generation.
|
|
472
|
-
// P1/A1
|
|
499
|
+
// Legacy P1/X1/A1 mini: project_file returns 405004002 for .gcode.3mf (firmware
|
|
473
500
|
// doesn't recognise the container), so use gcode_file instead.
|
|
474
|
-
// H2-series: gcode_file is not supported
|
|
501
|
+
// H2-series and P2S: gcode_file is not supported (P2S answers 0500-4002
|
|
502
|
+
// "Unsupported file path or name"); project_file works because the
|
|
475
503
|
// firmware can open the zip and find Metadata/plate_<n>.gcode directly.
|
|
476
504
|
if (options.filePath.toLowerCase().endsWith(".gcode.3mf")) {
|
|
477
|
-
if (!
|
|
505
|
+
if (!usesH2ProjectFile && !isA1) {
|
|
478
506
|
const printer = await this.getPrinter(host, serial, token);
|
|
479
507
|
await invokeWithoutAck(printer, new GCodeFileCommand({ fileName: remoteProjectPath }));
|
|
480
508
|
return {
|
|
@@ -483,7 +511,7 @@ export class BambuImplementation {
|
|
|
483
511
|
remoteProjectPath,
|
|
484
512
|
};
|
|
485
513
|
}
|
|
486
|
-
// H2-series: fall through to project_file path below
|
|
514
|
+
// H2-series and P2S: fall through to project_file path below
|
|
487
515
|
}
|
|
488
516
|
const projectMetadata = await this.resolveProjectFileMetadata(options.filePath, options.plateIndex);
|
|
489
517
|
// Send project_file command via bambu-node MQTT (bypasses bambu-js
|
|
@@ -550,7 +578,7 @@ export class BambuImplementation {
|
|
|
550
578
|
}
|
|
551
579
|
let amsMapping;
|
|
552
580
|
let amsMapping2;
|
|
553
|
-
if (
|
|
581
|
+
if (usesH2ProjectFile) {
|
|
554
582
|
const projLen = Math.max(projectMetadata.projectFilamentCount, baseMapping.length, 1);
|
|
555
583
|
amsMapping = Array.from({ length: projLen }, (_, i) => i < baseMapping.length ? baseMapping[i] : -1);
|
|
556
584
|
amsMapping2 = amsMapping.map((v) => {
|
|
@@ -564,12 +592,12 @@ export class BambuImplementation {
|
|
|
564
592
|
});
|
|
565
593
|
}
|
|
566
594
|
else {
|
|
567
|
-
amsMapping = Array.from({ length: 5 }, (_, i) => i < baseMapping.length ? baseMapping[i] : -1);
|
|
595
|
+
amsMapping = Array.from({ length: Math.max(5, baseMapping.length, projectMetadata.projectFilamentCount) }, (_, i) => i < baseMapping.length ? baseMapping[i] : -1);
|
|
568
596
|
amsMapping2 = [];
|
|
569
597
|
}
|
|
570
598
|
const b = (v) => (v ? 1 : 0);
|
|
571
599
|
let projectFileCmd;
|
|
572
|
-
if (
|
|
600
|
+
if (usesH2ProjectFile) {
|
|
573
601
|
const submissionId = String(Date.now() & 0x7fffffff);
|
|
574
602
|
projectFileCmd = {
|
|
575
603
|
print: {
|
|
@@ -911,7 +939,9 @@ export class BambuImplementation {
|
|
|
911
939
|
exists,
|
|
912
940
|
};
|
|
913
941
|
}
|
|
914
|
-
async uploadFile(host, serial, token, filePath, filename, print) {
|
|
942
|
+
async uploadFile(host, serial, token, filePath, filename, print, bambuModel) {
|
|
943
|
+
if (print)
|
|
944
|
+
assertDirectPrintSupported(bambuModel, serial);
|
|
915
945
|
await fs.access(filePath);
|
|
916
946
|
const normalizedFileName = filename.replace(/^\/+/, "");
|
|
917
947
|
const remotePath = normalizedFileName.includes("/")
|
|
@@ -927,7 +957,7 @@ export class BambuImplementation {
|
|
|
927
957
|
};
|
|
928
958
|
if (print) {
|
|
929
959
|
if (remotePath.toLowerCase().endsWith(".gcode")) {
|
|
930
|
-
response.startResult = await this.startJob(host, serial, token, remotePath);
|
|
960
|
+
response.startResult = await this.startJob(host, serial, token, remotePath, bambuModel);
|
|
931
961
|
}
|
|
932
962
|
else if (remotePath.toLowerCase().endsWith(".3mf")) {
|
|
933
963
|
response.note =
|
|
@@ -939,7 +969,8 @@ export class BambuImplementation {
|
|
|
939
969
|
}
|
|
940
970
|
return response;
|
|
941
971
|
}
|
|
942
|
-
async startJob(host, serial, token, filename) {
|
|
972
|
+
async startJob(host, serial, token, filename, bambuModel) {
|
|
973
|
+
assertDirectPrintSupported(bambuModel, serial);
|
|
943
974
|
const lower = filename.toLowerCase();
|
|
944
975
|
if (lower.endsWith(".3mf") && !lower.endsWith(".gcode.3mf")) {
|
|
945
976
|
throw new Error("Use print_3mf for .3mf project files.");
|
|
@@ -989,15 +1020,15 @@ export class BambuImplementation {
|
|
|
989
1020
|
// P1/A1 series still use the proprietary TCP-on-6000 framed JPEG path
|
|
990
1021
|
// (per https://github.com/Doridian/OpenBambuAPI/blob/main/video.md).
|
|
991
1022
|
const TCP_CAMERA_MODELS = new Set(["a1", "a1mini", "p1s", "p1p"]);
|
|
992
|
-
// X1, P2S,
|
|
1023
|
+
// X1, P2S, H2 (H2S/H2D/H2C), AND X2D all use RTSP on port 322. The
|
|
993
1024
|
// OpenBambuAPI doc only mentions X1/P2S, but the HA bambulab
|
|
994
1025
|
// integration's models.py shows the printer reports its own
|
|
995
1026
|
// `ipcam.rtsp_url` for these models, and Parker (H2S) rejects the
|
|
996
1027
|
// A1/P1 80-byte auth packet on port 6000 (verified 2026-04-27 --
|
|
997
|
-
//
|
|
1028
|
+
// confirmed by local H2 camera transport probes).
|
|
998
1029
|
const RTSP_MODELS = new Set([
|
|
999
1030
|
"x1", "x1c", "x1carbon", "x1e", "p2s",
|
|
1000
|
-
"h2", "h2s", "h2d", "h2c", "h2dpro",
|
|
1031
|
+
"h2", "h2s", "h2d", "h2c", "h2dpro", "x2d",
|
|
1001
1032
|
]);
|
|
1002
1033
|
if (!model) {
|
|
1003
1034
|
throw new Error("camera_snapshot requires bambu_model or BAMBU_MODEL so it can choose the correct Bambu camera protocol.");
|