mcp-3d-printer-server 1.2.26 → 1.2.28

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,23 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.2.28] – 2026-11-04
4
+
5
+ ### Added
6
+
7
+ - `slice_stl` takes `enable_support`, `support_type` (`normal(auto)`, `tree(auto)`, `normal(manual)`, `tree(manual)`) and `support_threshold_angle`, written over the selected process profile like `layer_height` and `wall_loops`. Bambu Studio and OrcaSlicer only; PrusaSlicer and Cura refuse them. Tested with the fake Bambu Studio CLI install; bambu-printer-mcp 1.1.54 checks the same keys against the installed Bambu Studio 02.08.02.61. No printer was involved.
8
+
9
+ ## [1.2.27] – 2026-11-04
10
+
11
+ ### Added
12
+
13
+ - Every tool now carries the four MCP tool annotations (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`) from one table, `src/tool-annotations.ts`, so clients can tell reads from actions without parsing descriptions. Before, 7 of 45 tools had partial annotations. Destructive means the tool may end or replace something that exists: `cancel_print`, `upload_gcode` (same file name), `camera_snapshot` with `save_path`, `get_slice_settings` (a same-named 3MF replaces the extracted settings file), `save_template`, `counterpart_print_route` with a route, and the pass-through calls `fulu_bambu_network_rpc`, `blender_mcp_call` and `fusion_mcp_call`. Starting a print and heating are marked additive, matching the no-confirmation decision. Annotations are hints for clients; every safety check still applies. A test lists the tools over stdio and fails for any tool without all four.
14
+
15
+ ### Fixed
16
+
17
+ - Resume a Bambu job that the printer paused itself. A P1S paused by Pause or by a `pause_at_layers` stop reports `print_error` 0300_8013 ("paused by the user" / "paused by the pause command in the file"), and Resume refused it as a print error, so a paused job could only be resumed at the printer. The notice is excused only for Resume, only while the printer reports PAUSE; any other print error still refuses, and starting a job still needs no error. Ported from bambu-printer-mcp.
18
+ - FULU's BambuNetwork agent now receives the client headers FULU's own GUI sends before `net.start` (Bambu Studio 02.05.02.51 on Linux, with a per-install device id kept in the config folder). Bambu's cloud issues the printer-control certificate and creates print tasks for this client; without the headers the agent was an unknown client. `BAMBU_NETWORK_CLIENT_NAME` and `BAMBU_NETWORK_CLIENT_VERSION` override the name and version. Ported from bambu-printer-mcp.
19
+ - Evidence: mocked Bambu MQTT/FTPS boundaries and the fake FULU host only; no printer was contacted.
20
+
3
21
  ## [1.2.26] – 2026-11-04
4
22
 
5
23
  ### Changed
package/README.md CHANGED
@@ -690,6 +690,8 @@ Options for the Bambu-compatible path:
690
690
 
691
691
  `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
692
 
693
+ `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.
694
+
693
695
  Generic slicers keep their own profile formats: PrusaSlicer and Slic3r load one exported config, plus an optional filament preset `.ini` from `load_filaments`, and generic OrcaSlicer takes `machine.json;process.json`, optionally followed by `|filament.json`. `slicer_type`, `slicer_profile`, and `filament_profile` fall back to `SLICER_TYPE`, `SLICER_PROFILE`, and `FILAMENT_PROFILE`. The slicer executable comes from `SLICER_PATH`; a per-call `slicer_path` requires `MCP_ALLOW_EXECUTABLE_ARG=1`. See the [slicing guide](https://github.com/DMontgomery40/mcp-3D-printer-server/blob/main/docs/SLICING.md#bambu-compatible-slicing).
694
696
 
695
697
  #### slice_with_template
@@ -106,6 +106,17 @@ export type BambuNetworkBridgeStatus = {
106
106
  export declare function bambuNetworkCertDir(configDir: string, env?: NodeJS.ProcessEnv): string;
107
107
  export declare function prepareBambuNetworkCertDir(configDir: string, env?: NodeJS.ProcessEnv): string;
108
108
  export declare function defaultBambuNetworkConfigDir(): string;
109
+ /**
110
+ * The client identity FULU's GUI gives the plug-in before agent start
111
+ * (GUI_App::init_http_extra_header). Through the Linux bridge FULU presents
112
+ * itself as Bambu Studio at the version its plug-in ABI belongs to
113
+ * (PJarczakLinuxBridgeConfig.cpp forced_client_version, "02.05.02.51").
114
+ * Bambu's cloud issues the printer-control certificate and creates print
115
+ * tasks for this client, so an agent started without it is an unknown client.
116
+ * The device id is kept in the config folder so it stays the same per install.
117
+ * Ported from bambu-printer-mcp src/bambu-network-bridge.ts.
118
+ */
119
+ export declare function bambuNetworkClientHeaders(configDir: string, env?: NodeJS.ProcessEnv): Record<string, string>;
109
120
  export declare class BambuNetworkBridge {
110
121
  private child?;
111
122
  private commandLine?;
@@ -1,5 +1,5 @@
1
1
  import { spawn } from "node:child_process";
2
- import { createHash } from "node:crypto";
2
+ import { createHash, randomUUID } from "node:crypto";
3
3
  import fs from "node:fs";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
@@ -89,6 +89,41 @@ function trimEnv(value) {
89
89
  const trimmed = value?.trim();
90
90
  return trimmed ? trimmed : undefined;
91
91
  }
92
+ /**
93
+ * The client identity FULU's GUI gives the plug-in before agent start
94
+ * (GUI_App::init_http_extra_header). Through the Linux bridge FULU presents
95
+ * itself as Bambu Studio at the version its plug-in ABI belongs to
96
+ * (PJarczakLinuxBridgeConfig.cpp forced_client_version, "02.05.02.51").
97
+ * Bambu's cloud issues the printer-control certificate and creates print
98
+ * tasks for this client, so an agent started without it is an unknown client.
99
+ * The device id is kept in the config folder so it stays the same per install.
100
+ * Ported from bambu-printer-mcp src/bambu-network-bridge.ts.
101
+ */
102
+ export function bambuNetworkClientHeaders(configDir, env = process.env) {
103
+ const idFile = path.join(configDir, "client-device-id");
104
+ let deviceId = "";
105
+ try {
106
+ deviceId = fs.readFileSync(idFile, "utf8").trim();
107
+ }
108
+ catch { /* first start */ }
109
+ if (!deviceId) {
110
+ deviceId = randomUUID();
111
+ try {
112
+ fs.mkdirSync(configDir, { recursive: true });
113
+ fs.writeFileSync(idFile, `${deviceId}\n`, { mode: 0o600 });
114
+ }
115
+ catch { /* still send this run's id */ }
116
+ }
117
+ return {
118
+ "X-BBL-Client-Type": "slicer",
119
+ "X-BBL-Client-Name": trimEnv(env.BAMBU_NETWORK_CLIENT_NAME) ?? "BambuStudio",
120
+ "X-BBL-Client-Version": trimEnv(env.BAMBU_NETWORK_CLIENT_VERSION) ?? "02.05.02.51",
121
+ "X-BBL-OS-Type": "linux",
122
+ "X-BBL-OS-Version": os.release(),
123
+ "X-BBL-Device-ID": deviceId,
124
+ "X-BBL-Language": "en",
125
+ };
126
+ }
92
127
  function fileExists(filePath) {
93
128
  try {
94
129
  return fs.existsSync(filePath);
@@ -350,6 +385,8 @@ export class BambuNetworkBridge {
350
385
  // plug-in verifies a LAN printer with printer.cer from this folder, and
351
386
  // FULU's host crashes on connect_printer when no folder was ever set.
352
387
  const certificates = await withAgent("net.set_cert_file", { folder: certDir, filename: "slicer_base64.cer" }, false);
388
+ // FULU's GUI order: set_cert_file, then the client headers, then start.
389
+ await withAgent("net.set_extra_http_header", { headers: bambuNetworkClientHeaders(configDir) }, false);
353
390
  await withAgent("net.set_country_code", { country_code: countryCode });
354
391
  await withAgent("net.start");
355
392
  // The plug-in runs callbacks through queue_on_main; FULU's host executes
package/dist/index.js CHANGED
@@ -17,10 +17,11 @@ import { createServer as createHttpServer } from "node:http";
17
17
  import { randomUUID } from "node:crypto";
18
18
  import * as THREE from 'three';
19
19
  import { PrinterFactory } from "./printers/printer-factory.js";
20
- import { STLManipulator } from "./stl/stl-manipulator.js";
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 { CounterpartService } from "./counterpart/service.js";
23
23
  import { CAMERA_STREAM_TOOLS } from "./counterpart/camera-tools.js";
24
+ import { annotateTools } from "./tool-annotations.js";
24
25
  import { CAMERA_SNAPSHOT_OUTPUT_SCHEMA, CONTROL_OUTPUT_SCHEMA, FILAMENTS_OUTPUT_SCHEMA, PRINT_PROGRESS_OUTPUT_SCHEMA, PRINTER_INFO_OUTPUT_SCHEMA, } from "./counterpart/contract.js";
25
26
  import { SlicerError } from "./slicer/slicer-error.js";
26
27
  import { inspectSliceSettings, listTemplateRegistry, resolveTemplatePath, resolveTemplateSlicerProfilePath, saveTemplate, } from "./slicer/templates.js";
@@ -317,6 +318,12 @@ const SLICE_TOOL_PROPERTIES = {
317
318
  type: "string",
318
319
  description: "Sparse infill density such as \"15%\" (0–100%), written over the selected process profile (Orca/Bambu Studio sparse_infill_density; PrusaSlicer --fill-density). Not mapped for Cura.",
319
320
  },
321
+ enable_support: { type: "boolean", description: "Turn support generation on or off (Orca/Bambu Studio enable_support), written over the selected process profile. Bambu Studio and OrcaSlicer only." },
322
+ support_type: { type: "string", enum: [...SUPPORT_TYPES], description: "Support style (Orca/Bambu Studio support_type), written over the selected process profile. Bambu Studio and OrcaSlicer only." },
323
+ support_threshold_angle: {
324
+ type: "integer", minimum: 0, maximum: 90,
325
+ description: "Overhang threshold in degrees for automatic supports (Orca/Bambu Studio support_threshold_angle; 0 lets the slicer decide). Bambu Studio and OrcaSlicer only.",
326
+ },
320
327
  };
321
328
  /**
322
329
  * upload_gcode / start_print results name the remote file that was stored or
@@ -347,7 +354,7 @@ const OUTPUT_SCHEMA_TOOLS = new Set([
347
354
  "camera_stream_start", "camera_frame", "camera_stream_stop",
348
355
  "fusion_mcp_status", "fusion_mcp_call", "fusion_mcp_new_design", "fusion_mcp_export",
349
356
  ]);
350
- /** layer_height / wall_loops / sparse_infill_density (PRD §8.4), validated before slicing. */
357
+ /** layer_height / wall_loops / sparse_infill_density / support settings (PRD §8.4), validated before slicing. */
351
358
  function parseProcessOverrides(args) {
352
359
  const overrides = {};
353
360
  if (args?.layer_height !== undefined) {
@@ -374,6 +381,25 @@ function parseProcessOverrides(args) {
374
381
  }
375
382
  overrides.sparseInfillDensity = `${value}%`;
376
383
  }
384
+ if (args?.enable_support !== undefined) {
385
+ if (typeof args.enable_support !== "boolean")
386
+ throw new Error("Invalid enable_support: expected true or false.");
387
+ overrides.enableSupport = args.enable_support;
388
+ }
389
+ if (args?.support_type !== undefined) {
390
+ const value = args.support_type;
391
+ if (typeof value !== "string" || !SUPPORT_TYPES.includes(value)) {
392
+ throw new Error(`Invalid support_type: expected one of ${SUPPORT_TYPES.join(", ")}.`);
393
+ }
394
+ overrides.supportType = value;
395
+ }
396
+ if (args?.support_threshold_angle !== undefined) {
397
+ const value = args.support_threshold_angle;
398
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > 90) {
399
+ throw new Error("Invalid support_threshold_angle: expected an integer number of degrees from 0 to 90.");
400
+ }
401
+ overrides.supportThresholdAngle = value;
402
+ }
377
403
  return overrides;
378
404
  }
379
405
  function validateSliceBambuModel(model) {
@@ -1104,7 +1130,7 @@ class ThreeDPrinterMCPServer {
1104
1130
  // List available tools
1105
1131
  this.server.setRequestHandler(ListToolsRequestSchema, async () => {
1106
1132
  return {
1107
- tools: [
1133
+ tools: annotateTools([
1108
1134
  {
1109
1135
  name: "get_printer_status",
1110
1136
  description: "Get the current status of the 3D printer",
@@ -1327,14 +1353,12 @@ class ThreeDPrinterMCPServer {
1327
1353
  description: "Describe this printer for the counterpart/1 contract: server name and version, printer kind and identity, and which features work (progress, layers, camera, filaments, pause, resume, cancel, print_3mf, G-code), detected from the firmware. Read-only.",
1328
1354
  inputSchema: { type: "object", properties: { ...PRINTER_TARGET_PROPERTIES } },
1329
1355
  outputSchema: PRINTER_INFO_OUTPUT_SCHEMA,
1330
- annotations: { readOnlyHint: true, openWorldHint: true },
1331
1356
  },
1332
1357
  {
1333
1358
  name: "counterpart_print_progress",
1334
1359
  description: "Read the printer's state and current job in the counterpart/1 shape: state, job name, progress (0-100), elapsed and remaining seconds, layers when the firmware reports them, temperatures and problems. job.name matches the jobName that upload_gcode and start_print return. Read-only.",
1335
1360
  inputSchema: { type: "object", properties: { ...PRINTER_TARGET_PROPERTIES } },
1336
1361
  outputSchema: PRINT_PROGRESS_OUTPUT_SCHEMA,
1337
- annotations: { readOnlyHint: true, openWorldHint: true },
1338
1362
  },
1339
1363
  {
1340
1364
  name: "camera_snapshot",
@@ -1348,7 +1372,6 @@ class ThreeDPrinterMCPServer {
1348
1372
  },
1349
1373
  },
1350
1374
  outputSchema: CAMERA_SNAPSHOT_OUTPUT_SCHEMA,
1351
- annotations: { readOnlyHint: true, openWorldHint: true },
1352
1375
  },
1353
1376
  {
1354
1377
  name: "get_printer_filaments",
@@ -1362,21 +1385,18 @@ class ThreeDPrinterMCPServer {
1362
1385
  },
1363
1386
  },
1364
1387
  outputSchema: FILAMENTS_OUTPUT_SCHEMA,
1365
- annotations: { readOnlyHint: true, openWorldHint: true },
1366
1388
  },
1367
1389
  {
1368
1390
  name: "pause_print",
1369
1391
  description: "Pause the current print (Bambu Lab, OctoPrint, Klipper/Moonraker, PrusaLink, Duet M25, Repetier, and Creality printers that run Moonraker). Bambu pauses over the same connection that prints. Pause is never gated.",
1370
1392
  inputSchema: { type: "object", properties: { ...PRINTER_TARGET_PROPERTIES, ...CONNECTION_PROPERTIES } },
1371
1393
  outputSchema: CONTROL_OUTPUT_SCHEMA,
1372
- annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
1373
1394
  },
1374
1395
  {
1375
1396
  name: "resume_print",
1376
1397
  description: "Resume a paused print. Resume reheats and moves the printer, so it only resumes a job this server instance inspected and started, and only when the printer freshly reports that job paused. Bambu re-reads the job over FTPS (or the plate G-code the printer extracted) and compares its SHA-256, and resumes over the same connection that printed. When the file cannot be re-read, a person confirms through MCP elicitation first (Bambu: only for files Bambu's plug-in placed; a file this server uploaded must re-read). Resume other jobs at the printer.",
1377
1398
  inputSchema: { type: "object", properties: { ...PRINTER_TARGET_PROPERTIES, ...CONNECTION_PROPERTIES } },
1378
1399
  outputSchema: CONTROL_OUTPUT_SCHEMA,
1379
- annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
1380
1400
  },
1381
1401
  {
1382
1402
  name: "set_printer_temperature",
@@ -1815,7 +1835,6 @@ class ThreeDPrinterMCPServer {
1815
1835
  {
1816
1836
  name: "inspect_print_file",
1817
1837
  description: "Inspect an exact private snapshot of a prepared sliced 3MF or textual G-code file and selected plate without contacting a printer. Returns versioned file-only evidence; no printer readiness or permission to dispatch is established. Does not auto-slice.",
1818
- annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
1819
1838
  inputSchema: {
1820
1839
  type: "object",
1821
1840
  additionalProperties: false,
@@ -2188,7 +2207,7 @@ class ThreeDPrinterMCPServer {
2188
2207
  },
2189
2208
  outputSchema: FUSION_EXPORT_OUTPUT_SCHEMA,
2190
2209
  }
2191
- ]
2210
+ ])
2192
2211
  };
2193
2212
  });
2194
2213
  // Handle tool calls
@@ -81,6 +81,15 @@ export declare function isHiddenNotice(serial: string, model: string, entry: Hms
81
81
  export declare function hiddenHmsNotices(raw: unknown, serial: string, model: string): string[];
82
82
  /** Cleared-code keys of reported hidden entries; clearing never removes them, so they are not re-acknowledged. */
83
83
  export declare function hiddenHmsKeys(raw: unknown, serial: string, model: string): string[];
84
+ /**
85
+ * 0300_8013, "Printing was paused by the user" / "paused due to the pause command added to the
86
+ * printing file": the printer's report of a deliberate pause (a Pause, or a pause_at_layers stop),
87
+ * which Resume clears. A paused P1S reports it as print_error, so it is not an error for resuming
88
+ * a paused job. requireIdle is false only for Resume here; starting a job still needs no error.
89
+ * Ported from bambu-printer-mcp src/safety/printer-state.ts.
90
+ */
91
+ export declare const PAUSE_NOTICE = 50364435;
92
+ export declare function isPauseNotice(raw: Record<string, any>, requireIdle: boolean): boolean;
84
93
  /** Verifies reported configuration, not a physical nozzle/spool inspection. */
85
94
  export declare function validatePrinterState(status: any, requirements: PrinterStateRequirements): ValidatedPrinterState;
86
95
  /** Manual M104 cannot remap filament slots. Validate the material already at the nozzle. */
@@ -316,6 +316,18 @@ function logHiddenNotices(raw, serial, model) {
316
316
  console.error(`Printer ${serial} reports ${hmsLabel(entry.hex)}, which Bambu lists for it without text and never shows as a fault; it does not block operations.`);
317
317
  }
318
318
  }
319
+ /**
320
+ * 0300_8013, "Printing was paused by the user" / "paused due to the pause command added to the
321
+ * printing file": the printer's report of a deliberate pause (a Pause, or a pause_at_layers stop),
322
+ * which Resume clears. A paused P1S reports it as print_error, so it is not an error for resuming
323
+ * a paused job. requireIdle is false only for Resume here; starting a job still needs no error.
324
+ * Ported from bambu-printer-mcp src/safety/printer-state.ts.
325
+ */
326
+ export const PAUSE_NOTICE = 0x03008013;
327
+ export function isPauseNotice(raw, requireIdle) {
328
+ const state = typeof raw?.gcode_state === "string" ? raw.gcode_state.toUpperCase() : "";
329
+ return !requireIdle && state === "PAUSE" && finiteNumber(raw?.print_error) === PAUSE_NOTICE;
330
+ }
319
331
  function validateErrors(raw, requireIdle, serial, model) {
320
332
  const state = typeof raw.gcode_state === "string" ? raw.gcode_state.toUpperCase() : "UNKNOWN";
321
333
  const terminal = isCancelledX2DJob(raw, model) || isFinishedP1SJob(raw, model);
@@ -323,7 +335,7 @@ function validateErrors(raw, requireIdle, serial, model) {
323
335
  throw new Error(`Printer state ${state} is not safely idle.`);
324
336
  if (!requireIdle && !["IDLE", "FINISH", "RUNNING", "PAUSE"].includes(state) && !terminal)
325
337
  throw new Error(`Printer state ${state} is unknown or failed.`);
326
- if (finiteNumber(raw.print_error) !== 0)
338
+ if (finiteNumber(raw.print_error) !== 0 && !isPauseNotice(raw, requireIdle))
327
339
  throw new Error("Printer reports a print error or did not report its error state.");
328
340
  if (!Array.isArray(raw.hms))
329
341
  throw new Error("Printer HMS error state is missing or malformed.");
@@ -15,7 +15,14 @@ export interface ProcessOverrides {
15
15
  wallLoops?: number;
16
16
  /** e.g. "15%" */
17
17
  sparseInfillDensity?: string;
18
+ enableSupport?: boolean;
19
+ /** normal(auto), tree(auto), normal(manual) or tree(manual) */
20
+ supportType?: string;
21
+ /** degrees, 0-90 */
22
+ supportThresholdAngle?: number;
18
23
  }
24
+ /** Bambu Studio's and OrcaSlicer's support_type values. */
25
+ export declare const SUPPORT_TYPES: readonly ["normal(auto)", "tree(auto)", "normal(manual)", "tree(manual)"];
19
26
  /** Orca/Bambu Studio process keys; profile JSON values are strings. */
20
27
  export declare function orcaProcessOverrides(overrides: ProcessOverrides | undefined): Record<string, string>;
21
28
  /** PrusaSlicer/Slic3r command-line options, which override the loaded config. */
@@ -13,6 +13,16 @@ import { BambuCliProfilePreparer, assertSlicedProjectOutput, bambuCliFailureReas
13
13
  import { runSlicerProcess } from '../slicer/run-slicer.js';
14
14
  import { SlicerError, asPreparationError } from '../slicer/slicer-error.js';
15
15
  import { customGcodesWithPauses, layerTops, machinePauseGcode, pausedLayers, readPlateGcode, readProjectCustomGcodes } from '../slicer/layer-pauses.js';
16
+ /** Bambu Studio's and OrcaSlicer's support_type values. */
17
+ export const SUPPORT_TYPES = ["normal(auto)", "tree(auto)", "normal(manual)", "tree(manual)"];
18
+ /** Support overrides only Bambu Studio and OrcaSlicer take; PrusaSlicer and Cura refuse them. */
19
+ function supportOverrideNames(overrides) {
20
+ return [
21
+ overrides?.enableSupport !== undefined ? "enable_support" : "",
22
+ overrides?.supportType !== undefined ? "support_type" : "",
23
+ overrides?.supportThresholdAngle !== undefined ? "support_threshold_angle" : "",
24
+ ].filter(Boolean);
25
+ }
16
26
  /** Orca/Bambu Studio process keys; profile JSON values are strings. */
17
27
  export function orcaProcessOverrides(overrides) {
18
28
  const keys = {};
@@ -22,10 +32,19 @@ export function orcaProcessOverrides(overrides) {
22
32
  keys.wall_loops = String(overrides.wallLoops);
23
33
  if (overrides?.sparseInfillDensity !== undefined)
24
34
  keys.sparse_infill_density = overrides.sparseInfillDensity;
35
+ if (overrides?.enableSupport !== undefined)
36
+ keys.enable_support = overrides.enableSupport ? "1" : "0";
37
+ if (overrides?.supportType !== undefined)
38
+ keys.support_type = overrides.supportType;
39
+ if (overrides?.supportThresholdAngle !== undefined)
40
+ keys.support_threshold_angle = String(overrides.supportThresholdAngle);
25
41
  return keys;
26
42
  }
27
43
  /** PrusaSlicer/Slic3r command-line options, which override the loaded config. */
28
44
  export function prusaSlicerOverrideArgs(overrides) {
45
+ const unsupported = supportOverrideNames(overrides);
46
+ if (unsupported.length)
47
+ throw new Error(`${unsupported.join(", ")} apply to Bambu Studio and OrcaSlicer only; set them in the PrusaSlicer config passed as slicer_profile.`);
29
48
  const args = [];
30
49
  if (overrides?.layerHeight !== undefined)
31
50
  args.push('--layer-height', String(overrides.layerHeight));
@@ -1000,7 +1019,7 @@ export class STLManipulator extends EventEmitter {
1000
1019
  break;
1001
1020
  case 'cura':
1002
1021
  if (Object.keys(orcaProcessOverrides(processOverrides)).length) {
1003
- throw new SlicerError('preparation', 'layer_height, wall_loops and sparse_infill_density are not mapped for Cura; set them in the Cura definition passed as slicer_profile.', { slicerType, slicerPath });
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 });
1004
1023
  }
1005
1024
  // CuraEngine CLI args are different, often requiring -s for settings
1006
1025
  // Example: curaengine slice -v -j cura_settings.json -s layer_height=0.2 -o output.gcode -l input.stl
@@ -0,0 +1,25 @@
1
+ /**
2
+ * MCP tool annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint) for every tool.
3
+ * They are hints for clients, never a safety gate: the shared checks in src/safety/ still apply.
4
+ *
5
+ * - readOnlyHint: the tool changes nothing (no file, printer, setting, or external application state).
6
+ * - destructiveHint: it may overwrite or end something that already exists (a file, a setting, a job,
7
+ * an arbitrary remote call). Starting a print and heating are additive: there is no confirmation
8
+ * prompt for them (AGENTS.md, maintainer decision 2026-11-03), and the printer runs its own checks.
9
+ * - idempotentHint: calling it again with the same arguments has no further effect.
10
+ * - openWorldHint: it reaches outside this server and the local files (a printer, its camera, Bambu's
11
+ * network, Blender, or Fusion).
12
+ */
13
+ export interface ToolAnnotations {
14
+ readOnlyHint: boolean;
15
+ destructiveHint: boolean;
16
+ idempotentHint: boolean;
17
+ openWorldHint: boolean;
18
+ }
19
+ export declare const TOOL_ANNOTATIONS: Readonly<Record<string, ToolAnnotations>>;
20
+ /** Give each listed tool its annotations. tests/tool-annotations.test.mjs fails for a tool without an entry. */
21
+ export declare function annotateTools<T extends {
22
+ name: string;
23
+ }>(tools: T[]): Array<T & {
24
+ annotations?: ToolAnnotations;
25
+ }>;
@@ -0,0 +1,58 @@
1
+ const read = (openWorldHint) => ({ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint });
2
+ const write = (destructiveHint, idempotentHint, openWorldHint) => ({ readOnlyHint: false, destructiveHint, idempotentHint, openWorldHint });
3
+ export const TOOL_ANNOTATIONS = {
4
+ // Printer reads.
5
+ get_printer_status: read(true),
6
+ list_printer_files: read(true),
7
+ counterpart_printer_info: read(true),
8
+ counterpart_print_progress: read(true),
9
+ camera_snapshot: write(true, false, true), // save_path writes, and replaces, a file of the caller's choosing
10
+ camera_frame: read(true),
11
+ get_printer_filaments: read(true),
12
+ // Printer actions.
13
+ upload_gcode: write(true, false, true), // can replace a file of the same name on the printer
14
+ start_print: write(false, false, true),
15
+ print_3mf: write(false, false, true),
16
+ process_and_print_stl: write(false, false, true),
17
+ cancel_print: write(true, true, true), // ends the job; it cannot be resumed
18
+ pause_print: write(false, true, true),
19
+ resume_print: write(false, true, true),
20
+ set_printer_temperature: write(false, true, true),
21
+ camera_stream_start: write(false, true, true), // "start or renew" one shared connection
22
+ camera_stream_stop: write(false, true, true),
23
+ counterpart_print_route: write(true, true, false), // with route, replaces the saved connection in .env
24
+ // Local files: each STL edit and slice writes a new output file, so a repeat writes another.
25
+ extend_stl_base: write(false, false, false),
26
+ scale_stl: write(false, false, false),
27
+ rotate_stl: write(false, false, false),
28
+ translate_stl: write(false, false, false),
29
+ modify_stl_section: write(false, false, false),
30
+ merge_vertices: write(false, false, false),
31
+ center_model: write(false, false, false),
32
+ lay_flat: write(false, false, false),
33
+ generate_stl_visualization: write(false, false, false), // writes a new SVG file
34
+ get_stl_info: read(false),
35
+ slice_stl: write(false, false, false),
36
+ slice_with_template: write(false, false, false),
37
+ list_templates: read(false),
38
+ save_template: write(true, true, false), // replaces a template of the same name
39
+ get_slice_settings: write(true, true, false), // extracts a 3MF's settings to a TEMP_DIR path named after the file; a same-named 3MF replaces it
40
+ confirm_temperatures: read(false),
41
+ inspect_print_file: read(false),
42
+ // FULU BambuNetwork.
43
+ check_fulu_orca_setup: read(true), // run_bridge_probe starts FULU's bridge host
44
+ fulu_bambu_network_rpc: write(true, false, true), // allow_mutating_methods reaches agent and session setup
45
+ // Blender and Fusion.
46
+ blender_mcp_status: read(true),
47
+ blender_mcp_call: write(true, false, true), // any Blender MCP tool
48
+ blender_mcp_export_stl: write(false, false, true), // never overwrites; leaves the scene unchanged
49
+ blender_mcp_edit_model: write(false, false, true),
50
+ fusion_mcp_status: write(false, true, true), // launch: true opens Fusion
51
+ fusion_mcp_call: write(true, false, true), // any Fusion tool, execute_code included
52
+ fusion_mcp_new_design: write(false, false, true),
53
+ fusion_mcp_export: write(false, false, true), // never overwrites files
54
+ };
55
+ /** Give each listed tool its annotations. tests/tool-annotations.test.mjs fails for a tool without an entry. */
56
+ export function annotateTools(tools) {
57
+ return tools.map(tool => (TOOL_ANNOTATIONS[tool.name] ? { ...tool, annotations: TOOL_ANNOTATIONS[tool.name] } : tool));
58
+ }
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.26",
4
+ "version": "1.2.28",
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",
@@ -39,6 +39,8 @@
39
39
  "dist/slicer",
40
40
  "dist/stl",
41
41
  "dist/test",
42
+ "dist/tool-annotations.d.ts",
43
+ "dist/tool-annotations.js",
42
44
  "dist/types.d.ts",
43
45
  "dist/types.js",
44
46
  "dist/ensure-web-crypto.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.26",
10
+ "version": "1.2.28",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "mcp-3d-printer-server",
15
- "version": "1.2.26",
15
+ "version": "1.2.28",
16
16
  "runtimeHint": "npx",
17
17
  "transport": {
18
18
  "type": "stdio"
@@ -1,5 +1,5 @@
1
1
  import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
2
- import { createHash } from "node:crypto";
2
+ import { createHash, randomUUID } from "node:crypto";
3
3
  import fs from "node:fs";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
@@ -180,6 +180,35 @@ function trimEnv(value: string | undefined): string | undefined {
180
180
  return trimmed ? trimmed : undefined;
181
181
  }
182
182
 
183
+ /**
184
+ * The client identity FULU's GUI gives the plug-in before agent start
185
+ * (GUI_App::init_http_extra_header). Through the Linux bridge FULU presents
186
+ * itself as Bambu Studio at the version its plug-in ABI belongs to
187
+ * (PJarczakLinuxBridgeConfig.cpp forced_client_version, "02.05.02.51").
188
+ * Bambu's cloud issues the printer-control certificate and creates print
189
+ * tasks for this client, so an agent started without it is an unknown client.
190
+ * The device id is kept in the config folder so it stays the same per install.
191
+ * Ported from bambu-printer-mcp src/bambu-network-bridge.ts.
192
+ */
193
+ export function bambuNetworkClientHeaders(configDir: string, env: NodeJS.ProcessEnv = process.env): Record<string, string> {
194
+ const idFile = path.join(configDir, "client-device-id");
195
+ let deviceId = "";
196
+ try { deviceId = fs.readFileSync(idFile, "utf8").trim(); } catch { /* first start */ }
197
+ if (!deviceId) {
198
+ deviceId = randomUUID();
199
+ try { fs.mkdirSync(configDir, { recursive: true }); fs.writeFileSync(idFile, `${deviceId}\n`, { mode: 0o600 }); } catch { /* still send this run's id */ }
200
+ }
201
+ return {
202
+ "X-BBL-Client-Type": "slicer",
203
+ "X-BBL-Client-Name": trimEnv(env.BAMBU_NETWORK_CLIENT_NAME) ?? "BambuStudio",
204
+ "X-BBL-Client-Version": trimEnv(env.BAMBU_NETWORK_CLIENT_VERSION) ?? "02.05.02.51",
205
+ "X-BBL-OS-Type": "linux",
206
+ "X-BBL-OS-Version": os.release(),
207
+ "X-BBL-Device-ID": deviceId,
208
+ "X-BBL-Language": "en",
209
+ };
210
+ }
211
+
183
212
  function fileExists(filePath: string): boolean {
184
213
  try {
185
214
  return fs.existsSync(filePath);
@@ -493,6 +522,8 @@ export class BambuNetworkBridge {
493
522
  // plug-in verifies a LAN printer with printer.cer from this folder, and
494
523
  // FULU's host crashes on connect_printer when no folder was ever set.
495
524
  const certificates = await withAgent("net.set_cert_file", { folder: certDir, filename: "slicer_base64.cer" }, false);
525
+ // FULU's GUI order: set_cert_file, then the client headers, then start.
526
+ await withAgent("net.set_extra_http_header", { headers: bambuNetworkClientHeaders(configDir) }, false);
496
527
  await withAgent("net.set_country_code", { country_code: countryCode });
497
528
  await withAgent("net.start");
498
529
  // The plug-in runs callbacks through queue_on_main; FULU's host executes
package/src/index.ts CHANGED
@@ -34,10 +34,11 @@ import { createServer as createHttpServer, type IncomingMessage, type Server as
34
34
  import { randomUUID } from "node:crypto";
35
35
  import * as THREE from 'three';
36
36
  import { PrinterFactory } from "./printers/printer-factory.js";
37
- import { STLManipulator, type BambuSliceOptions, type ProcessOverrides, type SliceOutput } from "./stl/stl-manipulator.js";
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 { CounterpartService, type BambuControlRoute, type PrinterTarget } from "./counterpart/service.js";
40
40
  import {CAMERA_STREAM_TOOLS} from "./counterpart/camera-tools.js";
41
+ import { annotateTools } from "./tool-annotations.js";
41
42
  import {
42
43
  CAMERA_SNAPSHOT_OUTPUT_SCHEMA,
43
44
  CONTROL_OUTPUT_SCHEMA,
@@ -395,6 +396,12 @@ const SLICE_TOOL_PROPERTIES = {
395
396
  type: "string",
396
397
  description: "Sparse infill density such as \"15%\" (0–100%), written over the selected process profile (Orca/Bambu Studio sparse_infill_density; PrusaSlicer --fill-density). Not mapped for Cura.",
397
398
  },
399
+ enable_support: { type: "boolean", description: "Turn support generation on or off (Orca/Bambu Studio enable_support), written over the selected process profile. Bambu Studio and OrcaSlicer only." },
400
+ support_type: { type: "string", enum: [...SUPPORT_TYPES], description: "Support style (Orca/Bambu Studio support_type), written over the selected process profile. Bambu Studio and OrcaSlicer only." },
401
+ support_threshold_angle: {
402
+ type: "integer", minimum: 0, maximum: 90,
403
+ description: "Overhang threshold in degrees for automatic supports (Orca/Bambu Studio support_threshold_angle; 0 lets the slicer decide). Bambu Studio and OrcaSlicer only.",
404
+ },
398
405
  };
399
406
 
400
407
  /**
@@ -427,7 +434,7 @@ const OUTPUT_SCHEMA_TOOLS = new Set([
427
434
  "fusion_mcp_status", "fusion_mcp_call", "fusion_mcp_new_design", "fusion_mcp_export",
428
435
  ]);
429
436
 
430
- /** layer_height / wall_loops / sparse_infill_density (PRD §8.4), validated before slicing. */
437
+ /** layer_height / wall_loops / sparse_infill_density / support settings (PRD §8.4), validated before slicing. */
431
438
  function parseProcessOverrides(args: Record<string, unknown> | undefined): ProcessOverrides {
432
439
  const overrides: ProcessOverrides = {};
433
440
  if (args?.layer_height !== undefined) {
@@ -454,6 +461,24 @@ function parseProcessOverrides(args: Record<string, unknown> | undefined): Proce
454
461
  }
455
462
  overrides.sparseInfillDensity = `${value}%`;
456
463
  }
464
+ if (args?.enable_support !== undefined) {
465
+ if (typeof args.enable_support !== "boolean") throw new Error("Invalid enable_support: expected true or false.");
466
+ overrides.enableSupport = args.enable_support;
467
+ }
468
+ if (args?.support_type !== undefined) {
469
+ const value = args.support_type;
470
+ if (typeof value !== "string" || !(SUPPORT_TYPES as readonly string[]).includes(value)) {
471
+ throw new Error(`Invalid support_type: expected one of ${SUPPORT_TYPES.join(", ")}.`);
472
+ }
473
+ overrides.supportType = value;
474
+ }
475
+ if (args?.support_threshold_angle !== undefined) {
476
+ const value = args.support_threshold_angle;
477
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > 90) {
478
+ throw new Error("Invalid support_threshold_angle: expected an integer number of degrees from 0 to 90.");
479
+ }
480
+ overrides.supportThresholdAngle = value;
481
+ }
457
482
  return overrides;
458
483
  }
459
484
 
@@ -1303,7 +1328,7 @@ class ThreeDPrinterMCPServer {
1303
1328
  // List available tools
1304
1329
  this.server.setRequestHandler(ListToolsRequestSchema, async () => {
1305
1330
  return {
1306
- tools: [
1331
+ tools: annotateTools([
1307
1332
  {
1308
1333
  name: "get_printer_status",
1309
1334
  description: "Get the current status of the 3D printer",
@@ -1526,14 +1551,12 @@ class ThreeDPrinterMCPServer {
1526
1551
  description: "Describe this printer for the counterpart/1 contract: server name and version, printer kind and identity, and which features work (progress, layers, camera, filaments, pause, resume, cancel, print_3mf, G-code), detected from the firmware. Read-only.",
1527
1552
  inputSchema: { type: "object", properties: { ...PRINTER_TARGET_PROPERTIES } },
1528
1553
  outputSchema: PRINTER_INFO_OUTPUT_SCHEMA,
1529
- annotations: { readOnlyHint: true, openWorldHint: true },
1530
1554
  },
1531
1555
  {
1532
1556
  name: "counterpart_print_progress",
1533
1557
  description: "Read the printer's state and current job in the counterpart/1 shape: state, job name, progress (0-100), elapsed and remaining seconds, layers when the firmware reports them, temperatures and problems. job.name matches the jobName that upload_gcode and start_print return. Read-only.",
1534
1558
  inputSchema: { type: "object", properties: { ...PRINTER_TARGET_PROPERTIES } },
1535
1559
  outputSchema: PRINT_PROGRESS_OUTPUT_SCHEMA,
1536
- annotations: { readOnlyHint: true, openWorldHint: true },
1537
1560
  },
1538
1561
  {
1539
1562
  name: "camera_snapshot",
@@ -1547,7 +1570,6 @@ class ThreeDPrinterMCPServer {
1547
1570
  },
1548
1571
  },
1549
1572
  outputSchema: CAMERA_SNAPSHOT_OUTPUT_SCHEMA,
1550
- annotations: { readOnlyHint: true, openWorldHint: true },
1551
1573
  },
1552
1574
  {
1553
1575
  name: "get_printer_filaments",
@@ -1561,21 +1583,18 @@ class ThreeDPrinterMCPServer {
1561
1583
  },
1562
1584
  },
1563
1585
  outputSchema: FILAMENTS_OUTPUT_SCHEMA,
1564
- annotations: { readOnlyHint: true, openWorldHint: true },
1565
1586
  },
1566
1587
  {
1567
1588
  name: "pause_print",
1568
1589
  description: "Pause the current print (Bambu Lab, OctoPrint, Klipper/Moonraker, PrusaLink, Duet M25, Repetier, and Creality printers that run Moonraker). Bambu pauses over the same connection that prints. Pause is never gated.",
1569
1590
  inputSchema: { type: "object", properties: { ...PRINTER_TARGET_PROPERTIES, ...CONNECTION_PROPERTIES } },
1570
1591
  outputSchema: CONTROL_OUTPUT_SCHEMA,
1571
- annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
1572
1592
  },
1573
1593
  {
1574
1594
  name: "resume_print",
1575
1595
  description: "Resume a paused print. Resume reheats and moves the printer, so it only resumes a job this server instance inspected and started, and only when the printer freshly reports that job paused. Bambu re-reads the job over FTPS (or the plate G-code the printer extracted) and compares its SHA-256, and resumes over the same connection that printed. When the file cannot be re-read, a person confirms through MCP elicitation first (Bambu: only for files Bambu's plug-in placed; a file this server uploaded must re-read). Resume other jobs at the printer.",
1576
1596
  inputSchema: { type: "object", properties: { ...PRINTER_TARGET_PROPERTIES, ...CONNECTION_PROPERTIES } },
1577
1597
  outputSchema: CONTROL_OUTPUT_SCHEMA,
1578
- annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
1579
1598
  },
1580
1599
  {
1581
1600
  name: "set_printer_temperature",
@@ -2016,7 +2035,6 @@ class ThreeDPrinterMCPServer {
2016
2035
  {
2017
2036
  name: "inspect_print_file",
2018
2037
  description: "Inspect an exact private snapshot of a prepared sliced 3MF or textual G-code file and selected plate without contacting a printer. Returns versioned file-only evidence; no printer readiness or permission to dispatch is established. Does not auto-slice.",
2019
- annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
2020
2038
  inputSchema: {
2021
2039
  type: "object",
2022
2040
  additionalProperties: false,
@@ -2396,7 +2414,7 @@ class ThreeDPrinterMCPServer {
2396
2414
  },
2397
2415
  outputSchema: FUSION_EXPORT_OUTPUT_SCHEMA,
2398
2416
  }
2399
- ]
2417
+ ])
2400
2418
  };
2401
2419
  });
2402
2420
 
@@ -326,12 +326,24 @@ function logHiddenNotices(raw: Record<string, any>, serial: string, model: strin
326
326
  }
327
327
  }
328
328
 
329
+ /**
330
+ * 0300_8013, "Printing was paused by the user" / "paused due to the pause command added to the
331
+ * printing file": the printer's report of a deliberate pause (a Pause, or a pause_at_layers stop),
332
+ * which Resume clears. A paused P1S reports it as print_error, so it is not an error for resuming
333
+ * a paused job. requireIdle is false only for Resume here; starting a job still needs no error.
334
+ * Ported from bambu-printer-mcp src/safety/printer-state.ts.
335
+ */
336
+ export const PAUSE_NOTICE = 0x03008013;
337
+ export function isPauseNotice(raw: Record<string, any>, requireIdle: boolean): boolean {
338
+ const state = typeof raw?.gcode_state === "string" ? raw.gcode_state.toUpperCase() : "";
339
+ return !requireIdle && state === "PAUSE" && finiteNumber(raw?.print_error) === PAUSE_NOTICE;
340
+ }
329
341
  function validateErrors(raw: Record<string,any>, requireIdle: boolean, serial: string, model: string) {
330
342
  const state = typeof raw.gcode_state === "string" ? raw.gcode_state.toUpperCase() : "UNKNOWN";
331
343
  const terminal = isCancelledX2DJob(raw, model) || isFinishedP1SJob(raw, model);
332
344
  if (requireIdle && !["IDLE","FINISH"].includes(state) && !terminal) throw new Error(`Printer state ${state} is not safely idle.`);
333
345
  if (!requireIdle && !["IDLE","FINISH","RUNNING","PAUSE"].includes(state) && !terminal) throw new Error(`Printer state ${state} is unknown or failed.`);
334
- if (finiteNumber(raw.print_error) !== 0) throw new Error("Printer reports a print error or did not report its error state.");
346
+ if (finiteNumber(raw.print_error) !== 0 && !isPauseNotice(raw, requireIdle)) throw new Error("Printer reports a print error or did not report its error state.");
335
347
  if (!Array.isArray(raw.hms)) throw new Error("Printer HMS error state is missing or malformed.");
336
348
  for (const reported of raw.hms) {
337
349
  const entry = hmsCode(reported);
@@ -36,6 +36,23 @@ export interface ProcessOverrides {
36
36
  wallLoops?: number;
37
37
  /** e.g. "15%" */
38
38
  sparseInfillDensity?: string;
39
+ enableSupport?: boolean;
40
+ /** normal(auto), tree(auto), normal(manual) or tree(manual) */
41
+ supportType?: string;
42
+ /** degrees, 0-90 */
43
+ supportThresholdAngle?: number;
44
+ }
45
+
46
+ /** Bambu Studio's and OrcaSlicer's support_type values. */
47
+ export const SUPPORT_TYPES = ["normal(auto)", "tree(auto)", "normal(manual)", "tree(manual)"] as const;
48
+
49
+ /** Support overrides only Bambu Studio and OrcaSlicer take; PrusaSlicer and Cura refuse them. */
50
+ function supportOverrideNames(overrides: ProcessOverrides | undefined): string[] {
51
+ return [
52
+ overrides?.enableSupport !== undefined ? "enable_support" : "",
53
+ overrides?.supportType !== undefined ? "support_type" : "",
54
+ overrides?.supportThresholdAngle !== undefined ? "support_threshold_angle" : "",
55
+ ].filter(Boolean);
39
56
  }
40
57
 
41
58
  /** Orca/Bambu Studio process keys; profile JSON values are strings. */
@@ -44,11 +61,16 @@ export function orcaProcessOverrides(overrides: ProcessOverrides | undefined): R
44
61
  if (overrides?.layerHeight !== undefined) keys.layer_height = String(overrides.layerHeight);
45
62
  if (overrides?.wallLoops !== undefined) keys.wall_loops = String(overrides.wallLoops);
46
63
  if (overrides?.sparseInfillDensity !== undefined) keys.sparse_infill_density = overrides.sparseInfillDensity;
64
+ if (overrides?.enableSupport !== undefined) keys.enable_support = overrides.enableSupport ? "1" : "0";
65
+ if (overrides?.supportType !== undefined) keys.support_type = overrides.supportType;
66
+ if (overrides?.supportThresholdAngle !== undefined) keys.support_threshold_angle = String(overrides.supportThresholdAngle);
47
67
  return keys;
48
68
  }
49
69
 
50
70
  /** PrusaSlicer/Slic3r command-line options, which override the loaded config. */
51
71
  export function prusaSlicerOverrideArgs(overrides: ProcessOverrides | undefined): string[] {
72
+ const unsupported = supportOverrideNames(overrides);
73
+ if (unsupported.length) throw new Error(`${unsupported.join(", ")} apply to Bambu Studio and OrcaSlicer only; set them in the PrusaSlicer config passed as slicer_profile.`);
52
74
  const args: string[] = [];
53
75
  if (overrides?.layerHeight !== undefined) args.push('--layer-height', String(overrides.layerHeight));
54
76
  if (overrides?.wallLoops !== undefined) args.push('--perimeters', String(overrides.wallLoops));
@@ -1295,7 +1317,7 @@ export class STLManipulator extends EventEmitter {
1295
1317
  case 'cura':
1296
1318
  if (Object.keys(orcaProcessOverrides(processOverrides)).length) {
1297
1319
  throw new SlicerError('preparation',
1298
- 'layer_height, wall_loops and sparse_infill_density are not mapped for Cura; set them in the Cura definition passed as slicer_profile.',
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.',
1299
1321
  { slicerType, slicerPath });
1300
1322
  }
1301
1323
  // CuraEngine CLI args are different, often requiring -s for settings