mcp-3d-printer-server 0.2.3 → 1.0.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.
Files changed (41) hide show
  1. package/README.md +267 -29
  2. package/dist/3mf_parser.d.ts +3 -0
  3. package/dist/3mf_parser.js +216 -0
  4. package/dist/index.d.ts +2 -0
  5. package/dist/index.js +1018 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/printers/bambu.d.ts +42 -0
  8. package/dist/printers/bambu.js +278 -0
  9. package/dist/printers/creality.d.ts +10 -0
  10. package/dist/printers/creality.js +90 -0
  11. package/dist/printers/duet.d.ts +10 -0
  12. package/dist/printers/duet.js +66 -0
  13. package/dist/printers/klipper.d.ts +10 -0
  14. package/dist/printers/klipper.js +62 -0
  15. package/dist/printers/octoprint.d.ts +10 -0
  16. package/dist/printers/octoprint.js +97 -0
  17. package/dist/printers/printer-factory.d.ts +9 -0
  18. package/dist/printers/printer-factory.js +33 -0
  19. package/dist/printers/prusa.d.ts +10 -0
  20. package/dist/printers/prusa.js +93 -0
  21. package/dist/printers/repetier.d.ts +10 -0
  22. package/dist/printers/repetier.js +59 -0
  23. package/dist/stl/stl-manipulator.d.ts +140 -0
  24. package/dist/stl/stl-manipulator.js +1081 -0
  25. package/dist/test/stl-manipulator-test.d.ts +2 -0
  26. package/dist/test/stl-manipulator-test.js +91 -0
  27. package/dist/types.d.ts +75 -0
  28. package/dist/types.js +26 -0
  29. package/package.json +14 -4
  30. package/src/3mf_parser.ts +223 -0
  31. package/src/index.ts +239 -10
  32. package/src/printers/bambu.ts +305 -100
  33. package/src/stl/stl-manipulator.ts +350 -117
  34. package/src/test/3DBenchy.3mf +0 -0
  35. package/src/test/stl-manipulator-test.js +114 -0
  36. package/src/test/stl-manipulator-test.ts +124 -0
  37. package/src/test/tsconfig.json +13 -0
  38. package/src/types.ts +65 -0
  39. package/.env.example +0 -17
  40. package/scripts/mcp-inspector.js +0 -49
  41. package/tsconfig.json +0 -16
@@ -1,140 +1,345 @@
1
1
  import { PrinterImplementation, BambuPrinterStore, BambuFTP } from "../types.js";
2
- import { BambuPrinter } from "bambu-js";
2
+ import { BambuPrinter } from "bambu-js"; // Keep for FTP manipulation
3
+ import * as mqtt from 'mqtt'; // Import MQTT library
4
+ import * as fs from 'fs/promises';
5
+ import * as path from 'path';
6
+ import * as tls from 'tls'; // Needed for TLS connection
7
+
8
+ // Define interface for MQTT client store if needed
9
+ interface MqttClientStore {
10
+ [key: string]: mqtt.MqttClient;
11
+ }
12
+
13
+ // Define interface for print options matching OpenBambuAPI spec
14
+ interface BambuPrintCommandOptions {
15
+ sequence_id: string;
16
+ command: "project_file";
17
+ param: string; // Path on printer, e.g., gcodes/filename.3mf
18
+ project_name: string; // Name of the project (often derived from filename)
19
+ project_id?: string; // Default "0" for local
20
+ profile_id?: string; // Default "0"
21
+ task_id?: string; // Default "0"
22
+ subtask_id?: string; // Default "0"
23
+ subtask_name?: string; // Default ""
24
+ url?: string; // Optional: file:///mnt/sdcard/gcodes/filename.3mf (can override param?)
25
+ md5?: string; // MD5 hash of the file (optional but recommended)
26
+ timelapse?: boolean;
27
+ bed_type?: string; // Default "auto"
28
+ bed_levelling?: boolean;
29
+ flow_cali?: boolean;
30
+ vibration_cali?: boolean;
31
+ layer_inspect?: boolean;
32
+ ams_mapping?: number[]; // Array of AMS slot indices (e.g., [0, 1, 2, 3]) - Needs verification how this maps to filaments
33
+ use_ams?: boolean;
34
+ plate_idx?: number; // Add missing property
35
+ }
36
+
37
+ // Define interface for print options including overrides passed from index.ts
38
+ interface BambuPrintOptionsInternal {
39
+ projectName: string;
40
+ filePath: string; // Path to the local .3mf file
41
+ // Base options from OpenBambuAPI spec
42
+ useAMS?: boolean;
43
+ plateIndex?: number;
44
+ bedLeveling?: boolean;
45
+ flowCalibration?: boolean;
46
+ vibrationCalibration?: boolean;
47
+ layerInspect?: boolean;
48
+ timelapse?: boolean;
49
+ // Overrides / parsed data
50
+ amsMapping?: number[]; // Explicit mapping (from user override or parsed 3MF)
51
+ md5?: string; // Optional MD5 hash of the file
52
+ }
3
53
 
