mcp-3d-printer-server 1.2.30 → 1.2.31

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,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.2.31] – 2026-11-04
4
+
5
+ ### Added
6
+
7
+ - A printer refusal on `print_3mf` carries a stable `code` in its structured error: `printer-command-rejected` when the printer refuses the print command itself (Bambu HMS 0500-0500-0001-0007, MQTT command verification; the file is on the printer, nothing is printing), and `printer-not-ready` when the printer isn't in a state that can start a print. The message and suggestion are unchanged. Clients such as Counterpart can act on the code instead of matching the wording, which changes with printers and firmware. Checked with mocked MQTT only. Same change as bambu-printer-mcp 1.1.57.
8
+
3
9
  ## [1.2.30] – 2026-11-04
4
10
 
5
11
  ### Added
package/README.md CHANGED
@@ -210,6 +210,7 @@ The optional FULU **BambuNetwork bridge** is a separate runtime. With `BAMBU_DEF
210
210
  - Per-call `type`, `host`, `port`, and `api_key` arguments for working with more than one printer
211
211
  - Printer status, file listing, G-code upload from inline content or a local path, starting a stored file, pausing, resuming, cancelling a job, and setting bed or nozzle temperatures
212
212
  - The `counterpart/1` tools shared with [bambu-printer-mcp](https://github.com/DMontgomery40/bambu-printer-mcp): one progress shape for every firmware (state, job, progress, time left, layers, temperatures, problems), detected printer capabilities, camera snapshots, and the loaded filament (Spoolman on Moonraker)
213
+ - Errors carry their details in `structuredContent` (`status`, `retryable`, `suggestion`, `message`); a printer refusal on `print_3mf` also carries a stable `code`, `printer-command-rejected` or `printer-not-ready`, so a client can act on the code instead of the wording.
213
214
  - STL tools: inspect dimensions, scale, rotate, translate, extend the base, merge vertices, center, lay flat, transform one section of a model, and render multi-angle SVG previews
214
215
  - A print and heating safety gate on every backend: the exact G-code is inspected, every heater target is checked against hardware and material ceilings, and the printer's state is checked. There is no confirmation prompt and no bed check: your client's own print action is the approval. Heater-off, pause, and cancel are never gated; resume only continues a paused job this server inspected and started, and asks a person only when the printer cannot hand the file back for re-reading
215
216
  - Slicing through PrusaSlicer, Slic3r, OrcaSlicer, CuraEngine, FULU OrcaSlicer-bambulab, or Bambu Studio command lines, with G-code peak-temperature checks and a one-call process-and-print pipeline
package/dist/index.js CHANGED
@@ -20,6 +20,7 @@ import { PrinterFactory } from "./printers/printer-factory.js";
20
20
  import { STLManipulator, SUPPORT_TYPES } from "./stl/stl-manipulator.js";
21
21
  import { parsePauseLayers, readPlateGcode, pausedLayers, machinePauseGcode } from "./slicer/layer-pauses.js";
22
22
  import { PRINT_QUALITY_PROPERTIES, withPrintQuality } from "./slicer/print-quality.js";
23
+ import { PrinterRefusal } from "./refusal.js";
23
24
  import { CounterpartService } from "./counterpart/service.js";
24
25
  import { CAMERA_STREAM_TOOLS } from "./counterpart/camera-tools.js";
25
26
  import { annotateTools } from "./tool-annotations.js";
@@ -2817,7 +2818,9 @@ class ThreeDPrinterMCPServer {
2817
2818
  }
2818
2819
  catch (printError) {
2819
2820
  console.error(`Error starting 3MF print for ${threeMfFilename}:`, printError);
2820
- throw new Error(`Failed to start print: ${printError.message}`);
2821
+ const wrapped = `Failed to start print: ${printError.message}`;
2822
+ // A printer refusal keeps its code through the wrap: clients act on it (src/refusal.ts).
2823
+ throw printError instanceof PrinterRefusal ? new PrinterRefusal(printError.code, wrapped) : new Error(wrapped);
2821
2824
  }
2822
2825
  break;
2823
2826
  }
@@ -2910,7 +2913,8 @@ class ThreeDPrinterMCPServer {
2910
2913
  catch (error) {
2911
2914
  console.error(`Error calling tool ${name}:`, error);
2912
2915
  const errorMessage = error instanceof Error ? error.message : String(error);
2913
- const structuredError = this.toStructuredToolError(name, errorMessage, error);
2916
+ // A printer refusal adds its stable code (src/refusal.ts); the message and suggestion stay as they were.
2917
+ const structuredError = { ...this.toStructuredToolError(name, errorMessage, error), ...(error instanceof PrinterRefusal ? { code: error.code } : {}) };
2914
2918
  return {
2915
2919
  content: [
2916
2920
  {
@@ -1,3 +1,4 @@
1
+ import { PrinterRefusal } from "../refusal.js";
1
2
  import { PrinterImplementation } from "../types.js";
2
3
  import { createHash } from "node:crypto";
3
4
  import { createReadStream, readFileSync, mkdirSync, writeFileSync, renameSync, rmSync } from "node:fs";
@@ -671,7 +672,7 @@ export class BambuImplementation extends PrinterImplementation {
671
672
  catch { /* stays unconfirmed */ }
672
673
  }
673
674
  if (verdict === "rejected")
674
- throw new Error(COMMAND_REJECTED_MESSAGE);
675
+ throw new PrinterRefusal("printer-command-rejected", COMMAND_REJECTED_MESSAGE);
675
676
  stillActive();
676
677
  return verdict ?? "unconfirmed";
677
678
  },
@@ -0,0 +1,15 @@
1
+ /**
2
+ * A printer refusal with a stable code, returned as structuredContent.code on the tool's error, so clients
3
+ * such as Counterpart act on the code instead of matching the message (whose words, HMS numbers and
4
+ * firmware details change with the printer). Neither one starts a print.
5
+ *
6
+ * - printer-command-rejected: the printer refused the print command itself (for Bambu, HMS 0500-0500-0001-0007,
7
+ * MQTT command verification); the file was uploaded and the command sent, but nothing is printing.
8
+ * - printer-not-ready: the printer isn't in a state that can start a print (busy, paused, failed, unknown).
9
+ */
10
+ export declare const REFUSAL_CODES: readonly ["printer-command-rejected", "printer-not-ready"];
11
+ export type RefusalCode = (typeof REFUSAL_CODES)[number];
12
+ export declare class PrinterRefusal extends Error {
13
+ readonly code: RefusalCode;
14
+ constructor(code: RefusalCode, message: string);
15
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * A printer refusal with a stable code, returned as structuredContent.code on the tool's error, so clients
3
+ * such as Counterpart act on the code instead of matching the message (whose words, HMS numbers and
4
+ * firmware details change with the printer). Neither one starts a print.
5
+ *
6
+ * - printer-command-rejected: the printer refused the print command itself (for Bambu, HMS 0500-0500-0001-0007,
7
+ * MQTT command verification); the file was uploaded and the command sent, but nothing is printing.
8
+ * - printer-not-ready: the printer isn't in a state that can start a print (busy, paused, failed, unknown).
9
+ */
10
+ export const REFUSAL_CODES = ["printer-command-rejected", "printer-not-ready"];
11
+ export class PrinterRefusal extends Error {
12
+ constructor(code, message) {
13
+ super(message);
14
+ this.code = code;
15
+ this.name = "PrinterRefusal";
16
+ }
17
+ }
@@ -1,3 +1,4 @@
1
+ import { PrinterRefusal } from "../refusal.js";
1
2
  import { HIDDEN_HMS } from "./hidden-hms.js";
2
3
  import { isDualNozzleModel, normalizeMaterial, normalizeModel } from "./limits.js";
3
4
  const MAX_REPORT_AGE_MS = 15000;
@@ -332,7 +333,7 @@ function validateErrors(raw, requireIdle, serial, model) {
332
333
  const state = typeof raw.gcode_state === "string" ? raw.gcode_state.toUpperCase() : "UNKNOWN";
333
334
  const terminal = isCancelledX2DJob(raw, model) || isFinishedP1SJob(raw, model);
334
335
  if (requireIdle && !["IDLE", "FINISH"].includes(state) && !terminal)
335
- throw new Error(`Printer state ${state} is not safely idle.`);
336
+ throw new PrinterRefusal("printer-not-ready", `Printer state ${state} is not safely idle.`);
336
337
  if (!requireIdle && !["IDLE", "FINISH", "RUNNING", "PAUSE"].includes(state) && !terminal)
337
338
  throw new Error(`Printer state ${state} is unknown or failed.`);
338
339
  if (finiteNumber(raw.print_error) !== 0 && !isPauseNotice(raw, requireIdle))
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.30",
4
+ "version": "1.2.31",
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",
@@ -32,6 +32,8 @@
32
32
  "dist/http-server.js",
33
33
  "dist/index.d.ts",
34
34
  "dist/index.js",
35
+ "dist/refusal.d.ts",
36
+ "dist/refusal.js",
35
37
  "dist/printers",
36
38
  "dist/safety",
37
39
  "dist/sse-server.d.ts",
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.30",
10
+ "version": "1.2.31",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "mcp-3d-printer-server",
15
- "version": "1.2.30",
15
+ "version": "1.2.31",
16
16
  "runtimeHint": "npx",
17
17
  "transport": {
18
18
  "type": "stdio"
package/src/index.ts CHANGED
@@ -37,6 +37,7 @@ import { PrinterFactory } from "./printers/printer-factory.js";
37
37
  import { STLManipulator, SUPPORT_TYPES, type BambuSliceOptions, type ProcessOverrides, type SliceOutput } from "./stl/stl-manipulator.js";
38
38
  import { parsePauseLayers, readPlateGcode, pausedLayers, machinePauseGcode } from "./slicer/layer-pauses.js";
39
39
  import { PRINT_QUALITY_PROPERTIES, withPrintQuality } from "./slicer/print-quality.js";
40
+ import { PrinterRefusal } from "./refusal.js";
40
41
  import { CounterpartService, type BambuControlRoute, type PrinterTarget } from "./counterpart/service.js";
41
42
  import {CAMERA_STREAM_TOOLS} from "./counterpart/camera-tools.js";
42
43
  import { annotateTools } from "./tool-annotations.js";
@@ -736,6 +737,8 @@ type StructuredToolError = {
736
737
  suggestion: string;
737
738
  message: string;
738
739
  tool: string;
740
+ /** A stable refusal code (src/refusal.ts) when the printer refused; clients act on it, not on the words. */
741
+ code?: string;
739
742
  slicer?: Record<string, unknown>;
740
743
  /** The slicer's own result.json, verbatim. */
741
744
  slicer_result?: Record<string, unknown>;
@@ -3185,7 +3188,9 @@ class ThreeDPrinterMCPServer {
3185
3188
  // Report what the printer did, not merely that a command was published.
3186
3189
  } catch (printError) {
3187
3190
  console.error(`Error starting 3MF print for ${threeMfFilename}:`, printError);
3188
- throw new Error(`Failed to start print: ${(printError as Error).message}`);
3191
+ const wrapped = `Failed to start print: ${(printError as Error).message}`;
3192
+ // A printer refusal keeps its code through the wrap: clients act on it (src/refusal.ts).
3193
+ throw printError instanceof PrinterRefusal ? new PrinterRefusal(printError.code, wrapped) : new Error(wrapped);
3189
3194
  }
3190
3195
 
3191
3196
  break;
@@ -3342,7 +3347,8 @@ class ThreeDPrinterMCPServer {
3342
3347
  console.error(`Error calling tool ${name}:`, error);
3343
3348
 
3344
3349
  const errorMessage = error instanceof Error ? error.message : String(error);
3345
- const structuredError = this.toStructuredToolError(name, errorMessage, error);
3350
+ // A printer refusal adds its stable code (src/refusal.ts); the message and suggestion stay as they were.
3351
+ const structuredError = { ...this.toStructuredToolError(name, errorMessage, error), ...(error instanceof PrinterRefusal ? { code: error.code } : {}) };
3346
3352
 
3347
3353
  return {
3348
3354
  content: [
@@ -1,3 +1,4 @@
1
+ import { PrinterRefusal } from "../refusal.js";
1
2
  import { PrinterImplementation } from "../types.js";
2
3
  import { createHash } from "node:crypto";
3
4
  import { createReadStream, readFileSync, mkdirSync, writeFileSync, renameSync, rmSync } from "node:fs";
@@ -782,7 +783,7 @@ export class BambuImplementation extends PrinterImplementation {
782
783
  if (verdict === undefined) {
783
784
  try { verdict = judge((await this.getSafetyStatus(host, serial, token))?.raw); } catch { /* stays unconfirmed */ }
784
785
  }
785
- if (verdict === "rejected") throw new Error(COMMAND_REJECTED_MESSAGE);
786
+ if (verdict === "rejected") throw new PrinterRefusal("printer-command-rejected", COMMAND_REJECTED_MESSAGE);
786
787
  stillActive();
787
788
  return verdict ?? "unconfirmed";
788
789
  },
@@ -1,3 +1,4 @@
1
+ import { PrinterRefusal } from "../refusal.js";
1
2
  import { HIDDEN_HMS } from "./hidden-hms.js";
2
3
  import type { BambuClient } from "bambu-node";
3
4
  import { isDualNozzleModel, normalizeMaterial, normalizeModel } from "./limits.js";
@@ -341,7 +342,7 @@ export function isPauseNotice(raw: Record<string, any>, requireIdle: boolean): b
341
342
  function validateErrors(raw: Record<string,any>, requireIdle: boolean, serial: string, model: string) {
342
343
  const state = typeof raw.gcode_state === "string" ? raw.gcode_state.toUpperCase() : "UNKNOWN";
343
344
  const terminal = isCancelledX2DJob(raw, model) || isFinishedP1SJob(raw, model);
344
- if (requireIdle && !["IDLE","FINISH"].includes(state) && !terminal) throw new Error(`Printer state ${state} is not safely idle.`);
345
+ if (requireIdle && !["IDLE","FINISH"].includes(state) && !terminal) throw new PrinterRefusal("printer-not-ready", `Printer state ${state} is not safely idle.`);
345
346
  if (!requireIdle && !["IDLE","FINISH","RUNNING","PAUSE"].includes(state) && !terminal) throw new Error(`Printer state ${state} is unknown or failed.`);
346
347
  if (finiteNumber(raw.print_error) !== 0 && !isPauseNotice(raw, requireIdle)) throw new Error("Printer reports a print error or did not report its error state.");
347
348
  if (!Array.isArray(raw.hms)) throw new Error("Printer HMS error state is missing or malformed.");