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/dist/index.js CHANGED
@@ -7,6 +7,8 @@ import axios from "axios";
7
7
  import dotenv from "dotenv";
8
8
  import fs from "fs";
9
9
  import path from "path";
10
+ import os from "node:os";
11
+ import { BlenderMcpBridge } from "./blender-mcp-bridge.js";
10
12
  import { createServer as createHttpServer } from "node:http";
11
13
  import { randomUUID } from "node:crypto";
12
14
  import * as THREE from 'three';
@@ -22,7 +24,7 @@ const DEFAULT_HOST = process.env.PRINTER_HOST || "localhost";
22
24
  const DEFAULT_PORT = process.env.PRINTER_PORT || "80";
23
25
  const DEFAULT_API_KEY = process.env.API_KEY || "";
24
26
  const DEFAULT_TYPE = process.env.PRINTER_TYPE || "octoprint"; // Default to OctoPrint
25
- const TEMP_DIR = process.env.TEMP_DIR || path.join(process.cwd(), "temp");
27
+ const TEMP_DIR = process.env.TEMP_DIR || fs.mkdtempSync(path.join(os.tmpdir(), "mcp-3d-printer-server-"));
26
28
  // Slicer configuration
27
29
  const CANONICAL_SLICER_TYPES = [
28
30
  "prusaslicer",
@@ -193,6 +195,8 @@ function readRuntimeConfig() {
193
195
  enableJsonResponse: parseBooleanEnv(process.env.MCP_HTTP_JSON_RESPONSE, true),
194
196
  allowedOrigins: parseCsvEnv(process.env.MCP_HTTP_ALLOWED_ORIGINS),
195
197
  blenderBridgeCommand: process.env.BLENDER_MCP_BRIDGE_COMMAND?.trim() || undefined,
198
+ allowExecutableArg: parseBooleanEnv(process.env.MCP_ALLOW_EXECUTABLE_ARG, false),
199
+ allowBridgeCommandArg: parseBooleanEnv(process.env.MCP_ALLOW_BRIDGE_COMMAND_ARG, false),
196
200
  };
197
201
  }
198
202
  // Ensure temp directory exists
@@ -201,10 +205,11 @@ if (!fs.existsSync(TEMP_DIR)) {
201
205
  }
