mcp-3d-printer-server 1.2.4 → 1.2.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/src/index.ts CHANGED
@@ -15,6 +15,8 @@ import axios from "axios";
15
15
  import dotenv from "dotenv";
16
16
  import fs from "fs";
17
17
  import path from "path";
18
+ import os from "node:os";
19
+ import { BlenderMcpBridge } from "./blender-mcp-bridge.js";
18
20
  import { createServer as createHttpServer, type IncomingMessage, type Server as HttpServer, type ServerResponse } from "node:http";
19
21
  import { randomUUID } from "node:crypto";
20
22
  import * as THREE from 'three';
@@ -36,7 +38,7 @@ const DEFAULT_HOST = process.env.PRINTER_HOST || "localhost";
36
38
  const DEFAULT_PORT = process.env.PRINTER_PORT || "80";
37
39
  const DEFAULT_API_KEY = process.env.API_KEY || "";
38
40
  const DEFAULT_TYPE = process.env.PRINTER_TYPE || "octoprint"; // Default to OctoPrint
39
- const TEMP_DIR = process.env.TEMP_DIR || path.join(process.cwd(), "temp");
41
+ const TEMP_DIR = process.env.TEMP_DIR || fs.mkdtempSync(path.join(os.tmpdir(), "mcp-3d-printer-server-"));
40
42
 
41
43
  // Slicer configuration
