mcp-3d-printer-server 0.1.0

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.
@@ -0,0 +1,82 @@
1
+ import { PrinterImplementation } from "../types.js";
2
+ import fs from "fs";
3
+ import FormData from "form-data";
4
+
5
+ export class DuetImplementation extends PrinterImplementation {
6
+ async getStatus(host: string, port: string, apiKey: string) {
7
+ const url = `http://${host}:${port}/machine/status`;
8
+ const response = await this.apiClient.get(url);
9
+ return response.data;
10
+ }
11
+
12
+ async getFiles(host: string, port: string, apiKey: string) {
13
+ const url = `http://${host}:${port}/machine/file-list`;
14
+ const response = await this.apiClient.get(url);
15
+ return response.data;
16
+ }
17
+
18
+ async getFile(host: string, port: string, apiKey: string, filename: string) {
19
+ const url = `http://${host}:${port}/machine/file/${encodeURIComponent(filename)}`;
20
+ const response = await this.apiClient.get(url);
21
+ return response.data;
22
+ }
23
+
24
+ async uploadFile(host: string, port: string, apiKey: string, filePath: string, filename: string, print: boolean) {
25
+ const url = `http://${host}:${port}/machine/file-upload`;
26
+
27
+ const formData = new FormData();
28
+ formData.append("file", fs.createReadStream(filePath));
29
+ formData.append("filename", filename);
30
+
31
+ const response = await this.apiClient.post(url, formData as any, {
32
+ headers: {
33
+ ...formData.getHeaders()
34
+ }
35
+ });
36
+
37
+ if (print && response.data.err === 0) {
38
+ await this.startJob(host, port, apiKey, filename);
39
+ }
40
+
41
+ return response.data;
42
+ }
43
+
44
+ async startJob(host: string, port: string, apiKey: string, filename: string) {
45
+ const url = `http://${host}:${port}/machine/code`;
46
+
47
+ const response = await this.apiClient.post(url, {
48
+ code: `M32 "${filename}"`
49
+ } as any);
50
+
51
+ return response.data;
52
+ }
53
+
54
+ async cancelJob(host: string, port: string, apiKey: string) {
55
+ const url = `http://${host}:${port}/machine/code`;
56
+
57
+ const response = await this.apiClient.post(url, {
58
+ code: "M0"
59
+ } as any);
60
+
61
+ return response.data;
62
+ }
63
+
64
+ async setTemperature(host: string, port: string, apiKey: string, component: string, temperature: number) {
65
+ const url = `http://${host}:${port}/machine/code`;
66
+
67
+ let gcode;
68
+ if (component === "bed") {
69
+ gcode = `M140 S${temperature}`;
70
+ } else if (component === "extruder") {
71
+ gcode = `M104 S${temperature}`;
72
+ } else {
73
+ throw new Error(`Unsupported component: ${component}`);
74
+ }
75
+
76
+ const response = await this.apiClient.post(url, {
77
+ code: gcode
78
+ } as any);
79
+
80
+ return response.data;
81
+ }
82
+ }
@@ -0,0 +1,78 @@
1
+ import { PrinterImplementation } from "../types.js";
2
+ import fs from "fs";
3
+ import FormData from "form-data";
4
+
5
+ export class KlipperImplementation extends PrinterImplementation {
6
+ async getStatus(host: string, port: string, apiKey: string) {
7
+ const url = `http://${host}:${port}/printer/info`;
8
+ const response = await this.apiClient.get(url);
9
+ return response.data;
10
+ }
11
+
12
+ async getFiles(host: string, port: string, apiKey: string) {
13
+ const url = `http://${host}:${port}/server/files/list`;
14
+ const response = await this.apiClient.get(url);
15
+ return response.data;
16
+ }
17
+
18
+ async getFile(host: string, port: string, apiKey: string, filename: string) {
19
+ const url = `http://${host}:${port}/server/files/metadata?filename=${encodeURIComponent(filename)}`;
20
+ const response = await this.apiClient.get(url);
21
+ return response.data;
22
+ }
23
+
24
+ async uploadFile(host: string, port: string, apiKey: string, filePath: string, filename: string, print: boolean) {
25
+ const url = `http://${host}:${port}/server/files/upload`;
26
+
27
+ const formData = new FormData();
28
+ formData.append("file", fs.createReadStream(filePath));
29
+ formData.append("filename", filename);
30
+
31
+ const response = await this.apiClient.post(url, formData as any, {
32
+ headers: {
33
+ ...formData.getHeaders()
34
+ }
35
+ });
36
+
37
+ if (print && response.data.result === "success") {
38
+ await this.startJob(host, port, apiKey, filename);
39
+ }
40
+
41
+ return response.data;
42
+ }
43
+
44
+ async startJob(host: string, port: string, apiKey: string, filename: string) {
45
+ const url = `http://${host}:${port}/printer/print/start`;
46
+
47
+ const response = await this.apiClient.post(url, { filename } as any);
48
+
49
+ return response.data;
50
+ }
51
+
52
+ async cancelJob(host: string, port: string, apiKey: string) {
53
+ const url = `http://${host}:${port}/printer/print/cancel`;
54
+
55
+ const response = await this.apiClient.post(url, null as any);
56
+
57
+ return response.data;
58
+ }
59
+
60
+ async setTemperature(host: string, port: string, apiKey: string, component: string, temperature: number) {
61
+ const url = `http://${host}:${port}/printer/gcode/script`;
62
+
63
+ let gcode;
64
+ if (component === "bed") {
65
+ gcode = `SET_HEATER_TEMPERATURE HEATER=heater_bed TARGET=${temperature}`;
66
+ } else if (component === "extruder") {
67
+ gcode = `SET_HEATER_TEMPERATURE HEATER=extruder TARGET=${temperature}`;
68
+ } else {
69
+ throw new Error(`Unsupported component: ${component}`);
70
+ }
71
+
72
+ const response = await this.apiClient.post(url, {
73
+ script: gcode
74
+ } as any);
75
+
76
+ return response.data;
77
+ }
78
+ }
@@ -0,0 +1,113 @@
1
+ import { PrinterImplementation } from "../types.js";
2
+ import fs from "fs";
3
+ import FormData from "form-data";
4
+
5
+ export class OctoPrintImplementation extends PrinterImplementation {
6
+ async getStatus(host: string, port: string, apiKey: string) {
7
+ const url = `http://${host}:${port}/api/printer`;
8
+ const response = await this.apiClient.get(url, {
9
+ headers: {
10
+ "X-Api-Key": apiKey
11
+ }
12
+ });
13
+ return response.data;
14
+ }
15
+
16
+ async getFiles(host: string, port: string, apiKey: string) {
17
+ const url = `http://${host}:${port}/api/files`;
18
+ const response = await this.apiClient.get(url, {
19
+ headers: {
20
+ "X-Api-Key": apiKey
21
+ }
22
+ });
23
+ return response.data;
24
+ }
25
+
26
+ async getFile(host: string, port: string, apiKey: string, filename: string) {
27
+ const url = `http://${host}:${port}/api/files/local/${filename}`;
28
+ const response = await this.apiClient.get(url, {
29
+ headers: {
30
+ "X-Api-Key": apiKey
31
+ }
32
+ });
33
+ return response.data;
34
+ }
35
+
36
+ async uploadFile(host: string, port: string, apiKey: string, filePath: string, filename: string, print: boolean) {
37
+ const url = `http://${host}:${port}/api/files/local`;
38
+
39
+ const formData = new FormData();
40
+ formData.append("file", fs.createReadStream(filePath));
41
+ formData.append("filename", filename);
42
+
43
+ if (print) {
44
+ formData.append("print", "true");
45
+ }
46
+
47
+ const response = await this.apiClient.post(url, formData as any, {
48
+ headers: {
49
+ "X-Api-Key": apiKey,
50
+ ...formData.getHeaders()
51
+ }
52
+ });
53
+
54
+ return response.data;
55
+ }
56
+
57
+ async startJob(host: string, port: string, apiKey: string, filename: string) {
58
+ const url = `http://${host}:${port}/api/files/local/${filename}`;
59
+
60
+ const response = await this.apiClient.post(url, {
61
+ command: "select",
62
+ print: true
63
+ } as any, {
64
+ headers: {
65
+ "X-Api-Key": apiKey,
66
+ "Content-Type": "application/json"
67
+ }
68
+ });
69
+
70
+ return response.data;
71
+ }
72
+
73
+ async cancelJob(host: string, port: string, apiKey: string) {
74
+ const url = `http://${host}:${port}/api/job`;
75
+
76
+ const response = await this.apiClient.post(url, {
77
+ command: "cancel"
78
+ } as any, {
79
+ headers: {
80
+ "X-Api-Key": apiKey,
81
+ "Content-Type": "application/json"
82
+ }
83
+ });
84
+
85
+ return response.data;
86
+ }
87
+
88
+ async setTemperature(host: string, port: string, apiKey: string, component: string, temperature: number) {
89
+ let url = `http://${host}:${port}/api/printer/tool`;
90
+
91
+ const data: Record<string, any> = {};
92
+ if (component === "bed") {
93
+ data.command = "target";
94
+ data.target = temperature;
95
+ url = `http://${host}:${port}/api/printer/bed`;
96
+ } else if (component.startsWith("extruder")) {
97
+ data.command = "target";
98
+ data.targets = {};
99
+ data.targets[component] = temperature;
100
+ } else {
101
+ throw new Error(`Unsupported component: ${component}`);
102
+ }
103
+
104
+ const response = await this.apiClient.post(url, data as any, {
105
+ headers: {
106
+ "X-Api-Key": apiKey,
107
+ "Content-Type": "application/json"
108
+ }
109
+ });
110
+
111
+ return response.data;
112
+ }
113
+ }
@@ -0,0 +1,37 @@
1
+ import { PrinterImplementation, BambuPrinterStore } from "../types.js";
2
+ import { OctoPrintImplementation } from "./octoprint.js";
3
+ import { KlipperImplementation } from "./klipper.js";
4
+ import { DuetImplementation } from "./duet.js";
5
+ import { RepetierImplementation } from "./repetier.js";
6
+ import { BambuImplementation } from "./bambu.js";
7
+ import { PrusaImplementation } from "./prusa.js";
8
+ import { CrealityImplementation } from "./creality.js";
9
+ import axios from "axios";
10
+
11
+ export class PrinterFactory {
12
+ private implementations: Map<string, PrinterImplementation> = new Map();
13
+ private bambuPrinterStore = new BambuPrinterStore();
14
+ private apiClient = axios.create({ timeout: 10000 });
15
+
16
+ constructor() {
17
+ this.implementations.set("octoprint", new OctoPrintImplementation(this.apiClient));
18
+ this.implementations.set("klipper", new KlipperImplementation(this.apiClient));
19
+ this.implementations.set("duet", new DuetImplementation(this.apiClient));
20
+ this.implementations.set("repetier", new RepetierImplementation(this.apiClient));
21
+ this.implementations.set("bambu", new BambuImplementation(this.apiClient, this.bambuPrinterStore));
22
+ this.implementations.set("prusa", new PrusaImplementation(this.apiClient));
23
+ this.implementations.set("creality", new CrealityImplementation(this.apiClient));
24
+ }
25
+
26
+ getImplementation(type: string): PrinterImplementation {
27
+ const implementation = this.implementations.get(type.toLowerCase());
28
+ if (!implementation) {
29
+ throw new Error(`Unsupported printer type: ${type}`);
30
+ }
31
+ return implementation;
32
+ }
33
+
34
+ async disconnectAll(): Promise<void> {
35
+ await this.bambuPrinterStore.disconnectAll();
36
+ }
37
+ }
@@ -0,0 +1,109 @@
1
+ import { PrinterImplementation } from "../types.js";
2
+ import fs from "fs";
3
+ import FormData from "form-data";
4
+
5
+ export class PrusaImplementation extends PrinterImplementation {
6
+ async getStatus(host: string, port: string, apiKey: string) {
7
+ const url = `http://${host}:${port}/api/v1/printer`;
8
+ const response = await this.apiClient.get(url, {
9
+ headers: {
10
+ "X-Api-Key": apiKey
11
+ }
12
+ });
13
+ return response.data;
14
+ }
15
+
16
+ async getFiles(host: string, port: string, apiKey: string) {
17
+ const url = `http://${host}:${port}/api/v1/storage`;
18
+ const response = await this.apiClient.get(url, {
19
+ headers: {
20
+ "X-Api-Key": apiKey
21
+ }
22
+ });
23
+ return response.data;
24
+ }
25
+
26
+ async getFile(host: string, port: string, apiKey: string, filename: string) {
27
+ const url = `http://${host}:${port}/api/v1/storage/${encodeURIComponent(filename)}`;
28
+ const response = await this.apiClient.get(url, {
29
+ headers: {
30
+ "X-Api-Key": apiKey
31
+ }
32
+ });
33
+ return response.data;
34
+ }
35
+
36
+ async uploadFile(host: string, port: string, apiKey: string, filePath: string, filename: string, print: boolean) {
37
+ const url = `http://${host}:${port}/api/v1/storage`;
38
+
39
+ const formData = new FormData();
40
+ formData.append("file", fs.createReadStream(filePath));
41
+ formData.append("filename", filename);
42
+
43
+ const response = await this.apiClient.post(url, formData as any, {
44
+ headers: {
45
+ "X-Api-Key": apiKey,
46
+ ...formData.getHeaders()
47
+ }
48
+ });
49
+
50
+ if (print && response.data.success) {
51
+ await this.startJob(host, port, apiKey, filename);
52
+ }
53
+
54
+ return response.data;
55
+ }
56
+
57
+ async startJob(host: string, port: string, apiKey: string, filename: string) {
58
+ const url = `http://${host}:${port}/api/v1/job`;
59
+
60
+ const response = await this.apiClient.post(url, {
61
+ command: "start",
62
+ file: filename
63
+ } as any, {
64
+ headers: {
65
+ "X-Api-Key": apiKey,
66
+ "Content-Type": "application/json"
67
+ }
68
+ });
69
+
70
+ return response.data;
71
+ }
72
+
73
+ async cancelJob(host: string, port: string, apiKey: string) {
74
+ const url = `http://${host}:${port}/api/v1/job`;
75
+
76
+ const response = await this.apiClient.post(url, {
77
+ command: "cancel"
78
+ } as any, {
79
+ headers: {
80
+ "X-Api-Key": apiKey,
81
+ "Content-Type": "application/json"
82
+ }
83
+ });
84
+
85
+ return response.data;
86
+ }
87
+
88
+ async setTemperature(host: string, port: string, apiKey: string, component: string, temperature: number) {
89
+ const url = `http://${host}:${port}/api/v1/printer/temperature`;
90
+
91
+ let data: Record<string, any> = { command: "set" };
92
+ if (component === "bed") {
93
+ data.target = { bed: temperature };
94
+ } else if (component.startsWith("extruder")) {
95
+ data.target = { tool0: temperature }; // Prusa typically uses tool0 for the extruder
96
+ } else {
97
+ throw new Error(`Unsupported component: ${component}`);
98
+ }
99
+
100
+ const response = await this.apiClient.post(url, data as any, {
101
+ headers: {
102
+ "X-Api-Key": apiKey,
103
+ "Content-Type": "application/json"
104
+ }
105
+ });
106
+
107
+ return response.data;
108
+ }
109
+ }
@@ -0,0 +1,68 @@
1
+ import { PrinterImplementation } from "../types.js";
2
+ import fs from "fs";
3
+ import FormData from "form-data";
4
+
5
+ export class RepetierImplementation extends PrinterImplementation {
6
+ async getStatus(host: string, port: string, apiKey: string) {
7
+ const url = `http://${host}:${port}/printer/api/?a=getPrinterInfo&apikey=${apiKey}`;
8
+ const response = await this.apiClient.get(url);
9
+ return response.data;
10
+ }
11
+
12
+ async getFiles(host: string, port: string, apiKey: string) {
13
+ const url = `http://${host}:${port}/printer/api/?a=ls&apikey=${apiKey}`;
14
+ const response = await this.apiClient.get(url);
15
+ return response.data;
16
+ }
17
+
18
+ async getFile(host: string, port: string, apiKey: string, filename: string) {
19
+ const url = `http://${host}:${port}/printer/api/?a=getFileInfo&apikey=${apiKey}&filename=${encodeURIComponent(filename)}`;
20
+ const response = await this.apiClient.get(url);
21
+ return response.data;
22
+ }
23
+
24
+ async uploadFile(host: string, port: string, apiKey: string, filePath: string, filename: string, print: boolean) {
25
+ const url = `http://${host}:${port}/printer/api/`;
26
+
27
+ const formData = new FormData();
28
+ formData.append("a", "upload");
29
+ formData.append("apikey", apiKey);
30
+ formData.append("filename", filename);
31
+ formData.append("print", print ? "1" : "0");
32
+ formData.append("file", fs.createReadStream(filePath));
33
+
34
+ const response = await this.apiClient.post(url, formData as any, {
35
+ headers: {
36
+ ...formData.getHeaders()
37
+ }
38
+ });
39
+
40
+ return response.data;
41
+ }
42
+
43
+ async startJob(host: string, port: string, apiKey: string, filename: string) {
44
+ const url = `http://${host}:${port}/printer/api/?a=startJob&apikey=${apiKey}&filename=${encodeURIComponent(filename)}`;
45
+ const response = await this.apiClient.get(url);
46
+ return response.data;
47
+ }
48
+
49
+ async cancelJob(host: string, port: string, apiKey: string) {
50
+ const url = `http://${host}:${port}/printer/api/?a=stopJob&apikey=${apiKey}`;
51
+ const response = await this.apiClient.get(url);
52
+ return response.data;
53
+ }
54
+
55
+ async setTemperature(host: string, port: string, apiKey: string, component: string, temperature: number) {
56
+ let url;
57
+ if (component === "bed") {
58
+ url = `http://${host}:${port}/printer/api/?a=setBedTemp&apikey=${apiKey}&temp=${temperature}`;
59
+ } else if (component === "extruder") {
60
+ url = `http://${host}:${port}/printer/api/?a=setExtruderTemp&apikey=${apiKey}&temp=${temperature}`;
61
+ } else {
62
+ throw new Error(`Unsupported component: ${component}`);
63
+ }
64
+
65
+ const response = await this.apiClient.get(url);
66
+ return response.data;
67
+ }
68
+ }
package/src/types.ts ADDED
@@ -0,0 +1,46 @@
1
+ import { AxiosInstance } from "axios";
2
+ import { BambuPrinter } from "bambu-js";
3
+
4
+ // Define shared types for the printer implementations
5
+ export type BambuFTP = {
6
+ readDir: (path: string) => Promise<string[]>;
7
+ sendFile: (sourcePath: string, destinationPath: string, progressCallback?: (progress: number) => void) => Promise<void>;
8
+ removeFile: (path: string) => Promise<void>;
9
+ };
10
+
11
+ // Base class for printer implementations
12
+ export abstract class PrinterImplementation {
13
+ protected apiClient: AxiosInstance;
14
+
15
+ constructor(apiClient: AxiosInstance) {
16
+ this.apiClient = apiClient;
17
+ }
18
+
19
+ abstract getStatus(host: string, port: string, apiKey: string): Promise<any>;
20
+ abstract getFiles(host: string, port: string, apiKey: string): Promise<any>;
21
+ abstract getFile(host: string, port: string, apiKey: string, filename: string): Promise<any>;
22
+ abstract uploadFile(host: string, port: string, apiKey: string, filePath: string, filename: string, print: boolean): Promise<any>;
23
+ abstract startJob(host: string, port: string, apiKey: string, filename: string): Promise<any>;
24
+ abstract cancelJob(host: string, port: string, apiKey: string): Promise<any>;
25
+ abstract setTemperature(host: string, port: string, apiKey: string, component: string, temperature: number): Promise<any>;
26
+ }
27
+
28
+ // Store for Bambu printers
29
+ export class BambuPrinterStore {
30
+ private printers: Map<string, InstanceType<typeof BambuPrinter>> = new Map();
31
+
32
+ get(host: string, serial: string, token: string): InstanceType<typeof BambuPrinter> {
33
+ const key = `${host}-${serial}`;
34
+ if (!this.printers.has(key)) {
35
+ const printer = new BambuPrinter(host, serial, token);
36
+ this.printers.set(key, printer);
37
+ }
38
+ return this.printers.get(key)!;
39
+ }
40
+
41
+ async disconnectAll(): Promise<void> {
42
+ for (const printer of this.printers.values()) {
43
+ await printer.disconnect();
44
+ }
45
+ }
46
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "esModuleInterop": true,
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "strict": true,
10
+ "declaration": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true
13
+ },
14
+ "include": ["src/**/*"],
15
+ "exclude": ["node_modules", "dist"]
16
+ }