mcp-3d-printer-server 1.0.3 → 1.2.2

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.
@@ -1,109 +1,254 @@
1
+ import { isAxiosError, type AxiosRequestConfig, type AxiosResponse } from "axios";
1
2
  import { PrinterImplementation } from "../types.js";
2
3
  import fs from "fs";
3
4
  import FormData from "form-data";
4
5
 
6
+ type RequestCandidate = {
7
+ route: string;
8
+ data?: unknown;
9
+ };
10
+
5
11
  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
12
+ private buildAuthHeaders(apiKey: string): Record<string, string> {
13
+ return {
14
+ "X-Api-Key": apiKey,
15
+ Authorization: `Bearer ${apiKey}`,
16
+ };
17
+ }
18
+
19
+ private buildBaseUrl(host: string, port: string): string {
20
+ return `http://${host}:${port}`;
21
+ }
22
+
23
+ private isFallbackStatus(error: unknown): boolean {
24
+ if (!isAxiosError(error)) {
25
+ return false;
26
+ }
27
+
28
+ const status = error.response?.status;
29
+ return status === 404 || status === 405 || status === 501;
30
+ }
31
+
32
+ private async getWithFallback(
33
+ host: string,
34
+ port: string,
35
+ apiKey: string,
36
+ routes: string[]
37
+ ): Promise<AxiosResponse> {
38
+ const baseUrl = this.buildBaseUrl(host, port);
39
+ let lastError: unknown;
40
+
41
+ for (const route of routes) {
42
+ try {
43
+ return await this.apiClient.get(`${baseUrl}${route}`, {
44
+ headers: this.buildAuthHeaders(apiKey),
45
+ });
46
+ } catch (error) {
47
+ lastError = error;
48
+ if (!this.isFallbackStatus(error)) {
49
+ throw error;
50
+ }
51
+ }
52
+ }
53
+
54
+ throw lastError ?? new Error("No compatible Prusa GET endpoint found.");
55
+ }
56
+
57
+ private async postWithFallback(
58
+ host: string,
59
+ port: string,
60
+ apiKey: string,
61
+ candidates: RequestCandidate[],
62
+ config?: AxiosRequestConfig
63
+ ): Promise<AxiosResponse> {
64
+ const baseUrl = this.buildBaseUrl(host, port);
65
+ let lastError: unknown;
66
+
67
+ for (const candidate of candidates) {
68
+ try {
69
+ return await this.apiClient.post(`${baseUrl}${candidate.route}`, candidate.data as any, {
70
+ ...(config ?? {}),
71
+ headers: {
72
+ ...this.buildAuthHeaders(apiKey),
73
+ ...(config?.headers ?? {}),
74
+ },
75
+ });
76
+ } catch (error) {
77
+ lastError = error;
78
+ if (!this.isFallbackStatus(error)) {
79
+ throw error;
80
+ }
11
81
  }
12
- });
82
+ }
83
+
84
+ throw lastError ?? new Error("No compatible Prusa POST endpoint found.");
85
+ }
86
+
87
+ async getStatus(host: string, port: string, apiKey: string) {
88
+ const response = await this.getWithFallback(host, port, apiKey, [
89
+ "/api/v1/printer",
90
+ "/api/printer",
91
+ ]);
13
92
  return response.data;
14
93
  }
15
94
 
16
95
  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
- });
96
+ const response = await this.getWithFallback(host, port, apiKey, [
97
+ "/api/v1/storage",
98
+ "/api/files",
99
+ "/api/files/local",
100
+ ]);
23
101
  return response.data;
24
102
  }
25
103
 
26
104
  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
- });
105
+ const encodedFile = encodeURIComponent(filename);
106
+ const response = await this.getWithFallback(host, port, apiKey, [
107
+ `/api/v1/storage/${encodedFile}`,
108
+ `/api/files/local/${encodedFile}`,
109
+ ]);
33
110
  return response.data;
34
111
  }
35
112
 
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
-
113
+ async uploadFile(
114
+ host: string,
115
+ port: string,
116
+ apiKey: string,
117
+ filePath: string,
118
+ filename: string,
119
+ print: boolean
120
+ ) {
39
121
  const formData = new FormData();
40
122
  formData.append("file", fs.createReadStream(filePath));
41
123
  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()
124
+
125
+ const response = await this.postWithFallback(
126
+ host,
127
+ port,
128
+ apiKey,
129
+ [
130
+ { route: "/api/v1/storage", data: formData },
131
+ { route: "/api/files/local", data: formData },
132
+ ],
133
+ {
134
+ headers: {
135
+ ...formData.getHeaders(),
136
+ },
47
137
  }
48
- });
49
-
50
- if (print && response.data.success) {
138
+ );
139
+
140
+ if (print) {
51
141
  await this.startJob(host, port, apiKey, filename);
52
142
  }
53
-
143
+
54
144
  return response.data;
55
145
  }
56
146
 
57
147
  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
-
148
+ const response = await this.postWithFallback(host, port, apiKey, [
149
+ {
150
+ route: "/api/v1/job",
151
+ data: {
152
+ command: "start",
153
+ file: filename,
154
+ },
155
+ },
156
+ {
157
+ route: "/api/v1/job",
158
+ data: {
159
+ command: "start",
160
+ path: filename,
161
+ },
162
+ },
163
+ {
164
+ route: "/api/job",
165
+ data: {
166
+ command: "start",
167
+ file: filename,
168
+ },
169
+ },
170
+ {
171
+ route: "/api/job",
172
+ data: {
173
+ command: "start",
174
+ path: filename,
175
+ },
176
+ },
177
+ ]);
178
+
70
179
  return response.data;
71
180
  }
72
181
 
73
182
  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
-
183
+ const response = await this.postWithFallback(host, port, apiKey, [
184
+ {
185
+ route: "/api/v1/job",
186
+ data: {
187
+ command: "cancel",
188
+ },
189
+ },
190
+ {
191
+ route: "/api/job",
192
+ data: {
193
+ command: "cancel",
194
+ },
195
+ },
196
+ ]);
197
+
85
198
  return response.data;
86
199
  }
87
200
 
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}`);
201
+ async setTemperature(
202
+ host: string,
203
+ port: string,
204
+ apiKey: string,
205
+ component: string,
206
+ temperature: number
207
+ ) {
208
+ const normalized = component.toLowerCase();
209
+
210
+ if (normalized === "bed") {
211
+ const response = await this.postWithFallback(host, port, apiKey, [
212
+ {
213
+ route: "/api/v1/printer/temperature",
214
+ data: {
215
+ command: "set",
216
+ target: { bed: temperature },
217
+ },
218
+ },
219
+ {
220
+ route: "/api/printer/bed",
221
+ data: {
222
+ command: "target",
223
+ target: temperature,
224
+ },
225
+ },
226
+ ]);
227
+
228
+ return response.data;
98
229
  }
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;
230
+
231
+ if (normalized.startsWith("extruder") || normalized === "nozzle" || normalized === "tool0") {
232
+ const response = await this.postWithFallback(host, port, apiKey, [
233
+ {
234
+ route: "/api/v1/printer/temperature",
235
+ data: {
236
+ command: "set",
237
+ target: { tool0: temperature },
238
+ },
239
+ },
240
+ {
241
+ route: "/api/printer/tool",
242
+ data: {
243
+ command: "target",
244
+ targets: { tool0: temperature },
245
+ },
246
+ },
247
+ ]);
248
+
249
+ return response.data;
250
+ }
251
+
252
+ throw new Error(`Unsupported component: ${component}`);
108
253
  }
109
- }
254
+ }