mcp-3d-printer-server 1.2.29 → 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 +12 -0
- package/README.md +3 -0
- package/dist/index.js +9 -3
- 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/print-quality.d.ts +26 -0
- package/dist/slicer/print-quality.js +53 -0
- package/dist/stl/stl-manipulator.js +1 -1
- package/package.json +3 -1
- package/server.json +2 -2
- package/src/index.ts +11 -3
- package/src/printers/bambu.ts +2 -1
- package/src/safety/printer-state.ts +2 -1
- package/src/slicer/print-quality.ts +55 -0
- package/src/stl/stl-manipulator.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
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
|
+
|
|
9
|
+
## [1.2.30] – 2026-11-04
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `slice_stl` and `slice_with_template` take `quality` (`draft`, `standard`, `fine`) and `strength` (`standard`, `strong`, `max`). The server turns them into the slicer's own settings: Draft and Fine set the layer height for the nozzle in use (0.28 and 0.12 mm on a 0.4 mm nozzle, scaled with it), Strong and Max set walls and infill (4 walls at 25%, 6 at 40%), and Standard leaves the resolved preset alone. An explicit `layer_height`, `wall_loops` or `sparse_infill_density` still wins. Clients such as Counterpart can now send the person's choice instead of carrying slicer numbers. Checked with stand-in slicers only. For PrusaSlicer, Slic3r and generic OrcaSlicer the layer height scales with `nozzle_diameter` (or `NOZZLE_DIAMETER`), which must match the profile's nozzle; Cura takes only Standard. Same change as bambu-printer-mcp 1.1.56.
|
|
14
|
+
|
|
3
15
|
## [1.2.29] – 2026-11-04
|
|
4
16
|
|
|
5
17
|
### Changed
|
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
|
|
@@ -688,6 +689,8 @@ Options for the Bambu-compatible path:
|
|
|
688
689
|
| `scale`, `rotate`, `rotate_x`, `rotate_y` | Transform before slicing (uniform scale; rotations in degrees). For OrcaSlicer STL input the server applies them to a mesh copy in Orca's order to avoid a CLI crash; see [OrcaSlicer CLI limits](https://github.com/DMontgomery40/mcp-3D-printer-server/blob/main/docs/SLICING.md#bambu-compatible-slicing) |
|
|
689
690
|
| `enable_timelapse`, `allow_mix_temp` | Insert timelapse parking moves; allow filaments with different temperature needs on one plate |
|
|
690
691
|
|
|
692
|
+
`slice_stl` and `slice_with_template` take `quality` (`draft`, `standard`, `fine`) and `strength` (`standard`, `strong`, `max`), so a client can send the person's choice without slicer numbers. The server picks the layer height for the nozzle (0.28 and 0.12 mm on a 0.4 mm nozzle; it scales with `nozzle_diameter`, or `NOZZLE_DIAMETER` when that's not given, which for PrusaSlicer, Slic3r and generic OrcaSlicer must match the nozzle in `slicer_profile`) and the walls and infill (4 walls at 25%, 6 at 40%). Standard keeps the preset; explicit values win. Cura takes only Standard.
|
|
693
|
+
|
|
691
694
|
`layer_height` (mm), `wall_loops` (a whole number), and `sparse_infill_density` (such as `"15%"`) are written over the selected profile on every slicer: as the process keys `layer_height`, `wall_loops`, and `sparse_infill_density` for Bambu Studio and OrcaSlicer, and as `--layer-height`, `--perimeters`, and `--fill-density` for PrusaSlicer and Slic3r. They win over a template. Cura refuses them; set them in its definition file. Your profile files are never modified.
|
|
692
695
|
|
|
693
696
|
`enable_support` (true or false), `support_type` (`normal(auto)`, `tree(auto)`, `normal(manual)` or `tree(manual)`) and `support_threshold_angle` (0 to 90 degrees; 0 lets the slicer decide) are written over the selected process profile as the same-named Bambu Studio and OrcaSlicer keys. PrusaSlicer, Slic3r and Cura refuse them; set supports in their own profile.
|
package/dist/index.js
CHANGED
|
@@ -19,6 +19,8 @@ import * as THREE from 'three';
|
|
|
19
19
|
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
|
+
import { PRINT_QUALITY_PROPERTIES, withPrintQuality } from "./slicer/print-quality.js";
|
|
23
|
+
import { PrinterRefusal } from "./refusal.js";
|
|
22
24
|
import { CounterpartService } from "./counterpart/service.js";
|
|
23
25
|
import { CAMERA_STREAM_TOOLS } from "./counterpart/camera-tools.js";
|
|
24
26
|
import { annotateTools } from "./tool-annotations.js";
|
|
@@ -306,6 +308,7 @@ const SLICE_TOOL_PROPERTIES = {
|
|
|
306
308
|
min_save: { type: "boolean", description: "Bambu-compatible slicing: write a smaller output 3MF (--min-save)." },
|
|
307
309
|
skip_modified_gcodes: { type: "boolean", description: "Bambu-compatible slicing: ignore custom G-code embedded in an input 3MF (--skip-modified-gcodes)." },
|
|
308
310
|
slice_plate: { type: "number", description: "Bambu-compatible slicing: plate number to slice; 0 slices all plates (default)." },
|
|
311
|
+
...PRINT_QUALITY_PROPERTIES,
|
|
309
312
|
layer_height: {
|
|
310
313
|
type: "number", exclusiveMinimum: 0, maximum: 2,
|
|
311
314
|
description: "Layer height in mm, written over the selected process profile (Orca/Bambu Studio layer_height; PrusaSlicer --layer-height). Not mapped for Cura.",
|
|
@@ -748,7 +751,7 @@ class ThreeDPrinterMCPServer {
|
|
|
748
751
|
if (!args?.stl_path) {
|
|
749
752
|
throw new Error("Missing required parameter: stl_path");
|
|
750
753
|
}
|
|
751
|
-
const processOverrides = parseProcessOverrides(args);
|
|
754
|
+
const processOverrides = parseProcessOverrides(withPrintQuality(args, Number(args.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER)));
|
|
752
755
|
const slicerType = normalizeSlicerType(String(args.slicer_type || DEFAULT_SLICER_TYPE));
|
|
753
756
|
const explicitModel = typeof args.bambu_model === "string" && args.bambu_model.trim() ? args.bambu_model : undefined;
|
|
754
757
|
// Generic OrcaSlicer keeps its user-supplied machine profile unless the
|
|
@@ -2815,7 +2818,9 @@ class ThreeDPrinterMCPServer {
|
|
|
2815
2818
|
}
|
|
2816
2819
|
catch (printError) {
|
|
2817
2820
|
console.error(`Error starting 3MF print for ${threeMfFilename}:`, printError);
|
|
2818
|
-
|
|
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);
|
|
2819
2824
|
}
|
|
2820
2825
|
break;
|
|
2821
2826
|
}
|
|
@@ -2908,7 +2913,8 @@ class ThreeDPrinterMCPServer {
|
|
|
2908
2913
|
catch (error) {
|
|
2909
2914
|
console.error(`Error calling tool ${name}:`, error);
|
|
2910
2915
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2911
|
-
|
|
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 } : {}) };
|
|
2912
2918
|
return {
|
|
2913
2919
|
content: [
|
|
2914
2920
|
{
|
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,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* slice_stl's `quality` and `strength` (PRD §8.4): a client's print-sheet choice, mapped here to the
|
|
3
|
+
* slicer's own numbers so no client carries layer heights, wall counts or infill. Draft and Fine set
|
|
4
|
+
* the layer height for the nozzle in use (0.28 and 0.12 mm on a 0.4 mm nozzle, scaled with it); Strong
|
|
5
|
+
* and Max set walls and infill. Standard sends nothing: the resolved preset decides. An explicit
|
|
6
|
+
* layer_height, wall_loops or sparse_infill_density in the same call wins over these.
|
|
7
|
+
*/
|
|
8
|
+
export declare const PRINT_QUALITIES: readonly ["draft", "standard", "fine"];
|
|
9
|
+
export declare const PRINT_STRENGTHS: readonly ["standard", "strong", "max"];
|
|
10
|
+
export type PrintQuality = (typeof PRINT_QUALITIES)[number];
|
|
11
|
+
export type PrintStrength = (typeof PRINT_STRENGTHS)[number];
|
|
12
|
+
/** The call's arguments with `quality` and `strength` written out as slicer settings. */
|
|
13
|
+
export declare function withPrintQuality(args: Record<string, unknown>, nozzleDiameter?: number): Record<string, unknown>;
|
|
14
|
+
/** slice_stl input-schema properties for both arguments. */
|
|
15
|
+
export declare const PRINT_QUALITY_PROPERTIES: {
|
|
16
|
+
readonly quality: {
|
|
17
|
+
readonly type: "string";
|
|
18
|
+
readonly enum: readonly ["draft", "standard", "fine"];
|
|
19
|
+
readonly description: "Print quality: draft (thicker layers, faster), standard (the preset's own layer height) or fine (thinner layers). The server picks the layer height for the nozzle in use; an explicit layer_height wins.";
|
|
20
|
+
};
|
|
21
|
+
readonly strength: {
|
|
22
|
+
readonly type: "string";
|
|
23
|
+
readonly enum: readonly ["standard", "strong", "max"];
|
|
24
|
+
readonly description: "Part strength: standard (the preset's walls and infill), strong or max (more walls and denser infill). The server picks the values; explicit wall_loops or sparse_infill_density win.";
|
|
25
|
+
};
|
|
26
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* slice_stl's `quality` and `strength` (PRD §8.4): a client's print-sheet choice, mapped here to the
|
|
3
|
+
* slicer's own numbers so no client carries layer heights, wall counts or infill. Draft and Fine set
|
|
4
|
+
* the layer height for the nozzle in use (0.28 and 0.12 mm on a 0.4 mm nozzle, scaled with it); Strong
|
|
5
|
+
* and Max set walls and infill. Standard sends nothing: the resolved preset decides. An explicit
|
|
6
|
+
* layer_height, wall_loops or sparse_infill_density in the same call wins over these.
|
|
7
|
+
*/
|
|
8
|
+
export const PRINT_QUALITIES = ["draft", "standard", "fine"];
|
|
9
|
+
export const PRINT_STRENGTHS = ["standard", "strong", "max"];
|
|
10
|
+
/** Layer heights on the reference 0.4 mm nozzle; other nozzles scale from it. */
|
|
11
|
+
const REFERENCE_NOZZLE_MM = 0.4;
|
|
12
|
+
const LAYER_MM_ON_REFERENCE = { draft: 0.28, fine: 0.12 };
|
|
13
|
+
const STRENGTH = {
|
|
14
|
+
strong: { wall_loops: 4, sparse_infill_density: "25%" },
|
|
15
|
+
max: { wall_loops: 6, sparse_infill_density: "40%" },
|
|
16
|
+
};
|
|
17
|
+
/** The call's arguments with `quality` and `strength` written out as slicer settings. */
|
|
18
|
+
export function withPrintQuality(args, nozzleDiameter) {
|
|
19
|
+
if (args.quality === undefined && args.strength === undefined)
|
|
20
|
+
return args;
|
|
21
|
+
const quality = args.quality === undefined ? "standard" : args.quality;
|
|
22
|
+
const strength = args.strength === undefined ? "standard" : args.strength;
|
|
23
|
+
if (typeof quality !== "string" || !PRINT_QUALITIES.includes(quality)) {
|
|
24
|
+
throw new Error(`quality must be one of ${PRINT_QUALITIES.join(", ")}.`);
|
|
25
|
+
}
|
|
26
|
+
if (typeof strength !== "string" || !PRINT_STRENGTHS.includes(strength)) {
|
|
27
|
+
throw new Error(`strength must be one of ${PRINT_STRENGTHS.join(", ")}.`);
|
|
28
|
+
}
|
|
29
|
+
const out = { ...args };
|
|
30
|
+
const scale = typeof nozzleDiameter === "number" && Number.isFinite(nozzleDiameter) && nozzleDiameter > 0 ? nozzleDiameter / REFERENCE_NOZZLE_MM : 1;
|
|
31
|
+
const layer = LAYER_MM_ON_REFERENCE[quality];
|
|
32
|
+
if (layer !== undefined && out.layer_height === undefined)
|
|
33
|
+
out.layer_height = Math.round(layer * scale * 100) / 100;
|
|
34
|
+
const walls = STRENGTH[strength];
|
|
35
|
+
if (walls) {
|
|
36
|
+
if (out.wall_loops === undefined)
|
|
37
|
+
out.wall_loops = walls.wall_loops;
|
|
38
|
+
if (out.sparse_infill_density === undefined)
|
|
39
|
+
out.sparse_infill_density = walls.sparse_infill_density;
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
/** slice_stl input-schema properties for both arguments. */
|
|
44
|
+
export const PRINT_QUALITY_PROPERTIES = {
|
|
45
|
+
quality: {
|
|
46
|
+
type: "string", enum: [...PRINT_QUALITIES],
|
|
47
|
+
description: "Print quality: draft (thicker layers, faster), standard (the preset's own layer height) or fine (thinner layers). The server picks the layer height for the nozzle in use; an explicit layer_height wins.",
|
|
48
|
+
},
|
|
49
|
+
strength: {
|
|
50
|
+
type: "string", enum: [...PRINT_STRENGTHS],
|
|
51
|
+
description: "Part strength: standard (the preset's walls and infill), strong or max (more walls and denser infill). The server picks the values; explicit wall_loops or sparse_infill_density win.",
|
|
52
|
+
},
|
|
53
|
+
};
|
|
@@ -1019,7 +1019,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
1019
1019
|
break;
|
|
1020
1020
|
case 'cura':
|
|
1021
1021
|
if (Object.keys(orcaProcessOverrides(processOverrides)).length) {
|
|
1022
|
-
throw new SlicerError('preparation', 'layer_height, wall_loops, sparse_infill_density and the support settings are not mapped for Cura; set them in the Cura definition passed as slicer_profile.', { slicerType, slicerPath });
|
|
1022
|
+
throw new SlicerError('preparation', 'quality, strength, layer_height, wall_loops, sparse_infill_density and the support settings are not mapped for Cura; set them in the Cura definition passed as slicer_profile.', { slicerType, slicerPath });
|
|
1023
1023
|
}
|
|
1024
1024
|
// CuraEngine CLI args are different, often requiring -s for settings
|
|
1025
1025
|
// Example: curaengine slice -v -j cura_settings.json -s layer_height=0.2 -o output.gcode -l input.stl
|
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.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.
|
|
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.
|
|
15
|
+
"version": "1.2.31",
|
|
16
16
|
"runtimeHint": "npx",
|
|
17
17
|
"transport": {
|
|
18
18
|
"type": "stdio"
|
package/src/index.ts
CHANGED
|
@@ -36,6 +36,8 @@ import * as THREE from 'three';
|
|
|
36
36
|
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
|
+
import { PRINT_QUALITY_PROPERTIES, withPrintQuality } from "./slicer/print-quality.js";
|
|
40
|
+
import { PrinterRefusal } from "./refusal.js";
|
|
39
41
|
import { CounterpartService, type BambuControlRoute, type PrinterTarget } from "./counterpart/service.js";
|
|
40
42
|
import {CAMERA_STREAM_TOOLS} from "./counterpart/camera-tools.js";
|
|
41
43
|
import { annotateTools } from "./tool-annotations.js";
|
|
@@ -384,6 +386,7 @@ const SLICE_TOOL_PROPERTIES = {
|
|
|
384
386
|
min_save: { type: "boolean", description: "Bambu-compatible slicing: write a smaller output 3MF (--min-save)." },
|
|
385
387
|
skip_modified_gcodes: { type: "boolean", description: "Bambu-compatible slicing: ignore custom G-code embedded in an input 3MF (--skip-modified-gcodes)." },
|
|
386
388
|
slice_plate: { type: "number", description: "Bambu-compatible slicing: plate number to slice; 0 slices all plates (default)." },
|
|
389
|
+
...PRINT_QUALITY_PROPERTIES,
|
|
387
390
|
layer_height: {
|
|
388
391
|
type: "number", exclusiveMinimum: 0, maximum: 2,
|
|
389
392
|
description: "Layer height in mm, written over the selected process profile (Orca/Bambu Studio layer_height; PrusaSlicer --layer-height). Not mapped for Cura.",
|
|
@@ -734,6 +737,8 @@ type StructuredToolError = {
|
|
|
734
737
|
suggestion: string;
|
|
735
738
|
message: string;
|
|
736
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;
|
|
737
742
|
slicer?: Record<string, unknown>;
|
|
738
743
|
/** The slicer's own result.json, verbatim. */
|
|
739
744
|
slicer_result?: Record<string, unknown>;
|
|
@@ -904,7 +909,7 @@ class ThreeDPrinterMCPServer {
|
|
|
904
909
|
if (!args?.stl_path) {
|
|
905
910
|
throw new Error("Missing required parameter: stl_path");
|
|
906
911
|
}
|
|
907
|
-
const processOverrides = parseProcessOverrides(args);
|
|
912
|
+
const processOverrides = parseProcessOverrides(withPrintQuality(args, Number(args.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER)));
|
|
908
913
|
const slicerType = normalizeSlicerType(String(args.slicer_type || DEFAULT_SLICER_TYPE));
|
|
909
914
|
const explicitModel =
|
|
910
915
|
typeof args.bambu_model === "string" && args.bambu_model.trim() ? args.bambu_model : undefined;
|
|
@@ -3183,7 +3188,9 @@ class ThreeDPrinterMCPServer {
|
|
|
3183
3188
|
// Report what the printer did, not merely that a command was published.
|
|
3184
3189
|
} catch (printError) {
|
|
3185
3190
|
console.error(`Error starting 3MF print for ${threeMfFilename}:`, printError);
|
|
3186
|
-
|
|
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);
|
|
3187
3194
|
}
|
|
3188
3195
|
|
|
3189
3196
|
break;
|
|
@@ -3340,7 +3347,8 @@ class ThreeDPrinterMCPServer {
|
|
|
3340
3347
|
console.error(`Error calling tool ${name}:`, error);
|
|
3341
3348
|
|
|
3342
3349
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3343
|
-
|
|
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 } : {}) };
|
|
3344
3352
|
|
|
3345
3353
|
return {
|
|
3346
3354
|
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,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* slice_stl's `quality` and `strength` (PRD §8.4): a client's print-sheet choice, mapped here to the
|
|
3
|
+
* slicer's own numbers so no client carries layer heights, wall counts or infill. Draft and Fine set
|
|
4
|
+
* the layer height for the nozzle in use (0.28 and 0.12 mm on a 0.4 mm nozzle, scaled with it); Strong
|
|
5
|
+
* and Max set walls and infill. Standard sends nothing: the resolved preset decides. An explicit
|
|
6
|
+
* layer_height, wall_loops or sparse_infill_density in the same call wins over these.
|
|
7
|
+
*/
|
|
8
|
+
export const PRINT_QUALITIES = ["draft", "standard", "fine"] as const;
|
|
9
|
+
export const PRINT_STRENGTHS = ["standard", "strong", "max"] as const;
|
|
10
|
+
|
|
11
|
+
export type PrintQuality = (typeof PRINT_QUALITIES)[number];
|
|
12
|
+
export type PrintStrength = (typeof PRINT_STRENGTHS)[number];
|
|
13
|
+
|
|
14
|
+
/** Layer heights on the reference 0.4 mm nozzle; other nozzles scale from it. */
|
|
15
|
+
const REFERENCE_NOZZLE_MM = 0.4;
|
|
16
|
+
const LAYER_MM_ON_REFERENCE: Partial<Record<PrintQuality, number>> = { draft: 0.28, fine: 0.12 };
|
|
17
|
+
const STRENGTH: Partial<Record<PrintStrength, { wall_loops: number; sparse_infill_density: string }>> = {
|
|
18
|
+
strong: { wall_loops: 4, sparse_infill_density: "25%" },
|
|
19
|
+
max: { wall_loops: 6, sparse_infill_density: "40%" },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** The call's arguments with `quality` and `strength` written out as slicer settings. */
|
|
23
|
+
export function withPrintQuality(args: Record<string, unknown>, nozzleDiameter?: number): Record<string, unknown> {
|
|
24
|
+
if (args.quality === undefined && args.strength === undefined) return args;
|
|
25
|
+
const quality = args.quality === undefined ? "standard" : args.quality;
|
|
26
|
+
const strength = args.strength === undefined ? "standard" : args.strength;
|
|
27
|
+
if (typeof quality !== "string" || !(PRINT_QUALITIES as readonly string[]).includes(quality)) {
|
|
28
|
+
throw new Error(`quality must be one of ${PRINT_QUALITIES.join(", ")}.`);
|
|
29
|
+
}
|
|
30
|
+
if (typeof strength !== "string" || !(PRINT_STRENGTHS as readonly string[]).includes(strength)) {
|
|
31
|
+
throw new Error(`strength must be one of ${PRINT_STRENGTHS.join(", ")}.`);
|
|
32
|
+
}
|
|
33
|
+
const out: Record<string, unknown> = { ...args };
|
|
34
|
+
const scale = typeof nozzleDiameter === "number" && Number.isFinite(nozzleDiameter) && nozzleDiameter > 0 ? nozzleDiameter / REFERENCE_NOZZLE_MM : 1;
|
|
35
|
+
const layer = LAYER_MM_ON_REFERENCE[quality as PrintQuality];
|
|
36
|
+
if (layer !== undefined && out.layer_height === undefined) out.layer_height = Math.round(layer * scale * 100) / 100;
|
|
37
|
+
const walls = STRENGTH[strength as PrintStrength];
|
|
38
|
+
if (walls) {
|
|
39
|
+
if (out.wall_loops === undefined) out.wall_loops = walls.wall_loops;
|
|
40
|
+
if (out.sparse_infill_density === undefined) out.sparse_infill_density = walls.sparse_infill_density;
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** slice_stl input-schema properties for both arguments. */
|
|
46
|
+
export const PRINT_QUALITY_PROPERTIES = {
|
|
47
|
+
quality: {
|
|
48
|
+
type: "string", enum: [...PRINT_QUALITIES],
|
|
49
|
+
description: "Print quality: draft (thicker layers, faster), standard (the preset's own layer height) or fine (thinner layers). The server picks the layer height for the nozzle in use; an explicit layer_height wins.",
|
|
50
|
+
},
|
|
51
|
+
strength: {
|
|
52
|
+
type: "string", enum: [...PRINT_STRENGTHS],
|
|
53
|
+
description: "Part strength: standard (the preset's walls and infill), strong or max (more walls and denser infill). The server picks the values; explicit wall_loops or sparse_infill_density win.",
|
|
54
|
+
},
|
|
55
|
+
} as const;
|
|
@@ -1317,7 +1317,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
1317
1317
|
case 'cura':
|
|
1318
1318
|
if (Object.keys(orcaProcessOverrides(processOverrides)).length) {
|
|
1319
1319
|
throw new SlicerError('preparation',
|
|
1320
|
-
'layer_height, wall_loops, sparse_infill_density and the support settings are not mapped for Cura; set them in the Cura definition passed as slicer_profile.',
|
|
1320
|
+
'quality, strength, layer_height, wall_loops, sparse_infill_density and the support settings are not mapped for Cura; set them in the Cura definition passed as slicer_profile.',
|
|
1321
1321
|
{ slicerType, slicerPath });
|
|
1322
1322
|
}
|
|
1323
1323
|
// CuraEngine CLI args are different, often requiring -s for settings
|