mcp-3d-printer-server 1.2.25 → 1.2.26
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 +55 -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 +200 -0
- package/dist/bambu-network-bridge.js +881 -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 +512 -62
- 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 +59 -8
- package/dist/safety/printer-state.js +174 -39
- 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/package.json +25 -4
- package/server.json +2 -2
- package/src/ams-mapping.ts +132 -0
- package/src/bambu-network-bridge.ts +1075 -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 +526 -75
- 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 +179 -40
- 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/src/index.ts
CHANGED
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
ReadResourceRequestSchema,
|
|
11
11
|
ListToolsRequestSchema,
|
|
12
12
|
CallToolRequestSchema,
|
|
13
|
+
ListPromptsRequestSchema,
|
|
14
|
+
GetPromptRequestSchema,
|
|
13
15
|
ErrorCode,
|
|
14
16
|
McpError
|
|
15
17
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
@@ -33,7 +35,9 @@ import { randomUUID } from "node:crypto";
|
|
|
33
35
|
import * as THREE from 'three';
|
|
34
36
|
import { PrinterFactory } from "./printers/printer-factory.js";
|
|
35
37
|
import { STLManipulator, type BambuSliceOptions, type ProcessOverrides, type SliceOutput } from "./stl/stl-manipulator.js";
|
|
36
|
-
import {
|
|
38
|
+
import { parsePauseLayers, readPlateGcode, pausedLayers, machinePauseGcode } from "./slicer/layer-pauses.js";
|
|
39
|
+
import { CounterpartService, type BambuControlRoute, type PrinterTarget } from "./counterpart/service.js";
|
|
40
|
+
import {CAMERA_STREAM_TOOLS} from "./counterpart/camera-tools.js";
|
|
37
41
|
import {
|
|
38
42
|
CAMERA_SNAPSHOT_OUTPUT_SCHEMA,
|
|
39
43
|
CONTROL_OUTPUT_SCHEMA,
|
|
@@ -55,8 +59,27 @@ import {
|
|
|
55
59
|
inspectFuluOrcaSetup,
|
|
56
60
|
invokeFuluBridgeRpc,
|
|
57
61
|
} from "./fulu-orca.js";
|
|
62
|
+
import {
|
|
63
|
+
bridgeAmsMapping2Entry,
|
|
64
|
+
bridgeAmsMappingValue,
|
|
65
|
+
externalTrayForNozzle,
|
|
66
|
+
hasAmsMappingInput,
|
|
67
|
+
normalizeAmsMappingObject,
|
|
68
|
+
normalizeBridgeAmsTrayValue,
|
|
69
|
+
studioAmsMappingInfo,
|
|
70
|
+
trayColour,
|
|
71
|
+
} from "./ams-mapping.js";
|
|
72
|
+
import { isDualNozzleModel } from "./safety/limits.js";
|
|
73
|
+
import { BambuNetworkBridge, describeBambuNetworkPrintError, type BridgeJobUpdate } from "./bambu-network-bridge.js";
|
|
74
|
+
import { PRINT_ROUTES, connectionForRoute, routeOf, routeOptions, writeEnvValues, type PrintRoute } from "./counterpart/route.js";
|
|
75
|
+
import { resolveStudioPrintOptions, usedNozzleCount, type CallerPrintOptions } from "./studio-print-options.js";
|
|
76
|
+
import { inspectPrintFile } from "./safety/print-file.js";
|
|
77
|
+
import { validatePrinterState } from "./safety/printer-state.js";
|
|
78
|
+
import { uniquePrintName, withPrinterOperation, withPrintSnapshot } from "./safety/artifact.js";
|
|
79
|
+
import { checkedFile } from "./printers/bambu.js";
|
|
58
80
|
import { CONFIRMATION_UNAVAILABLE, confirmationTimedOut, confirmationTimeoutMs } from "./safety/confirmation.js";
|
|
59
81
|
import { validateExpectedPeaks, type ExpectedPeakTemperatures, type PrintSafetyOptions } from "./safety/expected-peaks.js";
|
|
82
|
+
import { DESIGN_PROMPTS, FDM_CAPABILITY_GUIDANCE, FDM_CAPABILITY_RESOURCE, FDM_CAPABILITY_URI, SERVER_INSTRUCTIONS } from "./design-guidance.js";
|
|
60
83
|
|
|
61
84
|
// Load environment variables from .env file
|
|
62
85
|
dotenv.config();
|
|
@@ -69,7 +92,10 @@ const DEFAULT_HOST = process.env.PRINTER_HOST || "localhost";
|
|
|
69
92
|
const DEFAULT_PORT = process.env.PRINTER_PORT || "80";
|
|
70
93
|
const DEFAULT_API_KEY = process.env.API_KEY || "";
|
|
71
94
|
const DEFAULT_TYPE = process.env.PRINTER_TYPE || "octoprint"; // Default to OctoPrint
|
|
72
|
-
|
|
95
|
+
// Resolve an explicit TEMP_DIR (e.g. ./temp) against the server's working directory, as
|
|
96
|
+
// dotenv's own .env lookup is: slicer CLIs receive paths under it, and Bambu Studio fails
|
|
97
|
+
// every 3MF export to a relative --outputdir (bambu-printer-mcp 1.1.44 fixed the same).
|
|
98
|
+
const TEMP_DIR = process.env.TEMP_DIR ? path.resolve(process.env.TEMP_DIR) : fs.mkdtempSync(path.join(os.tmpdir(), "mcp-3d-printer-server-"));
|
|
73
99
|
|
|
74
100
|
// Slicer configuration
|
|
75
101
|
const CANONICAL_SLICER_TYPES = [
|
|
@@ -161,7 +187,9 @@ function defaultBambuStudioPath(): string {
|
|
|
161
187
|
}
|
|
162
188
|
|
|
163
189
|
const DEFAULT_SLICER_TYPE = normalizeSlicerType(
|
|
164
|
-
|
|
190
|
+
// Bambu Studio for Bambu printers, as in bambu-printer-mcp: FULU OrcaSlicer-bambulab v1.0.0's CLI
|
|
191
|
+
// segfaults on every slice (FULU #56/#57 open), so it is chosen explicitly, not by default.
|
|
192
|
+
process.env.SLICER_TYPE || (DEFAULT_TYPE.toLowerCase() === "bambu" ? "bambustudio" : "prusaslicer")
|
|
165
193
|
);
|
|
166
194
|
const DEFAULT_SLICER_PATH =
|
|
167
195
|
process.env.SLICER_PATH ||
|
|
@@ -182,6 +210,53 @@ const DEFAULT_BAMBU_TOKEN = process.env.BAMBU_TOKEN || "";
|
|
|
182
210
|
const DEFAULT_BAMBU_MODEL = process.env.BAMBU_MODEL?.trim().toLowerCase() || "";
|
|
183
211
|
const DEFAULT_BED_TYPE = process.env.BED_TYPE?.trim().toLowerCase() || "textured_plate";
|
|
184
212
|
const DEFAULT_NOZZLE_DIAMETER = process.env.NOZZLE_DIAMETER?.trim() || "0.4";
|
|
213
|
+
// counterpart_print_route changes these at run time (and in the .env file they came from).
|
|
214
|
+
let DEFAULT_BAMBU_CONNECTION_MODE = process.env.BAMBU_DEFAULT_CONNECTION_MODE?.trim().toLowerCase() || "";
|
|
215
|
+
let DEFAULT_BAMBU_CONNECTION_TYPE = process.env.BAMBU_DEFAULT_CONNECTION_TYPE?.trim().toLowerCase() || "";
|
|
216
|
+
const CONNECTION_MODES = ["lan_mqtt_ftps", "bambu_network", "bambu_native", "bambu_connect"];
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* The one route decision for a Bambu printer, as in bambu-printer-mcp:
|
|
220
|
+
* print_3mf and every print control (cancel, pause, resume) resolve it here,
|
|
221
|
+
* so a job is never controlled over a different connection than the one that
|
|
222
|
+
* started it. Arguments override BAMBU_DEFAULT_CONNECTION_MODE/_TYPE; without
|
|
223
|
+
* either, direct LAN MQTT/FTPS. The type (lan or cloud) applies to FULU's
|
|
224
|
+
* BambuNetwork route and defaults to cloud, as bambu-printer-mcp's does.
|
|
225
|
+
*/
|
|
226
|
+
function resolveConnection(args: Record<string, unknown> | undefined): { mode: string; type: string } {
|
|
227
|
+
const mode = String(args?.connection_mode || DEFAULT_BAMBU_CONNECTION_MODE || "lan_mqtt_ftps").trim().toLowerCase();
|
|
228
|
+
if (!CONNECTION_MODES.includes(mode)) throw new Error("Unsupported connection_mode.");
|
|
229
|
+
const type = String(args?.connection_type || DEFAULT_BAMBU_CONNECTION_TYPE || "cloud").trim().toLowerCase();
|
|
230
|
+
if (!["cloud", "lan"].includes(type)) throw new Error('connection_type must be "cloud" or "lan".');
|
|
231
|
+
return { mode, type };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** bambu-printer-mcp's installed-plug-in (X2D native) and Bambu Connect routes are not part of this server. */
|
|
235
|
+
function assertConnectionAvailable(mode: string): void {
|
|
236
|
+
if (mode === "bambu_native" || mode === "bambu_connect") {
|
|
237
|
+
throw new Error(
|
|
238
|
+
`connection_mode ${mode} is not available in mcp-3D-printer-server. Use lan_mqtt_ftps or bambu_network (FULU), ` +
|
|
239
|
+
`or bambu-printer-mcp for ${mode === "bambu_native" ? "the X2D native route" : "Bambu Connect"}. Nothing was sent.`
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const CONNECTION_PROPERTIES = {
|
|
245
|
+
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." },
|
|
246
|
+
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." },
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
/** The Send dialog options a caller set explicitly; the rest resolve as Bambu Studio does (studio-print-options.ts). */
|
|
250
|
+
function callerPrintOptions(args: Record<string, any> | undefined): CallerPrintOptions {
|
|
251
|
+
const flag = (value: unknown) => value === undefined ? undefined : Boolean(value);
|
|
252
|
+
const options: CallerPrintOptions = {
|
|
253
|
+
bedLeveling: flag(args?.bed_leveling),
|
|
254
|
+
flowCalibration: flag(args?.flow_calibration),
|
|
255
|
+
vibrationCalibration: flag(args?.vibration_calibration),
|
|
256
|
+
layerInspect: flag(args?.layer_inspect),
|
|
257
|
+
};
|
|
258
|
+
return Object.fromEntries(Object.entries(options).filter(([, value]) => value !== undefined)) as CallerPrintOptions;
|
|
259
|
+
}
|
|
185
260
|
|
|
186
261
|
const VALID_BAMBU_MODELS = ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"] as const;
|
|
187
262
|
type BambuModel = typeof VALID_BAMBU_MODELS[number];
|
|
@@ -225,6 +300,7 @@ function resolveNozzleType(value: unknown): string | undefined {
|
|
|
225
300
|
|
|
226
301
|
// Shared slice_stl / slice_with_template argument schema.
|
|
227
302
|
const SLICE_TOOL_PROPERTIES = {
|
|
303
|
+
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." },
|
|
228
304
|
bambu_model: {
|
|
229
305
|
type: "string",
|
|
230
306
|
enum: [...SLICE_BAMBU_MODELS],
|
|
@@ -292,7 +368,7 @@ const SLICE_TOOL_PROPERTIES = {
|
|
|
292
368
|
description: "Template directory override when resolving template_name (default: BAMBU_TEMPLATE_DIR or ~/Sync/bambu/templates).",
|
|
293
369
|
},
|
|
294
370
|
uptodate: { type: "boolean", description: "Bambu-compatible slicing: refresh 3MF preset configs to the installed slicer version (--uptodate)." },
|
|
295
|
-
repetitions: { type: "number", description: "Bambu-compatible slicing: print N identical copies
|
|
371
|
+
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." },
|
|
296
372
|
orient: { type: "boolean", description: "Bambu-compatible slicing: auto-orient for printability (--orient)." },
|
|
297
373
|
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." },
|
|
298
374
|
ensure_on_bed: { type: "boolean", description: "Bambu-compatible slicing: lower floating models onto the bed (--ensure-on-bed)." },
|
|
@@ -347,6 +423,7 @@ const PRINTER_TARGET_PROPERTIES = {
|
|
|
347
423
|
/** Tools that declare an outputSchema: their errors carry no structuredContent, which clients would validate. */
|
|
348
424
|
const OUTPUT_SCHEMA_TOOLS = new Set([
|
|
349
425
|
"counterpart_printer_info", "counterpart_print_progress", "camera_snapshot", "get_printer_filaments", "pause_print", "resume_print",
|
|
426
|
+
"camera_stream_start", "camera_frame", "camera_stream_stop",
|
|
350
427
|
"fusion_mcp_status", "fusion_mcp_call", "fusion_mcp_new_design", "fusion_mcp_export",
|
|
351
428
|
]);
|
|
352
429
|
|
|
@@ -444,6 +521,7 @@ function buildBambuSliceOptions(args: Record<string, unknown> | undefined): Bamb
|
|
|
444
521
|
options.rotateX = optionalFiniteNumber(args, "rotate_x");
|
|
445
522
|
options.rotateY = optionalFiniteNumber(args, "rotate_y");
|
|
446
523
|
options.slicePlate = optionalFiniteNumber(args, "slice_plate");
|
|
524
|
+
options.pauseAtLayers = parsePauseLayers(args?.pause_at_layers);
|
|
447
525
|
if (options.repetitions !== undefined && (!Number.isInteger(options.repetitions) || options.repetitions < 1)) {
|
|
448
526
|
throw new Error("Invalid repetitions: expected a positive integer.");
|
|
449
527
|
}
|
|
@@ -500,6 +578,12 @@ function validateBambuModel(model: string): BambuModel {
|
|
|
500
578
|
return normalized as BambuModel;
|
|
501
579
|
}
|
|
502
580
|
|
|
581
|
+
/** An inventory model hint: the argument, else BAMBU_MODEL; validated when present, never required. */
|
|
582
|
+
function optionalBambuModelHint(value: unknown): string | undefined {
|
|
583
|
+
const requested = String(value ?? DEFAULT_BAMBU_MODEL ?? "").trim().toLowerCase();
|
|
584
|
+
return requested ? validateBambuModel(requested) : undefined;
|
|
585
|
+
}
|
|
586
|
+
|
|
503
587
|
function resolveBedType(bedType: string | undefined): string {
|
|
504
588
|
const resolved = (bedType || DEFAULT_BED_TYPE).trim().toLowerCase();
|
|
505
589
|
if (!(VALID_BED_TYPES as readonly string[]).includes(resolved)) {
|
|
@@ -657,6 +741,8 @@ class ThreeDPrinterMCPServer {
|
|
|
657
741
|
private printerFactory: PrinterFactory;
|
|
658
742
|
private readonly counterpart: CounterpartService;
|
|
659
743
|
private stlManipulator: STLManipulator;
|
|
744
|
+
/** FULU's BambuNetwork bridge host, started on first use by the bambu_network route. */
|
|
745
|
+
private bambuNetwork = new BambuNetworkBridge();
|
|
660
746
|
private readonly runtimeConfig: RuntimeConfig;
|
|
661
747
|
private httpRuntime?: { transport: StreamableHTTPServerTransport; httpServer: HttpServer };
|
|
662
748
|
|
|
@@ -669,9 +755,11 @@ class ThreeDPrinterMCPServer {
|
|
|
669
755
|
},
|
|
670
756
|
{
|
|
671
757
|
capabilities: {
|
|
758
|
+
prompts: {},
|
|
672
759
|
resources: {},
|
|
673
760
|
tools: {}
|
|
674
|
-
}
|
|
761
|
+
},
|
|
762
|
+
instructions: SERVER_INSTRUCTIONS
|
|
675
763
|
}
|
|
676
764
|
);
|
|
677
765
|
|
|
@@ -680,7 +768,8 @@ class ThreeDPrinterMCPServer {
|
|
|
680
768
|
(printerType) => this.printerFactory.getImplementation(printerType),
|
|
681
769
|
axios.create({ timeout: 10000 }),
|
|
682
770
|
{ name: SERVER_NAME, version: SERVER_VERSION },
|
|
683
|
-
(material) => resolveFilamentProfile(material, defaultFilamentProfileContext())
|
|
771
|
+
(material) => resolveFilamentProfile(material, defaultFilamentProfileContext()),
|
|
772
|
+
() => ({ slicerType: DEFAULT_SLICER_TYPE, slicerPath: DEFAULT_SLICER_PATH || undefined })
|
|
684
773
|
);
|
|
685
774
|
this.stlManipulator = new STLManipulator(TEMP_DIR);
|
|
686
775
|
|
|
@@ -688,7 +777,7 @@ class ThreeDPrinterMCPServer {
|
|
|
688
777
|
this.setupErrorHandling();
|
|
689
778
|
}
|
|
690
779
|
|
|
691
|
-
/** Human MCP elicitation
|
|
780
|
+
/** Human MCP elicitation for a physical check software cannot make (resuming a job that cannot be re-read). */
|
|
692
781
|
private async confirmHardware(message: string): Promise<boolean> {
|
|
693
782
|
let response;
|
|
694
783
|
const timeout = confirmationTimeoutMs();
|
|
@@ -705,7 +794,7 @@ class ThreeDPrinterMCPServer {
|
|
|
705
794
|
},
|
|
706
795
|
}, { timeout });
|
|
707
796
|
} catch (error) {
|
|
708
|
-
// A person may take minutes to check the
|
|
797
|
+
// A person may take minutes to check the printer; a timeout is not a missing capability.
|
|
709
798
|
if ((error as { code?: unknown })?.code === ErrorCode.RequestTimeout) throw new Error(confirmationTimedOut(timeout));
|
|
710
799
|
throw new Error(CONFIRMATION_UNAVAILABLE);
|
|
711
800
|
}
|
|
@@ -797,6 +886,9 @@ class ThreeDPrinterMCPServer {
|
|
|
797
886
|
// Generic OrcaSlicer keeps its user-supplied machine profile unless the
|
|
798
887
|
// call explicitly targets a Bambu model.
|
|
799
888
|
const bambuCli = isBambuProjectSlicer(slicerType) || (slicerType === "orcaslicer" && explicitModel !== undefined);
|
|
889
|
+
if (!bambuCli && parsePauseLayers(args.pause_at_layers)) {
|
|
890
|
+
throw new Error(`Pausing at a layer needs the Bambu Studio or OrcaSlicer CLI, not ${slicerType}.`);
|
|
891
|
+
}
|
|
800
892
|
|
|
801
893
|
let printerPreset: string | undefined;
|
|
802
894
|
let options: BambuSliceOptions | undefined;
|
|
@@ -870,59 +962,254 @@ class ThreeDPrinterMCPServer {
|
|
|
870
962
|
|
|
871
963
|
setupHandlers() {
|
|
872
964
|
this.setupResourceHandlers();
|
|
965
|
+
this.setupPromptHandlers();
|
|
873
966
|
this.setupToolHandlers();
|
|
874
967
|
}
|
|
875
968
|
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
);
|
|
884
|
-
|
|
885
|
-
return slots.length > 0 ? slots.sort((a, b) => a - b) : undefined;
|
|
886
|
-
}
|
|
887
|
-
|
|
969
|
+
/**
|
|
970
|
+
* The AMS choice for a Bambu print, as bambu-printer-mcp's print_3mf resolves
|
|
971
|
+
* it: the 3MF's own mapping (ordered by project filament index), overridden by
|
|
972
|
+
* an explicit ams_mapping, which ams_slots (one tray per used filament)
|
|
973
|
+
* replaces. use_ams:false prints from the external spool. Tray values are
|
|
974
|
+
* checked here; the adapter expands ams_slots against the inspected plate.
|
|
975
|
+
*/
|
|
888
976
|
private resolveAmsPrintOptions(
|
|
889
977
|
parsed3MFData: ThreeMFData | undefined,
|
|
890
|
-
args: Record<string, unknown> | undefined
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
978
|
+
args: Record<string, unknown> | undefined,
|
|
979
|
+
model?: string
|
|
980
|
+
): { useAMS: boolean; amsMapping?: number[]; amsSlots?: number[] } {
|
|
981
|
+
const trayOptions = { dualNozzle: isDualNozzleModel(model) };
|
|
982
|
+
let finalAmsMapping: number[] | undefined;
|
|
983
|
+
let finalAmsSlots: number[] | undefined;
|
|
984
|
+
if (parsed3MFData?.slicerConfig?.ams_mapping) {
|
|
985
|
+
const slots = normalizeAmsMappingObject(parsed3MFData.slicerConfig.ams_mapping as Record<string, unknown>, "ams_mapping", trayOptions);
|
|
986
|
+
if (slots.length > 0) finalAmsMapping = slots;
|
|
987
|
+
}
|
|
988
|
+
let useAMS = args?.use_ams !== undefined ? Boolean(args.use_ams) : !!finalAmsMapping && finalAmsMapping.length > 0;
|
|
989
|
+
const hasUserAmsMapping = hasAmsMappingInput(args?.ams_mapping);
|
|
990
|
+
const hasUserAmsSlots = Array.isArray(args?.ams_slots);
|
|
991
|
+
|
|
992
|
+
if (hasUserAmsMapping) {
|
|
993
|
+
let userMapping: number[] | undefined;
|
|
994
|
+
if (Array.isArray(args!.ams_mapping)) {
|
|
995
|
+
userMapping = (args!.ams_mapping as unknown[]).map((value, index) =>
|
|
996
|
+
normalizeBridgeAmsTrayValue(value, `ams_mapping[${index}]`, trayOptions));
|
|
997
|
+
} else if (args!.ams_mapping && typeof args!.ams_mapping === "object") {
|
|
998
|
+
userMapping = normalizeAmsMappingObject(args!.ams_mapping as Record<string, unknown>, "ams_mapping", trayOptions);
|
|
999
|
+
} else {
|
|
1000
|
+
throw new Error("ams_mapping must be an array of trays or an object of trays keyed by project filament index.");
|
|
908
1001
|
}
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
1002
|
+
if (userMapping.length > 0) {
|
|
1003
|
+
finalAmsMapping = userMapping;
|
|
1004
|
+
useAMS = true;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
if (hasUserAmsSlots) {
|
|
1008
|
+
const userSlots = (args!.ams_slots as unknown[]).map((value, index) =>
|
|
1009
|
+
normalizeBridgeAmsTrayValue(value, `ams_slots[${index}]`, trayOptions));
|
|
1010
|
+
if (userSlots.length > 0) {
|
|
1011
|
+
finalAmsSlots = userSlots;
|
|
1012
|
+
finalAmsMapping = undefined;
|
|
912
1013
|
useAMS = true;
|
|
913
1014
|
}
|
|
914
1015
|
}
|
|
915
|
-
|
|
916
1016
|
if (args?.use_ams === false) {
|
|
917
|
-
finalAmsMapping = undefined;
|
|
1017
|
+
if (!hasUserAmsMapping) finalAmsMapping = undefined;
|
|
1018
|
+
if (!hasUserAmsSlots) finalAmsSlots = undefined;
|
|
918
1019
|
useAMS = false;
|
|
919
1020
|
}
|
|
920
|
-
|
|
921
|
-
if (!finalAmsMapping || finalAmsMapping.length === 0) {
|
|
1021
|
+
if ((!finalAmsMapping || finalAmsMapping.length === 0) && (!finalAmsSlots || finalAmsSlots.length === 0)) {
|
|
922
1022
|
useAMS = false;
|
|
923
1023
|
}
|
|
1024
|
+
return { useAMS, amsMapping: finalAmsMapping, amsSlots: finalAmsSlots };
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
/**
|
|
1028
|
+
* print_3mf over FULU's BambuNetwork bridge (connection_mode bambu_network),
|
|
1029
|
+
* ported from bambu-printer-mcp: the route a P1S on authorization firmware
|
|
1030
|
+
* without Developer Mode prints through. connection_type lan opens the
|
|
1031
|
+
* plug-in's authenticated local session (start_local_print); cloud sends
|
|
1032
|
+
* through Bambu Cloud (start_print). The shared gates run first: the exact
|
|
1033
|
+
* snapshot bytes are inspected, the AMS mapping checked, and fresh LAN MQTT
|
|
1034
|
+
* state validated before the bridge starts, again immediately before the
|
|
1035
|
+
* print frame, and the job is recorded for a checked resume.
|
|
1036
|
+
*/
|
|
1037
|
+
private async print3mfViaBambuNetwork(options: {
|
|
1038
|
+
args: Record<string, any>; host: string; serial: string; token: string; threeMFPath: string;
|
|
1039
|
+
model: string; nozzleDiameters?: number[]; bedType: string; connectionType: string;
|
|
1040
|
+
ams: { useAMS: boolean; amsMapping?: number[]; amsSlots?: number[] };
|
|
1041
|
+
/** The zero-based plate print_3mf validated: inspected and dispatched as the same plate. */
|
|
1042
|
+
plateIndex: number;
|
|
1043
|
+
}): Promise<Record<string, unknown>> {
|
|
1044
|
+
const { args, model, bedType, connectionType } = options;
|
|
1045
|
+
const devId = options.serial.trim();
|
|
1046
|
+
const devIp = options.host.trim();
|
|
1047
|
+
const password = options.token.trim();
|
|
1048
|
+
if (!devId || !devIp || devIp === "localhost" || !password) {
|
|
1049
|
+
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.");
|
|
1050
|
+
}
|
|
1051
|
+
const isLocalBridgePrint = connectionType === "lan";
|
|
1052
|
+
const bridgePrintMethod = isLocalBridgePrint ? "start_local_print" : "start_print";
|
|
1053
|
+
const bridgeMethod = `net.${bridgePrintMethod}`;
|
|
1054
|
+
const { plateIndex } = options;
|
|
1055
|
+
const threeMfFilename = path.basename(options.threeMFPath);
|
|
1056
|
+
const projectName = threeMfFilename.replace(/\.3mf$/i, "");
|
|
1057
|
+
const presetName = `${projectName}_plate_${plateIndex + 1}`;
|
|
1058
|
+
const clientJobId = Date.now();
|
|
1059
|
+
const bambu = this.printerFactory.getImplementation("bambu") as unknown as BambuImplementation;
|
|
1060
|
+
const dualNozzle = isDualNozzleModel(model);
|
|
1061
|
+
|
|
1062
|
+
return withPrinterOperation(devIp, devId, (assertActive) => withPrintSnapshot(options.threeMFPath, async (snapshot, retainForDispatchedUpload) => {
|
|
1063
|
+
const inspection = await inspectPrintFile(snapshot, { model, nozzleDiameters: options.nozzleDiameters, plateIndex, bedType });
|
|
1064
|
+
const resumeFile = await checkedFile(inspection, snapshot);
|
|
1065
|
+
// The options Bambu Studio's Send dialog would send; explicit ones win.
|
|
1066
|
+
const studio = resolveStudioPrintOptions(model, callerPrintOptions(args), { usedNozzles: usedNozzleCount(inspection) });
|
|
1067
|
+
const { useAMS, amsSlots } = options.ams;
|
|
1068
|
+
let mapping = options.ams.amsMapping?.slice();
|
|
1069
|
+
if (amsSlots) {
|
|
1070
|
+
if (amsSlots.length !== inspection.usedFilamentPositions.length) throw new Error("ams_slots must supply exactly one physical tray per used filament.");
|
|
1071
|
+
mapping = Array<number>(inspection.materials.length).fill(-1);
|
|
1072
|
+
inspection.usedFilamentPositions.forEach((position, index) => { mapping![position] = amsSlots[index]; });
|
|
1073
|
+
}
|
|
1074
|
+
if (!useAMS) {
|
|
1075
|
+
mapping ??= Array<number>(inspection.materials.length).fill(-1);
|
|
1076
|
+
for (const position of inspection.usedFilamentPositions) {
|
|
1077
|
+
if (mapping[position] === undefined || mapping[position] < 0) {
|
|
1078
|
+
mapping[position] = externalTrayForNozzle(inspection.filamentNozzleIndices?.[position], dualNozzle);
|
|
1079
|
+
}
|
|
1080
|
+
if (mapping[position] !== 254 && !(dualNozzle && mapping[position] === 255)) {
|
|
1081
|
+
throw new Error("use_ams:false requires an external-spool mapping for every used filament.");
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
for (const position of inspection.usedFilamentPositions) {
|
|
1086
|
+
if (mapping?.[position] === undefined || mapping[position] < 0) {
|
|
1087
|
+
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.`);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
const requirements = { ...inspection, amsMapping: mapping, useAMS };
|
|
1091
|
+
validatePrinterState(await bambu.getSafetyStatus(devIp, devId, password), requirements);
|
|
1092
|
+
const confirmedStatus = await bambu.getSafetyStatus(devIp, devId, password);
|
|
1093
|
+
validatePrinterState(confirmedStatus, requirements);
|
|
1094
|
+
const wireMapping = mapping!.map((value) => bridgeAmsMappingValue(value, dualNozzle));
|
|
1095
|
+
const remoteFileName = uniquePrintName(options.threeMFPath);
|
|
1096
|
+
const checkedName = remoteFileName.replace(/(?:\.gcode)?\.3mf$/i, "");
|
|
1097
|
+
const params: Record<string, unknown> = {
|
|
1098
|
+
dev_id: devId,
|
|
1099
|
+
// The printer reports the task or project name back; both carry the unique
|
|
1100
|
+
// checked name, which binds the job to this inspected artifact for a later resume.
|
|
1101
|
+
task_name: checkedName,
|
|
1102
|
+
project_name: checkedName,
|
|
1103
|
+
preset_name: presetName,
|
|
1104
|
+
filename: snapshot,
|
|
1105
|
+
config_filename: snapshot,
|
|
1106
|
+
plate_index: plateIndex + 1,
|
|
1107
|
+
ftp_folder: "",
|
|
1108
|
+
ftp_file: "",
|
|
1109
|
+
ftp_file_md5: "",
|
|
1110
|
+
nozzle_mapping: "",
|
|
1111
|
+
ams_mapping: JSON.stringify(wireMapping),
|
|
1112
|
+
ams_mapping2: JSON.stringify(mapping!.map((value) => bridgeAmsMapping2Entry(value, dualNozzle))),
|
|
1113
|
+
// Bambu Studio and FULU's slicer send this with every job (get_ams_mapping_result).
|
|
1114
|
+
ams_mapping_info: inspection.projectFilaments?.length
|
|
1115
|
+
? JSON.stringify(studioAmsMappingInfo(inspection.projectFilaments, mapping!, (tray) => trayColour(confirmedStatus.raw, tray)))
|
|
1116
|
+
: "",
|
|
1117
|
+
nozzles_info: "",
|
|
1118
|
+
connection_type: connectionType,
|
|
1119
|
+
comments: "",
|
|
1120
|
+
origin_profile_id: 0,
|
|
1121
|
+
stl_design_id: 0,
|
|
1122
|
+
origin_model_id: "",
|
|
1123
|
+
print_type: "from_normal",
|
|
1124
|
+
dst_file: remoteFileName,
|
|
1125
|
+
dev_name: "",
|
|
1126
|
+
dev_ip: devIp,
|
|
1127
|
+
use_ssl_for_ftp: true,
|
|
1128
|
+
use_ssl_for_mqtt: true,
|
|
1129
|
+
username: "bblp",
|
|
1130
|
+
password,
|
|
1131
|
+
task_bed_leveling: studio.task_bed_leveling,
|
|
1132
|
+
task_flow_cali: studio.task_flow_cali,
|
|
1133
|
+
task_vibration_cali: studio.task_vibration_cali,
|
|
1134
|
+
task_layer_inspect: studio.task_layer_inspect,
|
|
1135
|
+
task_record_timelapse: args?.timelapse !== undefined ? Boolean(args.timelapse) : false,
|
|
1136
|
+
task_use_ams: useAMS,
|
|
1137
|
+
task_bed_type: bedType,
|
|
1138
|
+
extra_options: "",
|
|
1139
|
+
auto_bed_leveling: studio.auto_bed_leveling,
|
|
1140
|
+
auto_flow_cali: studio.auto_flow_cali,
|
|
1141
|
+
auto_offset_cali: studio.auto_offset_cali,
|
|
1142
|
+
extruder_cali_manual_mode: studio.extruder_cali_manual_mode,
|
|
1143
|
+
task_ext_change_assist: false,
|
|
1144
|
+
try_emmc_print: false,
|
|
1145
|
+
};
|
|
1146
|
+
assertActive();
|
|
1147
|
+
const bridgeOptions = {
|
|
1148
|
+
// Trusted in-process preflight immediately before the print frame.
|
|
1149
|
+
beforeDispatch: async (method: string) => {
|
|
1150
|
+
if (method !== bridgeMethod) return;
|
|
1151
|
+
assertActive();
|
|
1152
|
+
validatePrinterState(await bambu.getSafetyStatus(devIp, devId, password), requirements);
|
|
1153
|
+
},
|
|
1154
|
+
assertDispatchAllowed: (method: string) => { if (method === bridgeMethod) assertActive(); },
|
|
1155
|
+
onDispatched: (method: string) => { if (method === bridgeMethod) retainForDispatchedUpload(); },
|
|
1156
|
+
};
|
|
1157
|
+
let jobUpdates: BridgeJobUpdate[] = [];
|
|
1158
|
+
let bridgeResult: unknown;
|
|
1159
|
+
if (isLocalBridgePrint) {
|
|
1160
|
+
({ result: bridgeResult, jobUpdates } = await this.bambuNetwork.startLocalPrint(
|
|
1161
|
+
{ devId, devIp, username: "bblp", password, useSsl: true },
|
|
1162
|
+
bridgeMethod, { client_job_id: clientJobId, params }, bridgeOptions, assertActive
|
|
1163
|
+
));
|
|
1164
|
+
} else {
|
|
1165
|
+
({ result: bridgeResult, jobUpdates } = await this.bambuNetwork.startCloudPrint(devId, bridgeMethod, { client_job_id: clientJobId, params }, bridgeOptions, assertActive));
|
|
1166
|
+
}
|
|
1167
|
+
if (typeof bridgeResult === "object" && bridgeResult !== null && (bridgeResult as { ok?: unknown }).ok === false) {
|
|
1168
|
+
throw new Error(`FULU BambuNetwork bridge method ${bridgeMethod} failed: ${String((bridgeResult as { error?: unknown }).error || "unknown bridge error")}`);
|
|
1169
|
+
}
|
|
1170
|
+
const value = typeof bridgeResult === "object" && bridgeResult !== null ? (bridgeResult as { value?: unknown }).value : undefined;
|
|
1171
|
+
if (typeof value === "number" && value !== 0) {
|
|
1172
|
+
const reported = jobUpdates.filter((update) => update.code !== 0 || update.msg).slice(-3)
|
|
1173
|
+
.map((update) => `status ${update.status}, code ${update.code}${update.msg ? `: ${update.msg}` : ""}`).join("; ");
|
|
1174
|
+
throw new Error(`FULU BambuNetwork bridge method ${bridgeMethod} returned non-zero result ${value}${describeBambuNetworkPrintError(value)}.` +
|
|
1175
|
+
(reported ? ` Plug-in reported: ${reported}.` : ""));
|
|
1176
|
+
}
|
|
1177
|
+
assertActive();
|
|
1178
|
+
// Bambu's plug-in placed the file, so a resume re-reads it if it can and otherwise asks a person.
|
|
1179
|
+
bambu.recordCheckedJob(devIp, devId, remoteFileName, requirements, resumeFile, false);
|
|
1180
|
+
return {
|
|
1181
|
+
status: "success",
|
|
1182
|
+
message: `FULU BambuNetwork ${bridgePrintMethod} command for ${threeMfFilename} sent successfully.`,
|
|
1183
|
+
connectionMode: "bambu_network",
|
|
1184
|
+
connectionType,
|
|
1185
|
+
bridgeMethod,
|
|
1186
|
+
bridgeResult,
|
|
1187
|
+
clientJobId,
|
|
1188
|
+
projectName,
|
|
1189
|
+
plateIndex,
|
|
1190
|
+
bridgePlateIndex: plateIndex + 1,
|
|
1191
|
+
printerJobName: checkedName,
|
|
1192
|
+
useAMS,
|
|
1193
|
+
amsMapping: mapping,
|
|
1194
|
+
sha256: inspection.sha256,
|
|
1195
|
+
params: { ...params, password: "[redacted]" },
|
|
1196
|
+
};
|
|
1197
|
+
}));
|
|
1198
|
+
}
|
|
924
1199
|
|
|
925
|
-
|
|
1200
|
+
/** The FDM design guidance as MCP prompts: the client decides when to attach each one. */
|
|
1201
|
+
setupPromptHandlers() {
|
|
1202
|
+
this.server.setRequestHandler(ListPromptsRequestSchema, async () => ({
|
|
1203
|
+
prompts: DESIGN_PROMPTS.map(({ name, title, description }) => ({ name, title, description })),
|
|
1204
|
+
}));
|
|
1205
|
+
this.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
1206
|
+
const prompt = DESIGN_PROMPTS.find(item => item.name === request.params.name);
|
|
1207
|
+
if (!prompt) throw new McpError(ErrorCode.InvalidParams, `Unknown prompt: ${request.params.name}`);
|
|
1208
|
+
return {
|
|
1209
|
+
description: prompt.description,
|
|
1210
|
+
messages: [{ role: "user" as const, content: { type: "text" as const, text: prompt.text } }],
|
|
1211
|
+
};
|
|
1212
|
+
});
|
|
926
1213
|
}
|
|
927
1214
|
|
|
928
1215
|
setupResourceHandlers() {
|
|
@@ -941,7 +1228,8 @@ class ThreeDPrinterMCPServer {
|
|
|
941
1228
|
name: "3D Printer Files",
|
|
942
1229
|
mimeType: "application/json",
|
|
943
1230
|
description: "List of files available on the 3D printer"
|
|
944
|
-
}
|
|
1231
|
+
},
|
|
1232
|
+
FDM_CAPABILITY_RESOURCE
|
|
945
1233
|
],
|
|
946
1234
|
templates: [
|
|
947
1235
|
{
|
|
@@ -966,6 +1254,9 @@ class ThreeDPrinterMCPServer {
|
|
|
966
1254
|
// Read resource
|
|
967
1255
|
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
968
1256
|
const uri = request.params.uri;
|
|
1257
|
+
if (uri === FDM_CAPABILITY_URI) {
|
|
1258
|
+
return { contents: [{ uri, mimeType: FDM_CAPABILITY_RESOURCE.mimeType, text: FDM_CAPABILITY_GUIDANCE }] };
|
|
1259
|
+
}
|
|
969
1260
|
const match = uri.match(/^printer:\/\/([^\/]+)\/(.+)$/);
|
|
970
1261
|
|
|
971
1262
|
if (!match) {
|
|
@@ -1081,7 +1372,7 @@ class ThreeDPrinterMCPServer {
|
|
|
1081
1372
|
},
|
|
1082
1373
|
{
|
|
1083
1374
|
name: "upload_gcode",
|
|
1084
|
-
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)
|
|
1375
|
+
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.",
|
|
1085
1376
|
inputSchema: {
|
|
1086
1377
|
type: "object",
|
|
1087
1378
|
properties: {
|
|
@@ -1139,7 +1430,7 @@ class ThreeDPrinterMCPServer {
|
|
|
1139
1430
|
},
|
|
1140
1431
|
{
|
|
1141
1432
|
name: "start_print",
|
|
1142
|
-
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
|
|
1433
|
+
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.",
|
|
1143
1434
|
inputSchema: {
|
|
1144
1435
|
type: "object",
|
|
1145
1436
|
properties: {
|
|
@@ -1213,10 +1504,23 @@ class ThreeDPrinterMCPServer {
|
|
|
1213
1504
|
bambu_token: {
|
|
1214
1505
|
type: "string",
|
|
1215
1506
|
description: "Access token for Bambu Lab printers (default: value from env)"
|
|
1216
|
-
}
|
|
1507
|
+
},
|
|
1508
|
+
...CONNECTION_PROPERTIES,
|
|
1217
1509
|
}
|
|
1218
1510
|
}
|
|
1219
1511
|
},
|
|
1512
|
+
...CAMERA_STREAM_TOOLS,
|
|
1513
|
+
{
|
|
1514
|
+
name: "counterpart_print_route",
|
|
1515
|
+
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.",
|
|
1516
|
+
inputSchema: {
|
|
1517
|
+
type: "object",
|
|
1518
|
+
properties: {
|
|
1519
|
+
route: { type: "string", enum: ["lan", "cloud", "developer"], description: "Change the route. Omit to read it." }
|
|
1520
|
+
},
|
|
1521
|
+
additionalProperties: false
|
|
1522
|
+
}
|
|
1523
|
+
},
|
|
1220
1524
|
{
|
|
1221
1525
|
name: "counterpart_printer_info",
|
|
1222
1526
|
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.",
|
|
@@ -1247,28 +1551,35 @@ class ThreeDPrinterMCPServer {
|
|
|
1247
1551
|
},
|
|
1248
1552
|
{
|
|
1249
1553
|
name: "get_printer_filaments",
|
|
1250
|
-
description: "Report the loaded filament as one external slot (id \"ext\")
|
|
1251
|
-
inputSchema: {
|
|
1554
|
+
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.",
|
|
1555
|
+
inputSchema: {
|
|
1556
|
+
type: "object",
|
|
1557
|
+
properties: {
|
|
1558
|
+
...PRINTER_TARGET_PROPERTIES,
|
|
1559
|
+
bambu_model: { type: "string", description: "Bambu only: optional model hint used to resolve Bambu/Orca filament profile JSONs for each tray (default: BAMBU_MODEL)." },
|
|
1560
|
+
nozzle_diameter: { type: "string", description: "Bambu only: optional nozzle diameter used when resolving model-specific filament profile JSONs (default: 0.4)." },
|
|
1561
|
+
},
|
|
1562
|
+
},
|
|
1252
1563
|
outputSchema: FILAMENTS_OUTPUT_SCHEMA,
|
|
1253
1564
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
1254
1565
|
},
|
|
1255
1566
|
{
|
|
1256
1567
|
name: "pause_print",
|
|
1257
|
-
description: "Pause the current print (OctoPrint, Klipper/Moonraker, PrusaLink, Duet M25, Repetier, and Creality printers that run Moonraker). Pause is never gated.",
|
|
1258
|
-
inputSchema: { type: "object", properties: { ...PRINTER_TARGET_PROPERTIES } },
|
|
1568
|
+
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
|
+
inputSchema: { type: "object", properties: { ...PRINTER_TARGET_PROPERTIES, ...CONNECTION_PROPERTIES } },
|
|
1259
1570
|
outputSchema: CONTROL_OUTPUT_SCHEMA,
|
|
1260
1571
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
1261
1572
|
},
|
|
1262
1573
|
{
|
|
1263
1574
|
name: "resume_print",
|
|
1264
|
-
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.",
|
|
1265
|
-
inputSchema: { type: "object", properties: { ...PRINTER_TARGET_PROPERTIES } },
|
|
1575
|
+
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
|
+
inputSchema: { type: "object", properties: { ...PRINTER_TARGET_PROPERTIES, ...CONNECTION_PROPERTIES } },
|
|
1266
1577
|
outputSchema: CONTROL_OUTPUT_SCHEMA,
|
|
1267
1578
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
1268
1579
|
},
|
|
1269
1580
|
{
|
|
1270
1581
|
name: "set_printer_temperature",
|
|
1271
|
-
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
|
|
1582
|
+
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.",
|
|
1272
1583
|
inputSchema: {
|
|
1273
1584
|
type: "object",
|
|
1274
1585
|
properties: {
|
|
@@ -1702,16 +2013,35 @@ class ThreeDPrinterMCPServer {
|
|
|
1702
2013
|
required: ["stl_path"]
|
|
1703
2014
|
}
|
|
1704
2015
|
},
|
|
2016
|
+
{
|
|
2017
|
+
name: "inspect_print_file",
|
|
2018
|
+
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
|
+
inputSchema: {
|
|
2021
|
+
type: "object",
|
|
2022
|
+
additionalProperties: false,
|
|
2023
|
+
properties: {
|
|
2024
|
+
file_path: { type: "string", minLength: 1, description: "Local prepared .3mf or textual .gcode artifact to inspect; checksum-only and unsliced projects reject." },
|
|
2025
|
+
bambu_model: { type: "string", enum: [...VALID_BAMBU_MODELS], description: "Explicit file-inspection target model; never inferred from environment or live state." },
|
|
2026
|
+
plate_index: { type: "integer", minimum: 0, description: "Zero-based selected plate; defaults to 0. Raw G-code only supports 0." },
|
|
2027
|
+
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." },
|
|
2028
|
+
bed_type: { type: "string", enum: [...VALID_BED_TYPES], description: "Optional declared bed type to compare with file metadata." },
|
|
2029
|
+
},
|
|
2030
|
+
required: ["file_path", "bambu_model"],
|
|
2031
|
+
},
|
|
2032
|
+
},
|
|
1705
2033
|
{
|
|
1706
2034
|
name: "print_3mf",
|
|
1707
|
-
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
|
|
2035
|
+
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.",
|
|
1708
2036
|
inputSchema: {
|
|
1709
2037
|
type: "object",
|
|
1710
2038
|
properties: {
|
|
2039
|
+
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." },
|
|
1711
2040
|
three_mf_path: {
|
|
1712
2041
|
type: "string",
|
|
1713
2042
|
description: "Path to the 3MF file to print."
|
|
1714
2043
|
},
|
|
2044
|
+
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." },
|
|
1715
2045
|
bambu_model: {
|
|
1716
2046
|
type: "string",
|
|
1717
2047
|
enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
|
|
@@ -1747,10 +2077,18 @@ class ThreeDPrinterMCPServer {
|
|
|
1747
2077
|
nozzle_temperature: { type: "number", description: "Override nozzle temperature (°C)." },
|
|
1748
2078
|
bed_temperature: { type: "number", description: "Override bed temperature (°C)." },
|
|
1749
2079
|
support_enabled: { type: "boolean", description: "Override support generation." },
|
|
2080
|
+
...CONNECTION_PROPERTIES,
|
|
1750
2081
|
ams_mapping: {
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
2082
|
+
anyOf: [
|
|
2083
|
+
{ type: "array", items: { type: "number" } },
|
|
2084
|
+
{ type: "object", additionalProperties: { type: "number" } },
|
|
2085
|
+
],
|
|
2086
|
+
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.",
|
|
2087
|
+
},
|
|
2088
|
+
ams_slots: {
|
|
2089
|
+
type: "array",
|
|
2090
|
+
items: { type: "number" },
|
|
2091
|
+
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.",
|
|
1754
2092
|
},
|
|
1755
2093
|
slicer_type: {
|
|
1756
2094
|
type: "string",
|
|
@@ -1771,7 +2109,7 @@ class ThreeDPrinterMCPServer {
|
|
|
1771
2109
|
},
|
|
1772
2110
|
use_ams: {
|
|
1773
2111
|
type: "boolean",
|
|
1774
|
-
description: "Whether to use
|
|
2112
|
+
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."
|
|
1775
2113
|
},
|
|
1776
2114
|
bed_leveling: {
|
|
1777
2115
|
type: "boolean",
|
|
@@ -2084,12 +2422,39 @@ class ThreeDPrinterMCPServer {
|
|
|
2084
2422
|
type, host, port,
|
|
2085
2423
|
apiKey: type.toLowerCase() === "bambu" ? `${bambuSerial}:${bambuToken}` : apiKey,
|
|
2086
2424
|
};
|
|
2425
|
+
// Bambu print controls go over the same connection as the print (resolveConnection);
|
|
2426
|
+
// undefined keeps direct LAN MQTT in BambuImplementation.
|
|
2427
|
+
const bambuControlRoute: BambuControlRoute = (command) => {
|
|
2428
|
+
if (type.toLowerCase() !== "bambu") return undefined;
|
|
2429
|
+
const route = resolveConnection(args);
|
|
2430
|
+
assertConnectionAvailable(route.mode);
|
|
2431
|
+
if (route.mode !== "bambu_network") return undefined;
|
|
2432
|
+
return (assertActive?: () => void, beforeDispatch?: () => Promise<void>) => {
|
|
2433
|
+
const options = { assertDispatchAllowed: () => { extra.signal.throwIfAborted(); assertActive?.(); } };
|
|
2434
|
+
return route.type === "cloud"
|
|
2435
|
+
? this.bambuNetwork.sendCloudPrintCommand(bambuSerial, command, options, beforeDispatch)
|
|
2436
|
+
: this.bambuNetwork.sendLocalPrintCommand({ devId: bambuSerial, devIp: host, username: "bblp", password: bambuToken, useSsl: true }, command, options, beforeDispatch);
|
|
2437
|
+
};
|
|
2438
|
+
};
|
|
2087
2439
|
const structured = (value: object) => ({
|
|
2088
2440
|
content: [{ type: "text" as const, text: JSON.stringify(value, null, 2) }],
|
|
2089
2441
|
structuredContent: value as Record<string, unknown>,
|
|
2090
2442
|
});
|
|
2091
2443
|
|
|
2092
2444
|
switch (name) {
|
|
2445
|
+
case "camera_stream_start":
|
|
2446
|
+
return structured(await this.counterpart.cameraStreamStart(target, {fps: args?.fps as number | undefined, idleTimeoutMs: args?.idle_timeout_ms as number | undefined}));
|
|
2447
|
+
case "camera_frame": {
|
|
2448
|
+
const frame = await this.counterpart.cameraStreamFrame(target, {
|
|
2449
|
+
sinceSeq: args?.since_seq as number | undefined, waitMs: args?.wait_ms as number | undefined,
|
|
2450
|
+
maxWidth: args?.max_width as number | undefined, maxBytes: args?.max_bytes as number | undefined,
|
|
2451
|
+
changedOnly: args?.changed_only as boolean | undefined, minChange: args?.min_change as number | undefined, signal: extra.signal,
|
|
2452
|
+
});
|
|
2453
|
+
// The JPEG appears only once in the MCP payload; text is useful metadata.
|
|
2454
|
+
return {content: [{type: "text" as const, text: JSON.stringify(Object.fromEntries(Object.entries(frame).filter(([key]) => key !== "base64")))}], structuredContent: frame};
|
|
2455
|
+
}
|
|
2456
|
+
case "camera_stream_stop":
|
|
2457
|
+
return structured(this.counterpart.cameraStreamStop(target));
|
|
2093
2458
|
case "counterpart_printer_info":
|
|
2094
2459
|
return structured(await this.counterpart.printerInfo(target));
|
|
2095
2460
|
|
|
@@ -2105,15 +2470,75 @@ class ThreeDPrinterMCPServer {
|
|
|
2105
2470
|
}));
|
|
2106
2471
|
}
|
|
2107
2472
|
|
|
2108
|
-
case "get_printer_filaments":
|
|
2109
|
-
|
|
2473
|
+
case "get_printer_filaments": {
|
|
2474
|
+
const filamentOptions = type.toLowerCase() === "bambu" ? {
|
|
2475
|
+
bambuModel: optionalBambuModelHint(args?.bambu_model),
|
|
2476
|
+
nozzleDiameter: String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER),
|
|
2477
|
+
} : {};
|
|
2478
|
+
return structured(await this.counterpart.filaments(target, filamentOptions));
|
|
2479
|
+
}
|
|
2110
2480
|
|
|
2111
2481
|
case "pause_print":
|
|
2112
|
-
return structured(await this.counterpart.pause(target));
|
|
2482
|
+
return structured(await this.counterpart.pause(target, bambuControlRoute));
|
|
2113
2483
|
|
|
2114
2484
|
case "resume_print":
|
|
2115
|
-
return structured(await this.counterpart.resume(target));
|
|
2485
|
+
return structured(await this.counterpart.resume(target, bambuControlRoute));
|
|
2486
|
+
|
|
2487
|
+
case "counterpart_print_route": {
|
|
2488
|
+
let savedTo: string | null = null;
|
|
2489
|
+
// FULU's bridge is set up (configured or discovered), so the routes through it can be used.
|
|
2490
|
+
const bridgeConfigured = Boolean(this.bambuNetwork.resolveBridgeCommand());
|
|
2491
|
+
if (args?.route !== undefined) {
|
|
2492
|
+
if (!PRINT_ROUTES.includes(args.route as PrintRoute)) throw new Error('route must be "lan", "cloud" or "developer".');
|
|
2493
|
+
const next = connectionForRoute(args.route as PrintRoute, { mode: DEFAULT_BAMBU_CONNECTION_MODE, type: DEFAULT_BAMBU_CONNECTION_TYPE }, bridgeConfigured);
|
|
2494
|
+
// dotenv loaded .env from the working directory at start; a client's own env block has no file to update.
|
|
2495
|
+
const file = path.resolve(process.cwd(), ".env");
|
|
2496
|
+
if (fs.existsSync(file)) {
|
|
2497
|
+
writeEnvValues(file, {
|
|
2498
|
+
...(next.mode ? { BAMBU_DEFAULT_CONNECTION_MODE: next.mode } : {}),
|
|
2499
|
+
...(next.type ? { BAMBU_DEFAULT_CONNECTION_TYPE: next.type } : {}),
|
|
2500
|
+
});
|
|
2501
|
+
savedTo = file;
|
|
2502
|
+
}
|
|
2503
|
+
DEFAULT_BAMBU_CONNECTION_MODE = next.mode;
|
|
2504
|
+
DEFAULT_BAMBU_CONNECTION_TYPE = next.type;
|
|
2505
|
+
}
|
|
2506
|
+
const connection = resolveConnection({});
|
|
2507
|
+
const options = routeOptions(connection, bridgeConfigured);
|
|
2508
|
+
result = { route: routeOf(connection), mode: connection.mode, type: connection.type,
|
|
2509
|
+
choices: options.filter(option => option.available).map(option => option.id), options, savedTo };
|
|
2510
|
+
break;
|
|
2511
|
+
}
|
|
2116
2512
|
|
|
2513
|
+
case "inspect_print_file": {
|
|
2514
|
+
const allowed = new Set(["file_path", "bambu_model", "plate_index", "nozzle_diameters", "bed_type"]);
|
|
2515
|
+
if (!args || Object.keys(args).some(key => !allowed.has(key))) throw new Error("inspect_print_file accepts only file/model/plate/nozzle/bed arguments.");
|
|
2516
|
+
if (typeof args.file_path !== "string" || !args.file_path.trim() || args.file_path.includes("\0")) throw new Error("file_path must name a local prepared artifact.");
|
|
2517
|
+
if (typeof args.bambu_model !== "string" || !args.bambu_model.trim()) throw new Error("An explicit bambu_model is required for file inspection.");
|
|
2518
|
+
const model = validateBambuModel(args.bambu_model);
|
|
2519
|
+
const plateIndex = args.plate_index === undefined ? 0 : args.plate_index;
|
|
2520
|
+
if (typeof plateIndex !== "number" || !Number.isSafeInteger(plateIndex) || plateIndex < 0) throw new Error("plate_index must be a nonnegative safe integer.");
|
|
2521
|
+
if (args.nozzle_diameters !== undefined && (!Array.isArray(args.nozzle_diameters) || args.nozzle_diameters.some(value => typeof value !== "number"))) throw new Error("nozzle_diameters must be numeric sizes.");
|
|
2522
|
+
const nozzles = args.nozzle_diameters as number[] | undefined;
|
|
2523
|
+
if (nozzles && (nozzles.length < 1 || nozzles.length > 2 || nozzles.some(value => !SUPPORTED_NOZZLE_DIAMETERS.includes(value)))) throw new Error("nozzle_diameters must contain one or two supported sizes.");
|
|
2524
|
+
if (args.bed_type !== undefined && (typeof args.bed_type !== "string" || !args.bed_type.trim())) throw new Error("bed_type must be a supported plate type.");
|
|
2525
|
+
const bedType = args.bed_type === undefined ? undefined : resolveBedType(args.bed_type);
|
|
2526
|
+
if (extra.signal.aborted) throw new Error("File inspection cancelled.");
|
|
2527
|
+
result = await withPrintSnapshot(args.file_path, async snapshot => {
|
|
2528
|
+
const inspection = await inspectPrintFile(snapshot, { model, nozzleDiameters: nozzles, plateIndex, bedType });
|
|
2529
|
+
if (extra.signal.aborted) throw new Error("File inspection cancelled.");
|
|
2530
|
+
const byteLength = (await fs.promises.stat(snapshot)).size;
|
|
2531
|
+
return {
|
|
2532
|
+
receipt_version: "1",
|
|
2533
|
+
evidence: "file-only",
|
|
2534
|
+
dispatch_authorized: false,
|
|
2535
|
+
artifact: { sha256: inspection.sha256, byte_length: byteLength, file_format: path.extname(snapshot).toLowerCase() === ".3mf" ? "3mf" : "gcode" },
|
|
2536
|
+
selection: { bambu_model: model, plate_index: plateIndex, ...(nozzles === undefined ? {} : { nozzle_diameters: nozzles }), ...(bedType === undefined ? {} : { bed_type: bedType }) },
|
|
2537
|
+
inspection,
|
|
2538
|
+
};
|
|
2539
|
+
});
|
|
2540
|
+
break;
|
|
2541
|
+
}
|
|
2117
2542
|
case "get_printer_status":
|
|
2118
2543
|
result = await this.getPrinterStatus(host, port, type, apiKey, bambuSerial, bambuToken);
|
|
2119
2544
|
break;
|
|
@@ -2165,7 +2590,7 @@ class ThreeDPrinterMCPServer {
|
|
|
2165
2590
|
break;
|
|
2166
2591
|
|
|
2167
2592
|
case "cancel_print":
|
|
2168
|
-
result = await this.cancelPrint(host, port, type, apiKey, bambuSerial, bambuToken);
|
|
2593
|
+
result = await this.cancelPrint(host, port, type, apiKey, bambuSerial, bambuToken, bambuControlRoute("stop"));
|
|
2169
2594
|
break;
|
|
2170
2595
|
|
|
2171
2596
|
case "set_printer_temperature":
|
|
@@ -2334,7 +2759,8 @@ class ThreeDPrinterMCPServer {
|
|
|
2334
2759
|
}
|
|
2335
2760
|
const amsOptions = this.resolveAmsPrintOptions(
|
|
2336
2761
|
parsedProcess3MF,
|
|
2337
|
-
args as Record<string, unknown> | undefined
|
|
2762
|
+
args as Record<string, unknown> | undefined,
|
|
2763
|
+
processModel
|
|
2338
2764
|
);
|
|
2339
2765
|
|
|
2340
2766
|
result = await factoryImplementation.print3mf(host, bambuSerial, bambuToken, {
|
|
@@ -2368,7 +2794,7 @@ class ThreeDPrinterMCPServer {
|
|
|
2368
2794
|
// 3. Upload and print through the adapter's hard safety gate. It
|
|
2369
2795
|
// inspects the exact bytes sent (S and R targets, tool changes,
|
|
2370
2796
|
// hardware and material ceilings), requires the expected peaks to
|
|
2371
|
-
// match, reads fresh printer state
|
|
2797
|
+
// match, and reads fresh printer state.
|
|
2372
2798
|
// A mismatch refuses before upload; nothing is only logged.
|
|
2373
2799
|
const filename = path.basename(gcodePath);
|
|
2374
2800
|
const printResult = await this.uploadGcode(
|
|
@@ -2587,17 +3013,21 @@ class ThreeDPrinterMCPServer {
|
|
|
2587
3013
|
throw new Error("Bambu serial number and access token are required for print_3mf.");
|
|
2588
3014
|
}
|
|
2589
3015
|
|
|
3016
|
+
const printPlate = args.plate_index === undefined ? 0 : args.plate_index;
|
|
3017
|
+
if (typeof printPlate !== "number" || !Number.isSafeInteger(printPlate) || printPlate < 0 || printPlate >= Number.MAX_SAFE_INTEGER) throw new Error("plate_index must be a nonnegative safe integer with a supported one-based plate number.");
|
|
2590
3018
|
const printModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
2591
3019
|
const printBedType = resolveBedType(args?.bed_type as string | undefined);
|
|
2592
3020
|
const explicitPrintNozzle = optionalNozzleDiameterArg(args?.nozzle_diameter);
|
|
2593
3021
|
const printNozzle = String(explicitPrintNozzle ?? DEFAULT_NOZZLE_DIAMETER);
|
|
2594
3022
|
const printPreset = BAMBU_MODEL_PRESETS[printModel]?.(printNozzle);
|
|
3023
|
+
const printConnection = resolveConnection(args);
|
|
3024
|
+
assertConnectionAvailable(printConnection.mode);
|
|
2595
3025
|
|
|
2596
3026
|
let threeMFPath = String(args.three_mf_path);
|
|
2597
3027
|
|
|
2598
3028
|
// Only the selected plate's exact G-code entry counts as sliced output;
|
|
2599
3029
|
// a stray .gcode entry elsewhere in the archive is not printable.
|
|
2600
|
-
const selectedPlateEntry =
|
|
3030
|
+
const selectedPlateEntry = `Metadata/plate_${printPlate + 1}.gcode`;
|
|
2601
3031
|
let shouldAutoSlice = false;
|
|
2602
3032
|
try {
|
|
2603
3033
|
const JSZip = (await import('jszip')).default;
|
|
@@ -2613,6 +3043,13 @@ class ThreeDPrinterMCPServer {
|
|
|
2613
3043
|
|
|
2614
3044
|
// Only treat slicer_path as an executable selector when a slicer
|
|
2615
3045
|
// will actually launch. Ready-to-print archives ignore it.
|
|
3046
|
+
const pauseLayers = parsePauseLayers(args?.pause_at_layers);
|
|
3047
|
+
if (!shouldAutoSlice && pauseLayers) {
|
|
3048
|
+
const gcode = await readPlateGcode(threeMFPath, printPlate + 1);
|
|
3049
|
+
const placed = gcode ? pausedLayers(gcode, machinePauseGcode(gcode)) : [];
|
|
3050
|
+
const missing = pauseLayers.filter(layer => !placed.includes(layer));
|
|
3051
|
+
if (missing.length) 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.`);
|
|
3052
|
+
}
|
|
2616
3053
|
if (shouldAutoSlice) {
|
|
2617
3054
|
const printSlicerPath =
|
|
2618
3055
|
this.resolveExecutableSelectorArg(
|
|
@@ -2631,7 +3068,7 @@ class ThreeDPrinterMCPServer {
|
|
|
2631
3068
|
printPreset,
|
|
2632
3069
|
filamentProfile || undefined,
|
|
2633
3070
|
// Slice for the plate and nozzle the print will be checked against.
|
|
2634
|
-
{ bedType: printBedType, ...(resolveNozzleType(args?.nozzle_type) ? { nozzleType: resolveNozzleType(args?.nozzle_type) } : {}) }
|
|
3071
|
+
{ bedType: printBedType, slicePlate: printPlate + 1, ...(pauseLayers ? { pauseAtLayers: pauseLayers } : {}), ...(resolveNozzleType(args?.nozzle_type) ? { nozzleType: resolveNozzleType(args?.nozzle_type) } : {}) }
|
|
2635
3072
|
);
|
|
2636
3073
|
console.error("Auto-sliced to: " + threeMFPath);
|
|
2637
3074
|
} catch (sliceErr: any) {
|
|
@@ -2662,7 +3099,8 @@ class ThreeDPrinterMCPServer {
|
|
|
2662
3099
|
console.error(`Successfully parsed 3MF file: ${threeMFPath}`);
|
|
2663
3100
|
const amsOptions = this.resolveAmsPrintOptions(
|
|
2664
3101
|
parsed3MFData,
|
|
2665
|
-
args as Record<string, unknown> | undefined
|
|
3102
|
+
args as Record<string, unknown> | undefined,
|
|
3103
|
+
printModel
|
|
2666
3104
|
);
|
|
2667
3105
|
|
|
2668
3106
|
// --- Prepare Implementation and Print Options ---
|
|
@@ -2693,12 +3131,22 @@ class ThreeDPrinterMCPServer {
|
|
|
2693
3131
|
throw new Error(`Failed during 3MF processing: ${(error as Error).message}`);
|
|
2694
3132
|
}
|
|
2695
3133
|
|
|
3134
|
+
if (printConnection.mode === "bambu_network") {
|
|
3135
|
+
result = await this.print3mfViaBambuNetwork({
|
|
3136
|
+
args: args as Record<string, any>, host, serial: bambuSerial, token: bambuToken, threeMFPath,
|
|
3137
|
+
model: printModel, nozzleDiameters: requestedPrintNozzles, bedType: printBedType,
|
|
3138
|
+
connectionType: printConnection.type, plateIndex: printPlate,
|
|
3139
|
+
ams: { useAMS: printOptions.useAMS, amsMapping: printOptions.amsMapping, amsSlots: printOptions.amsSlots },
|
|
3140
|
+
});
|
|
3141
|
+
break;
|
|
3142
|
+
}
|
|
3143
|
+
|
|
2696
3144
|
// --- Call Implementation (Now variables are in scope) ---
|
|
2697
3145
|
try {
|
|
2698
3146
|
result = await implementation.print3mf(host, bambuSerial, bambuToken, {
|
|
2699
3147
|
projectName: projectName,
|
|
2700
3148
|
filePath: threeMFPath,
|
|
2701
|
-
plateIndex:
|
|
3149
|
+
plateIndex: printPlate,
|
|
2702
3150
|
...printOptions // Spread the final options
|
|
2703
3151
|
});
|
|
2704
3152
|
// Report what the printer did, not merely that a command was published.
|
|
@@ -2999,13 +3447,14 @@ class ThreeDPrinterMCPServer {
|
|
|
2999
3447
|
type: string,
|
|
3000
3448
|
apiKey: string,
|
|
3001
3449
|
bambuSerial: string,
|
|
3002
|
-
bambuToken: string
|
|
3450
|
+
bambuToken: string,
|
|
3451
|
+
bambuDispatch?: () => Promise<any>
|
|
3003
3452
|
) {
|
|
3004
3453
|
const implementation = this.printerFactory.getImplementation(type);
|
|
3005
3454
|
|
|
3006
3455
|
if (type.toLowerCase() === "bambu") {
|
|
3007
3456
|
const bambuApiKey = `${bambuSerial}:${bambuToken}`;
|
|
3008
|
-
return await implementation.cancelJob(host, port, bambuApiKey);
|
|
3457
|
+
return await (implementation as unknown as BambuImplementation).cancelJob(host, port, bambuApiKey, bambuDispatch);
|
|
3009
3458
|
}
|
|
3010
3459
|
|
|
3011
3460
|
return await implementation.cancelJob(host, port, apiKey);
|
|
@@ -3265,7 +3714,9 @@ class ThreeDPrinterMCPServer {
|
|
|
3265
3714
|
}
|
|
3266
3715
|
|
|
3267
3716
|
private async closeOnce(): Promise<void> {
|
|
3717
|
+
this.counterpart.close();
|
|
3268
3718
|
await this.printerFactory.disconnectAll();
|
|
3719
|
+
await this.bambuNetwork.stop();
|
|
3269
3720
|
await this.fusion.close();
|
|
3270
3721
|
|
|
3271
3722
|
if (this.httpRuntime) {
|