mcp-3d-printer-server 1.2.25 → 1.2.27

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 (95) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/CONTRIBUTORS.md +5 -1
  3. package/README.md +64 -29
  4. package/dist/ams-mapping.d.ts +38 -0
  5. package/dist/ams-mapping.js +125 -0
  6. package/dist/bambu-network-bridge.d.ts +211 -0
  7. package/dist/bambu-network-bridge.js +918 -0
  8. package/dist/bambu-network-certs.d.ts +13 -0
  9. package/dist/bambu-network-certs.js +152 -0
  10. package/dist/control-reply.d.ts +21 -0
  11. package/dist/control-reply.js +44 -0
  12. package/dist/counterpart/camera-sources.d.ts +16 -0
  13. package/dist/counterpart/camera-sources.js +138 -0
  14. package/dist/counterpart/camera-stream.d.ts +66 -0
  15. package/dist/counterpart/camera-stream.js +246 -0
  16. package/dist/counterpart/camera-tools.d.ts +247 -0
  17. package/dist/counterpart/camera-tools.js +35 -0
  18. package/dist/counterpart/contract.d.ts +21 -5
  19. package/dist/counterpart/contract.js +8 -2
  20. package/dist/counterpart/route.d.ts +31 -0
  21. package/dist/counterpart/route.js +70 -0
  22. package/dist/counterpart/service.d.ts +60 -4
  23. package/dist/counterpart/service.js +142 -16
  24. package/dist/design-guidance.d.ts +26 -0
  25. package/dist/design-guidance.js +177 -0
  26. package/dist/fulu-orca.d.ts +7 -0
  27. package/dist/fulu-orca.js +18 -2
  28. package/dist/index.js +514 -70
  29. package/dist/printers/bambu-filaments.d.ts +68 -0
  30. package/dist/printers/bambu-filaments.js +262 -0
  31. package/dist/printers/bambu.d.ts +99 -6
  32. package/dist/printers/bambu.js +478 -72
  33. package/dist/printers/creality.d.ts +2 -1
  34. package/dist/printers/creality.js +5 -1
  35. package/dist/printers/generic-base.d.ts +11 -6
  36. package/dist/printers/generic-base.js +21 -25
  37. package/dist/printers/klipper.d.ts +2 -1
  38. package/dist/printers/klipper.js +10 -7
  39. package/dist/printers/octoprint.d.ts +2 -1
  40. package/dist/printers/octoprint.js +15 -8
  41. package/dist/printers/printer-factory.d.ts +1 -1
  42. package/dist/printers/printer-factory.js +3 -3
  43. package/dist/printers/prusa.js +0 -1
  44. package/dist/safety/confirmation.d.ts +11 -16
  45. package/dist/safety/confirmation.js +8 -32
  46. package/dist/safety/hidden-hms.d.ts +32 -0
  47. package/dist/safety/hidden-hms.js +405 -0
  48. package/dist/safety/limits.d.ts +8 -2
  49. package/dist/safety/limits.js +14 -5
  50. package/dist/safety/print-file.d.ts +5 -0
  51. package/dist/safety/print-file.js +156 -19
  52. package/dist/safety/printer-state.d.ts +68 -8
  53. package/dist/safety/printer-state.js +187 -40
  54. package/dist/slicer/bambu-cli.d.ts +2 -0
  55. package/dist/slicer/bambu-cli.js +7 -5
  56. package/dist/slicer/layer-pauses.d.ts +47 -0
  57. package/dist/slicer/layer-pauses.js +130 -0
  58. package/dist/stl/stl-manipulator.d.ts +1 -0
  59. package/dist/stl/stl-manipulator.js +40 -0
  60. package/dist/studio-print-options.d.ts +57 -0
  61. package/dist/studio-print-options.js +179 -0
  62. package/dist/tool-annotations.d.ts +25 -0
  63. package/dist/tool-annotations.js +58 -0
  64. package/package.json +27 -4
  65. package/server.json +2 -2
  66. package/src/ams-mapping.ts +132 -0
  67. package/src/bambu-network-bridge.ts +1106 -0
  68. package/src/bambu-network-certs.ts +154 -0
  69. package/src/control-reply.ts +41 -0
  70. package/src/counterpart/camera-sources.ts +102 -0
  71. package/src/counterpart/camera-stream.ts +195 -0
  72. package/src/counterpart/camera-tools.ts +35 -0
  73. package/src/counterpart/contract.ts +12 -4
  74. package/src/counterpart/route.ts +82 -0
  75. package/src/counterpart/service.ts +150 -16
  76. package/src/design-guidance.ts +188 -0
  77. package/src/fulu-orca.ts +18 -2
  78. package/src/index.ts +528 -83
  79. package/src/printers/bambu-filaments.ts +346 -0
  80. package/src/printers/bambu.ts +506 -81
  81. package/src/printers/creality.ts +8 -3
  82. package/src/printers/generic-base.ts +33 -40
  83. package/src/printers/klipper.ts +12 -8
  84. package/src/printers/octoprint.ts +16 -9
  85. package/src/printers/printer-factory.ts +3 -3
  86. package/src/printers/prusa.ts +0 -1
  87. package/src/safety/confirmation.ts +15 -38
  88. package/src/safety/hidden-hms.ts +406 -0
  89. package/src/safety/limits.ts +15 -5
  90. package/src/safety/print-file.ts +139 -17
  91. package/src/safety/printer-state.ts +192 -41
  92. package/src/slicer/bambu-cli.ts +8 -5
  93. package/src/slicer/layer-pauses.ts +143 -0
  94. package/src/stl/stl-manipulator.ts +45 -0
  95. package/src/studio-print-options.ts +201 -0
