mcp-3d-printer-server 1.2.1 → 1.2.4

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/src/index.ts CHANGED
@@ -22,6 +22,11 @@ import { PrinterFactory } from "./printers/printer-factory.js";
22
22
  import { STLManipulator } from "./stl/stl-manipulator.js";
23
23
  import { parse3MF, ThreeMFData } from './3mf_parser.js';
24
24
  import { BambuImplementation } from "./printers/bambu.js";
25
+ import {
26
+ inspectFuluOrcaSetup,
27
+ invokeFuluBridgeRpc,
28
+ isFuluPrintRpcMethod,
29
+ } from "./fulu-orca.js";
25
30
 
26
31
  // Load environment variables from .env file
27
32
  dotenv.config();
@@ -34,9 +39,107 @@ const DEFAULT_TYPE = process.env.PRINTER_TYPE || "octoprint"; // Default to Octo
34
39
  const TEMP_DIR = process.env.TEMP_DIR || path.join(process.cwd(), "temp");
35
40
 
36
41
  // Slicer configuration
37
- const DEFAULT_SLICER_TYPE = process.env.SLICER_TYPE || (process.env.PRINTER_TYPE === "bambu" ? "bambustudio" : "prusaslicer");
38
- const DEFAULT_SLICER_PATH = process.env.SLICER_PATH || (process.env.PRINTER_TYPE === "bambu" ? "/Applications/BambuStudio.app/Contents/MacOS/BambuStudio" : "");
42
+ const CANONICAL_SLICER_TYPES = [
43
+ "prusaslicer",
44
+ "cura",
45
+ "slic3r",
46
+ "orcaslicer",
47
+ "orcaslicer-bambulab",
48
+ "bambustudio",
49
+ ] as const;
50
+ type SlicerType = typeof CANONICAL_SLICER_TYPES[number];
51
+
52
+ const SLICER_TYPE_ALIASES: Record<string, SlicerType> = {
53
+ prusa: "prusaslicer",
54
+ prusaslicer: "prusaslicer",
55
+ cura: "cura",
56
+ curaengine: "cura",
57
+ slic3r: "slic3r",
58
+ orca: "orcaslicer",
59
+ orcaslicer: "orcaslicer",
60
+ "orca-slicer": "orcaslicer",
61
+ "orcaslicer-bambulab": "orcaslicer-bambulab",
62
+ "orcaslicer_bambulab": "orcaslicer-bambulab",
63
+ "orca-bambulab": "orcaslicer-bambulab",
64
+ "orca_bambulab": "orcaslicer-bambulab",
65
+ "fulu-orca": "orcaslicer-bambulab",
66
+ "fulu_orca": "orcaslicer-bambulab",
67
+ "orca-studio": "orcaslicer-bambulab",
68
+ "orca_studio": "orcaslicer-bambulab",
69
+ orcastudio: "orcaslicer-bambulab",
70
+ bambustudio: "bambustudio",
71
+ "bambu-studio": "bambustudio",
72
+ "bambu_studio": "bambustudio",
73
+ };
74
+
75
+ const SLICER_TYPE_ENUM = [
76
+ ...CANONICAL_SLICER_TYPES,
77
+ "fulu_orca",
78
+ "fulu-orca",
79
+ "orca-studio",
80
+ "orca_bambulab",
81
+ ] as const;
82
+
83
+ function normalizeSlicerType(rawValue: string | undefined): SlicerType {
84
+ const normalized = (rawValue || "").trim().toLowerCase();
85
+ const resolved = SLICER_TYPE_ALIASES[normalized];
86
+ if (!resolved) {
87
+ throw new Error(
88
+ `Unsupported slicer type "${rawValue}". Valid types: ${SLICER_TYPE_ENUM.join(", ")}`
89
+ );
90
+ }
91
+ return resolved;
92
+ }
93
+
94
+ function isBambuProjectSlicer(slicerType: SlicerType): boolean {
95
+ return slicerType === "orcaslicer-bambulab" || slicerType === "bambustudio";
96
+ }
97
+
98
+ function firstExistingPath(candidates: string[]): string | undefined {
99
+ return candidates.find((candidate) => fs.existsSync(candidate));
100
+ }
101
+
102
+ function defaultFuluOrcaPath(): string {
103
+ return (
104
+ process.env.FULU_ORCA_PATH ||
105
+ process.env.ORCASLICER_BAMBULAB_PATH ||
106
+ process.env.ORCA_SLICER_BAMBULAB_PATH ||
107
+ firstExistingPath([
108
+ "/Applications/OrcaSlicer.app/Contents/MacOS/OrcaSlicer",
109
+ "/Applications/Orca Studio.app/Contents/MacOS/OrcaSlicer",
110
+ "/Applications/OrcaStudio.app/Contents/MacOS/OrcaStudio",
111
+ "/usr/local/bin/orcaslicer",
112
+ "/usr/bin/orcaslicer",
113
+ ]) ||
114
+ "orcaslicer"
115
+ );
116
+ }
117
+
118
+ function defaultBambuStudioPath(): string {
119
+ return (
120
+ firstExistingPath([
121
+ "/Applications/BambuStudio.app/Contents/MacOS/BambuStudio",
122
+ "/Applications/Bambu Studio.app/Contents/MacOS/BambuStudio",
123
+ "/usr/local/bin/bambustudio",
124
+ "/usr/bin/bambustudio",
125
+ ]) ||
126
+ "bambustudio"
127
+ );
128
+ }
129
+
130
+ const DEFAULT_SLICER_TYPE = normalizeSlicerType(
131
+ process.env.SLICER_TYPE || (DEFAULT_TYPE.toLowerCase() === "bambu" ? "orcaslicer-bambulab" : "prusaslicer")
132
+ );
133
+ const DEFAULT_SLICER_PATH =
134
+ process.env.SLICER_PATH ||
135
+ (DEFAULT_SLICER_TYPE === "orcaslicer-bambulab"
136
+ ? defaultFuluOrcaPath()
137
+ : DEFAULT_SLICER_TYPE === "bambustudio"
138
+ ? defaultBambuStudioPath()
139
+ : "");
39
140
  const DEFAULT_SLICER_PROFILE = process.env.SLICER_PROFILE || "";
