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 type { ConfirmHardware } from "../safety/confirmation.js";
4
4
  import type { CounterpartProgress, FilamentSlot, PrinterIdentity } from "../counterpart/contract.js";
5
5
  /**
@@ -27,6 +27,7 @@ export declare class CrealityImplementation extends GenericPrinterImplementation
27
27
  reportsLayers(host: string): Promise<boolean>;
28
28
  supportsPauseResume(host: string): Promise<boolean>;
29
29
  snapshotCandidates(host: string): Promise<SnapshotCandidate[]>;
30
+ cameraCandidates(host: string): Promise<CameraCandidate[]>;
30
31
  counterpartFilaments(host: string, port: string, apiKey: string): Promise<FilamentSlot[]>;
31
32
  counterpartIdentity(host: string): Promise<PrinterIdentity>;
32
33
  /** Pause and resume only through a detected Moonraker; the device API has no verified route. */
@@ -65,6 +65,10 @@ export class CrealityImplementation extends GenericPrinterImplementation {
65
65
  const moonraker = await this.moonrakerPort(host);
66
66
  return moonraker ? this.moonraker.snapshotCandidates(host, moonraker, "") : [];
67
67
  }
68
+ async cameraCandidates(host) {
69
+ const moonraker = await this.moonrakerPort(host);
70
+ return moonraker ? this.moonraker.cameraCandidates(host, moonraker, "") : [];
71
+ }
68
72
  async counterpartFilaments(host, port, apiKey) {
69
73
  const moonraker = await this.moonrakerPort(host);
70
74
  return moonraker ? this.moonraker.counterpartFilaments(host, moonraker, "") : super.counterpartFilaments(host, port, apiKey);
@@ -104,7 +108,7 @@ export class CrealityImplementation extends GenericPrinterImplementation {
104
108
  if (IDLE_STATES.has(state))
105
109
  return { state, ready: true };
106
110
  if (FINISHED_STATES.has(state))
107
- return { state, ready: true, finishedJob: JSON.stringify([state]) };
111
+ return { state, ready: true };
108
112
  return { state, ready: false, reason: `state is ${state}` };
109
113
  }
110
114
  async getStatus(host, port, apiKey) {
@@ -11,6 +11,12 @@ export interface SnapshotCandidate {
11
11
  /** True when the firmware's own settings name this URL; false for a conventional fallback. */
12
12
  configured: boolean;
13
13
  }
14
+ export interface CameraCandidate extends SnapshotCandidate {
15
+ transport: "http-mjpeg" | "http-snapshot";
16
+ /** Snapshot belonging to this same camera, never an unrelated later candidate. */
17
+ snapshotUrl?: string;
18
+ unsupportedStream?: boolean;
19
+ }
14
20
  /** Normalized adapter state used by the print and heating preflight. */
15
21
  export interface GenericPrinterState {
16
22
  /** Adapter-reported state text. */
@@ -19,8 +25,6 @@ export interface GenericPrinterState {
19
25
  ready: boolean;
20
26
  /** Why the printer is not ready. */
21
27
  reason?: string;
22
- /** Set when the printer explicitly reports a finished or cancelled job that may still be on the bed. */
23
- finishedJob?: string;
24
28
  }
25
29
  export interface HeaterComponent {
26
30
  heater: Heater;
@@ -49,6 +53,8 @@ export declare abstract class GenericPrinterImplementation extends PrinterImplem
49
53
  counterpartIdentity(_host: string, _port: string, _apiKey: string): Promise<PrinterIdentity>;
50
54
  /** Snapshot URLs from the firmware's own settings, in preference order. */
51
55
  snapshotCandidates(_host: string, _port: string, _apiKey: string): Promise<SnapshotCandidate[]>;
56
+ /** Existing snapshot discovery is the fallback for cameras without an MJPEG URL. */
57
+ cameraCandidates(host: string, port: string, apiKey: string): Promise<CameraCandidate[]>;
52
58
  /** The single extruder slot; adapters with a spool manager override this. */
53
59
  counterpartFilaments(_host: string, _port: string, _apiKey: string): Promise<FilamentSlot[]>;
54
60
  /** Whether progress reports carry layer numbers on this firmware. */
@@ -66,9 +72,9 @@ export declare abstract class GenericPrinterImplementation extends PrinterImplem
66
72
  * Resume reheats and moves the printer, so it is gated like a print start:
67
73
  * only the job this server instance inspected and started, only while the
68
74
  * printer freshly reports that job paused, with its bytes re-read and
69
- * compared where the firmware can download them, and after human
70
- * confirmation (unless the server opted out). A stop or heater-off request
71
- * in the meantime cancels it.
75
+ * compared where the firmware can download them. Only when the bytes cannot
76
+ * be re-read does a person confirm (MCP elicitation). A stop or heater-off
77
+ * request in the meantime cancels it.
72
78
  */
73
79
  resumeJob(host: string, port: string, apiKey: string): Promise<{
74
80
  status: string;
@@ -110,5 +116,4 @@ export declare abstract class GenericPrinterImplementation extends PrinterImplem
110
116
  private withNamedSnapshot;
111
117
  private assertReady;
112
118
  private printChecked;
113
- private printPrompt;
114
119
  }
@@ -47,6 +47,10 @@ export class GenericPrinterImplementation extends PrinterImplementation {
47
47
  async snapshotCandidates(_host, _port, _apiKey) {
48
48
  return [];
49
49
  }
50
+ /** Existing snapshot discovery is the fallback for cameras without an MJPEG URL. */
51
+ async cameraCandidates(host, port, apiKey) {
52
+ return (await this.snapshotCandidates(host, port, apiKey)).map(candidate => ({ ...candidate, transport: "http-snapshot" }));
53
+ }
50
54
  /** The single extruder slot; adapters with a spool manager override this. */
51
55
  async counterpartFilaments(_host, _port, _apiKey) {
52
56
  return [{ id: "ext", source: "unknown" }];
@@ -70,9 +74,9 @@ export class GenericPrinterImplementation extends PrinterImplementation {
70
74
  * Resume reheats and moves the printer, so it is gated like a print start:
71
75
  * only the job this server instance inspected and started, only while the
72
76
  * printer freshly reports that job paused, with its bytes re-read and
73
- * compared where the firmware can download them, and after human
74
- * confirmation (unless the server opted out). A stop or heater-off request
75
- * in the meantime cancels it.
77
+ * compared where the firmware can download them. Only when the bytes cannot
78
+ * be re-read does a person confirm (MCP elicitation). A stop or heater-off
79
+ * request in the meantime cancels it.
76
80
  */
77
81
  async resumeJob(host, port, apiKey) {
78
82
  if (!(await this.supportsPauseResume(host, port, apiKey)) || !this.rawResumeJob) {
@@ -98,17 +102,20 @@ export class GenericPrinterImplementation extends PrinterImplementation {
98
102
  bytesVerified = true;
99
103
  }
100
104
  assertActive();
101
- await requireHumanConfirmation(this.confirm, `Resume ${checked.name} on ${this.printerLabel} (${host}:${port})? It reheats to its targets (peak nozzle ${checked.peaks.nozzle}°C, ` +
102
- `bed ${checked.peaks.bed}°C, chamber ${checked.peaks.chamber}°C) and moves. Materials: ${checked.materials.length ? checked.materials.join(", ") : "none declared"}. ` +
103
- `File SHA-256: ${checked.sha256}${bytesVerified ? " (re-read from the printer)" : " (this firmware cannot re-read the file; matched by name)"}. ` +
104
- "Confirm the nozzle path is clear and the filament is still loaded.", "generic");
105
- assertActive();
106
- // Someone may have resumed or stopped it at the printer while the prompt was open.
105
+ if (!bytesVerified) {
106
+ // This firmware cannot hand the file back, so only a person can tell it is still the inspected job.
107
+ await requireHumanConfirmation(this.confirm, `Resume ${checked.name} on ${this.printerLabel} (${host}:${port})? This firmware cannot re-read the file, so it is matched by name only. ` +
108
+ `It reheats to its targets (peak nozzle ${checked.peaks.nozzle}°C, bed ${checked.peaks.bed}°C, chamber ${checked.peaks.chamber}°C) and moves. ` +
109
+ `Materials: ${checked.materials.length ? checked.materials.join(", ") : "none declared"}. Inspected file SHA-256: ${checked.sha256}. ` +
110
+ "Confirm the paused job is that file, the nozzle path is clear and the filament is still loaded.");
111
+ assertActive();
112
+ }
113
+ // Someone may have resumed or stopped it at the printer meanwhile (for example while a prompt was open).
107
114
  await this.assertPausedJob(host, port, apiKey, checked.name);
108
- // A progress read during the prompt may have retired this record (the job ended, and a new one
109
- // could now be paused under the same name); only the record that was confirmed may be resumed.
115
+ // A progress read meanwhile may have retired this record (the job ended, and a new one
116
+ // could now be paused under the same name); only the record that was checked may be resumed.
110
117
  if (this.checkedJobs.get(key) !== checked) {
111
- throw new Error(`Print safety: ${checked.name} ended on ${this.printerLabel} while the confirmation was open; nothing was sent.`);
118
+ throw new Error(`Print safety: ${checked.name} ended on ${this.printerLabel} during the resume checks; nothing was sent.`);
112
119
  }
113
120
  assertActive();
114
121
  await this.rawResumeJob(host, port, apiKey);
@@ -186,8 +193,6 @@ export class GenericPrinterImplementation extends PrinterImplementation {
186
193
  validateGenericTemperature(target.heater, value, material ? [material] : undefined);
187
194
  return withPrinterOperation(host, key, async (assertActive) => {
188
195
  this.assertReady(await this.readPrinterState(host, port, apiKey), "heat");
189
- await requireHumanConfirmation(this.confirm, `Heat ${target.heater} on ${this.printerLabel} (${host}:${port}) to ${value}°C?` +
190
- (material && target.heater === "nozzle" ? ` Declared material: ${material}. Confirm the loaded filament matches.` : ""), "generic");
191
196
  assertActive();
192
197
  this.assertReady(await this.readPrinterState(host, port, apiKey), "heat");
193
198
  assertActive();
@@ -275,10 +280,9 @@ export class GenericPrinterImplementation extends PrinterImplementation {
275
280
  assertExpectedPeaks(options.expectedPeaks, { nozzle: inspection.peaks.nozzle, bed: inspection.peaks.bed });
276
281
  const initial = await this.readPrinterState(host, port, apiKey);
277
282
  this.assertReady(initial, "print");
278
- await requireHumanConfirmation(this.confirm, this.printPrompt(host, port, remoteName, inspection, initial), "generic");
279
283
  assertActive();
280
- const confirmed = await this.readPrinterState(host, port, apiKey);
281
- this.assertReady(confirmed, "print");
284
+ const rechecked = await this.readPrinterState(host, port, apiKey);
285
+ this.assertReady(rechecked, "print");
282
286
  assertActive();
283
287
  const summary = {
284
288
  status: "success",
@@ -318,12 +322,4 @@ export class GenericPrinterImplementation extends PrinterImplementation {
318
322
  this.checkedJobs.set(this.operationKey(host, port), checkedJob(remoteName, inspection));
319
323
  return { ...summary, upload, response };
320
324
  }
321
- printPrompt(host, port, remoteName, inspection, state) {
322
- const materials = inspection.materials.length ? inspection.materials.join(", ") : "none declared (no positive nozzle target)";
323
- return (`Start a checked print of ${remoteName} on ${this.printerLabel} (${host}:${port})? ` +
324
- `Materials: ${materials} (${inspection.materialSource}). Peak targets: nozzle ${inspection.peaks.nozzle}°C, bed ${inspection.peaks.bed}°C, chamber ${inspection.peaks.chamber}°C ` +
325
- `(server ceilings ${inspection.ceilings.nozzle}/${inspection.ceilings.bed}/${inspection.ceilings.chamber}°C). File SHA-256: ${inspection.sha256}. ` +
326
- "Confirm the loaded filament matches." +
327
- "");
328
- }
329
325
  }
@@ -1,4 +1,4 @@
1
- import { GenericPrinterImplementation, type GenericPrinterState, type SnapshotCandidate } from "./generic-base.js";
1
+ import { GenericPrinterImplementation, type GenericPrinterState, type SnapshotCandidate, type CameraCandidate } from "./generic-base.js";
2
2
  import type { CounterpartProgress, FilamentSlot, PrinterIdentity } from "../counterpart/contract.js";
3
3
  export declare const MACRO_TIMEOUT_MS = 60000;
4
4
  export declare class KlipperImplementation extends GenericPrinterImplementation {
@@ -18,6 +18,7 @@ export declare class KlipperImplementation extends GenericPrinterImplementation
18
18
  * path relative to the host. https://moonraker.readthedocs.io/en/latest/external_api/webcams/
19
19
  */
20
20
  snapshotCandidates(host: string, port: string, apiKey: string): Promise<SnapshotCandidate[]>;
21
+ cameraCandidates(host: string, port: string, apiKey: string): Promise<CameraCandidate[]>;
21
22
  /**
22
23
  * Spoolman through Moonraker: GET /server/spoolman/spool_id, then the proxy
23
24
  * for /v1/spool/<id>. https://moonraker.readthedocs.io/en/latest/external_api/integrations/
@@ -62,11 +62,18 @@ export class KlipperImplementation extends GenericPrinterImplementation {
62
62
  * path relative to the host. https://moonraker.readthedocs.io/en/latest/external_api/webcams/
63
63
  */
64
64
  async snapshotCandidates(host, port, apiKey) {
65
+ return (await this.cameraCandidates(host, port, apiKey)).filter(candidate => candidate.transport === "http-snapshot");
66
+ }
67
+ async cameraCandidates(host, port, apiKey) {
65
68
  const data = (await this.apiClient.get(`http://${host}:${port}/server/webcams/list`, this.authHeaders(apiKey))).data;
66
69
  const webcams = Array.isArray(data?.result?.webcams) ? data.result.webcams : [];
67
70
  return webcams
68
- .filter((webcam) => webcam?.enabled !== false && nonEmptyText(webcam?.snapshot_url))
69
- .map((webcam) => ({ url: String(webcam.snapshot_url).trim(), source: "moonraker-webcam", configured: true }));
71
+ .filter((webcam) => webcam?.enabled !== false)
72
+ .flatMap((webcam) => [
73
+ ...(nonEmptyText(webcam?.stream_url)
74
+ ? [{ url: String(webcam.stream_url).trim(), snapshotUrl: nonEmptyText(webcam?.snapshot_url), unsupportedStream: /webrtc|hls/i.test(String(webcam?.service ?? "")), transport: "http-mjpeg" }] : []),
75
+ ...(nonEmptyText(webcam?.snapshot_url) ? [{ url: String(webcam.snapshot_url).trim(), transport: "http-snapshot" }] : []),
76
+ ].map(candidate => ({ ...candidate, source: "moonraker-webcam", configured: true })));
70
77
  }
71
78
  /**
72
79
  * Spoolman through Moonraker: GET /server/spoolman/spool_id, then the proxy
@@ -159,11 +166,7 @@ export class KlipperImplementation extends GenericPrinterImplementation {
159
166
  if (!["standby", "complete", "cancelled"].includes(job)) {
160
167
  return { state, ready: false, reason: `print_stats.state is ${job}` };
161
168
  }
162
- return {
163
- state,
164
- ready: true,
165
- finishedJob: job === "standby" ? undefined : JSON.stringify([job, status?.print_stats?.filename ?? null]),
166
- };
169
+ return { state, ready: true };
167
170
  }
168
171
  async downloadRemoteFile(host, port, apiKey, filename, destination) {
169
172
  await this.downloadToFile(`http://${host}:${port}/server/files/gcodes/${encodePath(filename)}`, apiKey ? { "X-Api-Key": apiKey } : {}, destination);
@@ -1,4 +1,4 @@
1
- import { GenericPrinterImplementation, type GenericPrinterState, type HeaterComponent, type SnapshotCandidate } from "./generic-base.js";
1
+ import { GenericPrinterImplementation, type GenericPrinterState, type HeaterComponent, type SnapshotCandidate, type CameraCandidate } from "./generic-base.js";
2
2
  import type { CounterpartProgress, PrinterIdentity } from "../counterpart/contract.js";
3
3
  export declare class OctoPrintImplementation extends GenericPrinterImplementation {
4
4
  protected readonly printerLabel = "OctoPrint";
@@ -19,6 +19,7 @@ export declare class OctoPrintImplementation extends GenericPrinterImplementatio
19
19
  * src/octoprint/plugins/classicwebcam/__init__.py; checked on 1.8.7 and 1.11.2.
20
20
  */
21
21
  snapshotCandidates(host: string, port: string, apiKey: string): Promise<SnapshotCandidate[]>;
22
+ cameraCandidates(host: string, port: string, apiKey: string): Promise<CameraCandidate[]>;
22
23
  counterpartIdentity(host: string, port: string, apiKey: string): Promise<PrinterIdentity>;
23
24
  /** POST /api/job {command:"pause", action:"pause"|"resume"}; 204 on success, 409 without an active job. */
24
25
  protected rawPauseJob(host: string, port: string, apiKey: string): Promise<any>;
@@ -54,21 +54,28 @@ export class OctoPrintImplementation extends GenericPrinterImplementation {
54
54
  * src/octoprint/plugins/classicwebcam/__init__.py; checked on 1.8.7 and 1.11.2.
55
55
  */
56
56
  async snapshotCandidates(host, port, apiKey) {
57
+ return (await this.cameraCandidates(host, port, apiKey)).filter(candidate => candidate.transport === "http-snapshot");
58
+ }
59
+ async cameraCandidates(host, port, apiKey) {
57
60
  const settings = (await this.apiClient.get(`http://${host}:${port}/api/settings`, { headers: { "X-Api-Key": apiKey } })).data;
58
61
  const candidates = [];
59
62
  if (settings?.webcam?.webcamEnabled !== false) {
63
+ // The settings API maps webcam.stream to streamUrl. Classic Webcam on
64
+ // 1.9+ exposes its own stream field; only configured URLs are used.
60
65
  const configured = [
61
- settings?.plugins?.classicwebcam?.snapshot,
62
- settings?.webcam?.snapshotUrl,
63
- ...(Array.isArray(settings?.webcam?.webcams) ? settings.webcam.webcams.map((webcam) => webcam?.compat?.snapshot) : []),
66
+ settings?.plugins?.classicwebcam,
67
+ { stream: settings?.webcam?.streamUrl, snapshot: settings?.webcam?.snapshotUrl },
68
+ ...(Array.isArray(settings?.webcam?.webcams) ? settings.webcam.webcams.map((webcam) => webcam?.compat) : []),
64
69
  ];
65
- for (const url of configured) {
66
- const text = nonEmptyText(url);
67
- if (text)
68
- candidates.push({ url: text, source: "octoprint-settings", configured: true });
70
+ for (const camera of configured) {
71
+ const stream = nonEmptyText(camera?.stream), snapshot = nonEmptyText(camera?.snapshot);
72
+ if (stream)
73
+ candidates.push({ url: stream, snapshotUrl: snapshot, source: "octoprint-settings", configured: true, transport: "http-mjpeg" });
74
+ if (snapshot)
75
+ 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
  async counterpartIdentity(host, port, apiKey) {
@@ -10,7 +10,7 @@ export type GuardedPrinterImplementation = PrinterImplementation & {
10
10
  export declare class PrinterFactory {
11
11
  private implementations;
12
12
  private apiClient;
13
- /** confirm is the human (MCP elicitation) confirmation used before prints and positive heating. */
13
+ /** confirm is the human (MCP elicitation) confirmation for physical checks software cannot make, such as resuming a job that cannot be re-read. */
14
14
  constructor(confirm?: ConfirmHardware);
15
15
  getImplementation(type: string): GuardedPrinterImplementation;
16
16
  disconnectAll(): Promise<void>;
@@ -2,12 +2,12 @@ import { OctoPrintImplementation } from "./octoprint.js";
2
2
  import { KlipperImplementation } from "./klipper.js";
3
3
  import { DuetImplementation } from "./duet.js";
4
4
  import { RepetierImplementation } from "./repetier.js";
5
- import { BambuImplementation } from "./bambu.js";
5
+ import { BambuImplementation, defaultResumeStateDir } from "./bambu.js";
6
6
  import { PrusaImplementation } from "./prusa.js";
7
7
  import { CrealityImplementation } from "./creality.js";
8
8
  import axios from "axios";
9
9
  export class PrinterFactory {
10
- /** confirm is the human (MCP elicitation) confirmation used before prints and positive heating. */
10
+ /** confirm is the human (MCP elicitation) confirmation for physical checks software cannot make, such as resuming a job that cannot be re-read. */
11
11
  constructor(confirm) {
12
12
  this.implementations = new Map();
13
13
  this.apiClient = axios.create({ timeout: 10000 });
@@ -15,7 +15,7 @@ export class PrinterFactory {
15
15
  this.implementations.set("klipper", new KlipperImplementation(this.apiClient, confirm));
16
16
  this.implementations.set("duet", new DuetImplementation(this.apiClient, confirm));
17
17
  this.implementations.set("repetier", new RepetierImplementation(this.apiClient, confirm));
18
- this.implementations.set("bambu", new BambuImplementation(this.apiClient, confirm));
18
+ this.implementations.set("bambu", new BambuImplementation(this.apiClient, confirm, { resumeStateDir: defaultResumeStateDir() }));
19
19
  this.implementations.set("prusa", new PrusaImplementation(this.apiClient, confirm));
20
20
  this.implementations.set("creality", new CrealityImplementation(this.apiClient, confirm));
21
21
  }
@@ -201,7 +201,6 @@ export class PrusaImplementation extends GenericPrinterImplementation {
201
201
  state,
202
202
  ready,
203
203
  reason: ready ? undefined : `printer.state is ${state}`,
204
- finishedJob: ready && (state === "FINISHED" || state === "STOPPED") ? JSON.stringify([state, data?.job?.id ?? null]) : undefined,
205
204
  };
206
205
  }
207
206
  const flags = data?.state?.flags;
@@ -1,22 +1,17 @@
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
1
  /**
5
- * Ordinary print/heat prompts are on by default. Only an explicit "0" opts out.
6
- * PRINT_REQUIRE_CONFIRMATION applies to every printer type; the older
7
- * BAMBU_REQUIRE_CONFIRMATION applies to Bambu printers only. If any variable
8
- * 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.
9
6
  */
10
- export declare function confirmationOptOut(scope: ConfirmationScope): boolean;
7
+ export type ConfirmHardware = (message: string) => Promise<boolean>;
11
8
  /**
12
- * How long a human has to answer a confirmation prompt. The MCP SDK's default
13
- * request timeout (60 s) is too short for someone walking to the printer.
9
+ * How long a person has to answer a prompt from the server (a resume of a job
10
+ * that could not be re-read, or the Bambu printer-model prompt). The MCP SDK's
11
+ * default request timeout (60 s) is too short for someone walking to the printer.
14
12
  */
15
13
  export declare function confirmationTimeoutMs(): number;
16
14
  export declare function confirmationTimedOut(timeoutMs: number): string;
17
- export declare const CONFIRMATION_UNAVAILABLE: string;
18
- /**
19
- * physicalCheck marks confirmations that software cannot replace (for example
20
- * clearing hardware errors). Those are required even when prompts are opted out.
21
- */
22
- export declare function requireHumanConfirmation(confirm: ConfirmHardware | undefined, message: string, scope: ConfirmationScope, physicalCheck?: boolean): Promise<void>;
15
+ export declare const CONFIRMATION_UNAVAILABLE = "This step needs a person to confirm a check at the printer, which requires an MCP client with elicitation support. No command was sent.";
16
+ /** Ask a person to accept a physical check that software cannot replace. */
17
+ export declare function requireHumanConfirmation(confirm: ConfirmHardware | undefined, message: string): Promise<void>;
@@ -1,21 +1,7 @@
1
1
  /**
2
- * Ordinary print/heat prompts are on by default. Only an explicit "0" opts out.
3
- * PRINT_REQUIRE_CONFIRMATION applies to every printer type; the older
4
- * BAMBU_REQUIRE_CONFIRMATION applies to Bambu printers only. If any variable
5
- * relevant to the scope is set to another value, confirmation stays required.
6
- */
7
- export function confirmationOptOut(scope) {
8
- const names = scope === "bambu"
9
- ? ["BAMBU_REQUIRE_CONFIRMATION", "PRINT_REQUIRE_CONFIRMATION"]
10
- : ["PRINT_REQUIRE_CONFIRMATION"];
11
- const values = names
12
- .map((name) => process.env[name]?.trim())
13
- .filter((value) => value !== undefined && value !== "");
14
- return values.length > 0 && values.every((value) => value === "0");
15
- }
16
- /**
17
- * How long a human has to answer a confirmation prompt. The MCP SDK's default
18
- * request timeout (60 s) is too short for someone walking to the printer.
2
+ * How long a person has to answer a prompt from the server (a resume of a job
3
+ * that could not be re-read, or the Bambu printer-model prompt). The MCP SDK's
4
+ * default request timeout (60 s) is too short for someone walking to the printer.
19
5
  */
20
6
  export function confirmationTimeoutMs() {
21
7
  const raw = process.env.PRINT_CONFIRMATION_TIMEOUT_MS?.trim();
@@ -27,23 +13,13 @@ export function confirmationTimedOut(timeoutMs) {
27
13
  return `No confirmation was received within ${minutes >= 1 ? `${Number(minutes.toFixed(1))} minute(s)` : `${timeoutMs / 1000} seconds`}; ` +
28
14
  "nothing was sent to the printer. Ask again when you are at the printer, or raise PRINT_CONFIRMATION_TIMEOUT_MS.";
29
15
  }
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. No command was sent.";
32
- /**
33
- * physicalCheck marks confirmations that software cannot replace (for example
34
- * clearing hardware errors). Those are required even when prompts are opted out.
35
- */
36
- export async function requireHumanConfirmation(confirm, message, scope, physicalCheck = false) {
37
- if (!physicalCheck && confirmationOptOut(scope))
38
- return;
16
+ export const CONFIRMATION_UNAVAILABLE = "This step needs a person to confirm a check at the printer, which requires an MCP client with elicitation support. No command was sent.";
17
+ /** Ask a person to accept a physical check that software cannot replace. */
18
+ export async function requireHumanConfirmation(confirm, message) {
39
19
  if (!confirm)
40
20
  throw new Error(CONFIRMATION_UNAVAILABLE);
41
21
  if (!(await confirm(message))) {
42
- throw new Error("Hardware safety confirmation was declined or cancelled in the MCP client. No command was sent. " + (physicalCheck
43
- ? "This confirmation covers a physical check at the printer and always needs a person to accept it."
44
- : "Some clients decline prompts automatically, for example in a non-interactive permission mode. A deliberately headless " +
45
- `setup that approves jobs another way can set ${scope === "bambu"
46
- ? "PRINT_REQUIRE_CONFIRMATION=0 (or BAMBU_REQUIRE_CONFIRMATION=0)"
47
- : "PRINT_REQUIRE_CONFIRMATION=0"} to skip ordinary print and heating prompts.`));
22
+ throw new Error("Hardware safety confirmation was declined or cancelled in the MCP client. No command was sent. " +
23
+ "This confirmation covers a physical check at the printer and always needs a person to accept it.");
48
24
  }
49
25
  }
@@ -0,0 +1,32 @@
1
+ export declare const HIDDEN_HMS_SOURCE: {
2
+ readonly studioTag: "v02.08.02.61";
3
+ readonly studioCommit: "926a7192574bcb9b3a732e1ec59a46d79cb45466";
4
+ /** Per table: Bambu's device_hms version and the SHA-256 of the English file used. */
5
+ readonly tables: {
6
+ readonly "093": {
7
+ readonly version: 202508191204;
8
+ readonly sha256: "7801fd6c355854b62ecb62ee32727b4e5dcca908841dbcfd5e5de45b3dfd1c25";
9
+ };
10
+ readonly "094": {
11
+ readonly version: 202503231214;
12
+ readonly sha256: "047c15e2140738ec08258b74536e6cb996cddf0c586c5c39e2318e54125a9e76";
13
+ };
14
+ readonly "20P": {
15
+ readonly version: 202511162200;
16
+ readonly sha256: "e4f067d027b9bcbb9bbf6cb90f41f11130cf4d83dffd3dee101db7826dc808c3";
17
+ };
18
+ readonly "22E": {
19
+ readonly version: 202510142200;
20
+ readonly sha256: "87426021aea2bd4e8d721e46e1c86808fd7c6fc06f52baf6d952ead9e8d14d1e";
21
+ };
22
+ readonly "239": {
23
+ readonly version: 202506171403;
24
+ readonly sha256: "c1d23d5302756d57df5cb9bc795d95fc54ef1db7565dca1f04b69a024886426a";
25
+ };
26
+ readonly "31B": {
27
+ readonly version: 202511162200;
28
+ readonly sha256: "dcf63657bff5451f0715ea3c032efe482279bc8cbc28cb9b8ae51c7354826574";
29
+ };
30
+ };
31
+ };
32
+ export declare const HIDDEN_HMS: Readonly<Record<string, readonly string[]>>;