4
54
  export class BambuImplementation extends PrinterImplementation {
5
55
  private bambuPrinterStore: BambuPrinterStore;
56
+ private mqttClients: MqttClientStore = {}; // Store connected MQTT clients
57
+ private mqttConnectionPromises: { [key: string]: Promise<mqtt.MqttClient> } = {}; // Track connection attempts
6
58
 
7
59
  constructor(apiClient: any, bambuPrinterStore: BambuPrinterStore) {
8
60
  super(apiClient);
9
61
  this.bambuPrinterStore = bambuPrinterStore;
10
62
  }
11
63
 
12
- private getBambuPrinter(host: string, serial: string, token: string): InstanceType<typeof BambuPrinter> {
64
+ // Get BambuPrinter instance for FTP
65
+ private getBambuPrinterForFTP(host: string, serial: string, token: string): InstanceType<typeof BambuPrinter> {
66
+ // This still uses bambu-js just for the FTP part
13
67
  return this.bambuPrinterStore.get(host, serial, token);
14
68
  }
15
69
 
16
- async getStatus(host: string, port: string, apiKey: string) {
17
- // Extracting Bambu-specific parameters from apiKey
18
- // Format should be "serial:token"
70
+ // Get or establish MQTT connection
71
+ private async getMqttClient(host: string, serial: string, token: string): Promise<mqtt.MqttClient> {
72
+ const clientKey = `${host}-${serial}`;
73
+
74
+ if (this.mqttClients[clientKey] && this.mqttClients[clientKey].connected) {
75
+ return this.mqttClients[clientKey];
76
+ }
77
+
78
+ // If a connection attempt is already in progress, wait for it
79
+ if (this.mqttConnectionPromises[clientKey] !== undefined) {
80
+ console.log(`Waiting for existing MQTT connection attempt to ${clientKey}...`);
81
+ return this.mqttConnectionPromises[clientKey];
82
+ }
83
+
84
+ console.log(`Attempting to establish new MQTT connection to ${host} for ${serial}...`);
85
+ const connectPromise = new Promise<mqtt.MqttClient>((resolve, reject) => {
86
+ // Use port 8883 and TLS
87
+ const mqttOptions: mqtt.IClientOptions = {
88
+ port: 8883,
89
+ host: host,
90
+ protocol: 'mqtts',
91
+ username: 'bblp', // As per OpenBambuAPI docs
92
+ password: token,
93
+ clientId: `mcp_client_${serial}_${Date.now()}`,
94
+ rejectUnauthorized: false, // Might be needed for self-signed certs? Test carefully.
95
+ // ca: [fs.readFileSync('path/to/ca_cert.pem')], // Add CA cert if needed
96
+ connectTimeout: 10000, // 10 seconds
97
+ reconnectPeriod: 5000 // Try reconnecting every 5 seconds
98
+ };
99
+
100
+ const client = mqtt.connect(mqttOptions);
101
+
102
+ client.on('connect', () => {
103
+ console.log(`MQTT connected successfully to ${host} for ${serial}`);
104
+ this.mqttClients[clientKey] = client;
105
+ // Subscribe to necessary topics (e.g., responses/status)
106
+ client.subscribe(`device/${serial}/report`, (err) => {
107
+ if (err) {
108
+ console.error(`Failed to subscribe to report topic for ${serial}:`, err);
109
+ }
110
+ });
111
+ delete this.mqttConnectionPromises[clientKey]; // Clear promise tracker on success
112
+ resolve(client);
113
+ });
114
+
115
+ client.on('error', (err) => {
116
+ console.error(`MQTT connection error for ${serial} on ${host}:`, err);
117
+ delete this.mqttClients[clientKey]; // Remove potentially bad client
118
+ delete this.mqttConnectionPromises[clientKey]; // Clear promise tracker on error
119
+ reject(err);
120
+ });
121
+
122
+ client.on('close', () => {
123
+ console.log(`MQTT connection closed for ${serial} on ${host}`);
124
+ delete this.mqttClients[clientKey]; // Remove client on close
125
+ });
126
+
127
+ // Handle status messages from the report topic
128
+ client.on('message', (topic, message) => {
129
+ try {
130
+ const data = JSON.parse(message.toString());
131
+ // Processing logic for status updates would go here
132
+ // For now, we just acknowledge receiving it (optional logging)
133
+ // console.log(`MQTT message received on ${topic}:`, data?.print?.subtask_name ?? data?.print?.command ?? 'Unknown message type');
134
+ } catch(e) {
135
+ console.error('Failed to parse MQTT message:', e);
136
+ }
137
+ });
138
+ });
139
+
140
+ this.mqttConnectionPromises[clientKey] = connectPromise; // Store the promise
141
+ return connectPromise;
142
+ }
143
+
144
+ // Disconnect MQTT clients on shutdown
145
+ async disconnectAllMqtt(): Promise<void> {
146
+ console.log("Disconnecting all MQTT clients...");
147
+ for (const key in this.mqttClients) {
148
+ const client = this.mqttClients[key];
149
+ if (client) {
150
+ await new Promise<void>((resolve, reject) => {
151
+ client.end(false, {}, (err) => {
152
+ if (err) {
153
+ console.error(`Error ending MQTT client ${key}:`, err);
154
+ reject(err);
155
+ } else {
156
+ console.log(`MQTT client ${key} disconnected.`);
157
+ resolve();
158
+ }
159
+ });
160
+ });
161
+ }
162
+ }
163
+ this.mqttClients = {};
164
+ this.mqttConnectionPromises = {};
165
+ }
166
+
167
+ // --- getStatus (Limited Implementation) ---
168
+ async getStatus(host: string, port: string, apiKey: string): Promise<any> {
19
169
  const [serial, token] = this.extractBambuCredentials(apiKey);
170
+ try {
171
+ // Ensure MQTT connection is attempted/established
172
+ const client = await this.getMqttClient(host, serial, token);
173
+ // Return minimal status indicating connection success
174
+ // Full status requires implementing state storage based on /report topic messages
175
+ return {
176
+ status: client.connected ? "connected" : "connecting",
177
+ serial: serial,
178
+ note: "Full status details require MQTT state tracking implementation."
179
+ };
180
+ } catch (error) {
181
+ console.error(`Failed to get MQTT client/status for ${serial}:`, error);
182
+ return { status: "error", error: (error as Error).message };
183
+ }
184
+ }
185
+
186
+ // --- Refactored print3mf using direct MQTT ---
187
+ async print3mf(host: string, serial: string, token: string, options: BambuPrintOptionsInternal): Promise<any> {
188
+
189
+ // Still use bambu-js for FTP upload
190
+ const printerFTP = this.getBambuPrinterForFTP(host, serial, token);
191
+ const remoteFilename = path.basename(options.filePath);
192
+ // Ensure path uses forward slashes for printer consistency
193
+ const remotePath = `gcodes/${remoteFilename}`.replace(/\\/g, '/');
194
+
195
+ // 1. Connect FTP if necessary (via bambu-js helper)
196
+ if (!printerFTP.isConnected) {
197
+ console.log(`Connecting Bambu FTP ${host} (${serial}) before uploading...`);
198
+ await printerFTP.connect(); // Connects MQTT and FTP
199
+ // await printerFTP.awaitInitialState(10000); // Might not be needed just for FTP
200
+ console.log(`Connected Bambu FTP.`);
201
+ }
202
+
203
+ // 2. Upload the file via FTP
204
+ console.log(`Uploading ${options.filePath} to ${remotePath} via FTP...`);
205
+ try {
206
+ await printerFTP.manipulateFiles(async (context: BambuFTP) => {
207
+ await context.sendFile(options.filePath, remotePath);
208
+ });
209
+ console.log(`File ${remoteFilename} uploaded successfully.`);
210
+ } catch (uploadError) {
211
+ console.error(`FTP Upload Error:`, uploadError);
212
+ throw new Error(`Failed to upload 3MF file via FTP: ${(uploadError as Error).message}`);
213
+ }
20
214
 
21
- const printer = this.getBambuPrinter(host, serial, token);
215
+ // 3. Get MQTT Client
216
+ const mqttClient = await this.getMqttClient(host, serial, token);
217
+
218
+ // 4. Construct the MQTT Print Command Payload based on OpenBambuAPI
219
+ const commandPayload: BambuPrintCommandOptions = {
220
+ sequence_id: Date.now().toString(),
221
+ command: "project_file",
222
+ param: remotePath, // Path on SD card
223
+ project_name: options.projectName,
224
+ use_ams: options.amsMapping && options.amsMapping.length > 0 ? true : (options.useAMS ?? false),
225
+ plate_idx: options.plateIndex ?? 0,
226
+ bed_levelling: options.bedLeveling ?? true,
227
+ flow_cali: options.flowCalibration ?? false,
228
+ vibration_cali: options.vibrationCalibration ?? false,
229
+ layer_inspect: options.layerInspect ?? false,
230
+ timelapse: options.timelapse ?? false,
231
+ ams_mapping: options.amsMapping,
232
+ md5: options.md5
233
+ };
22
234
 
23
- // Connect if not already connected
24
- if (!printer.isConnected) {
25
- await printer.connect();
26
- // Wait for initial state
27
- await printer.awaitInitialState(10000); // 10 second timeout
235
+ // Remove ams_mapping if not provided or empty
236
+ if (!commandPayload.ams_mapping || commandPayload.ams_mapping.length === 0) {
237
+ delete commandPayload.ams_mapping;
238
+ // Also ensure use_ams is false if no mapping exists
239
+ commandPayload.use_ams = false;
28
240
  }
29
-
30
- return printer.getState();
241
+ // Remove md5 if not provided
242
+ if (!commandPayload.md5) {
243
+ delete commandPayload.md5;
244
+ }
245
+
246
+ // 5. Publish the command
247
+ return this.publishMqttCommand(mqttClient, serial, 'print', commandPayload);
31
248
  }
32
249
 
250
+ // --- Refactored cancelJob using MQTT ---
251
+ async cancelJob(host: string, port: string, apiKey: string): Promise<any> {
252
+ const [serial, token] = this.extractBambuCredentials(apiKey);
253
+ const mqttClient = await this.getMqttClient(host, serial, token);
254
+ // Command structure from OpenBambuAPI (assuming 'stop_print' is correct)
255
+ const payload = { sequence_id: Date.now().toString(), command: "stop_print" };
256
+ return this.publishMqttCommand(mqttClient, serial, 'print', payload);
257
+ }
258
+
259
+ // --- setTemperature (still not directly possible via MQTT command) ---
260
+ async setTemperature(host: string, port: string, apiKey: string, component: string, temperature: number) {
261
+ console.warn("Setting temperatures directly is not supported via known Bambu MQTT commands. Use G-code.");
262
+ // We could potentially implement a tool to send custom G-code?
263
+ throw new Error("Direct temperature setting via MQTT is not supported.");
264
+ }
265
+
266
+ // --- Other methods (getFiles, getFile, uploadFile, startJob) need review/refactoring ---
267
+ // For now, keep using bambu-js FTP for file ops, but MQTT for commands
33
268
  async getFiles(host: string, port: string, apiKey: string) {
34
- // Extracting Bambu-specific parameters
35
269
  const [serial, token] = this.extractBambuCredentials(apiKey);
36
-
37
- const printer = this.getBambuPrinter(host, serial, token);
38
-
39
- // Connect if not already connected
40
- if (!printer.isConnected) {
41
- await printer.connect();
42
- }
43
-
44
- // Using the manipulateFiles API to list files
270
+ const printerFTP = this.getBambuPrinterForFTP(host, serial, token);
271
+ if (!printerFTP.isConnected) await printerFTP.connect();
45
272
  const fileList: string[] = [];
46
- await printer.manipulateFiles(async (context: BambuFTP) => {
47
- const files = await context.readDir("gcodes");
48
- fileList.push(...files);
273
+ await printerFTP.manipulateFiles(async (context: BambuFTP) => {
274
+ const files = await context.readDir("gcodes");
275
+ fileList.push(...files);
49
276
  });
50
-
51
277
  return { files: fileList };
52
278
  }
53
-
279
+
54
280
  async getFile(host: string, port: string, apiKey: string, filename: string) {
55
- // Extracting Bambu-specific parameters
281
+ // Still relies on checking existence via FTP
56
282
  const [serial, token] = this.extractBambuCredentials(apiKey);
57
-
58
- // Bambu doesn't have a direct API to get file content
59
- // Instead, this returns metadata about the file by confirming it exists
60
- const printer = this.getBambuPrinter(host, serial, token);
61
-
62
- // Connect if not already connected
63
- if (!printer.isConnected) {
64
- await printer.connect();
65
- }
66
-
283
+ const printerFTP = this.getBambuPrinterForFTP(host, serial, token);
284
+ if (!printerFTP.isConnected) await printerFTP.connect();
67
285
  let fileExists = false;
68
- await printer.manipulateFiles(async (context: BambuFTP) => {
69
- const files = await context.readDir("gcodes");
70
- fileExists = files.includes(filename);
286
+ await printerFTP.manipulateFiles(async (context: BambuFTP) => {
287
+ const files = await context.readDir("gcodes");
288
+ fileExists = files.includes(filename);
71
289
  });
72
-
73
- if (!fileExists) {
74
- throw new Error(`File not found: ${filename}`);
75
- }
76
-
290
+ if (!fileExists) throw new Error(`File not found: ${filename}`);
77
291
  return { name: filename, exists: true };
78
292
  }
79
-
293
+
80
294
  async uploadFile(host: string, port: string, apiKey: string, filePath: string, filename: string, print: boolean) {
81
- // Extracting Bambu-specific parameters
82
- const [serial, token] = this.extractBambuCredentials(apiKey);
83
-
84
- const printer = this.getBambuPrinter(host, serial, token);
85
-
86
- // Connect if not already connected
87
- if (!printer.isConnected) {
88
- await printer.connect();
89
- }
90
-
91
- // Upload file via FTP
92
- await printer.manipulateFiles(async (context: BambuFTP) => {
93
- await context.sendFile(filePath, `gcodes/${filename}`);
94
- });
95
-
96
- if (print) {
97
- // To start a print directly, we would need more info
98
- // This is a placeholder - starting a print needs more details
99
- throw new Error("Direct printing of uploaded files is not implemented yet");
100
- }
101
-
102
- return { status: "success", message: `File ${filename} uploaded successfully` };
295
+ // This method just uploads now, doesn't handle printing
296
+ const [serial, token] = this.extractBambuCredentials(apiKey);
297
+ const printerFTP = this.getBambuPrinterForFTP(host, serial, token);
298
+ const remotePath = `gcodes/${filename}`.replace(/\\/g, '/');
299
+ if (!printerFTP.isConnected) await printerFTP.connect();
300
+ console.log(`Uploading ${filePath} to ${remotePath} via FTP...`);
301
+ try {
302
+ await printerFTP.manipulateFiles(async (context: BambuFTP) => {
303
+ await context.sendFile(filePath, remotePath);
304
+ });
305
+ console.log(`File ${filename} uploaded successfully.`);
306
+ if (print) {
307
+ console.warn("'print=true' ignored in uploadFile for Bambu. Use print_3mf tool instead.");
308
+ }
309
+ return { status: "success", message: `File ${filename} uploaded successfully` };
310
+ } catch (uploadError) {
311
+ console.error(`FTP Upload Error:`, uploadError);
312
+ throw new Error(`Failed to upload file via FTP: ${(uploadError as Error).message}`);
313
+ }
103
314
  }
104
-
315
+
105
316
  async startJob(host: string, port: string, apiKey: string, filename: string) {
106
- // Extracting Bambu-specific parameters
107
- const [serial, token] = this.extractBambuCredentials(apiKey);
108
-
109
- // Starting a job requires more information for Bambu printers
110
- // This is a simplified implementation - in reality, we need
111
- // more details about the 3MF project file structure
112
- throw new Error("Starting a print job on Bambu printers requires more information about the file structure");
317
+ // This should likely be deprecated or only work for G-code files via a different MQTT command?
318
+ console.warn("startJob is not recommended for Bambu .3mf files. Use print_3mf tool.");
319
+ throw new Error("startJob is not implemented for Bambu printers using MQTT commands yet.");
113
320
  }
114
321
 
115
- async cancelJob(host: string, port: string, apiKey: string) {
116
- // Extracting Bambu-specific parameters
117
- const [serial, token] = this.extractBambuCredentials(apiKey);
118
-
119
- const printer = this.getBambuPrinter(host, serial, token);
120
-
121
- // Connect if not already connected
122
- if (!printer.isConnected) {
123
- await printer.connect();
124
- }
125
-
126
- // Cancel print
127
- printer.stop();
128
-
129
- return { status: "success", message: "Print job cancelled" };
130
- }
131
-
132
- async setTemperature(host: string, port: string, apiKey: string, component: string, temperature: number) {
133
- // Bambu API doesn't have direct temperature controls
134
- // We would need to send custom G-code commands
135
- throw new Error("Setting temperatures directly is not supported via the Bambu API");
322
+ // --- Helper to publish MQTT commands ---
323
+ private async publishMqttCommand(client: mqtt.MqttClient, serial: string, commandType: string, payload: any): Promise<any> {
324
+ const topic = `device/${serial}/request`;
325
+ const message = JSON.stringify({ [commandType]: payload });
326
+
327
+ console.log(`Publishing MQTT to ${topic}: ${message}`);
328
+
329
+ return new Promise((resolve, reject) => {
330
+ client.publish(topic, message, { qos: 1 }, (err) => {
331
+ if (err) {
332
+ console.error(`MQTT Publish Error to ${topic}:`, err);
333
+ reject(new Error(`Failed to publish MQTT command: ${err.message}`));
334
+ } else {
335
+ console.log(`MQTT command published successfully to ${topic}`);
336
+ // Note: We don't wait for a response here. Status updates come via /report topic.
337
+ resolve({ status: "success", message: "Command sent successfully via MQTT." });
338
+ }
339
+ });
340
+ });
136
341
  }
137
-
342
+
138
343
  // Helper method to extract Bambu-specific credentials from apiKey
139
344
  private extractBambuCredentials(apiKey: string): [string, string] {
140
345
  const parts = apiKey.split(':');