mcp-3d-printer-server 1.2.20 → 1.2.22
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 +27 -0
- package/README.md +112 -9
- package/dist/blender-mcp-bridge.d.ts +21 -0
- package/dist/blender-mcp-bridge.js +3 -3
- package/dist/counterpart/contract.d.ts +395 -0
- package/dist/counterpart/contract.js +220 -0
- package/dist/counterpart/progress.d.ts +32 -0
- package/dist/counterpart/progress.js +443 -0
- package/dist/counterpart/service.d.ts +56 -0
- package/dist/counterpart/service.js +232 -0
- package/dist/fusion-mcp-bridge.d.ts +166 -0
- package/dist/fusion-mcp-bridge.js +529 -0
- package/dist/index.js +272 -8
- package/dist/printers/creality.d.ts +26 -1
- package/dist/printers/creality.js +75 -2
- package/dist/printers/duet.d.ts +16 -0
- package/dist/printers/duet.js +64 -0
- package/dist/printers/generic-base.d.ts +52 -6
- package/dist/printers/generic-base.js +134 -7
- package/dist/printers/klipper.d.ts +33 -2
- package/dist/printers/klipper.js +106 -0
- package/dist/printers/octoprint.d.ts +23 -1
- package/dist/printers/octoprint.js +81 -0
- package/dist/printers/prusa.d.ts +13 -0
- package/dist/printers/prusa.js +59 -0
- package/dist/printers/repetier.d.ts +17 -1
- package/dist/printers/repetier.js +55 -0
- package/dist/slicer/bambu-cli.d.ts +2 -0
- package/dist/slicer/bambu-cli.js +11 -0
- package/dist/slicer/filament-profiles.d.ts +25 -0
- package/dist/slicer/filament-profiles.js +249 -0
- package/dist/stl/stl-manipulator.d.ts +21 -1
- package/dist/stl/stl-manipulator.js +86 -6
- package/package.json +7 -2
- package/server.json +2 -2
- package/src/blender-mcp-bridge.ts +3 -3
- package/src/counterpart/contract.ts +326 -0
- package/src/counterpart/progress.ts +470 -0
- package/src/counterpart/service.ts +253 -0
- package/src/fusion-mcp-bridge.ts +512 -0
- package/src/index.ts +314 -10
- package/src/printers/creality.ts +84 -1
- package/src/printers/duet.ts +67 -0
- package/src/printers/generic-base.ts +172 -1
- package/src/printers/klipper.ts +118 -2
- package/src/printers/octoprint.ts +86 -1
- package/src/printers/prusa.ts +60 -0
- package/src/printers/repetier.ts +63 -1
- package/src/slicer/bambu-cli.ts +13 -0
- package/src/slicer/filament-profiles.ts +249 -0
- package/src/stl/stl-manipulator.ts +100 -6
package/src/index.ts
CHANGED
|
@@ -19,11 +19,28 @@ import fs from "fs";
|
|
|
19
19
|
import path from "path";
|
|
20
20
|
import os from "node:os";
|
|
21
21
|
import { BlenderMcpBridge } from "./blender-mcp-bridge.js";
|
|
22
|
+
import { resolveFilamentProfile } from "./slicer/filament-profiles.js";
|
|
23
|
+
import {
|
|
24
|
+
FUSION_CALL_OUTPUT_SCHEMA,
|
|
25
|
+
FUSION_EXPORT_OUTPUT_SCHEMA,
|
|
26
|
+
FUSION_MCP_PINNED_COMMIT,
|
|
27
|
+
FUSION_NEW_DESIGN_OUTPUT_SCHEMA,
|
|
28
|
+
FUSION_STATUS_OUTPUT_SCHEMA,
|
|
29
|
+
FusionMcpBridge,
|
|
30
|
+
} from "./fusion-mcp-bridge.js";
|
|
22
31
|
import { createServer as createHttpServer, type IncomingMessage, type Server as HttpServer, type ServerResponse } from "node:http";
|
|
23
32
|
import { randomUUID } from "node:crypto";
|
|
24
33
|
import * as THREE from 'three';
|
|
25
34
|
import { PrinterFactory } from "./printers/printer-factory.js";
|
|
26
|
-
import { STLManipulator, type BambuSliceOptions } from "./stl/stl-manipulator.js";
|
|
35
|
+
import { STLManipulator, type BambuSliceOptions, type ProcessOverrides } from "./stl/stl-manipulator.js";
|
|
36
|
+
import { CounterpartService, type PrinterTarget } from "./counterpart/service.js";
|
|
37
|
+
import {
|
|
38
|
+
CAMERA_SNAPSHOT_OUTPUT_SCHEMA,
|
|
39
|
+
CONTROL_OUTPUT_SCHEMA,
|
|
40
|
+
FILAMENTS_OUTPUT_SCHEMA,
|
|
41
|
+
PRINT_PROGRESS_OUTPUT_SCHEMA,
|
|
42
|
+
PRINTER_INFO_OUTPUT_SCHEMA,
|
|
43
|
+
} from "./counterpart/contract.js";
|
|
27
44
|
import { SlicerError } from "./slicer/slicer-error.js";
|
|
28
45
|
import {
|
|
29
46
|
inspectSliceSettings,
|
|
@@ -44,6 +61,9 @@ import { validateExpectedPeaks, type ExpectedPeakTemperatures, type PrintSafetyO
|
|
|
44
61
|
// Load environment variables from .env file
|
|
45
62
|
dotenv.config();
|
|
46
63
|
|
|
64
|
+
const SERVER_NAME = "mcp-3d-printer-server";
|
|
65
|
+
const SERVER_VERSION: string = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
66
|
+
|
|
47
67
|
// Default values
|
|
48
68
|
const DEFAULT_HOST = process.env.PRINTER_HOST || "localhost";
|
|
49
69
|
const DEFAULT_PORT = process.env.PRINTER_PORT || "80";
|
|
@@ -287,8 +307,79 @@ const SLICE_TOOL_PROPERTIES = {
|
|
|
287
307
|
min_save: { type: "boolean", description: "Bambu-compatible slicing: write a smaller output 3MF (--min-save)." },
|
|
288
308
|
skip_modified_gcodes: { type: "boolean", description: "Bambu-compatible slicing: ignore custom G-code embedded in an input 3MF (--skip-modified-gcodes)." },
|
|
289
309
|
slice_plate: { type: "number", description: "Bambu-compatible slicing: plate number to slice; 0 slices all plates (default)." },
|
|
310
|
+
layer_height: {
|
|
311
|
+
type: "number", exclusiveMinimum: 0, maximum: 2,
|
|
312
|
+
description: "Layer height in mm, written over the selected process profile (Orca/Bambu Studio layer_height; PrusaSlicer --layer-height). Not mapped for Cura.",
|
|
313
|
+
},
|
|
314
|
+
wall_loops: {
|
|
315
|
+
type: "integer", minimum: 0, maximum: 100,
|
|
316
|
+
description: "Number of walls, written over the selected process profile (Orca/Bambu Studio wall_loops; PrusaSlicer --perimeters). Not mapped for Cura.",
|
|
317
|
+
},
|
|
318
|
+
sparse_infill_density: {
|
|
319
|
+
type: "string",
|
|
320
|
+
description: "Sparse infill density such as \"15%\" (0–100%), written over the selected process profile (Orca/Bambu Studio sparse_infill_density; PrusaSlicer --fill-density). Not mapped for Cura.",
|
|
321
|
+
},
|
|
290
322
|
};
|
|
291
323
|
|
|
324
|
+
/**
|
|
325
|
+
* upload_gcode / start_print results name the remote file that was stored or
|
|
326
|
+
* started (start_print starts a uniquely named checked copy), so a monitor can
|
|
327
|
+
* match counterpart_print_progress job.name to it.
|
|
328
|
+
*/
|
|
329
|
+
function withJobName(result: unknown, requested: string): unknown {
|
|
330
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) return result;
|
|
331
|
+
const record = result as Record<string, unknown>;
|
|
332
|
+
if (typeof record.jobName === "string" && record.jobName) return result;
|
|
333
|
+
const remote = typeof record.remotePath === "string" && record.remotePath ? record.remotePath : requested;
|
|
334
|
+
return { ...record, jobName: path.posix.basename(remote.replace(/\\/g, "/")) };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** Connection overrides shared by the counterpart/1 tools. */
|
|
338
|
+
const PRINTER_TARGET_PROPERTIES = {
|
|
339
|
+
host: { type: "string", description: "Hostname or IP address of the printer (default: value from env)" },
|
|
340
|
+
port: { type: "string", description: "Port of the printer API (default: value from env)" },
|
|
341
|
+
type: { type: "string", description: "Type of printer management system (octoprint, klipper, duet, repetier, bambu, prusa, creality) (default: value from env)" },
|
|
342
|
+
api_key: { type: "string", description: "API key for authentication (default: value from env)" },
|
|
343
|
+
bambu_serial: { type: "string", description: "Serial number for Bambu Lab printers (default: value from env)" },
|
|
344
|
+
bambu_token: { type: "string", description: "Access token for Bambu Lab printers (default: value from env)" },
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
/** Tools that declare an outputSchema: their errors carry no structuredContent, which clients would validate. */
|
|
348
|
+
const OUTPUT_SCHEMA_TOOLS = new Set([
|
|
349
|
+
"counterpart_printer_info", "counterpart_print_progress", "camera_snapshot", "get_printer_filaments", "pause_print", "resume_print",
|
|
350
|
+
"fusion_mcp_status", "fusion_mcp_call", "fusion_mcp_new_design", "fusion_mcp_export",
|
|
351
|
+
]);
|
|
352
|
+
|
|
353
|
+
/** layer_height / wall_loops / sparse_infill_density (PRD §8.4), validated before slicing. */
|
|
354
|
+
function parseProcessOverrides(args: Record<string, unknown> | undefined): ProcessOverrides {
|
|
355
|
+
const overrides: ProcessOverrides = {};
|
|
356
|
+
if (args?.layer_height !== undefined) {
|
|
357
|
+
const value = args.layer_height;
|
|
358
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0 || value > 2) {
|
|
359
|
+
throw new Error("Invalid layer_height: expected a number of millimetres greater than 0 and at most 2.");
|
|
360
|
+
}
|
|
361
|
+
overrides.layerHeight = value;
|
|
362
|
+
}
|
|
363
|
+
if (args?.wall_loops !== undefined) {
|
|
364
|
+
const value = args.wall_loops;
|
|
365
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > 100) {
|
|
366
|
+
throw new Error("Invalid wall_loops: expected an integer from 0 to 100.");
|
|
367
|
+
}
|
|
368
|
+
overrides.wallLoops = value;
|
|
369
|
+
}
|
|
370
|
+
if (args?.sparse_infill_density !== undefined) {
|
|
371
|
+
const raw = args.sparse_infill_density;
|
|
372
|
+
const text = typeof raw === "number" ? String(raw) : typeof raw === "string" ? raw.trim() : "";
|
|
373
|
+
const match = text.match(/^(\d+(?:\.\d+)?)\s*%?$/);
|
|
374
|
+
const value = match ? Number(match[1]) : NaN;
|
|
375
|
+
if (!Number.isFinite(value) || value < 0 || value > 100) {
|
|
376
|
+
throw new Error("Invalid sparse_infill_density: expected a percentage such as \"15%\" from 0% to 100%.");
|
|
377
|
+
}
|
|
378
|
+
overrides.sparseInfillDensity = `${value}%`;
|
|
379
|
+
}
|
|
380
|
+
return overrides;
|
|
381
|
+
}
|
|
382
|
+
|
|
292
383
|
function validateSliceBambuModel(model: string): string {
|
|
293
384
|
const normalized = model.trim().toLowerCase();
|
|
294
385
|
if (!(SLICE_BAMBU_MODELS as readonly string[]).includes(normalized)) {
|
|
@@ -376,6 +467,21 @@ function buildBambuSliceOptions(args: Record<string, unknown> | undefined): Bamb
|
|
|
376
467
|
return Object.fromEntries(Object.entries(options).filter(([, value]) => value !== undefined)) as BambuSliceOptions;
|
|
377
468
|
}
|
|
378
469
|
|
|
470
|
+
/** The slicer, printer and filament slice_stl uses when a call names none (get_printer_filaments' profile). */
|
|
471
|
+
function defaultFilamentProfileContext() {
|
|
472
|
+
const splitList = (value: string) => value.split(";").map((item) => item.trim()).filter(Boolean);
|
|
473
|
+
const pipeFilaments = DEFAULT_SLICER_PROFILE.includes("|") ? splitList(DEFAULT_SLICER_PROFILE.split("|", 2)[1]) : [];
|
|
474
|
+
const configuredFilaments = DEFAULT_SLICER_TYPE === "cura" ? []
|
|
475
|
+
: DEFAULT_SLICER_TYPE === "prusaslicer" || DEFAULT_SLICER_TYPE === "slic3r" ? splitList(DEFAULT_FILAMENT_PROFILE)
|
|
476
|
+
: [...pipeFilaments, ...splitList(DEFAULT_FILAMENT_PROFILE)];
|
|
477
|
+
return {
|
|
478
|
+
slicerType: DEFAULT_SLICER_TYPE,
|
|
479
|
+
slicerPath: defaultSlicerPathFor(DEFAULT_SLICER_TYPE) || undefined,
|
|
480
|
+
slicerProfile: DEFAULT_SLICER_PROFILE || undefined,
|
|
481
|
+
configuredFilaments,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
379
485
|
function defaultSlicerPathFor(slicerType: SlicerType): string {
|
|
380
486
|
if (process.env.SLICER_PATH) return process.env.SLICER_PATH;
|
|
381
487
|
if (slicerType === DEFAULT_SLICER_TYPE) return DEFAULT_SLICER_PATH;
|
|
@@ -545,7 +651,9 @@ function credentialSuggestion(): string {
|
|
|
545
651
|
class ThreeDPrinterMCPServer {
|
|
546
652
|
private server: Server;
|
|
547
653
|
private readonly blender = new BlenderMcpBridge();
|
|
654
|
+
private readonly fusion = new FusionMcpBridge();
|
|
548
655
|
private printerFactory: PrinterFactory;
|
|
656
|
+
private readonly counterpart: CounterpartService;
|
|
549
657
|
private stlManipulator: STLManipulator;
|
|
550
658
|
private readonly runtimeConfig: RuntimeConfig;
|
|
551
659
|
private httpRuntime?: { transport: StreamableHTTPServerTransport; httpServer: HttpServer };
|
|
@@ -554,8 +662,8 @@ class ThreeDPrinterMCPServer {
|
|
|
554
662
|
this.runtimeConfig = readRuntimeConfig();
|
|
555
663
|
this.server = new Server(
|
|
556
664
|
{
|
|
557
|
-
name:
|
|
558
|
-
version:
|
|
665
|
+
name: SERVER_NAME,
|
|
666
|
+
version: SERVER_VERSION
|
|
559
667
|
},
|
|
560
668
|
{
|
|
561
669
|
capabilities: {
|
|
@@ -566,6 +674,12 @@ class ThreeDPrinterMCPServer {
|
|
|
566
674
|
);
|
|
567
675
|
|
|
568
676
|
this.printerFactory = new PrinterFactory((message) => this.confirmHardware(message));
|
|
677
|
+
this.counterpart = new CounterpartService(
|
|
678
|
+
(printerType) => this.printerFactory.getImplementation(printerType),
|
|
679
|
+
axios.create({ timeout: 10000 }),
|
|
680
|
+
{ name: SERVER_NAME, version: SERVER_VERSION },
|
|
681
|
+
(material) => resolveFilamentProfile(material, defaultFilamentProfileContext())
|
|
682
|
+
);
|
|
569
683
|
this.stlManipulator = new STLManipulator(TEMP_DIR);
|
|
570
684
|
|
|
571
685
|
this.setupHandlers();
|
|
@@ -674,6 +788,7 @@ class ThreeDPrinterMCPServer {
|
|
|
674
788
|
if (!args?.stl_path) {
|
|
675
789
|
throw new Error("Missing required parameter: stl_path");
|
|
676
790
|
}
|
|
791
|
+
const processOverrides = parseProcessOverrides(args);
|
|
677
792
|
const slicerType = normalizeSlicerType(String(args.slicer_type || DEFAULT_SLICER_TYPE));
|
|
678
793
|
const explicitModel =
|
|
679
794
|
typeof args.bambu_model === "string" && args.bambu_model.trim() ? args.bambu_model : undefined;
|
|
@@ -717,7 +832,18 @@ class ThreeDPrinterMCPServer {
|
|
|
717
832
|
|
|
718
833
|
const explicitFilament =
|
|
719
834
|
typeof args.filament_profile === "string" && args.filament_profile.trim() ? args.filament_profile.trim() : undefined;
|
|
720
|
-
|
|
835
|
+
// Generic slicers take load_filaments too (filament_profile is its alias), as the Bambu CLI path does.
|
|
836
|
+
const explicitLoad =
|
|
837
|
+
!bambuCli && typeof args.load_filaments === "string" && args.load_filaments.trim() ? args.load_filaments.trim() : undefined;
|
|
838
|
+
if (explicitLoad && explicitFilament && explicitLoad !== explicitFilament) {
|
|
839
|
+
throw new Error("Provide either load_filaments or filament_profile, not conflicting values.");
|
|
840
|
+
}
|
|
841
|
+
const filamentProfile = options?.loadFilaments ?? explicitLoad ?? explicitFilament ?? (DEFAULT_FILAMENT_PROFILE || undefined);
|
|
842
|
+
// A filament chosen for this call replaces the default riding in "machine;process|filament",
|
|
843
|
+
// or Orca would keep that default as slot 1 and add the chosen one as slot 2.
|
|
844
|
+
if (!bambuCli && (explicitLoad ?? explicitFilament) && slicerProfile?.includes("|")) {
|
|
845
|
+
slicerProfile = slicerProfile.split("|", 1)[0];
|
|
846
|
+
}
|
|
721
847
|
const slicerPath =
|
|
722
848
|
this.resolveExecutableSelectorArg(args.slicer_path, toolName, "slicer_path") ?? defaultSlicerPathFor(slicerType);
|
|
723
849
|
|
|
@@ -729,7 +855,8 @@ class ThreeDPrinterMCPServer {
|
|
|
729
855
|
undefined, // progressCallback
|
|
730
856
|
printerPreset,
|
|
731
857
|
filamentProfile,
|
|
732
|
-
options
|
|
858
|
+
options,
|
|
859
|
+
processOverrides
|
|
733
860
|
);
|
|
734
861
|
}
|
|
735
862
|
|
|
@@ -1088,6 +1215,55 @@ class ThreeDPrinterMCPServer {
|
|
|
1088
1215
|
}
|
|
1089
1216
|
}
|
|
1090
1217
|
},
|
|
1218
|
+
{
|
|
1219
|
+
name: "counterpart_printer_info",
|
|
1220
|
+
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.",
|
|
1221
|
+
inputSchema: { type: "object", properties: { ...PRINTER_TARGET_PROPERTIES } },
|
|
1222
|
+
outputSchema: PRINTER_INFO_OUTPUT_SCHEMA,
|
|
1223
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
1224
|
+
},
|
|
1225
|
+
{
|
|
1226
|
+
name: "counterpart_print_progress",
|
|
1227
|
+
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.",
|
|
1228
|
+
inputSchema: { type: "object", properties: { ...PRINTER_TARGET_PROPERTIES } },
|
|
1229
|
+
outputSchema: PRINT_PROGRESS_OUTPUT_SCHEMA,
|
|
1230
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
1231
|
+
},
|
|
1232
|
+
{
|
|
1233
|
+
name: "camera_snapshot",
|
|
1234
|
+
description: "Capture one JPEG frame from the printer's camera: OctoPrint's configured snapshot URL (Classic Webcam on 1.9+, webcam.snapshotUrl on 1.8, then /webcam/?action=snapshot), Moonraker's first enabled webcam, or Repetier's webcam staticUrl. PrusaLink and Duet have no local snapshot API. PRINTER_CAMERA_SNAPSHOT_URL overrides the source for any printer. Returns the JPEG as base64; pass save_path to also write it to disk.",
|
|
1235
|
+
inputSchema: {
|
|
1236
|
+
type: "object",
|
|
1237
|
+
properties: {
|
|
1238
|
+
...PRINTER_TARGET_PROPERTIES,
|
|
1239
|
+
save_path: { type: "string", description: "Optional absolute path to write the JPEG to disk. If omitted, only the base64 payload is returned." },
|
|
1240
|
+
timeout_ms: { type: "number", description: "Max ms to wait for a frame (default 8000, 100 to 120000)." },
|
|
1241
|
+
},
|
|
1242
|
+
},
|
|
1243
|
+
outputSchema: CAMERA_SNAPSHOT_OUTPUT_SCHEMA,
|
|
1244
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
1245
|
+
},
|
|
1246
|
+
{
|
|
1247
|
+
name: "get_printer_filaments",
|
|
1248
|
+
description: "Report the loaded filament as one external slot (id \"ext\"). Moonraker printers with Spoolman report the active spool's material, colour and name; other printers report source \"unknown\". Read-only.",
|
|
1249
|
+
inputSchema: { type: "object", properties: { ...PRINTER_TARGET_PROPERTIES } },
|
|
1250
|
+
outputSchema: FILAMENTS_OUTPUT_SCHEMA,
|
|
1251
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
1252
|
+
},
|
|
1253
|
+
{
|
|
1254
|
+
name: "pause_print",
|
|
1255
|
+
description: "Pause the current print (OctoPrint, Klipper/Moonraker, PrusaLink, Duet M25, Repetier, and Creality printers that run Moonraker). Pause is never gated.",
|
|
1256
|
+
inputSchema: { type: "object", properties: { ...PRINTER_TARGET_PROPERTIES } },
|
|
1257
|
+
outputSchema: CONTROL_OUTPUT_SCHEMA,
|
|
1258
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
1259
|
+
},
|
|
1260
|
+
{
|
|
1261
|
+
name: "resume_print",
|
|
1262
|
+
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.",
|
|
1263
|
+
inputSchema: { type: "object", properties: { ...PRINTER_TARGET_PROPERTIES } },
|
|
1264
|
+
outputSchema: CONTROL_OUTPUT_SCHEMA,
|
|
1265
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
1266
|
+
},
|
|
1091
1267
|
{
|
|
1092
1268
|
name: "set_printer_temperature",
|
|
1093
1269
|
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 and a human confirmation.",
|
|
@@ -1823,6 +1999,62 @@ class ThreeDPrinterMCPServer {
|
|
|
1823
1999
|
required: ["stl_path", "operations"],
|
|
1824
2000
|
additionalProperties: false
|
|
1825
2001
|
}
|
|
2002
|
+
},
|
|
2003
|
+
{
|
|
2004
|
+
name: "fusion_mcp_status",
|
|
2005
|
+
description: `Check the Autodesk Fusion bridge: whether the Fusion MCP server is configured (faust-machines/fusion360-mcp-server@${FUSION_MCP_PINNED_COMMIT}), whether Fusion is running, whether its Fusion360MCP add-in answers on its port, and the server's tools and schemas. connected is true only when the add-in answered. With launch: true and Fusion not running on this Mac, opens Autodesk Fusion and waits up to 90 s for the add-in.`,
|
|
2006
|
+
inputSchema: {
|
|
2007
|
+
type: "object",
|
|
2008
|
+
properties: {
|
|
2009
|
+
launch: { type: "boolean", description: "Open Autodesk Fusion when it is not running (macOS), then wait up to 90 s for the add-in (default false)." },
|
|
2010
|
+
timeout_ms: { type: "integer", minimum: 100, maximum: 600000, description: "Deadline in milliseconds; defaults to FUSION_MCP_TIMEOUT_MS or 180000. The first start downloads the pinned server with uvx." }
|
|
2011
|
+
},
|
|
2012
|
+
additionalProperties: false
|
|
2013
|
+
},
|
|
2014
|
+
outputSchema: FUSION_STATUS_OUTPUT_SCHEMA,
|
|
2015
|
+
},
|
|
2016
|
+
{
|
|
2017
|
+
name: "fusion_mcp_call",
|
|
2018
|
+
description: "Call a discovered tool on the Fusion MCP server (create_sketch, extrude, fillet, set_parameter, export_stl, execute_code, and others), preserving its full MCP content and errors. Discover tool schemas with fusion_mcp_status first. Calls change the active Fusion design and are never automatically retried. Most tools need an active design: call fusion_mcp_new_design first.",
|
|
2019
|
+
inputSchema: {
|
|
2020
|
+
type: "object",
|
|
2021
|
+
properties: {
|
|
2022
|
+
tool_name: { type: "string", description: "Exact name advertised by the Fusion MCP server." },
|
|
2023
|
+
arguments: { type: "object", description: "Arguments matching the remote tool's discovered input schema. Fusion lengths are in centimetres unless a tool says otherwise." },
|
|
2024
|
+
timeout_ms: { type: "integer", minimum: 100, maximum: 600000, description: "Deadline in milliseconds; defaults to FUSION_MCP_TIMEOUT_MS or 180000." }
|
|
2025
|
+
},
|
|
2026
|
+
required: ["tool_name"],
|
|
2027
|
+
additionalProperties: false
|
|
2028
|
+
},
|
|
2029
|
+
outputSchema: FUSION_CALL_OUTPUT_SCHEMA,
|
|
2030
|
+
},
|
|
2031
|
+
{
|
|
2032
|
+
name: "fusion_mcp_new_design",
|
|
2033
|
+
description: "Open a new parametric design document in Fusion, so the Fusion tools have an active design. Fusion must already have some document open: at the pinned faust-machines commit no tool works from Fusion's home screen (issue #12).",
|
|
2034
|
+
inputSchema: {
|
|
2035
|
+
type: "object",
|
|
2036
|
+
properties: {
|
|
2037
|
+
timeout_ms: { type: "integer", minimum: 100, maximum: 600000, description: "Deadline in milliseconds; defaults to FUSION_MCP_TIMEOUT_MS or 180000." }
|
|
2038
|
+
},
|
|
2039
|
+
additionalProperties: false
|
|
2040
|
+
},
|
|
2041
|
+
outputSchema: FUSION_NEW_DESIGN_OUTPUT_SCHEMA,
|
|
2042
|
+
},
|
|
2043
|
+
{
|
|
2044
|
+
name: "fusion_mcp_export",
|
|
2045
|
+
description: "Export the active Fusion design into output_dir as <name>.stl (export_stl), <name>.f3d (export_f3d) and <name>.parameters.json (the design's user parameters), and return their absolute paths. The STL is checked as a finite triangle mesh and its size compared with the Fusion body to report its units. Existing files are never overwritten. Fusion and this server must share the computer's file system.",
|
|
2046
|
+
inputSchema: {
|
|
2047
|
+
type: "object",
|
|
2048
|
+
properties: {
|
|
2049
|
+
output_dir: { type: "string", description: "Existing absolute directory for the three files." },
|
|
2050
|
+
name: { type: "string", description: "Base file name without a path or extension, such as \"bracket\"." },
|
|
2051
|
+
body_name: { type: "string", description: "Body to export as the STL. Optional when the design has exactly one visible solid body." },
|
|
2052
|
+
timeout_ms: { type: "integer", minimum: 100, maximum: 600000, description: "Deadline in milliseconds; defaults to FUSION_MCP_TIMEOUT_MS or 180000." }
|
|
2053
|
+
},
|
|
2054
|
+
required: ["output_dir", "name"],
|
|
2055
|
+
additionalProperties: false
|
|
2056
|
+
},
|
|
2057
|
+
outputSchema: FUSION_EXPORT_OUTPUT_SCHEMA,
|
|
1826
2058
|
}
|
|
1827
2059
|
]
|
|
1828
2060
|
};
|
|
@@ -1846,7 +2078,40 @@ class ThreeDPrinterMCPServer {
|
|
|
1846
2078
|
try {
|
|
1847
2079
|
let result;
|
|
1848
2080
|
|
|
2081
|
+
const target: PrinterTarget = {
|
|
2082
|
+
type, host, port,
|
|
2083
|
+
apiKey: type.toLowerCase() === "bambu" ? `${bambuSerial}:${bambuToken}` : apiKey,
|
|
2084
|
+
};
|
|
2085
|
+
const structured = (value: object) => ({
|
|
2086
|
+
content: [{ type: "text" as const, text: JSON.stringify(value, null, 2) }],
|
|
2087
|
+
structuredContent: value as Record<string, unknown>,
|
|
2088
|
+
});
|
|
2089
|
+
|
|
1849
2090
|
switch (name) {
|
|
2091
|
+
case "counterpart_printer_info":
|
|
2092
|
+
return structured(await this.counterpart.printerInfo(target));
|
|
2093
|
+
|
|
2094
|
+
case "counterpart_print_progress":
|
|
2095
|
+
return structured(await this.counterpart.printProgress(target));
|
|
2096
|
+
|
|
2097
|
+
case "camera_snapshot": {
|
|
2098
|
+
if (args?.save_path !== undefined && typeof args.save_path !== "string") throw new Error("Invalid save_path: expected a string.");
|
|
2099
|
+
if (args?.timeout_ms !== undefined && typeof args.timeout_ms !== "number") throw new Error("Invalid timeout_ms: expected a number.");
|
|
2100
|
+
return structured(await this.counterpart.cameraSnapshot(target, {
|
|
2101
|
+
savePath: args?.save_path as string | undefined,
|
|
2102
|
+
timeoutMs: args?.timeout_ms as number | undefined,
|
|
2103
|
+
}));
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
case "get_printer_filaments":
|
|
2107
|
+
return structured(await this.counterpart.filaments(target));
|
|
2108
|
+
|
|
2109
|
+
case "pause_print":
|
|
2110
|
+
return structured(await this.counterpart.pause(target));
|
|
2111
|
+
|
|
2112
|
+
case "resume_print":
|
|
2113
|
+
return structured(await this.counterpart.resume(target));
|
|
2114
|
+
|
|
1850
2115
|
case "get_printer_status":
|
|
1851
2116
|
result = await this.getPrinterStatus(host, port, type, apiKey, bambuSerial, bambuToken);
|
|
1852
2117
|
break;
|
|
@@ -1874,13 +2139,13 @@ class ThreeDPrinterMCPServer {
|
|
|
1874
2139
|
}
|
|
1875
2140
|
const uploadFilename = String(args?.filename || path.basename(String(args.gcode_path)));
|
|
1876
2141
|
const uploadGcodeSource = String(args?.gcode_path || args.gcode);
|
|
1877
|
-
result = await this.uploadGcode(
|
|
2142
|
+
result = withJobName(await this.uploadGcode(
|
|
1878
2143
|
host, port, type, apiKey, bambuSerial, bambuToken,
|
|
1879
2144
|
uploadFilename,
|
|
1880
2145
|
uploadGcodeSource,
|
|
1881
2146
|
Boolean(args.print || false),
|
|
1882
2147
|
uploadOptions
|
|
1883
|
-
);
|
|
2148
|
+
), uploadFilename);
|
|
1884
2149
|
break;
|
|
1885
2150
|
|
|
1886
2151
|
case "start_print":
|
|
@@ -1891,7 +2156,10 @@ class ThreeDPrinterMCPServer {
|
|
|
1891
2156
|
if (type.toLowerCase() === "bambu") {
|
|
1892
2157
|
startOptions.bambuModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
|
|
1893
2158
|
}
|
|
1894
|
-
result =
|
|
2159
|
+
result = withJobName(
|
|
2160
|
+
await this.startPrint(host, port, type, apiKey, bambuSerial, bambuToken, String(args.filename), startOptions),
|
|
2161
|
+
String(args.filename)
|
|
2162
|
+
);
|
|
1895
2163
|
break;
|
|
1896
2164
|
|
|
1897
2165
|
case "cancel_print":
|
|
@@ -2534,6 +2802,18 @@ class ThreeDPrinterMCPServer {
|
|
|
2534
2802
|
result = await this.stlManipulator.layFlat(String(args.stl_path));
|
|
2535
2803
|
break;
|
|
2536
2804
|
|
|
2805
|
+
case "fusion_mcp_status":
|
|
2806
|
+
return structured(await this.fusion.status(args ?? {}, extra.signal));
|
|
2807
|
+
|
|
2808
|
+
case "fusion_mcp_call":
|
|
2809
|
+
return await this.fusion.call(args ?? {}, extra.signal);
|
|
2810
|
+
|
|
2811
|
+
case "fusion_mcp_new_design":
|
|
2812
|
+
return structured(await this.fusion.newDesign(args ?? {}, extra.signal));
|
|
2813
|
+
|
|
2814
|
+
case "fusion_mcp_export":
|
|
2815
|
+
return structured(await this.fusion.export(args ?? {}, extra.signal));
|
|
2816
|
+
|
|
2537
2817
|
case "blender_mcp_status":
|
|
2538
2818
|
result = await this.blender.status(args ?? {}, extra.signal);
|
|
2539
2819
|
break;
|
|
@@ -2584,7 +2864,8 @@ class ThreeDPrinterMCPServer {
|
|
|
2584
2864
|
text: `Error: ${errorMessage}\nSuggestion: ${structuredError.suggestion}`
|
|
2585
2865
|
}
|
|
2586
2866
|
],
|
|
2587
|
-
structuredContent
|
|
2867
|
+
// Clients validate structuredContent against a declared outputSchema even on errors.
|
|
2868
|
+
...(OUTPUT_SCHEMA_TOOLS.has(name) ? {} : { structuredContent: structuredError }),
|
|
2588
2869
|
isError: true
|
|
2589
2870
|
};
|
|
2590
2871
|
}
|
|
@@ -2765,6 +3046,15 @@ class ThreeDPrinterMCPServer {
|
|
|
2765
3046
|
tool,
|
|
2766
3047
|
};
|
|
2767
3048
|
}
|
|
3049
|
+
if (tool.startsWith("fusion_mcp_")) {
|
|
3050
|
+
return {
|
|
3051
|
+
status: "error",
|
|
3052
|
+
retryable: false,
|
|
3053
|
+
suggestion: "Check that Autodesk Fusion is open with the Fusion360MCP add-in running and the FUSION_MCP_* settings, then ask again. Fusion requests are never replayed; inspect the design before repeating a change.",
|
|
3054
|
+
message,
|
|
3055
|
+
tool,
|
|
3056
|
+
};
|
|
3057
|
+
}
|
|
2768
3058
|
if (isCredentialRejection(message)) {
|
|
2769
3059
|
return { status: "error", retryable: false, suggestion: credentialSuggestion(), message, tool };
|
|
2770
3060
|
}
|
|
@@ -2828,6 +3118,11 @@ class ThreeDPrinterMCPServer {
|
|
|
2828
3118
|
private async connectStdio(): Promise<void> {
|
|
2829
3119
|
const transport = new StdioServerTransport();
|
|
2830
3120
|
await this.server.connect(transport);
|
|
3121
|
+
// A host that disconnects by closing stdin sends no signal. Shut down then,
|
|
3122
|
+
// so the server and its persistent Fusion MCP child do not outlive it.
|
|
3123
|
+
const disconnected = () => { void this.close().finally(() => process.exit(0)); };
|
|
3124
|
+
process.stdin.once("end", disconnected);
|
|
3125
|
+
process.stdin.once("close", disconnected);
|
|
2831
3126
|
console.error("3D Printer MCP server running on stdio transport");
|
|
2832
3127
|
}
|
|
2833
3128
|
|
|
@@ -2953,8 +3248,17 @@ class ThreeDPrinterMCPServer {
|
|
|
2953
3248
|
return value;
|
|
2954
3249
|
}
|
|
2955
3250
|
|
|
2956
|
-
|
|
3251
|
+
private closing?: Promise<void>;
|
|
3252
|
+
|
|
3253
|
+
/** Idempotent: a signal and stdin EOF can both arrive. */
|
|
3254
|
+
close(): Promise<void> {
|
|
3255
|
+
this.closing ??= this.closeOnce();
|
|
3256
|
+
return this.closing;
|
|
3257
|
+
}
|
|
3258
|
+
|
|
3259
|
+
private async closeOnce(): Promise<void> {
|
|
2957
3260
|
await this.printerFactory.disconnectAll();
|
|
3261
|
+
await this.fusion.close();
|
|
2958
3262
|
|
|
2959
3263
|
if (this.httpRuntime) {
|
|
2960
3264
|
await this.httpRuntime.transport.close();
|
package/src/printers/creality.ts
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { AxiosInstance } from "axios";
|
|
2
|
+
import { GenericPrinterImplementation, type GenericPrinterState, type SnapshotCandidate } from "./generic-base.js";
|
|
3
|
+
import { KlipperImplementation, MACRO_TIMEOUT_MS } from "./klipper.js";
|
|
4
|
+
import type { ConfirmHardware } from "../safety/confirmation.js";
|
|
5
|
+
import type { CounterpartProgress, FilamentSlot, PrinterIdentity } from "../counterpart/contract.js";
|
|
6
|
+
import { mapCrealityDevice } from "../counterpart/progress.js";
|
|
2
7
|
import fs from "fs";
|
|
3
8
|
import FormData from "form-data";
|
|
4
9
|
|
|
10
|
+
/**
|
|
11
|
+
* K1/K2-series printers can run Moonraker: 7125 is Moonraker's own port and
|
|
12
|
+
* 4408 the Fluidd nginx front end that proxies its API. Neither is documented
|
|
13
|
+
* by Creality; stock firmware may run neither, so this is probed, never assumed.
|
|
14
|
+
*/
|
|
15
|
+
export const CREALITY_MOONRAKER_PORTS = ["7125", "4408"];
|
|
16
|
+
const MOONRAKER_PROBE_TTL_MS = 5 * 60_000;
|
|
17
|
+
|
|
5
18
|
const IDLE_STATES = new Set(["idle", "ready", "standby", "operational"]);
|
|
6
19
|
const FINISHED_STATES = new Set(["complete", "completed", "finished", "finish", "cancelled", "canceled", "stopped"]);
|
|
7
20
|
|
|
@@ -13,6 +26,76 @@ const FINISHED_STATES = new Set(["complete", "completed", "finished", "finish",
|
|
|
13
26
|
*/
|
|
14
27
|
export class CrealityImplementation extends GenericPrinterImplementation {
|
|
15
28
|
protected readonly printerLabel = "Creality";
|
|
29
|
+
private readonly moonraker: KlipperImplementation;
|
|
30
|
+
private readonly probes = new Map<string, { port?: string; at: number }>();
|
|
31
|
+
/** Ports probed for Moonraker, in order. */
|
|
32
|
+
moonrakerPorts: string[] = [...CREALITY_MOONRAKER_PORTS];
|
|
33
|
+
|
|
34
|
+
constructor(apiClient: AxiosInstance, confirm?: ConfirmHardware) {
|
|
35
|
+
super(apiClient, confirm);
|
|
36
|
+
this.moonraker = new KlipperImplementation(apiClient, confirm);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The Moonraker port that answers /server/info on this host, if any. */
|
|
40
|
+
async moonrakerPort(host: string): Promise<string | undefined> {
|
|
41
|
+
const cached = this.probes.get(host);
|
|
42
|
+
if (cached && Date.now() - cached.at < MOONRAKER_PROBE_TTL_MS) return cached.port;
|
|
43
|
+
let found: string | undefined;
|
|
44
|
+
for (const port of this.moonrakerPorts) {
|
|
45
|
+
try {
|
|
46
|
+
const info = (await this.apiClient.get(`http://${host}:${port}/server/info`, { timeout: 2_000 })).data;
|
|
47
|
+
if (info?.result && typeof info.result === "object") { found = port; break; }
|
|
48
|
+
} catch {
|
|
49
|
+
// Try the next port.
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
this.probes.set(host, { port: found, at: Date.now() });
|
|
53
|
+
return found;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async counterpartProgress(host: string, port: string, apiKey: string): Promise<CounterpartProgress> {
|
|
57
|
+
const moonraker = await this.moonrakerPort(host);
|
|
58
|
+
if (moonraker) return this.moonraker.counterpartProgress(host, moonraker, "");
|
|
59
|
+
const data = (await this.apiClient.get(`http://${host}:${port}/api/device/status`, { headers: { "Authorization": `Bearer ${apiKey}` } })).data;
|
|
60
|
+
return mapCrealityDevice(data, new Date().toISOString());
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async reportsLayers(host: string): Promise<boolean> {
|
|
64
|
+
return (await this.moonrakerPort(host)) !== undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async supportsPauseResume(host: string): Promise<boolean> {
|
|
68
|
+
return (await this.moonrakerPort(host)) !== undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async snapshotCandidates(host: string): Promise<SnapshotCandidate[]> {
|
|
72
|
+
const moonraker = await this.moonrakerPort(host);
|
|
73
|
+
return moonraker ? this.moonraker.snapshotCandidates(host, moonraker, "") : [];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async counterpartFilaments(host: string, port: string, apiKey: string): Promise<FilamentSlot[]> {
|
|
77
|
+
const moonraker = await this.moonrakerPort(host);
|
|
78
|
+
return moonraker ? this.moonraker.counterpartFilaments(host, moonraker, "") : super.counterpartFilaments(host, port, apiKey);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async counterpartIdentity(host: string): Promise<PrinterIdentity> {
|
|
82
|
+
const moonraker = await this.moonrakerPort(host);
|
|
83
|
+
return moonraker ? this.moonraker.counterpartIdentity(host, moonraker, "") : {};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Pause and resume only through a detected Moonraker; the device API has no verified route. */
|
|
87
|
+
protected async rawPauseJob(host: string) {
|
|
88
|
+
const moonraker = await this.moonrakerPort(host);
|
|
89
|
+
if (!moonraker) throw new Error("Unsupported: this Creality printer has no Moonraker API for pause.");
|
|
90
|
+
return (await this.apiClient.post(`http://${host}:${moonraker}/printer/print/pause`, null as any, { timeout: MACRO_TIMEOUT_MS })).data;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
protected async rawResumeJob(host: string) {
|
|
94
|
+
const moonraker = await this.moonrakerPort(host);
|
|
95
|
+
if (!moonraker) throw new Error("Unsupported: this Creality printer has no Moonraker API for resume.");
|
|
96
|
+
// The Creality adapter already checked the job identity; send the raw resume.
|
|
97
|
+
return (await this.apiClient.post(`http://${host}:${moonraker}/printer/print/resume`, null as any, { timeout: MACRO_TIMEOUT_MS })).data;
|
|
98
|
+
}
|
|
16
99
|
|
|
17
100
|
protected async readPrinterState(host: string, port: string, apiKey: string): Promise<GenericPrinterState> {
|
|
18
101
|
let data: any;
|
package/src/printers/duet.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
|
+
import { isAxiosError } from "axios";
|
|
2
3
|
import { GenericPrinterImplementation, type GenericPrinterState } from "./generic-base.js";
|
|
4
|
+
import type { CounterpartProgress, PrinterIdentity } from "../counterpart/contract.js";
|
|
5
|
+
import { nonEmptyText } from "../counterpart/contract.js";
|
|
6
|
+
import { mapDuet } from "../counterpart/progress.js";
|
|
7
|
+
|
|
8
|
+
function notFound(error: unknown): boolean {
|
|
9
|
+
return isAxiosError(error) && (error.response?.status === 404 || error.response?.status === 405);
|
|
10
|
+
}
|
|
3
11
|
|
|
4
12
|
/**
|
|
5
13
|
* Duet Software Framework (DuetWebServer) REST routes, verified against
|
|
@@ -25,6 +33,65 @@ export class DuetImplementation extends GenericPrinterImplementation {
|
|
|
25
33
|
return response.data;
|
|
26
34
|
}
|
|
27
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Object model keys for counterpart_print_progress and identity. Standalone
|
|
38
|
+
* RepRapFirmware and DSF 3.5+ answer rr_model; older DSF only /machine/status
|
|
39
|
+
* (an alias of /machine/model). 'v' (verbose) is required: 'f' omits job.file.
|
|
40
|
+
* Without a machine password no rr_connect is needed; with one, requests get 401.
|
|
41
|
+
* https://github.com/Duet3D/RepRapFirmware/wiki/HTTP-requests
|
|
42
|
+
*/
|
|
43
|
+
private async objectModel(host: string, port: string, keys: string[]): Promise<Record<string, any>> {
|
|
44
|
+
const base = `http://${host}:${port}`;
|
|
45
|
+
const model: Record<string, any> = {};
|
|
46
|
+
try {
|
|
47
|
+
// Sequential: standalone boards serve few concurrent HTTP connections.
|
|
48
|
+
for (const key of keys) {
|
|
49
|
+
model[key] = (await this.apiClient.get(`${base}/rr_model?key=${encodeURIComponent(key)}&flags=d99v`)).data?.result;
|
|
50
|
+
}
|
|
51
|
+
return model;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (!notFound(error)) throw error;
|
|
54
|
+
}
|
|
55
|
+
const full = (await this.apiClient.get(`${base}/machine/status`)).data;
|
|
56
|
+
const root = full?.result && typeof full.result === "object" ? full.result : full;
|
|
57
|
+
for (const key of keys) model[key] = root?.[key];
|
|
58
|
+
return model;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async counterpartProgress(host: string, port: string, apiKey: string): Promise<CounterpartProgress> {
|
|
62
|
+
const model = await this.objectModel(host, port, ["state", "job"]);
|
|
63
|
+
if (!model.state || typeof model.state !== "object") throw new Error("Duet did not return state from its object model.");
|
|
64
|
+
return mapDuet(model.state, model.job ?? {}, new Date().toISOString());
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async reportsLayers(): Promise<boolean> {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async counterpartIdentity(host: string, port: string): Promise<PrinterIdentity> {
|
|
72
|
+
const model = await this.objectModel(host, port, ["network", "boards"]);
|
|
73
|
+
const board = Array.isArray(model.boards) ? model.boards[0] : undefined;
|
|
74
|
+
return { name: nonEmptyText(model.network?.name), model: nonEmptyText(board?.name) ?? nonEmptyText(board?.shortName) };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** M25 pauses and M24 resumes: rr_gcode on standalone/DSF 3.5+, POST /machine/code on older DSF. */
|
|
78
|
+
protected async rawPauseJob(host: string, port: string) {
|
|
79
|
+
return this.sendGcode(host, port, "M25");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
protected async rawResumeJob(host: string, port: string) {
|
|
83
|
+
return this.sendGcode(host, port, "M24");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private async sendGcode(host: string, port: string, gcode: string) {
|
|
87
|
+
try {
|
|
88
|
+
return (await this.apiClient.get(`http://${host}:${port}/rr_gcode?gcode=${encodeURIComponent(gcode)}`)).data;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (!notFound(error)) throw error;
|
|
91
|
+
}
|
|
92
|
+
return this.code(host, port, gcode);
|
|
93
|
+
}
|
|
94
|
+
|
|
28
95
|
async getStatus(host: string, port: string, apiKey: string) {
|
|
29
96
|
const url = `http://${host}:${port}/machine/status`;
|
|
30
97
|
const response = await this.apiClient.get(url);
|