bambu-printer-mcp 1.1.5 → 1.1.8

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/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,6 +25,7 @@ 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";
@@ -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
- const TEMP_DIR = process.env.TEMP_DIR || path.join(process.cwd(), "temp");
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 =
@@ -601,6 +619,9 @@ type RuntimeConfig = {
601
619
  enableJsonResponse: boolean;
602
620
  allowedOrigins: Set<string>;
603
621
  blenderBridgeCommand?: string;
622
+ ffmpegPath?: string;
623
+ allowExecutableArg: boolean;
624
+ allowBridgeCommandArg: boolean;
604
625
  };
605
626
 
606
627
  function parseBooleanEnv(rawValue: string | undefined, fallback: boolean): boolean {
@@ -698,6 +719,9 @@ function readRuntimeConfig(): RuntimeConfig {
698
719
  enableJsonResponse: parseBooleanEnv(process.env.MCP_HTTP_JSON_RESPONSE, true),
699
720
  allowedOrigins: parseCsvEnv(process.env.MCP_HTTP_ALLOWED_ORIGINS),
700
721
  blenderBridgeCommand: process.env.BLENDER_MCP_BRIDGE_COMMAND?.trim() || undefined,
722
+ ffmpegPath: process.env.FFMPEG_PATH?.trim() || undefined,
723
+ allowExecutableArg: parseBooleanEnv(process.env.MCP_ALLOW_EXECUTABLE_ARG, false),
724
+ allowBridgeCommandArg: parseBooleanEnv(process.env.MCP_ALLOW_BRIDGE_COMMAND_ARG, false),
701
725
  };
702
726
  }
703
727
 