141
+ const DEFAULT_FILAMENT_PROFILE =
142
+ process.env.FILAMENT_PROFILE || process.env.SLICER_FILAMENT_PROFILE || "";
40
143
 
41
144
  // Bambu-specific default values
42
145
  const DEFAULT_BAMBU_SERIAL = process.env.BAMBU_SERIAL || "";
@@ -270,6 +373,58 @@ class ThreeDPrinterMCPServer {
270
373
  this.setupToolHandlers();
271
374
  }
272
375
 
376
+ private extractParsedAmsMapping(parsed3MFData: ThreeMFData | undefined): number[] | undefined {
377
+ if (!parsed3MFData?.slicerConfig?.ams_mapping) {
378
+ return undefined;
379
+ }
380
+
381
+ const slots = Object.values(parsed3MFData.slicerConfig.ams_mapping).filter(
382
+ (value): value is number => typeof value === "number"
383
+ );
384
+
385
+ return slots.length > 0 ? slots.sort((a, b) => a - b) : undefined;
386
+ }
387
+
388
+ private resolveAmsPrintOptions(
389
+ parsed3MFData: ThreeMFData | undefined,
390
+ args: Record<string, unknown> | undefined
391
+ ): { useAMS: boolean; amsMapping?: number[] } {
392
+ let finalAmsMapping = this.extractParsedAmsMapping(parsed3MFData);
393
+ let useAMS =
394
+ args?.use_ams !== undefined
395
+ ? Boolean(args.use_ams)
396
+ : !!finalAmsMapping && finalAmsMapping.length > 0;
397
+
398
+ const override = args?.ams_mapping;
399
+ if (override !== undefined) {
400
+ let userMappingOverride: number[] | undefined;
401
+
402
+ if (Array.isArray(override)) {
403
+ userMappingOverride = override.filter((value): value is number => typeof value === "number");
404
+ } else if (typeof override === "object" && override !== null) {
405
+ userMappingOverride = Object.values(override as Record<string, unknown>)
406
+ .filter((value): value is number => typeof value === "number")
407
+ .sort((a, b) => a - b);
408
+ }
409
+
410
+ if (userMappingOverride && userMappingOverride.length > 0) {
411
+ finalAmsMapping = userMappingOverride;
412
+ useAMS = true;
413
+ }
414
+ }
415
+
416
+ if (args?.use_ams === false) {
417
+ finalAmsMapping = undefined;
418
+ useAMS = false;
419
+ }
420
+
421
+ if (!finalAmsMapping || finalAmsMapping.length === 0) {
422
+ useAMS = false;
423
+ }
424
+
425
+ return { useAMS, amsMapping: finalAmsMapping };
426
+ }
427
+
273
428
  setupResourceHandlers() {
274
429
  // List available resources
275
430
  this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
@@ -391,6 +546,205 @@ class ThreeDPrinterMCPServer {
391
546
  }
392
547
  }
393
548
  },
