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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PrinterImplementation
|
|
1
|
+
import { PrinterImplementation } from "../types.js";
|
|
2
2
|
import { OctoPrintImplementation } from "./octoprint.js";
|
|
3
3
|
import { KlipperImplementation } from "./klipper.js";
|
|
4
4
|
import { DuetImplementation } from "./duet.js";
|
|
@@ -10,7 +10,6 @@ import axios from "axios";
|
|
|
10
10
|
|
|
11
11
|
export class PrinterFactory {
|
|
12
12
|
private implementations: Map<string, PrinterImplementation> = new Map();
|
|
13
|
-
private bambuPrinterStore = new BambuPrinterStore();
|
|
14
13
|
private apiClient = axios.create({ timeout: 10000 });
|
|
15
14
|
|
|
16
15
|
constructor() {
|
|
@@ -18,7 +17,7 @@ export class PrinterFactory {
|
|
|
18
17
|
this.implementations.set("klipper", new KlipperImplementation(this.apiClient));
|
|
19
18
|
this.implementations.set("duet", new DuetImplementation(this.apiClient));
|
|
20
19
|
this.implementations.set("repetier", new RepetierImplementation(this.apiClient));
|
|
21
|
-
this.implementations.set("bambu", new BambuImplementation(this.apiClient
|
|
20
|
+
this.implementations.set("bambu", new BambuImplementation(this.apiClient));
|
|
22
21
|
this.implementations.set("prusa", new PrusaImplementation(this.apiClient));
|
|
23
22
|
this.implementations.set("creality", new CrealityImplementation(this.apiClient));
|
|
24
23
|
}
|
|
@@ -32,6 +31,10 @@ export class PrinterFactory {
|
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
async disconnectAll(): Promise<void> {
|
|
35
|
-
|
|
34
|
+
// Disconnect all printers if needed
|
|
35
|
+
const bambuImpl = this.implementations.get("bambu") as BambuImplementation;
|
|
36
|
+
if (bambuImpl) {
|
|
37
|
+
await bambuImpl.disconnectAll();
|
|
38
|
+
}
|
|
36
39
|
}
|
|
37
40
|
}
|
package/src/printers/prusa.ts
CHANGED
|
@@ -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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
28
|
-
const response = await this.
|
|
29
|
-
|
|
30
|
-
|
|
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(
|
|
37
|
-
|
|
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.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
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
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
+
}
|