mcp-3d-printer-server 1.2.5 → 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/index.js +148 -112
- package/dist/printers/bambu.js +7 -1
- package/package.json +19 -10
- package/src/blender-mcp-bridge.ts +430 -0
- package/src/index.ts +215 -131
- package/src/printers/bambu.ts +7 -1
- package/dist/index.js.map +0 -1
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(
|
|
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: "
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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: "
|
|
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
|
|
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
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1839
|
+
shouldAutoSlice = !Object.keys(zip.files).some(
|
|
1774
1840
|
f => f.match(/Metadata\/plate_\d+\.gcode/i) || f.endsWith('.gcode')
|
|
1775
1841
|
);
|
|
1776
|
-
|
|
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
|
-
|
|
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:
|
|
1854
|
-
|
|
1855
|
-
|
|
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:
|
|
1858
|
-
|
|
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
|
|
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
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
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
|
-
|
|
2032
|
-
|
|
2033
|
-
if (!sourceIsFilePath) {
|
|
2034
|
-
|
|
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 (
|
|
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
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
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
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
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
|
-
|
|
2391
|
+
const isAllowed =
|
|
2392
|
+
this.runtimeConfig.allowExecutableArg ||
|
|
2393
|
+
(allowLegacyBridgeArg && this.runtimeConfig.allowBridgeCommandArg);
|
|
2394
|
+
if (executionRequested && !isAllowed) {
|
|
2270
2395
|
throw new Error(
|
|
2271
|
-
|
|
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
|
-
|
|
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> {
|
package/src/printers/bambu.ts
CHANGED
|
@@ -473,7 +473,13 @@ export class BambuImplementation extends PrinterImplementation {
|
|
|
473
473
|
user: "bblp",
|
|
474
474
|
password: token,
|
|
475
475
|
secure: "implicit",
|
|
476
|
-
|
|
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}`;
|