549
+ {
550
+ name: "list_printer_files",
551
+ description: "List files available on the 3D printer",
552
+ inputSchema: {
553
+ type: "object",
554
+ properties: {
555
+ host: {
556
+ type: "string",
557
+ description: "Hostname or IP address of the printer (default: value from env)"
558
+ },
559
+ port: {
560
+ type: "string",
561
+ description: "Port of the printer API (default: value from env)"
562
+ },
563
+ type: {
564
+ type: "string",
565
+ description: "Type of printer management system (octoprint, klipper, duet, repetier, bambu, prusa, creality) (default: value from env)"
566
+ },
567
+ api_key: {
568
+ type: "string",
569
+ description: "API key for authentication (default: value from env)"
570
+ },
571
+ bambu_serial: {
572
+ type: "string",
573
+ description: "Serial number for Bambu Lab printers (default: value from env)"
574
+ },
575
+ bambu_token: {
576
+ type: "string",
577
+ description: "Access token for Bambu Lab printers (default: value from env)"
578
+ }
579
+ }
580
+ }
581
+ },
582
+ {
583
+ name: "upload_gcode",
584
+ description: "Upload G-code content or a local G-code file path to the printer",
585
+ inputSchema: {
586
+ type: "object",
587
+ properties: {
588
+ host: {
589
+ type: "string",
590
+ description: "Hostname or IP address of the printer (default: value from env)"
591
+ },
592
+ port: {
593
+ type: "string",
594
+ description: "Port of the printer API (default: value from env)"
595
+ },
596
+ type: {
597
+ type: "string",
598
+ description: "Type of printer management system (octoprint, klipper, duet, repetier, bambu, prusa, creality) (default: value from env)"
599
+ },
600
+ api_key: {
601
+ type: "string",
602
+ description: "API key for authentication (default: value from env)"
603
+ },
604
+ bambu_serial: {
605
+ type: "string",
606
+ description: "Serial number for Bambu Lab printers (default: value from env)"
607
+ },
608
+ bambu_token: {
609
+ type: "string",
610
+ description: "Access token for Bambu Lab printers (default: value from env)"
611
+ },
612
+ filename: {
613
+ type: "string",
614
+ description: "Filename to use on the printer. Defaults to the basename of gcode_path when omitted."
615
+ },
616
+ gcode: {
617
+ type: "string",
618
+ description: "G-code content, or a local path to a G-code file."
619
+ },
620
+ gcode_path: {
621
+ type: "string",
622
+ description: "Local path to a G-code file to upload."
623
+ },
624
+ print: {
625
+ type: "boolean",
626
+ description: "Start printing after upload when the printer backend supports it."
627
+ }
628
+ },
629
+ anyOf: [
630
+ { required: ["filename", "gcode"] },
631
+ { required: ["gcode_path"] }
632
+ ]
633
+ }
634
+ },
635
+ {
636
+ name: "start_print",
637
+ description: "Start printing a file that is already available on the printer",
638
+ inputSchema: {
639
+ type: "object",
640
+ properties: {
641
+ host: {
642
+ type: "string",
643
+ description: "Hostname or IP address of the printer (default: value from env)"
644
+ },
645
+ port: {
646
+ type: "string",
647
+ description: "Port of the printer API (default: value from env)"
648
+ },
649
+ type: {
650
+ type: "string",
651
+ description: "Type of printer management system (octoprint, klipper, duet, repetier, bambu, prusa, creality) (default: value from env)"
652
+ },
653
+ api_key: {
654
+ type: "string",
655
+ description: "API key for authentication (default: value from env)"
656
+ },
657
+ bambu_serial: {
658
+ type: "string",
659
+ description: "Serial number for Bambu Lab printers (default: value from env)"
660
+ },
661
+ bambu_token: {
662
+ type: "string",
663
+ description: "Access token for Bambu Lab printers (default: value from env)"
664
+ },
665
+ filename: {
666
+ type: "string",
667
+ description: "Name/path of the printer-side G-code file to start."
668
+ }
669
+ },
670
+ required: ["filename"]
671
+ }
672
+ },
673
+ {
674
+ name: "cancel_print",
675
+ description: "Cancel the current print job",
676
+ inputSchema: {
677
+ type: "object",
678
+ properties: {
679
+ host: {
680
+ type: "string",
681
+ description: "Hostname or IP address of the printer (default: value from env)"
682
+ },
683
+ port: {
684
+ type: "string",
685
+ description: "Port of the printer API (default: value from env)"
686
+ },
687
+ type: {
688
+ type: "string",
689
+ description: "Type of printer management system (octoprint, klipper, duet, repetier, bambu, prusa, creality) (default: value from env)"
690
+ },
691
+ api_key: {
692
+ type: "string",
693
+ description: "API key for authentication (default: value from env)"
694
+ },
695
+ bambu_serial: {
696
+ type: "string",
697
+ description: "Serial number for Bambu Lab printers (default: value from env)"
698
+ },
699
+ bambu_token: {
700
+ type: "string",
701
+ description: "Access token for Bambu Lab printers (default: value from env)"
702
+ }
703
+ }
704
+ }
705
+ },
706
+ {
707
+ name: "set_printer_temperature",
708
+ description: "Set the temperature of a printer component",
709
+ inputSchema: {
710
+ type: "object",
711
+ properties: {
712
+ host: {
713
+ type: "string",
714
+ description: "Hostname or IP address of the printer (default: value from env)"
715
+ },
716
+ port: {
717
+ type: "string",
718
+ description: "Port of the printer API (default: value from env)"
719
+ },
720
+ type: {
721
+ type: "string",
722
+ description: "Type of printer management system (octoprint, klipper, duet, repetier, bambu, prusa, creality) (default: value from env)"
723
+ },
724
+ api_key: {
725
+ type: "string",
726
+ description: "API key for authentication (default: value from env)"
727
+ },
728
+ bambu_serial: {
729
+ type: "string",
730
+ description: "Serial number for Bambu Lab printers (default: value from env)"
731
+ },
732
+ bambu_token: {
733
+ type: "string",
734
+ description: "Access token for Bambu Lab printers (default: value from env)"
735
+ },
736
+ component: {
737
+ type: "string",
738
+ description: "Printer component to heat, such as extruder or bed."
739
+ },
740
+ temperature: {
741
+ type: "number",
742
+ description: "Target temperature in Celsius."
743
+ }
744
+ },
745
+ required: ["component", "temperature"]
746
+ }
747
+ },
394
748
  // New STL manipulation tools
