mcp-3d-printer-server 1.2.21 → 1.2.23
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/CHANGELOG.md +12 -0
- package/README.md +1 -1
- package/dist/counterpart/service.js +8 -0
- package/dist/printers/bambu.d.ts +0 -1
- package/dist/printers/bambu.js +4 -15
- package/dist/printers/generic-base.d.ts +0 -1
- package/dist/printers/generic-base.js +3 -10
- package/dist/printers/klipper.d.ts +1 -1
- package/dist/printers/klipper.js +1 -1
- package/dist/printers/prusa.d.ts +2 -2
- package/dist/printers/prusa.js +2 -2
- package/dist/safety/confirmation.d.ts +2 -2
- package/dist/safety/confirmation.js +3 -4
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/counterpart/service.ts +6 -0
- package/src/printers/bambu.ts +4 -16
- package/src/printers/generic-base.ts +3 -10
- package/src/printers/klipper.ts +1 -1
- package/src/printers/prusa.ts +2 -2
- package/src/safety/confirmation.ts +3 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.2.23] – 2026-11-02
|
|
4
|
+
|
|
5
|
+
### Removed
|
|
6
|
+
|
|
7
|
+
- Remove bed-clearance checks. A finished, complete, cancelled or stopped job prints like an idle printer. There is no "remove the previous part and debris" confirmation or forced human prompt, and no "the build plate is clear" line in print confirmations. The printer detects an occupied bed itself during bed leveling. Print confirmations keep their spool and file checks, and `PRINT_REQUIRE_CONFIRMATION=0` now skips every print prompt. Verified with the full suite (570 tests) using mocked printers; no physical print was run.
|
|
8
|
+
|
|
9
|
+
## [1.2.22] – 2026-11-02
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- `camera_snapshot` tries both 127.0.0.1 and ::1 for a `localhost` snapshot URL. Node 18 resolves `localhost` to ::1 first with no IPv4 fallback, so a webcam that listens on IPv4 only was missed. Verified with the OctoPrint Classic Webcam test on Node 18.20.8 and the full suite on Node 22.
|
|
14
|
+
|
|
3
15
|
## [1.2.21] – 2026-11-02
|
|
4
16
|
|
|
5
17
|
### Added
|
package/README.md
CHANGED
|
@@ -876,7 +876,7 @@ For example, `printer://192.168.1.100/status` reads the status of the printer at
|
|
|
876
876
|
7. **Bambu AMS mapping is simple.** `ams_mapping` values are sorted and padded to five entries; real behavior still depends on firmware, loaded filament, and the project's metadata. For AMS inventory and color matching, see [bambu-printer-mcp](https://github.com/DMontgomery40/bambu-printer-mcp).
|
|
877
877
|
8. **Bambu temperatures go through G-code.** Temperature targets are sent as `M104` or `M140` over MQTT, so the printer's firmware and current state decide whether they apply.
|
|
878
878
|
9. **Bambu networking assumes a trusted LAN.** MQTT and FTPS use the printer's self-signed certificate. Uploads use TLS 1.2 with session reuse; confirmation on the X1C firmware reported in [#22](https://github.com/DMontgomery40/mcp-3D-printer-server/issues/22) is still outstanding.
|
|
879
|
-
10. **Safety checks read files and reports, not the physical printer.** A declared material cannot prove what spool is loaded,
|
|
879
|
+
10. **Safety checks read files and reports, not the physical printer.** A declared material cannot prove what spool is loaded, which is why a human confirms. There is no bed check: the printer detects an occupied bed itself during bed leveling. Klipper macro parameters that name a heater are checked, but macro bodies stored on the printer cannot be inspected. On Duet/RepRapFirmware, selecting a tool (`T0`) heats it to the temperature already stored on the printer; activation-only `M568` and `M144` are refused, but a plain tool change is allowed because every firmware uses it. The safety gate is tested with mocked printer transports and loopback HTTP APIs, plus one real Bambu P1S run that reached the firmware's command check; other printer systems have not been exercised on real hardware in this release.
|
|
880
880
|
11. **Remote starts need a download route.** `start_print` of a file already on the printer works on Bambu Lab, OctoPrint, Klipper, and Duet. Repetier, Prusa, and Creality refuse it; upload the local G-code with `print: true` instead.
|
|
881
881
|
|
|
882
882
|
</details>
|
|
@@ -201,6 +201,14 @@ export class CounterpartService {
|
|
|
201
201
|
throw new Error(`Unsupported snapshot URL scheme: ${parsed.protocol}`);
|
|
202
202
|
if (LOOPBACK.has(parsed.hostname) && !LOOPBACK.has(target.host))
|
|
203
203
|
parsed.hostname = target.host;
|
|
204
|
+
// "localhost" can resolve to ::1 first, and Node 18 has no IPv4 fallback for it: try both loopbacks.
|
|
205
|
+
if (parsed.hostname === "localhost") {
|
|
206
|
+
const v4 = new URL(parsed.toString());
|
|
207
|
+
v4.hostname = "127.0.0.1";
|
|
208
|
+
const v6 = new URL(parsed.toString());
|
|
209
|
+
v6.hostname = "[::1]";
|
|
210
|
+
return [v4.toString(), v6.toString()];
|
|
211
|
+
}
|
|
204
212
|
return [parsed.toString()];
|
|
205
213
|
}
|
|
206
214
|
async fetchJpeg(url, target, timeout) {
|
package/dist/printers/bambu.d.ts
CHANGED
|
@@ -29,7 +29,6 @@ export declare class BambuImplementation extends PrinterImplementation {
|
|
|
29
29
|
confirmHardwareAction(message: string, physicalCheck?: boolean): Promise<void>;
|
|
30
30
|
private finishedJobIdentity;
|
|
31
31
|
confirmPrintPreflight(serial: string, status: any, inspection: PrintFileInspection): Promise<string | undefined>;
|
|
32
|
-
assertBedClearance(status: any, confirmedFinishedJob: string | undefined): void;
|
|
33
32
|
private getPrinter;
|
|
34
33
|
/** Safety reads never use the display cache or configured-serial model inference. */
|
|
35
34
|
getSafetyStatus(host: string, serial: string, token: string): Promise<any>;
|
package/dist/printers/bambu.js
CHANGED
|
@@ -121,18 +121,11 @@ export class BambuImplementation extends PrinterImplementation {
|
|
|
121
121
|
const unchipped = unchippedNozzles(status?.raw);
|
|
122
122
|
await this.confirmHardwareAction(`Start a checked print on ${inspection.model.toUpperCase()} (${serial})? Nozzles: ${inspection.nozzleDiameters.join(", ")} mm. ` +
|
|
123
123
|
`Materials: ${inspection.materials.join(", ")}. Peak targets: nozzle ${inspection.maxNozzleTemperature}°C, bed ${inspection.maxBedTemperature}°C, chamber ${inspection.maxChamberTemperature}°C. ` +
|
|
124
|
-
`File SHA-256: ${inspection.sha256}. Confirm the physical spool labels
|
|
125
|
-
(finishedJob !== undefined ? " The printer reports FINISH: remove the previous part and debris before confirming." : "") +
|
|
124
|
+
`File SHA-256: ${inspection.sha256}. Confirm the physical spool labels.` +
|
|
126
125
|
(unchipped.length ? ` Printer ${unchipped.length === 1 ? `nozzle ${unchipped[0]} reports` : `nozzles ${unchipped.join(" and ")} report`} no identity chip, ` +
|
|
127
|
-
`so ${unchipped.length === 1 ? "its" : "their"} diameter and type are the printer's configured settings; confirm they match the installed ${unchipped.length === 1 ? "hotend" : "hotends"}.` : "")
|
|
126
|
+
`so ${unchipped.length === 1 ? "its" : "their"} diameter and type are the printer's configured settings; confirm they match the installed ${unchipped.length === 1 ? "hotend" : "hotends"}.` : ""));
|
|
128
127
|
return finishedJob;
|
|
129
128
|
}
|
|
130
|
-
assertBedClearance(status, confirmedFinishedJob) {
|
|
131
|
-
const current = this.finishedJobIdentity(status);
|
|
132
|
-
if (current !== undefined && current !== confirmedFinishedJob) {
|
|
133
|
-
throw new Error("Printer reports a newly finished job. Confirm that its part and debris have been removed before retrying the print.");
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
129
|
async getPrinter(host, serial, token) {
|
|
137
130
|
return this.printerStore.getPrinter(host, serial, token);
|
|
138
131
|
}
|
|
@@ -276,11 +269,10 @@ export class BambuImplementation extends PrinterImplementation {
|
|
|
276
269
|
const requirements = { ...inspection, amsMapping: checkedMapping, useAMS };
|
|
277
270
|
const initialStatus = await this.getSafetyStatus(host, serial, token);
|
|
278
271
|
validatePrinterState(initialStatus, requirements);
|
|
279
|
-
|
|
272
|
+
await this.confirmPrintPreflight(serial, initialStatus, inspection);
|
|
280
273
|
// Recheck after the human response: the printer may have changed meanwhile.
|
|
281
274
|
const confirmedStatus = await this.getSafetyStatus(host, serial, token);
|
|
282
275
|
validatePrinterState(confirmedStatus, requirements);
|
|
283
|
-
this.assertBedClearance(confirmedStatus, bedClearance);
|
|
284
276
|
assertActive();
|
|
285
277
|
const remoteFileName = uniquePrintName(options.filePath);
|
|
286
278
|
const remoteProjectPath = `cache/${remoteFileName}`;
|
|
@@ -288,7 +280,6 @@ export class BambuImplementation extends PrinterImplementation {
|
|
|
288
280
|
// Uploads can be long. Recheck current state before the command is dispatched.
|
|
289
281
|
const dispatchStatus = await this.getSafetyStatus(host, serial, token);
|
|
290
282
|
validatePrinterState(dispatchStatus, requirements);
|
|
291
|
-
this.assertBedClearance(dispatchStatus, bedClearance);
|
|
292
283
|
const printer = await this.getPrinter(host, serial, token);
|
|
293
284
|
assertActive();
|
|
294
285
|
const projectFileCmd = {
|
|
@@ -554,17 +545,15 @@ export class BambuImplementation extends PrinterImplementation {
|
|
|
554
545
|
}
|
|
555
546
|
const initialStatus = await this.getSafetyStatus(host, serial, token);
|
|
556
547
|
this.validateLoadedGcodeState(initialStatus, inspection);
|
|
557
|
-
|
|
548
|
+
await this.confirmPrintPreflight(serial, initialStatus, inspection);
|
|
558
549
|
const confirmedStatus = await this.getSafetyStatus(host, serial, token);
|
|
559
550
|
this.validateLoadedGcodeState(confirmedStatus, inspection);
|
|
560
|
-
this.assertBedClearance(confirmedStatus, bedClearance);
|
|
561
551
|
assertActive();
|
|
562
552
|
// Start a uniquely named copy of the inspected bytes, never a name that can change.
|
|
563
553
|
const remotePath = path.posix.join(path.posix.dirname(filename), uniquePrintName(filename));
|
|
564
554
|
await this.ftpUpload(host, token, filePath, `/${remotePath}`);
|
|
565
555
|
const dispatchStatus = await this.getSafetyStatus(host, serial, token);
|
|
566
556
|
this.validateLoadedGcodeState(dispatchStatus, inspection);
|
|
567
|
-
this.assertBedClearance(dispatchStatus, bedClearance);
|
|
568
557
|
const printer = await this.getPrinter(host, serial, token);
|
|
569
558
|
assertActive();
|
|
570
559
|
const watcher = this.watchDispatch(printer, host, serial, token, dispatchStatus, assertActive);
|
|
@@ -109,7 +109,6 @@ export declare abstract class GenericPrinterImplementation extends PrinterImplem
|
|
|
109
109
|
/** A private read-only copy named like the remote file; multipart adapters use its basename. */
|
|
110
110
|
private withNamedSnapshot;
|
|
111
111
|
private assertReady;
|
|
112
|
-
private assertBedClearance;
|
|
113
112
|
private printChecked;
|
|
114
113
|
private printPrompt;
|
|
115
114
|
}
|
|
@@ -270,21 +270,15 @@ export class GenericPrinterImplementation extends PrinterImplementation {
|
|
|
270
270
|
`it is not safely idle, so the ${action === "print" ? "print was not started" : "heater was not changed"}.`);
|
|
271
271
|
}
|
|
272
272
|
}
|
|
273
|
-
assertBedClearance(state, confirmedFinishedJob) {
|
|
274
|
-
if (state.finishedJob !== undefined && state.finishedJob !== confirmedFinishedJob) {
|
|
275
|
-
throw new Error("Print safety: the printer reports a newly finished job. Confirm its part and debris were removed before retrying.");
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
273
|
async printChecked(host, port, apiKey, snapshot, remoteName, options, assertActive) {
|
|
279
274
|
const inspection = await inspectGcodeFile(snapshot, { material: options.material });
|
|
280
275
|
assertExpectedPeaks(options.expectedPeaks, { nozzle: inspection.peaks.nozzle, bed: inspection.peaks.bed });
|
|
281
276
|
const initial = await this.readPrinterState(host, port, apiKey);
|
|
282
277
|
this.assertReady(initial, "print");
|
|
283
|
-
await requireHumanConfirmation(this.confirm, this.printPrompt(host, port, remoteName, inspection, initial), "generic"
|
|
278
|
+
await requireHumanConfirmation(this.confirm, this.printPrompt(host, port, remoteName, inspection, initial), "generic");
|
|
284
279
|
assertActive();
|
|
285
280
|
const confirmed = await this.readPrinterState(host, port, apiKey);
|
|
286
281
|
this.assertReady(confirmed, "print");
|
|
287
|
-
this.assertBedClearance(confirmed, initial.finishedJob);
|
|
288
282
|
assertActive();
|
|
289
283
|
const summary = {
|
|
290
284
|
status: "success",
|
|
@@ -319,7 +313,6 @@ export class GenericPrinterImplementation extends PrinterImplementation {
|
|
|
319
313
|
// Uploads can be long. Recheck state before the start command.
|
|
320
314
|
const dispatch = await this.readPrinterState(host, port, apiKey);
|
|
321
315
|
this.assertReady(dispatch, "print");
|
|
322
|
-
this.assertBedClearance(dispatch, initial.finishedJob);
|
|
323
316
|
assertActive();
|
|
324
317
|
const response = await this.rawStartJob(host, port, apiKey, remoteName);
|
|
325
318
|
this.checkedJobs.set(this.operationKey(host, port), checkedJob(remoteName, inspection));
|
|
@@ -330,7 +323,7 @@ export class GenericPrinterImplementation extends PrinterImplementation {
|
|
|
330
323
|
return (`Start a checked print of ${remoteName} on ${this.printerLabel} (${host}:${port})? ` +
|
|
331
324
|
`Materials: ${materials} (${inspection.materialSource}). Peak targets: nozzle ${inspection.peaks.nozzle}°C, bed ${inspection.peaks.bed}°C, chamber ${inspection.peaks.chamber}°C ` +
|
|
332
325
|
`(server ceilings ${inspection.ceilings.nozzle}/${inspection.ceilings.bed}/${inspection.ceilings.chamber}°C). File SHA-256: ${inspection.sha256}. ` +
|
|
333
|
-
"Confirm the loaded filament matches
|
|
334
|
-
|
|
326
|
+
"Confirm the loaded filament matches." +
|
|
327
|
+
"");
|
|
335
328
|
}
|
|
336
329
|
}
|
|
@@ -39,7 +39,7 @@ export declare class KlipperImplementation extends GenericPrinterImplementation
|
|
|
39
39
|
/**
|
|
40
40
|
* Moonraker object query: webhooks.state must be "ready" (not startup,
|
|
41
41
|
* shutdown or error) and print_stats.state standby/complete/cancelled.
|
|
42
|
-
* complete/cancelled
|
|
42
|
+
* complete/cancelled print like standby: the printer detects an occupied bed itself.
|
|
43
43
|
*/
|
|
44
44
|
protected readPrinterState(host: string, port: string, apiKey: string): Promise<GenericPrinterState>;
|
|
45
45
|
protected downloadRemoteFile(host: string, port: string, apiKey: string, filename: string, destination: string): Promise<void>;
|
package/dist/printers/klipper.js
CHANGED
|
@@ -136,7 +136,7 @@ export class KlipperImplementation extends GenericPrinterImplementation {
|
|
|
136
136
|
/**
|
|
137
137
|
* Moonraker object query: webhooks.state must be "ready" (not startup,
|
|
138
138
|
* shutdown or error) and print_stats.state standby/complete/cancelled.
|
|
139
|
-
* complete/cancelled
|
|
139
|
+
* complete/cancelled print like standby: the printer detects an occupied bed itself.
|
|
140
140
|
*/
|
|
141
141
|
async readPrinterState(host, port, apiKey) {
|
|
142
142
|
let data;
|
package/dist/printers/prusa.d.ts
CHANGED
|
@@ -27,8 +27,8 @@ export declare class PrusaImplementation extends GenericPrinterImplementation {
|
|
|
27
27
|
getFile(host: string, port: string, apiKey: string, filename: string): Promise<any>;
|
|
28
28
|
/**
|
|
29
29
|
* PrusaLink /api/v1/status printer.state, falling back to the legacy
|
|
30
|
-
* OctoPrint-compatible /api/printer flags. FINISHED and STOPPED are idle
|
|
31
|
-
*
|
|
30
|
+
* OctoPrint-compatible /api/printer flags. FINISHED and STOPPED are idle; the
|
|
31
|
+
* printer detects an occupied bed itself.
|
|
32
32
|
*/
|
|
33
33
|
protected readPrinterState(host: string, port: string, apiKey: string): Promise<GenericPrinterState>;
|
|
34
34
|
protected rawUploadFile(host: string, port: string, apiKey: string, filePath: string, filename: string, print: boolean): Promise<any>;
|
package/dist/printers/prusa.js
CHANGED
|
@@ -181,8 +181,8 @@ export class PrusaImplementation extends GenericPrinterImplementation {
|
|
|
181
181
|
}
|
|
182
182
|
/**
|
|
183
183
|
* PrusaLink /api/v1/status printer.state, falling back to the legacy
|
|
184
|
-
* OctoPrint-compatible /api/printer flags. FINISHED and STOPPED are idle
|
|
185
|
-
*
|
|
184
|
+
* OctoPrint-compatible /api/printer flags. FINISHED and STOPPED are idle; the
|
|
185
|
+
* printer detects an occupied bed itself.
|
|
186
186
|
*/
|
|
187
187
|
async readPrinterState(host, port, apiKey) {
|
|
188
188
|
let response;
|
|
@@ -16,7 +16,7 @@ export declare function confirmationTimeoutMs(): number;
|
|
|
16
16
|
export declare function confirmationTimedOut(timeoutMs: number): string;
|
|
17
17
|
export declare const CONFIRMATION_UNAVAILABLE: string;
|
|
18
18
|
/**
|
|
19
|
-
* physicalCheck marks confirmations that software cannot replace (for example
|
|
20
|
-
*
|
|
19
|
+
* physicalCheck marks confirmations that software cannot replace (for example
|
|
20
|
+
* clearing hardware errors). Those are required even when prompts are opted out.
|
|
21
21
|
*/
|
|
22
22
|
export declare function requireHumanConfirmation(confirm: ConfirmHardware | undefined, message: string, scope: ConfirmationScope, physicalCheck?: boolean): Promise<void>;
|
|
@@ -28,11 +28,10 @@ export function confirmationTimedOut(timeoutMs) {
|
|
|
28
28
|
"nothing was sent to the printer. Ask again when you are at the printer, or raise PRINT_CONFIRMATION_TIMEOUT_MS.";
|
|
29
29
|
}
|
|
30
30
|
export const CONFIRMATION_UNAVAILABLE = "Human hardware confirmation requires an MCP client with elicitation support. For deliberately headless operation, " +
|
|
31
|
-
"PRINT_REQUIRE_CONFIRMATION=0 (or BAMBU_REQUIRE_CONFIRMATION=0 for Bambu printers) disables ordinary print/heat prompts
|
|
32
|
-
"finished-bed clearance still requires elicitation. No command was sent.";
|
|
31
|
+
"PRINT_REQUIRE_CONFIRMATION=0 (or BAMBU_REQUIRE_CONFIRMATION=0 for Bambu printers) disables ordinary print/heat prompts. No command was sent.";
|
|
33
32
|
/**
|
|
34
|
-
* physicalCheck marks confirmations that software cannot replace (for example
|
|
35
|
-
*
|
|
33
|
+
* physicalCheck marks confirmations that software cannot replace (for example
|
|
34
|
+
* clearing hardware errors). Those are required even when prompts are opted out.
|
|
36
35
|
*/
|
|
37
36
|
export async function requireHumanConfirmation(confirm, message, scope, physicalCheck = false) {
|
|
38
37
|
if (!physicalCheck && confirmationOptOut(scope))
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-3d-printer-server",
|
|
3
3
|
"mcpName": "io.github.DMontgomery40/mcp-3d-printer-server",
|
|
4
|
-
"version": "1.2.
|
|
4
|
+
"version": "1.2.23",
|
|
5
5
|
"description": "MCP server for connecting Claude with 3D printer management systems",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"repository": "https://github.com/DMontgomery40/mcp-3D-printer-server",
|
package/server.json
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
"url": "https://github.com/DMontgomery40/mcp-3D-printer-server",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "1.2.
|
|
10
|
+
"version": "1.2.23",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "mcp-3d-printer-server",
|
|
15
|
-
"version": "1.2.
|
|
15
|
+
"version": "1.2.23",
|
|
16
16
|
"runtimeHint": "npx",
|
|
17
17
|
"transport": {
|
|
18
18
|
"type": "stdio"
|
|
@@ -223,6 +223,12 @@ export class CounterpartService {
|
|
|
223
223
|
}
|
|
224
224
|
if (!["http:", "https:"].includes(parsed.protocol)) throw new Error(`Unsupported snapshot URL scheme: ${parsed.protocol}`);
|
|
225
225
|
if (LOOPBACK.has(parsed.hostname) && !LOOPBACK.has(target.host)) parsed.hostname = target.host;
|
|
226
|
+
// "localhost" can resolve to ::1 first, and Node 18 has no IPv4 fallback for it: try both loopbacks.
|
|
227
|
+
if (parsed.hostname === "localhost") {
|
|
228
|
+
const v4 = new URL(parsed.toString()); v4.hostname = "127.0.0.1";
|
|
229
|
+
const v6 = new URL(parsed.toString()); v6.hostname = "[::1]";
|
|
230
|
+
return [v4.toString(), v6.toString()];
|
|
231
|
+
}
|
|
226
232
|
return [parsed.toString()];
|
|
227
233
|
}
|
|
228
234
|
|
package/src/printers/bambu.ts
CHANGED
|
@@ -189,21 +189,13 @@ export class BambuImplementation extends PrinterImplementation {
|
|
|
189
189
|
await this.confirmHardwareAction(
|
|
190
190
|
`Start a checked print on ${inspection.model.toUpperCase()} (${serial})? Nozzles: ${inspection.nozzleDiameters.join(", ")} mm. ` +
|
|
191
191
|
`Materials: ${inspection.materials.join(", ")}. Peak targets: nozzle ${inspection.maxNozzleTemperature}°C, bed ${inspection.maxBedTemperature}°C, chamber ${inspection.maxChamberTemperature}°C. ` +
|
|
192
|
-
`File SHA-256: ${inspection.sha256}. Confirm the physical spool labels
|
|
193
|
-
(finishedJob !== undefined ? " The printer reports FINISH: remove the previous part and debris before confirming." : "") +
|
|
192
|
+
`File SHA-256: ${inspection.sha256}. Confirm the physical spool labels.` +
|
|
194
193
|
(unchipped.length ? ` Printer ${unchipped.length === 1 ? `nozzle ${unchipped[0]} reports` : `nozzles ${unchipped.join(" and ")} report`} no identity chip, ` +
|
|
195
|
-
`so ${unchipped.length === 1 ? "its" : "their"} diameter and type are the printer's configured settings; confirm they match the installed ${unchipped.length === 1 ? "hotend" : "hotends"}.` : "")
|
|
196
|
-
finishedJob !== undefined
|
|
194
|
+
`so ${unchipped.length === 1 ? "its" : "their"} diameter and type are the printer's configured settings; confirm they match the installed ${unchipped.length === 1 ? "hotend" : "hotends"}.` : "")
|
|
197
195
|
);
|
|
198
196
|
return finishedJob;
|
|
199
197
|
}
|
|
200
198
|
|
|
201
|
-
assertBedClearance(status: any, confirmedFinishedJob: string | undefined): void {
|
|
202
|
-
const current = this.finishedJobIdentity(status);
|
|
203
|
-
if (current !== undefined && current !== confirmedFinishedJob) {
|
|
204
|
-
throw new Error("Printer reports a newly finished job. Confirm that its part and debris have been removed before retrying the print.");
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
199
|
|
|
208
200
|
private async getPrinter(host: string, serial: string, token: string): Promise<BambuClient> {
|
|
209
201
|
return this.printerStore.getPrinter(host, serial, token);
|
|
@@ -378,11 +370,10 @@ export class BambuImplementation extends PrinterImplementation {
|
|
|
378
370
|
const requirements: PrinterStateRequirements = { ...inspection, amsMapping: checkedMapping, useAMS };
|
|
379
371
|
const initialStatus = await this.getSafetyStatus(host, serial, token);
|
|
380
372
|
validatePrinterState(initialStatus, requirements);
|
|
381
|
-
|
|
373
|
+
await this.confirmPrintPreflight(serial, initialStatus, inspection);
|
|
382
374
|
// Recheck after the human response: the printer may have changed meanwhile.
|
|
383
375
|
const confirmedStatus = await this.getSafetyStatus(host, serial, token);
|
|
384
376
|
validatePrinterState(confirmedStatus, requirements);
|
|
385
|
-
this.assertBedClearance(confirmedStatus, bedClearance);
|
|
386
377
|
assertActive();
|
|
387
378
|
|
|
388
379
|
const remoteFileName = uniquePrintName(options.filePath);
|
|
@@ -391,7 +382,6 @@ export class BambuImplementation extends PrinterImplementation {
|
|
|
391
382
|
// Uploads can be long. Recheck current state before the command is dispatched.
|
|
392
383
|
const dispatchStatus = await this.getSafetyStatus(host, serial, token);
|
|
393
384
|
validatePrinterState(dispatchStatus, requirements);
|
|
394
|
-
this.assertBedClearance(dispatchStatus, bedClearance);
|
|
395
385
|
const printer = await this.getPrinter(host, serial, token);
|
|
396
386
|
assertActive();
|
|
397
387
|
|
|
@@ -678,17 +668,15 @@ export class BambuImplementation extends PrinterImplementation {
|
|
|
678
668
|
}
|
|
679
669
|
const initialStatus = await this.getSafetyStatus(host, serial, token);
|
|
680
670
|
this.validateLoadedGcodeState(initialStatus, inspection);
|
|
681
|
-
|
|
671
|
+
await this.confirmPrintPreflight(serial, initialStatus, inspection);
|
|
682
672
|
const confirmedStatus = await this.getSafetyStatus(host, serial, token);
|
|
683
673
|
this.validateLoadedGcodeState(confirmedStatus, inspection);
|
|
684
|
-
this.assertBedClearance(confirmedStatus, bedClearance);
|
|
685
674
|
assertActive();
|
|
686
675
|
// Start a uniquely named copy of the inspected bytes, never a name that can change.
|
|
687
676
|
const remotePath = path.posix.join(path.posix.dirname(filename), uniquePrintName(filename));
|
|
688
677
|
await this.ftpUpload(host, token, filePath, `/${remotePath}`);
|
|
689
678
|
const dispatchStatus = await this.getSafetyStatus(host, serial, token);
|
|
690
679
|
this.validateLoadedGcodeState(dispatchStatus, inspection);
|
|
691
|
-
this.assertBedClearance(dispatchStatus, bedClearance);
|
|
692
680
|
const printer = await this.getPrinter(host, serial, token);
|
|
693
681
|
assertActive();
|
|
694
682
|
const watcher = this.watchDispatch(printer, host, serial, token, dispatchStatus, assertActive);
|
|
@@ -379,11 +379,6 @@ export abstract class GenericPrinterImplementation extends PrinterImplementation
|
|
|
379
379
|
}
|
|
380
380
|
}
|
|
381
381
|
|
|
382
|
-
private assertBedClearance(state: GenericPrinterState, confirmedFinishedJob: string | undefined): void {
|
|
383
|
-
if (state.finishedJob !== undefined && state.finishedJob !== confirmedFinishedJob) {
|
|
384
|
-
throw new Error("Print safety: the printer reports a newly finished job. Confirm its part and debris were removed before retrying.");
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
382
|
|
|
388
383
|
private async printChecked(
|
|
389
384
|
host: string,
|
|
@@ -398,11 +393,10 @@ export abstract class GenericPrinterImplementation extends PrinterImplementation
|
|
|
398
393
|
assertExpectedPeaks(options.expectedPeaks, { nozzle: inspection.peaks.nozzle, bed: inspection.peaks.bed });
|
|
399
394
|
const initial = await this.readPrinterState(host, port, apiKey);
|
|
400
395
|
this.assertReady(initial, "print");
|
|
401
|
-
await requireHumanConfirmation(this.confirm, this.printPrompt(host, port, remoteName, inspection, initial), "generic"
|
|
396
|
+
await requireHumanConfirmation(this.confirm, this.printPrompt(host, port, remoteName, inspection, initial), "generic");
|
|
402
397
|
assertActive();
|
|
403
398
|
const confirmed = await this.readPrinterState(host, port, apiKey);
|
|
404
399
|
this.assertReady(confirmed, "print");
|
|
405
|
-
this.assertBedClearance(confirmed, initial.finishedJob);
|
|
406
400
|
assertActive();
|
|
407
401
|
|
|
408
402
|
const summary = {
|
|
@@ -437,7 +431,6 @@ export abstract class GenericPrinterImplementation extends PrinterImplementation
|
|
|
437
431
|
// Uploads can be long. Recheck state before the start command.
|
|
438
432
|
const dispatch = await this.readPrinterState(host, port, apiKey);
|
|
439
433
|
this.assertReady(dispatch, "print");
|
|
440
|
-
this.assertBedClearance(dispatch, initial.finishedJob);
|
|
441
434
|
assertActive();
|
|
442
435
|
const response = await this.rawStartJob(host, port, apiKey, remoteName);
|
|
443
436
|
this.checkedJobs.set(this.operationKey(host, port), checkedJob(remoteName, inspection));
|
|
@@ -450,8 +443,8 @@ export abstract class GenericPrinterImplementation extends PrinterImplementation
|
|
|
450
443
|
`Start a checked print of ${remoteName} on ${this.printerLabel} (${host}:${port})? ` +
|
|
451
444
|
`Materials: ${materials} (${inspection.materialSource}). Peak targets: nozzle ${inspection.peaks.nozzle}°C, bed ${inspection.peaks.bed}°C, chamber ${inspection.peaks.chamber}°C ` +
|
|
452
445
|
`(server ceilings ${inspection.ceilings.nozzle}/${inspection.ceilings.bed}/${inspection.ceilings.chamber}°C). File SHA-256: ${inspection.sha256}. ` +
|
|
453
|
-
"Confirm the loaded filament matches
|
|
454
|
-
|
|
446
|
+
"Confirm the loaded filament matches." +
|
|
447
|
+
""
|
|
455
448
|
);
|
|
456
449
|
}
|
|
457
450
|
}
|
package/src/printers/klipper.ts
CHANGED
|
@@ -150,7 +150,7 @@ export class KlipperImplementation extends GenericPrinterImplementation {
|
|
|
150
150
|
/**
|
|
151
151
|
* Moonraker object query: webhooks.state must be "ready" (not startup,
|
|
152
152
|
* shutdown or error) and print_stats.state standby/complete/cancelled.
|
|
153
|
-
* complete/cancelled
|
|
153
|
+
* complete/cancelled print like standby: the printer detects an occupied bed itself.
|
|
154
154
|
*/
|
|
155
155
|
protected async readPrinterState(host: string, port: string, apiKey: string): Promise<GenericPrinterState> {
|
|
156
156
|
let data: any;
|
package/src/printers/prusa.ts
CHANGED
|
@@ -213,8 +213,8 @@ export class PrusaImplementation extends GenericPrinterImplementation {
|
|
|
213
213
|
|
|
214
214
|
/**
|
|
215
215
|
* PrusaLink /api/v1/status printer.state, falling back to the legacy
|
|
216
|
-
* OctoPrint-compatible /api/printer flags. FINISHED and STOPPED are idle
|
|
217
|
-
*
|
|
216
|
+
* OctoPrint-compatible /api/printer flags. FINISHED and STOPPED are idle; the
|
|
217
|
+
* printer detects an occupied bed itself.
|
|
218
218
|
*/
|
|
219
219
|
protected async readPrinterState(host: string, port: string, apiKey: string): Promise<GenericPrinterState> {
|
|
220
220
|
let response: AxiosResponse;
|
|
@@ -36,12 +36,11 @@ export function confirmationTimedOut(timeoutMs: number): string {
|
|
|
36
36
|
|
|
37
37
|
export const CONFIRMATION_UNAVAILABLE =
|
|
38
38
|
"Human hardware confirmation requires an MCP client with elicitation support. For deliberately headless operation, " +
|
|
39
|
-
"PRINT_REQUIRE_CONFIRMATION=0 (or BAMBU_REQUIRE_CONFIRMATION=0 for Bambu printers) disables ordinary print/heat prompts
|
|
40
|
-
"finished-bed clearance still requires elicitation. No command was sent.";
|
|
39
|
+
"PRINT_REQUIRE_CONFIRMATION=0 (or BAMBU_REQUIRE_CONFIRMATION=0 for Bambu printers) disables ordinary print/heat prompts. No command was sent.";
|
|
41
40
|
|
|
42
41
|
/**
|
|
43
|
-
* physicalCheck marks confirmations that software cannot replace (for example
|
|
44
|
-
*
|
|
42
|
+
* physicalCheck marks confirmations that software cannot replace (for example
|
|
43
|
+
* clearing hardware errors). Those are required even when prompts are opted out.
|
|
45
44
|
*/
|
|
46
45
|
export async function requireHumanConfirmation(
|
|
47
46
|
confirm: ConfirmHardware | undefined,
|