mcp-3d-printer-server 1.2.2 → 1.2.5
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/README.md +236 -26
- package/dist/fulu-orca.d.ts +20 -0
- package/dist/fulu-orca.js +453 -0
- package/dist/http-server.d.ts +1 -15
- package/dist/http-server.js +6 -266
- package/dist/index.js +572 -72
- package/dist/printers/prusa.d.ts +3 -0
- package/dist/printers/prusa.js +33 -1
- package/dist/sse-server.d.ts +1 -19
- package/dist/sse-server.js +6 -316
- package/dist/stl/stl-manipulator.d.ts +11 -2
- package/dist/stl/stl-manipulator.js +162 -26
- package/package.json +32 -4
- package/src/fulu-orca.ts +602 -0
- package/src/http-server.ts +4 -323
- package/src/index.ts +642 -72
- package/src/printers/prusa.ts +38 -1
- package/src/sse-server.ts +4 -404
- package/src/stl/stl-manipulator.ts +217 -29
- package/src/test/Tape+Card+55mm+Ring.stl +0 -0
|
@@ -10,6 +10,7 @@ import * as crypto from 'crypto';
|
|
|
10
10
|
import { execFile } from 'child_process';
|
|
11
11
|
const readFileAsync = promisify(fs.readFile);
|
|
12
12
|
const writeFileAsync = promisify(fs.writeFile);
|
|
13
|
+
const DEFAULT_SLICER_TIMEOUT_MS = 10 * 60 * 1000;
|
|
13
14
|
export class STLManipulator extends EventEmitter {
|
|
14
15
|
constructor(tempDir = path.join(process.cwd(), 'temp')) {
|
|
15
16
|
super();
|
|
@@ -26,6 +27,97 @@ export class STLManipulator extends EventEmitter {
|
|
|
26
27
|
generateOperationId() {
|
|
27
28
|
return crypto.randomUUID();
|
|
28
29
|
}
|
|
30
|
+
validateSlicerExecutable(slicerPath) {
|
|
31
|
+
if (!slicerPath.trim()) {
|
|
32
|
+
throw new Error("Slicer executable path is required.");
|
|
33
|
+
}
|
|
34
|
+
const looksLikePath = path.isAbsolute(slicerPath) || /[\\/]/.test(slicerPath);
|
|
35
|
+
if (looksLikePath && !fs.existsSync(slicerPath)) {
|
|
36
|
+
throw new Error(`Slicer executable not found at: ${slicerPath}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
splitProfileList(profileList) {
|
|
40
|
+
return (profileList || "")
|
|
41
|
+
.split(";")
|
|
42
|
+
.map((profile) => profile.trim())
|
|
43
|
+
.filter((profile) => profile.length > 0);
|
|
44
|
+
}
|
|
45
|
+
parseOrcaProfileArgs(slicerProfile, filamentProfile) {
|
|
46
|
+
const [settingsSegment = "", filamentSegment = ""] = (slicerProfile || "").split("|", 2);
|
|
47
|
+
return {
|
|
48
|
+
settingsProfiles: this.splitProfileList(settingsSegment),
|
|
49
|
+
filamentProfiles: [
|
|
50
|
+
...this.splitProfileList(filamentSegment),
|
|
51
|
+
...this.splitProfileList(filamentProfile),
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
warnForMissingProfiles(profiles, label) {
|
|
56
|
+
for (const profile of profiles) {
|
|
57
|
+
if (!fs.existsSync(profile)) {
|
|
58
|
+
console.warn(`${label} specified but not found: ${profile}. Slicer might use defaults.`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
appendOrcaProfileArgs(args, profiles) {
|
|
63
|
+
for (const profile of profiles.settingsProfiles) {
|
|
64
|
+
args.push('--load-settings', profile);
|
|
65
|
+
}
|
|
66
|
+
for (const profile of profiles.filamentProfiles) {
|
|
67
|
+
args.push('--load-filaments', profile);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
resolveOrcaGcodeOutput(outputDir) {
|
|
71
|
+
const preferredPath = path.join(outputDir, 'plate_1.gcode');
|
|
72
|
+
if (fs.existsSync(preferredPath)) {
|
|
73
|
+
return preferredPath;
|
|
74
|
+
}
|
|
75
|
+
const plateOutputs = fs.readdirSync(outputDir)
|
|
76
|
+
.filter((entry) => /^plate_\d+\.gcode$/i.test(entry))
|
|
77
|
+
.sort((a, b) => {
|
|
78
|
+
const aNumber = Number(a.match(/\d+/)?.[0] || 0);
|
|
79
|
+
const bNumber = Number(b.match(/\d+/)?.[0] || 0);
|
|
80
|
+
return aNumber - bNumber;
|
|
81
|
+
});
|
|
82
|
+
if (plateOutputs.length === 0) {
|
|
83
|
+
throw new Error(`OrcaSlicer finished but no plate_N.gcode file was found in: ${outputDir}`);
|
|
84
|
+
}
|
|
85
|
+
return path.join(outputDir, plateOutputs[0]);
|
|
86
|
+
}
|
|
87
|
+
resolveBambuMachinePresetPath(slicerPath, printerPreset) {
|
|
88
|
+
if (fs.existsSync(printerPreset)) {
|
|
89
|
+
return printerPreset;
|
|
90
|
+
}
|
|
91
|
+
const presetFile = printerPreset.endsWith(".json") ? printerPreset : `${printerPreset}.json`;
|
|
92
|
+
const candidateDirs = [];
|
|
93
|
+
const macAppMarker = `${path.sep}Contents${path.sep}MacOS${path.sep}`;
|
|
94
|
+
const macAppIndex = slicerPath.indexOf(macAppMarker);
|
|
95
|
+
if (macAppIndex >= 0) {
|
|
96
|
+
candidateDirs.push(path.join(slicerPath.slice(0, macAppIndex + `${path.sep}Contents`.length), "Resources", "profiles", "BBL", "machine"));
|
|
97
|
+
}
|
|
98
|
+
const executableDir = path.dirname(slicerPath);
|
|
99
|
+
candidateDirs.push(path.join(executableDir, "..", "Resources", "profiles", "BBL", "machine"), path.join(executableDir, "..", "share", "OrcaSlicer", "resources", "profiles", "BBL", "machine"), path.join(executableDir, "..", "resources", "profiles", "BBL", "machine"), path.join(executableDir, "resources", "profiles", "BBL", "machine"));
|
|
100
|
+
for (const candidateDir of candidateDirs) {
|
|
101
|
+
const candidate = path.resolve(candidateDir, presetFile);
|
|
102
|
+
if (fs.existsSync(candidate)) {
|
|
103
|
+
return candidate;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
throw new Error(`Could not find Bambu machine preset "${presetFile}" near slicer executable ${slicerPath}. ` +
|
|
107
|
+
"Pass slicer_profile with an explicit machine/process settings file, or use a slicer bundle that includes Resources/profiles/BBL/machine.");
|
|
108
|
+
}
|
|
109
|
+
resolveSlicerTimeoutMs() {
|
|
110
|
+
const rawTimeout = process.env.SLICER_TIMEOUT_MS?.trim();
|
|
111
|
+
if (!rawTimeout) {
|
|
112
|
+
return DEFAULT_SLICER_TIMEOUT_MS;
|
|
113
|
+
}
|
|
114
|
+
const timeoutMs = Number(rawTimeout);
|
|
115
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
116
|
+
console.warn(`Invalid SLICER_TIMEOUT_MS value "${rawTimeout}", using ${DEFAULT_SLICER_TIMEOUT_MS}.`);
|
|
117
|
+
return DEFAULT_SLICER_TIMEOUT_MS;
|
|
118
|
+
}
|
|
119
|
+
return timeoutMs;
|
|
120
|
+
}
|
|
29
121
|
/**
|
|
30
122
|
* Load STL file and return geometry and bounding box
|
|
31
123
|
*/
|
|
@@ -646,24 +738,31 @@ export class STLManipulator extends EventEmitter {
|
|
|
646
738
|
/**
|
|
647
739
|
* Slice an STL or 3MF file using the specified slicer
|
|
648
740
|
* @param stlFilePath Path to the input STL or 3MF file
|
|
649
|
-
* @param slicerType Type of slicer (prusaslicer, cura, slic3r, orcaslicer, bambustudio)
|
|
741
|
+
* @param slicerType Type of slicer (prusaslicer, cura, slic3r, orcaslicer, orcaslicer-bambulab, bambustudio)
|
|
650
742
|
* @param slicerPath Path to the slicer executable
|
|
651
743
|
* @param slicerProfile Optional path to the slicer profile/config file
|
|
652
744
|
* @param progressCallback Optional callback for progress updates
|
|
653
745
|
* @returns Path to the generated G-code or sliced 3MF file
|
|
654
746
|
*/
|
|
655
|
-
async sliceSTL(stlFilePath, slicerType, slicerPath, slicerProfile, progressCallback, printerPreset) {
|
|
747
|
+
async sliceSTL(stlFilePath, slicerType, slicerPath, slicerProfile, progressCallback, printerPreset, filamentProfile) {
|
|
656
748
|
const operationId = this.generateOperationId();
|
|
657
749
|
this.activeOperations.set(operationId, true);
|
|
658
|
-
|
|
659
|
-
|
|
750
|
+
this.validateSlicerExecutable(slicerPath);
|
|
751
|
+
const orcaProfiles = this.parseOrcaProfileArgs(slicerProfile, filamentProfile);
|
|
752
|
+
if (slicerType === 'orcaslicer') {
|
|
753
|
+
this.warnForMissingProfiles(orcaProfiles.settingsProfiles, 'Slicer profile');
|
|
754
|
+
this.warnForMissingProfiles(orcaProfiles.filamentProfiles, 'Filament profile');
|
|
660
755
|
}
|
|
661
|
-
if (slicerProfile && !fs.existsSync(slicerProfile)) {
|
|
756
|
+
else if (slicerProfile && !fs.existsSync(slicerProfile)) {
|
|
662
757
|
console.warn(`Slicer profile specified but not found: ${slicerProfile}. Slicer might use defaults.`);
|
|
663
758
|
// Allow proceeding without profile, slicer might handle it
|
|
664
759
|
}
|
|
760
|
+
if (slicerType !== 'orcaslicer' && filamentProfile && !fs.existsSync(filamentProfile)) {
|
|
761
|
+
console.warn(`Filament profile specified but not found: ${filamentProfile}. Slicer might use defaults.`);
|
|
762
|
+
}
|
|
665
763
|
const outputFileName = path.basename(stlFilePath, '.stl') + '.gcode';
|
|
666
764
|
let outputFilePath = path.join(this.tempDir, outputFileName);
|
|
765
|
+
let orcaOutputDir;
|
|
667
766
|
let args = [];
|
|
668
767
|
try {
|
|
669
768
|
if (progressCallback)
|
|
@@ -681,22 +780,14 @@ export class STLManipulator extends EventEmitter {
|
|
|
681
780
|
args = args.filter(arg => arg !== '');
|
|
682
781
|
break;
|
|
683
782
|
case 'orcaslicer': // Add OrcaSlicer case
|
|
783
|
+
orcaOutputDir = path.join(this.tempDir, `${operationId}-orca-output`);
|
|
784
|
+
fs.mkdirSync(orcaOutputDir, { recursive: true });
|
|
684
785
|
args = [
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
// Also assuming profile path points to the main .ini or .json config.
|
|
688
|
-
'--load-settings', slicerProfile || '',
|
|
689
|
-
'--output', outputFilePath, // Assuming --output works like PrusaSlicer
|
|
690
|
-
stlFilePath
|
|
786
|
+
'--slice', '0',
|
|
787
|
+
'--outputdir', orcaOutputDir,
|
|
691
788
|
];
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
// '--load-settings', slicerProfile || '',
|
|
695
|
-
// '--outputdir', this.tempDir,
|
|
696
|
-
// stlFilePath
|
|
697
|
-
// ];
|
|
698
|
-
// Remove empty profile arg if not provided
|
|
699
|
-
args = args.filter(arg => arg !== '');
|
|
789
|
+
this.appendOrcaProfileArgs(args, orcaProfiles);
|
|
790
|
+
args.push(stlFilePath);
|
|
700
791
|
break;
|
|
701
792
|
case 'cura':
|
|
702
793
|
// CuraEngine CLI args are different, often requiring -s for settings
|
|
@@ -712,8 +803,10 @@ export class STLManipulator extends EventEmitter {
|
|
|
712
803
|
args.push('-j', slicerProfile); // Load settings from profile definition file
|
|
713
804
|
}
|
|
714
805
|
break;
|
|
806
|
+
case 'orcaslicer-bambulab':
|
|
715
807
|
case 'bambustudio':
|
|
716
|
-
// Bambu
|
|
808
|
+
// Bambu project slicers: slice and export as 3MF with embedded gcode.
|
|
809
|
+
// orcaslicer-bambulab is the FULU fork that restores BambuNetwork support.
|
|
717
810
|
// For 3MF input: slice in place and export sliced 3MF
|
|
718
811
|
// For STL input: slice and export as 3MF
|
|
719
812
|
{
|
|
@@ -724,13 +817,16 @@ export class STLManipulator extends EventEmitter {
|
|
|
724
817
|
'--slice', '0', // Slice all plates
|
|
725
818
|
'--export-3mf', bambuOutputPath,
|
|
726
819
|
];
|
|
727
|
-
//
|
|
820
|
+
// FULU Orca/Bambu Studio load machine presets as JSON settings files.
|
|
728
821
|
if (printerPreset && !slicerProfile) {
|
|
729
|
-
args.push('--load-
|
|
822
|
+
args.push('--load-settings', this.resolveBambuMachinePresetPath(slicerPath, printerPreset));
|
|
730
823
|
}
|
|
731
824
|
if (slicerProfile) {
|
|
732
825
|
args.push('--load-settings', slicerProfile);
|
|
733
826
|
}
|
|
827
|
+
if (filamentProfile) {
|
|
828
|
+
args.push('--load-filaments', filamentProfile);
|
|
829
|
+
}
|
|
734
830
|
args.push(stlFilePath);
|
|
735
831
|
// Override outputFilePath for bambustudio since it produces 3MF, not gcode
|
|
736
832
|
outputFilePath = bambuOutputPath;
|
|
@@ -744,20 +840,47 @@ export class STLManipulator extends EventEmitter {
|
|
|
744
840
|
console.log(`Executing: ${slicerPath} ${args.join(' ')}`);
|
|
745
841
|
// Execute the slicer
|
|
746
842
|
await new Promise((resolve, reject) => {
|
|
843
|
+
const timeoutMs = this.resolveSlicerTimeoutMs();
|
|
844
|
+
let settled = false;
|
|
845
|
+
let slicerProcessKilledByTimeout = false;
|
|
846
|
+
let timeoutHandle;
|
|
847
|
+
const settle = (callback) => {
|
|
848
|
+
if (settled) {
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
settled = true;
|
|
852
|
+
if (timeoutHandle) {
|
|
853
|
+
clearTimeout(timeoutHandle);
|
|
854
|
+
}
|
|
855
|
+
callback();
|
|
856
|
+
};
|
|
747
857
|
const process = execFile(slicerPath, args, (error, stdout, stderr) => {
|
|
748
858
|
if (error) {
|
|
749
859
|
console.error(`Slicer Error: ${error.message}`);
|
|
750
860
|
console.error(`Slicer Stderr: ${stderr}`);
|
|
751
|
-
|
|
861
|
+
const timeoutMessage = slicerProcessKilledByTimeout
|
|
862
|
+
? ` Slicer exceeded timeout ${timeoutMs}ms.`
|
|
863
|
+
: "";
|
|
864
|
+
settle(() => reject(new Error(`Slicer failed: ${error.message}.${timeoutMessage} Stderr: ${stderr}`)));
|
|
752
865
|
}
|
|
753
866
|
else {
|
|
754
867
|
console.log(`Slicer Stdout: ${stdout}`);
|
|
755
868
|
if (stderr) {
|
|
756
869
|
console.warn(`Slicer Stderr: ${stderr}`); // Log stderr even on success
|
|
757
870
|
}
|
|
758
|
-
resolve();
|
|
871
|
+
settle(() => resolve());
|
|
759
872
|
}
|
|
760
873
|
});
|
|
874
|
+
timeoutHandle = setTimeout(() => {
|
|
875
|
+
slicerProcessKilledByTimeout = true;
|
|
876
|
+
try {
|
|
877
|
+
process.kill("SIGKILL");
|
|
878
|
+
}
|
|
879
|
+
catch (killError) {
|
|
880
|
+
console.error("Error attempting to kill timed-out slicer process:", killError);
|
|
881
|
+
}
|
|
882
|
+
settle(() => reject(new Error(`Slicer exceeded timeout ${timeoutMs}ms.`)));
|
|
883
|
+
}, timeoutMs);
|
|
761
884
|
// Optional: Add listeners for stdout/stderr for real-time progress if slicer provides it
|
|
762
885
|
// process.stdout?.on('data', (data) => { console.log(`Slicer stdout: ${data}`); });
|
|
763
886
|
// process.stderr?.on('data', (data) => { console.log(`Slicer stderr: ${data}`); });
|
|
@@ -767,11 +890,11 @@ export class STLManipulator extends EventEmitter {
|
|
|
767
890
|
clearInterval(checkCancel);
|
|
768
891
|
try {
|
|
769
892
|
process.kill(); // Attempt to kill the slicer process
|
|
770
|
-
reject(new Error("Slicing operation cancelled"));
|
|
893
|
+
settle(() => reject(new Error("Slicing operation cancelled")));
|
|
771
894
|
}
|
|
772
895
|
catch (killError) {
|
|
773
896
|
console.error("Error attempting to kill slicer process:", killError);
|
|
774
|
-
reject(new Error("Slicing operation cancelled, but failed to kill process."));
|
|
897
|
+
settle(() => reject(new Error("Slicing operation cancelled, but failed to kill process.")));
|
|
775
898
|
}
|
|
776
899
|
}
|
|
777
900
|
}, 500);
|
|
@@ -782,6 +905,16 @@ export class STLManipulator extends EventEmitter {
|
|
|
782
905
|
// This check might be redundant if the promise rejected on kill, but good safety
|
|
783
906
|
throw new Error("Slicing operation cancelled after process finished");
|
|
784
907
|
}
|
|
908
|
+
if (slicerType === 'orcaslicer') {
|
|
909
|
+
if (!orcaOutputDir) {
|
|
910
|
+
throw new Error("Internal error: OrcaSlicer output directory was not initialized.");
|
|
911
|
+
}
|
|
912
|
+
const orcaGcodePath = this.resolveOrcaGcodeOutput(orcaOutputDir);
|
|
913
|
+
if (fs.existsSync(outputFilePath)) {
|
|
914
|
+
fs.unlinkSync(outputFilePath);
|
|
915
|
+
}
|
|
916
|
+
fs.renameSync(orcaGcodePath, outputFilePath);
|
|
917
|
+
}
|
|
785
918
|
if (!fs.existsSync(outputFilePath)) {
|
|
786
919
|
throw new Error(`Slicer finished but output file not found: ${outputFilePath}`);
|
|
787
920
|
}
|
|
@@ -805,6 +938,9 @@ export class STLManipulator extends EventEmitter {
|
|
|
805
938
|
throw error;
|
|
806
939
|
}
|
|
807
940
|
finally {
|
|
941
|
+
if (orcaOutputDir && fs.existsSync(orcaOutputDir)) {
|
|
942
|
+
fs.rmSync(orcaOutputDir, { recursive: true, force: true });
|
|
943
|
+
}
|
|
808
944
|
this.activeOperations.delete(operationId);
|
|
809
945
|
}
|
|
810
946
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-3d-printer-server",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.5",
|
|
4
4
|
"description": "MCP server for connecting Claude with 3D printer management systems",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"repository": "https://github.com/DMontgomery40/mcp-3D-printer-server",
|
|
@@ -15,11 +15,39 @@
|
|
|
15
15
|
"inspect": "npm run build && npx @modelcontextprotocol/inspector dist/index.js"
|
|
16
16
|
},
|
|
17
17
|
"bin": {
|
|
18
|
-
"mcp-3d-printer-server": "./dist/index.js"
|
|
18
|
+
"mcp-3d-printer-server": "./dist/index.js",
|
|
19
|
+
"mcp-3d-printer-server-http": "./dist/http-server.js",
|
|
20
|
+
"mcp-3d-printer-server-sse": "./dist/sse-server.js"
|
|
19
21
|
},
|
|
20
22
|
"files": [
|
|
21
|
-
"dist",
|
|
22
|
-
"
|
|
23
|
+
"dist/3mf_parser.d.ts",
|
|
24
|
+
"dist/3mf_parser.js",
|
|
25
|
+
"dist/fulu-orca.d.ts",
|
|
26
|
+
"dist/fulu-orca.js",
|
|
27
|
+
"dist/http-server.d.ts",
|
|
28
|
+
"dist/http-server.js",
|
|
29
|
+
"dist/index.d.ts",
|
|
30
|
+
"dist/index.js",
|
|
31
|
+
"dist/index.js.map",
|
|
32
|
+
"dist/printers",
|
|
33
|
+
"dist/sse-server.d.ts",
|
|
34
|
+
"dist/sse-server.js",
|
|
35
|
+
"dist/stl",
|
|
36
|
+
"dist/test",
|
|
37
|
+
"dist/types.d.ts",
|
|
38
|
+
"dist/types.js",
|
|
39
|
+
"src/3mf_parser.ts",
|
|
40
|
+
"src/fulu-orca.ts",
|
|
41
|
+
"src/http-server.ts",
|
|
42
|
+
"src/index.ts",
|
|
43
|
+
"src/printers",
|
|
44
|
+
"src/sse-server.ts",
|
|
45
|
+
"src/stl",
|
|
46
|
+
"src/test/3DBenchy.3mf",
|
|
47
|
+
"src/test/stl-manipulator-test.js",
|
|
48
|
+
"src/test/stl-manipulator-test.ts",
|
|
49
|
+
"src/test/tsconfig.json",
|
|
50
|
+
"src/types.ts",
|
|
23
51
|
"README.md",
|
|
24
52
|
"LICENSE"
|
|
25
53
|
],
|