395
749
  {
396
750
  name: "extend_stl_base",
@@ -423,7 +777,7 @@ class ThreeDPrinterMCPServer {
423
777
  bambu_model: {
424
778
  type: "string",
425
779
  enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
426
- description: "Bambu Lab printer model — required when using bambustudio slicer to ensure correct G-code."
780
+ description: "Bambu Lab printer model - required when using Bambu project slicers to ensure correct G-code."
427
781
  },
428
782
  nozzle_diameter: {
429
783
  type: "string",
@@ -431,7 +785,8 @@ class ThreeDPrinterMCPServer {
431
785
  },
432
786
  slicer_type: {
433
787
  type: "string",
434
- description: "Type of slicer to use (prusaslicer, cura, slic3r, orcaslicer, bambustudio) (default: value from env)"
788
+ enum: [...SLICER_TYPE_ENUM],
789
+ description: "Type of slicer to use (prusaslicer, cura, slic3r, orcaslicer, orcaslicer-bambulab, bambustudio). Use orcaslicer-bambulab for the FULU fork."
435
790
  },
436
791
  slicer_path: {
437
792
  type: "string",
@@ -439,7 +794,11 @@ class ThreeDPrinterMCPServer {
439
794
  },
440
795
  slicer_profile: {
441
796
  type: "string",
442
- description: "Profile to use for slicing (default: value from env)"
797
+ description: "Profile to use for slicing (default: value from env). OrcaSlicer also accepts machine/process profiles separated with ';', optionally followed by '|filament.json'."
798
+ },
799
+ filament_profile: {
800
+ type: "string",
801
+ description: "OrcaSlicer filament profile path loaded with --load-filaments (default: FILAMENT_PROFILE/SLICER_FILAMENT_PROFILE env)."
443
802
  }
444
803
  },
445
804
  required: ["stl_path"]
@@ -504,6 +863,37 @@ class ThreeDPrinterMCPServer {
504
863
  api_key: {
505
864
  type: "string",
506
865
  description: "API key for authentication (default: value from env)"
866
+ },
867
+ bambu_model: {
868
+ type: "string",
869
+ enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
870
+ description: "Bambu Lab printer model. Required for Bambu print operations."
871
+ },
872
+ bed_type: {
873
+ type: "string",
874
+ enum: ["textured_plate", "cool_plate", "engineering_plate", "hot_plate"],
875
+ description: "Bed/plate type installed on the printer (default: textured_plate)."
876
+ },
877
+ nozzle_diameter: {
878
+ type: "string",
879
+ description: "Nozzle diameter in mm (default: 0.4)."
880
+ },
881
+ slicer_type: {
882
+ type: "string",
883
+ enum: [...SLICER_TYPE_ENUM],
884
+ description: "Type of slicer to use. Use orcaslicer-bambulab for FULU OrcaSlicer-bambulab."
885
+ },
886
+ slicer_path: {
887
+ type: "string",
888
+ description: "Path to the slicer executable (default: value from env)."
889
+ },
890
+ slicer_profile: {
891
+ type: "string",
892
+ description: "Profile to use for slicing (default: value from env). OrcaSlicer also accepts machine/process profiles separated with ';', optionally followed by '|filament.json'."
893
+ },
894
+ filament_profile: {
895
+ type: "string",
896
+ description: "OrcaSlicer filament profile path loaded with --load-filaments (default: FILAMENT_PROFILE/SLICER_FILAMENT_PROFILE env)."
507
897
  }
508
898
  },
509
899
  required: ["stl_path", "extension_inches"]
@@ -732,11 +1122,128 @@ class ThreeDPrinterMCPServer {
732
1122
  type: "object",
733
1123
  description: "Override AMS filament mapping (e.g., {\"Generic PLA\": 0, \"Generic PETG\": 1}).",
734
1124
  additionalProperties: { type: "number" }
1125
+ },
1126
+ slicer_type: {
1127
+ type: "string",
1128
+ enum: [...SLICER_TYPE_ENUM],
1129
+ description: "Slicer to use if the 3MF needs auto-slicing. Use orcaslicer-bambulab for FULU OrcaSlicer-bambulab."
1130
+ },
1131
+ slicer_path: {
1132
+ type: "string",
1133
+ description: "Path to the slicer executable if auto-slicing is needed."
1134
+ },
1135
+ slicer_profile: {
1136
+ type: "string",
1137
+ description: "Optional slicer settings/profile path for auto-slicing."
1138
+ },
1139
+ filament_profile: {
1140
+ type: "string",
1141
+ description: "Optional filament profile path for auto-slicing with OrcaSlicer/Bambu Studio."
1142
+ },
1143
+ use_ams: {
1144
+ type: "boolean",
1145
+ description: "Whether to use AMS for the print. Defaults from parsed 3MF mapping when present."
1146
+ },
1147
+ bed_leveling: {
1148
+ type: "boolean",
1149
+ description: "Override bed leveling flag for the Bambu print command."
1150
+ },
1151
+ flow_calibration: {
1152
+ type: "boolean",
1153
+ description: "Override flow calibration flag for the Bambu print command."
1154
+ },
1155
+ vibration_calibration: {
1156
+ type: "boolean",
1157
+ description: "Override vibration calibration flag for the Bambu print command."
1158
+ },
1159
+ layer_inspect: {
1160
+ type: "boolean",
1161
+ description: "Override layer inspection flag for the Bambu print command."
1162
+ },
1163
+ timelapse: {
1164
+ type: "boolean",
1165
+ description: "Override timelapse flag for the Bambu print command."
735
1166
  }
736
1167
  },
737
1168
  required: ["three_mf_path", "bambu_model"]
738
1169
  }
739
1170
  },