202
206
  class ThreeDPrinterMCPServer {
203
207
  constructor() {
208
+ this.blender = new BlenderMcpBridge();
204
209
  this.runtimeConfig = readRuntimeConfig();
205
210
  this.server = new Server({
206
211
  name: "mcp-3d-printer-server",
207
- version: "1.2.0"
212
+ version: JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")).version
208
213
  }, {
209
214
  capabilities: {
210
215
  resources: {},
@@ -489,6 +494,11 @@ class ThreeDPrinterMCPServer {
489
494
  type: "string",
490
495
  description: "Access token for Bambu Lab printers (default: value from env)"
491
496
  },
497
+ bambu_model: {
498
+ type: "string",
499
+ enum: [...VALID_BAMBU_MODELS],
500
+ description: "Required for Bambu print operations unless BAMBU_MODEL is configured. Must match the printer and pre-sliced G-code."
501
+ },
492
502
  filename: {
493
503
  type: "string",
494
504
  description: "Filename to use on the printer. Defaults to the basename of gcode_path when omitted."
@@ -505,11 +515,7 @@ class ThreeDPrinterMCPServer {
505
515
  type: "boolean",
506
516
  description: "Start printing after upload when the printer backend supports it."
507
517
  }
508
- },
509
- anyOf: [
510
- { required: ["filename", "gcode"] },
511
- { required: ["gcode_path"] }
512
- ]
518
+ }
513
519
  }
514
520
  },
515
521
  {
@@ -542,6 +548,11 @@ class ThreeDPrinterMCPServer {
542
548
  type: "string",
543
549
  description: "Access token for Bambu Lab printers (default: value from env)"
544
550
  },
551
+ bambu_model: {
552
+ type: "string",
553
+ enum: [...VALID_BAMBU_MODELS],
554
+ description: "Required for Bambu print operations unless BAMBU_MODEL is configured. Must match the printer and pre-sliced G-code."
555
+ },
545
556
  filename: {
546
557
  type: "string",
547
558
  description: "Name/path of the printer-side G-code file to start."
@@ -670,7 +681,7 @@ class ThreeDPrinterMCPServer {
670
681
  },
671
682
  slicer_path: {
672
683
  type: "string",
673
- description: "Path to the slicer executable (default: value from env)"
684
+ description: "Path to the slicer executable (default: value from env). Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1."
674
685
  },
675
686
  slicer_profile: {
676
687
  type: "string",
@@ -765,7 +776,7 @@ class ThreeDPrinterMCPServer {
765
776
  },
766
777
  slicer_path: {
767
778
  type: "string",
768
- description: "Path to the slicer executable (default: value from env)."
779
+ description: "Path to the slicer executable (default: value from env). Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1."
769
780
  },
770
781
  slicer_profile: {
771
782
  type: "string",
@@ -1010,7 +1021,7 @@ class ThreeDPrinterMCPServer {
1010
1021
  },
1011
1022
  slicer_path: {
1012
1023
  type: "string",
1013
- description: "Path to the slicer executable if auto-slicing is needed."
1024
+ description: "Path to the slicer executable if auto-slicing is needed. Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1."
1014
1025
  },
1015
1026
  slicer_profile: {
1016
1027
  type: "string",
@@ -1056,15 +1067,20 @@ class ThreeDPrinterMCPServer {
1056
1067
  properties: {
1057
1068
  slicer_path: {
1058
1069
  type: "string",
1059
- description: "Path to the FULU OrcaSlicer executable. Defaults from SLICER_PATH/FULU_ORCA_PATH."
1070
+ description: "Path to the FULU OrcaSlicer executable. Defaults from SLICER_PATH/FULU_ORCA_PATH. " +
1071
+ "When run_bridge_probe=true, requires MCP_ALLOW_EXECUTABLE_ARG=1 to be accepted here."
1060
1072
  },
1061
1073
  plugin_dir: {
1062
1074
  type: "string",
1063
- description: "Directory containing the FULU Bambu runtime payload; on macOS this is usually OrcaSlicer.app/Contents/MacOS."
1075
+ description: "Directory containing the FULU Bambu runtime payload; on macOS this is usually " +
1076
+ "OrcaSlicer.app/Contents/MacOS. When run_bridge_probe=true, requires " +
1077
+ "MCP_ALLOW_EXECUTABLE_ARG=1 to be accepted here."
1064
1078
  },
1065
1079
  runtime_dir: {
1066
1080
  type: "string",
1067
- description: "Installed runtime directory. On macOS this defaults to ~/Library/Application Support/OrcaSlicer/macos-bridge/runtime."
1081
+ description: "Installed runtime directory. On macOS this defaults to ~/Library/Application " +
1082
+ "Support/OrcaSlicer/macos-bridge/runtime. When run_bridge_probe=true, requires " +
1083
+ "MCP_ALLOW_EXECUTABLE_ARG=1 to be accepted here."
1068
1084
  },
1069
1085
  platform: {
1070
1086
  type: "string",
@@ -1073,7 +1089,9 @@ class ThreeDPrinterMCPServer {
1073
1089
  },
1074
1090
  bridge_command: {
1075
1091
  type: "string",
1076
- description: "Command that starts the FULU BambuNetwork bridge host for probing."
1092
+ description: "Command that starts the FULU BambuNetwork bridge host for probing. Read from " +
1093
+ "FULU_BAMBU_BRIDGE_COMMAND by default; requires MCP_ALLOW_EXECUTABLE_ARG=1 " +
1094
+ "to be accepted here."
1077
1095
  },
1078
1096
  run_bridge_probe: {
1079
1097
  type: "boolean",
@@ -1094,7 +1112,9 @@ class ThreeDPrinterMCPServer {
1094
1112
  properties: {
1095
1113
  bridge_command: {
1096
1114
  type: "string",
1097
- description: "Command that starts the FULU BambuNetwork bridge host. Defaults to FULU_BAMBU_BRIDGE_COMMAND."
1115
+ description: "Command that starts the FULU BambuNetwork bridge host. Defaults to " +
1116
+ "FULU_BAMBU_BRIDGE_COMMAND; requires MCP_ALLOW_EXECUTABLE_ARG=1 to be " +
1117
+ "accepted here."
1098
1118
  },
1099
1119
  method: {
1100
1120
  type: "string",
@@ -1168,38 +1188,61 @@ class ThreeDPrinterMCPServer {
1168
1188
  required: ["stl_path"]
1169
1189
  }
1170
1190
  },
1191
+ {
1192
+ name: "blender_mcp_status",
1193
+ 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.",
1194
+ inputSchema: {
1195
+ type: "object",
1196
+ properties: {
1197
+ connect: { type: "boolean", description: "Initialize the configured stdio MCP server and discover its tools (default false)." },
1198
+ timeout_ms: { type: "integer", minimum: 100, maximum: 300000, description: "Total connection and discovery deadline in milliseconds; defaults to BLENDER_MCP_TIMEOUT_MS or 120000." }
1199
+ },
1200
+ additionalProperties: false
1201
+ }
1202
+ },
1203
+ {
1204
+ name: "blender_mcp_call",
1205
+ 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.",
1206
+ inputSchema: {
1207
+ type: "object",
1208
+ properties: {
1209
+ tool_name: { type: "string", description: "Exact name advertised by Blender MCP, such as get_scene_info or execute_blender_code." },
1210
+ 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." },
1211
+ 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." }
1212
+ },
1213
+ required: ["tool_name"],
1214
+ additionalProperties: false
1215
+ }
1216
+ },
1171
1217
  {
1172
1218
  name: "blender_mcp_edit_model",
1173
- description: "Optionally send STL-edit instructions to a Blender MCP bridge command. Use for advanced model edits outside built-in STL tools.",
1219
+ 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.",
1174
1220
  inputSchema: {
1175
1221
  type: "object",
1176
1222
  properties: {
1177
- stl_path: {
1178
- type: "string",
1179
- description: "Path to the local STL file."
1180
- },
1223
+ stl_path: { type: "string", description: "Path to the local STL file" },
1181
1224
  operations: {
1182
1225
  type: "array",
1183
- description: "Ordered edit operations for Blender (e.g. remesh, boolean, decimate).",
1226
+ 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.",
1227
+ minItems: 1,
1228
+ maxItems: 64,
1184
1229
  items: { type: "string" }
1185
1230
  },
1186
- bridge_command: {
1187
- type: "string",
1188
- description: "Optional override command for invoking a local Blender MCP bridge."
1189
- },
1190
- execute: {
1191
- type: "boolean",
1192
- description: "When true, attempts to execute bridge command; otherwise returns a prepared payload only."
1193
- }
1231
+ 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." },
1232
+ user_prompt: { type: "string", description: "The user's own words describing the edit, passed unchanged to Blender MCP." },
1233
+ timeout_ms: { type: "integer", minimum: 100, maximum: 300000, description: "Total Blender request deadline in milliseconds; defaults to BLENDER_MCP_TIMEOUT_MS or 120000." },
1234
+ bridge_command: { type: "string", description: "Legacy custom bridge executable override, not a standard MCP command. Per-call overrides require MCP_ALLOW_EXECUTABLE_ARG=1." },
1235
+ execute: { type: "boolean", description: "Apply edits and export (true) or validate and return the prepared request without connecting (false, default)." }
1194
1236
  },
1195
- required: ["stl_path", "operations"]
1237
+ required: ["stl_path", "operations"],
1238
+ additionalProperties: false
1196
1239
  }
1197
1240
  }
1198
1241
  ]
1199
1242
  };
1200
1243
  });
1201
1244
  // Handle tool calls
1202
- this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
1245
+ this.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
1203
1246
  const { name, arguments: args } = request.params;
1204
1247
  // Set default values for common parameters
1205
1248
  const host = String(args?.host || DEFAULT_HOST);
@@ -1209,7 +1252,6 @@ class ThreeDPrinterMCPServer {
1209
1252
  const bambuSerial = String(args?.bambu_serial || DEFAULT_BAMBU_SERIAL);
1210
1253
  const bambuToken = String(args?.bambu_token || DEFAULT_BAMBU_TOKEN);
1211
1254
  const slicerType = normalizeSlicerType(String(args?.slicer_type || DEFAULT_SLICER_TYPE));
1212
- const slicerPath = String(args?.slicer_path || DEFAULT_SLICER_PATH);
1213
1255
  const slicerProfile = String(args?.slicer_profile || DEFAULT_SLICER_PROFILE);
1214
1256
  const filamentProfile = String(args?.filament_profile || DEFAULT_FILAMENT_PROFILE);
1215
1257
  try {
@@ -1231,6 +1273,12 @@ class ThreeDPrinterMCPServer {
1231
1273
  if (!args?.filename && !args?.gcode_path) {
1232
1274
  throw new Error("Missing required parameter: filename");
1233
1275
  }
1276
+ if (args.print !== undefined && typeof args.print !== "boolean") {
1277
+ throw new Error("print must be a boolean.");
1278
+ }
1279
+ if (type.toLowerCase() === "bambu" && args.print === true) {
1280
+ await this.resolveBambuModel(args?.bambu_model);
1281
+ }
1234
1282
  const uploadFilename = String(args?.filename || path.basename(String(args.gcode_path)));
1235
1283
  const uploadGcodeSource = String(args?.gcode_path || args.gcode);
1236
1284
  result = await this.uploadGcode(host, port, type, apiKey, bambuSerial, bambuToken, uploadFilename, uploadGcodeSource, Boolean(args.print || false));
@@ -1239,6 +1287,9 @@ class ThreeDPrinterMCPServer {
1239
1287
  if (!args?.filename) {
1240
1288
  throw new Error("Missing required parameter: filename");
1241
1289
  }
1290
+ if (type.toLowerCase() === "bambu") {
1291
+ await this.resolveBambuModel(args?.bambu_model);
1292
+ }
1242
1293
  result = await this.startPrint(host, port, type, apiKey, bambuSerial, bambuToken, String(args.filename));
1243
1294
  break;
1244
1295
  case "cancel_print":
@@ -1268,7 +1319,7 @@ class ThreeDPrinterMCPServer {
1268
1319
  const nozzleDiam = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
1269
1320
  slicePreset = BAMBU_MODEL_PRESETS[sliceModel]?.(nozzleDiam);
1270
1321
  }
1271
- result = await this.stlManipulator.sliceSTL(String(args.stl_path), slicerType, slicerPath, slicerProfile || undefined, undefined, // progressCallback
1322
+ result = await this.stlManipulator.sliceSTL(String(args.stl_path), slicerType, this.resolveExecutableSelectorArg(args?.slicer_path, "slice_stl", "slicer_path") ?? DEFAULT_SLICER_PATH, slicerProfile || undefined, undefined, // progressCallback
1272
1323
  slicePreset, filamentProfile || undefined);
1273
1324
  break;
1274
1325
  }
@@ -1300,7 +1351,7 @@ class ThreeDPrinterMCPServer {
1300
1351
  processPreset = BAMBU_MODEL_PRESETS[processModel]?.(processNozzle);
1301
1352
  }
1302
1353
  }
1303
- const gcodePath = await this.stlManipulator.sliceSTL(extendedStlPath, slicerType, slicerPath, slicerProfile || undefined, processProgressCallback, processPreset, filamentProfile || undefined);
1354
+ const gcodePath = await this.stlManipulator.sliceSTL(extendedStlPath, slicerType, this.resolveExecutableSelectorArg(args?.slicer_path, "process_and_print_stl", "slicer_path") ?? DEFAULT_SLICER_PATH, slicerProfile || undefined, processProgressCallback, processPreset, filamentProfile || undefined);
1304
1355
  if (type.toLowerCase() === 'bambu' && gcodePath.toLowerCase().endsWith(".3mf")) {
1305
1356
  if (!bambuSerial || !bambuToken) {
1306
1357
  throw new Error("Bambu serial number and access token are required for Bambu 3MF printing.");
@@ -1507,21 +1558,29 @@ class ThreeDPrinterMCPServer {
1507
1558
  const printNozzle = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
1508
1559
  const printPreset = BAMBU_MODEL_PRESETS[printModel]?.(printNozzle);
1509
1560
  let threeMFPath = String(args.three_mf_path);
1510
- // Auto-slice if the 3MF doesn't contain gcode
1561
+ let shouldAutoSlice = false;
1511
1562
  try {
1512
1563
  const JSZip = (await import('jszip')).default;
1513
1564
  const zipData = fs.readFileSync(threeMFPath);
1514
1565
  const zip = await JSZip.loadAsync(zipData);
1515
- const hasGcode = Object.keys(zip.files).some(f => f.match(/Metadata\/plate_\d+\.gcode/i) || f.endsWith('.gcode'));
1516
- if (!hasGcode) {
1566
+ shouldAutoSlice = !Object.keys(zip.files).some(f => f.match(/Metadata\/plate_\d+\.gcode/i) || f.endsWith('.gcode'));
1567
+ }
1568
+ catch (sliceCheckErr) {
1569
+ console.warn("Could not check 3MF for embedded gcode, proceeding with original:", sliceCheckErr.message);
1570
+ }
1571
+ // Only treat slicer_path as an executable selector when a slicer
1572
+ // will actually launch. Ready-to-print archives ignore it.
1573
+ if (shouldAutoSlice) {
1574
+ const printSlicerPath = this.resolveExecutableSelectorArg(args?.slicer_path, "print_3mf", "slicer_path") ?? DEFAULT_SLICER_PATH;
1575
+ try {
1517
1576
  console.log(`3MF has no gcode — auto-slicing with ${slicerType} for ${printModel}`);
1518
- threeMFPath = await this.stlManipulator.sliceSTL(threeMFPath, slicerType, slicerPath, slicerProfile || undefined, undefined, // progressCallback
1577
+ threeMFPath = await this.stlManipulator.sliceSTL(threeMFPath, slicerType, printSlicerPath, slicerProfile || undefined, undefined, // progressCallback
1519
1578
  printPreset, filamentProfile || undefined);
1520
1579
  console.log("Auto-sliced to: " + threeMFPath);
1521
1580
  }
1522
- }
1523
- catch (sliceCheckErr) {
1524
- console.warn("Could not check/slice 3MF, proceeding with original:", sliceCheckErr.message);
1581
+ catch (sliceErr) {
1582
+ console.warn("Could not auto-slice 3MF, proceeding with original:", sliceErr.message);
1583
+ }
1525
1584
  }
1526
1585
  // Define variables needed outside the parse try block
1527
1586
  let implementation;
@@ -1573,13 +1632,14 @@ class ThreeDPrinterMCPServer {
1573
1632
  break;
1574
1633
  }
1575
1634
  case "check_fulu_orca_setup": {
1635
+ const runBridgeProbe = Boolean(args?.run_bridge_probe ?? false);
1576
1636
  result = await inspectFuluOrcaSetup({
1577
- slicerPath: args?.slicer_path !== undefined ? String(args.slicer_path) : undefined,
1578
- pluginDir: args?.plugin_dir !== undefined ? String(args.plugin_dir) : undefined,
1579
- runtimeDir: args?.runtime_dir !== undefined ? String(args.runtime_dir) : undefined,
1637
+ slicerPath: this.resolveExecutableSelectorArg(args?.slicer_path, "check_fulu_orca_setup", "slicer_path", runBridgeProbe, true),
1638
+ pluginDir: this.resolveExecutableSelectorArg(args?.plugin_dir, "check_fulu_orca_setup", "plugin_dir", runBridgeProbe, true),
1639
+ runtimeDir: this.resolveExecutableSelectorArg(args?.runtime_dir, "check_fulu_orca_setup", "runtime_dir", runBridgeProbe, true),
1580
1640
  platform: args?.platform !== undefined ? String(args.platform) : undefined,
1581
- bridgeCommand: args?.bridge_command !== undefined ? String(args.bridge_command) : undefined,
1582
- runBridgeProbe: Boolean(args?.run_bridge_probe ?? false),
1641
+ bridgeCommand: this.resolveExecutableSelectorArg(args?.bridge_command, "check_fulu_orca_setup", "bridge_command", true, true),
1642
+ runBridgeProbe,
1583
1643
  probeTimeoutMs: args?.probe_timeout_ms !== undefined ? Number(args.probe_timeout_ms) : undefined,
1584
1644
  });
1585
1645
  break;
@@ -1597,7 +1657,7 @@ class ThreeDPrinterMCPServer {
1597
1657
  ? args.payload
1598
1658
  : undefined;
1599
1659
  result = await invokeFuluBridgeRpc({
1600
- bridgeCommand: args?.bridge_command !== undefined ? String(args.bridge_command) : undefined,
1660
+ bridgeCommand: this.resolveExecutableSelectorArg(args?.bridge_command, "fulu_bambu_network_rpc", "bridge_command", true, true),
1601
1661
  method,
1602
1662
  payload,
1603
1663
  timeoutMs: args?.timeout_ms !== undefined ? Number(args.timeout_ms) : undefined,
@@ -1625,16 +1685,13 @@ class ThreeDPrinterMCPServer {
1625
1685
  }
1626
1686
  result = await this.stlManipulator.layFlat(String(args.stl_path));
1627
1687
  break;
1688
+ case "blender_mcp_status":
1689
+ result = await this.blender.status(args ?? {}, extra.signal);
1690
+ break;
1691
+ case "blender_mcp_call":
1692
+ return await this.blender.call(args ?? {}, extra.signal);
1628
1693
  case "blender_mcp_edit_model":
1629
- if (!args?.stl_path || !Array.isArray(args.operations)) {
1630
- throw new Error("Missing required parameters: stl_path and operations");
1631
- }
1632
- result = await this.invokeBlenderBridge({
1633
- stlPath: String(args.stl_path),
1634
- operations: args.operations.map((entry) => String(entry)),
1635
- execute: Boolean(args.execute ?? false),
1636
- bridgeCommand: args.bridge_command !== undefined ? String(args.bridge_command) : undefined,
1637
- });
1694
+ result = await this.blender.edit(args ?? {}, this.resolveExecutableSelectorArg(args?.bridge_command, "blender_mcp_edit_model", "bridge_command", true, true) ?? (!process.env.BLENDER_MCP_COMMAND?.trim() ? this.runtimeConfig.blenderBridgeCommand : undefined), extra.signal);
1638
1695
  break;
1639
1696
  default:
1640
1697
  throw new Error(`Unknown tool: ${name}`);
@@ -1692,12 +1749,17 @@ class ThreeDPrinterMCPServer {
1692
1749
  }
1693
1750
  async uploadGcode(host, port, type, apiKey, bambuSerial, bambuToken, filename, gcode, print) {
1694
1751
  const sourceIsFilePath = fs.existsSync(gcode) && fs.statSync(gcode).isFile();
1695
- const tempFilePath = sourceIsFilePath ? gcode : path.join(TEMP_DIR, filename);
1696
- if (!sourceIsFilePath) {
1697
- fs.mkdirSync(TEMP_DIR, { recursive: true });
1698
- fs.writeFileSync(tempFilePath, gcode);
1752
+ // The remote filename must never select a local write/delete target.
1753
+ const localFilename = path.basename(filename);
1754
+ if (!sourceIsFilePath && (!localFilename || localFilename === "." || localFilename === "..")) {
1755
+ throw new Error("filename must include a file basename.");
1699
1756
  }
1757
+ const scratchDir = sourceIsFilePath ? undefined : fs.mkdtempSync(path.join(TEMP_DIR, "upload-"));
1758
+ // Multipart adapters infer their file-part name from this safe basename.
1759
+ const tempFilePath = scratchDir ? path.join(scratchDir, localFilename) : gcode;
1700
1760
  try {
1761
+ if (scratchDir)
1762
+ fs.writeFileSync(tempFilePath, gcode);
1701
1763
  const implementation = this.printerFactory.getImplementation(type);
1702
1764
  if (type.toLowerCase() === "bambu") {
1703
1765
  const bambuApiKey = `${bambuSerial}:${bambuToken}`;
@@ -1706,9 +1768,8 @@ class ThreeDPrinterMCPServer {
1706
1768
  return await implementation.uploadFile(host, port, apiKey, tempFilePath, filename, print);
1707
1769
  }
1708
1770
  finally {
1709
- if (!sourceIsFilePath && fs.existsSync(tempFilePath)) {
1710
- fs.unlinkSync(tempFilePath);
1711
- }
1771
+ if (scratchDir)
1772
+ fs.rmSync(scratchDir, { recursive: true, force: true });
1712
1773
  }
1713
1774
  }
1714
1775
  async startPrint(host, port, type, apiKey, bambuSerial, bambuToken, gcodeFilename) {
@@ -1847,57 +1908,32 @@ class ThreeDPrinterMCPServer {
1847
1908
  this.httpRuntime = { transport, httpServer };
1848
1909
  console.error(`3D Printer MCP server running on streamable-http at http://${this.runtimeConfig.httpHost}:${this.runtimeConfig.httpPort}${this.runtimeConfig.httpPath}`);
1849
1910
  }
1850
- async invokeBlenderBridge(params) {
1851
- const command = params.bridgeCommand ?? this.runtimeConfig.blenderBridgeCommand;
1852
- const payload = {
1853
- modelPath: params.stlPath,
1854
- operations: params.operations,
1855
- source: "mcp-3d-printer-server"
1856
- };
1857
- if (!params.execute) {
1858
- return {
1859
- status: "prepared",
1860
- message: "Prepared Blender MCP payload. Set execute=true to run bridge command.",
1861
- bridgeCommand: command ?? null,
1862
- payload
1863
- };
1911
+ /**
1912
+ * Bridge commands and path-derived bridge executables are machine-local
1913
+ * settings. Accepting their selectors as tool arguments means anything that
1914
+ * can steer the model — a model description, a README in a downloaded
1915
+ * archive, 3MF metadata — can choose the program this server runs. They are
1916
+ * read from environment configuration by default and only accepted for
1917
+ * execution when MCP_ALLOW_EXECUTABLE_ARG is set. The older
1918
+ * MCP_ALLOW_BRIDGE_COMMAND_ARG name remains a compatibility alias only for
1919
+ * bridge commands and path selectors used to derive a bridge executable.
1920
+ */
1921
+ resolveExecutableSelectorArg(rawValue, toolName, argumentName, executionRequested = true, allowLegacyBridgeArg = false) {
1922
+ if (rawValue === undefined) {
1923
+ return undefined;
1864
1924
  }
1865
- if (!command) {
1866
- throw new Error("execute=true requires bridge_command or BLENDER_MCP_BRIDGE_COMMAND env var.");
1925
+ const value = String(rawValue);
1926
+ if (value.length === 0) {
1927
+ return undefined;
1867
1928
  }
1868
- const { spawn } = await import("node:child_process");
1869
- const result = await new Promise((resolve, reject) => {
1870
- const child = spawn(command, {
1871
- shell: true,
1872
- stdio: ["pipe", "pipe", "pipe"]
1873
- });
1874
- let stdout = "";
1875
- let stderr = "";
1876
- child.stdout.setEncoding("utf8");
1877
- child.stderr.setEncoding("utf8");
1878
- child.stdout.on("data", (chunk) => {
1879
- stdout += chunk;
1880
- });
1881
- child.stderr.on("data", (chunk) => {
1882
- stderr += chunk;
1883
- });
1884
- child.on("error", reject);
1885
- child.on("close", (code) => {
1886
- if (code === 0) {
1887
- resolve({ stdout, stderr });
1888
- return;
1889
- }
1890
- reject(new Error(`Blender bridge command exited with code ${code ?? "unknown"}${stderr.trim() ? `: ${stderr.trim()}` : ""}`));
1891
- });
1892
- child.stdin.write(JSON.stringify(payload));
1893
- child.stdin.end();
1894
- });
1895
- return {
1896
- status: "executed",
1897
- bridgeCommand: command,
1898
- stdout: result.stdout?.toString() ?? "",
1899
- stderr: result.stderr?.toString() ?? ""
1900
- };
1929
+ const isAllowed = this.runtimeConfig.allowExecutableArg ||
1930
+ (allowLegacyBridgeArg && this.runtimeConfig.allowBridgeCommandArg);
1931
+ if (executionRequested && !isAllowed) {
1932
+ throw new Error(`${toolName}: the ${argumentName} argument cannot select an executable by default. ` +
1933
+ "Executable paths and commands are read from server environment configuration. Set " +
1934
+ `MCP_ALLOW_EXECUTABLE_ARG=1 to accept ${argumentName} for process execution.`);
1935
+ }
1936
+ return value;
1901
1937
  }
1902
1938
  async close() {
1903
1939
  await this.printerFactory.disconnectAll();
@@ -340,7 +340,13 @@ export class BambuImplementation extends PrinterImplementation {
340
340
  user: "bblp",
341
341
  password: token,
342
342
  secure: "implicit",
343
- secureOptions: { rejectUnauthorized: false },
343
+ // basic-ftp passes getSession() to the data socket. Keep both channels
344
+ // on TLS 1.2, where that session is available at handshake completion;
345
+ // TLS 1.3 tickets arrive later and can break Bambu's required reuse.
346
+ // Node also binds resumable sessions to the host identity. basic-ftp
347
+ // retains these options for its wrapped data socket, so explicitly keep
348
+ // the same host there (nodejs/node#64402 affects implicit FTPS).
349
+ secureOptions: { host, rejectUnauthorized: false, minVersion: "TLSv1.2", maxVersion: "TLSv1.2" },
344
350
  });
345
351
  // Use absolute path to avoid CWD side-effects
346
352
  const absoluteRemote = remotePath.startsWith("/") ? remotePath : `/${remotePath}`;
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ var _a;
3
+ (_a = process.env).MCP_TRANSPORT ?? (_a.MCP_TRANSPORT = "streamable-http");
4
+ if (!process.env.MCP_HTTP_PORT && process.env.SSE_PORT) {
5
+ process.env.MCP_HTTP_PORT = process.env.SSE_PORT;
6
+ }
7
+ void import("./index.js");
8
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-3d-printer-server",
3
- "version": "1.2.4",
3
+ "version": "1.2.9",
4
4
  "description": "MCP server for connecting Claude with 3D printer management systems",
5
5
  "main": "dist/index.js",
6
6
  "repository": "https://github.com/DMontgomery40/mcp-3D-printer-server",
@@ -11,29 +11,38 @@
11
11
  "start:http": "MCP_TRANSPORT=streamable-http node dist/index.js",
12
12
  "dev": "nodemon src/index.ts --exec \"npm run build && npm start\"",
13
13
  "build": "tsc",
14
- "test": "npm run build && node dist/test/stl-manipulator-test.js && node --test tests/**/*.test.mjs",
15
- "inspect": "npm run build && npx @modelcontextprotocol/inspector dist/index.js"
14
+ "test": "npm run build && node dist/test/stl-manipulator-test.js && node --test tests/*.test.mjs",
15
+ "inspect": "npm run build && npx @modelcontextprotocol/inspector dist/index.js",
16
+ "prepack": "npm run build",
17
+ "test:package": "node scripts/test-package.mjs"
16
18
  },
17
19
  "bin": {
18
- "mcp-3d-printer-server": "./dist/index.js"
20
+ "mcp-3d-printer-server": "./dist/index.js",
21
+ "mcp-3d-printer-server-http": "./dist/http-server.js",
22
+ "mcp-3d-printer-server-sse": "./dist/sse-server.js"
19
23
  },
20
24
  "files": [
21
25
  "dist/3mf_parser.d.ts",
22
26
  "dist/3mf_parser.js",
23
27
  "dist/fulu-orca.d.ts",
24
28
  "dist/fulu-orca.js",
29
+ "dist/http-server.d.ts",
30
+ "dist/http-server.js",
25
31
  "dist/index.d.ts",
26
32
  "dist/index.js",
27
- "dist/index.js.map",
28
33
  "dist/printers",
34
+ "dist/sse-server.d.ts",
35
+ "dist/sse-server.js",
29
36
  "dist/stl",
30
37
  "dist/test",
31
38
  "dist/types.d.ts",
32
39
  "dist/types.js",
33
40
  "src/3mf_parser.ts",
34
41
  "src/fulu-orca.ts",
42
+ "src/http-server.ts",
35
43
  "src/index.ts",
36
44
  "src/printers",
45
+ "src/sse-server.ts",
37
46
  "src/stl",
38
47
  "src/test/3DBenchy.3mf",
39
48
  "src/test/stl-manipulator-test.js",
@@ -41,7 +50,11 @@
41
50
  "src/test/tsconfig.json",
42
51
  "src/types.ts",
43
52
  "README.md",
44
- "LICENSE"
53
+ "LICENSE",
54
+ "dist/blender-mcp-bridge.js",
55
+ "dist/blender-mcp-bridge.d.ts",
56
+ "src/blender-mcp-bridge.ts",
57
+ "CHANGELOG.md"
45
58
  ],
46
59
  "keywords": [
47
60
  "mcp",
@@ -59,12 +72,12 @@
59
72
  "license": "GPL-2.0",
60
73
  "dependencies": {
61
74
  "@modelcontextprotocol/sdk": "^1.27.0",
62
- "axios": "^1.6.2",
75
+ "axios": "^1.20.0",
63
76
  "bambu-js": "^2.2.2",
64
- "bambu-node": "^3.22.21",
65
- "basic-ftp": "^5.2.0",
77
+ "bambu-node": "3.22.21",
78
+ "basic-ftp": "^5.3.1",
66
79
  "dotenv": "^16.3.1",
67
- "form-data": "^4.0.0",
80
+ "form-data": "^4.0.6",
68
81
  "jszip": "^3.10.1",
69
82
  "three": "^0.173.0",
70
83
  "xml2js": "^0.6.2",
@@ -75,6 +88,10 @@
75
88
  "@types/node": "^18.0.0",
76
89
  "@types/three": "^0.173.0",
77
90
  "@types/xml2js": "^0.4.14",
78
- "nodemon": "^2.0.22"
91
+ "nodemon": "^3.1.14",
92
+ "typescript": "~5.9.3"
93
+ },
94
+ "engines": {
95
+ "node": ">=18"
79
96
  }
80
97
  }