mcp-3d-printer-server 1.2.30 → 1.2.32
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 +16 -0
- package/README.md +2 -0
- package/dist/counterpart/contract.d.ts +22 -0
- package/dist/counterpart/contract.js +5 -0
- package/dist/counterpart/service.js +3 -0
- package/dist/index.js +12 -6
- package/dist/printers/bambu.js +2 -1
- package/dist/refusal.d.ts +15 -0
- package/dist/refusal.js +17 -0
- package/dist/safety/printer-state.js +2 -1
- package/dist/slicer/capabilities.d.ts +19 -0
- package/dist/slicer/capabilities.js +11 -0
- package/package.json +3 -1
- package/server.json +2 -2
- package/src/counterpart/contract.ts +8 -0
- package/src/counterpart/service.ts +3 -0
- package/src/index.ts +14 -6
- package/src/printers/bambu.ts +2 -1
- package/src/safety/printer-state.ts +2 -1
- package/src/slicer/capabilities.ts +29 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.2.32] – 2026-11-04
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `counterpart_printer_info` reports `capabilities.slicing`: whether `slice_stl` can apply supports, copies (`repetitions`) and auto-orient with the slicer this server uses for that printer. The Bambu-compatible CLI route takes all three, generic OrcaSlicer takes supports, and PrusaSlicer, Slic3r and Cura take none. For a Bambu printer on OrcaSlicer, that assumes the call passes the printer's `bambu_model`. Clients such as Counterpart offer those choices from this instead of knowing which slicers accept what. Same field as bambu-printer-mcp 1.1.58. Checked with mocked transports only.
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- `slice_stl` with `repetitions` above 1 on a slicer that can't make copies (anything but the Bambu-compatible CLI route) now says so and slices nothing; it used to slice one copy without a word. Checked with stand-in slicers only.
|
|
12
|
+
|
|
13
|
+
## [1.2.31] – 2026-11-04
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- 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.
|
|
18
|
+
|
|
3
19
|
## [1.2.30] – 2026-11-04
|
|
4
20
|
|
|
5
21
|
### Added
|
package/README.md
CHANGED
|
@@ -210,6 +210,8 @@ 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
|
+
- `counterpart_printer_info` reports `capabilities.slicing` (`supports`, `copies`, `orient`) for the slicer route this server uses for that printer (for a Bambu printer on OrcaSlicer, when the call passes its `bambu_model`), so a client can offer those choices without knowing the slicer.
|
|
214
|
+
- 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
215
|
- 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
216
|
- 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
217
|
- 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
|
|
@@ -55,6 +55,13 @@ export interface CounterpartCapabilities {
|
|
|
55
55
|
cancel: boolean;
|
|
56
56
|
print3mf: boolean;
|
|
57
57
|
gcode: boolean;
|
|
58
|
+
/** What slice_stl can apply with this server's slicer for this printer (src/slicer/capabilities.ts). For a Bambu printer on
|
|
59
|
+
* OrcaSlicer this assumes the call passes the printer's bambu_model, as Counterpart does; without one, OrcaSlicer slices generically. */
|
|
60
|
+
slicing?: {
|
|
61
|
+
supports: boolean;
|
|
62
|
+
copies: boolean;
|
|
63
|
+
orient: boolean;
|
|
64
|
+
};
|
|
58
65
|
}
|
|
59
66
|
export interface PrinterIdentity {
|
|
60
67
|
model?: string;
|
|
@@ -172,6 +179,21 @@ export declare const PRINTER_INFO_OUTPUT_SCHEMA: {
|
|
|
172
179
|
readonly gcode: {
|
|
173
180
|
readonly type: "boolean";
|
|
174
181
|
};
|
|
182
|
+
readonly slicing: {
|
|
183
|
+
readonly type: "object";
|
|
184
|
+
readonly required: readonly ["supports", "copies", "orient"];
|
|
185
|
+
readonly properties: {
|
|
186
|
+
readonly supports: {
|
|
187
|
+
readonly type: "boolean";
|
|
188
|
+
};
|
|
189
|
+
readonly copies: {
|
|
190
|
+
readonly type: "boolean";
|
|
191
|
+
};
|
|
192
|
+
readonly orient: {
|
|
193
|
+
readonly type: "boolean";
|
|
194
|
+
};
|
|
195
|
+
};
|
|
196
|
+
};
|
|
175
197
|
};
|
|
176
198
|
};
|
|
177
199
|
};
|
|
@@ -59,6 +59,11 @@ export const PRINTER_INFO_OUTPUT_SCHEMA = {
|
|
|
59
59
|
cancel: { type: "boolean" },
|
|
60
60
|
print3mf: { type: "boolean" },
|
|
61
61
|
gcode: { type: "boolean" },
|
|
62
|
+
slicing: {
|
|
63
|
+
type: "object",
|
|
64
|
+
required: ["supports", "copies", "orient"],
|
|
65
|
+
properties: { supports: { type: "boolean" }, copies: { type: "boolean" }, orient: { type: "boolean" } },
|
|
66
|
+
},
|
|
62
67
|
},
|
|
63
68
|
},
|
|
64
69
|
},
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* adapters supply firmware reads; this module turns them into the shared
|
|
4
4
|
* results, detects capabilities, and fetches camera snapshots.
|
|
5
5
|
*/
|
|
6
|
+
import { sliceCapabilities } from "../slicer/capabilities.js";
|
|
6
7
|
import fs from "node:fs/promises";
|
|
7
8
|
import path from "node:path";
|
|
8
9
|
import { isAxiosError } from "axios";
|
|
@@ -83,6 +84,7 @@ export class CounterpartService {
|
|
|
83
84
|
capabilities = {
|
|
84
85
|
progress: true, layers: true, camera: overrideSnapshotUrl() ? "http-snapshot" : "none", filaments: "ams",
|
|
85
86
|
pause: true, resume: true, cancel: true, print3mf: true, gcode: true,
|
|
87
|
+
slicing: sliceCapabilities(this.bambuProfileSource().slicerType ?? "bambustudio", true),
|
|
86
88
|
};
|
|
87
89
|
}
|
|
88
90
|
else {
|
|
@@ -96,6 +98,7 @@ export class CounterpartService {
|
|
|
96
98
|
capabilities = {
|
|
97
99
|
progress: true, layers, camera, filaments: "single",
|
|
98
100
|
pause: pauseResume, resume: pauseResume, cancel: true, print3mf: false, gcode: true,
|
|
101
|
+
slicing: sliceCapabilities(this.bambuProfileSource().slicerType ?? "prusaslicer", false),
|
|
99
102
|
};
|
|
100
103
|
}
|
|
101
104
|
const streamCandidates = await this.cameraCandidates(target).catch(() => []);
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,8 @@ 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";
|
|
24
|
+
import { isBambuProjectSlicer, usesBambuCli } from "./slicer/capabilities.js";
|
|
23
25
|
import { CounterpartService } from "./counterpart/service.js";
|
|
24
26
|
import { CAMERA_STREAM_TOOLS } from "./counterpart/camera-tools.js";
|
|
25
27
|
import { annotateTools } from "./tool-annotations.js";
|
|
@@ -100,9 +102,6 @@ function normalizeSlicerType(rawValue) {
|
|
|
100
102
|
}
|
|
101
103
|
return resolved;
|
|
102
104
|
}
|
|
103
|
-
function isBambuProjectSlicer(slicerType) {
|
|
104
|
-
return slicerType === "orcaslicer-bambulab" || slicerType === "bambustudio";
|
|
105
|
-
}
|
|
106
105
|
function firstExistingPath(candidates) {
|
|
107
106
|
return candidates.find((candidate) => fs.existsSync(candidate));
|
|
108
107
|
}
|
|
@@ -755,10 +754,14 @@ class ThreeDPrinterMCPServer {
|
|
|
755
754
|
const explicitModel = typeof args.bambu_model === "string" && args.bambu_model.trim() ? args.bambu_model : undefined;
|
|
756
755
|
// Generic OrcaSlicer keeps its user-supplied machine profile unless the
|
|
757
756
|
// call explicitly targets a Bambu model.
|
|
758
|
-
const bambuCli =
|
|
757
|
+
const bambuCli = usesBambuCli(slicerType, explicitModel !== undefined);
|
|
759
758
|
if (!bambuCli && parsePauseLayers(args.pause_at_layers)) {
|
|
760
759
|
throw new Error(`Pausing at a layer needs the Bambu Studio or OrcaSlicer CLI, not ${slicerType}.`);
|
|
761
760
|
}
|
|
761
|
+
// Copies only come from the Bambu-compatible CLI; elsewhere one copy would be sliced without a word.
|
|
762
|
+
if (!bambuCli && args.repetitions !== undefined && Number(args.repetitions) > 1) {
|
|
763
|
+
throw new Error(`Copies (repetitions) need the Bambu Studio or OrcaSlicer CLI with a Bambu model, not ${slicerType}; nothing was sliced.`);
|
|
764
|
+
}
|
|
762
765
|
let printerPreset;
|
|
763
766
|
let options;
|
|
764
767
|
if (bambuCli) {
|
|
@@ -2817,7 +2820,9 @@ class ThreeDPrinterMCPServer {
|
|
|
2817
2820
|
}
|
|
2818
2821
|
catch (printError) {
|
|
2819
2822
|
console.error(`Error starting 3MF print for ${threeMfFilename}:`, printError);
|
|
2820
|
-
|
|
2823
|
+
const wrapped = `Failed to start print: ${printError.message}`;
|
|
2824
|
+
// A printer refusal keeps its code through the wrap: clients act on it (src/refusal.ts).
|
|
2825
|
+
throw printError instanceof PrinterRefusal ? new PrinterRefusal(printError.code, wrapped) : new Error(wrapped);
|
|
2821
2826
|
}
|
|
2822
2827
|
break;
|
|
2823
2828
|
}
|
|
@@ -2910,7 +2915,8 @@ class ThreeDPrinterMCPServer {
|
|
|
2910
2915
|
catch (error) {
|
|
2911
2916
|
console.error(`Error calling tool ${name}:`, error);
|
|
2912
2917
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2913
|
-
|
|
2918
|
+
// A printer refusal adds its stable code (src/refusal.ts); the message and suggestion stay as they were.
|
|
2919
|
+
const structuredError = { ...this.toStructuredToolError(name, errorMessage, error), ...(error instanceof PrinterRefusal ? { code: error.code } : {}) };
|
|
2914
2920
|
return {
|
|
2915
2921
|
content: [
|
|
2916
2922
|
{
|
package/dist/printers/bambu.js
CHANGED
|
@@ -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
|
|
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
|
+
}
|
package/dist/refusal.js
ADDED
|
@@ -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
|
|
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))
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a slice from this server can do with the slicer it runs (counterpart_printer_info
|
|
3
|
+
* capabilities.slicing). Clients such as Counterpart offer supports, copies and auto-orient only
|
|
4
|
+
* when this says so, instead of knowing which slicer accepts what.
|
|
5
|
+
*
|
|
6
|
+
* The Bambu-compatible CLI route (Bambu Studio, FULU OrcaSlicer-bambulab, or OrcaSlicer given a
|
|
7
|
+
* Bambu model) takes all three. Generic OrcaSlicer takes the support keys over its process profile.
|
|
8
|
+
* PrusaSlicer, Slic3r and Cura take none of them through this server. For a Bambu printer on OrcaSlicer, the
|
|
9
|
+
* Bambu route needs the call's bambu_model; clients that report these to the person pass it.
|
|
10
|
+
*/
|
|
11
|
+
export interface SliceCapabilities {
|
|
12
|
+
supports: boolean;
|
|
13
|
+
copies: boolean;
|
|
14
|
+
orient: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare function isBambuProjectSlicer(slicerType: string): boolean;
|
|
17
|
+
/** True when slice_stl uses the Bambu-compatible CLI route for this slicer and target. */
|
|
18
|
+
export declare function usesBambuCli(slicerType: string, bambuModel: boolean): boolean;
|
|
19
|
+
export declare function sliceCapabilities(slicerType: string, bambuModel: boolean): SliceCapabilities;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function isBambuProjectSlicer(slicerType) {
|
|
2
|
+
return slicerType === "orcaslicer-bambulab" || slicerType === "bambustudio";
|
|
3
|
+
}
|
|
4
|
+
/** True when slice_stl uses the Bambu-compatible CLI route for this slicer and target. */
|
|
5
|
+
export function usesBambuCli(slicerType, bambuModel) {
|
|
6
|
+
return isBambuProjectSlicer(slicerType) || (slicerType === "orcaslicer" && bambuModel);
|
|
7
|
+
}
|
|
8
|
+
export function sliceCapabilities(slicerType, bambuModel) {
|
|
9
|
+
const bambuCli = usesBambuCli(slicerType, bambuModel);
|
|
10
|
+
return { supports: bambuCli || slicerType === "orcaslicer", copies: bambuCli, orient: bambuCli };
|
|
11
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-3d-printer-server",
|
|
3
3
|
"mcpName": "io.github.DMontgomery40/mcp-3d-printer-server",
|
|
4
|
-
"version": "1.2.
|
|
4
|
+
"version": "1.2.32",
|
|
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.
|
|
10
|
+
"version": "1.2.32",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "mcp-3d-printer-server",
|
|
15
|
-
"version": "1.2.
|
|
15
|
+
"version": "1.2.32",
|
|
16
16
|
"runtimeHint": "npx",
|
|
17
17
|
"transport": {
|
|
18
18
|
"type": "stdio"
|
|
@@ -63,6 +63,9 @@ export interface CounterpartCapabilities {
|
|
|
63
63
|
cancel: boolean;
|
|
64
64
|
print3mf: boolean;
|
|
65
65
|
gcode: boolean;
|
|
66
|
+
/** What slice_stl can apply with this server's slicer for this printer (src/slicer/capabilities.ts). For a Bambu printer on
|
|
67
|
+
* OrcaSlicer this assumes the call passes the printer's bambu_model, as Counterpart does; without one, OrcaSlicer slices generically. */
|
|
68
|
+
slicing?: { supports: boolean; copies: boolean; orient: boolean };
|
|
66
69
|
}
|
|
67
70
|
|
|
68
71
|
export interface PrinterIdentity {
|
|
@@ -153,6 +156,11 @@ export const PRINTER_INFO_OUTPUT_SCHEMA = {
|
|
|
153
156
|
cancel: { type: "boolean" },
|
|
154
157
|
print3mf: { type: "boolean" },
|
|
155
158
|
gcode: { type: "boolean" },
|
|
159
|
+
slicing: {
|
|
160
|
+
type: "object",
|
|
161
|
+
required: ["supports", "copies", "orient"],
|
|
162
|
+
properties: { supports: { type: "boolean" }, copies: { type: "boolean" }, orient: { type: "boolean" } },
|
|
163
|
+
},
|
|
156
164
|
},
|
|
157
165
|
},
|
|
158
166
|
},
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* adapters supply firmware reads; this module turns them into the shared
|
|
4
4
|
* results, detects capabilities, and fetches camera snapshots.
|
|
5
5
|
*/
|
|
6
|
+
import { sliceCapabilities } from "../slicer/capabilities.js";
|
|
6
7
|
import fs from "node:fs/promises";
|
|
7
8
|
import path from "node:path";
|
|
8
9
|
import { isAxiosError, type AxiosInstance } from "axios";
|
|
@@ -132,6 +133,7 @@ export class CounterpartService {
|
|
|
132
133
|
capabilities = {
|
|
133
134
|
progress: true, layers: true, camera: overrideSnapshotUrl() ? "http-snapshot" : "none", filaments: "ams",
|
|
134
135
|
pause: true, resume: true, cancel: true, print3mf: true, gcode: true,
|
|
136
|
+
slicing: sliceCapabilities(this.bambuProfileSource().slicerType ?? "bambustudio", true),
|
|
135
137
|
};
|
|
136
138
|
} else {
|
|
137
139
|
const { host, port, apiKey } = target;
|
|
@@ -144,6 +146,7 @@ export class CounterpartService {
|
|
|
144
146
|
capabilities = {
|
|
145
147
|
progress: true, layers, camera, filaments: "single",
|
|
146
148
|
pause: pauseResume, resume: pauseResume, cancel: true, print3mf: false, gcode: true,
|
|
149
|
+
slicing: sliceCapabilities(this.bambuProfileSource().slicerType ?? "prusaslicer", false),
|
|
147
150
|
};
|
|
148
151
|
}
|
|
149
152
|
const streamCandidates = await this.cameraCandidates(target).catch(() => []);
|
package/src/index.ts
CHANGED
|
@@ -37,6 +37,8 @@ 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";
|
|
41
|
+
import { isBambuProjectSlicer, usesBambuCli } from "./slicer/capabilities.js";
|
|
40
42
|
import { CounterpartService, type BambuControlRoute, type PrinterTarget } from "./counterpart/service.js";
|
|
41
43
|
import {CAMERA_STREAM_TOOLS} from "./counterpart/camera-tools.js";
|
|
42
44
|
import { annotateTools } from "./tool-annotations.js";
|
|
@@ -152,9 +154,6 @@ function normalizeSlicerType(rawValue: string | undefined): SlicerType {
|
|
|
152
154
|
return resolved;
|
|
153
155
|
}
|
|
154
156
|
|
|
155
|
-
function isBambuProjectSlicer(slicerType: SlicerType): boolean {
|
|
156
|
-
return slicerType === "orcaslicer-bambulab" || slicerType === "bambustudio";
|
|
157
|
-
}
|
|
158
157
|
|
|
159
158
|
function firstExistingPath(candidates: string[]): string | undefined {
|
|
160
159
|
return candidates.find((candidate) => fs.existsSync(candidate));
|
|
@@ -736,6 +735,8 @@ type StructuredToolError = {
|
|
|
736
735
|
suggestion: string;
|
|
737
736
|
message: string;
|
|
738
737
|
tool: string;
|
|
738
|
+
/** A stable refusal code (src/refusal.ts) when the printer refused; clients act on it, not on the words. */
|
|
739
|
+
code?: string;
|
|
739
740
|
slicer?: Record<string, unknown>;
|
|
740
741
|
/** The slicer's own result.json, verbatim. */
|
|
741
742
|
slicer_result?: Record<string, unknown>;
|
|
@@ -912,10 +913,14 @@ class ThreeDPrinterMCPServer {
|
|
|
912
913
|
typeof args.bambu_model === "string" && args.bambu_model.trim() ? args.bambu_model : undefined;
|
|
913
914
|
// Generic OrcaSlicer keeps its user-supplied machine profile unless the
|
|
914
915
|
// call explicitly targets a Bambu model.
|
|
915
|
-
const bambuCli =
|
|
916
|
+
const bambuCli = usesBambuCli(slicerType, explicitModel !== undefined);
|
|
916
917
|
if (!bambuCli && parsePauseLayers(args.pause_at_layers)) {
|
|
917
918
|
throw new Error(`Pausing at a layer needs the Bambu Studio or OrcaSlicer CLI, not ${slicerType}.`);
|
|
918
919
|
}
|
|
920
|
+
// Copies only come from the Bambu-compatible CLI; elsewhere one copy would be sliced without a word.
|
|
921
|
+
if (!bambuCli && args.repetitions !== undefined && Number(args.repetitions) > 1) {
|
|
922
|
+
throw new Error(`Copies (repetitions) need the Bambu Studio or OrcaSlicer CLI with a Bambu model, not ${slicerType}; nothing was sliced.`);
|
|
923
|
+
}
|
|
919
924
|
|
|
920
925
|
let printerPreset: string | undefined;
|
|
921
926
|
let options: BambuSliceOptions | undefined;
|
|
@@ -3185,7 +3190,9 @@ class ThreeDPrinterMCPServer {
|
|
|
3185
3190
|
// Report what the printer did, not merely that a command was published.
|
|
3186
3191
|
} catch (printError) {
|
|
3187
3192
|
console.error(`Error starting 3MF print for ${threeMfFilename}:`, printError);
|
|
3188
|
-
|
|
3193
|
+
const wrapped = `Failed to start print: ${(printError as Error).message}`;
|
|
3194
|
+
// A printer refusal keeps its code through the wrap: clients act on it (src/refusal.ts).
|
|
3195
|
+
throw printError instanceof PrinterRefusal ? new PrinterRefusal(printError.code, wrapped) : new Error(wrapped);
|
|
3189
3196
|
}
|
|
3190
3197
|
|
|
3191
3198
|
break;
|
|
@@ -3342,7 +3349,8 @@ class ThreeDPrinterMCPServer {
|
|
|
3342
3349
|
console.error(`Error calling tool ${name}:`, error);
|
|
3343
3350
|
|
|
3344
3351
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3345
|
-
|
|
3352
|
+
// A printer refusal adds its stable code (src/refusal.ts); the message and suggestion stay as they were.
|
|
3353
|
+
const structuredError = { ...this.toStructuredToolError(name, errorMessage, error), ...(error instanceof PrinterRefusal ? { code: error.code } : {}) };
|
|
3346
3354
|
|
|
3347
3355
|
return {
|
|
3348
3356
|
content: [
|
package/src/printers/bambu.ts
CHANGED
|
@@ -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
|
|
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
|
|
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.");
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a slice from this server can do with the slicer it runs (counterpart_printer_info
|
|
3
|
+
* capabilities.slicing). Clients such as Counterpart offer supports, copies and auto-orient only
|
|
4
|
+
* when this says so, instead of knowing which slicer accepts what.
|
|
5
|
+
*
|
|
6
|
+
* The Bambu-compatible CLI route (Bambu Studio, FULU OrcaSlicer-bambulab, or OrcaSlicer given a
|
|
7
|
+
* Bambu model) takes all three. Generic OrcaSlicer takes the support keys over its process profile.
|
|
8
|
+
* PrusaSlicer, Slic3r and Cura take none of them through this server. For a Bambu printer on OrcaSlicer, the
|
|
9
|
+
* Bambu route needs the call's bambu_model; clients that report these to the person pass it.
|
|
10
|
+
*/
|
|
11
|
+
export interface SliceCapabilities {
|
|
12
|
+
supports: boolean;
|
|
13
|
+
copies: boolean;
|
|
14
|
+
orient: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function isBambuProjectSlicer(slicerType: string): boolean {
|
|
18
|
+
return slicerType === "orcaslicer-bambulab" || slicerType === "bambustudio";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** True when slice_stl uses the Bambu-compatible CLI route for this slicer and target. */
|
|
22
|
+
export function usesBambuCli(slicerType: string, bambuModel: boolean): boolean {
|
|
23
|
+
return isBambuProjectSlicer(slicerType) || (slicerType === "orcaslicer" && bambuModel);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function sliceCapabilities(slicerType: string, bambuModel: boolean): SliceCapabilities {
|
|
27
|
+
const bambuCli = usesBambuCli(slicerType, bambuModel);
|
|
28
|
+
return { supports: bambuCli || slicerType === "orcaslicer", copies: bambuCli, orient: bambuCli };
|
|
29
|
+
}
|