42
44
  const CANONICAL_SLICER_TYPES = [
@@ -195,6 +197,8 @@ type RuntimeConfig = {
195
197
  enableJsonResponse: boolean;
196
198
  allowedOrigins: Set<string>;
197
199
  blenderBridgeCommand?: string;
200
+ allowExecutableArg: boolean;
201
+ allowBridgeCommandArg: boolean;
198
202
  };
199
203
 
200
204
  function parseBooleanEnv(rawValue: string | undefined, fallback: boolean): boolean {
@@ -259,6 +263,8 @@ function readRuntimeConfig(): RuntimeConfig {
259
263
  enableJsonResponse: parseBooleanEnv(process.env.MCP_HTTP_JSON_RESPONSE, true),
260
264
  allowedOrigins: parseCsvEnv(process.env.MCP_HTTP_ALLOWED_ORIGINS),
261
265
  blenderBridgeCommand: process.env.BLENDER_MCP_BRIDGE_COMMAND?.trim() || undefined,
266
+ allowExecutableArg: parseBooleanEnv(process.env.MCP_ALLOW_EXECUTABLE_ARG, false),
267
+ allowBridgeCommandArg: parseBooleanEnv(process.env.MCP_ALLOW_BRIDGE_COMMAND_ARG, false),
262
268
  };
263
269
  }
264
270
 
@@ -277,6 +283,7 @@ if (!fs.existsSync(TEMP_DIR)) {
277
283
 
278
284
  class ThreeDPrinterMCPServer {
279
285
  private server: Server;
286
+ private readonly blender = new BlenderMcpBridge();
280
287
  private printerFactory: PrinterFactory;
281
288
  private stlManipulator: STLManipulator;
282
289
  private readonly runtimeConfig: RuntimeConfig;
@@ -287,7 +294,7 @@ class ThreeDPrinterMCPServer {
287
294
  this.server = new Server(
288
295
  {
289
296
  name: "mcp-3d-printer-server",
290
- version: "1.2.0"
297
+ version: JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")).version
291
298
  },
292
299
  {
293
300
  capabilities: {
@@ -609,6 +616,11 @@ class ThreeDPrinterMCPServer {
609
616
  type: "string",
610
617
  description: "Access token for Bambu Lab printers (default: value from env)"
611
618
  },
619
+ bambu_model: {
620
+ type: "string",
621
+ enum: [...VALID_BAMBU_MODELS],
622
+ description: "Required for Bambu print operations unless BAMBU_MODEL is configured. Must match the printer and pre-sliced G-code."
623
+ },
612
624
  filename: {
613
625
  type: "string",
614
626
  description: "Filename to use on the printer. Defaults to the basename of gcode_path when omitted."
@@ -625,11 +637,7 @@ class ThreeDPrinterMCPServer {
625
637
  type: "boolean",
626
638
  description: "Start printing after upload when the printer backend supports it."
627
639
  }
628
- },
629
- anyOf: [
630
- { required: ["filename", "gcode"] },
631
- { required: ["gcode_path"] }
632
- ]
640
+ }
633
641
  }
634
642
  },
635
643
  {
@@ -662,6 +670,11 @@ class ThreeDPrinterMCPServer {
662
670
  type: "string",
663
671
  description: "Access token for Bambu Lab printers (default: value from env)"
664
672
  },
673
+ bambu_model: {
674
+ type: "string",
675
+ enum: [...VALID_BAMBU_MODELS],
676
+ description: "Required for Bambu print operations unless BAMBU_MODEL is configured. Must match the printer and pre-sliced G-code."
677
+ },
665
678
  filename: {
666
679
  type: "string",
667
680
  description: "Name/path of the printer-side G-code file to start."
@@ -790,7 +803,7 @@ class ThreeDPrinterMCPServer {
790
803
  },
791
804
  slicer_path: {
792
805
  type: "string",
793
- description: "Path to the slicer executable (default: value from env)"
806
+ description: "Path to the slicer executable (default: value from env). Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1."
794
807
  },
795
808
  slicer_profile: {
796
809
  type: "string",
@@ -885,7 +898,7 @@ class ThreeDPrinterMCPServer {
885
898
  },
886
899
  slicer_path: {
887
900
  type: "string",
888
- description: "Path to the slicer executable (default: value from env)."
901
+ description: "Path to the slicer executable (default: value from env). Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1."
889
902
  },
890
903
  slicer_profile: {
891
904
  type: "string",
@@ -1130,7 +1143,7 @@ class ThreeDPrinterMCPServer {
1130
1143
  },
1131
1144
  slicer_path: {
1132
1145
  type: "string",
1133
- description: "Path to the slicer executable if auto-slicing is needed."
1146
+ description: "Path to the slicer executable if auto-slicing is needed. Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1."
1134
1147
  },
1135
1148
  slicer_profile: {
1136
1149
  type: "string",
@@ -1177,15 +1190,23 @@ class ThreeDPrinterMCPServer {
1177
1190
  properties: {
1178
1191
  slicer_path: {
1179
1192
  type: "string",
1180
- description: "Path to the FULU OrcaSlicer executable. Defaults from SLICER_PATH/FULU_ORCA_PATH."
1193
+ description:
1194
+ "Path to the FULU OrcaSlicer executable. Defaults from SLICER_PATH/FULU_ORCA_PATH. " +
1195
+ "When run_bridge_probe=true, requires MCP_ALLOW_EXECUTABLE_ARG=1 to be accepted here."
1181
1196
  },
1182
1197
  plugin_dir: {
1183
1198
  type: "string",
1184
- description: "Directory containing the FULU Bambu runtime payload; on macOS this is usually OrcaSlicer.app/Contents/MacOS."
1199
+ description:
1200
+ "Directory containing the FULU Bambu runtime payload; on macOS this is usually " +
1201
+ "OrcaSlicer.app/Contents/MacOS. When run_bridge_probe=true, requires " +
1202
+ "MCP_ALLOW_EXECUTABLE_ARG=1 to be accepted here."
1185
1203
  },
1186
1204
  runtime_dir: {
1187
1205
  type: "string",
1188
- description: "Installed runtime directory. On macOS this defaults to ~/Library/Application Support/OrcaSlicer/macos-bridge/runtime."
1206
+ description:
1207
+ "Installed runtime directory. On macOS this defaults to ~/Library/Application " +
1208
+ "Support/OrcaSlicer/macos-bridge/runtime. When run_bridge_probe=true, requires " +
1209
+ "MCP_ALLOW_EXECUTABLE_ARG=1 to be accepted here."
1189
1210
  },
1190
1211
  platform: {
1191
1212
  type: "string",
@@ -1194,7 +1215,10 @@ class ThreeDPrinterMCPServer {
1194
1215
  },
1195
1216
  bridge_command: {
1196
1217
  type: "string",
1197
- description: "Command that starts the FULU BambuNetwork bridge host for probing."
1218
+ description:
1219
+ "Command that starts the FULU BambuNetwork bridge host for probing. Read from " +
1220
+ "FULU_BAMBU_BRIDGE_COMMAND by default; requires MCP_ALLOW_EXECUTABLE_ARG=1 " +
1221
+ "to be accepted here."
1198
1222
  },
1199
1223
  run_bridge_probe: {
1200
1224
  type: "boolean",
@@ -1216,7 +1240,10 @@ class ThreeDPrinterMCPServer {
1216
1240
  properties: {
1217
1241
  bridge_command: {
1218
1242
  type: "string",
1219
- description: "Command that starts the FULU BambuNetwork bridge host. Defaults to FULU_BAMBU_BRIDGE_COMMAND."
1243
+ description:
1244
+ "Command that starts the FULU BambuNetwork bridge host. Defaults to " +
1245
+ "FULU_BAMBU_BRIDGE_COMMAND; requires MCP_ALLOW_EXECUTABLE_ARG=1 to be " +
1246
+ "accepted here."
1220
1247
  },
1221
1248
  method: {
1222
1249
  type: "string",
@@ -1290,31 +1317,54 @@ class ThreeDPrinterMCPServer {
1290
1317
  required: ["stl_path"]
1291
1318
  }
1292
1319
  },
1320
+ {
1321
+ name: "blender_mcp_status",
1322
+ 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.",
1323
+ inputSchema: {
1324
+ type: "object",
1325
+ properties: {
1326
+ connect: { type: "boolean", description: "Initialize the configured stdio MCP server and discover its tools (default false)." },
1327
+ timeout_ms: { type: "integer", minimum: 100, maximum: 300000, description: "Total connection and discovery deadline in milliseconds; defaults to BLENDER_MCP_TIMEOUT_MS or 120000." }
1328
+ },
1329
+ additionalProperties: false
1330
+ }
1331
+ },
1332
+ {
1333
+ name: "blender_mcp_call",
1334
+ 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.",
1335
+ inputSchema: {
1336
+ type: "object",
1337
+ properties: {
1338
+ tool_name: { type: "string", description: "Exact name advertised by Blender MCP, such as get_scene_info or execute_blender_code." },
1339
+ 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." },
1340
+ 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." }
1341
+ },
1342
+ required: ["tool_name"],
1343
+ additionalProperties: false
1344
+ }
1345
+ },
1293
1346
  {
1294
1347
  name: "blender_mcp_edit_model",
1295
- description: "Optionally send STL-edit instructions to a Blender MCP bridge command. Use for advanced model edits outside built-in STL tools.",
1348
+ 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.",
1296
1349
  inputSchema: {
1297
1350
  type: "object",
1298
1351
  properties: {
1299
- stl_path: {
1300
- type: "string",
1301
- description: "Path to the local STL file."
1302
- },
1352
+ stl_path: { type: "string", description: "Path to the local STL file" },
1303
1353
  operations: {
1304
1354
  type: "array",
1305
- description: "Ordered edit operations for Blender (e.g. remesh, boolean, decimate).",
1355
+ 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.",
1356
+ minItems: 1,
1357
+ maxItems: 64,
1306
1358
  items: { type: "string" }
1307
1359
  },
1308
- bridge_command: {
1309
- type: "string",
1310
- description: "Optional override command for invoking a local Blender MCP bridge."
1311
- },
1312
- execute: {
1313
- type: "boolean",
1314
- description: "When true, attempts to execute bridge command; otherwise returns a prepared payload only."
1315
- }
1360
+ output_path: { type: "string", description: "New local STL output path for standard MCP editing; defaults to a unique model-edited-<id>.stl beside the input. Its parent must exist and existing files are never overwritten. Reuse the preview's output_path when executing that plan." },
1361
+ user_prompt: { type: "string", description: "The user's own words describing the edit, passed unchanged to Blender MCP." },
1362
+ timeout_ms: { type: "integer", minimum: 100, maximum: 300000, description: "Total Blender request deadline in milliseconds; defaults to BLENDER_MCP_TIMEOUT_MS or 120000." },
1363
+ bridge_command: { type: "string", description: "Legacy custom bridge executable override, not a standard MCP command. Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1." },
1364
+ execute: { type: "boolean", description: "Apply edits and export (true) or validate and return the prepared request without connecting (false, default)." }
1316
1365
  },
1317
- required: ["stl_path", "operations"]
1366
+ required: ["stl_path", "operations"],
1367
+ additionalProperties: false
1318
1368
  }
1319
1369
  }
1320
1370
  ]
@@ -1322,7 +1372,7 @@ class ThreeDPrinterMCPServer {
1322
1372
  });
1323
1373
 
1324
1374
  // Handle tool calls
1325
- this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
1375
+ this.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
1326
1376
  const { name, arguments: args } = request.params;
1327
1377
 
1328
1378
  // Set default values for common parameters
@@ -1333,7 +1383,6 @@ class ThreeDPrinterMCPServer {
1333
1383
  const bambuSerial = String(args?.bambu_serial || DEFAULT_BAMBU_SERIAL);
1334
1384
  const bambuToken = String(args?.bambu_token || DEFAULT_BAMBU_TOKEN);
1335
1385
  const slicerType = normalizeSlicerType(String(args?.slicer_type || DEFAULT_SLICER_TYPE));
1336
- const slicerPath = String(args?.slicer_path || DEFAULT_SLICER_PATH);
1337
1386
  const slicerProfile = String(args?.slicer_profile || DEFAULT_SLICER_PROFILE);
1338
1387
  const filamentProfile = String(args?.filament_profile || DEFAULT_FILAMENT_PROFILE);
1339
1388
 
@@ -1359,6 +1408,12 @@ class ThreeDPrinterMCPServer {
1359
1408
  if (!args?.filename && !args?.gcode_path) {
1360
1409
  throw new Error("Missing required parameter: filename");
1361
1410
  }
1411
+ if (args.print !== undefined && typeof args.print !== "boolean") {
1412
+ throw new Error("print must be a boolean.");
1413
+ }
1414
+ if (type.toLowerCase() === "bambu" && args.print === true) {
1415
+ await this.resolveBambuModel(args?.bambu_model as string | undefined);
1416
+ }
1362
1417
  const uploadFilename = String(args?.filename || path.basename(String(args.gcode_path)));
1363
1418
  const uploadGcodeSource = String(args?.gcode_path || args.gcode);
1364
1419
  result = await this.uploadGcode(
@@ -1373,6 +1428,9 @@ class ThreeDPrinterMCPServer {
1373
1428
  if (!args?.filename) {
1374
1429
  throw new Error("Missing required parameter: filename");
1375
1430
  }
1431
+ if (type.toLowerCase() === "bambu") {
1432
+ await this.resolveBambuModel(args?.bambu_model as string | undefined);
1433
+ }
1376
1434
  result = await this.startPrint(host, port, type, apiKey, bambuSerial, bambuToken, String(args.filename));
1377
1435
  break;
1378
1436
 
@@ -1416,7 +1474,11 @@ class ThreeDPrinterMCPServer {
1416
1474
  result = await this.stlManipulator.sliceSTL(
1417
1475
  String(args.stl_path),
1418
1476
  slicerType,
1419
- slicerPath,
1477
+ this.resolveExecutableSelectorArg(
1478
+ args?.slicer_path,
1479
+ "slice_stl",
1480
+ "slicer_path"
1481
+ ) ?? DEFAULT_SLICER_PATH,
1420
1482
  slicerProfile || undefined,
1421
1483
  undefined, // progressCallback
1422
1484
  slicePreset,
@@ -1467,7 +1529,11 @@ class ThreeDPrinterMCPServer {
1467
1529
  const gcodePath = await this.stlManipulator.sliceSTL(
1468
1530
  extendedStlPath,
1469
1531
  slicerType,
1470
- slicerPath,
1532
+ this.resolveExecutableSelectorArg(
1533
+ args?.slicer_path,
1534
+ "process_and_print_stl",
1535
+ "slicer_path"
1536
+ ) ?? DEFAULT_SLICER_PATH,
1471
1537
  slicerProfile || undefined,
1472
1538
  processProgressCallback,
1473
1539
  processPreset,
@@ -1765,29 +1831,42 @@ class ThreeDPrinterMCPServer {
1765
1831
 
1766
1832
  let threeMFPath = String(args.three_mf_path);
1767
1833
 
1768
- // Auto-slice if the 3MF doesn't contain gcode
1834
+ let shouldAutoSlice = false;
1769
1835
  try {
1770
1836
  const JSZip = (await import('jszip')).default;
1771
1837
  const zipData = fs.readFileSync(threeMFPath);
1772
1838
  const zip = await JSZip.loadAsync(zipData);
1773
- const hasGcode = Object.keys(zip.files).some(
1839
+ shouldAutoSlice = !Object.keys(zip.files).some(
1774
1840
  f => f.match(/Metadata\/plate_\d+\.gcode/i) || f.endsWith('.gcode')
1775
1841
  );
1776
- if (!hasGcode) {
1842
+ } catch (sliceCheckErr: any) {
1843
+ console.warn("Could not check 3MF for embedded gcode, proceeding with original:", sliceCheckErr.message);
1844
+ }
1845
+
1846
+ // Only treat slicer_path as an executable selector when a slicer
1847
+ // will actually launch. Ready-to-print archives ignore it.
1848
+ if (shouldAutoSlice) {
1849
+ const printSlicerPath =
1850
+ this.resolveExecutableSelectorArg(
1851
+ args?.slicer_path,
1852
+ "print_3mf",
1853
+ "slicer_path"
1854
+ ) ?? DEFAULT_SLICER_PATH;
1855
+ try {
1777
1856
  console.log(`3MF has no gcode — auto-slicing with ${slicerType} for ${printModel}`);
1778
1857
  threeMFPath = await this.stlManipulator.sliceSTL(
1779
1858
  threeMFPath,
1780
1859
  slicerType,
1781
- slicerPath,
1860
+ printSlicerPath,
1782
1861
  slicerProfile || undefined,
1783
1862
  undefined, // progressCallback
1784
1863
  printPreset,
1785
1864
  filamentProfile || undefined
1786
1865
  );
1787
1866
  console.log("Auto-sliced to: " + threeMFPath);
1867
+ } catch (sliceErr: any) {
1868
+ console.warn("Could not auto-slice 3MF, proceeding with original:", sliceErr.message);
1788
1869
  }
1789
- } catch (sliceCheckErr: any) {
1790
- console.warn("Could not check/slice 3MF, proceeding with original:", sliceCheckErr.message);
1791
1870
  }
1792
1871
 
1793
1872
  // Define variables needed outside the parse try block
@@ -1849,13 +1928,38 @@ class ThreeDPrinterMCPServer {
1849
1928
  }
1850
1929
 
1851
1930
  case "check_fulu_orca_setup": {
1931
+ const runBridgeProbe = Boolean(args?.run_bridge_probe ?? false);
1852
1932
  result = await inspectFuluOrcaSetup({
1853
- slicerPath: args?.slicer_path !== undefined ? String(args.slicer_path) : undefined,
1854
- pluginDir: args?.plugin_dir !== undefined ? String(args.plugin_dir) : undefined,
1855
- runtimeDir: args?.runtime_dir !== undefined ? String(args.runtime_dir) : undefined,
1933
+ slicerPath: this.resolveExecutableSelectorArg(
1934
+ args?.slicer_path,
1935
+ "check_fulu_orca_setup",
1936
+ "slicer_path",
1937
+ runBridgeProbe,
1938
+ true
1939
+ ),
1940
+ pluginDir: this.resolveExecutableSelectorArg(
1941
+ args?.plugin_dir,
1942
+ "check_fulu_orca_setup",
1943
+ "plugin_dir",
1944
+ runBridgeProbe,
1945
+ true
1946
+ ),
1947
+ runtimeDir: this.resolveExecutableSelectorArg(
1948
+ args?.runtime_dir,
1949
+ "check_fulu_orca_setup",
1950
+ "runtime_dir",
1951
+ runBridgeProbe,
1952
+ true
1953
+ ),
1856
1954
  platform: args?.platform !== undefined ? String(args.platform) : undefined,
1857
- bridgeCommand: args?.bridge_command !== undefined ? String(args.bridge_command) : undefined,
1858
- runBridgeProbe: Boolean(args?.run_bridge_probe ?? false),
1955
+ bridgeCommand: this.resolveExecutableSelectorArg(
1956
+ args?.bridge_command,
1957
+ "check_fulu_orca_setup",
1958
+ "bridge_command",
1959
+ true,
1960
+ true
1961
+ ),
1962
+ runBridgeProbe,
1859
1963
  probeTimeoutMs:
1860
1964
  args?.probe_timeout_ms !== undefined ? Number(args.probe_timeout_ms) : undefined,
1861
1965
  });
@@ -1879,8 +1983,13 @@ class ThreeDPrinterMCPServer {
1879
1983
  : undefined;
1880
1984
 
1881
1985
  result = await invokeFuluBridgeRpc({
1882
- bridgeCommand:
1883
- args?.bridge_command !== undefined ? String(args.bridge_command) : undefined,
1986
+ bridgeCommand: this.resolveExecutableSelectorArg(
1987
+ args?.bridge_command,
1988
+ "fulu_bambu_network_rpc",
1989
+ "bridge_command",
1990
+ true,
1991
+ true
1992
+ ),
1884
1993
  method,
1885
1994
  payload,
1886
1995
  timeoutMs: args?.timeout_ms !== undefined ? Number(args.timeout_ms) : undefined,
@@ -1914,19 +2023,27 @@ class ThreeDPrinterMCPServer {
1914
2023
  result = await this.stlManipulator.layFlat(String(args.stl_path));
1915
2024
  break;
1916
2025
 
2026
+ case "blender_mcp_status":
2027
+ result = await this.blender.status(args ?? {}, extra.signal);
2028
+ break;
2029
+
2030
+ case "blender_mcp_call":
2031
+ return await this.blender.call(args ?? {}, extra.signal);
2032
+
1917
2033
  case "blender_mcp_edit_model":
1918
- if (!args?.stl_path || !Array.isArray(args.operations)) {
1919
- throw new Error("Missing required parameters: stl_path and operations");
1920
- }
1921
- result = await this.invokeBlenderBridge({
1922
- stlPath: String(args.stl_path),
1923
- operations: args.operations.map((entry) => String(entry)),
1924
- execute: Boolean(args.execute ?? false),
1925
- bridgeCommand:
1926
- args.bridge_command !== undefined ? String(args.bridge_command) : undefined,
1927
- });
2034
+ result = await this.blender.edit(
2035
+ args ?? {},
2036
+ this.resolveExecutableSelectorArg(
2037
+ args?.bridge_command,
2038
+ "blender_mcp_edit_model",
2039
+ "bridge_command",
2040
+ true,
2041
+ true
2042
+ ) ?? (!process.env.BLENDER_MCP_COMMAND?.trim() ? this.runtimeConfig.blenderBridgeCommand : undefined),
2043
+ extra.signal
2044
+ );
1928
2045
  break;
1929
-
2046
+
1930
2047
  default:
1931
2048
  throw new Error(`Unknown tool: ${name}`);
1932
2049
  }
@@ -2028,14 +2145,17 @@ class ThreeDPrinterMCPServer {
2028
2145
  print: boolean
2029
2146
  ) {
2030
2147
  const sourceIsFilePath = fs.existsSync(gcode) && fs.statSync(gcode).isFile();
2031
- const tempFilePath = sourceIsFilePath ? gcode : path.join(TEMP_DIR, filename);
2032
-
2033
- if (!sourceIsFilePath) {
2034
- fs.mkdirSync(TEMP_DIR, { recursive: true });
2035
- fs.writeFileSync(tempFilePath, gcode);
2148
+ // The remote filename must never select a local write/delete target.
2149
+ const localFilename = path.basename(filename);
2150
+ if (!sourceIsFilePath && (!localFilename || localFilename === "." || localFilename === "..")) {
2151
+ throw new Error("filename must include a file basename.");
2036
2152
  }
2153
+ const scratchDir = sourceIsFilePath ? undefined : fs.mkdtempSync(path.join(TEMP_DIR, "upload-"));
2154
+ // Multipart adapters infer their file-part name from this safe basename.
2155
+ const tempFilePath = scratchDir ? path.join(scratchDir, localFilename) : gcode;
2037
2156
 
2038
2157
  try {
2158
+ if (scratchDir) fs.writeFileSync(tempFilePath, gcode);
2039
2159
  const implementation = this.printerFactory.getImplementation(type);
2040
2160
 
2041
2161
  if (type.toLowerCase() === "bambu") {
@@ -2045,9 +2165,7 @@ class ThreeDPrinterMCPServer {
2045
2165
 
2046
2166
  return await implementation.uploadFile(host, port, apiKey, tempFilePath, filename, print);
2047
2167
  } finally {
2048
- if (!sourceIsFilePath && fs.existsSync(tempFilePath)) {
2049
- fs.unlinkSync(tempFilePath);
2050
- }
2168
+ if (scratchDir) fs.rmSync(scratchDir, { recursive: true, force: true });
2051
2169
  }
2052
2170
  }
2053
2171
 
@@ -2244,78 +2362,44 @@ class ThreeDPrinterMCPServer {
2244
2362
  );
2245
2363
  }
2246
2364
 
2247
- private async invokeBlenderBridge(params: {
2248
- stlPath: string;
2249
- operations: string[];
2250
- execute: boolean;
2251
- bridgeCommand?: string;
2252
- }): Promise<Record<string, unknown>> {
2253
- const command = params.bridgeCommand ?? this.runtimeConfig.blenderBridgeCommand;
2254
- const payload = {
2255
- modelPath: params.stlPath,
2256
- operations: params.operations,
2257
- source: "mcp-3d-printer-server"
2258
- };
2365
+ /**
2366
+ * Bridge commands and path-derived bridge executables are machine-local
2367
+ * settings. Accepting their selectors as tool arguments means anything that
2368
+ * can steer the model — a model description, a README in a downloaded
2369
+ * archive, 3MF metadata — can choose the program this server runs. They are
2370
+ * read from environment configuration by default and only accepted for
2371
+ * execution when MCP_ALLOW_EXECUTABLE_ARG is set. The older
2372
+ * MCP_ALLOW_BRIDGE_COMMAND_ARG name remains a compatibility alias only for
2373
+ * bridge commands and path selectors used to derive a bridge executable.
2374
+ */
2375
+ private resolveExecutableSelectorArg(
2376
+ rawValue: unknown,
2377
+ toolName: string,
2378
+ argumentName: string,
2379
+ executionRequested = true,
2380
+ allowLegacyBridgeArg = false
2381
+ ): string | undefined {
2382
+ if (rawValue === undefined) {
2383
+ return undefined;
2384
+ }
2259
2385
 
2260
- if (!params.execute) {
2261
- return {
2262
- status: "prepared",
2263
- message: "Prepared Blender MCP payload. Set execute=true to run bridge command.",
2264
- bridgeCommand: command ?? null,
2265
- payload
2266
- };
2386
+ const value = String(rawValue);
2387
+ if (value.length === 0) {
2388
+ return undefined;
2267
2389
  }
2268
2390
 
2269
- if (!command) {
2391
+ const isAllowed =
2392
+ this.runtimeConfig.allowExecutableArg ||
2393
+ (allowLegacyBridgeArg && this.runtimeConfig.allowBridgeCommandArg);
2394
+ if (executionRequested && !isAllowed) {
2270
2395
  throw new Error(
2271
- "execute=true requires bridge_command or BLENDER_MCP_BRIDGE_COMMAND env var."
2396
+ `${toolName}: the ${argumentName} argument cannot select an executable by default. ` +
2397
+ "Executable paths and commands are read from server environment configuration. Set " +
2398
+ `MCP_ALLOW_EXECUTABLE_ARG=1 to accept ${argumentName} for process execution.`
2272
2399
  );
2273
2400
  }
2274
2401
 
2275
- const { spawn } = await import("node:child_process");
2276
- const result = await new Promise<{ stdout: string; stderr: string }>((resolve, reject) => {
2277
- const child = spawn(command, {
2278
- shell: true,
2279
- stdio: ["pipe", "pipe", "pipe"]
2280
- });
2281
-
2282
- let stdout = "";
2283
- let stderr = "";
2284
-
2285
- child.stdout.setEncoding("utf8");
2286
- child.stderr.setEncoding("utf8");
2287
- child.stdout.on("data", (chunk: string) => {
2288
- stdout += chunk;
2289
- });
2290
- child.stderr.on("data", (chunk: string) => {
2291
- stderr += chunk;
2292
- });
2293
- child.on("error", reject);
2294
- child.on("close", (code) => {
2295
- if (code === 0) {
2296
- resolve({ stdout, stderr });
2297
- return;
2298
- }
2299
-
2300
- reject(
2301
- new Error(
2302
- `Blender bridge command exited with code ${code ?? "unknown"}${
2303
- stderr.trim() ? `: ${stderr.trim()}` : ""
2304
- }`
2305
- )
2306
- );
2307
- });
2308
-
2309
- child.stdin.write(JSON.stringify(payload));
2310
- child.stdin.end();
2311
- });
2312
-
2313
- return {
2314
- status: "executed",
2315
- bridgeCommand: command,
2316
- stdout: result.stdout?.toString() ?? "",
2317
- stderr: result.stderr?.toString() ?? ""
2318
- };
2402
+ return value;
2319
2403
  }
2320
2404
 
2321
2405
  async close(): Promise<void> {
@@ -473,7 +473,13 @@ export class BambuImplementation extends PrinterImplementation {
473
473
  user: "bblp",
474
474
  password: token,
475
475
  secure: "implicit",
476
- secureOptions: { rejectUnauthorized: false },
476
+ // basic-ftp passes getSession() to the data socket. Keep both channels
477
+ // on TLS 1.2, where that session is available at handshake completion;
478
+ // TLS 1.3 tickets arrive later and can break Bambu's required reuse.
479
+ // Node also binds resumable sessions to the host identity. basic-ftp
480
+ // retains these options for its wrapped data socket, so explicitly keep
481
+ // the same host there (nodejs/node#64402 affects implicit FTPS).
482
+ secureOptions: { host, rejectUnauthorized: false, minVersion: "TLSv1.2", maxVersion: "TLSv1.2" },
477
483
  });
478
484
  // Use absolute path to avoid CWD side-effects
479
485
  const absoluteRemote = remotePath.startsWith("/") ? remotePath : `/${remotePath}`;
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+
3
+ process.env.MCP_TRANSPORT ??= "streamable-http";
4
+
5
+ if (!process.env.MCP_HTTP_PORT && process.env.SSE_PORT) {
6
+ process.env.MCP_HTTP_PORT = process.env.SSE_PORT;
7
+ }
8
+
9
+ void import("./index.js");