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 +18 -0
- package/README.md +2 -0
- package/dist/bambu-network-bridge.d.ts +11 -0
- package/dist/bambu-network-bridge.js +38 -1
- package/dist/index.js +30 -11
- package/dist/safety/printer-state.d.ts +9 -0
- package/dist/safety/printer-state.js +13 -1
- package/dist/stl/stl-manipulator.d.ts +7 -0
- package/dist/stl/stl-manipulator.js +20 -1
- package/dist/tool-annotations.d.ts +25 -0
- package/dist/tool-annotations.js +58 -0
- package/package.json +3 -1
- package/server.json +2 -2
- package/src/bambu-network-bridge.ts +32 -1
- package/src/index.ts +29 -11
- package/src/safety/printer-state.ts +13 -1
- package/src/stl/stl-manipulator.ts +23 -1
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|