@@ -1,5 +1,5 @@
1
1
  import type { AxiosInstance } from "axios";
2
- import { GenericPrinterImplementation, type GenericPrinterState, type SnapshotCandidate } from "./generic-base.js";
2
+ import { GenericPrinterImplementation, type GenericPrinterState, type SnapshotCandidate, type CameraCandidate } from "./generic-base.js";
3
3
  import { KlipperImplementation, MACRO_TIMEOUT_MS } from "./klipper.js";
4
4
  import type { ConfirmHardware } from "../safety/confirmation.js";
5
5
  import type { CounterpartProgress, FilamentSlot, PrinterIdentity } from "../counterpart/contract.js";
@@ -73,6 +73,11 @@ export class CrealityImplementation extends GenericPrinterImplementation {
73
73
  return moonraker ? this.moonraker.snapshotCandidates(host, moonraker, "") : [];
74
74
  }
75
75
 
76
+ async cameraCandidates(host: string): Promise<CameraCandidate[]> {
77
+ const moonraker = await this.moonrakerPort(host);
78
+ return moonraker ? this.moonraker.cameraCandidates(host, moonraker, "") : [];
79
+ }
80
+
76
81
  async counterpartFilaments(host: string, port: string, apiKey: string): Promise<FilamentSlot[]> {
77
82
  const moonraker = await this.moonrakerPort(host);
78
83
  return moonraker ? this.moonraker.counterpartFilaments(host, moonraker, "") : super.counterpartFilaments(host, port, apiKey);
@@ -111,7 +116,7 @@ export class CrealityImplementation extends GenericPrinterImplementation {
111
116
  }
112
117
  const state = raw.trim().toLowerCase();
113
118
  if (IDLE_STATES.has(state)) return { state, ready: true };
114
- if (FINISHED_STATES.has(state)) return { state, ready: true, finishedJob: JSON.stringify([state]) };
119
+ if (FINISHED_STATES.has(state)) return { state, ready: true };
115
120
  return { state, ready: false, reason: `state is ${state}` };
116
121
  }
117
122
 
@@ -217,4 +222,4 @@ export class CrealityImplementation extends GenericPrinterImplementation {
217
222
 
218
223
  return response.data;
219
224
  }
220
- }
225
+ }
@@ -26,6 +26,12 @@ export interface SnapshotCandidate {
26
26
  /** True when the firmware's own settings name this URL; false for a conventional fallback. */
27
27
  configured: boolean;
28
28
  }
29
+ export interface CameraCandidate extends SnapshotCandidate {
30
+ transport: "http-mjpeg" | "http-snapshot";
31
+ /** Snapshot belonging to this same camera, never an unrelated later candidate. */
32
+ snapshotUrl?: string;
33
+ unsupportedStream?: boolean;
34
+ }
29
35
 
30
36
  /** Normalized adapter state used by the print and heating preflight. */
