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/CHANGELOG.md +21 -0
- package/README.md +81 -8
- package/dist/blender-mcp-bridge.d.ts +10 -0
- package/dist/blender-mcp-bridge.js +473 -0
- package/dist/http-server.d.ts +2 -0
- package/dist/http-server.js +8 -0
- package/dist/index.js +148 -112
- package/dist/printers/bambu.js +7 -1
- package/dist/sse-server.d.ts +2 -0
- package/dist/sse-server.js +8 -0
- package/package.json +28 -11
- package/src/blender-mcp-bridge.ts +430 -0
- package/src/http-server.ts +9 -0
- package/src/index.ts +215 -131
- package/src/printers/bambu.ts +7 -1
- package/src/sse-server.ts +9 -0
- package/dist/index.js.map +0 -1
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(
|
|
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: "
|
|
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
|
|
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
|
|
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
|
|
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: "
|
|
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
|
|
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
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
1516
|
-
|
|
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,
|
|
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
|
-
|
|
1524
|
-
|
|
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
|
|
1578
|
-
pluginDir: args?.plugin_dir
|
|
1579
|
-
runtimeDir: args?.runtime_dir
|
|
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
|
|
1582
|
-
runBridgeProbe
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
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 (
|
|
1710
|
-
fs.
|
|
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
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
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
|
-
|
|
1866
|
-
|
|
1925
|
+
const value = String(rawValue);
|
|
1926
|
+
if (value.length === 0) {
|
|
1927
|
+
return undefined;
|
|
1867
1928
|
}
|
|
1868
|
-
const
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
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();
|
package/dist/printers/bambu.js
CHANGED
|
@@ -340,7 +340,13 @@ export class BambuImplementation extends PrinterImplementation {
|
|
|
340
340
|
user: "bblp",
|
|
341
341
|
password: token,
|
|
342
342
|
secure: "implicit",
|
|
343
|
-
|
|
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}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-3d-printer-server",
|
|
3
|
-
"version": "1.2.
|
|
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
|
|
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.
|
|
75
|
+
"axios": "^1.20.0",
|
|
63
76
|
"bambu-js": "^2.2.2",
|
|
64
|
-
"bambu-node": "
|
|
65
|
-
"basic-ftp": "^5.
|
|
77
|
+
"bambu-node": "3.22.21",
|
|
78
|
+
"basic-ftp": "^5.3.1",
|
|
66
79
|
"dotenv": "^16.3.1",
|
|
67
|
-
"form-data": "^4.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": "^
|
|
91
|
+
"nodemon": "^3.1.14",
|
|
92
|
+
"typescript": "~5.9.3"
|
|
93
|
+
},
|
|
94
|
+
"engines": {
|
|
95
|
+
"node": ">=18"
|
|
79
96
|
}
|
|
80
97
|
}
|