mcp-3d-printer-server 1.2.22 → 1.2.24

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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.2.24] – 2026-11-02
4
+
5
+ ### Fixed
6
+
7
+ - `fusion_mcp_status` reports the Fusion360MCP add-in as reachable. The pinned add-in answers a ping on its fast path with `{"ok":true,"status":"pong"}`, and the bridge accepted only `{"pong":true}`, so it reported `connected: false` with a running add-in. Verified against a live Fusion with the add-in at `19b9f1e95192` through bambu-printer-mcp's identical bridge, and with socket fixtures here.
8
+
9
+ ## [1.2.23] – 2026-11-02
10
+
11
+ ### Removed
12
+
13
+ - 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.
14
+
3
15
  ## [1.2.22] – 2026-11-02
4
16
 
5
17
  ### Fixed
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, and a finished-job report cannot prove the bed is clear, which is why a human confirms. 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.
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>
@@ -130,8 +130,10 @@ const NO_ACTIVE_DESIGN_HELP = "Fusion has no open design, and at the pinned faus
130
130
  * True only when the Fusion360MCP add-in answers its ping. A bare TCP connect
131
131
  * is not enough: Blender's MCP addon listens on the same default port (9876)
132
132
  * and answers "unknown command" to this harmless request.
133
- * Protocol: newline-delimited JSON, {"type","params"} in and
134
- * {"status":"success","result":{"pong":true}} out (add-in server/socket_server.py).
133
+ * Protocol: newline-delimited JSON, {"type","params"} in (add-in server/socket_server.py).
134
+ * The pinned add-in answers ping on its fast path (event_bridge.py) with
135
+ * {"status":"success","result":{"ok":true,"status":"pong"}}, and through its
136
+ * command handler with {"pong":true}.
135
137
  */
136
138
  function addinAnswers(host, port, timeoutMs) {
137
139
  return new Promise((resolve) => {
@@ -150,7 +152,7 @@ function addinAnswers(host, port, timeoutMs) {
150
152
  const line = buffer.includes("\n") ? buffer.slice(0, buffer.indexOf("\n")) : buffer;
151
153
  try {
152
154
  const reply = JSON.parse(line);
153
- done(reply?.status === "success" && reply?.result?.pong === true);
155
+ done(reply?.status === "success" && (reply?.result?.status === "pong" || reply?.result?.pong === true));
154
156
  }
155
157
  catch {
156
158
  if (buffer.includes("\n"))
@@ -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>;
@@ -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 and that the build plate is clear.` +
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"}.` : ""), finishedJob !== undefined);
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
- const bedClearance = await this.confirmPrintPreflight(serial, initialStatus, inspection);
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
- const bedClearance = await this.confirmPrintPreflight(serial, initialStatus, inspection);
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", initial.finishedJob !== undefined);
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 and that the build plate is clear." +
334
- (state.finishedJob !== undefined ? ` The printer reports '${state.state}': remove the previous part and debris before confirming.` : ""));
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 can leave a part on the bed, so they need a human.
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>;
@@ -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 can leave a part on the bed, so they need a human.
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;
@@ -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 but
31
- * may leave a part on the bed, so they require a human confirmation.
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>;
@@ -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 but
185
- * may leave a part on the bed, so they require a human confirmation.
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 a
20
- * previous part left on the bed). Those are required even when prompts are opted out.
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 a
35
- * previous part left on the bed). Those are required even when prompts are opted out.
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.22",
4
+ "version": "1.2.24",
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.22",
10
+ "version": "1.2.24",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "mcp-3d-printer-server",
15
- "version": "1.2.22",
15
+ "version": "1.2.24",
16
16
  "runtimeHint": "npx",
17
17
  "transport": {
18
18
  "type": "stdio"
@@ -136,8 +136,10 @@ const NO_ACTIVE_DESIGN_HELP =
136
136
  * True only when the Fusion360MCP add-in answers its ping. A bare TCP connect
137
137
  * is not enough: Blender's MCP addon listens on the same default port (9876)
138
138
  * and answers "unknown command" to this harmless request.
139
- * Protocol: newline-delimited JSON, {"type","params"} in and
140
- * {"status":"success","result":{"pong":true}} out (add-in server/socket_server.py).
139
+ * Protocol: newline-delimited JSON, {"type","params"} in (add-in server/socket_server.py).
140
+ * The pinned add-in answers ping on its fast path (event_bridge.py) with
141
+ * {"status":"success","result":{"ok":true,"status":"pong"}}, and through its
142
+ * command handler with {"pong":true}.
141
143
  */
142
144
  function addinAnswers(host: string, port: number, timeoutMs: number): Promise<boolean> {
143
145
  return new Promise((resolve) => {
@@ -154,7 +156,7 @@ function addinAnswers(host: string, port: number, timeoutMs: number): Promise<bo
154
156
  const line = buffer.includes("\n") ? buffer.slice(0, buffer.indexOf("\n")) : buffer;
155
157
  try {
156
158
  const reply = JSON.parse(line);
157
- done(reply?.status === "success" && reply?.result?.pong === true);
159
+ done(reply?.status === "success" && (reply?.result?.status === "pong" || reply?.result?.pong === true));
158
160
  } catch {
159
161
  if (buffer.includes("\n")) done(false);
160
162
  }
@@ -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 and that the build plate is clear.` +
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
- const bedClearance = await this.confirmPrintPreflight(serial, initialStatus, inspection);
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
- const bedClearance = await this.confirmPrintPreflight(serial, initialStatus, inspection);
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", initial.finishedJob !== undefined);
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 and that the build plate is clear." +
454
- (state.finishedJob !== undefined ? ` The printer reports '${state.state}': remove the previous part and debris before confirming.` : "")
446
+ "Confirm the loaded filament matches." +
447
+ ""
455
448
  );
456
449
  }
457
450
  }
@@ -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 can leave a part on the bed, so they need a human.
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;
@@ -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 but
217
- * may leave a part on the bed, so they require a human confirmation.
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 a
44
- * previous part left on the bed). Those are required even when prompts are opted out.
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,