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/src/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
14
14
|
import dotenv from "dotenv";
|
|
15
15
|
import fs from "fs";
|
|
16
|
+
import os from "node:os";
|
|
16
17
|
import path from "path";
|
|
17
18
|
import { createServer as createHttpServer, type IncomingMessage, type Server as HttpServer, type ServerResponse } from "node:http";
|
|
18
19
|
import { randomUUID } from "node:crypto";
|
|
@@ -24,10 +25,11 @@ import {
|
|
|
24
25
|
type SlicerType,
|
|
25
26
|
} from "./stl/stl-manipulator.js";
|
|
26
27
|
import { BambuNetworkBridge, type BambuNetworkBridgeOptions } from "./bambu-network-bridge.js";
|
|
28
|
+
import { BlenderMcpBridge } from "./blender-mcp-bridge.js";
|
|
27
29
|
import { hasAmsMappingInput, normalizeAmsMappingObject, normalizeBridgeAmsTrayValue } from "./ams-mapping.js";
|
|
28
30
|
import { analyze3MFAmsRequirements, analyze3MFPlateObjects, analyzeCollarCharm3MF, extractBambuTemplateSettings, getCollarCharmRolePolicy, parse3MF } from './3mf_parser.js';
|
|
29
31
|
import type { ThreeMFAmsRequirements } from "./types.js";
|
|
30
|
-
import { BambuImplementation } from "./printers/bambu.js";
|
|
32
|
+
import { BambuImplementation, assertDirectPrintSupported } from "./printers/bambu.js";
|
|
31
33
|
|
|
32
34
|
dotenv.config();
|
|
33
35
|
|
|
@@ -36,7 +38,23 @@ const DEFAULT_BAMBU_SERIAL = process.env.BAMBU_PRINTER_SERIAL || process.env.BAM
|
|
|
36
38
|
const DEFAULT_BAMBU_TOKEN =
|
|
37
39
|
process.env.BAMBU_PRINTER_ACCESS_TOKEN || process.env.BAMBU_TOKEN || "";
|
|
38
40
|
const DEFAULT_BAMBU_DEV_ID = process.env.BAMBU_DEV_ID || DEFAULT_BAMBU_SERIAL;
|
|
39
|
-
|
|
41
|
+
// os.tmpdir(), not process.cwd() — a packaged Claude Desktop extension is
|
|
42
|
+
// spawned with a cwd we don't control (and may not be writable), so a
|
|
43
|
+
// cwd-relative default here can throw at module load via the mkdirSync
|
|
44
|
+
// below and kill the server before it ever opens the stdio transport.
|
|
45
|
+
const AUTOMATIC_TEMP_DIR = process.env.TEMP_DIR ? undefined : fs.mkdtempSync(path.join(os.tmpdir(), "bambu-printer-mcp-"));
|
|
46
|
+
const TEMP_DIR = process.env.TEMP_DIR || AUTOMATIC_TEMP_DIR!;
|
|
47
|
+
if (AUTOMATIC_TEMP_DIR) {
|
|
48
|
+
// The exit event also covers startup failures and normal event-loop exit.
|
|
49
|
+
// Only the directory created above belongs to us; explicit TEMP_DIR is retained.
|
|
50
|
+
process.once("exit", () => {
|
|
51
|
+
try {
|
|
52
|
+
fs.rmSync(AUTOMATIC_TEMP_DIR, { recursive: true, force: true });
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error("Unable to remove the server's temporary directory:", error);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
40
58
|
|
|
41
59
|
// Printer model and bed type
|
|
42
60
|
const DEFAULT_BAMBU_MODEL =
|
|
@@ -46,7 +64,7 @@ const DEFAULT_BAMBU_MODEL =
|
|
|
46
64
|
const DEFAULT_BED_TYPE = process.env.BED_TYPE?.trim().toLowerCase() || "textured_plate";
|
|
47
65
|
const DEFAULT_NOZZLE_DIAMETER = process.env.NOZZLE_DIAMETER?.trim() || "0.4";
|
|
48
66
|
|
|
49
|
-
const VALID_BAMBU_MODELS = ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s", "h2c"] as const;
|
|
67
|
+
const VALID_BAMBU_MODELS = ["p1s", "p1p", "p2s", "x1c", "x1e", "a1", "a1mini", "h2d", "h2s", "h2c", "x2d"] as const;
|
|
50
68
|
type BambuModel = typeof VALID_BAMBU_MODELS[number];
|
|
51
69
|
const H2_BAMBU_MODELS = new Set<string>(["h2d", "h2s", "h2c"]);
|
|
52
70
|
|
|
@@ -65,6 +83,7 @@ const BAMBU_MODEL_PRESETS: Record<string, (nozzle: string) => string> = {
|
|
|
65
83
|
h2d: (n) => `Bambu Lab H2D ${n} nozzle`,
|
|
66
84
|
h2s: (n) => `Bambu Lab H2S ${n} nozzle`,
|
|
67
85
|
h2c: (n) => `Bambu Lab H2C ${n} nozzle`,
|
|
86
|
+
x2d: (n) => `Bambu Lab X2D ${n} nozzle`,
|
|
68
87
|
};
|
|
69
88
|
|
|
70
89
|
const FILAMENT_PROFILE_DIR =
|
|
@@ -80,6 +99,7 @@ const FILAMENT_MODEL_CODES: Record<string, string> = {
|
|
|
80
99
|
h2d: "H2D",
|
|
81
100
|
h2s: "H2S",
|
|
82
101
|
h2c: "H2C",
|
|
102
|
+
x2d: "X2D",
|
|
83
103
|
};
|
|
84
104
|
|
|
85
105
|
type FilamentProfileIndex = {
|
|
@@ -498,6 +518,19 @@ function extractPrinterDiagnostics(status: any): Record<string, unknown> {
|
|
|
498
518
|
};
|
|
499
519
|
}
|
|
500
520
|
|
|
521
|
+
/** Parse "#RRGGBB;#RRGGBB" (leading '#' optional) into positional slot colours. */
|
|
522
|
+
function parseFilamentColours(value: string): string[] {
|
|
523
|
+
const colours = value.split(";").map((c) => c.trim()).filter(Boolean)
|
|
524
|
+
.map((c) => (c.startsWith("#") ? c : `#${c}`).toUpperCase());
|
|
525
|
+
for (const c of colours) {
|
|
526
|
+
if (!/^#[0-9A-F]{6}([0-9A-F]{2})?$/.test(c)) {
|
|
527
|
+
throw new Error(`Invalid filament colour "${c}"; expected #RRGGBB.`);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (colours.length === 0) throw new Error("filament_colours must list at least one colour.");
|
|
531
|
+
return colours;
|
|
532
|
+
}
|
|
533
|
+
|
|
501
534
|
function validateBambuModel(model: string): string {
|
|
502
535
|
const normalized = model.trim().toLowerCase();
|
|
503
536
|
if (!VALID_BAMBU_MODELS.includes(normalized as BambuModel)) {
|
|
@@ -1015,16 +1048,18 @@ class BambuPrinterMCPServer {
|
|
|
1015
1048
|
private server: Server;
|
|
1016
1049
|
private bambu: BambuImplementation;
|
|
1017
1050
|
private bambuNetwork: BambuNetworkBridge;
|
|
1051
|
+
private readonly blender = new BlenderMcpBridge();
|
|
1018
1052
|
private stlManipulator: STLManipulator;
|
|
1019
1053
|
private readonly runtimeConfig: RuntimeConfig;
|
|
1020
1054
|
private httpRuntime?: { transport: StreamableHTTPServerTransport; httpServer: HttpServer };
|
|
1055
|
+
private shuttingDown = false;
|
|
1021
1056
|
|
|
1022
1057
|
constructor() {
|
|
1023
1058
|
this.runtimeConfig = readRuntimeConfig();
|
|
1024
1059
|
this.server = new Server(
|
|
1025
1060
|
{
|
|
1026
1061
|
name: "bambu-printer-mcp",
|
|
1027
|
-
version: "
|
|
1062
|
+
version: JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")).version
|
|
1028
1063
|
},
|
|
1029
1064
|
{
|
|
1030
1065
|
capabilities: {
|
|
@@ -1088,6 +1123,7 @@ class BambuPrinterMCPServer {
|
|
|
1088
1123
|
{ const: "h2d", title: "H2D" },
|
|
1089
1124
|
{ const: "h2s", title: "H2S" },
|
|
1090
1125
|
{ const: "h2c", title: "H2C" },
|
|
1126
|
+
{ const: "x2d", title: "X2D" },
|
|
1091
1127
|
],
|
|
1092
1128
|
},
|
|
1093
1129
|
},
|
|
@@ -2025,6 +2061,7 @@ class BambuPrinterMCPServer {
|
|
|
2025
2061
|
bambu_token: { type: "string", description: "Access token (default: value from env)" },
|
|
2026
2062
|
load_filaments: { type: "string", description: "Override filament profiles. Semicolon-separated paths to filament JSON configs." },
|
|
2027
2063
|
load_filament_ids: { type: "string", description: "Optional filament-to-object mapping string." },
|
|
2064
|
+
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." },
|
|
2028
2065
|
ensure_on_bed: { type: "boolean", description: "Lift floating models onto the bed." },
|
|
2029
2066
|
arrange: { type: "boolean", description: "Auto-arrange objects on the build plate." },
|
|
2030
2067
|
orient: { type: "boolean", description: "Auto-orient model for optimal printability." },
|
|
@@ -2079,6 +2116,7 @@ class BambuPrinterMCPServer {
|
|
|
2079
2116
|
load_filaments: { type: "string", description: "Override filament profiles. Semicolon-separated paths to filament JSON configs, e.g. 'pla_basic.json;petg_cf.json'." },
|
|
2080
2117
|
filament_profile: { type: "string", description: "Compatibility alias for load_filaments. Semicolon-separated Orca/Bambu filament profile JSON paths." },
|
|
2081
2118
|
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." },
|
|
2119
|
+
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." },
|
|
2082
2120
|
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." },
|
|
2083
2121
|
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." },
|
|
2084
2122
|
scale: { type: "number", description: "Uniform scale factor applied to all axes. 1.0 = original size, 2.0 = double, 0.5 = half. Applied before slicing." },
|
|
@@ -2583,7 +2621,7 @@ class BambuPrinterMCPServer {
|
|
|
2583
2621
|
bambu_model: {
|
|
2584
2622
|
type: "string",
|
|
2585
2623
|
enum: [...VALID_BAMBU_MODELS],
|
|
2586
|
-
description: "REQUIRED: Bambu Lab printer model. H2D, H2S, and H2C are the primary intended paths."
|
|
2624
|
+
description: "REQUIRED: Bambu Lab printer model. H2D, H2S, and H2C are the primary intended paths. X2D direct printing is not supported."
|
|
2587
2625
|
},
|
|
2588
2626
|
host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
|
|
2589
2627
|
bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
|
|
@@ -2637,29 +2675,63 @@ class BambuPrinterMCPServer {
|
|
|
2637
2675
|
required: ["stl_path"]
|
|
2638
2676
|
}
|
|
2639
2677
|
},
|
|
2678
|
+
{
|
|
2679
|
+
name: "blender_mcp_status",
|
|
2680
|
+
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.",
|
|
2681
|
+
inputSchema: {
|
|
2682
|
+
type: "object",
|
|
2683
|
+
properties: {
|
|
2684
|
+
connect: { type: "boolean", description: "Initialize the configured stdio MCP server and discover its tools (default false)." },
|
|
2685
|
+
timeout_ms: { type: "integer", minimum: 100, maximum: 300000, description: "Total connection and discovery deadline in milliseconds; defaults to BLENDER_MCP_TIMEOUT_MS or 120000." }
|
|
2686
|
+
},
|
|
2687
|
+
additionalProperties: false
|
|
2688
|
+
}
|
|
2689
|
+
},
|
|
2690
|
+
{
|
|
2691
|
+
name: "blender_mcp_call",
|
|
2692
|
+
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.",
|
|
2693
|
+
inputSchema: {
|
|
2694
|
+
type: "object",
|
|
2695
|
+
properties: {
|
|
2696
|
+
tool_name: { type: "string", description: "Exact name advertised by Blender MCP, such as get_scene_info or execute_blender_code." },
|
|
2697
|
+
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." },
|
|
2698
|
+
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." }
|
|
2699
|
+
},
|
|
2700
|
+
required: ["tool_name"],
|
|
2701
|
+
additionalProperties: false
|
|
2702
|
+
}
|
|
2703
|
+
},
|
|
2640
2704
|
{
|
|
2641
2705
|
name: "blender_mcp_edit_model",
|
|
2642
|
-
description: "
|
|
2706
|
+
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.",
|
|
2643
2707
|
inputSchema: {
|
|
2644
2708
|
type: "object",
|
|
2645
2709
|
properties: {
|
|
2646
2710
|
stl_path: { type: "string", description: "Path to the local STL file" },
|
|
2647
2711
|
operations: {
|
|
2648
2712
|
type: "array",
|
|
2649
|
-
description: "Ordered
|
|
2713
|
+
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.",
|
|
2714
|
+
minItems: 1,
|
|
2715
|
+
maxItems: 64,
|
|
2650
2716
|
items: { type: "string" }
|
|
2651
2717
|
},
|
|
2652
|
-
|
|
2653
|
-
|
|
2718
|
+
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." },
|
|
2719
|
+
user_prompt: { type: "string", description: "The user's own words describing the edit, passed unchanged to Blender MCP." },
|
|
2720
|
+
timeout_ms: { type: "integer", minimum: 100, maximum: 300000, description: "Total Blender request deadline in milliseconds; defaults to BLENDER_MCP_TIMEOUT_MS or 120000." },
|
|
2721
|
+
bridge_command: { type: "string", description: "Legacy custom bridge executable override, not a standard MCP command. Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1." },
|
|
2722
|
+
execute: { type: "boolean", description: "Apply edits and export (true) or validate and return the prepared request without connecting (false, default)." }
|
|
2654
2723
|
},
|
|
2655
|
-
required:
|
|
2724
|
+
required: process.env.BLENDER_MCP_COMMAND?.trim()
|
|
2725
|
+
? ["stl_path", "operations", "output_path"]
|
|
2726
|
+
: ["stl_path", "operations"],
|
|
2727
|
+
additionalProperties: false
|
|
2656
2728
|
}
|
|
2657
2729
|
}
|
|
2658
2730
|
]
|
|
2659
2731
|
};
|
|
2660
2732
|
});
|
|
2661
2733
|
|
|
2662
|
-
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
2734
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
2663
2735
|
const { name, arguments: args } = request.params;
|
|
2664
2736
|
|
|
2665
2737
|
const host = String(args?.host || DEFAULT_HOST);
|
|
@@ -2841,28 +2913,31 @@ class BambuPrinterMCPServer {
|
|
|
2841
2913
|
break;
|
|
2842
2914
|
}
|
|
2843
2915
|
|
|
2844
|
-
case "upload_file":
|
|
2916
|
+
case "upload_file": {
|
|
2845
2917
|
if (!args?.file_path || !args?.filename) {
|
|
2846
2918
|
throw new Error("Missing required parameters: file_path and filename");
|
|
2847
2919
|
}
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2920
|
+
const print = Boolean(args.print ?? false);
|
|
2921
|
+
const uploadModel = print ? await this.resolveBambuModel(args?.bambu_model as string | undefined) : undefined;
|
|
2922
|
+
if (print) assertDirectPrintSupported(uploadModel, bambuSerial);
|
|
2851
2923
|
result = await this.bambu.uploadFile(
|
|
2852
2924
|
host, bambuSerial, bambuToken,
|
|
2853
2925
|
String(args.file_path), String(args.filename),
|
|
2854
|
-
|
|
2926
|
+
print, uploadModel
|
|
2855
2927
|
);
|
|
2856
2928
|
break;
|
|
2929
|
+
}
|
|
2857
2930
|
|
|
2858
2931
|
case "start_print":
|
|
2859
|
-
case "start_print_job":
|
|
2932
|
+
case "start_print_job": {
|
|
2860
2933
|
if (!args?.filename) {
|
|
2861
2934
|
throw new Error("Missing required parameter: filename");
|
|
2862
2935
|
}
|
|
2863
|
-
await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
2864
|
-
|
|
2936
|
+
const startModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
2937
|
+
assertDirectPrintSupported(startModel, bambuSerial);
|
|
2938
|
+
result = await this.bambu.startJob(host, bambuSerial, bambuToken, String(args.filename), startModel);
|
|
2865
2939
|
break;
|
|
2940
|
+
}
|
|
2866
2941
|
|
|
2867
2942
|
case "cancel_print":
|
|
2868
2943
|
result = await this.bambu.cancelJob(host, bambuSerial, bambuToken);
|
|
@@ -3064,6 +3139,7 @@ class BambuPrinterMCPServer {
|
|
|
3064
3139
|
sliceBambuOptions.loadFilaments = String(args.filament_profile);
|
|
3065
3140
|
}
|
|
3066
3141
|
if (args?.load_filament_ids !== undefined) sliceBambuOptions.loadFilamentIds = String(args.load_filament_ids);
|
|
3142
|
+
if (args?.filament_colours !== undefined) sliceBambuOptions.filamentColours = parseFilamentColours(String(args.filament_colours));
|
|
3067
3143
|
sliceBambuOptions.bedType = resolveBambuStudioCliBedType(args?.bed_type as string | undefined);
|
|
3068
3144
|
if (args?.enable_timelapse !== undefined) sliceBambuOptions.enableTimelapse = Boolean(args.enable_timelapse);
|
|
3069
3145
|
if (args?.allow_mix_temp !== undefined) sliceBambuOptions.allowMixTemp = Boolean(args.allow_mix_temp);
|
|
@@ -3149,6 +3225,7 @@ class BambuPrinterMCPServer {
|
|
|
3149
3225
|
sliceBambuOptions.loadFilaments = String(args.filament_profile);
|
|
3150
3226
|
}
|
|
3151
3227
|
if (args?.load_filament_ids !== undefined) sliceBambuOptions.loadFilamentIds = String(args.load_filament_ids);
|
|
3228
|
+
if (args?.filament_colours !== undefined) sliceBambuOptions.filamentColours = parseFilamentColours(String(args.filament_colours));
|
|
3152
3229
|
sliceBambuOptions.bedType = resolveBambuStudioCliBedType(args?.bed_type as string | undefined);
|
|
3153
3230
|
if (args?.enable_timelapse !== undefined) sliceBambuOptions.enableTimelapse = Boolean(args.enable_timelapse);
|
|
3154
3231
|
if (args?.allow_mix_temp !== undefined) sliceBambuOptions.allowMixTemp = Boolean(args.allow_mix_temp);
|
|
@@ -3211,6 +3288,7 @@ class BambuPrinterMCPServer {
|
|
|
3211
3288
|
"print_3mf"
|
|
3212
3289
|
);
|
|
3213
3290
|
const printModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
3291
|
+
assertDirectPrintSupported(printModel, bambuSerial);
|
|
3214
3292
|
const printBedType = resolveBedType(args?.bed_type as string | undefined);
|
|
3215
3293
|
const printNozzle = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
3216
3294
|
const activeSlicerProfile = await resolveTemplateFirstSlicerProfilePath(
|
|
@@ -3229,57 +3307,50 @@ class BambuPrinterMCPServer {
|
|
|
3229
3307
|
|
|
3230
3308
|
let threeMFPath = String(args.three_mf_path);
|
|
3231
3309
|
|
|
3232
|
-
//
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
if (
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3310
|
+
// Inspect and auto-slice before upload. Preserve any failure instead of sending an unsliced project.
|
|
3311
|
+
const JSZip = (await import('jszip')).default;
|
|
3312
|
+
const zipData = fs.readFileSync(threeMFPath);
|
|
3313
|
+
const zip = await JSZip.loadAsync(zipData);
|
|
3314
|
+
const hasGcode = Object.values(zip.files).some(
|
|
3315
|
+
entry => !entry.dir && /\.gcode$/i.test(entry.name)
|
|
3316
|
+
);
|
|
3317
|
+
if (!hasGcode) {
|
|
3318
|
+
if (printBedType === "supertack_plate") {
|
|
3319
|
+
throw new Error(
|
|
3320
|
+
'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.'
|
|
3321
|
+
);
|
|
3322
|
+
}
|
|
3323
|
+
console.log(`3MF has no gcode — auto-slicing with ${slicerType} for ${printModel}`);
|
|
3324
|
+
const autoSliceOptions: BambuSliceOptions = {
|
|
3325
|
+
uptodate: true,
|
|
3326
|
+
ensureOnBed: true,
|
|
3327
|
+
minSave: true,
|
|
3328
|
+
skipModifiedGcodes: true,
|
|
3329
|
+
bedType: printBedType,
|
|
3330
|
+
};
|
|
3331
|
+
if (!explicitSlicerProfile) {
|
|
3332
|
+
try {
|
|
3333
|
+
const liveFilaments = await this.getResolvedPrinterFilamentInventory(
|
|
3334
|
+
host,
|
|
3335
|
+
bambuSerial,
|
|
3336
|
+
bambuToken,
|
|
3337
|
+
printModel,
|
|
3338
|
+
printNozzle
|
|
3244
3339
|
);
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
const autoSliceOptions: BambuSliceOptions = {
|
|
3248
|
-
uptodate: true,
|
|
3249
|
-
ensureOnBed: true,
|
|
3250
|
-
minSave: true,
|
|
3251
|
-
skipModifiedGcodes: true,
|
|
3252
|
-
bedType: printBedType,
|
|
3253
|
-
};
|
|
3254
|
-
if (!explicitSlicerProfile) {
|
|
3255
|
-
try {
|
|
3256
|
-
const liveFilaments = await this.getResolvedPrinterFilamentInventory(
|
|
3257
|
-
host,
|
|
3258
|
-
bambuSerial,
|
|
3259
|
-
bambuToken,
|
|
3260
|
-
printModel,
|
|
3261
|
-
printNozzle
|
|
3262
|
-
);
|
|
3263
|
-
if (liveFilaments.recommended?.load_filaments) {
|
|
3264
|
-
autoSliceOptions.loadFilaments = liveFilaments.recommended.load_filaments;
|
|
3265
|
-
}
|
|
3266
|
-
} catch (filamentError) {
|
|
3267
|
-
console.warn("Could not resolve live printer filaments for auto-slicing:", filamentError);
|
|
3340
|
+
if (liveFilaments.recommended?.load_filaments) {
|
|
3341
|
+
autoSliceOptions.loadFilaments = liveFilaments.recommended.load_filaments;
|
|
3268
3342
|
}
|
|
3343
|
+
} catch (filamentError) {
|
|
3344
|
+
console.warn("Could not resolve live printer filaments for auto-slicing:", filamentError);
|
|
3269
3345
|
}
|
|
3270
|
-
threeMFPath = await this.stlManipulator.sliceSTL(
|
|
3271
|
-
threeMFPath, slicerType, slicerPath, activeSlicerProfile,
|
|
3272
|
-
undefined, // progressCallback
|
|
3273
|
-
printPreset,
|
|
3274
|
-
autoSliceOptions
|
|
3275
|
-
);
|
|
3276
|
-
console.log("Auto-sliced to: " + threeMFPath);
|
|
3277
3346
|
}
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3347
|
+
threeMFPath = await this.stlManipulator.sliceSTL(
|
|
3348
|
+
threeMFPath, slicerType, slicerPath, activeSlicerProfile,
|
|
3349
|
+
undefined, // progressCallback
|
|
3350
|
+
printPreset,
|
|
3351
|
+
autoSliceOptions
|
|
3352
|
+
);
|
|
3353
|
+
console.log("Auto-sliced to: " + threeMFPath);
|
|
3283
3354
|
}
|
|
3284
3355
|
|
|
3285
3356
|
const parsed3MFData = await parse3MF(threeMFPath);
|
|
@@ -3403,6 +3474,7 @@ class BambuPrinterMCPServer {
|
|
|
3403
3474
|
"print_collar_charm"
|
|
3404
3475
|
);
|
|
3405
3476
|
const printModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
3477
|
+
assertDirectPrintSupported(printModel, bambuSerial);
|
|
3406
3478
|
const printBedType = resolveBedType(args?.bed_type as string | undefined);
|
|
3407
3479
|
const printNozzle = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
3408
3480
|
const activeSlicerProfile = await resolveTemplateFirstSlicerProfilePath(
|
|
@@ -3496,21 +3568,24 @@ class BambuPrinterMCPServer {
|
|
|
3496
3568
|
result = await this.stlManipulator.layFlat(String(args.stl_path));
|
|
3497
3569
|
break;
|
|
3498
3570
|
|
|
3571
|
+
case "blender_mcp_status":
|
|
3572
|
+
result = await this.blender.status(args ?? {}, extra.signal);
|
|
3573
|
+
break;
|
|
3574
|
+
|
|
3575
|
+
case "blender_mcp_call":
|
|
3576
|
+
return await this.blender.call(args ?? {}, extra.signal);
|
|
3577
|
+
|
|
3499
3578
|
case "blender_mcp_edit_model":
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
stlPath: String(args.stl_path),
|
|
3505
|
-
operations: args.operations.map((entry: any) => String(entry)),
|
|
3506
|
-
execute: Boolean(args.execute ?? false),
|
|
3507
|
-
bridgeCommand: this.resolveExecutableSelectorArg(
|
|
3508
|
-
args.bridge_command,
|
|
3579
|
+
result = await this.blender.edit(
|
|
3580
|
+
args ?? {},
|
|
3581
|
+
this.resolveExecutableSelectorArg(
|
|
3582
|
+
args?.bridge_command,
|
|
3509
3583
|
"blender_mcp_edit_model",
|
|
3510
3584
|
"bridge_command",
|
|
3511
3585
|
true
|
|
3512
|
-
) ?? this.runtimeConfig.blenderBridgeCommand,
|
|
3513
|
-
|
|
3586
|
+
) ?? (!process.env.BLENDER_MCP_COMMAND?.trim() ? this.runtimeConfig.blenderBridgeCommand : undefined),
|
|
3587
|
+
extra.signal
|
|
3588
|
+
);
|
|
3514
3589
|
break;
|
|
3515
3590
|
|
|
3516
3591
|
default:
|
|
@@ -3547,49 +3622,27 @@ class BambuPrinterMCPServer {
|
|
|
3547
3622
|
});
|
|
3548
3623
|
}
|
|
3549
3624
|
|
|
3550
|
-
private async invokeBlenderBridge(params: {
|
|
3551
|
-
stlPath: string;
|
|
3552
|
-
operations: string[];
|
|
3553
|
-
execute: boolean;
|
|
3554
|
-
bridgeCommand?: string;
|
|
3555
|
-
}): Promise<any> {
|
|
3556
|
-
const payload = {
|
|
3557
|
-
stlPath: params.stlPath,
|
|
3558
|
-
operations: params.operations,
|
|
3559
|
-
};
|
|
3560
|
-
|
|
3561
|
-
if (!params.execute || !params.bridgeCommand) {
|
|
3562
|
-
return {
|
|
3563
|
-
status: "prepared",
|
|
3564
|
-
payload,
|
|
3565
|
-
note: params.bridgeCommand
|
|
3566
|
-
? "Set execute=true to run the Blender bridge command."
|
|
3567
|
-
: "No BLENDER_MCP_BRIDGE_COMMAND configured. Set the env var or pass bridge_command.",
|
|
3568
|
-
};
|
|
3569
|
-
}
|
|
3570
|
-
|
|
3571
|
-
const { execFile } = await import("node:child_process");
|
|
3572
|
-
const { promisify } = await import("node:util");
|
|
3573
|
-
const execFileAsync = promisify(execFile);
|
|
3574
|
-
|
|
3575
|
-
const { stdout, stderr } = await execFileAsync(params.bridgeCommand, [], {
|
|
3576
|
-
env: { ...process.env, MCP_BLENDER_PAYLOAD: JSON.stringify(payload) },
|
|
3577
|
-
timeout: 120_000,
|
|
3578
|
-
});
|
|
3579
|
-
|
|
3580
|
-
return {
|
|
3581
|
-
status: "executed",
|
|
3582
|
-
stdout: stdout.trim(),
|
|
3583
|
-
stderr: stderr.trim(),
|
|
3584
|
-
};
|
|
3585
|
-
}
|
|
3586
|
-
|
|
3587
3625
|
async startStdio() {
|
|
3588
3626
|
const transport = new StdioServerTransport();
|
|
3589
3627
|
await this.server.connect(transport);
|
|
3628
|
+
process.stdin.once("end", () => { void this.shutdown(); });
|
|
3590
3629
|
console.error("Bambu Printer MCP server running on stdio");
|
|
3591
3630
|
}
|
|
3592
3631
|
|
|
3632
|
+
async shutdown(exitCode = 0) {
|
|
3633
|
+
if (this.shuttingDown) return;
|
|
3634
|
+
this.shuttingDown = true;
|
|
3635
|
+
try {
|
|
3636
|
+
// Closing the MCP transport aborts its active requests before exit cleanup.
|
|
3637
|
+
await this.server.close();
|
|
3638
|
+
this.httpRuntime?.httpServer.close();
|
|
3639
|
+
} catch (error) {
|
|
3640
|
+
console.error("[MCP Shutdown]", error);
|
|
3641
|
+
} finally {
|
|
3642
|
+
process.exit(exitCode);
|
|
3643
|
+
}
|
|
3644
|
+
}
|
|
3645
|
+
|
|
3593
3646
|
async startHttp() {
|
|
3594
3647
|
const { httpHost, httpPort, httpPath, statefulSession, enableJsonResponse, allowedOrigins } =
|
|
3595
3648
|
this.runtimeConfig;
|
|
@@ -3640,4 +3693,6 @@ class BambuPrinterMCPServer {
|
|
|
3640
3693
|
}
|
|
3641
3694
|
|
|
3642
3695
|
const server = new BambuPrinterMCPServer();
|
|
3696
|
+
process.once("SIGINT", () => { void server.shutdown(130); });
|
|
3697
|
+
process.once("SIGTERM", () => { void server.shutdown(143); });
|
|
3643
3698
|
server.run().catch(console.error);
|