1171
+ {
1172
+ name: "check_fulu_orca_setup",
1173
+ description:
1174
+ "Inspect a FULU OrcaSlicer-bambulab install, platform runtime payload, setup commands, and optionally probe the BambuNetwork bridge.",
1175
+ inputSchema: {
1176
+ type: "object",
1177
+ properties: {
1178
+ slicer_path: {
1179
+ type: "string",
1180
+ description: "Path to the FULU OrcaSlicer executable. Defaults from SLICER_PATH/FULU_ORCA_PATH."
1181
+ },
1182
+ plugin_dir: {
1183
+ type: "string",
1184
+ description: "Directory containing the FULU Bambu runtime payload; on macOS this is usually OrcaSlicer.app/Contents/MacOS."
1185
+ },
1186
+ runtime_dir: {
1187
+ type: "string",
1188
+ description: "Installed runtime directory. On macOS this defaults to ~/Library/Application Support/OrcaSlicer/macos-bridge/runtime."
1189
+ },
1190
+ platform: {
1191
+ type: "string",
1192
+ enum: ["darwin", "macos", "win32", "windows", "linux"],
1193
+ description: "Platform to inspect. Defaults to the current Node.js platform."
1194
+ },
1195
+ bridge_command: {
1196
+ type: "string",
1197
+ description: "Command that starts the FULU BambuNetwork bridge host for probing."
1198
+ },
1199
+ run_bridge_probe: {
1200
+ type: "boolean",
1201
+ description: "When true, sends bridge.handshake, bridge.capabilities, and bridge.runtime_info to the bridge host."
1202
+ },
1203
+ probe_timeout_ms: {
1204
+ type: "number",
1205
+ description: "Bridge probe timeout in milliseconds (default: 5000)."
1206
+ }
1207
+ }
1208
+ }
1209
+ },
1210
+ {
1211
+ name: "fulu_bambu_network_rpc",
1212
+ description:
1213
+ "Advanced FULU bridge RPC for BambuNetwork diagnostics and development. Read-only methods are allowed by default; mutating methods require allow_mutating_method=true, and print methods require bambu_model.",
1214
+ inputSchema: {
1215
+ type: "object",
1216
+ properties: {
1217
+ bridge_command: {
1218
+ type: "string",
1219
+ description: "Command that starts the FULU BambuNetwork bridge host. Defaults to FULU_BAMBU_BRIDGE_COMMAND."
1220
+ },
1221
+ method: {
1222
+ type: "string",
1223
+ description: "FULU bridge method, e.g. bridge.handshake, bridge.runtime_info, net.get_user_print_info, net.start_print."
1224
+ },
1225
+ payload: {
1226
+ type: "object",
1227
+ description: "JSON payload sent to the FULU bridge method.",
1228
+ additionalProperties: true
1229
+ },
1230
+ timeout_ms: {
1231
+ type: "number",
1232
+ description: "Bridge request timeout in milliseconds (default: 5000)."
1233
+ },
1234
+ allow_mutating_method: {
1235
+ type: "boolean",
1236
+ description: "Required for methods outside the read-only bridge/net allowlist."
1237
+ },
1238
+ bambu_model: {
1239
+ type: "string",
1240
+ enum: ["p1s", "p1p", "x1c", "x1e", "a1", "a1mini", "h2d"],
1241
+ description: "Required when calling FULU print RPC methods; preserves the Bambu model safety gate."
1242
+ }
1243
+ },
1244
+ required: ["method"]
1245
+ }
1246
+ },
740
1247
  {
741
1248
  name: "merge_vertices",
742
1249
  description: "Merge vertices in an STL file that are closer than the specified tolerance.",
@@ -825,9 +1332,10 @@ class ThreeDPrinterMCPServer {
825
1332
  const apiKey = String(args?.api_key || DEFAULT_API_KEY);
826
1333
  const bambuSerial = String(args?.bambu_serial || DEFAULT_BAMBU_SERIAL);
827
1334
  const bambuToken = String(args?.bambu_token || DEFAULT_BAMBU_TOKEN);
828
- const slicerType = String(args?.slicer_type || DEFAULT_SLICER_TYPE) as 'prusaslicer' | 'cura' | 'slic3r' | 'orcaslicer' | 'bambustudio';
1335
+ const slicerType = normalizeSlicerType(String(args?.slicer_type || DEFAULT_SLICER_TYPE));
829
1336
  const slicerPath = String(args?.slicer_path || DEFAULT_SLICER_PATH);
830
1337
  const slicerProfile = String(args?.slicer_profile || DEFAULT_SLICER_PROFILE);
1338
+ const filamentProfile = String(args?.filament_profile || DEFAULT_FILAMENT_PROFILE);
831
1339
 
832
1340
  try {
833
1341
  let result;
@@ -842,13 +1350,21 @@ class ThreeDPrinterMCPServer {
842
1350
  break;
843
1351
 
844
1352
  case "upload_gcode":
845
- if (!args?.filename || !args?.gcode) {
846
- throw new Error("Missing required parameters: filename and gcode");
1353
+ if (!args?.gcode && !args?.gcode_path) {
1354
+ throw new Error("Missing required parameter: gcode or gcode_path");
1355
+ }
1356
+ if (args?.gcode_path && !fs.existsSync(String(args.gcode_path))) {
1357
+ throw new Error(`G-code file not found: ${String(args.gcode_path)}`);
1358
+ }
1359
+ if (!args?.filename && !args?.gcode_path) {
1360
+ throw new Error("Missing required parameter: filename");
847
1361
  }
1362
+ const uploadFilename = String(args?.filename || path.basename(String(args.gcode_path)));
1363
+ const uploadGcodeSource = String(args?.gcode_path || args.gcode);
848
1364
  result = await this.uploadGcode(
849
1365
  host, port, type, apiKey, bambuSerial, bambuToken,
850
- String(args.filename),
851
- String(args.gcode),
1366
+ uploadFilename,
1367
+ uploadGcodeSource,
852
1368
  Boolean(args.print || false)
853
1369
  );
854
1370
  break;
@@ -892,7 +1408,7 @@ class ThreeDPrinterMCPServer {
892
1408
  }
893
1409
  // Resolve printer model for BambuStudio slicer
894
1410
  let slicePreset: string | undefined;
895
- if (slicerType === 'bambustudio') {
1411
+ if (isBambuProjectSlicer(slicerType)) {
896
1412
  const sliceModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
897
1413
  const nozzleDiam = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
898
1414
  slicePreset = BAMBU_MODEL_PRESETS[sliceModel]?.(nozzleDiam);
@@ -903,7 +1419,8 @@ class ThreeDPrinterMCPServer {
903
1419
  slicerPath,
904
1420
  slicerProfile || undefined,
905
1421
  undefined, // progressCallback
906
- slicePreset
1422
+ slicePreset,
1423
+ filamentProfile || undefined
907
1424
  );
908
1425
  break;
909
1426
  }
@@ -940,10 +1457,12 @@ class ThreeDPrinterMCPServer {
940
1457
 
941
1458
  // 2. Slice the extended STL file
942
1459
  let processPreset: string | undefined;
943
- if (slicerType === 'bambustudio') {
1460
+ if (type.toLowerCase() === 'bambu' || isBambuProjectSlicer(slicerType)) {
944
1461
  const processModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
945
1462
  const processNozzle = String(args?.nozzle_diameter || DEFAULT_NOZZLE_DIAMETER);
946
- processPreset = BAMBU_MODEL_PRESETS[processModel]?.(processNozzle);
1463
+ if (isBambuProjectSlicer(slicerType)) {
1464
+ processPreset = BAMBU_MODEL_PRESETS[processModel]?.(processNozzle);
1465
+ }
947
1466
  }
948
1467
  const gcodePath = await this.stlManipulator.sliceSTL(
949
1468
  extendedStlPath,
@@ -951,8 +1470,59 @@ class ThreeDPrinterMCPServer {
951
1470
  slicerPath,
952
1471
  slicerProfile || undefined,
953
1472
  processProgressCallback,
954
- processPreset
1473
+ processPreset,
1474
+ filamentProfile || undefined
955
1475
  );
1476
+
1477
+ if (type.toLowerCase() === 'bambu' && gcodePath.toLowerCase().endsWith(".3mf")) {
1478
+ if (!bambuSerial || !bambuToken) {
1479
+ throw new Error("Bambu serial number and access token are required for Bambu 3MF printing.");
1480
+ }
1481
+
1482
+ const factoryImplementation = this.printerFactory.getImplementation('bambu');
1483
+ if (!(factoryImplementation instanceof BambuImplementation)) {
1484
+ throw new Error("Internal error: Could not get Bambu printer implementation.");
1485
+ }
1486
+
1487
+ const printBedType = resolveBedType(args?.bed_type as string | undefined);
1488
+ let parsedProcess3MF: ThreeMFData | undefined;
1489
+ try {
1490
+ parsedProcess3MF = await parse3MF(gcodePath);
1491
+ } catch (error) {
1492
+ console.warn(
1493
+ `Could not parse AMS metadata from sliced 3MF ${gcodePath}:`,
1494
+ (error as Error).message
1495
+ );
1496
+ }
1497
+ const amsOptions = this.resolveAmsPrintOptions(
1498
+ parsedProcess3MF,
1499
+ args as Record<string, unknown> | undefined
1500
+ );
1501
+
1502
+ result = await factoryImplementation.print3mf(host, bambuSerial, bambuToken, {
1503
+ projectName: path.basename(gcodePath).replace(/\.3mf$/i, ''),
1504
+ filePath: gcodePath,
1505
+ plateIndex: 0,
1506
+ ...amsOptions,
1507
+ bedType: printBedType,
1508
+ bedLeveling: args?.bed_leveling !== undefined ? Boolean(args.bed_leveling) : undefined,
1509
+ flowCalibration:
1510
+ args?.flow_calibration !== undefined ? Boolean(args.flow_calibration) : undefined,
1511
+ vibrationCalibration:
1512
+ args?.vibration_calibration !== undefined
1513
+ ? Boolean(args.vibration_calibration)
1514
+ : undefined,
1515
+ layerInspect: args?.layer_inspect !== undefined ? Boolean(args.layer_inspect) : undefined,
1516
+ timelapse: args?.timelapse !== undefined ? Boolean(args.timelapse) : undefined,
1517
+ });
1518
+ break;
1519
+ }
1520
+
1521
+ if (gcodePath.toLowerCase().endsWith(".3mf")) {
1522
+ throw new Error(
1523
+ "process_and_print_stl produced a sliced 3MF. Direct process-and-print for sliced 3MF is only supported for Bambu printers via print_3mf."
1524
+ );
1525
+ }
956
1526
 
957
1527
  // 3. Confirm temperatures if specified
958
1528
  if (args.extruder_temp !== undefined || args.bed_temp !== undefined) {
@@ -1211,7 +1781,8 @@ class ThreeDPrinterMCPServer {
1211
1781
  slicerPath,
1212
1782
  slicerProfile || undefined,
1213
1783
  undefined, // progressCallback
1214
- printPreset
1784
+ printPreset,
1785
+ filamentProfile || undefined
1215
1786
  );
1216
1787
  console.log("Auto-sliced to: " + threeMFPath);
1217
1788
  }
@@ -1223,60 +1794,16 @@ class ThreeDPrinterMCPServer {
1223
1794
  let implementation: BambuImplementation;
1224
1795
  let threeMfFilename: string;
1225
1796
  let projectName: string;
1226
- let finalAmsMapping: number[] | undefined;
1227
- let useAMS: boolean;
1228
1797
  let printOptions: any; // Use a more specific type later if possible
1229
1798
 
1230
1799
  try {
1231
1800
  // --- Parse 3MF ---
1232
1801
  const parsed3MFData = await parse3MF(threeMFPath);
1233
1802
  console.log(`Successfully parsed 3MF file: ${threeMFPath}`);
1234
- let parsedAmsMapping: number[] | undefined = undefined;
1235
- // ... (Extract default AMS mapping logic) ...
1236
- if (parsed3MFData.slicerConfig?.ams_mapping) {
1237
- const slots = Object.values(parsed3MFData.slicerConfig.ams_mapping)
1238
- .filter(v => typeof v === 'number') as number[];
1239
- if (slots.length > 0) {
1240
- parsedAmsMapping = slots.sort((a, b) => a - b);
1241
- console.log("Extracted default AMS mapping from 3MF:", parsedAmsMapping);
1242
- } else {
1243
- console.log("AMS mapping found in 3MF, but no valid slots extracted.");
1244
- }
1245
- } else {
1246
- console.log("No default AMS mapping found in 3MF slicer config.");
1247
- }
1248
-
1249
- // --- Gather Overrides and Determine Final Options ---
1250
- finalAmsMapping = parsedAmsMapping; // Start with parsed
1251
- useAMS = args?.use_ams !== undefined ? Boolean(args.use_ams) : (!!finalAmsMapping && finalAmsMapping.length > 0);
1252
- // ... (Process user AMS mapping override logic) ...
1253
- if (args?.ams_mapping) {
1254
- let userMappingOverride: number[] | undefined = undefined;
1255
- if (Array.isArray(args.ams_mapping)) {
1256
- userMappingOverride = args.ams_mapping.filter(v => typeof v === 'number');
1257
- } else if (typeof args.ams_mapping === 'object') {
1258
- userMappingOverride = Object.values(args.ams_mapping)
1259
- .filter(v => typeof v === 'number')
1260
- .sort((a, b) => a - b) as number[];
1261
- }
1262
-
1263
- if (userMappingOverride && userMappingOverride.length > 0) {
1264
- console.log("Applying user AMS mapping override:", userMappingOverride);
1265
- finalAmsMapping = userMappingOverride;
1266
- useAMS = true; // Force useAMS if override provided
1267
- } else {
1268
- console.warn("Received ams_mapping override, but it was empty or invalid.");
1269
- }
1270
- }
1271
- // ... (Handle explicit use_ams=false) ...
1272
- if (args?.use_ams === false) {
1273
- console.log("User explicitly disabled AMS.");
1274
- finalAmsMapping = undefined;
1275
- useAMS = false;
1276
- }
1277
- if (!finalAmsMapping || finalAmsMapping.length === 0) {
1278
- useAMS = false;
1279
- }
1803
+ const amsOptions = this.resolveAmsPrintOptions(
1804
+ parsed3MFData,
1805
+ args as Record<string, unknown> | undefined
1806
+ );
1280
1807
 
1281
1808
  // --- Prepare Implementation and Print Options ---
1282
1809
  const factoryImplementation = this.printerFactory.getImplementation('bambu');
@@ -1289,8 +1816,7 @@ class ThreeDPrinterMCPServer {
1289
1816
  projectName = threeMfFilename.replace(/\.3mf$/i, ''); // Assign to outer scope variable
1290
1817
 
1291
1818
  printOptions = { // Assign to outer scope variable
1292
- useAMS: useAMS,
1293
- amsMapping: finalAmsMapping,
1819
+ ...amsOptions,
1294
1820
  bedType: printBedType,
1295
1821
  bedLeveling: args?.bed_leveling !== undefined ? Boolean(args.bed_leveling) : undefined,
1296
1822
  flowCalibration: args?.flow_calibration !== undefined ? Boolean(args.flow_calibration) : undefined,
@@ -1322,6 +1848,48 @@ class ThreeDPrinterMCPServer {
1322
1848
  break;
1323
1849
  }
1324
1850
 
1851
+ case "check_fulu_orca_setup": {
1852
+ result = await inspectFuluOrcaSetup({
1853
+ slicerPath: args?.slicer_path !== undefined ? String(args.slicer_path) : undefined,
1854
+ pluginDir: args?.plugin_dir !== undefined ? String(args.plugin_dir) : undefined,
1855
+ runtimeDir: args?.runtime_dir !== undefined ? String(args.runtime_dir) : undefined,
1856
+ platform: args?.platform !== undefined ? String(args.platform) : undefined,
1857
+ bridgeCommand: args?.bridge_command !== undefined ? String(args.bridge_command) : undefined,
1858
+ runBridgeProbe: Boolean(args?.run_bridge_probe ?? false),
1859
+ probeTimeoutMs:
1860
+ args?.probe_timeout_ms !== undefined ? Number(args.probe_timeout_ms) : undefined,
1861
+ });
1862
+ break;
1863
+ }
1864
+
1865
+ case "fulu_bambu_network_rpc": {
1866
+ if (!args?.method) {
1867
+ throw new Error("Missing required parameter: method");
1868
+ }
1869
+
1870
+ const method = String(args.method);
1871
+ let rpcBambuModel: string | undefined;
1872
+ if (isFuluPrintRpcMethod(method)) {
1873
+ rpcBambuModel = await this.resolveBambuModel(args?.bambu_model as string | undefined);
1874
+ }
1875
+
1876
+ const payload =
1877
+ typeof args.payload === "object" && args.payload !== null && !Array.isArray(args.payload)
1878
+ ? (args.payload as Record<string, unknown>)
1879
+ : undefined;
1880
+
1881
+ result = await invokeFuluBridgeRpc({
1882
+ bridgeCommand:
1883
+ args?.bridge_command !== undefined ? String(args.bridge_command) : undefined,
1884
+ method,
1885
+ payload,
1886
+ timeoutMs: args?.timeout_ms !== undefined ? Number(args.timeout_ms) : undefined,
1887
+ allowMutatingMethod: Boolean(args?.allow_mutating_method ?? false),
1888
+ bambuModel: rpcBambuModel,
1889
+ });
1890
+ break;
1891
+ }
1892
+
1325
1893
  case "merge_vertices":
1326
1894
  if (!args?.stl_path) {
1327
1895
  throw new Error("Missing required parameter: stl_path");
@@ -1459,10 +2027,13 @@ class ThreeDPrinterMCPServer {
1459
2027
  gcode: string,
1460
2028
  print: boolean
1461
2029
  ) {
1462
- const tempFilePath = path.join(TEMP_DIR, filename);
1463
-
1464
- // Write gcode to temporary file
1465
- fs.writeFileSync(tempFilePath, gcode);
2030
+ const sourceIsFilePath = fs.existsSync(gcode) && fs.statSync(gcode).isFile();
2031
+ const tempFilePath = sourceIsFilePath ? gcode : path.join(TEMP_DIR, filename);
2032
+
2033
+ if (!sourceIsFilePath) {
2034
+ fs.mkdirSync(TEMP_DIR, { recursive: true });
2035
+ fs.writeFileSync(tempFilePath, gcode);
2036
+ }
1466
2037
 
1467
2038
  try {
1468
2039
  const implementation = this.printerFactory.getImplementation(type);
@@ -1474,8 +2045,7 @@ class ThreeDPrinterMCPServer {
1474
2045
 
1475
2046
  return await implementation.uploadFile(host, port, apiKey, tempFilePath, filename, print);
1476
2047
  } finally {
1477
- // Clean up temporary file
1478
- if (fs.existsSync(tempFilePath)) {
2048
+ if (!sourceIsFilePath && fs.existsSync(tempFilePath)) {
1479
2049
  fs.unlinkSync(tempFilePath);
1480
2050
  }
1481
2051
  }