mcp-3d-printer-server 1.0.2 → 1.2.1
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/README.md +105 -29
- package/dist/http-server.d.ts +16 -0
- package/dist/http-server.js +268 -0
- package/dist/index.js +462 -37
- package/dist/printers/bambu.d.ts +23 -12
- package/dist/printers/bambu.js +319 -232
- package/dist/printers/printer-factory.d.ts +0 -1
- package/dist/printers/printer-factory.js +6 -4
- package/dist/printers/prusa.d.ts +5 -0
- package/dist/printers/prusa.js +160 -59
- package/dist/sse-server.d.ts +20 -0
- package/dist/sse-server.js +318 -0
- package/dist/stl/stl-manipulator.d.ts +5 -5
- package/dist/stl/stl-manipulator.js +32 -7
- package/dist/types.d.ts +0 -6
- package/dist/types.js +0 -20
- package/package.json +10 -4
- package/src/http-server.ts +328 -0
- package/src/index.ts +582 -50
- package/src/printers/bambu.ts +452 -299
- package/src/printers/printer-factory.ts +7 -4
- package/src/printers/prusa.ts +218 -73
- package/src/sse-server.ts +409 -0
- package/src/stl/stl-manipulator.ts +38 -8
- package/src/test/Tape+Card+55mm+Ring.stl +0 -0
- package/src/types.ts +0 -19
package/dist/index.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
4
5
|
import { ListResourcesRequestSchema, ReadResourceRequestSchema, ListToolsRequestSchema, CallToolRequestSchema, ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
|
|
5
6
|
import axios from "axios";
|
|
6
7
|
import dotenv from "dotenv";
|
|
7
8
|
import fs from "fs";
|
|
8
9
|
import path from "path";
|
|
10
|
+
import { createServer as createHttpServer } from "node:http";
|
|
11
|
+
import { randomUUID } from "node:crypto";
|
|
9
12
|
import * as THREE from 'three';
|
|
10
13
|
import { PrinterFactory } from "./printers/printer-factory.js";
|
|
11
14
|
import { STLManipulator } from "./stl/stl-manipulator.js";
|
|
@@ -20,21 +23,108 @@ const DEFAULT_API_KEY = process.env.API_KEY || "";
|
|
|
20
23
|
const DEFAULT_TYPE = process.env.PRINTER_TYPE || "octoprint"; // Default to OctoPrint
|
|
21
24
|
const TEMP_DIR = process.env.TEMP_DIR || path.join(process.cwd(), "temp");
|
|
22
25
|
// Slicer configuration
|
|
23
|
-
const DEFAULT_SLICER_TYPE = process.env.SLICER_TYPE || "prusaslicer";
|
|
24
|
-
const DEFAULT_SLICER_PATH = process.env.SLICER_PATH || "";
|
|
26
|
+
const DEFAULT_SLICER_TYPE = process.env.SLICER_TYPE || (process.env.PRINTER_TYPE === "bambu" ? "bambustudio" : "prusaslicer");
|
|
27
|
+
const DEFAULT_SLICER_PATH = process.env.SLICER_PATH || (process.env.PRINTER_TYPE === "bambu" ? "/Applications/BambuStudio.app/Contents/MacOS/BambuStudio" : "");
|
|
25
28
|
const DEFAULT_SLICER_PROFILE = process.env.SLICER_PROFILE || "";
|
|
26
29
|
// Bambu-specific default values
|
|
27
30
|
const DEFAULT_BAMBU_SERIAL = process.env.BAMBU_SERIAL || "";
|
|
28
31
|
const DEFAULT_BAMBU_TOKEN = process.env.BAMBU_TOKEN || "";
|
|
32
|
+
// Printer model and bed type (Bambu safety)
|
|
33
|
+
const DEFAULT_BAMBU_MODEL = process.env.BAMBU_MODEL?.trim().toLowerCase() || "";
|
|
34
|
+
const DEFAULT_BED_TYPE = process.env.BED_TYPE?.trim().toLowerCase() || "textured_plate";
|
|
35
|
+
const DEFAULT_NOZZLE_DIAMETER = process.env.NOZZLE_DIAMETER?.trim() || "0.4";
|
|
36
|
+
const VALID_BAMBU_MODELS = ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"];
|
|
37
|
+
const VALID_BED_TYPES = ["textured_plate", "cool_plate", "engineering_plate", "hot_plate"];
|
|
38
|
+
// Map model IDs to BambuStudio --load-machine preset names
|
|
39
|
+
const BAMBU_MODEL_PRESETS = {
|
|
40
|
+
p1s: (n) => `Bambu Lab P1S ${n} nozzle`,
|
|
41
|
+
p1p: (n) => `Bambu Lab P1P ${n} nozzle`,
|
|
42
|
+
x1c: (n) => `Bambu Lab X1 Carbon ${n} nozzle`,
|
|
43
|
+
x1e: (n) => `Bambu Lab X1E ${n} nozzle`,
|
|
44
|
+
a1: (n) => `Bambu Lab A1 ${n} nozzle`,
|
|
45
|
+
a1mini: (n) => `Bambu Lab A1 mini ${n} nozzle`,
|
|
46
|
+
h2d: (n) => `Bambu Lab H2D ${n} nozzle`,
|
|
47
|
+
};
|
|
48
|
+
function validateBambuModel(model) {
|
|
49
|
+
const normalized = model.trim().toLowerCase();
|
|
50
|
+
if (!VALID_BAMBU_MODELS.includes(normalized)) {
|
|
51
|
+
throw new Error(`Invalid Bambu printer model "${model}". Valid models: ${VALID_BAMBU_MODELS.join(", ")}`);
|
|
52
|
+
}
|
|
53
|
+
return normalized;
|
|
54
|
+
}
|
|
55
|
+
function resolveBedType(bedType) {
|
|
56
|
+
const resolved = (bedType || DEFAULT_BED_TYPE).trim().toLowerCase();
|
|
57
|
+
if (!VALID_BED_TYPES.includes(resolved)) {
|
|
58
|
+
throw new Error(`Invalid bed type "${bedType}". Valid types: ${VALID_BED_TYPES.join(", ")}`);
|
|
59
|
+
}
|
|
60
|
+
return resolved;
|
|
61
|
+
}
|
|
62
|
+
function parseBooleanEnv(rawValue, fallback) {
|
|
63
|
+
if (rawValue === undefined) {
|
|
64
|
+
return fallback;
|
|
65
|
+
}
|
|
66
|
+
const value = rawValue.trim().toLowerCase();
|
|
67
|
+
if (["1", "true", "yes", "on"].includes(value)) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
if (["0", "false", "no", "off"].includes(value)) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
return fallback;
|
|
74
|
+
}
|
|
75
|
+
function parsePort(value, fallback) {
|
|
76
|
+
if (!value) {
|
|
77
|
+
return fallback;
|
|
78
|
+
}
|
|
79
|
+
const parsed = Number(value);
|
|
80
|
+
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65535) {
|
|
81
|
+
throw new Error(`Invalid MCP_HTTP_PORT value: ${value}`);
|
|
82
|
+
}
|
|
83
|
+
return parsed;
|
|
84
|
+
}
|
|
85
|
+
function normalizePath(pathValue) {
|
|
86
|
+
const value = (pathValue ?? "/mcp").trim();
|
|
87
|
+
if (!value) {
|
|
88
|
+
return "/mcp";
|
|
89
|
+
}
|
|
90
|
+
return value.startsWith("/") ? value : `/${value}`;
|
|
91
|
+
}
|
|
92
|
+
function parseCsvEnv(value) {
|
|
93
|
+
if (!value) {
|
|
94
|
+
return new Set();
|
|
95
|
+
}
|
|
96
|
+
const entries = value
|
|
97
|
+
.split(",")
|
|
98
|
+
.map((entry) => entry.trim())
|
|
99
|
+
.filter((entry) => entry.length > 0);
|
|
100
|
+
return new Set(entries);
|
|
101
|
+
}
|
|
102
|
+
function readRuntimeConfig() {
|
|
103
|
+
const rawTransport = process.env.MCP_TRANSPORT?.trim().toLowerCase();
|
|
104
|
+
const transport = rawTransport === "streamable-http" || rawTransport === "http"
|
|
105
|
+
? "streamable-http"
|
|
106
|
+
: "stdio";
|
|
107
|
+
return {
|
|
108
|
+
transport,
|
|
109
|
+
httpHost: process.env.MCP_HTTP_HOST?.trim() || "127.0.0.1",
|
|
110
|
+
httpPort: parsePort(process.env.MCP_HTTP_PORT, 3000),
|
|
111
|
+
httpPath: normalizePath(process.env.MCP_HTTP_PATH),
|
|
112
|
+
statefulSession: parseBooleanEnv(process.env.MCP_HTTP_STATEFUL, true),
|
|
113
|
+
enableJsonResponse: parseBooleanEnv(process.env.MCP_HTTP_JSON_RESPONSE, true),
|
|
114
|
+
allowedOrigins: parseCsvEnv(process.env.MCP_HTTP_ALLOWED_ORIGINS),
|
|
115
|
+
blenderBridgeCommand: process.env.BLENDER_MCP_BRIDGE_COMMAND?.trim() || undefined,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
29
118
|
// Ensure temp directory exists
|
|
30
119
|
if (!fs.existsSync(TEMP_DIR)) {
|
|
31
120
|
fs.mkdirSync(TEMP_DIR, { recursive: true });
|
|
32
121
|
}
|
|
33
122
|
class ThreeDPrinterMCPServer {
|
|
34
123
|
constructor() {
|
|
124
|
+
this.runtimeConfig = readRuntimeConfig();
|
|
35
125
|
this.server = new Server({
|
|
36
126
|
name: "mcp-3d-printer-server",
|
|
37
|
-
version: "1.
|
|
127
|
+
version: "1.2.0"
|
|
38
128
|
}, {
|
|
39
129
|
capabilities: {
|
|
40
130
|
resources: {},
|
|
@@ -46,16 +136,58 @@ class ThreeDPrinterMCPServer {
|
|
|
46
136
|
this.setupHandlers();
|
|
47
137
|
this.setupErrorHandling();
|
|
48
138
|
}
|
|
139
|
+
async resolveBambuModel(argsModel) {
|
|
140
|
+
const fromArgs = (argsModel || DEFAULT_BAMBU_MODEL).trim().toLowerCase();
|
|
141
|
+
if (fromArgs) {
|
|
142
|
+
return validateBambuModel(fromArgs);
|
|
143
|
+
}
|
|
144
|
+
// No model from args or env — ask the user via elicitation
|
|
145
|
+
try {
|
|
146
|
+
const result = await this.server.elicitInput({
|
|
147
|
+
mode: "form",
|
|
148
|
+
message: "Your Bambu Lab printer model is required for safe operation. " +
|
|
149
|
+
"Using the wrong model can cause the bed to crash into the nozzle and damage the printer.",
|
|
150
|
+
requestedSchema: {
|
|
151
|
+
type: "object",
|
|
152
|
+
properties: {
|
|
153
|
+
bambu_model: {
|
|
154
|
+
type: "string",
|
|
155
|
+
title: "Printer Model",
|
|
156
|
+
description: "Which Bambu Lab printer do you have?",
|
|
157
|
+
oneOf: [
|
|
158
|
+
{ const: "p1s", title: "P1S" },
|
|
159
|
+
{ const: "p1p", title: "P1P" },
|
|
160
|
+
{ const: "x1c", title: "X1 Carbon" },
|
|
161
|
+
{ const: "x1e", title: "X1E" },
|
|
162
|
+
{ const: "a1", title: "A1" },
|
|
163
|
+
{ const: "a1mini", title: "A1 Mini" },
|
|
164
|
+
{ const: "h2d", title: "H2D" },
|
|
165
|
+
],
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
required: ["bambu_model"],
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
if (result.action === "accept" && result.content?.bambu_model) {
|
|
172
|
+
return validateBambuModel(String(result.content.bambu_model));
|
|
173
|
+
}
|
|
174
|
+
throw new Error("Printer model selection was cancelled. Cannot proceed without knowing the printer model.");
|
|
175
|
+
}
|
|
176
|
+
catch (elicitError) {
|
|
177
|
+
const msg = elicitError?.message || String(elicitError);
|
|
178
|
+
if (elicitError?.code === -32601 || elicitError?.code === -32600 ||
|
|
179
|
+
msg.includes("does not support") || msg.includes("elicitation")) {
|
|
180
|
+
throw new Error("bambu_model is required but your MCP client does not support elicitation. " +
|
|
181
|
+
`Set the BAMBU_MODEL environment variable or pass bambu_model in the tool call. ` +
|
|
182
|
+
`Valid models: ${VALID_BAMBU_MODELS.join(", ")}`);
|
|
183
|
+
}
|
|
184
|
+
throw elicitError;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
49
187
|
setupErrorHandling() {
|
|
50
188
|
this.server.onerror = (error) => {
|
|
51
189
|
console.error("[MCP Error]", error);
|
|
52
190
|
};
|
|
53
|
-
process.on("SIGINT", async () => {
|
|
54
|
-
// Disconnect all printers
|
|
55
|
-
await this.printerFactory.disconnectAll();
|
|
56
|
-
await this.server.close();
|
|
57
|
-
process.exit(0);
|
|
58
|
-
});
|
|
59
191
|
}
|
|
60
192
|
setupHandlers() {
|
|
61
193
|
this.setupResourceHandlers();
|
|
@@ -198,17 +330,26 @@ class ThreeDPrinterMCPServer {
|
|
|
198
330
|
},
|
|
199
331
|
{
|
|
200
332
|
name: "slice_stl",
|
|
201
|
-
description: "Slice an STL file to generate G-code",
|
|
333
|
+
description: "Slice an STL or 3MF file to generate G-code or sliced 3MF",
|
|
202
334
|
inputSchema: {
|
|
203
335
|
type: "object",
|
|
204
336
|
properties: {
|
|
205
337
|
stl_path: {
|
|
206
338
|
type: "string",
|
|
207
|
-
description: "Path to the STL file to slice"
|
|
339
|
+
description: "Path to the STL or 3MF file to slice"
|
|
340
|
+
},
|
|
341
|
+
bambu_model: {
|
|
342
|
+
type: "string",
|
|
343
|
+
enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
|
|
344
|
+
description: "Bambu Lab printer model — required when using bambustudio slicer to ensure correct G-code."
|
|
345
|
+
},
|
|
346
|
+
nozzle_diameter: {
|
|
347
|
+
type: "string",
|
|
348
|
+
description: "Nozzle diameter in mm (default: 0.4)."
|
|
208
349
|
},
|
|
209
350
|
slicer_type: {
|
|
210
351
|
type: "string",
|
|
211
|
-
description: "Type of slicer to use (prusaslicer, cura, slic3r, orcaslicer) (default: value from env)"
|
|
352
|
+
description: "Type of slicer to use (prusaslicer, cura, slic3r, orcaslicer, bambustudio) (default: value from env)"
|
|
212
353
|
},
|
|
213
354
|
slicer_path: {
|
|
214
355
|
type: "string",
|
|
@@ -475,6 +616,20 @@ class ThreeDPrinterMCPServer {
|
|
|
475
616
|
type: "string",
|
|
476
617
|
description: "Path to the 3MF file to print."
|
|
477
618
|
},
|
|
619
|
+
bambu_model: {
|
|
620
|
+
type: "string",
|
|
621
|
+
enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
|
|
622
|
+
description: "REQUIRED: Bambu Lab printer model. Ensures correct G-code generation — wrong model can crash the bed into the nozzle."
|
|
623
|
+
},
|
|
624
|
+
bed_type: {
|
|
625
|
+
type: "string",
|
|
626
|
+
enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate"],
|
|
627
|
+
description: "Bed/plate type installed on the printer (default: textured_plate)."
|
|
628
|
+
},
|
|
629
|
+
nozzle_diameter: {
|
|
630
|
+
type: "string",
|
|
631
|
+
description: "Nozzle diameter in mm (default: 0.4)."
|
|
632
|
+
},
|
|
478
633
|
host: {
|
|
479
634
|
type: "string",
|
|
480
635
|
description: "Hostname or IP address of the Bambu printer (default: value from env)"
|
|
@@ -497,7 +652,7 @@ class ThreeDPrinterMCPServer {
|
|
|
497
652
|
additionalProperties: { type: "number" }
|
|
498
653
|
}
|
|
499
654
|
},
|
|
500
|
-
required: ["three_mf_path"]
|
|
655
|
+
required: ["three_mf_path", "bambu_model"]
|
|
501
656
|
}
|
|
502
657
|
},
|
|
503
658
|
{
|
|
@@ -545,6 +700,33 @@ class ThreeDPrinterMCPServer {
|
|
|
545
700
|
},
|
|
546
701
|
required: ["stl_path"]
|
|
547
702
|
}
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
name: "blender_mcp_edit_model",
|
|
706
|
+
description: "Optionally send STL-edit instructions to a Blender MCP bridge command. Use for advanced model edits outside built-in STL tools.",
|
|
707
|
+
inputSchema: {
|
|
708
|
+
type: "object",
|
|
709
|
+
properties: {
|
|
710
|
+
stl_path: {
|
|
711
|
+
type: "string",
|
|
712
|
+
description: "Path to the local STL file."
|
|
713
|
+
},
|
|
714
|
+
operations: {
|
|
715
|
+
type: "array",
|
|
716
|
+
description: "Ordered edit operations for Blender (e.g. remesh, boolean, decimate).",
|
|
717
|
+
items: { type: "string" }
|
|
718
|
+
},
|
|
719
|
+
bridge_command: {
|
|
720
|
+
type: "string",
|
|
721
|
+
description: "Optional override command for invoking a local Blender MCP bridge."
|
|
722
|
+
},
|
|
723
|
+
execute: {
|
|
724
|
+
type: "boolean",
|
|
725
|
+
description: "When true, attempts to execute bridge command; otherwise returns a prepared payload only."
|
|
726
|
+
}
|
|
727
|
+
},
|
|
728
|
+
required: ["stl_path", "operations"]
|
|
729
|
+
}
|
|
548
730
|
}
|
|
549
731
|
]
|
|
550
732
|
};
|
|
@@ -599,12 +781,21 @@ class ThreeDPrinterMCPServer {
|
|
|
599
781
|
}
|
|
600
782
|
result = await this.stlManipulator.extendBase(String(args.stl_path), Number(args.extension_inches));
|
|
601
783
|
break;
|
|
602
|
-
case "slice_stl":
|
|
784
|
+
case "slice_stl": {
|
|
603
785
|
if (!args?.stl_path) {
|
|
604
786
|
throw new Error("Missing required parameter: stl_path");
|
|
605
787
|
}
|
|
606
|
-
|
|
788
|
+
// Resolve printer model for BambuStudio slicer
|
|
789
|
+
let slicePreset;
|
|
790
|
+
if (slicerType === 'bambustudio') {
|
|
791
|
+
const sliceModel = await this.resolveBambuModel(args?.bambu_model);
|
|
792
|
+
const nozzleDiam = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
793
|
+
slicePreset = BAMBU_MODEL_PRESETS[sliceModel]?.(nozzleDiam);
|
|
794
|
+
}
|
|
795
|
+
result = await this.stlManipulator.sliceSTL(String(args.stl_path), slicerType, slicerPath, slicerProfile || undefined, undefined, // progressCallback
|
|
796
|
+
slicePreset);
|
|
607
797
|
break;
|
|
798
|
+
}
|
|
608
799
|
case "confirm_temperatures":
|
|
609
800
|
if (!args?.gcode_path) {
|
|
610
801
|
throw new Error("Missing required parameter: gcode_path");
|
|
@@ -625,7 +816,13 @@ class ThreeDPrinterMCPServer {
|
|
|
625
816
|
// 1. Extend the base of the STL file
|
|
626
817
|
const extendedStlPath = await this.stlManipulator.extendBase(String(args.stl_path), Number(args.extension_inches), processProgressCallback);
|
|
627
818
|
// 2. Slice the extended STL file
|
|
628
|
-
|
|
819
|
+
let processPreset;
|
|
820
|
+
if (slicerType === 'bambustudio') {
|
|
821
|
+
const processModel = await this.resolveBambuModel(args?.bambu_model);
|
|
822
|
+
const processNozzle = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
823
|
+
processPreset = BAMBU_MODEL_PRESETS[processModel]?.(processNozzle);
|
|
824
|
+
}
|
|
825
|
+
const gcodePath = await this.stlManipulator.sliceSTL(extendedStlPath, slicerType, slicerPath, slicerProfile || undefined, processProgressCallback, processPreset);
|
|
629
826
|
// 3. Confirm temperatures if specified
|
|
630
827
|
if (args.extruder_temp !== undefined || args.bed_temp !== undefined) {
|
|
631
828
|
const tempConfirmation = await this.stlManipulator.confirmTemperatures(gcodePath, {
|
|
@@ -781,7 +978,7 @@ class ThreeDPrinterMCPServer {
|
|
|
781
978
|
const height = args.height !== undefined ? Number(args.height) : 300;
|
|
782
979
|
result = await this.stlManipulator.generateVisualization(String(args.stl_path), width, height, visualizationProgressCallback);
|
|
783
980
|
break;
|
|
784
|
-
case "print_3mf":
|
|
981
|
+
case "print_3mf": {
|
|
785
982
|
if (!args?.three_mf_path) {
|
|
786
983
|
throw new Error("Missing required parameter: three_mf_path");
|
|
787
984
|
}
|
|
@@ -791,7 +988,27 @@ class ThreeDPrinterMCPServer {
|
|
|
791
988
|
if (!bambuSerial || !bambuToken) {
|
|
792
989
|
throw new Error("Bambu serial number and access token are required for print_3mf.");
|
|
793
990
|
}
|
|
794
|
-
const
|
|
991
|
+
const printModel = await this.resolveBambuModel(args?.bambu_model);
|
|
992
|
+
const printBedType = resolveBedType(args?.bed_type);
|
|
993
|
+
const printNozzle = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
994
|
+
const printPreset = BAMBU_MODEL_PRESETS[printModel]?.(printNozzle);
|
|
995
|
+
let threeMFPath = String(args.three_mf_path);
|
|
996
|
+
// Auto-slice if the 3MF doesn't contain gcode
|
|
997
|
+
try {
|
|
998
|
+
const JSZip = (await import('jszip')).default;
|
|
999
|
+
const zipData = fs.readFileSync(threeMFPath);
|
|
1000
|
+
const zip = await JSZip.loadAsync(zipData);
|
|
1001
|
+
const hasGcode = Object.keys(zip.files).some(f => f.match(/Metadata\/plate_\d+\.gcode/i) || f.endsWith('.gcode'));
|
|
1002
|
+
if (!hasGcode) {
|
|
1003
|
+
console.log(`3MF has no gcode — auto-slicing with ${slicerType} for ${printModel}`);
|
|
1004
|
+
threeMFPath = await this.stlManipulator.sliceSTL(threeMFPath, slicerType, slicerPath, slicerProfile || undefined, undefined, // progressCallback
|
|
1005
|
+
printPreset);
|
|
1006
|
+
console.log("Auto-sliced to: " + threeMFPath);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
catch (sliceCheckErr) {
|
|
1010
|
+
console.warn("Could not check/slice 3MF, proceeding with original:", sliceCheckErr.message);
|
|
1011
|
+
}
|
|
795
1012
|
// Define variables needed outside the parse try block
|
|
796
1013
|
let implementation;
|
|
797
1014
|
let threeMfFilename;
|
|
@@ -862,12 +1079,13 @@ class ThreeDPrinterMCPServer {
|
|
|
862
1079
|
printOptions = {
|
|
863
1080
|
useAMS: useAMS,
|
|
864
1081
|
amsMapping: finalAmsMapping,
|
|
1082
|
+
bedType: printBedType,
|
|
865
1083
|
bedLeveling: args?.bed_leveling !== undefined ? Boolean(args.bed_leveling) : undefined,
|
|
866
1084
|
flowCalibration: args?.flow_calibration !== undefined ? Boolean(args.flow_calibration) : undefined,
|
|
867
1085
|
vibrationCalibration: args?.vibration_calibration !== undefined ? Boolean(args.vibration_calibration) : undefined,
|
|
868
1086
|
layerInspect: args?.layer_inspect !== undefined ? Boolean(args.layer_inspect) : undefined,
|
|
869
1087
|
timelapse: args?.timelapse !== undefined ? Boolean(args.timelapse) : undefined,
|
|
870
|
-
// md5: parsed3MFData?.metadata?.md5
|
|
1088
|
+
// md5: parsed3MFData?.metadata?.md5
|
|
871
1089
|
};
|
|
872
1090
|
}
|
|
873
1091
|
catch (error) { // Catch parsing or setup errors
|
|
@@ -889,6 +1107,7 @@ class ThreeDPrinterMCPServer {
|
|
|
889
1107
|
throw new Error(`Failed to start print: ${printError.message}`);
|
|
890
1108
|
}
|
|
891
1109
|
break;
|
|
1110
|
+
}
|
|
892
1111
|
case "merge_vertices":
|
|
893
1112
|
if (!args?.stl_path) {
|
|
894
1113
|
throw new Error("Missing required parameter: stl_path");
|
|
@@ -908,6 +1127,17 @@ class ThreeDPrinterMCPServer {
|
|
|
908
1127
|
}
|
|
909
1128
|
result = await this.stlManipulator.layFlat(String(args.stl_path));
|
|
910
1129
|
break;
|
|
1130
|
+
case "blender_mcp_edit_model":
|
|
1131
|
+
if (!args?.stl_path || !Array.isArray(args.operations)) {
|
|
1132
|
+
throw new Error("Missing required parameters: stl_path and operations");
|
|
1133
|
+
}
|
|
1134
|
+
result = await this.invokeBlenderBridge({
|
|
1135
|
+
stlPath: String(args.stl_path),
|
|
1136
|
+
operations: args.operations.map((entry) => String(entry)),
|
|
1137
|
+
execute: Boolean(args.execute ?? false),
|
|
1138
|
+
bridgeCommand: args.bridge_command !== undefined ? String(args.bridge_command) : undefined,
|
|
1139
|
+
});
|
|
1140
|
+
break;
|
|
911
1141
|
default:
|
|
912
1142
|
throw new Error(`Unknown tool: ${name}`);
|
|
913
1143
|
}
|
|
@@ -923,13 +1153,15 @@ class ThreeDPrinterMCPServer {
|
|
|
923
1153
|
catch (error) {
|
|
924
1154
|
console.error(`Error calling tool ${name}:`, error);
|
|
925
1155
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1156
|
+
const structuredError = this.toStructuredToolError(name, errorMessage);
|
|
926
1157
|
return {
|
|
927
1158
|
content: [
|
|
928
1159
|
{
|
|
929
1160
|
type: "text",
|
|
930
|
-
text: `Error: ${errorMessage}`
|
|
1161
|
+
text: `Error: ${errorMessage}\nSuggestion: ${structuredError.suggestion}`
|
|
931
1162
|
}
|
|
932
1163
|
],
|
|
1164
|
+
structuredContent: structuredError,
|
|
933
1165
|
isError: true
|
|
934
1166
|
};
|
|
935
1167
|
}
|
|
@@ -955,7 +1187,8 @@ class ThreeDPrinterMCPServer {
|
|
|
955
1187
|
async getPrinterFile(host, filename, port = DEFAULT_PORT, type = DEFAULT_TYPE, apiKey = DEFAULT_API_KEY, bambuSerial = DEFAULT_BAMBU_SERIAL, bambuToken = DEFAULT_BAMBU_TOKEN) {
|
|
956
1188
|
const implementation = this.printerFactory.getImplementation(type);
|
|
957
1189
|
if (type.toLowerCase() === "bambu") {
|
|
958
|
-
|
|
1190
|
+
const bambuApiKey = `${bambuSerial}:${bambuToken}`;
|
|
1191
|
+
return implementation.getFile(host, port, bambuApiKey, filename);
|
|
959
1192
|
}
|
|
960
1193
|
return implementation.getFile(host, port, apiKey, filename);
|
|
961
1194
|
}
|
|
@@ -966,7 +1199,8 @@ class ThreeDPrinterMCPServer {
|
|
|
966
1199
|
try {
|
|
967
1200
|
const implementation = this.printerFactory.getImplementation(type);
|
|
968
1201
|
if (type.toLowerCase() === "bambu") {
|
|
969
|
-
|
|
1202
|
+
const bambuApiKey = `${bambuSerial}:${bambuToken}`;
|
|
1203
|
+
return await implementation.uploadFile(host, port, bambuApiKey, tempFilePath, filename, print);
|
|
970
1204
|
}
|
|
971
1205
|
return await implementation.uploadFile(host, port, apiKey, tempFilePath, filename, print);
|
|
972
1206
|
}
|
|
@@ -980,39 +1214,230 @@ class ThreeDPrinterMCPServer {
|
|
|
980
1214
|
async startPrint(host, port, type, apiKey, bambuSerial, bambuToken, gcodeFilename) {
|
|
981
1215
|
const implementation = this.printerFactory.getImplementation(type);
|
|
982
1216
|
if (type.toLowerCase() === "bambu") {
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
// The print_3mf tool handles starting 3MF prints.
|
|
986
|
-
return await implementation.startJob(host, port, apiKey, bambuSerial, bambuToken, gcodeFilename);
|
|
1217
|
+
const bambuApiKey = `${bambuSerial}:${bambuToken}`;
|
|
1218
|
+
return await implementation.startJob(host, port, bambuApiKey, gcodeFilename);
|
|
987
1219
|
}
|
|
988
1220
|
return await implementation.startJob(host, port, apiKey, gcodeFilename);
|
|
989
1221
|
}
|
|
990
1222
|
async cancelPrint(host, port, type, apiKey, bambuSerial, bambuToken) {
|
|
991
1223
|
const implementation = this.printerFactory.getImplementation(type);
|
|
992
1224
|
if (type.toLowerCase() === "bambu") {
|
|
993
|
-
|
|
1225
|
+
const bambuApiKey = `${bambuSerial}:${bambuToken}`;
|
|
1226
|
+
return await implementation.cancelJob(host, port, bambuApiKey);
|
|
994
1227
|
}
|
|
995
1228
|
return await implementation.cancelJob(host, port, apiKey);
|
|
996
1229
|
}
|
|
997
1230
|
async setPrinterTemperature(host, port, type, apiKey, bambuSerial, bambuToken, component, temperature) {
|
|
998
1231
|
const implementation = this.printerFactory.getImplementation(type);
|
|
999
1232
|
if (type.toLowerCase() === "bambu") {
|
|
1000
|
-
const
|
|
1001
|
-
|
|
1002
|
-
return await bambuImplementation.setTemperature(host, bambuSerial, bambuToken, component, temperature);
|
|
1003
|
-
}
|
|
1004
|
-
else {
|
|
1005
|
-
console.warn('setTemperature not fully implemented for Bambu via direct commands yet.');
|
|
1006
|
-
return { status: 'Command sent (implementation pending)' }; // Avoid throwing error if method doesn't exist
|
|
1007
|
-
}
|
|
1233
|
+
const bambuApiKey = `${bambuSerial}:${bambuToken}`;
|
|
1234
|
+
return implementation.setTemperature(host, port, bambuApiKey, component, temperature);
|
|
1008
1235
|
}
|
|
1009
1236
|
return implementation.setTemperature(host, port, apiKey, component, temperature);
|
|
1010
1237
|
}
|
|
1011
|
-
|
|
1238
|
+
toStructuredToolError(tool, message) {
|
|
1239
|
+
const isInputError = message.toLowerCase().includes("missing required") ||
|
|
1240
|
+
message.toLowerCase().includes("invalid") ||
|
|
1241
|
+
message.toLowerCase().includes("unsupported");
|
|
1242
|
+
return {
|
|
1243
|
+
status: "error",
|
|
1244
|
+
retryable: !isInputError,
|
|
1245
|
+
suggestion: isInputError
|
|
1246
|
+
? "Fix tool arguments and retry."
|
|
1247
|
+
: "Retry the call. If it keeps failing, verify printer connectivity and credentials.",
|
|
1248
|
+
message,
|
|
1249
|
+
tool,
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
async parseHttpRequestBody(req) {
|
|
1253
|
+
const chunks = [];
|
|
1254
|
+
for await (const chunk of req) {
|
|
1255
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
1256
|
+
}
|
|
1257
|
+
if (chunks.length === 0) {
|
|
1258
|
+
return undefined;
|
|
1259
|
+
}
|
|
1260
|
+
const rawBody = Buffer.concat(chunks).toString("utf8").trim();
|
|
1261
|
+
if (!rawBody) {
|
|
1262
|
+
return undefined;
|
|
1263
|
+
}
|
|
1264
|
+
return JSON.parse(rawBody);
|
|
1265
|
+
}
|
|
1266
|
+
isAllowedOrigin(req) {
|
|
1267
|
+
const origin = req.headers.origin;
|
|
1268
|
+
if (!origin) {
|
|
1269
|
+
return true;
|
|
1270
|
+
}
|
|
1271
|
+
if (this.runtimeConfig.allowedOrigins.size === 0) {
|
|
1272
|
+
return false;
|
|
1273
|
+
}
|
|
1274
|
+
return this.runtimeConfig.allowedOrigins.has(origin);
|
|
1275
|
+
}
|
|
1276
|
+
async connectStdio() {
|
|
1012
1277
|
const transport = new StdioServerTransport();
|
|
1013
1278
|
await this.server.connect(transport);
|
|
1014
1279
|
console.error("3D Printer MCP server running on stdio transport");
|
|
1015
1280
|
}
|
|
1281
|
+
async connectStreamableHttp() {
|
|
1282
|
+
if (this.httpRuntime) {
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
const transport = new StreamableHTTPServerTransport({
|
|
1286
|
+
sessionIdGenerator: this.runtimeConfig.statefulSession ? () => randomUUID() : undefined,
|
|
1287
|
+
enableJsonResponse: this.runtimeConfig.enableJsonResponse
|
|
1288
|
+
});
|
|
1289
|
+
await this.server.connect(transport);
|
|
1290
|
+
const requestHandler = async (req, res) => {
|
|
1291
|
+
try {
|
|
1292
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
1293
|
+
if (url.pathname !== this.runtimeConfig.httpPath) {
|
|
1294
|
+
res.statusCode = 404;
|
|
1295
|
+
res.setHeader("content-type", "application/json");
|
|
1296
|
+
res.end(JSON.stringify({ error: "Not Found" }));
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
if (!this.isAllowedOrigin(req)) {
|
|
1300
|
+
res.statusCode = 403;
|
|
1301
|
+
res.setHeader("content-type", "application/json");
|
|
1302
|
+
res.end(JSON.stringify({ error: "Forbidden origin" }));
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
const method = req.method?.toUpperCase() ?? "GET";
|
|
1306
|
+
if (!["POST", "GET", "DELETE"].includes(method)) {
|
|
1307
|
+
res.statusCode = 405;
|
|
1308
|
+
res.setHeader("allow", "POST, GET, DELETE");
|
|
1309
|
+
res.setHeader("content-type", "application/json");
|
|
1310
|
+
res.end(JSON.stringify({ error: "Method Not Allowed" }));
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
let parsedBody;
|
|
1314
|
+
if (method === "POST") {
|
|
1315
|
+
try {
|
|
1316
|
+
parsedBody = await this.parseHttpRequestBody(req);
|
|
1317
|
+
}
|
|
1318
|
+
catch {
|
|
1319
|
+
res.statusCode = 400;
|
|
1320
|
+
res.setHeader("content-type", "application/json");
|
|
1321
|
+
res.end(JSON.stringify({ error: "Invalid JSON body" }));
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
await transport.handleRequest(req, res, parsedBody);
|
|
1326
|
+
}
|
|
1327
|
+
catch (error) {
|
|
1328
|
+
console.error("Error handling streamable-http request:", error);
|
|
1329
|
+
if (!res.headersSent) {
|
|
1330
|
+
res.statusCode = 500;
|
|
1331
|
+
res.setHeader("content-type", "application/json");
|
|
1332
|
+
res.end(JSON.stringify({
|
|
1333
|
+
jsonrpc: "2.0",
|
|
1334
|
+
error: { code: -32603, message: "Internal server error" },
|
|
1335
|
+
id: null
|
|
1336
|
+
}));
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
};
|
|
1340
|
+
const httpServer = createHttpServer((req, res) => {
|
|
1341
|
+
void requestHandler(req, res);
|
|
1342
|
+
});
|
|
1343
|
+
await new Promise((resolve, reject) => {
|
|
1344
|
+
httpServer.once("error", reject);
|
|
1345
|
+
httpServer.listen(this.runtimeConfig.httpPort, this.runtimeConfig.httpHost, () => resolve());
|
|
1346
|
+
});
|
|
1347
|
+
this.httpRuntime = { transport, httpServer };
|
|
1348
|
+
console.error(`3D Printer MCP server running on streamable-http at http://${this.runtimeConfig.httpHost}:${this.runtimeConfig.httpPort}${this.runtimeConfig.httpPath}`);
|
|
1349
|
+
}
|
|
1350
|
+
async invokeBlenderBridge(params) {
|
|
1351
|
+
const command = params.bridgeCommand ?? this.runtimeConfig.blenderBridgeCommand;
|
|
1352
|
+
const payload = {
|
|
1353
|
+
modelPath: params.stlPath,
|
|
1354
|
+
operations: params.operations,
|
|
1355
|
+
source: "mcp-3d-printer-server"
|
|
1356
|
+
};
|
|
1357
|
+
if (!params.execute) {
|
|
1358
|
+
return {
|
|
1359
|
+
status: "prepared",
|
|
1360
|
+
message: "Prepared Blender MCP payload. Set execute=true to run bridge command.",
|
|
1361
|
+
bridgeCommand: command ?? null,
|
|
1362
|
+
payload
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
if (!command) {
|
|
1366
|
+
throw new Error("execute=true requires bridge_command or BLENDER_MCP_BRIDGE_COMMAND env var.");
|
|
1367
|
+
}
|
|
1368
|
+
const { spawn } = await import("node:child_process");
|
|
1369
|
+
const result = await new Promise((resolve, reject) => {
|
|
1370
|
+
const child = spawn(command, {
|
|
1371
|
+
shell: true,
|
|
1372
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1373
|
+
});
|
|
1374
|
+
let stdout = "";
|
|
1375
|
+
let stderr = "";
|
|
1376
|
+
child.stdout.setEncoding("utf8");
|
|
1377
|
+
child.stderr.setEncoding("utf8");
|
|
1378
|
+
child.stdout.on("data", (chunk) => {
|
|
1379
|
+
stdout += chunk;
|
|
1380
|
+
});
|
|
1381
|
+
child.stderr.on("data", (chunk) => {
|
|
1382
|
+
stderr += chunk;
|
|
1383
|
+
});
|
|
1384
|
+
child.on("error", reject);
|
|
1385
|
+
child.on("close", (code) => {
|
|
1386
|
+
if (code === 0) {
|
|
1387
|
+
resolve({ stdout, stderr });
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
reject(new Error(`Blender bridge command exited with code ${code ?? "unknown"}${stderr.trim() ? `: ${stderr.trim()}` : ""}`));
|
|
1391
|
+
});
|
|
1392
|
+
child.stdin.write(JSON.stringify(payload));
|
|
1393
|
+
child.stdin.end();
|
|
1394
|
+
});
|
|
1395
|
+
return {
|
|
1396
|
+
status: "executed",
|
|
1397
|
+
bridgeCommand: command,
|
|
1398
|
+
stdout: result.stdout?.toString() ?? "",
|
|
1399
|
+
stderr: result.stderr?.toString() ?? ""
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
async close() {
|
|
1403
|
+
await this.printerFactory.disconnectAll();
|
|
1404
|
+
if (this.httpRuntime) {
|
|
1405
|
+
await this.httpRuntime.transport.close();
|
|
1406
|
+
await new Promise((resolve, reject) => {
|
|
1407
|
+
this.httpRuntime?.httpServer.close((error) => {
|
|
1408
|
+
if (error) {
|
|
1409
|
+
reject(error);
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
resolve();
|
|
1413
|
+
});
|
|
1414
|
+
});
|
|
1415
|
+
this.httpRuntime = undefined;
|
|
1416
|
+
}
|
|
1417
|
+
else {
|
|
1418
|
+
await this.server.close();
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
async run() {
|
|
1422
|
+
if (this.runtimeConfig.transport === "streamable-http") {
|
|
1423
|
+
await this.connectStreamableHttp();
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
await this.connectStdio();
|
|
1427
|
+
}
|
|
1016
1428
|
}
|
|
1017
1429
|
const server = new ThreeDPrinterMCPServer();
|
|
1018
|
-
|
|
1430
|
+
const shutdown = async () => {
|
|
1431
|
+
await server.close();
|
|
1432
|
+
process.exit(0);
|
|
1433
|
+
};
|
|
1434
|
+
process.once("SIGINT", () => {
|
|
1435
|
+
void shutdown();
|
|
1436
|
+
});
|
|
1437
|
+
process.once("SIGTERM", () => {
|
|
1438
|
+
void shutdown();
|
|
1439
|
+
});
|
|
1440
|
+
server.run().catch((error) => {
|
|
1441
|
+
console.error("[mcp-3d-printer-server] startup failed:", error);
|
|
1442
|
+
process.exit(1);
|
|
1443
|
+
});
|