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/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
5
6
|
import {
|
|
6
7
|
ListResourcesRequestSchema,
|
|
7
8
|
ReadResourceRequestSchema,
|
|
@@ -14,6 +15,8 @@ import axios from "axios";
|
|
|
14
15
|
import dotenv from "dotenv";
|
|
15
16
|
import fs from "fs";
|
|
16
17
|
import path from "path";
|
|
18
|
+
import { createServer as createHttpServer, type IncomingMessage, type Server as HttpServer, type ServerResponse } from "node:http";
|
|
19
|
+
import { randomUUID } from "node:crypto";
|
|
17
20
|
import * as THREE from 'three';
|
|
18
21
|
import { PrinterFactory } from "./printers/printer-factory.js";
|
|
19
22
|
import { STLManipulator } from "./stl/stl-manipulator.js";
|
|
@@ -31,14 +34,139 @@ const DEFAULT_TYPE = process.env.PRINTER_TYPE || "octoprint"; // Default to Octo
|
|
|
31
34
|
const TEMP_DIR = process.env.TEMP_DIR || path.join(process.cwd(), "temp");
|
|
32
35
|
|
|
33
36
|
// Slicer configuration
|
|
34
|
-
const DEFAULT_SLICER_TYPE = process.env.SLICER_TYPE || "prusaslicer";
|
|
35
|
-
const DEFAULT_SLICER_PATH = process.env.SLICER_PATH || "";
|
|
37
|
+
const DEFAULT_SLICER_TYPE = process.env.SLICER_TYPE || (process.env.PRINTER_TYPE === "bambu" ? "bambustudio" : "prusaslicer");
|
|
38
|
+
const DEFAULT_SLICER_PATH = process.env.SLICER_PATH || (process.env.PRINTER_TYPE === "bambu" ? "/Applications/BambuStudio.app/Contents/MacOS/BambuStudio" : "");
|
|
36
39
|
const DEFAULT_SLICER_PROFILE = process.env.SLICER_PROFILE || "";
|
|
37
40
|
|
|
38
41
|
// Bambu-specific default values
|
|
39
42
|
const DEFAULT_BAMBU_SERIAL = process.env.BAMBU_SERIAL || "";
|
|
40
43
|
const DEFAULT_BAMBU_TOKEN = process.env.BAMBU_TOKEN || "";
|
|
41
44
|
|
|
45
|
+
// Printer model and bed type (Bambu safety)
|
|
46
|
+
const DEFAULT_BAMBU_MODEL = process.env.BAMBU_MODEL?.trim().toLowerCase() || "";
|
|
47
|
+
const DEFAULT_BED_TYPE = process.env.BED_TYPE?.trim().toLowerCase() || "textured_plate";
|
|
48
|
+
const DEFAULT_NOZZLE_DIAMETER = process.env.NOZZLE_DIAMETER?.trim() || "0.4";
|
|
49
|
+
|
|
50
|
+
const VALID_BAMBU_MODELS = ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"] as const;
|
|
51
|
+
type BambuModel = typeof VALID_BAMBU_MODELS[number];
|
|
52
|
+
|
|
53
|
+
const VALID_BED_TYPES = ["textured_plate", "cool_plate", "engineering_plate", "hot_plate"] as const;
|
|
54
|
+
|
|
55
|
+
// Map model IDs to BambuStudio --load-machine preset names
|
|
56
|
+
const BAMBU_MODEL_PRESETS: Record<string, (nozzle: string) => string> = {
|
|
57
|
+
p1s: (n) => `Bambu Lab P1S ${n} nozzle`,
|
|
58
|
+
p1p: (n) => `Bambu Lab P1P ${n} nozzle`,
|
|
59
|
+
x1c: (n) => `Bambu Lab X1 Carbon ${n} nozzle`,
|
|
60
|
+
x1e: (n) => `Bambu Lab X1E ${n} nozzle`,
|
|
61
|
+
a1: (n) => `Bambu Lab A1 ${n} nozzle`,
|
|
62
|
+
a1mini: (n) => `Bambu Lab A1 mini ${n} nozzle`,
|
|
63
|
+
h2d: (n) => `Bambu Lab H2D ${n} nozzle`,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function validateBambuModel(model: string): BambuModel {
|
|
67
|
+
const normalized = model.trim().toLowerCase();
|
|
68
|
+
if (!(VALID_BAMBU_MODELS as readonly string[]).includes(normalized)) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`Invalid Bambu printer model "${model}". Valid models: ${VALID_BAMBU_MODELS.join(", ")}`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return normalized as BambuModel;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function resolveBedType(bedType: string | undefined): string {
|
|
77
|
+
const resolved = (bedType || DEFAULT_BED_TYPE).trim().toLowerCase();
|
|
78
|
+
if (!(VALID_BED_TYPES as readonly string[]).includes(resolved)) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`Invalid bed type "${bedType}". Valid types: ${VALID_BED_TYPES.join(", ")}`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return resolved;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
type RuntimeConfig = {
|
|
87
|
+
transport: "stdio" | "streamable-http";
|
|
88
|
+
httpHost: string;
|
|
89
|
+
httpPort: number;
|
|
90
|
+
httpPath: string;
|
|
91
|
+
statefulSession: boolean;
|
|
92
|
+
enableJsonResponse: boolean;
|
|
93
|
+
allowedOrigins: Set<string>;
|
|
94
|
+
blenderBridgeCommand?: string;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
function parseBooleanEnv(rawValue: string | undefined, fallback: boolean): boolean {
|
|
98
|
+
if (rawValue === undefined) {
|
|
99
|
+
return fallback;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const value = rawValue.trim().toLowerCase();
|
|
103
|
+
if (["1", "true", "yes", "on"].includes(value)) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
if (["0", "false", "no", "off"].includes(value)) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
return fallback;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function parsePort(value: string | undefined, fallback: number): number {
|
|
113
|
+
if (!value) {
|
|
114
|
+
return fallback;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const parsed = Number(value);
|
|
118
|
+
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65535) {
|
|
119
|
+
throw new Error(`Invalid MCP_HTTP_PORT value: ${value}`);
|
|
120
|
+
}
|
|
121
|
+
return parsed;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function normalizePath(pathValue: string | undefined): string {
|
|
125
|
+
const value = (pathValue ?? "/mcp").trim();
|
|
126
|
+
if (!value) {
|
|
127
|
+
return "/mcp";
|
|
128
|
+
}
|
|
129
|
+
return value.startsWith("/") ? value : `/${value}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function parseCsvEnv(value: string | undefined): Set<string> {
|
|
133
|
+
if (!value) {
|
|
134
|
+
return new Set();
|
|
135
|
+
}
|
|
136
|
+
const entries = value
|
|
137
|
+
.split(",")
|
|
138
|
+
.map((entry) => entry.trim())
|
|
139
|
+
.filter((entry) => entry.length > 0);
|
|
140
|
+
return new Set(entries);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function readRuntimeConfig(): RuntimeConfig {
|
|
144
|
+
const rawTransport = process.env.MCP_TRANSPORT?.trim().toLowerCase();
|
|
145
|
+
const transport =
|
|
146
|
+
rawTransport === "streamable-http" || rawTransport === "http"
|
|
147
|
+
? "streamable-http"
|
|
148
|
+
: "stdio";
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
transport,
|
|
152
|
+
httpHost: process.env.MCP_HTTP_HOST?.trim() || "127.0.0.1",
|
|
153
|
+
httpPort: parsePort(process.env.MCP_HTTP_PORT, 3000),
|
|
154
|
+
httpPath: normalizePath(process.env.MCP_HTTP_PATH),
|
|
155
|
+
statefulSession: parseBooleanEnv(process.env.MCP_HTTP_STATEFUL, true),
|
|
156
|
+
enableJsonResponse: parseBooleanEnv(process.env.MCP_HTTP_JSON_RESPONSE, true),
|
|
157
|
+
allowedOrigins: parseCsvEnv(process.env.MCP_HTTP_ALLOWED_ORIGINS),
|
|
158
|
+
blenderBridgeCommand: process.env.BLENDER_MCP_BRIDGE_COMMAND?.trim() || undefined,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
type StructuredToolError = {
|
|
163
|
+
status: "error";
|
|
164
|
+
retryable: boolean;
|
|
165
|
+
suggestion: string;
|
|
166
|
+
message: string;
|
|
167
|
+
tool: string;
|
|
168
|
+
};
|
|
169
|
+
|
|
42
170
|
// Ensure temp directory exists
|
|
43
171
|
if (!fs.existsSync(TEMP_DIR)) {
|
|
44
172
|
fs.mkdirSync(TEMP_DIR, { recursive: true });
|
|
@@ -48,12 +176,15 @@ class ThreeDPrinterMCPServer {
|
|
|
48
176
|
private server: Server;
|
|
49
177
|
private printerFactory: PrinterFactory;
|
|
50
178
|
private stlManipulator: STLManipulator;
|
|
179
|
+
private readonly runtimeConfig: RuntimeConfig;
|
|
180
|
+
private httpRuntime?: { transport: StreamableHTTPServerTransport; httpServer: HttpServer };
|
|
51
181
|
|
|
52
182
|
constructor() {
|
|
183
|
+
this.runtimeConfig = readRuntimeConfig();
|
|
53
184
|
this.server = new Server(
|
|
54
185
|
{
|
|
55
186
|
name: "mcp-3d-printer-server",
|
|
56
|
-
version: "1.
|
|
187
|
+
version: "1.2.0"
|
|
57
188
|
},
|
|
58
189
|
{
|
|
59
190
|
capabilities: {
|
|
@@ -70,17 +201,68 @@ class ThreeDPrinterMCPServer {
|
|
|
70
201
|
this.setupErrorHandling();
|
|
71
202
|
}
|
|
72
203
|
|
|
204
|
+
private async resolveBambuModel(argsModel: string | undefined): Promise<string> {
|
|
205
|
+
const fromArgs = (argsModel || DEFAULT_BAMBU_MODEL).trim().toLowerCase();
|
|
206
|
+
if (fromArgs) {
|
|
207
|
+
return validateBambuModel(fromArgs);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// No model from args or env — ask the user via elicitation
|
|
211
|
+
try {
|
|
212
|
+
const result = await this.server.elicitInput({
|
|
213
|
+
mode: "form" as const,
|
|
214
|
+
message:
|
|
215
|
+
"Your Bambu Lab printer model is required for safe operation. " +
|
|
216
|
+
"Using the wrong model can cause the bed to crash into the nozzle and damage the printer.",
|
|
217
|
+
requestedSchema: {
|
|
218
|
+
type: "object",
|
|
219
|
+
properties: {
|
|
220
|
+
bambu_model: {
|
|
221
|
+
type: "string",
|
|
222
|
+
title: "Printer Model",
|
|
223
|
+
description: "Which Bambu Lab printer do you have?",
|
|
224
|
+
oneOf: [
|
|
225
|
+
{ const: "p1s", title: "P1S" },
|
|
226
|
+
{ const: "p1p", title: "P1P" },
|
|
227
|
+
{ const: "x1c", title: "X1 Carbon" },
|
|
228
|
+
{ const: "x1e", title: "X1E" },
|
|
229
|
+
{ const: "a1", title: "A1" },
|
|
230
|
+
{ const: "a1mini", title: "A1 Mini" },
|
|
231
|
+
{ const: "h2d", title: "H2D" },
|
|
232
|
+
],
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
required: ["bambu_model"],
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
if (result.action === "accept" && result.content?.bambu_model) {
|
|
240
|
+
return validateBambuModel(String(result.content.bambu_model));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
throw new Error(
|
|
244
|
+
"Printer model selection was cancelled. Cannot proceed without knowing the printer model."
|
|
245
|
+
);
|
|
246
|
+
} catch (elicitError: any) {
|
|
247
|
+
const msg = elicitError?.message || String(elicitError);
|
|
248
|
+
if (
|
|
249
|
+
elicitError?.code === -32601 || elicitError?.code === -32600 ||
|
|
250
|
+
msg.includes("does not support") || msg.includes("elicitation")
|
|
251
|
+
) {
|
|
252
|
+
throw new Error(
|
|
253
|
+
"bambu_model is required but your MCP client does not support elicitation. " +
|
|
254
|
+
`Set the BAMBU_MODEL environment variable or pass bambu_model in the tool call. ` +
|
|
255
|
+
`Valid models: ${VALID_BAMBU_MODELS.join(", ")}`
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
throw elicitError;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
73
262
|
setupErrorHandling() {
|
|
74
263
|
this.server.onerror = (error) => {
|
|
75
264
|
console.error("[MCP Error]", error);
|
|
76
265
|
};
|
|
77
|
-
|
|
78
|
-
process.on("SIGINT", async () => {
|
|
79
|
-
// Disconnect all printers
|
|
80
|
-
await this.printerFactory.disconnectAll();
|
|
81
|
-
await this.server.close();
|
|
82
|
-
process.exit(0);
|
|
83
|
-
});
|
|
84
266
|
}
|
|
85
267
|
|
|
86
268
|
setupHandlers() {
|
|
@@ -230,17 +412,26 @@ class ThreeDPrinterMCPServer {
|
|
|
230
412
|
},
|
|
231
413
|
{
|
|
232
414
|
name: "slice_stl",
|
|
233
|
-
description: "Slice an STL file to generate G-code",
|
|
415
|
+
description: "Slice an STL or 3MF file to generate G-code or sliced 3MF",
|
|
234
416
|
inputSchema: {
|
|
235
417
|
type: "object",
|
|
236
418
|
properties: {
|
|
237
419
|
stl_path: {
|
|
238
420
|
type: "string",
|
|
239
|
-
description: "Path to the STL file to slice"
|
|
421
|
+
description: "Path to the STL or 3MF file to slice"
|
|
422
|
+
},
|
|
423
|
+
bambu_model: {
|
|
424
|
+
type: "string",
|
|
425
|
+
enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
|
|
426
|
+
description: "Bambu Lab printer model — required when using bambustudio slicer to ensure correct G-code."
|
|
427
|
+
},
|
|
428
|
+
nozzle_diameter: {
|
|
429
|
+
type: "string",
|
|
430
|
+
description: "Nozzle diameter in mm (default: 0.4)."
|
|
240
431
|
},
|
|
241
432
|
slicer_type: {
|
|
242
433
|
type: "string",
|
|
243
|
-
description: "Type of slicer to use (prusaslicer, cura, slic3r, orcaslicer) (default: value from env)"
|
|
434
|
+
description: "Type of slicer to use (prusaslicer, cura, slic3r, orcaslicer, bambustudio) (default: value from env)"
|
|
244
435
|
},
|
|
245
436
|
slicer_path: {
|
|
246
437
|
type: "string",
|
|
@@ -507,6 +698,20 @@ class ThreeDPrinterMCPServer {
|
|
|
507
698
|
type: "string",
|
|
508
699
|
description: "Path to the 3MF file to print."
|
|
509
700
|
},
|
|
701
|
+
bambu_model: {
|
|
702
|
+
type: "string",
|
|
703
|
+
enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
|
|
704
|
+
description: "REQUIRED: Bambu Lab printer model. Ensures correct G-code generation — wrong model can crash the bed into the nozzle."
|
|
705
|
+
},
|
|
706
|
+
bed_type: {
|
|
707
|
+
type: "string",
|
|
708
|
+
enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate"],
|
|
709
|
+
description: "Bed/plate type installed on the printer (default: textured_plate)."
|
|
710
|
+
},
|
|
711
|
+
nozzle_diameter: {
|
|
712
|
+
type: "string",
|
|
713
|
+
description: "Nozzle diameter in mm (default: 0.4)."
|
|
714
|
+
},
|
|
510
715
|
host: {
|
|
511
716
|
type: "string",
|
|
512
717
|
description: "Hostname or IP address of the Bambu printer (default: value from env)"
|
|
@@ -529,7 +734,7 @@ class ThreeDPrinterMCPServer {
|
|
|
529
734
|
additionalProperties: { type: "number" }
|
|
530
735
|
}
|
|
531
736
|
},
|
|
532
|
-
required: ["three_mf_path"]
|
|
737
|
+
required: ["three_mf_path", "bambu_model"]
|
|
533
738
|
}
|
|
534
739
|
},
|
|
535
740
|
{
|
|
@@ -577,6 +782,33 @@ class ThreeDPrinterMCPServer {
|
|
|
577
782
|
},
|
|
578
783
|
required: ["stl_path"]
|
|
579
784
|
}
|
|
785
|
+
},
|
|
786
|
+
{
|
|
787
|
+
name: "blender_mcp_edit_model",
|
|
788
|
+
description: "Optionally send STL-edit instructions to a Blender MCP bridge command. Use for advanced model edits outside built-in STL tools.",
|
|
789
|
+
inputSchema: {
|
|
790
|
+
type: "object",
|
|
791
|
+
properties: {
|
|
792
|
+
stl_path: {
|
|
793
|
+
type: "string",
|
|
794
|
+
description: "Path to the local STL file."
|
|
795
|
+
},
|
|
796
|
+
operations: {
|
|
797
|
+
type: "array",
|
|
798
|
+
description: "Ordered edit operations for Blender (e.g. remesh, boolean, decimate).",
|
|
799
|
+
items: { type: "string" }
|
|
800
|
+
},
|
|
801
|
+
bridge_command: {
|
|
802
|
+
type: "string",
|
|
803
|
+
description: "Optional override command for invoking a local Blender MCP bridge."
|
|
804
|
+
},
|
|
805
|
+
execute: {
|
|
806
|
+
type: "boolean",
|
|
807
|
+
description: "When true, attempts to execute bridge command; otherwise returns a prepared payload only."
|
|
808
|
+
}
|
|
809
|
+
},
|
|
810
|
+
required: ["stl_path", "operations"]
|
|
811
|
+
}
|
|
580
812
|
}
|
|
581
813
|
]
|
|
582
814
|
};
|
|
@@ -593,7 +825,7 @@ class ThreeDPrinterMCPServer {
|
|
|
593
825
|
const apiKey = String(args?.api_key || DEFAULT_API_KEY);
|
|
594
826
|
const bambuSerial = String(args?.bambu_serial || DEFAULT_BAMBU_SERIAL);
|
|
595
827
|
const bambuToken = String(args?.bambu_token || DEFAULT_BAMBU_TOKEN);
|
|
596
|
-
const slicerType = String(args?.slicer_type || DEFAULT_SLICER_TYPE) as 'prusaslicer' | 'cura' | 'slic3r';
|
|
828
|
+
const slicerType = String(args?.slicer_type || DEFAULT_SLICER_TYPE) as 'prusaslicer' | 'cura' | 'slic3r' | 'orcaslicer' | 'bambustudio';
|
|
597
829
|
const slicerPath = String(args?.slicer_path || DEFAULT_SLICER_PATH);
|
|
598
830
|
const slicerProfile = String(args?.slicer_profile || DEFAULT_SLICER_PROFILE);
|
|
599
831
|
|
|
@@ -654,17 +886,27 @@ class ThreeDPrinterMCPServer {
|
|
|
654
886
|
);
|
|
655
887
|
break;
|
|
656
888
|
|
|
657
|
-
case "slice_stl":
|
|
889
|
+
case "slice_stl": {
|
|
658
890
|
if (!args?.stl_path) {
|
|
659
891
|
throw new Error("Missing required parameter: stl_path");
|
|
660
892
|
}
|
|
893
|
+
// Resolve printer model for BambuStudio slicer
|
|
894
|
+
let slicePreset: string | undefined;
|
|
895
|
+
if (slicerType === 'bambustudio') {
|
|
896
|
+
const sliceModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
897
|
+
const nozzleDiam = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
898
|
+
slicePreset = BAMBU_MODEL_PRESETS[sliceModel]?.(nozzleDiam);
|
|
899
|
+
}
|
|
661
900
|
result = await this.stlManipulator.sliceSTL(
|
|
662
901
|
String(args.stl_path),
|
|
663
902
|
slicerType,
|
|
664
903
|
slicerPath,
|
|
665
|
-
slicerProfile || undefined
|
|
904
|
+
slicerProfile || undefined,
|
|
905
|
+
undefined, // progressCallback
|
|
906
|
+
slicePreset
|
|
666
907
|
);
|
|
667
908
|
break;
|
|
909
|
+
}
|
|
668
910
|
|
|
669
911
|
case "confirm_temperatures":
|
|
670
912
|
if (!args?.gcode_path) {
|
|
@@ -697,12 +939,19 @@ class ThreeDPrinterMCPServer {
|
|
|
697
939
|
);
|
|
698
940
|
|
|
699
941
|
// 2. Slice the extended STL file
|
|
942
|
+
let processPreset: string | undefined;
|
|
943
|
+
if (slicerType === 'bambustudio') {
|
|
944
|
+
const processModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
945
|
+
const processNozzle = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
946
|
+
processPreset = BAMBU_MODEL_PRESETS[processModel]?.(processNozzle);
|
|
947
|
+
}
|
|
700
948
|
const gcodePath = await this.stlManipulator.sliceSTL(
|
|
701
949
|
extendedStlPath,
|
|
702
950
|
slicerType,
|
|
703
951
|
slicerPath,
|
|
704
952
|
slicerProfile || undefined,
|
|
705
|
-
processProgressCallback
|
|
953
|
+
processProgressCallback,
|
|
954
|
+
processPreset
|
|
706
955
|
);
|
|
707
956
|
|
|
708
957
|
// 3. Confirm temperatures if specified
|
|
@@ -928,7 +1177,7 @@ class ThreeDPrinterMCPServer {
|
|
|
928
1177
|
);
|
|
929
1178
|
break;
|
|
930
1179
|
|
|
931
|
-
case "print_3mf":
|
|
1180
|
+
case "print_3mf": {
|
|
932
1181
|
if (!args?.three_mf_path) {
|
|
933
1182
|
throw new Error("Missing required parameter: three_mf_path");
|
|
934
1183
|
}
|
|
@@ -938,9 +1187,38 @@ class ThreeDPrinterMCPServer {
|
|
|
938
1187
|
if (!bambuSerial || !bambuToken) {
|
|
939
1188
|
throw new Error("Bambu serial number and access token are required for print_3mf.");
|
|
940
1189
|
}
|
|
941
|
-
|
|
942
|
-
const
|
|
943
|
-
|
|
1190
|
+
|
|
1191
|
+
const printModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
1192
|
+
const printBedType = resolveBedType(args?.bed_type as string | undefined);
|
|
1193
|
+
const printNozzle = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
|
|
1194
|
+
const printPreset = BAMBU_MODEL_PRESETS[printModel]?.(printNozzle);
|
|
1195
|
+
|
|
1196
|
+
let threeMFPath = String(args.three_mf_path);
|
|
1197
|
+
|
|
1198
|
+
// Auto-slice if the 3MF doesn't contain gcode
|
|
1199
|
+
try {
|
|
1200
|
+
const JSZip = (await import('jszip')).default;
|
|
1201
|
+
const zipData = fs.readFileSync(threeMFPath);
|
|
1202
|
+
const zip = await JSZip.loadAsync(zipData);
|
|
1203
|
+
const hasGcode = Object.keys(zip.files).some(
|
|
1204
|
+
f => f.match(/Metadata\/plate_\d+\.gcode/i) || f.endsWith('.gcode')
|
|
1205
|
+
);
|
|
1206
|
+
if (!hasGcode) {
|
|
1207
|
+
console.log(`3MF has no gcode — auto-slicing with ${slicerType} for ${printModel}`);
|
|
1208
|
+
threeMFPath = await this.stlManipulator.sliceSTL(
|
|
1209
|
+
threeMFPath,
|
|
1210
|
+
slicerType,
|
|
1211
|
+
slicerPath,
|
|
1212
|
+
slicerProfile || undefined,
|
|
1213
|
+
undefined, // progressCallback
|
|
1214
|
+
printPreset
|
|
1215
|
+
);
|
|
1216
|
+
console.log("Auto-sliced to: " + threeMFPath);
|
|
1217
|
+
}
|
|
1218
|
+
} catch (sliceCheckErr: any) {
|
|
1219
|
+
console.warn("Could not check/slice 3MF, proceeding with original:", sliceCheckErr.message);
|
|
1220
|
+
}
|
|
1221
|
+
|
|
944
1222
|
// Define variables needed outside the parse try block
|
|
945
1223
|
let implementation: BambuImplementation;
|
|
946
1224
|
let threeMfFilename: string;
|
|
@@ -1013,12 +1291,13 @@ class ThreeDPrinterMCPServer {
|
|
|
1013
1291
|
printOptions = { // Assign to outer scope variable
|
|
1014
1292
|
useAMS: useAMS,
|
|
1015
1293
|
amsMapping: finalAmsMapping,
|
|
1294
|
+
bedType: printBedType,
|
|
1016
1295
|
bedLeveling: args?.bed_leveling !== undefined ? Boolean(args.bed_leveling) : undefined,
|
|
1017
1296
|
flowCalibration: args?.flow_calibration !== undefined ? Boolean(args.flow_calibration) : undefined,
|
|
1018
1297
|
vibrationCalibration: args?.vibration_calibration !== undefined ? Boolean(args.vibration_calibration) : undefined,
|
|
1019
1298
|
layerInspect: args?.layer_inspect !== undefined ? Boolean(args.layer_inspect) : undefined,
|
|
1020
1299
|
timelapse: args?.timelapse !== undefined ? Boolean(args.timelapse) : undefined,
|
|
1021
|
-
// md5: parsed3MFData?.metadata?.md5
|
|
1300
|
+
// md5: parsed3MFData?.metadata?.md5
|
|
1022
1301
|
};
|
|
1023
1302
|
|
|
1024
1303
|
} catch (error) { // Catch parsing or setup errors
|
|
@@ -1041,7 +1320,8 @@ class ThreeDPrinterMCPServer {
|
|
|
1041
1320
|
}
|
|
1042
1321
|
|
|
1043
1322
|
break;
|
|
1044
|
-
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1045
1325
|
case "merge_vertices":
|
|
1046
1326
|
if (!args?.stl_path) {
|
|
1047
1327
|
throw new Error("Missing required parameter: stl_path");
|
|
@@ -1065,6 +1345,19 @@ class ThreeDPrinterMCPServer {
|
|
|
1065
1345
|
}
|
|
1066
1346
|
result = await this.stlManipulator.layFlat(String(args.stl_path));
|
|
1067
1347
|
break;
|
|
1348
|
+
|
|
1349
|
+
case "blender_mcp_edit_model":
|
|
1350
|
+
if (!args?.stl_path || !Array.isArray(args.operations)) {
|
|
1351
|
+
throw new Error("Missing required parameters: stl_path and operations");
|
|
1352
|
+
}
|
|
1353
|
+
result = await this.invokeBlenderBridge({
|
|
1354
|
+
stlPath: String(args.stl_path),
|
|
1355
|
+
operations: args.operations.map((entry) => String(entry)),
|
|
1356
|
+
execute: Boolean(args.execute ?? false),
|
|
1357
|
+
bridgeCommand:
|
|
1358
|
+
args.bridge_command !== undefined ? String(args.bridge_command) : undefined,
|
|
1359
|
+
});
|
|
1360
|
+
break;
|
|
1068
1361
|
|
|
1069
1362
|
default:
|
|
1070
1363
|
throw new Error(`Unknown tool: ${name}`);
|
|
@@ -1082,14 +1375,16 @@ class ThreeDPrinterMCPServer {
|
|
|
1082
1375
|
console.error(`Error calling tool ${name}:`, error);
|
|
1083
1376
|
|
|
1084
1377
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1378
|
+
const structuredError = this.toStructuredToolError(name, errorMessage);
|
|
1085
1379
|
|
|
1086
1380
|
return {
|
|
1087
1381
|
content: [
|
|
1088
1382
|
{
|
|
1089
1383
|
type: "text",
|
|
1090
|
-
text: `Error: ${errorMessage}`
|
|
1384
|
+
text: `Error: ${errorMessage}\nSuggestion: ${structuredError.suggestion}`
|
|
1091
1385
|
}
|
|
1092
1386
|
],
|
|
1387
|
+
structuredContent: structuredError,
|
|
1093
1388
|
isError: true
|
|
1094
1389
|
};
|
|
1095
1390
|
}
|
|
@@ -1146,7 +1441,8 @@ class ThreeDPrinterMCPServer {
|
|
|
1146
1441
|
const implementation = this.printerFactory.getImplementation(type);
|
|
1147
1442
|
|
|
1148
1443
|
if (type.toLowerCase() === "bambu") {
|
|
1149
|
-
|
|
1444
|
+
const bambuApiKey = `${bambuSerial}:${bambuToken}`;
|
|
1445
|
+
return implementation.getFile(host, port, bambuApiKey, filename);
|
|
1150
1446
|
}
|
|
1151
1447
|
|
|
1152
1448
|
return implementation.getFile(host, port, apiKey, filename);
|
|
@@ -1172,9 +1468,8 @@ class ThreeDPrinterMCPServer {
|
|
|
1172
1468
|
const implementation = this.printerFactory.getImplementation(type);
|
|
1173
1469
|
|
|
1174
1470
|
if (type.toLowerCase() === "bambu") {
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
);
|
|
1471
|
+
const bambuApiKey = `${bambuSerial}:${bambuToken}`;
|
|
1472
|
+
return await implementation.uploadFile(host, port, bambuApiKey, tempFilePath, filename, print);
|
|
1178
1473
|
}
|
|
1179
1474
|
|
|
1180
1475
|
return await implementation.uploadFile(host, port, apiKey, tempFilePath, filename, print);
|
|
@@ -1198,12 +1493,8 @@ class ThreeDPrinterMCPServer {
|
|
|
1198
1493
|
const implementation = this.printerFactory.getImplementation(type);
|
|
1199
1494
|
|
|
1200
1495
|
if (type.toLowerCase() === "bambu") {
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
// The print_3mf tool handles starting 3MF prints.
|
|
1204
|
-
return await (implementation as any).startJob(
|
|
1205
|
-
host, port, apiKey, bambuSerial, bambuToken, gcodeFilename
|
|
1206
|
-
);
|
|
1496
|
+
const bambuApiKey = `${bambuSerial}:${bambuToken}`;
|
|
1497
|
+
return await implementation.startJob(host, port, bambuApiKey, gcodeFilename);
|
|
1207
1498
|
}
|
|
1208
1499
|
|
|
1209
1500
|
return await implementation.startJob(host, port, apiKey, gcodeFilename);
|
|
@@ -1220,9 +1511,8 @@ class ThreeDPrinterMCPServer {
|
|
|
1220
1511
|
const implementation = this.printerFactory.getImplementation(type);
|
|
1221
1512
|
|
|
1222
1513
|
if (type.toLowerCase() === "bambu") {
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
);
|
|
1514
|
+
const bambuApiKey = `${bambuSerial}:${bambuToken}`;
|
|
1515
|
+
return await implementation.cancelJob(host, port, bambuApiKey);
|
|
1226
1516
|
}
|
|
1227
1517
|
|
|
1228
1518
|
return await implementation.cancelJob(host, port, apiKey);
|
|
@@ -1241,27 +1531,269 @@ class ThreeDPrinterMCPServer {
|
|
|
1241
1531
|
const implementation = this.printerFactory.getImplementation(type);
|
|
1242
1532
|
|
|
1243
1533
|
if (type.toLowerCase() === "bambu") {
|
|
1244
|
-
const
|
|
1245
|
-
|
|
1246
|
-
if (bambuImplementation instanceof BambuImplementation && typeof bambuImplementation.setTemperature === 'function') {
|
|
1247
|
-
return await bambuImplementation.setTemperature(
|
|
1248
|
-
host, bambuSerial, bambuToken, component, temperature
|
|
1249
|
-
);
|
|
1250
|
-
} else {
|
|
1251
|
-
console.warn('setTemperature not fully implemented for Bambu via direct commands yet.');
|
|
1252
|
-
return { status: 'Command sent (implementation pending)'}; // Avoid throwing error if method doesn't exist
|
|
1253
|
-
}
|
|
1534
|
+
const bambuApiKey = `${bambuSerial}:${bambuToken}`;
|
|
1535
|
+
return implementation.setTemperature(host, port, bambuApiKey, component, temperature);
|
|
1254
1536
|
}
|
|
1255
1537
|
|
|
1256
1538
|
return implementation.setTemperature(host, port, apiKey, component, temperature);
|
|
1257
1539
|
}
|
|
1258
1540
|
|
|
1259
|
-
|
|
1541
|
+
private toStructuredToolError(tool: string, message: string): StructuredToolError {
|
|
1542
|
+
const isInputError =
|
|
1543
|
+
message.toLowerCase().includes("missing required") ||
|
|
1544
|
+
message.toLowerCase().includes("invalid") ||
|
|
1545
|
+
message.toLowerCase().includes("unsupported");
|
|
1546
|
+
|
|
1547
|
+
return {
|
|
1548
|
+
status: "error",
|
|
1549
|
+
retryable: !isInputError,
|
|
1550
|
+
suggestion: isInputError
|
|
1551
|
+
? "Fix tool arguments and retry."
|
|
1552
|
+
: "Retry the call. If it keeps failing, verify printer connectivity and credentials.",
|
|
1553
|
+
message,
|
|
1554
|
+
tool,
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
private async parseHttpRequestBody(req: IncomingMessage): Promise<unknown> {
|
|
1559
|
+
const chunks: Buffer[] = [];
|
|
1560
|
+
for await (const chunk of req) {
|
|
1561
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
if (chunks.length === 0) {
|
|
1565
|
+
return undefined;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
const rawBody = Buffer.concat(chunks).toString("utf8").trim();
|
|
1569
|
+
if (!rawBody) {
|
|
1570
|
+
return undefined;
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
return JSON.parse(rawBody);
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
private isAllowedOrigin(req: IncomingMessage): boolean {
|
|
1577
|
+
const origin = req.headers.origin;
|
|
1578
|
+
if (!origin) {
|
|
1579
|
+
return true;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
if (this.runtimeConfig.allowedOrigins.size === 0) {
|
|
1583
|
+
return false;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
return this.runtimeConfig.allowedOrigins.has(origin);
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
private async connectStdio(): Promise<void> {
|
|
1260
1590
|
const transport = new StdioServerTransport();
|
|
1261
1591
|
await this.server.connect(transport);
|
|
1262
1592
|
console.error("3D Printer MCP server running on stdio transport");
|
|
1263
1593
|
}
|
|
1594
|
+
|
|
1595
|
+
private async connectStreamableHttp(): Promise<void> {
|
|
1596
|
+
if (this.httpRuntime) {
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
const transport = new StreamableHTTPServerTransport({
|
|
1601
|
+
sessionIdGenerator: this.runtimeConfig.statefulSession ? () => randomUUID() : undefined,
|
|
1602
|
+
enableJsonResponse: this.runtimeConfig.enableJsonResponse
|
|
1603
|
+
});
|
|
1604
|
+
|
|
1605
|
+
await this.server.connect(transport);
|
|
1606
|
+
|
|
1607
|
+
const requestHandler = async (req: IncomingMessage, res: ServerResponse) => {
|
|
1608
|
+
try {
|
|
1609
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
1610
|
+
if (url.pathname !== this.runtimeConfig.httpPath) {
|
|
1611
|
+
res.statusCode = 404;
|
|
1612
|
+
res.setHeader("content-type", "application/json");
|
|
1613
|
+
res.end(JSON.stringify({ error: "Not Found" }));
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
if (!this.isAllowedOrigin(req)) {
|
|
1618
|
+
res.statusCode = 403;
|
|
1619
|
+
res.setHeader("content-type", "application/json");
|
|
1620
|
+
res.end(JSON.stringify({ error: "Forbidden origin" }));
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
const method = req.method?.toUpperCase() ?? "GET";
|
|
1625
|
+
if (!["POST", "GET", "DELETE"].includes(method)) {
|
|
1626
|
+
res.statusCode = 405;
|
|
1627
|
+
res.setHeader("allow", "POST, GET, DELETE");
|
|
1628
|
+
res.setHeader("content-type", "application/json");
|
|
1629
|
+
res.end(JSON.stringify({ error: "Method Not Allowed" }));
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
let parsedBody: unknown;
|
|
1634
|
+
if (method === "POST") {
|
|
1635
|
+
try {
|
|
1636
|
+
parsedBody = await this.parseHttpRequestBody(req);
|
|
1637
|
+
} catch {
|
|
1638
|
+
res.statusCode = 400;
|
|
1639
|
+
res.setHeader("content-type", "application/json");
|
|
1640
|
+
res.end(JSON.stringify({ error: "Invalid JSON body" }));
|
|
1641
|
+
return;
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
await transport.handleRequest(req, res, parsedBody);
|
|
1646
|
+
} catch (error) {
|
|
1647
|
+
console.error("Error handling streamable-http request:", error);
|
|
1648
|
+
if (!res.headersSent) {
|
|
1649
|
+
res.statusCode = 500;
|
|
1650
|
+
res.setHeader("content-type", "application/json");
|
|
1651
|
+
res.end(
|
|
1652
|
+
JSON.stringify({
|
|
1653
|
+
jsonrpc: "2.0",
|
|
1654
|
+
error: { code: -32603, message: "Internal server error" },
|
|
1655
|
+
id: null
|
|
1656
|
+
})
|
|
1657
|
+
);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
};
|
|
1661
|
+
|
|
1662
|
+
const httpServer = createHttpServer((req, res) => {
|
|
1663
|
+
void requestHandler(req, res);
|
|
1664
|
+
});
|
|
1665
|
+
|
|
1666
|
+
await new Promise<void>((resolve, reject) => {
|
|
1667
|
+
httpServer.once("error", reject);
|
|
1668
|
+
httpServer.listen(this.runtimeConfig.httpPort, this.runtimeConfig.httpHost, () => resolve());
|
|
1669
|
+
});
|
|
1670
|
+
|
|
1671
|
+
this.httpRuntime = { transport, httpServer };
|
|
1672
|
+
console.error(
|
|
1673
|
+
`3D Printer MCP server running on streamable-http at http://${this.runtimeConfig.httpHost}:${this.runtimeConfig.httpPort}${this.runtimeConfig.httpPath}`
|
|
1674
|
+
);
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
private async invokeBlenderBridge(params: {
|
|
1678
|
+
stlPath: string;
|
|
1679
|
+
operations: string[];
|
|
1680
|
+
execute: boolean;
|
|
1681
|
+
bridgeCommand?: string;
|
|
1682
|
+
}): Promise<Record<string, unknown>> {
|
|
1683
|
+
const command = params.bridgeCommand ?? this.runtimeConfig.blenderBridgeCommand;
|
|
1684
|
+
const payload = {
|
|
1685
|
+
modelPath: params.stlPath,
|
|
1686
|
+
operations: params.operations,
|
|
1687
|
+
source: "mcp-3d-printer-server"
|
|
1688
|
+
};
|
|
1689
|
+
|
|
1690
|
+
if (!params.execute) {
|
|
1691
|
+
return {
|
|
1692
|
+
status: "prepared",
|
|
1693
|
+
message: "Prepared Blender MCP payload. Set execute=true to run bridge command.",
|
|
1694
|
+
bridgeCommand: command ?? null,
|
|
1695
|
+
payload
|
|
1696
|
+
};
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
if (!command) {
|
|
1700
|
+
throw new Error(
|
|
1701
|
+
"execute=true requires bridge_command or BLENDER_MCP_BRIDGE_COMMAND env var."
|
|
1702
|
+
);
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
const { spawn } = await import("node:child_process");
|
|
1706
|
+
const result = await new Promise<{ stdout: string; stderr: string }>((resolve, reject) => {
|
|
1707
|
+
const child = spawn(command, {
|
|
1708
|
+
shell: true,
|
|
1709
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1710
|
+
});
|
|
1711
|
+
|
|
1712
|
+
let stdout = "";
|
|
1713
|
+
let stderr = "";
|
|
1714
|
+
|
|
1715
|
+
child.stdout.setEncoding("utf8");
|
|
1716
|
+
child.stderr.setEncoding("utf8");
|
|
1717
|
+
child.stdout.on("data", (chunk: string) => {
|
|
1718
|
+
stdout += chunk;
|
|
1719
|
+
});
|
|
1720
|
+
child.stderr.on("data", (chunk: string) => {
|
|
1721
|
+
stderr += chunk;
|
|
1722
|
+
});
|
|
1723
|
+
child.on("error", reject);
|
|
1724
|
+
child.on("close", (code) => {
|
|
1725
|
+
if (code === 0) {
|
|
1726
|
+
resolve({ stdout, stderr });
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
reject(
|
|
1731
|
+
new Error(
|
|
1732
|
+
`Blender bridge command exited with code ${code ?? "unknown"}${
|
|
1733
|
+
stderr.trim() ? `: ${stderr.trim()}` : ""
|
|
1734
|
+
}`
|
|
1735
|
+
)
|
|
1736
|
+
);
|
|
1737
|
+
});
|
|
1738
|
+
|
|
1739
|
+
child.stdin.write(JSON.stringify(payload));
|
|
1740
|
+
child.stdin.end();
|
|
1741
|
+
});
|
|
1742
|
+
|
|
1743
|
+
return {
|
|
1744
|
+
status: "executed",
|
|
1745
|
+
bridgeCommand: command,
|
|
1746
|
+
stdout: result.stdout?.toString() ?? "",
|
|
1747
|
+
stderr: result.stderr?.toString() ?? ""
|
|
1748
|
+
};
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
async close(): Promise<void> {
|
|
1752
|
+
await this.printerFactory.disconnectAll();
|
|
1753
|
+
|
|
1754
|
+
if (this.httpRuntime) {
|
|
1755
|
+
await this.httpRuntime.transport.close();
|
|
1756
|
+
await new Promise<void>((resolve, reject) => {
|
|
1757
|
+
this.httpRuntime?.httpServer.close((error) => {
|
|
1758
|
+
if (error) {
|
|
1759
|
+
reject(error);
|
|
1760
|
+
return;
|
|
1761
|
+
}
|
|
1762
|
+
resolve();
|
|
1763
|
+
});
|
|
1764
|
+
});
|
|
1765
|
+
this.httpRuntime = undefined;
|
|
1766
|
+
} else {
|
|
1767
|
+
await this.server.close();
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
async run(): Promise<void> {
|
|
1772
|
+
if (this.runtimeConfig.transport === "streamable-http") {
|
|
1773
|
+
await this.connectStreamableHttp();
|
|
1774
|
+
return;
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
await this.connectStdio();
|
|
1778
|
+
}
|
|
1264
1779
|
}
|
|
1265
1780
|
|
|
1266
1781
|
const server = new ThreeDPrinterMCPServer();
|
|
1267
|
-
|
|
1782
|
+
|
|
1783
|
+
const shutdown = async () => {
|
|
1784
|
+
await server.close();
|
|
1785
|
+
process.exit(0);
|
|
1786
|
+
};
|
|
1787
|
+
|
|
1788
|
+
process.once("SIGINT", () => {
|
|
1789
|
+
void shutdown();
|
|
1790
|
+
});
|
|
1791
|
+
|
|
1792
|
+
process.once("SIGTERM", () => {
|
|
1793
|
+
void shutdown();
|
|
1794
|
+
});
|
|
1795
|
+
|
|
1796
|
+
server.run().catch((error) => {
|
|
1797
|
+
console.error("[mcp-3d-printer-server] startup failed:", error);
|
|
1798
|
+
process.exit(1);
|
|
1799
|
+
});
|