mcp-3d-printer-server 1.2.25 → 1.2.27
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 +67 -0
- package/CONTRIBUTORS.md +5 -1
- package/README.md +64 -29
- package/dist/ams-mapping.d.ts +38 -0
- package/dist/ams-mapping.js +125 -0
- package/dist/bambu-network-bridge.d.ts +211 -0
- package/dist/bambu-network-bridge.js +918 -0
- package/dist/bambu-network-certs.d.ts +13 -0
- package/dist/bambu-network-certs.js +152 -0
- package/dist/control-reply.d.ts +21 -0
- package/dist/control-reply.js +44 -0
- package/dist/counterpart/camera-sources.d.ts +16 -0
- package/dist/counterpart/camera-sources.js +138 -0
- package/dist/counterpart/camera-stream.d.ts +66 -0
- package/dist/counterpart/camera-stream.js +246 -0
- package/dist/counterpart/camera-tools.d.ts +247 -0
- package/dist/counterpart/camera-tools.js +35 -0
- package/dist/counterpart/contract.d.ts +21 -5
- package/dist/counterpart/contract.js +8 -2
- package/dist/counterpart/route.d.ts +31 -0
- package/dist/counterpart/route.js +70 -0
- package/dist/counterpart/service.d.ts +60 -4
- package/dist/counterpart/service.js +142 -16
- package/dist/design-guidance.d.ts +26 -0
- package/dist/design-guidance.js +177 -0
- package/dist/fulu-orca.d.ts +7 -0
- package/dist/fulu-orca.js +18 -2
- package/dist/index.js +514 -70
- package/dist/printers/bambu-filaments.d.ts +68 -0
- package/dist/printers/bambu-filaments.js +262 -0
- package/dist/printers/bambu.d.ts +99 -6
- package/dist/printers/bambu.js +478 -72
- package/dist/printers/creality.d.ts +2 -1
- package/dist/printers/creality.js +5 -1
- package/dist/printers/generic-base.d.ts +11 -6
- package/dist/printers/generic-base.js +21 -25
- package/dist/printers/klipper.d.ts +2 -1
- package/dist/printers/klipper.js +10 -7
- package/dist/printers/octoprint.d.ts +2 -1
- package/dist/printers/octoprint.js +15 -8
- package/dist/printers/printer-factory.d.ts +1 -1
- package/dist/printers/printer-factory.js +3 -3
- package/dist/printers/prusa.js +0 -1
- package/dist/safety/confirmation.d.ts +11 -16
- package/dist/safety/confirmation.js +8 -32
- package/dist/safety/hidden-hms.d.ts +32 -0
- package/dist/safety/hidden-hms.js +405 -0
- package/dist/safety/limits.d.ts +8 -2
- package/dist/safety/limits.js +14 -5
- package/dist/safety/print-file.d.ts +5 -0
- package/dist/safety/print-file.js +156 -19
- package/dist/safety/printer-state.d.ts +68 -8
- package/dist/safety/printer-state.js +187 -40
- package/dist/slicer/bambu-cli.d.ts +2 -0
- package/dist/slicer/bambu-cli.js +7 -5
- package/dist/slicer/layer-pauses.d.ts +47 -0
- package/dist/slicer/layer-pauses.js +130 -0
- package/dist/stl/stl-manipulator.d.ts +1 -0
- package/dist/stl/stl-manipulator.js +40 -0
- package/dist/studio-print-options.d.ts +57 -0
- package/dist/studio-print-options.js +179 -0
- package/dist/tool-annotations.d.ts +25 -0
- package/dist/tool-annotations.js +58 -0
- package/package.json +27 -4
- package/server.json +2 -2
- package/src/ams-mapping.ts +132 -0
- package/src/bambu-network-bridge.ts +1106 -0
- package/src/bambu-network-certs.ts +154 -0
- package/src/control-reply.ts +41 -0
- package/src/counterpart/camera-sources.ts +102 -0
- package/src/counterpart/camera-stream.ts +195 -0
- package/src/counterpart/camera-tools.ts +35 -0
- package/src/counterpart/contract.ts +12 -4
- package/src/counterpart/route.ts +82 -0
- package/src/counterpart/service.ts +150 -16
- package/src/design-guidance.ts +188 -0
- package/src/fulu-orca.ts +18 -2
- package/src/index.ts +528 -83
- package/src/printers/bambu-filaments.ts +346 -0
- package/src/printers/bambu.ts +506 -81
- package/src/printers/creality.ts +8 -3
- package/src/printers/generic-base.ts +33 -40
- package/src/printers/klipper.ts +12 -8
- package/src/printers/octoprint.ts +16 -9
- package/src/printers/printer-factory.ts +3 -3
- package/src/printers/prusa.ts +0 -1
- package/src/safety/confirmation.ts +15 -38
- package/src/safety/hidden-hms.ts +406 -0
- package/src/safety/limits.ts +15 -5
- package/src/safety/print-file.ts +139 -17
- package/src/safety/printer-state.ts +192 -41
- package/src/slicer/bambu-cli.ts +8 -5
- package/src/slicer/layer-pauses.ts +143 -0
- package/src/stl/stl-manipulator.ts +45 -0
- package/src/studio-print-options.ts +201 -0
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import "./ensure-web-crypto.js";
|
|
|
4
4
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
5
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
6
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
7
|
-
import { ListResourcesRequestSchema, ReadResourceRequestSchema, ListToolsRequestSchema, CallToolRequestSchema, ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
|
|
7
|
+
import { ListResourcesRequestSchema, ReadResourceRequestSchema, ListToolsRequestSchema, CallToolRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
|
|
8
8
|
import axios from "axios";
|
|
9
9
|
import dotenv from "dotenv";
|
|
10
10
|
import fs from "fs";
|
|
@@ -18,15 +18,28 @@ import { randomUUID } from "node:crypto";
|
|
|
18
18
|
import * as THREE from 'three';
|
|
19
19
|
import { PrinterFactory } from "./printers/printer-factory.js";
|
|
20
20
|
import { STLManipulator } from "./stl/stl-manipulator.js";
|
|
21
|
+
import { parsePauseLayers, readPlateGcode, pausedLayers, machinePauseGcode } from "./slicer/layer-pauses.js";
|
|
21
22
|
import { CounterpartService } from "./counterpart/service.js";
|
|
23
|
+
import { CAMERA_STREAM_TOOLS } from "./counterpart/camera-tools.js";
|
|
24
|
+
import { annotateTools } from "./tool-annotations.js";
|
|
22
25
|
import { CAMERA_SNAPSHOT_OUTPUT_SCHEMA, CONTROL_OUTPUT_SCHEMA, FILAMENTS_OUTPUT_SCHEMA, PRINT_PROGRESS_OUTPUT_SCHEMA, PRINTER_INFO_OUTPUT_SCHEMA, } from "./counterpart/contract.js";
|
|
23
26
|
import { SlicerError } from "./slicer/slicer-error.js";
|
|
24
27
|
import { inspectSliceSettings, listTemplateRegistry, resolveTemplatePath, resolveTemplateSlicerProfilePath, saveTemplate, } from "./slicer/templates.js";
|
|
25
28
|
import { parse3MF } from './3mf_parser.js';
|
|
26
29
|
import { BambuImplementation } from "./printers/bambu.js";
|
|
27
30
|
import { inspectFuluOrcaSetup, invokeFuluBridgeRpc, } from "./fulu-orca.js";
|
|
31
|
+
import { bridgeAmsMapping2Entry, bridgeAmsMappingValue, externalTrayForNozzle, hasAmsMappingInput, normalizeAmsMappingObject, normalizeBridgeAmsTrayValue, studioAmsMappingInfo, trayColour, } from "./ams-mapping.js";
|
|
32
|
+
import { isDualNozzleModel } from "./safety/limits.js";
|
|
33
|
+
import { BambuNetworkBridge, describeBambuNetworkPrintError } from "./bambu-network-bridge.js";
|
|
34
|
+
import { PRINT_ROUTES, connectionForRoute, routeOf, routeOptions, writeEnvValues } from "./counterpart/route.js";
|
|
35
|
+
import { resolveStudioPrintOptions, usedNozzleCount } from "./studio-print-options.js";
|
|
36
|
+
import { inspectPrintFile } from "./safety/print-file.js";
|
|
37
|
+
import { validatePrinterState } from "./safety/printer-state.js";
|
|
38
|
+
import { uniquePrintName, withPrinterOperation, withPrintSnapshot } from "./safety/artifact.js";
|
|
39
|
+
import { checkedFile } from "./printers/bambu.js";
|
|
28
40
|
import { CONFIRMATION_UNAVAILABLE, confirmationTimedOut, confirmationTimeoutMs } from "./safety/confirmation.js";
|
|
29
41
|
import { validateExpectedPeaks } from "./safety/expected-peaks.js";
|
|
42
|
+
import { DESIGN_PROMPTS, FDM_CAPABILITY_GUIDANCE, FDM_CAPABILITY_RESOURCE, FDM_CAPABILITY_URI, SERVER_INSTRUCTIONS } from "./design-guidance.js";
|
|
30
43
|
// Load environment variables from .env file
|
|
31
44
|
dotenv.config();
|
|
32
45
|
const SERVER_NAME = "mcp-3d-printer-server";
|
|
@@ -36,7 +49,10 @@ const DEFAULT_HOST = process.env.PRINTER_HOST || "localhost";
|
|
|
36
49
|
const DEFAULT_PORT = process.env.PRINTER_PORT || "80";
|
|
37
50
|
const DEFAULT_API_KEY = process.env.API_KEY || "";
|
|
38
51
|
const DEFAULT_TYPE = process.env.PRINTER_TYPE || "octoprint"; // Default to OctoPrint
|
|
39
|
-
|
|
52
|
+
// Resolve an explicit TEMP_DIR (e.g. ./temp) against the server's working directory, as
|
|
53
|
+
// dotenv's own .env lookup is: slicer CLIs receive paths under it, and Bambu Studio fails
|
|
54
|
+
// every 3MF export to a relative --outputdir (bambu-printer-mcp 1.1.44 fixed the same).
|
|
55
|
+
const TEMP_DIR = process.env.TEMP_DIR ? path.resolve(process.env.TEMP_DIR) : fs.mkdtempSync(path.join(os.tmpdir(), "mcp-3d-printer-server-"));
|
|
40
56
|
// Slicer configuration
|
|
41
57
|
const CANONICAL_SLICER_TYPES = [
|
|
42
58
|
"prusaslicer",
|
|
@@ -111,7 +127,10 @@ function defaultBambuStudioPath() {
|
|
|
111
127
|
]) ||
|
|
112
128
|
"bambustudio");
|
|
113
129
|
}
|
|
114
|
-
const DEFAULT_SLICER_TYPE = normalizeSlicerType(
|
|
130
|
+
const DEFAULT_SLICER_TYPE = normalizeSlicerType(
|
|
131
|
+
// Bambu Studio for Bambu printers, as in bambu-printer-mcp: FULU OrcaSlicer-bambulab v1.0.0's CLI
|
|
132
|
+
// segfaults on every slice (FULU #56/#57 open), so it is chosen explicitly, not by default.
|
|
133
|
+
process.env.SLICER_TYPE || (DEFAULT_TYPE.toLowerCase() === "bambu" ? "bambustudio" : "prusaslicer"));
|
|
115
134
|
const DEFAULT_SLICER_PATH = process.env.SLICER_PATH ||
|
|
116
135
|
(DEFAULT_SLICER_TYPE === "orcaslicer-bambulab"
|
|
117
136
|
? defaultFuluOrcaPath()
|
|
@@ -127,6 +146,49 @@ const DEFAULT_BAMBU_TOKEN = process.env.BAMBU_TOKEN || "";
|
|
|
127
146
|
const DEFAULT_BAMBU_MODEL = process.env.BAMBU_MODEL?.trim().toLowerCase() || "";
|
|
128
147
|
const DEFAULT_BED_TYPE = process.env.BED_TYPE?.trim().toLowerCase() || "textured_plate";
|
|
129
148
|
const DEFAULT_NOZZLE_DIAMETER = process.env.NOZZLE_DIAMETER?.trim() || "0.4";
|
|
149
|
+
// counterpart_print_route changes these at run time (and in the .env file they came from).
|
|
150
|
+
let DEFAULT_BAMBU_CONNECTION_MODE = process.env.BAMBU_DEFAULT_CONNECTION_MODE?.trim().toLowerCase() || "";
|
|
151
|
+
let DEFAULT_BAMBU_CONNECTION_TYPE = process.env.BAMBU_DEFAULT_CONNECTION_TYPE?.trim().toLowerCase() || "";
|
|
152
|
+
const CONNECTION_MODES = ["lan_mqtt_ftps", "bambu_network", "bambu_native", "bambu_connect"];
|
|
153
|
+
/**
|
|
154
|
+
* The one route decision for a Bambu printer, as in bambu-printer-mcp:
|
|
155
|
+
* print_3mf and every print control (cancel, pause, resume) resolve it here,
|
|
156
|
+
* so a job is never controlled over a different connection than the one that
|
|
157
|
+
* started it. Arguments override BAMBU_DEFAULT_CONNECTION_MODE/_TYPE; without
|
|
158
|
+
* either, direct LAN MQTT/FTPS. The type (lan or cloud) applies to FULU's
|
|
159
|
+
* BambuNetwork route and defaults to cloud, as bambu-printer-mcp's does.
|
|
160
|
+
*/
|
|
161
|
+
function resolveConnection(args) {
|
|
162
|
+
const mode = String(args?.connection_mode || DEFAULT_BAMBU_CONNECTION_MODE || "lan_mqtt_ftps").trim().toLowerCase();
|
|
163
|
+
if (!CONNECTION_MODES.includes(mode))
|
|
164
|
+
throw new Error("Unsupported connection_mode.");
|
|
165
|
+
const type = String(args?.connection_type || DEFAULT_BAMBU_CONNECTION_TYPE || "cloud").trim().toLowerCase();
|
|
166
|
+
if (!["cloud", "lan"].includes(type))
|
|
167
|
+
throw new Error('connection_type must be "cloud" or "lan".');
|
|
168
|
+
return { mode, type };
|
|
169
|
+
}
|
|
170
|
+
/** bambu-printer-mcp's installed-plug-in (X2D native) and Bambu Connect routes are not part of this server. */
|
|
171
|
+
function assertConnectionAvailable(mode) {
|
|
172
|
+
if (mode === "bambu_native" || mode === "bambu_connect") {
|
|
173
|
+
throw new Error(`connection_mode ${mode} is not available in mcp-3D-printer-server. Use lan_mqtt_ftps or bambu_network (FULU), ` +
|
|
174
|
+
`or bambu-printer-mcp for ${mode === "bambu_native" ? "the X2D native route" : "Bambu Connect"}. Nothing was sent.`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const CONNECTION_PROPERTIES = {
|
|
178
|
+
connection_mode: { type: "string", enum: ["lan_mqtt_ftps", "bambu_network"], description: "Bambu only: the connection the print uses (print_3mf) or used (controls). lan_mqtt_ftps is direct LAN MQTT/FTPS (Developer Mode); bambu_network is FULU's authenticated BambuNetwork bridge. Defaults to BAMBU_DEFAULT_CONNECTION_MODE, else lan_mqtt_ftps." },
|
|
179
|
+
connection_type: { type: "string", enum: ["lan", "cloud"], description: "Bambu only: FULU BambuNetwork connection type. lan is the authenticated local network session; cloud is Bambu Cloud. Defaults to BAMBU_DEFAULT_CONNECTION_TYPE, else cloud." },
|
|
180
|
+
};
|
|
181
|
+
/** The Send dialog options a caller set explicitly; the rest resolve as Bambu Studio does (studio-print-options.ts). */
|
|
182
|
+
function callerPrintOptions(args) {
|
|
183
|
+
const flag = (value) => value === undefined ? undefined : Boolean(value);
|
|
184
|
+
const options = {
|
|
185
|
+
bedLeveling: flag(args?.bed_leveling),
|
|
186
|
+
flowCalibration: flag(args?.flow_calibration),
|
|
187
|
+
vibrationCalibration: flag(args?.vibration_calibration),
|
|
188
|
+
layerInspect: flag(args?.layer_inspect),
|
|
189
|
+
};
|
|
190
|
+
return Object.fromEntries(Object.entries(options).filter(([, value]) => value !== undefined));
|
|
191
|
+
}
|
|
130
192
|
const VALID_BAMBU_MODELS = ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"];
|
|
131
193
|
const VALID_BED_TYPES = ["textured_plate", "cool_plate", "engineering_plate", "hot_plate"];
|
|
132
194
|
// Map model IDs to BambuStudio --load-machine preset names
|
|
@@ -164,6 +226,7 @@ function resolveNozzleType(value) {
|
|
|
164
226
|
}
|
|
165
227
|
// Shared slice_stl / slice_with_template argument schema.
|
|
166
228
|
const SLICE_TOOL_PROPERTIES = {
|
|
229
|
+
pause_at_layers: { type: "array", items: { type: "integer", minimum: 2 }, description: "Pause before these 1-based layers, for example to insert magnets. Bambu-compatible CLI only; the slicer writes the printer's own pause G-code. Existing project pauses are preserved." },
|
|
167
230
|
bambu_model: {
|
|
168
231
|
type: "string",
|
|
169
232
|
enum: [...SLICE_BAMBU_MODELS],
|
|
@@ -228,7 +291,7 @@ const SLICE_TOOL_PROPERTIES = {
|
|
|
228
291
|
description: "Template directory override when resolving template_name (default: BAMBU_TEMPLATE_DIR or ~/Sync/bambu/templates).",
|
|
229
292
|
},
|
|
230
293
|
uptodate: { type: "boolean", description: "Bambu-compatible slicing: refresh 3MF preset configs to the installed slicer version (--uptodate)." },
|
|
231
|
-
repetitions: { type: "number", description: "Bambu-compatible slicing: print N identical copies
|
|
294
|
+
repetitions: { type: "number", description: "Bambu-compatible slicing: print N identical copies. STL inputs use --clone-objects; selected 3MF plates retain --repetitions. Do not combine with clone_objects for STL." },
|
|
232
295
|
orient: { type: "boolean", description: "Bambu-compatible slicing: auto-orient for printability (--orient)." },
|
|
233
296
|
arrange: { type: "boolean", description: "Bambu-compatible slicing: auto-arrange objects on the plate (--arrange); STL input is arranged by default. Set false to keep an existing layout: an STL that is not already on the plate then fails to slice." },
|
|
234
297
|
ensure_on_bed: { type: "boolean", description: "Bambu-compatible slicing: lower floating models onto the bed (--ensure-on-bed)." },
|
|
@@ -282,6 +345,7 @@ const PRINTER_TARGET_PROPERTIES = {
|
|
|
282
345
|
/** Tools that declare an outputSchema: their errors carry no structuredContent, which clients would validate. */
|
|
283
346
|
const OUTPUT_SCHEMA_TOOLS = new Set([
|
|
284
347
|
"counterpart_printer_info", "counterpart_print_progress", "camera_snapshot", "get_printer_filaments", "pause_print", "resume_print",
|
|
348
|
+
"camera_stream_start", "camera_frame", "camera_stream_stop",
|
|
285
349
|
"fusion_mcp_status", "fusion_mcp_call", "fusion_mcp_new_design", "fusion_mcp_export",
|
|
286
350
|
]);
|
|
287
351
|
/** layer_height / wall_loops / sparse_infill_density (PRD §8.4), validated before slicing. */
|
|
@@ -375,6 +439,7 @@ function buildBambuSliceOptions(args) {
|
|
|
375
439
|
options.rotateX = optionalFiniteNumber(args, "rotate_x");
|
|
376
440
|
options.rotateY = optionalFiniteNumber(args, "rotate_y");
|
|
377
441
|
options.slicePlate = optionalFiniteNumber(args, "slice_plate");
|
|
442
|
+
options.pauseAtLayers = parsePauseLayers(args?.pause_at_layers);
|
|
378
443
|
if (options.repetitions !== undefined && (!Number.isInteger(options.repetitions) || options.repetitions < 1)) {
|
|
379
444
|
throw new Error("Invalid repetitions: expected a positive integer.");
|
|
380
445
|
}
|
|
@@ -431,6 +496,11 @@ function validateBambuModel(model) {
|
|
|
431
496
|
}
|
|
432
497
|
return normalized;
|
|
433
498
|
}
|
|
499
|
+
/** An inventory model hint: the argument, else BAMBU_MODEL; validated when present, never required. */
|
|
500
|
+
function optionalBambuModelHint(value) {
|
|
501
|
+
const requested = String(value ?? DEFAULT_BAMBU_MODEL ?? "").trim().toLowerCase();
|
|
502
|
+
return requested ? validateBambuModel(requested) : undefined;
|
|
503
|
+
}
|
|
434
504
|
function resolveBedType(bedType) {
|
|
435
505
|
const resolved = (bedType || DEFAULT_BED_TYPE).trim().toLowerCase();
|
|
436
506
|
if (!VALID_BED_TYPES.includes(resolved)) {
|
|
@@ -543,23 +613,27 @@ class ThreeDPrinterMCPServer {
|
|
|
543
613
|
constructor() {
|
|
544
614
|
this.blender = new BlenderMcpBridge();
|
|
545
615
|
this.fusion = new FusionMcpBridge();
|
|
616
|
+
/** FULU's BambuNetwork bridge host, started on first use by the bambu_network route. */
|
|
617
|
+
this.bambuNetwork = new BambuNetworkBridge();
|
|
546
618
|
this.runtimeConfig = readRuntimeConfig();
|
|
547
619
|
this.server = new Server({
|
|
548
620
|
name: SERVER_NAME,
|
|
549
621
|
version: SERVER_VERSION
|
|
550
622
|
}, {
|
|
551
623
|
capabilities: {
|
|
624
|
+
prompts: {},
|
|
552
625
|
resources: {},
|
|
553
626
|
tools: {}
|
|
554
|
-
}
|
|
627
|
+
},
|
|
628
|
+
instructions: SERVER_INSTRUCTIONS
|
|
555
629
|
});
|
|
556
630
|
this.printerFactory = new PrinterFactory((message) => this.confirmHardware(message));
|
|
557
|
-
this.counterpart = new CounterpartService((printerType) => this.printerFactory.getImplementation(printerType), axios.create({ timeout: 10000 }), { name: SERVER_NAME, version: SERVER_VERSION }, (material) => resolveFilamentProfile(material, defaultFilamentProfileContext()));
|
|
631
|
+
this.counterpart = new CounterpartService((printerType) => this.printerFactory.getImplementation(printerType), axios.create({ timeout: 10000 }), { name: SERVER_NAME, version: SERVER_VERSION }, (material) => resolveFilamentProfile(material, defaultFilamentProfileContext()), () => ({ slicerType: DEFAULT_SLICER_TYPE, slicerPath: DEFAULT_SLICER_PATH || undefined }));
|
|
558
632
|
this.stlManipulator = new STLManipulator(TEMP_DIR);
|
|
559
633
|
this.setupHandlers();
|
|
560
634
|
this.setupErrorHandling();
|
|
561
635
|
}
|
|
562
|
-
/** Human MCP elicitation
|
|
636
|
+
/** Human MCP elicitation for a physical check software cannot make (resuming a job that cannot be re-read). */
|
|
563
637
|
async confirmHardware(message) {
|
|
564
638
|
let response;
|
|
565
639
|
const timeout = confirmationTimeoutMs();
|
|
@@ -577,7 +651,7 @@ class ThreeDPrinterMCPServer {
|
|
|
577
651
|
}, { timeout });
|
|
578
652
|
}
|
|
579
653
|
catch (error) {
|
|
580
|
-
// A person may take minutes to check the
|
|
654
|
+
// A person may take minutes to check the printer; a timeout is not a missing capability.
|
|
581
655
|
if (error?.code === ErrorCode.RequestTimeout)
|
|
582
656
|
throw new Error(confirmationTimedOut(timeout));
|
|
583
657
|
throw new Error(CONFIRMATION_UNAVAILABLE);
|
|
@@ -655,6 +729,9 @@ class ThreeDPrinterMCPServer {
|
|
|
655
729
|
// Generic OrcaSlicer keeps its user-supplied machine profile unless the
|
|
656
730
|
// call explicitly targets a Bambu model.
|
|
657
731
|
const bambuCli = isBambuProjectSlicer(slicerType) || (slicerType === "orcaslicer" && explicitModel !== undefined);
|
|
732
|
+
if (!bambuCli && parsePauseLayers(args.pause_at_layers)) {
|
|
733
|
+
throw new Error(`Pausing at a layer needs the Bambu Studio or OrcaSlicer CLI, not ${slicerType}.`);
|
|
734
|
+
}
|
|
658
735
|
let printerPreset;
|
|
659
736
|
let options;
|
|
660
737
|
if (bambuCli) {
|
|
@@ -702,44 +779,245 @@ class ThreeDPrinterMCPServer {
|
|
|
702
779
|
}
|
|
703
780
|
setupHandlers() {
|
|
704
781
|
this.setupResourceHandlers();
|
|
782
|
+
this.setupPromptHandlers();
|
|
705
783
|
this.setupToolHandlers();
|
|
706
784
|
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
785
|
+
/**
|
|
786
|
+
* The AMS choice for a Bambu print, as bambu-printer-mcp's print_3mf resolves
|
|
787
|
+
* it: the 3MF's own mapping (ordered by project filament index), overridden by
|
|
788
|
+
* an explicit ams_mapping, which ams_slots (one tray per used filament)
|
|
789
|
+
* replaces. use_ams:false prints from the external spool. Tray values are
|
|
790
|
+
* checked here; the adapter expands ams_slots against the inspected plate.
|
|
791
|
+
*/
|
|
792
|
+
resolveAmsPrintOptions(parsed3MFData, args, model) {
|
|
793
|
+
const trayOptions = { dualNozzle: isDualNozzleModel(model) };
|
|
794
|
+
let finalAmsMapping;
|
|
795
|
+
let finalAmsSlots;
|
|
796
|
+
if (parsed3MFData?.slicerConfig?.ams_mapping) {
|
|
797
|
+
const slots = normalizeAmsMappingObject(parsed3MFData.slicerConfig.ams_mapping, "ams_mapping", trayOptions);
|
|
798
|
+
if (slots.length > 0)
|
|
799
|
+
finalAmsMapping = slots;
|
|
710
800
|
}
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
801
|
+
let useAMS = args?.use_ams !== undefined ? Boolean(args.use_ams) : !!finalAmsMapping && finalAmsMapping.length > 0;
|
|
802
|
+
const hasUserAmsMapping = hasAmsMappingInput(args?.ams_mapping);
|
|
803
|
+
const hasUserAmsSlots = Array.isArray(args?.ams_slots);
|
|
804
|
+
if (hasUserAmsMapping) {
|
|
805
|
+
let userMapping;
|
|
806
|
+
if (Array.isArray(args.ams_mapping)) {
|
|
807
|
+
userMapping = args.ams_mapping.map((value, index) => normalizeBridgeAmsTrayValue(value, `ams_mapping[${index}]`, trayOptions));
|
|
808
|
+
}
|
|
809
|
+
else if (args.ams_mapping && typeof args.ams_mapping === "object") {
|
|
810
|
+
userMapping = normalizeAmsMappingObject(args.ams_mapping, "ams_mapping", trayOptions);
|
|
811
|
+
}
|
|
812
|
+
else {
|
|
813
|
+
throw new Error("ams_mapping must be an array of trays or an object of trays keyed by project filament index.");
|
|
724
814
|
}
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
.sort((a, b) => a - b);
|
|
815
|
+
if (userMapping.length > 0) {
|
|
816
|
+
finalAmsMapping = userMapping;
|
|
817
|
+
useAMS = true;
|
|
729
818
|
}
|
|
730
|
-
|
|
731
|
-
|
|
819
|
+
}
|
|
820
|
+
if (hasUserAmsSlots) {
|
|
821
|
+
const userSlots = args.ams_slots.map((value, index) => normalizeBridgeAmsTrayValue(value, `ams_slots[${index}]`, trayOptions));
|
|
822
|
+
if (userSlots.length > 0) {
|
|
823
|
+
finalAmsSlots = userSlots;
|
|
824
|
+
finalAmsMapping = undefined;
|
|
732
825
|
useAMS = true;
|
|
733
826
|
}
|
|
734
827
|
}
|
|
735
828
|
if (args?.use_ams === false) {
|
|
736
|
-
|
|
829
|
+
if (!hasUserAmsMapping)
|
|
830
|
+
finalAmsMapping = undefined;
|
|
831
|
+
if (!hasUserAmsSlots)
|
|
832
|
+
finalAmsSlots = undefined;
|
|
737
833
|
useAMS = false;
|
|
738
834
|
}
|
|
739
|
-
if (!finalAmsMapping || finalAmsMapping.length === 0) {
|
|
835
|
+
if ((!finalAmsMapping || finalAmsMapping.length === 0) && (!finalAmsSlots || finalAmsSlots.length === 0)) {
|
|
740
836
|
useAMS = false;
|
|
741
837
|
}
|
|
742
|
-
return { useAMS, amsMapping: finalAmsMapping };
|
|
838
|
+
return { useAMS, amsMapping: finalAmsMapping, amsSlots: finalAmsSlots };
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* print_3mf over FULU's BambuNetwork bridge (connection_mode bambu_network),
|
|
842
|
+
* ported from bambu-printer-mcp: the route a P1S on authorization firmware
|
|
843
|
+
* without Developer Mode prints through. connection_type lan opens the
|
|
844
|
+
* plug-in's authenticated local session (start_local_print); cloud sends
|
|
845
|
+
* through Bambu Cloud (start_print). The shared gates run first: the exact
|
|
846
|
+
* snapshot bytes are inspected, the AMS mapping checked, and fresh LAN MQTT
|
|
847
|
+
* state validated before the bridge starts, again immediately before the
|
|
848
|
+
* print frame, and the job is recorded for a checked resume.
|
|
849
|
+
*/
|
|
850
|
+
async print3mfViaBambuNetwork(options) {
|
|
851
|
+
const { args, model, bedType, connectionType } = options;
|
|
852
|
+
const devId = options.serial.trim();
|
|
853
|
+
const devIp = options.host.trim();
|
|
854
|
+
const password = options.token.trim();
|
|
855
|
+
if (!devId || !devIp || devIp === "localhost" || !password) {
|
|
856
|
+
throw new Error("FULU BambuNetwork printing requires bambu_serial, the printer host and its LAN access code: fresh printer state is checked over MQTT before every bridge print, including cloud dispatch.");
|
|
857
|
+
}
|
|
858
|
+
const isLocalBridgePrint = connectionType === "lan";
|
|
859
|
+
const bridgePrintMethod = isLocalBridgePrint ? "start_local_print" : "start_print";
|
|
860
|
+
const bridgeMethod = `net.${bridgePrintMethod}`;
|
|
861
|
+
const { plateIndex } = options;
|
|
862
|
+
const threeMfFilename = path.basename(options.threeMFPath);
|
|
863
|
+
const projectName = threeMfFilename.replace(/\.3mf$/i, "");
|
|
864
|
+
const presetName = `${projectName}_plate_${plateIndex + 1}`;
|
|
865
|
+
const clientJobId = Date.now();
|
|
866
|
+
const bambu = this.printerFactory.getImplementation("bambu");
|
|
867
|
+
const dualNozzle = isDualNozzleModel(model);
|
|
868
|
+
return withPrinterOperation(devIp, devId, (assertActive) => withPrintSnapshot(options.threeMFPath, async (snapshot, retainForDispatchedUpload) => {
|
|
869
|
+
const inspection = await inspectPrintFile(snapshot, { model, nozzleDiameters: options.nozzleDiameters, plateIndex, bedType });
|
|
870
|
+
const resumeFile = await checkedFile(inspection, snapshot);
|
|
871
|
+
// The options Bambu Studio's Send dialog would send; explicit ones win.
|
|
872
|
+
const studio = resolveStudioPrintOptions(model, callerPrintOptions(args), { usedNozzles: usedNozzleCount(inspection) });
|
|
873
|
+
const { useAMS, amsSlots } = options.ams;
|
|
874
|
+
let mapping = options.ams.amsMapping?.slice();
|
|
875
|
+
if (amsSlots) {
|
|
876
|
+
if (amsSlots.length !== inspection.usedFilamentPositions.length)
|
|
877
|
+
throw new Error("ams_slots must supply exactly one physical tray per used filament.");
|
|
878
|
+
mapping = Array(inspection.materials.length).fill(-1);
|
|
879
|
+
inspection.usedFilamentPositions.forEach((position, index) => { mapping[position] = amsSlots[index]; });
|
|
880
|
+
}
|
|
881
|
+
if (!useAMS) {
|
|
882
|
+
mapping ?? (mapping = Array(inspection.materials.length).fill(-1));
|
|
883
|
+
for (const position of inspection.usedFilamentPositions) {
|
|
884
|
+
if (mapping[position] === undefined || mapping[position] < 0) {
|
|
885
|
+
mapping[position] = externalTrayForNozzle(inspection.filamentNozzleIndices?.[position], dualNozzle);
|
|
886
|
+
}
|
|
887
|
+
if (mapping[position] !== 254 && !(dualNozzle && mapping[position] === 255)) {
|
|
888
|
+
throw new Error("use_ams:false requires an external-spool mapping for every used filament.");
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
for (const position of inspection.usedFilamentPositions) {
|
|
893
|
+
if (mapping?.[position] === undefined || mapping[position] < 0) {
|
|
894
|
+
throw new Error(`Missing physical filament mapping for project filament ${position}. Pass ams_slots (one tray per used filament), ams_mapping, or use_ams:false for the external spool.`);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
const requirements = { ...inspection, amsMapping: mapping, useAMS };
|
|
898
|
+
validatePrinterState(await bambu.getSafetyStatus(devIp, devId, password), requirements);
|
|
899
|
+
const confirmedStatus = await bambu.getSafetyStatus(devIp, devId, password);
|
|
900
|
+
validatePrinterState(confirmedStatus, requirements);
|
|
901
|
+
const wireMapping = mapping.map((value) => bridgeAmsMappingValue(value, dualNozzle));
|
|
902
|
+
const remoteFileName = uniquePrintName(options.threeMFPath);
|
|
903
|
+
const checkedName = remoteFileName.replace(/(?:\.gcode)?\.3mf$/i, "");
|
|
904
|
+
const params = {
|
|
905
|
+
dev_id: devId,
|
|
906
|
+
// The printer reports the task or project name back; both carry the unique
|
|
907
|
+
// checked name, which binds the job to this inspected artifact for a later resume.
|
|
908
|
+
task_name: checkedName,
|
|
909
|
+
project_name: checkedName,
|
|
910
|
+
preset_name: presetName,
|
|
911
|
+
filename: snapshot,
|
|
912
|
+
config_filename: snapshot,
|
|
913
|
+
plate_index: plateIndex + 1,
|
|
914
|
+
ftp_folder: "",
|
|
915
|
+
ftp_file: "",
|
|
916
|
+
ftp_file_md5: "",
|
|
917
|
+
nozzle_mapping: "",
|
|
918
|
+
ams_mapping: JSON.stringify(wireMapping),
|
|
919
|
+
ams_mapping2: JSON.stringify(mapping.map((value) => bridgeAmsMapping2Entry(value, dualNozzle))),
|
|
920
|
+
// Bambu Studio and FULU's slicer send this with every job (get_ams_mapping_result).
|
|
921
|
+
ams_mapping_info: inspection.projectFilaments?.length
|
|
922
|
+
? JSON.stringify(studioAmsMappingInfo(inspection.projectFilaments, mapping, (tray) => trayColour(confirmedStatus.raw, tray)))
|
|
923
|
+
: "",
|
|
924
|
+
nozzles_info: "",
|
|
925
|
+
connection_type: connectionType,
|
|
926
|
+
comments: "",
|
|
927
|
+
origin_profile_id: 0,
|
|
928
|
+
stl_design_id: 0,
|
|
929
|
+
origin_model_id: "",
|
|
930
|
+
print_type: "from_normal",
|
|
931
|
+
dst_file: remoteFileName,
|
|
932
|
+
dev_name: "",
|
|
933
|
+
dev_ip: devIp,
|
|
934
|
+
use_ssl_for_ftp: true,
|
|
935
|
+
use_ssl_for_mqtt: true,
|
|
936
|
+
username: "bblp",
|
|
937
|
+
password,
|
|
938
|
+
task_bed_leveling: studio.task_bed_leveling,
|
|
939
|
+
task_flow_cali: studio.task_flow_cali,
|
|
940
|
+
task_vibration_cali: studio.task_vibration_cali,
|
|
941
|
+
task_layer_inspect: studio.task_layer_inspect,
|
|
942
|
+
task_record_timelapse: args?.timelapse !== undefined ? Boolean(args.timelapse) : false,
|
|
943
|
+
task_use_ams: useAMS,
|
|
944
|
+
task_bed_type: bedType,
|
|
945
|
+
extra_options: "",
|
|
946
|
+
auto_bed_leveling: studio.auto_bed_leveling,
|
|
947
|
+
auto_flow_cali: studio.auto_flow_cali,
|
|
948
|
+
auto_offset_cali: studio.auto_offset_cali,
|
|
949
|
+
extruder_cali_manual_mode: studio.extruder_cali_manual_mode,
|
|
950
|
+
task_ext_change_assist: false,
|
|
951
|
+
try_emmc_print: false,
|
|
952
|
+
};
|
|
953
|
+
assertActive();
|
|
954
|
+
const bridgeOptions = {
|
|
955
|
+
// Trusted in-process preflight immediately before the print frame.
|
|
956
|
+
beforeDispatch: async (method) => {
|
|
957
|
+
if (method !== bridgeMethod)
|
|
958
|
+
return;
|
|
959
|
+
assertActive();
|
|
960
|
+
validatePrinterState(await bambu.getSafetyStatus(devIp, devId, password), requirements);
|
|
961
|
+
},
|
|
962
|
+
assertDispatchAllowed: (method) => { if (method === bridgeMethod)
|
|
963
|
+
assertActive(); },
|
|
964
|
+
onDispatched: (method) => { if (method === bridgeMethod)
|
|
965
|
+
retainForDispatchedUpload(); },
|
|
966
|
+
};
|
|
967
|
+
let jobUpdates = [];
|
|
968
|
+
let bridgeResult;
|
|
969
|
+
if (isLocalBridgePrint) {
|
|
970
|
+
({ result: bridgeResult, jobUpdates } = await this.bambuNetwork.startLocalPrint({ devId, devIp, username: "bblp", password, useSsl: true }, bridgeMethod, { client_job_id: clientJobId, params }, bridgeOptions, assertActive));
|
|
971
|
+
}
|
|
972
|
+
else {
|
|
973
|
+
({ result: bridgeResult, jobUpdates } = await this.bambuNetwork.startCloudPrint(devId, bridgeMethod, { client_job_id: clientJobId, params }, bridgeOptions, assertActive));
|
|
974
|
+
}
|
|
975
|
+
if (typeof bridgeResult === "object" && bridgeResult !== null && bridgeResult.ok === false) {
|
|
976
|
+
throw new Error(`FULU BambuNetwork bridge method ${bridgeMethod} failed: ${String(bridgeResult.error || "unknown bridge error")}`);
|
|
977
|
+
}
|
|
978
|
+
const value = typeof bridgeResult === "object" && bridgeResult !== null ? bridgeResult.value : undefined;
|
|
979
|
+
if (typeof value === "number" && value !== 0) {
|
|
980
|
+
const reported = jobUpdates.filter((update) => update.code !== 0 || update.msg).slice(-3)
|
|
981
|
+
.map((update) => `status ${update.status}, code ${update.code}${update.msg ? `: ${update.msg}` : ""}`).join("; ");
|
|
982
|
+
throw new Error(`FULU BambuNetwork bridge method ${bridgeMethod} returned non-zero result ${value}${describeBambuNetworkPrintError(value)}.` +
|
|
983
|
+
(reported ? ` Plug-in reported: ${reported}.` : ""));
|
|
984
|
+
}
|
|
985
|
+
assertActive();
|
|
986
|
+
// Bambu's plug-in placed the file, so a resume re-reads it if it can and otherwise asks a person.
|
|
987
|
+
bambu.recordCheckedJob(devIp, devId, remoteFileName, requirements, resumeFile, false);
|
|
988
|
+
return {
|
|
989
|
+
status: "success",
|
|
990
|
+
message: `FULU BambuNetwork ${bridgePrintMethod} command for ${threeMfFilename} sent successfully.`,
|
|
991
|
+
connectionMode: "bambu_network",
|
|
992
|
+
connectionType,
|
|
993
|
+
bridgeMethod,
|
|
994
|
+
bridgeResult,
|
|
995
|
+
clientJobId,
|
|
996
|
+
projectName,
|
|
997
|
+
plateIndex,
|
|
998
|
+
bridgePlateIndex: plateIndex + 1,
|
|
999
|
+
printerJobName: checkedName,
|
|
1000
|
+
useAMS,
|
|
1001
|
+
amsMapping: mapping,
|
|
1002
|
+
sha256: inspection.sha256,
|
|
1003
|
+
params: { ...params, password: "[redacted]" },
|
|
1004
|
+
};
|
|
1005
|
+
}));
|
|
1006
|
+
}
|
|
1007
|
+
/** The FDM design guidance as MCP prompts: the client decides when to attach each one. */
|
|
1008
|
+
setupPromptHandlers() {
|
|
1009
|
+
this.server.setRequestHandler(ListPromptsRequestSchema, async () => ({
|
|
1010
|
+
prompts: DESIGN_PROMPTS.map(({ name, title, description }) => ({ name, title, description })),
|
|
1011
|
+
}));
|
|
1012
|
+
this.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
1013
|
+
const prompt = DESIGN_PROMPTS.find(item => item.name === request.params.name);
|
|
1014
|
+
if (!prompt)
|
|
1015
|
+
throw new McpError(ErrorCode.InvalidParams, `Unknown prompt: ${request.params.name}`);
|
|
1016
|
+
return {
|
|
1017
|
+
description: prompt.description,
|
|
1018
|
+
messages: [{ role: "user", content: { type: "text", text: prompt.text } }],
|
|
1019
|
+
};
|
|
1020
|
+
});
|
|
743
1021
|
}
|
|
744
1022
|
setupResourceHandlers() {
|
|
745
1023
|
// List available resources
|
|
@@ -757,7 +1035,8 @@ class ThreeDPrinterMCPServer {
|
|
|
757
1035
|
name: "3D Printer Files",
|
|
758
1036
|
mimeType: "application/json",
|
|
759
1037
|
description: "List of files available on the 3D printer"
|
|
760
|
-
}
|
|
1038
|
+
},
|
|
1039
|
+
FDM_CAPABILITY_RESOURCE
|
|
761
1040
|
],
|
|
762
1041
|
templates: [
|
|
763
1042
|
{
|
|
@@ -781,6 +1060,9 @@ class ThreeDPrinterMCPServer {
|
|
|
781
1060
|
// Read resource
|
|
782
1061
|
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
783
1062
|
const uri = request.params.uri;
|
|
1063
|
+
if (uri === FDM_CAPABILITY_URI) {
|
|
1064
|
+
return { contents: [{ uri, mimeType: FDM_CAPABILITY_RESOURCE.mimeType, text: FDM_CAPABILITY_GUIDANCE }] };
|
|
1065
|
+
}
|
|
784
1066
|
const match = uri.match(/^printer:\/\/([^\/]+)\/(.+)$/);
|
|
785
1067
|
if (!match) {
|
|
786
1068
|
throw new McpError(ErrorCode.InvalidRequest, `Invalid resource URI: ${uri}`);
|
|
@@ -823,7 +1105,7 @@ class ThreeDPrinterMCPServer {
|
|
|
823
1105
|
// List available tools
|
|
824
1106
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
825
1107
|
return {
|
|
826
|
-
tools: [
|
|
1108
|
+
tools: annotateTools([
|
|
827
1109
|
{
|
|
828
1110
|
name: "get_printer_status",
|
|
829
1111
|
description: "Get the current status of the 3D printer",
|
|
@@ -892,7 +1174,7 @@ class ThreeDPrinterMCPServer {
|
|
|
892
1174
|
},
|
|
893
1175
|
{
|
|
894
1176
|
name: "upload_gcode",
|
|
895
|
-
description: "Upload G-code content or a local G-code file path to the printer. With print=true the exact uploaded bytes are inspected first (every S/R heater target, tool changes, hardware and material ceilings)
|
|
1177
|
+
description: "Upload G-code content or a local G-code file path to the printer. With print=true the exact uploaded bytes are inspected first (every S/R heater target, tool changes, hardware and material ceilings) and printer state is checked before the print starts.",
|
|
896
1178
|
inputSchema: {
|
|
897
1179
|
type: "object",
|
|
898
1180
|
properties: {
|
|
@@ -950,7 +1232,7 @@ class ThreeDPrinterMCPServer {
|
|
|
950
1232
|
},
|
|
951
1233
|
{
|
|
952
1234
|
name: "start_print",
|
|
953
|
-
description: "Start printing a G-code file already stored on the printer. The server downloads and inspects the exact file, then starts a uniquely named checked copy after printer-state checks
|
|
1235
|
+
description: "Start printing a G-code file already stored on the printer. The server downloads and inspects the exact file, then starts a uniquely named checked copy after printer-state checks. Printers whose API cannot download files (Repetier, Prusa, Creality) refuse; use upload_gcode with print=true instead.",
|
|
954
1236
|
inputSchema: {
|
|
955
1237
|
type: "object",
|
|
956
1238
|
properties: {
|
|
@@ -1024,23 +1306,34 @@ class ThreeDPrinterMCPServer {
|
|
|
1024
1306
|
bambu_token: {
|
|
1025
1307
|
type: "string",
|
|
1026
1308
|
description: "Access token for Bambu Lab printers (default: value from env)"
|
|
1027
|
-
}
|
|
1309
|
+
},
|
|
1310
|
+
...CONNECTION_PROPERTIES,
|
|
1028
1311
|
}
|
|
1029
1312
|
}
|
|
1030
1313
|
},
|
|
1314
|
+
...CAMERA_STREAM_TOOLS,
|
|
1315
|
+
{
|
|
1316
|
+
name: "counterpart_print_route",
|
|
1317
|
+
description: "Counterpart contract (counterpart/1), Bambu: how this server reaches the printer for prints and for Stop/Pause/Resume. route is \"lan\" (the local network through FULU's BambuNetwork bridge), \"cloud\" (Bambu Cloud through FULU's bridge) or \"developer\" (the local network straight to a printer in LAN Only + Developer Mode, whose Pause, Resume and Stop need no Bambu app). options lists each with its label, whether it can be chosen here (and why not) and what controls do on it; choices lists the ids that can be chosen. mode and type are BAMBU_DEFAULT_CONNECTION_MODE and BAMBU_DEFAULT_CONNECTION_TYPE as resolved. Pass route to change it: the change applies to the next print and control at once, and is saved to the .env file in this server's working directory when that file exists (savedTo; null otherwise). Sends nothing to the printer.",
|
|
1318
|
+
inputSchema: {
|
|
1319
|
+
type: "object",
|
|
1320
|
+
properties: {
|
|
1321
|
+
route: { type: "string", enum: ["lan", "cloud", "developer"], description: "Change the route. Omit to read it." }
|
|
1322
|
+
},
|
|
1323
|
+
additionalProperties: false
|
|
1324
|
+
}
|
|
1325
|
+
},
|
|
1031
1326
|
{
|
|
1032
1327
|
name: "counterpart_printer_info",
|
|
1033
1328
|
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.",
|
|
1034
1329
|
inputSchema: { type: "object", properties: { ...PRINTER_TARGET_PROPERTIES } },
|
|
1035
1330
|
outputSchema: PRINTER_INFO_OUTPUT_SCHEMA,
|
|
1036
|
-
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
1037
1331
|
},
|
|
1038
1332
|
{
|
|
1039
1333
|
name: "counterpart_print_progress",
|
|
1040
1334
|
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.",
|
|
1041
1335
|
inputSchema: { type: "object", properties: { ...PRINTER_TARGET_PROPERTIES } },
|
|
1042
1336
|
outputSchema: PRINT_PROGRESS_OUTPUT_SCHEMA,
|
|
1043
|
-
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
1044
1337
|
},
|
|
1045
1338
|
{
|
|
1046
1339
|
name: "camera_snapshot",
|
|
@@ -1054,32 +1347,35 @@ class ThreeDPrinterMCPServer {
|
|
|
1054
1347
|
},
|
|
1055
1348
|
},
|
|
1056
1349
|
outputSchema: CAMERA_SNAPSHOT_OUTPUT_SCHEMA,
|
|
1057
|
-
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
1058
1350
|
},
|
|
1059
1351
|
{
|
|
1060
1352
|
name: "get_printer_filaments",
|
|
1061
|
-
description: "Report the loaded filament as one external slot (id \"ext\")
|
|
1062
|
-
inputSchema: {
|
|
1353
|
+
description: "Report the loaded filament. Bambu: the live AMS and external-spool inventory over MQTT, as bambu-printer-mcp reports it (trays with absolute slot, loaded state, material, colour, tray_info_idx and resolved slicer profile path; summary; recommended load_filaments), plus counterpart/1 slots for every loaded tray and external spool (254, and 255 on dual-nozzle printers). Other printers: one external slot (id \"ext\"); Moonraker printers with Spoolman report the active spool's material, colour and name, others source \"unknown\". Read-only.",
|
|
1354
|
+
inputSchema: {
|
|
1355
|
+
type: "object",
|
|
1356
|
+
properties: {
|
|
1357
|
+
...PRINTER_TARGET_PROPERTIES,
|
|
1358
|
+
bambu_model: { type: "string", description: "Bambu only: optional model hint used to resolve Bambu/Orca filament profile JSONs for each tray (default: BAMBU_MODEL)." },
|
|
1359
|
+
nozzle_diameter: { type: "string", description: "Bambu only: optional nozzle diameter used when resolving model-specific filament profile JSONs (default: 0.4)." },
|
|
1360
|
+
},
|
|
1361
|
+
},
|
|
1063
1362
|
outputSchema: FILAMENTS_OUTPUT_SCHEMA,
|
|
1064
|
-
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
1065
1363
|
},
|
|
1066
1364
|
{
|
|
1067
1365
|
name: "pause_print",
|
|
1068
|
-
description: "Pause the current print (OctoPrint, Klipper/Moonraker, PrusaLink, Duet M25, Repetier, and Creality printers that run Moonraker). Pause is never gated.",
|
|
1069
|
-
inputSchema: { type: "object", properties: { ...PRINTER_TARGET_PROPERTIES } },
|
|
1366
|
+
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.",
|
|
1367
|
+
inputSchema: { type: "object", properties: { ...PRINTER_TARGET_PROPERTIES, ...CONNECTION_PROPERTIES } },
|
|
1070
1368
|
outputSchema: CONTROL_OUTPUT_SCHEMA,
|
|
1071
|
-
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
1072
1369
|
},
|
|
1073
1370
|
{
|
|
1074
1371
|
name: "resume_print",
|
|
1075
|
-
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. Resume other jobs at the printer.",
|
|
1076
|
-
inputSchema: { type: "object", properties: { ...PRINTER_TARGET_PROPERTIES } },
|
|
1372
|
+
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.",
|
|
1373
|
+
inputSchema: { type: "object", properties: { ...PRINTER_TARGET_PROPERTIES, ...CONNECTION_PROPERTIES } },
|
|
1077
1374
|
outputSchema: CONTROL_OUTPUT_SCHEMA,
|
|
1078
|
-
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
1079
1375
|
},
|
|
1080
1376
|
{
|
|
1081
1377
|
name: "set_printer_temperature",
|
|
1082
|
-
description: "Set the temperature of a printer component. Temperature 0 switches a heater off and is never gated. Positive targets are validated before connecting, limited by independent hardware and material ceilings, require a ready printer
|
|
1378
|
+
description: "Set the temperature of a printer component. Temperature 0 switches a heater off and is never gated. Positive targets are validated before connecting, limited by independent hardware and material ceilings, and require a ready printer.",
|
|
1083
1379
|
inputSchema: {
|
|
1084
1380
|
type: "object",
|
|
1085
1381
|
properties: {
|
|
@@ -1511,16 +1807,34 @@ class ThreeDPrinterMCPServer {
|
|
|
1511
1807
|
required: ["stl_path"]
|
|
1512
1808
|
}
|
|
1513
1809
|
},
|
|
1810
|
+
{
|
|
1811
|
+
name: "inspect_print_file",
|
|
1812
|
+
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.",
|
|
1813
|
+
inputSchema: {
|
|
1814
|
+
type: "object",
|
|
1815
|
+
additionalProperties: false,
|
|
1816
|
+
properties: {
|
|
1817
|
+
file_path: { type: "string", minLength: 1, description: "Local prepared .3mf or textual .gcode artifact to inspect; checksum-only and unsliced projects reject." },
|
|
1818
|
+
bambu_model: { type: "string", enum: [...VALID_BAMBU_MODELS], description: "Explicit file-inspection target model; never inferred from environment or live state." },
|
|
1819
|
+
plate_index: { type: "integer", minimum: 0, description: "Zero-based selected plate; defaults to 0. Raw G-code only supports 0." },
|
|
1820
|
+
nozzle_diameters: { type: "array", minItems: 1, maxItems: 2, items: { type: "number", enum: [0.2, 0.4, 0.6, 0.8] }, description: "Optional requested nozzle diameters to compare with file declarations; this does not observe installed hardware." },
|
|
1821
|
+
bed_type: { type: "string", enum: [...VALID_BED_TYPES], description: "Optional declared bed type to compare with file metadata." },
|
|
1822
|
+
},
|
|
1823
|
+
required: ["file_path", "bambu_model"],
|
|
1824
|
+
},
|
|
1825
|
+
},
|
|
1514
1826
|
{
|
|
1515
1827
|
name: "print_3mf",
|
|
1516
|
-
description: "Print a 3MF file on a Bambu Lab printer. The exact selected plate is inspected (model, nozzle, bed type, materials, every heater target) and checked against a fresh MQTT report of the printer's identity, nozzle, state, errors and loaded filament
|
|
1828
|
+
description: "Print a 3MF file on a Bambu Lab printer. The exact selected plate is inspected (model, nozzle, bed type, materials, every heater target) and checked against a fresh MQTT report of the printer's identity, nozzle, state, errors and loaded filament before upload and start.",
|
|
1517
1829
|
inputSchema: {
|
|
1518
1830
|
type: "object",
|
|
1519
1831
|
properties: {
|
|
1832
|
+
pause_at_layers: { type: "array", items: { type: "integer", minimum: 2 }, description: "Pause before these 1-based layers when auto-slicing. An already sliced 3MF must contain the requested pauses or printing is refused." },
|
|
1520
1833
|
three_mf_path: {
|
|
1521
1834
|
type: "string",
|
|
1522
1835
|
description: "Path to the 3MF file to print."
|
|
1523
1836
|
},
|
|
1837
|
+
plate_index: { type: "integer", minimum: 0, description: "Zero-based selected plate; defaults to 0. The same plate is inspected and printed; auto-slicing uses its one-based CLI plate number." },
|
|
1524
1838
|
bambu_model: {
|
|
1525
1839
|
type: "string",
|
|
1526
1840
|
enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
|
|
@@ -1556,10 +1870,18 @@ class ThreeDPrinterMCPServer {
|
|
|
1556
1870
|
nozzle_temperature: { type: "number", description: "Override nozzle temperature (°C)." },
|
|
1557
1871
|
bed_temperature: { type: "number", description: "Override bed temperature (°C)." },
|
|
1558
1872
|
support_enabled: { type: "boolean", description: "Override support generation." },
|
|
1873
|
+
...CONNECTION_PROPERTIES,
|
|
1559
1874
|
ams_mapping: {
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1875
|
+
anyOf: [
|
|
1876
|
+
{ type: "array", items: { type: "number" } },
|
|
1877
|
+
{ type: "object", additionalProperties: { type: "number" } },
|
|
1878
|
+
],
|
|
1879
|
+
description: "Project-level AMS mapping. Array: position = project filament index, value = absolute AMS tray (0-3 = AMS 0, 4-7 = AMS 1, 8-11 = AMS 2, 12-15 = AMS 3, 128+ = AMS-HT, 254 = external spool, -1 = unused); on dual-nozzle printers (H2D, H2D Pro, H2C, X2D) 254 is the left/deputy nozzle external spool and 255 the right/main one. Object: trays keyed by project filament index (numeric keys first, in order), e.g. {\"0\": 2}. Prefer ams_slots unless you know the project-level layout.",
|
|
1880
|
+
},
|
|
1881
|
+
ams_slots: {
|
|
1882
|
+
type: "array",
|
|
1883
|
+
items: { type: "number" },
|
|
1884
|
+
description: "Preferred AMS input: one absolute tray per USED filament in plate order, e.g. [1] for a single-filament print from AMS 0 slot 1. Use 254 for the external spool (on dual-nozzle printers 254 = left/deputy, 255 = right/main). Expanded to the project-level ams_mapping from the 3MF's plate_N.json and G-code header. Overrides ams_mapping.",
|
|
1563
1885
|
},
|
|
1564
1886
|
slicer_type: {
|
|
1565
1887
|
type: "string",
|
|
@@ -1580,7 +1902,7 @@ class ThreeDPrinterMCPServer {
|
|
|
1580
1902
|
},
|
|
1581
1903
|
use_ams: {
|
|
1582
1904
|
type: "boolean",
|
|
1583
|
-
description: "Whether to use
|
|
1905
|
+
description: "Whether to use the AMS. Defaults to true when ams_slots, ams_mapping or the 3MF's own mapping is present; false prints every used filament from the external spool."
|
|
1584
1906
|
},
|
|
1585
1907
|
bed_leveling: {
|
|
1586
1908
|
type: "boolean",
|
|
@@ -1860,7 +2182,7 @@ class ThreeDPrinterMCPServer {
|
|
|
1860
2182
|
},
|
|
1861
2183
|
outputSchema: FUSION_EXPORT_OUTPUT_SCHEMA,
|
|
1862
2184
|
}
|
|
1863
|
-
]
|
|
2185
|
+
])
|
|
1864
2186
|
};
|
|
1865
2187
|
});
|
|
1866
2188
|
// Handle tool calls
|
|
@@ -1882,11 +2204,40 @@ class ThreeDPrinterMCPServer {
|
|
|
1882
2204
|
type, host, port,
|
|
1883
2205
|
apiKey: type.toLowerCase() === "bambu" ? `${bambuSerial}:${bambuToken}` : apiKey,
|
|
1884
2206
|
};
|
|
2207
|
+
// Bambu print controls go over the same connection as the print (resolveConnection);
|
|
2208
|
+
// undefined keeps direct LAN MQTT in BambuImplementation.
|
|
2209
|
+
const bambuControlRoute = (command) => {
|
|
2210
|
+
if (type.toLowerCase() !== "bambu")
|
|
2211
|
+
return undefined;
|
|
2212
|
+
const route = resolveConnection(args);
|
|
2213
|
+
assertConnectionAvailable(route.mode);
|
|
2214
|
+
if (route.mode !== "bambu_network")
|
|
2215
|
+
return undefined;
|
|
2216
|
+
return (assertActive, beforeDispatch) => {
|
|
2217
|
+
const options = { assertDispatchAllowed: () => { extra.signal.throwIfAborted(); assertActive?.(); } };
|
|
2218
|
+
return route.type === "cloud"
|
|
2219
|
+
? this.bambuNetwork.sendCloudPrintCommand(bambuSerial, command, options, beforeDispatch)
|
|
2220
|
+
: this.bambuNetwork.sendLocalPrintCommand({ devId: bambuSerial, devIp: host, username: "bblp", password: bambuToken, useSsl: true }, command, options, beforeDispatch);
|
|
2221
|
+
};
|
|
2222
|
+
};
|
|
1885
2223
|
const structured = (value) => ({
|
|
1886
2224
|
content: [{ type: "text", text: JSON.stringify(value, null, 2) }],
|
|
1887
2225
|
structuredContent: value,
|
|
1888
2226
|
});
|
|
1889
2227
|
switch (name) {
|
|
2228
|
+
case "camera_stream_start":
|
|
2229
|
+
return structured(await this.counterpart.cameraStreamStart(target, { fps: args?.fps, idleTimeoutMs: args?.idle_timeout_ms }));
|
|
2230
|
+
case "camera_frame": {
|
|
2231
|
+
const frame = await this.counterpart.cameraStreamFrame(target, {
|
|
2232
|
+
sinceSeq: args?.since_seq, waitMs: args?.wait_ms,
|
|
2233
|
+
maxWidth: args?.max_width, maxBytes: args?.max_bytes,
|
|
2234
|
+
changedOnly: args?.changed_only, minChange: args?.min_change, signal: extra.signal,
|
|
2235
|
+
});
|
|
2236
|
+
// The JPEG appears only once in the MCP payload; text is useful metadata.
|
|
2237
|
+
return { content: [{ type: "text", text: JSON.stringify(Object.fromEntries(Object.entries(frame).filter(([key]) => key !== "base64"))) }], structuredContent: frame };
|
|
2238
|
+
}
|
|
2239
|
+
case "camera_stream_stop":
|
|
2240
|
+
return structured(this.counterpart.cameraStreamStop(target));
|
|
1890
2241
|
case "counterpart_printer_info":
|
|
1891
2242
|
return structured(await this.counterpart.printerInfo(target));
|
|
1892
2243
|
case "counterpart_print_progress":
|
|
@@ -1901,12 +2252,81 @@ class ThreeDPrinterMCPServer {
|
|
|
1901
2252
|
timeoutMs: args?.timeout_ms,
|
|
1902
2253
|
}));
|
|
1903
2254
|
}
|
|
1904
|
-
case "get_printer_filaments":
|
|
1905
|
-
|
|
2255
|
+
case "get_printer_filaments": {
|
|
2256
|
+
const filamentOptions = type.toLowerCase() === "bambu" ? {
|
|
2257
|
+
bambuModel: optionalBambuModelHint(args?.bambu_model),
|
|
2258
|
+
nozzleDiameter: String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER),
|
|
2259
|
+
} : {};
|
|
2260
|
+
return structured(await this.counterpart.filaments(target, filamentOptions));
|
|
2261
|
+
}
|
|
1906
2262
|
case "pause_print":
|
|
1907
|
-
return structured(await this.counterpart.pause(target));
|
|
2263
|
+
return structured(await this.counterpart.pause(target, bambuControlRoute));
|
|
1908
2264
|
case "resume_print":
|
|
1909
|
-
return structured(await this.counterpart.resume(target));
|
|
2265
|
+
return structured(await this.counterpart.resume(target, bambuControlRoute));
|
|
2266
|
+
case "counterpart_print_route": {
|
|
2267
|
+
let savedTo = null;
|
|
2268
|
+
// FULU's bridge is set up (configured or discovered), so the routes through it can be used.
|
|
2269
|
+
const bridgeConfigured = Boolean(this.bambuNetwork.resolveBridgeCommand());
|
|
2270
|
+
if (args?.route !== undefined) {
|
|
2271
|
+
if (!PRINT_ROUTES.includes(args.route))
|
|
2272
|
+
throw new Error('route must be "lan", "cloud" or "developer".');
|
|
2273
|
+
const next = connectionForRoute(args.route, { mode: DEFAULT_BAMBU_CONNECTION_MODE, type: DEFAULT_BAMBU_CONNECTION_TYPE }, bridgeConfigured);
|
|
2274
|
+
// dotenv loaded .env from the working directory at start; a client's own env block has no file to update.
|
|
2275
|
+
const file = path.resolve(process.cwd(), ".env");
|
|
2276
|
+
if (fs.existsSync(file)) {
|
|
2277
|
+
writeEnvValues(file, {
|
|
2278
|
+
...(next.mode ? { BAMBU_DEFAULT_CONNECTION_MODE: next.mode } : {}),
|
|
2279
|
+
...(next.type ? { BAMBU_DEFAULT_CONNECTION_TYPE: next.type } : {}),
|
|
2280
|
+
});
|
|
2281
|
+
savedTo = file;
|
|
2282
|
+
}
|
|
2283
|
+
DEFAULT_BAMBU_CONNECTION_MODE = next.mode;
|
|
2284
|
+
DEFAULT_BAMBU_CONNECTION_TYPE = next.type;
|
|
2285
|
+
}
|
|
2286
|
+
const connection = resolveConnection({});
|
|
2287
|
+
const options = routeOptions(connection, bridgeConfigured);
|
|
2288
|
+
result = { route: routeOf(connection), mode: connection.mode, type: connection.type,
|
|
2289
|
+
choices: options.filter(option => option.available).map(option => option.id), options, savedTo };
|
|
2290
|
+
break;
|
|
2291
|
+
}
|
|
2292
|
+
case "inspect_print_file": {
|
|
2293
|
+
const allowed = new Set(["file_path", "bambu_model", "plate_index", "nozzle_diameters", "bed_type"]);
|
|
2294
|
+
if (!args || Object.keys(args).some(key => !allowed.has(key)))
|
|
2295
|
+
throw new Error("inspect_print_file accepts only file/model/plate/nozzle/bed arguments.");
|
|
2296
|
+
if (typeof args.file_path !== "string" || !args.file_path.trim() || args.file_path.includes("\0"))
|
|
2297
|
+
throw new Error("file_path must name a local prepared artifact.");
|
|
2298
|
+
if (typeof args.bambu_model !== "string" || !args.bambu_model.trim())
|
|
2299
|
+
throw new Error("An explicit bambu_model is required for file inspection.");
|
|
2300
|
+
const model = validateBambuModel(args.bambu_model);
|
|
2301
|
+
const plateIndex = args.plate_index === undefined ? 0 : args.plate_index;
|
|
2302
|
+
if (typeof plateIndex !== "number" || !Number.isSafeInteger(plateIndex) || plateIndex < 0)
|
|
2303
|
+
throw new Error("plate_index must be a nonnegative safe integer.");
|
|
2304
|
+
if (args.nozzle_diameters !== undefined && (!Array.isArray(args.nozzle_diameters) || args.nozzle_diameters.some(value => typeof value !== "number")))
|
|
2305
|
+
throw new Error("nozzle_diameters must be numeric sizes.");
|
|
2306
|
+
const nozzles = args.nozzle_diameters;
|
|
2307
|
+
if (nozzles && (nozzles.length < 1 || nozzles.length > 2 || nozzles.some(value => !SUPPORTED_NOZZLE_DIAMETERS.includes(value))))
|
|
2308
|
+
throw new Error("nozzle_diameters must contain one or two supported sizes.");
|
|
2309
|
+
if (args.bed_type !== undefined && (typeof args.bed_type !== "string" || !args.bed_type.trim()))
|
|
2310
|
+
throw new Error("bed_type must be a supported plate type.");
|
|
2311
|
+
const bedType = args.bed_type === undefined ? undefined : resolveBedType(args.bed_type);
|
|
2312
|
+
if (extra.signal.aborted)
|
|
2313
|
+
throw new Error("File inspection cancelled.");
|
|
2314
|
+
result = await withPrintSnapshot(args.file_path, async (snapshot) => {
|
|
2315
|
+
const inspection = await inspectPrintFile(snapshot, { model, nozzleDiameters: nozzles, plateIndex, bedType });
|
|
2316
|
+
if (extra.signal.aborted)
|
|
2317
|
+
throw new Error("File inspection cancelled.");
|
|
2318
|
+
const byteLength = (await fs.promises.stat(snapshot)).size;
|
|
2319
|
+
return {
|
|
2320
|
+
receipt_version: "1",
|
|
2321
|
+
evidence: "file-only",
|
|
2322
|
+
dispatch_authorized: false,
|
|
2323
|
+
artifact: { sha256: inspection.sha256, byte_length: byteLength, file_format: path.extname(snapshot).toLowerCase() === ".3mf" ? "3mf" : "gcode" },
|
|
2324
|
+
selection: { bambu_model: model, plate_index: plateIndex, ...(nozzles === undefined ? {} : { nozzle_diameters: nozzles }), ...(bedType === undefined ? {} : { bed_type: bedType }) },
|
|
2325
|
+
inspection,
|
|
2326
|
+
};
|
|
2327
|
+
});
|
|
2328
|
+
break;
|
|
2329
|
+
}
|
|
1910
2330
|
case "get_printer_status":
|
|
1911
2331
|
result = await this.getPrinterStatus(host, port, type, apiKey, bambuSerial, bambuToken);
|
|
1912
2332
|
break;
|
|
@@ -1945,7 +2365,7 @@ class ThreeDPrinterMCPServer {
|
|
|
1945
2365
|
result = withJobName(await this.startPrint(host, port, type, apiKey, bambuSerial, bambuToken, String(args.filename), startOptions), String(args.filename));
|
|
1946
2366
|
break;
|
|
1947
2367
|
case "cancel_print":
|
|
1948
|
-
result = await this.cancelPrint(host, port, type, apiKey, bambuSerial, bambuToken);
|
|
2368
|
+
result = await this.cancelPrint(host, port, type, apiKey, bambuSerial, bambuToken, bambuControlRoute("stop"));
|
|
1949
2369
|
break;
|
|
1950
2370
|
case "set_printer_temperature":
|
|
1951
2371
|
if (!args?.component || args?.temperature === undefined) {
|
|
@@ -2059,7 +2479,7 @@ class ThreeDPrinterMCPServer {
|
|
|
2059
2479
|
catch (error) {
|
|
2060
2480
|
console.warn(`Could not parse AMS metadata from sliced 3MF ${gcodePath}:`, error.message);
|
|
2061
2481
|
}
|
|
2062
|
-
const amsOptions = this.resolveAmsPrintOptions(parsedProcess3MF, args);
|
|
2482
|
+
const amsOptions = this.resolveAmsPrintOptions(parsedProcess3MF, args, processModel);
|
|
2063
2483
|
result = await factoryImplementation.print3mf(host, bambuSerial, bambuToken, {
|
|
2064
2484
|
projectName: path.basename(gcodePath).replace(/\.3mf$/i, ''),
|
|
2065
2485
|
filePath: gcodePath,
|
|
@@ -2085,7 +2505,7 @@ class ThreeDPrinterMCPServer {
|
|
|
2085
2505
|
// 3. Upload and print through the adapter's hard safety gate. It
|
|
2086
2506
|
// inspects the exact bytes sent (S and R targets, tool changes,
|
|
2087
2507
|
// hardware and material ceilings), requires the expected peaks to
|
|
2088
|
-
// match, reads fresh printer state
|
|
2508
|
+
// match, and reads fresh printer state.
|
|
2089
2509
|
// A mismatch refuses before upload; nothing is only logged.
|
|
2090
2510
|
const filename = path.basename(gcodePath);
|
|
2091
2511
|
const printResult = await this.uploadGcode(host, port, type, apiKey, bambuSerial, bambuToken, filename, gcodePath, true, // Start printing immediately
|
|
@@ -2241,15 +2661,20 @@ class ThreeDPrinterMCPServer {
|
|
|
2241
2661
|
if (!bambuSerial || !bambuToken) {
|
|
2242
2662
|
throw new Error("Bambu serial number and access token are required for print_3mf.");
|
|
2243
2663
|
}
|
|
2664
|
+
const printPlate = args.plate_index === undefined ? 0 : args.plate_index;
|
|
2665
|
+
if (typeof printPlate !== "number" || !Number.isSafeInteger(printPlate) || printPlate < 0 || printPlate >= Number.MAX_SAFE_INTEGER)
|
|
2666
|
+
throw new Error("plate_index must be a nonnegative safe integer with a supported one-based plate number.");
|
|
2244
2667
|
const printModel = await this.resolveBambuModel(args?.bambu_model);
|
|
2245
2668
|
const printBedType = resolveBedType(args?.bed_type);
|
|
2246
2669
|
const explicitPrintNozzle = optionalNozzleDiameterArg(args?.nozzle_diameter);
|
|
2247
2670
|
const printNozzle = String(explicitPrintNozzle ?? DEFAULT_NOZZLE_DIAMETER);
|
|
2248
2671
|
const printPreset = BAMBU_MODEL_PRESETS[printModel]?.(printNozzle);
|
|
2672
|
+
const printConnection = resolveConnection(args);
|
|
2673
|
+
assertConnectionAvailable(printConnection.mode);
|
|
2249
2674
|
let threeMFPath = String(args.three_mf_path);
|
|
2250
2675
|
// Only the selected plate's exact G-code entry counts as sliced output;
|
|
2251
2676
|
// a stray .gcode entry elsewhere in the archive is not printable.
|
|
2252
|
-
const selectedPlateEntry =
|
|
2677
|
+
const selectedPlateEntry = `Metadata/plate_${printPlate + 1}.gcode`;
|
|
2253
2678
|
let shouldAutoSlice = false;
|
|
2254
2679
|
try {
|
|
2255
2680
|
const JSZip = (await import('jszip')).default;
|
|
@@ -2263,6 +2688,14 @@ class ThreeDPrinterMCPServer {
|
|
|
2263
2688
|
}
|
|
2264
2689
|
// Only treat slicer_path as an executable selector when a slicer
|
|
2265
2690
|
// will actually launch. Ready-to-print archives ignore it.
|
|
2691
|
+
const pauseLayers = parsePauseLayers(args?.pause_at_layers);
|
|
2692
|
+
if (!shouldAutoSlice && pauseLayers) {
|
|
2693
|
+
const gcode = await readPlateGcode(threeMFPath, printPlate + 1);
|
|
2694
|
+
const placed = gcode ? pausedLayers(gcode, machinePauseGcode(gcode)) : [];
|
|
2695
|
+
const missing = pauseLayers.filter(layer => !placed.includes(layer));
|
|
2696
|
+
if (missing.length)
|
|
2697
|
+
throw new Error(`This 3MF is already sliced without a pause before layer ${missing.join(", ")}. Slice it again with pause_at_layers (slice_stl), or add the pause in Bambu Studio or OrcaSlicer.`);
|
|
2698
|
+
}
|
|
2266
2699
|
if (shouldAutoSlice) {
|
|
2267
2700
|
const printSlicerPath = this.resolveExecutableSelectorArg(args?.slicer_path, "print_3mf", "slicer_path") ?? DEFAULT_SLICER_PATH;
|
|
2268
2701
|
try {
|
|
@@ -2270,7 +2703,7 @@ class ThreeDPrinterMCPServer {
|
|
|
2270
2703
|
threeMFPath = await this.stlManipulator.sliceSTL(threeMFPath, slicerType, printSlicerPath, slicerProfile || undefined, undefined, // progressCallback
|
|
2271
2704
|
printPreset, filamentProfile || undefined,
|
|
2272
2705
|
// Slice for the plate and nozzle the print will be checked against.
|
|
2273
|
-
{ bedType: printBedType, ...(resolveNozzleType(args?.nozzle_type) ? { nozzleType: resolveNozzleType(args?.nozzle_type) } : {}) });
|
|
2706
|
+
{ bedType: printBedType, slicePlate: printPlate + 1, ...(pauseLayers ? { pauseAtLayers: pauseLayers } : {}), ...(resolveNozzleType(args?.nozzle_type) ? { nozzleType: resolveNozzleType(args?.nozzle_type) } : {}) });
|
|
2274
2707
|
console.error("Auto-sliced to: " + threeMFPath);
|
|
2275
2708
|
}
|
|
2276
2709
|
catch (sliceErr) {
|
|
@@ -2297,7 +2730,7 @@ class ThreeDPrinterMCPServer {
|
|
|
2297
2730
|
// --- Parse 3MF ---
|
|
2298
2731
|
const parsed3MFData = await parse3MF(threeMFPath);
|
|
2299
2732
|
console.error(`Successfully parsed 3MF file: ${threeMFPath}`);
|
|
2300
|
-
const amsOptions = this.resolveAmsPrintOptions(parsed3MFData, args);
|
|
2733
|
+
const amsOptions = this.resolveAmsPrintOptions(parsed3MFData, args, printModel);
|
|
2301
2734
|
// --- Prepare Implementation and Print Options ---
|
|
2302
2735
|
const factoryImplementation = this.printerFactory.getImplementation('bambu');
|
|
2303
2736
|
if (!(factoryImplementation instanceof BambuImplementation)) {
|
|
@@ -2323,12 +2756,21 @@ class ThreeDPrinterMCPServer {
|
|
|
2323
2756
|
console.error(`Error processing 3MF or setting up print:`, error);
|
|
2324
2757
|
throw new Error(`Failed during 3MF processing: ${error.message}`);
|
|
2325
2758
|
}
|
|
2759
|
+
if (printConnection.mode === "bambu_network") {
|
|
2760
|
+
result = await this.print3mfViaBambuNetwork({
|
|
2761
|
+
args: args, host, serial: bambuSerial, token: bambuToken, threeMFPath,
|
|
2762
|
+
model: printModel, nozzleDiameters: requestedPrintNozzles, bedType: printBedType,
|
|
2763
|
+
connectionType: printConnection.type, plateIndex: printPlate,
|
|
2764
|
+
ams: { useAMS: printOptions.useAMS, amsMapping: printOptions.amsMapping, amsSlots: printOptions.amsSlots },
|
|
2765
|
+
});
|
|
2766
|
+
break;
|
|
2767
|
+
}
|
|
2326
2768
|
// --- Call Implementation (Now variables are in scope) ---
|
|
2327
2769
|
try {
|
|
2328
2770
|
result = await implementation.print3mf(host, bambuSerial, bambuToken, {
|
|
2329
2771
|
projectName: projectName,
|
|
2330
2772
|
filePath: threeMFPath,
|
|
2331
|
-
plateIndex:
|
|
2773
|
+
plateIndex: printPlate,
|
|
2332
2774
|
...printOptions // Spread the final options
|
|
2333
2775
|
});
|
|
2334
2776
|
// Report what the printer did, not merely that a command was published.
|
|
@@ -2502,11 +2944,11 @@ class ThreeDPrinterMCPServer {
|
|
|
2502
2944
|
}
|
|
2503
2945
|
return await implementation.startJob(host, port, apiKey, gcodeFilename, options);
|
|
2504
2946
|
}
|
|
2505
|
-
async cancelPrint(host, port, type, apiKey, bambuSerial, bambuToken) {
|
|
2947
|
+
async cancelPrint(host, port, type, apiKey, bambuSerial, bambuToken, bambuDispatch) {
|
|
2506
2948
|
const implementation = this.printerFactory.getImplementation(type);
|
|
2507
2949
|
if (type.toLowerCase() === "bambu") {
|
|
2508
2950
|
const bambuApiKey = `${bambuSerial}:${bambuToken}`;
|
|
2509
|
-
return await implementation.cancelJob(host, port, bambuApiKey);
|
|
2951
|
+
return await implementation.cancelJob(host, port, bambuApiKey, bambuDispatch);
|
|
2510
2952
|
}
|
|
2511
2953
|
return await implementation.cancelJob(host, port, apiKey);
|
|
2512
2954
|
}
|
|
@@ -2711,7 +3153,9 @@ class ThreeDPrinterMCPServer {
|
|
|
2711
3153
|
return this.closing;
|
|
2712
3154
|
}
|
|
2713
3155
|
async closeOnce() {
|
|
3156
|
+
this.counterpart.close();
|
|
2714
3157
|
await this.printerFactory.disconnectAll();
|
|
3158
|
+
await this.bambuNetwork.stop();
|
|
2715
3159
|
await this.fusion.close();
|
|
2716
3160
|
if (this.httpRuntime) {
|
|
2717
3161
|
await this.httpRuntime.transport.close();
|