31
37
  export interface GenericPrinterState {
@@ -35,8 +41,6 @@ export interface GenericPrinterState {
35
41
  ready: boolean;
36
42
  /** Why the printer is not ready. */
37
43
  reason?: string;
38
- /** Set when the printer explicitly reports a finished or cancelled job that may still be on the bed. */
39
- finishedJob?: string;
40
44
  }
41
45
 
42
46
  export interface HeaterComponent {
@@ -51,8 +55,8 @@ export const REMOTE_DOWNLOAD_TIMEOUT_MS = 120_000;
51
55
  /**
52
56
  * Shared print and heating gate for non-Bambu adapters. Public mutations
53
57
  * validate every input before connecting, inspect the exact bytes that are
54
- * uploaded, check fresh adapter state, ask a human (MCP elicitation), recheck
55
- * state and only then dispatch. Heater-off and cancel are never gated.
58
+ * uploaded, check fresh adapter state, recheck it and only then dispatch.
59
+ * Heater-off and cancel are never gated.
56
60
  */
57
61
  /** seenRunning: a report showed this job printing or paused, so a later ended report is about it, not about the previous job. */
58
62
  type CheckedJob = { name: string; sha256: string; peaks: GenericPrintInspection["peaks"]; materials: string[]; seenRunning: boolean };
@@ -105,6 +109,11 @@ export abstract class GenericPrinterImplementation extends PrinterImplementation
105
109
  return [];
106
110
  }
107
111
 
112
+ /** Existing snapshot discovery is the fallback for cameras without an MJPEG URL. */
113
+ async cameraCandidates(host: string, port: string, apiKey: string): Promise<CameraCandidate[]> {
114
+ return (await this.snapshotCandidates(host, port, apiKey)).map(candidate => ({...candidate, transport: "http-snapshot"}));
115
+ }
116
+
108
117
  /** The single extruder slot; adapters with a spool manager override this. */
109
118
  async counterpartFilaments(_host: string, _port: string, _apiKey: string): Promise<FilamentSlot[]> {
110
119
  return [{ id: "ext", source: "unknown" }];
@@ -135,9 +144,9 @@ export abstract class GenericPrinterImplementation extends PrinterImplementation
135
144
  * Resume reheats and moves the printer, so it is gated like a print start:
136
145
  * only the job this server instance inspected and started, only while the
137
146
  * printer freshly reports that job paused, with its bytes re-read and
138
- * compared where the firmware can download them, and after human
139
- * confirmation (unless the server opted out). A stop or heater-off request
140
- * in the meantime cancels it.
147
+ * compared where the firmware can download them. Only when the bytes cannot
148
+ * be re-read does a person confirm (MCP elicitation). A stop or heater-off
149
+ * request in the meantime cancels it.
141
150
  */
142
151
  async resumeJob(host: string, port: string, apiKey: string): Promise<{ status: string; action: "resume"; message: string; jobName: string; bytesVerified: boolean }> {
143
152
  if (!(await this.supportsPauseResume(host, port, apiKey)) || !this.rawResumeJob) {
@@ -165,21 +174,23 @@ export abstract class GenericPrinterImplementation extends PrinterImplementation
165
174
  bytesVerified = true;
166
175
  }
167
176
  assertActive();
168
- await requireHumanConfirmation(
169
- this.confirm,
170
- `Resume ${checked.name} on ${this.printerLabel} (${host}:${port})? It reheats to its targets (peak nozzle ${checked.peaks.nozzle}°C, ` +
171
- `bed ${checked.peaks.bed}°C, chamber ${checked.peaks.chamber}°C) and moves. Materials: ${checked.materials.length ? checked.materials.join(", ") : "none declared"}. ` +
172
- `File SHA-256: ${checked.sha256}${bytesVerified ? " (re-read from the printer)" : " (this firmware cannot re-read the file; matched by name)"}. ` +
173
- "Confirm the nozzle path is clear and the filament is still loaded.",
174
- "generic"
175
- );
176
- assertActive();
177
- // Someone may have resumed or stopped it at the printer while the prompt was open.
177
+ if (!bytesVerified) {
178
+ // This firmware cannot hand the file back, so only a person can tell it is still the inspected job.
179
+ await requireHumanConfirmation(
180
+ this.confirm,
181
+ `Resume ${checked.name} on ${this.printerLabel} (${host}:${port})? This firmware cannot re-read the file, so it is matched by name only. ` +
182
+ `It reheats to its targets (peak nozzle ${checked.peaks.nozzle}°C, bed ${checked.peaks.bed}°C, chamber ${checked.peaks.chamber}°C) and moves. ` +
183
+ `Materials: ${checked.materials.length ? checked.materials.join(", ") : "none declared"}. Inspected file SHA-256: ${checked.sha256}. ` +
184
+ "Confirm the paused job is that file, the nozzle path is clear and the filament is still loaded."
185
+ );
186
+ assertActive();
187
+ }
188
+ // Someone may have resumed or stopped it at the printer meanwhile (for example while a prompt was open).
178
189
  await this.assertPausedJob(host, port, apiKey, checked.name);
179
- // A progress read during the prompt may have retired this record (the job ended, and a new one
180
- // could now be paused under the same name); only the record that was confirmed may be resumed.
190
+ // A progress read meanwhile may have retired this record (the job ended, and a new one
191
+ // could now be paused under the same name); only the record that was checked may be resumed.
181
192
  if (this.checkedJobs.get(key) !== checked) {
182
- throw new Error(`Print safety: ${checked.name} ended on ${this.printerLabel} while the confirmation was open; nothing was sent.`);
193
+ throw new Error(`Print safety: ${checked.name} ended on ${this.printerLabel} during the resume checks; nothing was sent.`);
183
194
  }
184
195
  assertActive();
185
196
  await this.rawResumeJob!(host, port, apiKey);
@@ -276,12 +287,6 @@ export abstract class GenericPrinterImplementation extends PrinterImplementation
276
287
  validateGenericTemperature(target.heater, value, material ? [material] : undefined);
277
288
  return withPrinterOperation(host, key, async (assertActive) => {
278
289
  this.assertReady(await this.readPrinterState(host, port, apiKey), "heat");
279
- await requireHumanConfirmation(
280
- this.confirm,
281
- `Heat ${target.heater} on ${this.printerLabel} (${host}:${port}) to ${value}°C?` +
282
- (material && target.heater === "nozzle" ? ` Declared material: ${material}. Confirm the loaded filament matches.` : ""),
283
- "generic"
284
- );
285
290
  assertActive();
286
291
  this.assertReady(await this.readPrinterState(host, port, apiKey), "heat");
287
292
  assertActive();
@@ -393,10 +398,9 @@ export abstract class GenericPrinterImplementation extends PrinterImplementation
393
398
  assertExpectedPeaks(options.expectedPeaks, { nozzle: inspection.peaks.nozzle, bed: inspection.peaks.bed });
394
399
  const initial = await this.readPrinterState(host, port, apiKey);
395
400
  this.assertReady(initial, "print");
396
- await requireHumanConfirmation(this.confirm, this.printPrompt(host, port, remoteName, inspection, initial), "generic");
397
401
  assertActive();
398
- const confirmed = await this.readPrinterState(host, port, apiKey);
399
- this.assertReady(confirmed, "print");
402
+ const rechecked = await this.readPrinterState(host, port, apiKey);
403
+ this.assertReady(rechecked, "print");
400
404
  assertActive();
401
405
 
402
406
  const summary = {
@@ -436,15 +440,4 @@ export abstract class GenericPrinterImplementation extends PrinterImplementation
436
440
  this.checkedJobs.set(this.operationKey(host, port), checkedJob(remoteName, inspection));
437
441
  return { ...summary, upload, response };
438
442
  }
439
-
440
- private printPrompt(host: string, port: string, remoteName: string, inspection: GenericPrintInspection, state: GenericPrinterState): string {
441
- const materials = inspection.materials.length ? inspection.materials.join(", ") : "none declared (no positive nozzle target)";
442
- return (
443
- `Start a checked print of ${remoteName} on ${this.printerLabel} (${host}:${port})? ` +
444
- `Materials: ${materials} (${inspection.materialSource}). Peak targets: nozzle ${inspection.peaks.nozzle}°C, bed ${inspection.peaks.bed}°C, chamber ${inspection.peaks.chamber}°C ` +
445
- `(server ceilings ${inspection.ceilings.nozzle}/${inspection.ceilings.bed}/${inspection.ceilings.chamber}°C). File SHA-256: ${inspection.sha256}. ` +
446
- "Confirm the loaded filament matches." +
447
- ""
448
- );
449
- }
450
443
  }
@@ -1,7 +1,7 @@
1
1
  import fs from "fs";
2
2
  import FormData from "form-data";
3
3
  import { isAxiosError } from "axios";
4
- import { GenericPrinterImplementation, type GenericPrinterState, type SnapshotCandidate } from "./generic-base.js";
4
+ import { GenericPrinterImplementation, type GenericPrinterState, type SnapshotCandidate, type CameraCandidate } from "./generic-base.js";
5
5
  import type { CounterpartProgress, FilamentSlot, PrinterIdentity } from "../counterpart/contract.js";
6
6
  import { finiteNumber, nonEmptyText } from "../counterpart/contract.js";
7
7
  import { mapMoonraker } from "../counterpart/progress.js";
@@ -66,11 +66,19 @@ export class KlipperImplementation extends GenericPrinterImplementation {
66
66
  * path relative to the host. https://moonraker.readthedocs.io/en/latest/external_api/webcams/
67
67
  */
68
68
  async snapshotCandidates(host: string, port: string, apiKey: string): Promise<SnapshotCandidate[]> {
69
+ return (await this.cameraCandidates(host, port, apiKey)).filter(candidate => candidate.transport === "http-snapshot");
70
+ }
71
+
72
+ async cameraCandidates(host: string, port: string, apiKey: string): Promise<CameraCandidate[]> {
69
73
  const data = (await this.apiClient.get(`http://${host}:${port}/server/webcams/list`, this.authHeaders(apiKey))).data;
70
74
  const webcams = Array.isArray(data?.result?.webcams) ? data.result.webcams : [];
71
75
  return webcams
72
- .filter((webcam: any) => webcam?.enabled !== false && nonEmptyText(webcam?.snapshot_url))
73
- .map((webcam: any) => ({ url: String(webcam.snapshot_url).trim(), source: "moonraker-webcam", configured: true }));
76
+ .filter((webcam: any) => webcam?.enabled !== false)
77
+ .flatMap((webcam: any) => [
78
+ ...(nonEmptyText(webcam?.stream_url)
79
+ ? [{url: String(webcam.stream_url).trim(), snapshotUrl: nonEmptyText(webcam?.snapshot_url), unsupportedStream: /webrtc|hls/i.test(String(webcam?.service ?? "")), transport: "http-mjpeg" as const}] : []),
80
+ ...(nonEmptyText(webcam?.snapshot_url) ? [{url: String(webcam.snapshot_url).trim(), transport: "http-snapshot" as const}] : []),
81
+ ].map(candidate => ({...candidate, source: "moonraker-webcam", configured: true})));
74
82
  }
75
83
 
76
84
  /**
@@ -175,11 +183,7 @@ export class KlipperImplementation extends GenericPrinterImplementation {
175
183
  if (!["standby", "complete", "cancelled"].includes(job)) {
176
184
  return { state, ready: false, reason: `print_stats.state is ${job}` };
177
185
  }
178
- return {
179
- state,
180
- ready: true,
181
- finishedJob: job === "standby" ? undefined : JSON.stringify([job, status?.print_stats?.filename ?? null]),
182
- };
186
+ return { state, ready: true };
183
187
  }
184
188
 
185
189
  protected async downloadRemoteFile(host: string, port: string, apiKey: string, filename: string, destination: string): Promise<void> {
@@ -1,7 +1,7 @@
1
1
  import fs from "fs";
2
2
  import FormData from "form-data";
3
3
  import { isAxiosError } from "axios";
4
- import { GenericPrinterImplementation, type GenericPrinterState, type HeaterComponent, type SnapshotCandidate } from "./generic-base.js";
4
+ import { GenericPrinterImplementation, type GenericPrinterState, type HeaterComponent, type SnapshotCandidate, type CameraCandidate } from "./generic-base.js";
5
5
  import type { CounterpartProgress, PrinterIdentity } from "../counterpart/contract.js";
6
6
  import { nonEmptyText } from "../counterpart/contract.js";
7
7
  import { mapOctoPrint } from "../counterpart/progress.js";
@@ -55,20 +55,27 @@ export class OctoPrintImplementation extends GenericPrinterImplementation {
55
55
  * src/octoprint/plugins/classicwebcam/__init__.py; checked on 1.8.7 and 1.11.2.
56
56
  */
57
57
  async snapshotCandidates(host: string, port: string, apiKey: string): Promise<SnapshotCandidate[]> {
58
+ return (await this.cameraCandidates(host, port, apiKey)).filter(candidate => candidate.transport === "http-snapshot");
59
+ }
60
+
61
+ async cameraCandidates(host: string, port: string, apiKey: string): Promise<CameraCandidate[]> {
58
62
  const settings = (await this.apiClient.get(`http://${host}:${port}/api/settings`, { headers: { "X-Api-Key": apiKey } })).data;
59
- const candidates: SnapshotCandidate[] = [];
63
+ const candidates: CameraCandidate[] = [];
60
64
  if (settings?.webcam?.webcamEnabled !== false) {
65
+ // The settings API maps webcam.stream to streamUrl. Classic Webcam on
66
+ // 1.9+ exposes its own stream field; only configured URLs are used.
61
67
  const configured = [
62
- settings?.plugins?.classicwebcam?.snapshot,
63
- settings?.webcam?.snapshotUrl,
64
- ...(Array.isArray(settings?.webcam?.webcams) ? settings.webcam.webcams.map((webcam: any) => webcam?.compat?.snapshot) : []),
68
+ settings?.plugins?.classicwebcam,
69
+ {stream: settings?.webcam?.streamUrl, snapshot: settings?.webcam?.snapshotUrl},
70
+ ...(Array.isArray(settings?.webcam?.webcams) ? settings.webcam.webcams.map((webcam: any) => webcam?.compat) : []),
65
71
  ];
66
- for (const url of configured) {
67
- const text = nonEmptyText(url);
68
- if (text) candidates.push({ url: text, source: "octoprint-settings", configured: true });
72
+ for (const camera of configured) {
73
+ const stream = nonEmptyText(camera?.stream), snapshot = nonEmptyText(camera?.snapshot);
74
+ if (stream) candidates.push({url: stream, snapshotUrl: snapshot, source: "octoprint-settings", configured: true, transport: "http-mjpeg"});
75
+ if (snapshot) candidates.push({url: snapshot, source: "octoprint-settings", configured: true, transport: "http-snapshot"});
69
76
  }
70
77
  }
71
- candidates.push({ url: `http://${host}/webcam/?action=snapshot`, source: "octoprint-fallback", configured: false });
78
+ candidates.push({ url: `http://${host}/webcam/?action=snapshot`, source: "octoprint-fallback", configured: false, transport: "http-snapshot" });
72
79
  return candidates;
73
80
  }
74
81
 
@@ -3,7 +3,7 @@ import { OctoPrintImplementation } from "./octoprint.js";
3
3
  import { KlipperImplementation } from "./klipper.js";
4
4
  import { DuetImplementation } from "./duet.js";
5
5
  import { RepetierImplementation } from "./repetier.js";
6
- import { BambuImplementation } from "./bambu.js";
6
+ import { BambuImplementation, defaultResumeStateDir } from "./bambu.js";
7
7
  import { PrusaImplementation } from "./prusa.js";
8
8
  import { CrealityImplementation } from "./creality.js";
9
9
  import type { ConfirmHardware } from "../safety/confirmation.js";
@@ -21,13 +21,13 @@ export class PrinterFactory {
21
21
  private implementations: Map<string, PrinterImplementation> = new Map();
22
22
  private apiClient = axios.create({ timeout: 10000 });
23
23
 
24
- /** confirm is the human (MCP elicitation) confirmation used before prints and positive heating. */
24
+ /** confirm is the human (MCP elicitation) confirmation for physical checks software cannot make, such as resuming a job that cannot be re-read. */
25
25
  constructor(confirm?: ConfirmHardware) {
26
26
  this.implementations.set("octoprint", new OctoPrintImplementation(this.apiClient, confirm));
27
27
  this.implementations.set("klipper", new KlipperImplementation(this.apiClient, confirm));
28
28
  this.implementations.set("duet", new DuetImplementation(this.apiClient, confirm));
29
29
  this.implementations.set("repetier", new RepetierImplementation(this.apiClient, confirm));
30
- this.implementations.set("bambu", new BambuImplementation(this.apiClient, confirm));
30
+ this.implementations.set("bambu", new BambuImplementation(this.apiClient, confirm, { resumeStateDir: defaultResumeStateDir() }));
31
31
  this.implementations.set("prusa", new PrusaImplementation(this.apiClient, confirm));
32
32
  this.implementations.set("creality", new CrealityImplementation(this.apiClient, confirm));
33
33
  }
@@ -232,7 +232,6 @@ export class PrusaImplementation extends GenericPrinterImplementation {
232
232
  state,
233
233
  ready,
234
234
  reason: ready ? undefined : `printer.state is ${state}`,
235
- finishedJob: ready && (state === "FINISHED" || state === "STOPPED") ? JSON.stringify([state, data?.job?.id ?? null]) : undefined,
236
235
  };
237
236
  }
238
237
  const flags = data?.state?.flags;
@@ -1,26 +1,15 @@
1
- /** Human confirmation for physical printer mutations (print start, positive heating). */
2
- export type ConfirmHardware = (message: string) => Promise<boolean>;
3
- export type ConfirmationScope = "bambu" | "generic";
4
-
5
1
  /**
6
- * Ordinary print/heat prompts are on by default. Only an explicit "0" opts out.
7
- * PRINT_REQUIRE_CONFIRMATION applies to every printer type; the older
8
- * BAMBU_REQUIRE_CONFIRMATION applies to Bambu printers only. If any variable
9
- * relevant to the scope is set to another value, confirmation stays required.
2
+ * Human confirmation through MCP elicitation. Ordinary print starts, positive
3
+ * heating and resumes of a re-read job do not ask: the client's own action is
4
+ * the approval. Only a check that software cannot make asks a person, such as
5
+ * resuming a job whose file the printer cannot hand back for re-reading.
10
6
  */
11
- export function confirmationOptOut(scope: ConfirmationScope): boolean {
12
- const names = scope === "bambu"
13
- ? ["BAMBU_REQUIRE_CONFIRMATION", "PRINT_REQUIRE_CONFIRMATION"]
14
- : ["PRINT_REQUIRE_CONFIRMATION"];
15
- const values = names
16
- .map((name) => process.env[name]?.trim())
17
- .filter((value): value is string => value !== undefined && value !== "");
18
- return values.length > 0 && values.every((value) => value === "0");
19
- }
7
+ export type ConfirmHardware = (message: string) => Promise<boolean>;
20
8
 
21
9
  /**
22
- * How long a human has to answer a confirmation prompt. The MCP SDK's default
23
- * request timeout (60 s) is too short for someone walking to the printer.
10
+ * How long a person has to answer a prompt from the server (a resume of a job
11
+ * that could not be re-read, or the Bambu printer-model prompt). The MCP SDK's
12
+ * default request timeout (60 s) is too short for someone walking to the printer.
24
13
  */
25
14
  export function confirmationTimeoutMs(): number {
26
15
  const raw = process.env.PRINT_CONFIRMATION_TIMEOUT_MS?.trim();
@@ -35,27 +24,15 @@ export function confirmationTimedOut(timeoutMs: number): string {
35
24
  }
36
25
 
37
26
  export const CONFIRMATION_UNAVAILABLE =
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. No command was sent.";
27
+ "This step needs a person to confirm a check at the printer, which requires an MCP client with elicitation support. No command was sent.";
40
28
 
41
- /**
42
- * physicalCheck marks confirmations that software cannot replace (for example
43
- * clearing hardware errors). Those are required even when prompts are opted out.
44
- */
45
- export async function requireHumanConfirmation(
46
- confirm: ConfirmHardware | undefined,
47
- message: string,
48
- scope: ConfirmationScope,
49
- physicalCheck = false
50
- ): Promise<void> {
51
- if (!physicalCheck && confirmationOptOut(scope)) return;
29
+ /** Ask a person to accept a physical check that software cannot replace. */
30
+ export async function requireHumanConfirmation(confirm: ConfirmHardware | undefined, message: string): Promise<void> {
52
31
  if (!confirm) throw new Error(CONFIRMATION_UNAVAILABLE);
53
32
  if (!(await confirm(message))) {
54
- throw new Error("Hardware safety confirmation was declined or cancelled in the MCP client. No command was sent. " + (physicalCheck
55
- ? "This confirmation covers a physical check at the printer and always needs a person to accept it."
56
- : "Some clients decline prompts automatically, for example in a non-interactive permission mode. A deliberately headless " +
57
- `setup that approves jobs another way can set ${scope === "bambu"
58
- ? "PRINT_REQUIRE_CONFIRMATION=0 (or BAMBU_REQUIRE_CONFIRMATION=0)"
59
- : "PRINT_REQUIRE_CONFIRMATION=0"} to skip ordinary print and heating prompts.`));
33
+ throw new Error(
34
+ "Hardware safety confirmation was declined or cancelled in the MCP client. No command was sent. " +
35
+ "This confirmation covers a physical check at the printer and always needs a person to accept it."
36
+ );
60
37
  }
61
38
  }