@@ -1009,16 +1033,18 @@ class BambuPrinterMCPServer {
1009
1033
  private server: Server;
1010
1034
  private bambu: BambuImplementation;
1011
1035
  private bambuNetwork: BambuNetworkBridge;
1036
+ private readonly blender = new BlenderMcpBridge();
1012
1037
  private stlManipulator: STLManipulator;
1013
1038
  private readonly runtimeConfig: RuntimeConfig;
1014
1039
  private httpRuntime?: { transport: StreamableHTTPServerTransport; httpServer: HttpServer };
1040
+ private shuttingDown = false;
1015
1041
 
1016
1042
  constructor() {
1017
1043
  this.runtimeConfig = readRuntimeConfig();
1018
1044
  this.server = new Server(
1019
1045
  {
1020
1046
  name: "bambu-printer-mcp",
1021
- version: "1.0.0"
1047
+ version: JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")).version
1022
1048
  },
1023
1049
  {
1024
1050
  capabilities: {
@@ -1113,9 +1139,17 @@ class BambuPrinterMCPServer {
1113
1139
  }
1114
1140
  }
1115
1141
 
1116
- private bridgeOptionsFromArgs(args: Record<string, any> | undefined): BambuNetworkBridgeOptions {
1142
+ private bridgeOptionsFromArgs(
1143
+ args: Record<string, any> | undefined,
1144
+ toolName: string
1145
+ ): BambuNetworkBridgeOptions {
1117
1146
  return {
1118
- bridgeCommand: args?.bridge_command !== undefined ? String(args.bridge_command) : undefined,
1147
+ bridgeCommand: this.resolveExecutableSelectorArg(
1148
+ args?.bridge_command,
1149
+ toolName,
1150
+ "bridge_command",
1151
+ true
1152
+ ),
1119
1153
  configDir: args?.bambu_network_config_dir !== undefined ? String(args.bambu_network_config_dir) : undefined,
1120
1154
  countryCode: args?.country_code !== undefined ? String(args.country_code) : undefined,
1121
1155
  userInfo: args?.user_info !== undefined ? String(args.user_info) : undefined,
@@ -1123,13 +1157,61 @@ class BambuPrinterMCPServer {
1123
1157
  };
1124
1158
  }
1125
1159
 
1160
+ private resolveExecutableSelectorArg(
1161
+ rawValue: unknown,
1162
+ toolName: string,
1163
+ argumentName: string,
1164
+ allowLegacyBridgeArg = false
1165
+ ): string | undefined {
1166
+ if (rawValue === undefined) {
1167
+ return undefined;
1168
+ }
1169
+
1170
+ const value = String(rawValue);
1171
+ if (value.length === 0) {
1172
+ return undefined;
1173
+ }
1174
+
1175
+ const isAllowed =
1176
+ this.runtimeConfig.allowExecutableArg ||
1177
+ (allowLegacyBridgeArg && this.runtimeConfig.allowBridgeCommandArg);
1178
+ if (!isAllowed) {
1179
+ throw new Error(
1180
+ `${toolName}: the ${argumentName} argument cannot select an executable by default. ` +
1181
+ "Executable paths and commands are read from server environment configuration. Set " +
1182
+ `MCP_ALLOW_EXECUTABLE_ARG=1 to accept ${argumentName} for process execution.`
1183
+ );
1184
+ }
1185
+
1186
+ return value;
1187
+ }
1188
+
1189
+ private resolveSlicerConfigFromArgs(
1190
+ args: Record<string, unknown> | undefined,
1191
+ toolName: string
1192
+ ): ReturnType<typeof resolveSlicerConfig> {
1193
+ const config = resolveSlicerConfig(args);
1194
+ if (args?.slicer_path !== undefined) {
1195
+ const resolvedSlicerPath = this.resolveExecutableSelectorArg(
1196
+ args.slicer_path,
1197
+ toolName,
1198
+ "slicer_path"
1199
+ );
1200
+ if (resolvedSlicerPath !== undefined) {
1201
+ config.slicerPath = resolvedSlicerPath;
1202
+ }
1203
+ }
1204
+ return config;
1205
+ }
1206
+
1126
1207
  private async ensurePrintableThreeMFPath(
1127
1208
  args: Record<string, any>,
1128
1209
  printModel: string,
1129
1210
  printPreset: string | undefined,
1130
1211
  bedType?: string
1131
1212
  ): Promise<{ threeMFPath: string; autoSliced: boolean }> {
1132
- const { slicerType, slicerPath, slicerProfile } = resolveSlicerConfig(args);
1213
+ const { slicerType, slicerPath, slicerProfile } =
1214
+ this.resolveSlicerConfigFromArgs(args, "print_3mf_bambu_network");
1133
1215
  let threeMFPath = String(args.three_mf_path);
1134
1216
 
1135
1217
  const JSZip = (await import('jszip')).default;
@@ -1366,7 +1448,7 @@ class BambuPrinterMCPServer {
1366
1448
  const bridgeResult = await this.bambuNetwork.callWithAgent(
1367
1449
  bridgeMethod,
1368
1450
  { client_job_id: clientJobId, params },
1369
- this.bridgeOptionsFromArgs(args)
1451
+ this.bridgeOptionsFromArgs(args, "print_3mf_bambu_network")
1370
1452
  );
1371
1453
 
1372
1454
  if (typeof bridgeResult === "object" && bridgeResult !== null && (bridgeResult as { ok?: unknown }).ok === false) {
@@ -1949,7 +2031,7 @@ class BambuPrinterMCPServer {
1949
2031
  enum: SLICER_SCHEMA_VALUES,
1950
2032
  description: "Type of slicer to use. Bambu-compatible choices (bambustudio, orcaslicer, orcaslicer-bambulab) export sliced 3MF; aliases such as fulu-orca and orca-studio are accepted."
1951
2033
  },
1952
- slicer_path: { type: "string", description: "Path to the slicer executable (default: value from env)" },
2034
+ slicer_path: { type: "string", description: "Path to the slicer executable (default: value from env). Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1." },
1953
2035
  slicer_profile: { type: "string", description: "Explicit slicer profile/config file. Overrides the named template only when provided in the tool call." },
1954
2036
  nozzle_diameter: { type: "string", description: "Nozzle diameter in mm (default: 0.4)" },
1955
2037
  bed_type: {
@@ -1995,7 +2077,7 @@ class BambuPrinterMCPServer {
1995
2077
  enum: SLICER_SCHEMA_VALUES,
1996
2078
  description: "Type of slicer to use. Bambu-compatible choices (bambustudio, orcaslicer, orcaslicer-bambulab) export sliced 3MF; aliases such as fulu-orca and orca-studio are accepted."
1997
2079
  },
1998
- slicer_path: { type: "string", description: "Path to the slicer executable (default: value from env)" },
2080
+ slicer_path: { type: "string", description: "Path to the slicer executable (default: value from env). Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1." },
1999
2081
  slicer_profile: { type: "string", description: "Path to the slicer profile/config file (optional, overrides bambu_model preset)" },
2000
2082
  template_3mf_path: { type: "string", description: "Optional template 3MF whose embedded Bambu slicer settings should be reused when slicing a new STL or 3MF." },
2001
2083
  template_name: { type: "string", description: "Optional named template from the local registry. Resolves to template_3mf_path automatically." },
@@ -2049,7 +2131,7 @@ class BambuPrinterMCPServer {
2049
2131
  type: "object",
2050
2132
  properties: {
2051
2133
  connect: { type: "boolean", description: "When true, start the bridge command and run a handshake plus agent initialization probe." },
2052
- bridge_command: { type: "string", description: "Override command for the FULU bridge host or macOS/WSL wrapper; defaults to BAMBU_NETWORK_BRIDGE_COMMAND." },
2134
+ bridge_command: { type: "string", description: "Override command for the FULU bridge host or macOS/WSL wrapper; defaults to BAMBU_NETWORK_BRIDGE_COMMAND. Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1." },
2053
2135
  bambu_network_config_dir: { type: "string", description: "Config/log directory used by the BambuNetwork agent; defaults to BAMBU_NETWORK_CONFIG_DIR or a user config directory." },
2054
2136
  country_code: { type: "string", description: "BambuNetwork country code, such as US, used by the agent during startup." },
2055
2137
  user_info: { type: "string", description: "Optional BambuNetwork user_info JSON string to pass to net.change_user after the agent starts." },
@@ -2066,7 +2148,7 @@ class BambuPrinterMCPServer {
2066
2148
  method: { type: "string", description: "FULU bridge method name, for example bridge.handshake, net.is_user_login, or net.get_user_selected_machine." },
2067
2149
  payload: { type: "object", description: "JSON payload passed to the bridge method." },
2068
2150
  with_agent: { type: "boolean", description: "When true, initialize a BambuNetwork agent and add its agent id to the payload before calling the method." },
2069
- bridge_command: { type: "string", description: "Override command for the FULU bridge host or macOS/WSL wrapper; defaults to BAMBU_NETWORK_BRIDGE_COMMAND." },
2151
+ bridge_command: { type: "string", description: "Override command for the FULU bridge host or macOS/WSL wrapper; defaults to BAMBU_NETWORK_BRIDGE_COMMAND. Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1." },
2070
2152
  bambu_network_config_dir: { type: "string", description: "Config/log directory used by the BambuNetwork agent; defaults to BAMBU_NETWORK_CONFIG_DIR or a user config directory." },
2071
2153
  country_code: { type: "string", description: "BambuNetwork country code, such as US, used by the agent during startup." },
2072
2154
  user_info: { type: "string", description: "Optional BambuNetwork user_info JSON string to pass to net.change_user after the agent starts." },
@@ -2102,13 +2184,13 @@ class BambuPrinterMCPServer {
2102
2184
  preset_name: { type: "string", description: "Optional preset name sent in FULU PrintParams; defaults to project plus one-based plate index." },
2103
2185
  task_name: { type: "string", description: "Optional BambuNetwork task name; defaults to the project name." },
2104
2186
  config_filename: { type: "string", description: "Optional config 3MF path for cloud print; defaults to the same 3MF path." },
2105
- bridge_command: { type: "string", description: "Override command for the FULU bridge host or macOS/WSL wrapper; defaults to BAMBU_NETWORK_BRIDGE_COMMAND." },
2187
+ bridge_command: { type: "string", description: "Override command for the FULU bridge host or macOS/WSL wrapper; defaults to BAMBU_NETWORK_BRIDGE_COMMAND. Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1." },
2106
2188
  bambu_network_config_dir: { type: "string", description: "Config/log directory used by the BambuNetwork agent; defaults to BAMBU_NETWORK_CONFIG_DIR or a user config directory." },
2107
2189
  country_code: { type: "string", description: "BambuNetwork country code, such as US, used by the agent during startup." },
2108
2190
  user_info: { type: "string", description: "Optional BambuNetwork user_info JSON string to pass to net.change_user after the agent starts." },
2109
2191
  timeout_ms: { type: "number", description: "Bridge request timeout in milliseconds." },
2110
2192
  slicer_type: { type: "string", enum: SLICER_SCHEMA_VALUES, description: "Slicer to use only if auto-slicing an unsliced 3MF; use orcaslicer-bambulab for FULU's fork." },
2111
- slicer_path: { type: "string", description: "Path to the slicer executable for auto-slicing; defaults to value from env or a platform default." },
2193
+ slicer_path: { type: "string", description: "Path to the slicer executable for auto-slicing; defaults to value from env or a platform default. Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1." },
2112
2194
  slicer_profile: { type: "string", description: "Path to an optional slicer profile/config file for auto-slicing." },
2113
2195
  nozzle_diameter: { type: "string", description: "Nozzle diameter in mm for auto-slicing (default: 0.4)." },
2114
2196
  use_ams: { type: "boolean", description: "Whether to use the AMS; defaults to auto-detect from the 3MF mapping." },
@@ -2144,7 +2226,7 @@ class BambuPrinterMCPServer {
2144
2226
  timeout_ms: { type: "number", description: "Max ms to wait for a full frame (default 8000). Camera may take a few seconds on cold start." },
2145
2227
  bambu_model: { type: "string", description: "Printer model. Used to route to the correct protocol or fail fast on unsupported models. Defaults to BAMBU_MODEL." },
2146
2228
  experimental: { type: "boolean", description: "Deprecated and ignored. Earlier this flag let callers probe H2 series via the A1/P1 TCP-on-6000 path; live testing on an H2S confirmed H2 uses RTSP instead, so the flag has no effect now." },
2147
- ffmpeg_path: { type: "string", description: "Override path to the ffmpeg binary used by the RTSP path. Defaults to ffmpeg via $PATH. Required only for the RTSP transport (X1, P2S, H2 series)." },
2229
+ ffmpeg_path: { type: "string", description: "Override path to the ffmpeg binary used by the RTSP path. Defaults to FFMPEG_PATH or ffmpeg via $PATH. Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1. Required only for the RTSP transport (X1, P2S, H2 series)." },
2148
2230
  host: { type: "string", description: "Hostname or IP of the printer (default: value from env)" },
2149
2231
  bambu_serial: { type: "string", description: "Serial number (default: value from env)" },
2150
2232
  bambu_token: { type: "string", description: "Access token (default: value from env)" }
@@ -2462,7 +2544,7 @@ class BambuPrinterMCPServer {
2462
2544
  bambu_network_method: { type: "string", enum: BAMBU_NETWORK_PRINT_METHODS, description: "FULU print method when connection_mode is bambu_network; defaults to start_print for cloud and start_local_print for lan." },
2463
2545
  dev_id: { type: "string", description: "Bambu device id for FULU BambuNetwork printing; defaults to BAMBU_DEV_ID or BAMBU_SERIAL." },
2464
2546
  dev_ip: { type: "string", description: "Printer IP address for FULU BambuNetwork LAN/local print methods; defaults to host when provided." },
2465
- bridge_command: { type: "string", description: "Override command for the FULU bridge host or macOS/WSL wrapper; defaults to BAMBU_NETWORK_BRIDGE_COMMAND." },
2547
+ bridge_command: { type: "string", description: "Override command for the FULU bridge host or macOS/WSL wrapper; defaults to BAMBU_NETWORK_BRIDGE_COMMAND. Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1." },
2466
2548
  bambu_network_config_dir: { type: "string", description: "Config/log directory used by the FULU BambuNetwork agent." },
2467
2549
  country_code: { type: "string", description: "BambuNetwork country code, such as US, used by the FULU bridge agent." },
2468
2550
  user_info: { type: "string", description: "Optional BambuNetwork user_info JSON string passed to net.change_user for the FULU bridge." },
@@ -2480,7 +2562,7 @@ class BambuPrinterMCPServer {
2480
2562
  enum: SLICER_SCHEMA_VALUES,
2481
2563
  description: "Slicer to use only if auto-slicing an unsliced 3MF. Bambu-compatible slicer aliases such as fulu-orca and orca-studio are accepted."
2482
2564
  },
2483
- slicer_path: { type: "string", description: "Path to the slicer executable for auto-slicing (default: value from env or a platform default)" },
2565
+ slicer_path: { type: "string", description: "Path to the slicer executable for auto-slicing (default: value from env or a platform default). Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1." },
2484
2566
  use_ams: { type: "boolean", description: "Whether to use the AMS (default: auto-detect from 3MF)" },
2485
2567
  ams_mapping: {
2486
2568
  type: "array",
@@ -2575,29 +2657,63 @@ class BambuPrinterMCPServer {
2575
2657
  required: ["stl_path"]
2576
2658
  }
2577
2659
  },
2660
+ {
2661
+ name: "blender_mcp_status",
2662
+ 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.",
2663
+ inputSchema: {
2664
+ type: "object",
2665
+ properties: {
2666
+ connect: { type: "boolean", description: "Initialize the configured stdio MCP server and discover its tools (default false)." },
2667
+ timeout_ms: { type: "integer", minimum: 100, maximum: 300000, description: "Total connection and discovery deadline in milliseconds; defaults to BLENDER_MCP_TIMEOUT_MS or 120000." }
2668
+ },
2669
+ additionalProperties: false
2670
+ }
2671
+ },
2672
+ {
2673
+ name: "blender_mcp_call",
2674
+ 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.",
2675
+ inputSchema: {
2676
+ type: "object",
2677
+ properties: {
2678
+ tool_name: { type: "string", description: "Exact name advertised by Blender MCP, such as get_scene_info or execute_blender_code." },
2679
+ 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." },
2680
+ 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." }
2681
+ },
2682
+ required: ["tool_name"],
2683
+ additionalProperties: false
2684
+ }
2685
+ },
2578
2686
  {
2579
2687
  name: "blender_mcp_edit_model",
2580
- description: "Send STL-edit instructions to a Blender MCP bridge command for advanced model edits",
2688
+ 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.",
2581
2689
  inputSchema: {
2582
2690
  type: "object",
2583
2691
  properties: {
2584
2692
  stl_path: { type: "string", description: "Path to the local STL file" },
2585
2693
  operations: {
2586
2694
  type: "array",
2587
- description: "Ordered edit operations for Blender (e.g. remesh, boolean, decimate)",
2695
+ 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.",
2696
+ minItems: 1,
2697
+ maxItems: 64,
2588
2698
  items: { type: "string" }
2589
2699
  },
2590
- bridge_command: { type: "string", description: "Override command for invoking Blender MCP bridge" },
2591
- execute: { type: "boolean", description: "Execute bridge command (true) or return payload only (false)" }
2700
+ 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." },
2701
+ user_prompt: { type: "string", description: "The user's own words describing the edit, passed unchanged to Blender MCP." },
2702
+ timeout_ms: { type: "integer", minimum: 100, maximum: 300000, description: "Total Blender request deadline in milliseconds; defaults to BLENDER_MCP_TIMEOUT_MS or 120000." },
2703
+ bridge_command: { type: "string", description: "Legacy custom bridge executable override, not a standard MCP command. Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1." },
2704
+ execute: { type: "boolean", description: "Apply edits and export (true) or validate and return the prepared request without connecting (false, default)." }
2592
2705
  },
2593
- required: ["stl_path", "operations"]
2706
+ required: process.env.BLENDER_MCP_COMMAND?.trim()
2707
+ ? ["stl_path", "operations", "output_path"]
2708
+ : ["stl_path", "operations"],
2709
+ additionalProperties: false
2594
2710
  }
2595
2711
  }
2596
2712
  ]
2597
2713
  };
2598
2714
  });
2599
2715
 
2600
- this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
2716
+ this.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
2601
2717
  const { name, arguments: args } = request.params;
2602
2718
 
2603
2719
  const host = String(args?.host || DEFAULT_HOST);
@@ -2696,7 +2812,10 @@ class BambuPrinterMCPServer {
2696
2812
 
2697
2813
  case "bambu_network_bridge_status": {
2698
2814
  const bridgeArgs = args as Record<string, any> | undefined;
2699
- const options = this.bridgeOptionsFromArgs(bridgeArgs);
2815
+ const options = this.bridgeOptionsFromArgs(
2816
+ bridgeArgs,
2817
+ "bambu_network_bridge_status"
2818
+ );
2700
2819
  result = this.bambuNetwork.getStatus(options);
2701
2820
  if (Boolean(bridgeArgs?.connect)) {
2702
2821
  const probe = await this.bambuNetwork.ensureAgent(options);
@@ -2718,7 +2837,7 @@ class BambuPrinterMCPServer {
2718
2837
  const payload = bridgeArgs.payload && typeof bridgeArgs.payload === "object"
2719
2838
  ? bridgeArgs.payload as Record<string, unknown>
2720
2839
  : {};
2721
- const options = this.bridgeOptionsFromArgs(bridgeArgs);
2840
+ const options = this.bridgeOptionsFromArgs(bridgeArgs, "bambu_network_call");
2722
2841
  result = bridgeArgs.with_agent === false
2723
2842
  ? await this.bambuNetwork.request(String(bridgeArgs.method), payload, options)
2724
2843
  : await this.bambuNetwork.callWithAgent(String(bridgeArgs.method), payload, options);
@@ -2737,7 +2856,11 @@ class BambuPrinterMCPServer {
2737
2856
  timeoutMs: args?.timeout_ms !== undefined ? Number(args.timeout_ms) : undefined,
2738
2857
  bambuModel: snapshotModel ? String(snapshotModel) : undefined,
2739
2858
  experimental: Boolean(args?.experimental),
2740
- ffmpegPath: args?.ffmpeg_path ? String(args.ffmpeg_path) : undefined,
2859
+ ffmpegPath: this.resolveExecutableSelectorArg(
2860
+ args?.ffmpeg_path,
2861
+ "camera_snapshot",
2862
+ "ffmpeg_path"
2863
+ ) ?? this.runtimeConfig.ffmpegPath,
2741
2864
  });
2742
2865
  break;
2743
2866
  }
@@ -2959,7 +3082,10 @@ class BambuPrinterMCPServer {
2959
3082
  if (!args?.template_name) {
2960
3083
  throw new Error("Missing required parameter: template_name");
2961
3084
  }
2962
- const { slicerType, slicerPath, slicerProfile } = resolveSlicerConfig(args as Record<string, unknown> | undefined);
3085
+ const { slicerType, slicerPath, slicerProfile } = this.resolveSlicerConfigFromArgs(
3086
+ args as Record<string, unknown> | undefined,
3087
+ "slice_with_template"
3088
+ );
2963
3089
  const sliceModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
2964
3090
  const nozzleDiam = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
2965
3091
  const activeSlicerProfile = await resolveTemplateFirstSlicerProfilePath(
@@ -3041,7 +3167,10 @@ class BambuPrinterMCPServer {
3041
3167
  if (!args?.stl_path) {
3042
3168
  throw new Error("Missing required parameter: stl_path");
3043
3169
  }
3044
- const { slicerType, slicerPath, slicerProfile } = resolveSlicerConfig(args as Record<string, unknown> | undefined);
3170
+ const { slicerType, slicerPath, slicerProfile } = this.resolveSlicerConfigFromArgs(
3171
+ args as Record<string, unknown> | undefined,
3172
+ "slice_stl"
3173
+ );
3045
3174
  const sliceModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
3046
3175
  const nozzleDiam = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
3047
3176
  const activeSlicerProfile = await resolveSlicerProfilePath(
@@ -3131,7 +3260,10 @@ class BambuPrinterMCPServer {
3131
3260
  throw new Error("Bambu serial number and access token are required for print_3mf.");
3132
3261
  }
3133
3262
 
3134
- const { slicerType, slicerPath, slicerProfile } = resolveSlicerConfig(args as Record<string, unknown> | undefined);
3263
+ const { slicerType, slicerPath, slicerProfile } = this.resolveSlicerConfigFromArgs(
3264
+ args as Record<string, unknown> | undefined,
3265
+ "print_3mf"
3266
+ );
3135
3267
  const printModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
3136
3268
  const printBedType = resolveBedType(args?.bed_type as string | undefined);
3137
3269
  const printNozzle = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
@@ -3151,57 +3283,50 @@ class BambuPrinterMCPServer {
3151
3283
 
3152
3284
  let threeMFPath = String(args.three_mf_path);
3153
3285
 
3154
- // Auto-slice if 3MF has no gcode
3155
- try {
3156
- const JSZip = (await import('jszip')).default;
3157
- const zipData = fs.readFileSync(threeMFPath);
3158
- const zip = await JSZip.loadAsync(zipData);
3159
- const hasGcode = Object.keys(zip.files).some(
3160
- f => f.match(/Metadata\/plate_\d+\.gcode/i) || f.endsWith('.gcode')
3161
- );
3162
- if (!hasGcode) {
3163
- if (printBedType === "supertack_plate") {
3164
- throw new Error(
3165
- '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.'
3286
+ // Inspect and auto-slice before upload. Preserve any failure instead of sending an unsliced project.
3287
+ const JSZip = (await import('jszip')).default;
3288
+ const zipData = fs.readFileSync(threeMFPath);
3289
+ const zip = await JSZip.loadAsync(zipData);
3290
+ const hasGcode = Object.values(zip.files).some(
3291
+ entry => !entry.dir && /\.gcode$/i.test(entry.name)
3292
+ );
3293
+ if (!hasGcode) {
3294
+ if (printBedType === "supertack_plate") {
3295
+ throw new Error(
3296
+ '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.'
3297
+ );
3298
+ }
3299
+ console.log(`3MF has no gcode — auto-slicing with ${slicerType} for ${printModel}`);
3300
+ const autoSliceOptions: BambuSliceOptions = {
3301
+ uptodate: true,
3302
+ ensureOnBed: true,
3303
+ minSave: true,
3304
+ skipModifiedGcodes: true,
3305
+ bedType: printBedType,
3306
+ };
3307
+ if (!explicitSlicerProfile) {
3308
+ try {
3309
+ const liveFilaments = await this.getResolvedPrinterFilamentInventory(
3310
+ host,
3311
+ bambuSerial,
3312
+ bambuToken,
3313
+ printModel,
3314
+ printNozzle
3166
3315
  );
3167
- }
3168
- console.log(`3MF has no gcode — auto-slicing with ${slicerType} for ${printModel}`);
3169
- const autoSliceOptions: BambuSliceOptions = {
3170
- uptodate: true,
3171
- ensureOnBed: true,
3172
- minSave: true,
3173
- skipModifiedGcodes: true,
3174
- bedType: printBedType,
3175
- };
3176
- if (!explicitSlicerProfile) {
3177
- try {
3178
- const liveFilaments = await this.getResolvedPrinterFilamentInventory(
3179
- host,
3180
- bambuSerial,
3181
- bambuToken,
3182
- printModel,
3183
- printNozzle
3184
- );
3185
- if (liveFilaments.recommended?.load_filaments) {
3186
- autoSliceOptions.loadFilaments = liveFilaments.recommended.load_filaments;
3187
- }
3188
- } catch (filamentError) {
3189
- console.warn("Could not resolve live printer filaments for auto-slicing:", filamentError);
3316
+ if (liveFilaments.recommended?.load_filaments) {
3317
+ autoSliceOptions.loadFilaments = liveFilaments.recommended.load_filaments;
3190
3318
  }
3319
+ } catch (filamentError) {
3320
+ console.warn("Could not resolve live printer filaments for auto-slicing:", filamentError);
3191
3321
  }
3192
- threeMFPath = await this.stlManipulator.sliceSTL(
3193
- threeMFPath, slicerType, slicerPath, activeSlicerProfile,
3194
- undefined, // progressCallback
3195
- printPreset,
3196
- autoSliceOptions
3197
- );
3198
- console.log("Auto-sliced to: " + threeMFPath);
3199
- }
3200
- } catch (sliceCheckErr: any) {
3201
- if (String(sliceCheckErr?.message || "").includes("SuperTack")) {
3202
- throw sliceCheckErr;
3203
3322
  }
3204
- console.warn("Could not check/slice 3MF, proceeding with original:", sliceCheckErr.message);
3323
+ threeMFPath = await this.stlManipulator.sliceSTL(
3324
+ threeMFPath, slicerType, slicerPath, activeSlicerProfile,
3325
+ undefined, // progressCallback
3326
+ printPreset,
3327
+ autoSliceOptions
3328
+ );
3329
+ console.log("Auto-sliced to: " + threeMFPath);
3205
3330
  }
3206
3331
 
3207
3332
  const parsed3MFData = await parse3MF(threeMFPath);
@@ -3320,7 +3445,10 @@ class BambuPrinterMCPServer {
3320
3445
  throw new Error("Bambu serial number and access token are required for print_collar_charm.");
3321
3446
  }
3322
3447
 
3323
- const { slicerType, slicerPath, slicerProfile } = resolveSlicerConfig(args as Record<string, unknown> | undefined);
3448
+ const { slicerType, slicerPath, slicerProfile } = this.resolveSlicerConfigFromArgs(
3449
+ args as Record<string, unknown> | undefined,
3450
+ "print_collar_charm"
3451
+ );
3324
3452
  const printModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
3325
3453
  const printBedType = resolveBedType(args?.bed_type as string | undefined);
3326
3454
  const printNozzle = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
@@ -3415,18 +3543,24 @@ class BambuPrinterMCPServer {
3415
3543
  result = await this.stlManipulator.layFlat(String(args.stl_path));
3416
3544
  break;
3417
3545
 
3546
+ case "blender_mcp_status":
3547
+ result = await this.blender.status(args ?? {}, extra.signal);
3548
+ break;
3549
+
3550
+ case "blender_mcp_call":
3551
+ return await this.blender.call(args ?? {}, extra.signal);
3552
+
3418
3553
  case "blender_mcp_edit_model":
3419
- if (!args?.stl_path || !Array.isArray(args.operations)) {
3420
- throw new Error("Missing required parameters: stl_path and operations");
3421
- }
3422
- result = await this.invokeBlenderBridge({
3423
- stlPath: String(args.stl_path),
3424
- operations: args.operations.map((entry: any) => String(entry)),
3425
- execute: Boolean(args.execute ?? false),
3426
- bridgeCommand: args.bridge_command
3427
- ? String(args.bridge_command)
3428
- : this.runtimeConfig.blenderBridgeCommand,
3429
- });
3554
+ result = await this.blender.edit(
3555
+ args ?? {},
3556
+ this.resolveExecutableSelectorArg(
3557
+ args?.bridge_command,
3558
+ "blender_mcp_edit_model",
3559
+ "bridge_command",
3560
+ true
3561
+ ) ?? (!process.env.BLENDER_MCP_COMMAND?.trim() ? this.runtimeConfig.blenderBridgeCommand : undefined),
3562
+ extra.signal
3563
+ );
3430
3564
  break;
3431
3565
 
3432
3566
  default:
@@ -3463,49 +3597,27 @@ class BambuPrinterMCPServer {
3463
3597
  });
3464
3598
  }
3465
3599
 
3466
- private async invokeBlenderBridge(params: {
3467
- stlPath: string;
3468
- operations: string[];
3469
- execute: boolean;
3470
- bridgeCommand?: string;
3471
- }): Promise<any> {
3472
- const payload = {
3473
- stlPath: params.stlPath,
3474
- operations: params.operations,
3475
- };
3476
-
3477
- if (!params.execute || !params.bridgeCommand) {
3478
- return {
3479
- status: "prepared",
3480
- payload,
3481
- note: params.bridgeCommand
3482
- ? "Set execute=true to run the Blender bridge command."
3483
- : "No BLENDER_MCP_BRIDGE_COMMAND configured. Set the env var or pass bridge_command.",
3484
- };
3485
- }
3486
-
3487
- const { execFile } = await import("node:child_process");
3488
- const { promisify } = await import("node:util");
3489
- const execFileAsync = promisify(execFile);
3490
-
3491
- const { stdout, stderr } = await execFileAsync(params.bridgeCommand, [], {
3492
- env: { ...process.env, MCP_BLENDER_PAYLOAD: JSON.stringify(payload) },
3493
- timeout: 120_000,
3494
- });
3495
-
3496
- return {
3497
- status: "executed",
3498
- stdout: stdout.trim(),
3499
- stderr: stderr.trim(),
3500
- };
3501
- }
3502
-
3503
3600
  async startStdio() {
3504
3601
  const transport = new StdioServerTransport();
3505
3602
  await this.server.connect(transport);
3603
+ process.stdin.once("end", () => { void this.shutdown(); });
3506
3604
  console.error("Bambu Printer MCP server running on stdio");
3507
3605
  }
3508
3606
 
3607
+ async shutdown(exitCode = 0) {
3608
+ if (this.shuttingDown) return;
3609
+ this.shuttingDown = true;
3610
+ try {
3611
+ // Closing the MCP transport aborts its active requests before exit cleanup.
3612
+ await this.server.close();
3613
+ this.httpRuntime?.httpServer.close();
3614
+ } catch (error) {
3615
+ console.error("[MCP Shutdown]", error);
3616
+ } finally {
3617
+ process.exit(exitCode);
3618
+ }
3619
+ }
3620
+
3509
3621
  async startHttp() {
3510
3622
  const { httpHost, httpPort, httpPath, statefulSession, enableJsonResponse, allowedOrigins } =
3511
3623
  this.runtimeConfig;
@@ -3556,4 +3668,6 @@ class BambuPrinterMCPServer {
3556
3668
  }
3557
3669
 
3558
3670
  const server = new BambuPrinterMCPServer();
3671
+ process.once("SIGINT", () => { void server.shutdown(130); });
3672
+ process.once("SIGTERM", () => { void server.shutdown(143); });
3559
3673
  server.run().catch(console.error);