mcp-3d-printer-server 0.2.3 → 1.0.0
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/.dockerignore +52 -0
- package/.env.example +1 -1
- package/Dockerfile +31 -0
- package/README.md +267 -29
- package/docker-compose.yml +31 -0
- package/package.json +6 -2
- package/scripts/docker-run.sh +25 -0
- package/src/3mf_parser.ts +223 -0
- package/src/index.ts +239 -10
- package/src/printers/bambu.ts +305 -100
- package/src/stl/stl-manipulator.ts +350 -117
- package/src/test/3DBenchy.3mf +0 -0
- package/src/test/stl-manipulator-test.js +114 -0
- package/src/test/stl-manipulator-test.ts +124 -0
- package/src/test/tsconfig.json +13 -0
- package/src/types.ts +65 -0
- package/test/sample_cube.stl +86 -0
- package/test/sample_cube_extended.stl +172 -0
- package/test/sample_cube_modified.stl +86 -0
- package/test/sample_cube_rotated.stl +86 -0
- package/test/sample_cube_scaled.stl +86 -0
- package/test/sample_cube_translated.stl +86 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import JSZip from 'jszip';
|
|
2
|
+
import { parseStringPromise } from 'xml2js';
|
|
3
|
+
import { ThreeMFData, BambuSlicerConfig, ThreeMFMetadata, ThreeMFObject, ThreeMFBuildItem, AMSFilamentMapping } from './types.js';
|
|
4
|
+
import * as fs from 'fs/promises';
|
|
5
|
+
|
|
6
|
+
// Parses Bambu Studio's JSON config data
|
|
7
|
+
function parseBambuJSONConfig(jsonData: any): Partial<BambuSlicerConfig> {
|
|
8
|
+
const config: Partial<BambuSlicerConfig> = {};
|
|
9
|
+
const amsMapping: AMSFilamentMapping = {};
|
|
10
|
+
|
|
11
|
+
// Map known keys, converting types as needed
|
|
12
|
+
if (jsonData.layer_height !== undefined) config.layer_height = parseFloat(jsonData.layer_height);
|
|
13
|
+
if (jsonData.initial_layer_print_height !== undefined) config.first_layer_height = parseFloat(jsonData.initial_layer_print_height);
|
|
14
|
+
if (jsonData.sparse_infill_density !== undefined) config.sparse_infill_density = parseFloat(jsonData.sparse_infill_density);
|
|
15
|
+
if (jsonData.sparse_infill_pattern !== undefined) config.sparse_infill_pattern = String(jsonData.sparse_infill_pattern);
|
|
16
|
+
if (jsonData.enable_support !== undefined) config.support_enabled = String(jsonData.enable_support) === '1' || String(jsonData.enable_support).toLowerCase() === 'true';
|
|
17
|
+
if (jsonData.support_type !== undefined) config.support_type = String(jsonData.support_type);
|
|
18
|
+
if (jsonData.support_angle !== undefined) config.support_threshold_angle = parseFloat(jsonData.support_angle);
|
|
19
|
+
if (jsonData.raft_layers !== undefined) config.raft_layers = parseInt(String(jsonData.raft_layers), 10);
|
|
20
|
+
if (jsonData.brim_width !== undefined) config.brim_width = parseFloat(jsonData.brim_width);
|
|
21
|
+
if (jsonData.wall_loops !== undefined) config.wall_loops = parseInt(String(jsonData.wall_loops), 10);
|
|
22
|
+
if (jsonData.top_shell_layers !== undefined) config.top_shell_layers = parseInt(String(jsonData.top_shell_layers), 10);
|
|
23
|
+
if (jsonData.bottom_shell_layers !== undefined) config.bottom_shell_layers = parseInt(String(jsonData.bottom_shell_layers), 10);
|
|
24
|
+
|
|
25
|
+
// Temperatures (handle potential arrays)
|
|
26
|
+
if (Array.isArray(jsonData.nozzle_temperature) && jsonData.nozzle_temperature.length > 0) {
|
|
27
|
+
config.nozzle_temperature = jsonData.nozzle_temperature.map((t: string | number) => parseFloat(String(t)));
|
|
28
|
+
} else if (jsonData.nozzle_temperature !== undefined) {
|
|
29
|
+
config.nozzle_temperature = [parseFloat(String(jsonData.nozzle_temperature))];
|
|
30
|
+
}
|
|
31
|
+
// Bed Temp (prefer initial layer temp)
|
|
32
|
+
let bedTempKey = jsonData.hot_plate_temp_initial_layer !== undefined ? 'hot_plate_temp_initial_layer' : 'hot_plate_temp';
|
|
33
|
+
if (jsonData[bedTempKey] !== undefined) {
|
|
34
|
+
if (Array.isArray(jsonData[bedTempKey]) && jsonData[bedTempKey].length > 0) {
|
|
35
|
+
config.bed_temperature = parseFloat(String(jsonData[bedTempKey][0]));
|
|
36
|
+
} else {
|
|
37
|
+
config.bed_temperature = parseFloat(String(jsonData[bedTempKey]));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Filaments and Flow (handle potential arrays)
|
|
42
|
+
if (Array.isArray(jsonData.filament_type) && jsonData.filament_type.length > 0) {
|
|
43
|
+
config.filament_type = jsonData.filament_type.map(String);
|
|
44
|
+
}
|
|
45
|
+
if (Array.isArray(jsonData.filament_flow_ratio) && jsonData.filament_flow_ratio.length > 0) {
|
|
46
|
+
config.flow_ratio = jsonData.filament_flow_ratio.map((f: string | number) => parseFloat(String(f)));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// AMS Mapping (derive from filament_settings_id order)
|
|
50
|
+
if (Array.isArray(jsonData.filament_settings_id) && jsonData.filament_settings_id.length > 0) {
|
|
51
|
+
jsonData.filament_settings_id.forEach((filamentId: string, index: number) => {
|
|
52
|
+
// Map filament ID (e.g., "Generic PLA @BBL P1P") to its index (assumed AMS slot)
|
|
53
|
+
if (filamentId) { // Ensure filamentId is not empty
|
|
54
|
+
amsMapping[filamentId] = index;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
if (Object.keys(amsMapping).length > 0) {
|
|
58
|
+
config.ams_mapping = amsMapping;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Store remaining keys found in the config
|
|
63
|
+
for (const key in jsonData) {
|
|
64
|
+
if (!config.hasOwnProperty(key)) {
|
|
65
|
+
// Simple assignment for remaining keys, preserving original type if possible
|
|
66
|
+
config[key] = jsonData[key];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return config;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function parseBambuConfig(zip: JSZip): Promise<Partial<BambuSlicerConfig>> {
|
|
74
|
+
// Look for common Bambu config file names
|
|
75
|
+
const potentialFiles = ['Metadata/project_settings.config', 'Metadata/model_settings.config', 'Metadata/slice_info.config'];
|
|
76
|
+
let configFile = null;
|
|
77
|
+
let configContent = '';
|
|
78
|
+
|
|
79
|
+
for (const name of potentialFiles) {
|
|
80
|
+
const file = zip.file(name);
|
|
81
|
+
if (file) {
|
|
82
|
+
configFile = file;
|
|
83
|
+
configContent = await file.async('string');
|
|
84
|
+
console.log(`Found Bambu config file: ${configFile.name}`);
|
|
85
|
+
break; // Use the first one found (project usually has the most)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (configFile && configContent) {
|
|
90
|
+
try {
|
|
91
|
+
// Attempt to parse as JSON
|
|
92
|
+
const jsonData = JSON.parse(configContent);
|
|
93
|
+
const parsedConfig = parseBambuJSONConfig(jsonData);
|
|
94
|
+
console.log('Bambu config parsed successfully as JSON.');
|
|
95
|
+
return parsedConfig;
|
|
96
|
+
} catch (jsonError: any) {
|
|
97
|
+
console.warn(`Failed to parse ${configFile.name} as JSON: ${jsonError.message}. Attempting INI parse as fallback...`);
|
|
98
|
+
// Fallback: Try parsing as INI if JSON fails (though unlikely based on sample)
|
|
99
|
+
try {
|
|
100
|
+
const parsedConfig = parseFallbackINIConfig(configContent); // Keep a simple INI parser as fallback
|
|
101
|
+
console.log('Bambu config parsed successfully as INI (fallback).');
|
|
102
|
+
return parsedConfig;
|
|
103
|
+
} catch (iniError: any) {
|
|
104
|
+
console.error(`Error parsing Bambu config ${configFile.name} as INI (fallback):`, iniError);
|
|
105
|
+
return {};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
console.log('No Bambu-specific config file found in Metadata directory.');
|
|
110
|
+
return {};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Fallback INI parser (Simplified version of the previous one)
|
|
115
|
+
function parseFallbackINIConfig(configContent: string): Partial<BambuSlicerConfig> {
|
|
116
|
+
const config: Partial<BambuSlicerConfig> = {};
|
|
117
|
+
const lines = configContent.split(/\r?\n/);
|
|
118
|
+
for (const line of lines) {
|
|
119
|
+
const trimmedLine = line.trim();
|
|
120
|
+
if (!trimmedLine || trimmedLine.startsWith('#') || trimmedLine.startsWith(';')) continue;
|
|
121
|
+
const equalsIndex = trimmedLine.indexOf('=');
|
|
122
|
+
if (equalsIndex === -1) continue;
|
|
123
|
+
const key = trimmedLine.substring(0, equalsIndex).trim();
|
|
124
|
+
const value = trimmedLine.substring(equalsIndex + 1).trim();
|
|
125
|
+
// Simple assignment - no type conversion here for fallback
|
|
126
|
+
config[key] = value;
|
|
127
|
+
}
|
|
128
|
+
return config;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function parse3DModelConfig(zip: JSZip): Promise<{ metadata: ThreeMFMetadata, objects: ThreeMFObject[], build: { items: ThreeMFBuildItem[] } }> {
|
|
132
|
+
const modelFile = zip.file('3D/3dmodel.model');
|
|
133
|
+
if (!modelFile) {
|
|
134
|
+
throw new Error('3D/3dmodel.model not found in 3MF archive');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const modelContent = await modelFile.async('string');
|
|
138
|
+
try {
|
|
139
|
+
const parsedXml = await parseStringPromise(modelContent, {
|
|
140
|
+
explicitArray: false,
|
|
141
|
+
mergeAttrs: true,
|
|
142
|
+
charkey: 'value'
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const modelData = parsedXml.model;
|
|
146
|
+
if (!modelData) {
|
|
147
|
+
throw new Error('Invalid 3dmodel.model format: <model> tag not found.');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const metadata: ThreeMFMetadata = { unit: modelData.unit || 'millimeter' };
|
|
151
|
+
if (modelData.metadata) {
|
|
152
|
+
const metas = Array.isArray(modelData.metadata) ? modelData.metadata : [modelData.metadata];
|
|
153
|
+
metas.forEach((meta: any) => {
|
|
154
|
+
if (meta.name && meta.value !== undefined) {
|
|
155
|
+
metadata[meta.name] = String(meta.value);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const objects: ThreeMFObject[] = [];
|
|
161
|
+
if (modelData.resources?.object) {
|
|
162
|
+
const resources = Array.isArray(modelData.resources.object) ? modelData.resources.object : [modelData.resources.object];
|
|
163
|
+
resources.forEach((objResource: any) => {
|
|
164
|
+
objects.push({
|
|
165
|
+
id: objResource.id,
|
|
166
|
+
name: objResource.name,
|
|
167
|
+
type: objResource.type || 'model'
|
|
168
|
+
// Actual mesh data (vertices, triangles) is nested deeper if needed
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const buildItems: ThreeMFBuildItem[] = [];
|
|
174
|
+
if (modelData.build?.item) {
|
|
175
|
+
const items = Array.isArray(modelData.build.item) ? modelData.build.item : [modelData.build.item];
|
|
176
|
+
items.forEach((item: any) => {
|
|
177
|
+
buildItems.push({
|
|
178
|
+
objectId: item.objectid,
|
|
179
|
+
transform: item.transform
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
console.log('3dmodel.model parsed successfully.');
|
|
185
|
+
return { metadata, objects, build: { items: buildItems } };
|
|
186
|
+
|
|
187
|
+
} catch (error: any) {
|
|
188
|
+
console.error('Error parsing 3dmodel.model XML:', error);
|
|
189
|
+
throw new Error(`Failed to parse 3dmodel.model: ${error.message}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export { ThreeMFData };
|
|
194
|
+
|
|
195
|
+
export async function parse3MF(filePath: string): Promise<ThreeMFData> {
|
|
196
|
+
console.log(`Parsing 3MF file: ${filePath}`);
|
|
197
|
+
try {
|
|
198
|
+
const data = await fs.readFile(filePath);
|
|
199
|
+
const zip = await JSZip.loadAsync(data);
|
|
200
|
+
|
|
201
|
+
// List files for debugging
|
|
202
|
+
// const fileList = Object.keys(zip.files);
|
|
203
|
+
// console.log('Files in 3MF:', fileList);
|
|
204
|
+
|
|
205
|
+
const { metadata, objects, build } = await parse3DModelConfig(zip);
|
|
206
|
+
const bambuConfig = await parseBambuConfig(zip);
|
|
207
|
+
|
|
208
|
+
// Combine data into the final structure
|
|
209
|
+
const combinedData: ThreeMFData = {
|
|
210
|
+
metadata,
|
|
211
|
+
objects,
|
|
212
|
+
build,
|
|
213
|
+
slicerConfig: bambuConfig
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
console.log('3MF parsing completed.');
|
|
217
|
+
return combinedData;
|
|
218
|
+
|
|
219
|
+
} catch (error: any) {
|
|
220
|
+
console.error(`Error parsing 3MF file ${filePath}:`, error);
|
|
221
|
+
throw new Error(`Failed to parse 3MF file: ${error.message}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -17,6 +17,8 @@ import path from "path";
|
|
|
17
17
|
import * as THREE from 'three';
|
|
18
18
|
import { PrinterFactory } from "./printers/printer-factory.js";
|
|
19
19
|
import { STLManipulator } from "./stl/stl-manipulator.js";
|
|
20
|
+
import { parse3MF, ThreeMFData } from './3mf_parser.js';
|
|
21
|
+
import { BambuImplementation } from "./printers/bambu.js";
|
|
20
22
|
|
|
21
23
|
// Load environment variables from .env file
|
|
22
24
|
dotenv.config();
|
|
@@ -238,7 +240,7 @@ class ThreeDPrinterMCPServer {
|
|
|
238
240
|
},
|
|
239
241
|
slicer_type: {
|
|
240
242
|
type: "string",
|
|
241
|
-
description: "Type of slicer to use (prusaslicer, cura, slic3r) (default: value from env)"
|
|
243
|
+
description: "Type of slicer to use (prusaslicer, cura, slic3r, orcaslicer) (default: value from env)"
|
|
242
244
|
},
|
|
243
245
|
slicer_path: {
|
|
244
246
|
type: "string",
|
|
@@ -495,8 +497,87 @@ class ThreeDPrinterMCPServer {
|
|
|
495
497
|
required: ["stl_path"]
|
|
496
498
|
}
|
|
497
499
|
},
|
|
498
|
-
|
|
499
|
-
|
|
500
|
+
{
|
|
501
|
+
name: "print_3mf",
|
|
502
|
+
description: "Print a 3MF file on a Bambu Lab printer, potentially overriding settings.",
|
|
503
|
+
inputSchema: {
|
|
504
|
+
type: "object",
|
|
505
|
+
properties: {
|
|
506
|
+
three_mf_path: {
|
|
507
|
+
type: "string",
|
|
508
|
+
description: "Path to the 3MF file to print."
|
|
509
|
+
},
|
|
510
|
+
host: {
|
|
511
|
+
type: "string",
|
|
512
|
+
description: "Hostname or IP address of the Bambu printer (default: value from env)"
|
|
513
|
+
},
|
|
514
|
+
bambu_serial: {
|
|
515
|
+
type: "string",
|
|
516
|
+
description: "Serial number for the Bambu Lab printer (default: value from env)"
|
|
517
|
+
},
|
|
518
|
+
bambu_token: {
|
|
519
|
+
type: "string",
|
|
520
|
+
description: "Access token for the Bambu Lab printer (default: value from env)"
|
|
521
|
+
},
|
|
522
|
+
layer_height: { type: "number", description: "Override layer height (mm)." },
|
|
523
|
+
nozzle_temperature: { type: "number", description: "Override nozzle temperature (°C)." },
|
|
524
|
+
bed_temperature: { type: "number", description: "Override bed temperature (°C)." },
|
|
525
|
+
support_enabled: { type: "boolean", description: "Override support generation." },
|
|
526
|
+
ams_mapping: {
|
|
527
|
+
type: "object",
|
|
528
|
+
description: "Override AMS filament mapping (e.g., {\"Generic PLA\": 0, \"Generic PETG\": 1}).",
|
|
529
|
+
additionalProperties: { type: "number" }
|
|
530
|
+
}
|
|
531
|
+
},
|
|
532
|
+
required: ["three_mf_path"]
|
|
533
|
+
}
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
name: "merge_vertices",
|
|
537
|
+
description: "Merge vertices in an STL file that are closer than the specified tolerance.",
|
|
538
|
+
inputSchema: {
|
|
539
|
+
type: "object",
|
|
540
|
+
properties: {
|
|
541
|
+
stl_path: {
|
|
542
|
+
type: "string",
|
|
543
|
+
description: "Path to the STL file to modify."
|
|
544
|
+
},
|
|
545
|
+
tolerance: {
|
|
546
|
+
type: "number",
|
|
547
|
+
description: "Maximum distance between vertices to merge (in mm, default: 0.01)."
|
|
548
|
+
}
|
|
549
|
+
},
|
|
550
|
+
required: ["stl_path"]
|
|
551
|
+
}
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
name: "center_model",
|
|
555
|
+
description: "Translate the model so its geometric center is at the origin (0,0,0).",
|
|
556
|
+
inputSchema: {
|
|
557
|
+
type: "object",
|
|
558
|
+
properties: {
|
|
559
|
+
stl_path: {
|
|
560
|
+
type: "string",
|
|
561
|
+
description: "Path to the STL file to center."
|
|
562
|
+
}
|
|
563
|
+
},
|
|
564
|
+
required: ["stl_path"]
|
|
565
|
+
}
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
name: "lay_flat",
|
|
569
|
+
description: "Attempt to rotate the model so its largest flat face lies on the XY plane (Z=0).",
|
|
570
|
+
inputSchema: {
|
|
571
|
+
type: "object",
|
|
572
|
+
properties: {
|
|
573
|
+
stl_path: {
|
|
574
|
+
type: "string",
|
|
575
|
+
description: "Path to the STL file to lay flat."
|
|
576
|
+
}
|
|
577
|
+
},
|
|
578
|
+
required: ["stl_path"]
|
|
579
|
+
}
|
|
580
|
+
}
|
|
500
581
|
]
|
|
501
582
|
};
|
|
502
583
|
});
|
|
@@ -847,6 +928,144 @@ class ThreeDPrinterMCPServer {
|
|
|
847
928
|
);
|
|
848
929
|
break;
|
|
849
930
|
|
|
931
|
+
case "print_3mf":
|
|
932
|
+
if (!args?.three_mf_path) {
|
|
933
|
+
throw new Error("Missing required parameter: three_mf_path");
|
|
934
|
+
}
|
|
935
|
+
if (type.toLowerCase() !== 'bambu') {
|
|
936
|
+
throw new Error("The print_3mf tool currently only supports Bambu printers.");
|
|
937
|
+
}
|
|
938
|
+
if (!bambuSerial || !bambuToken) {
|
|
939
|
+
throw new Error("Bambu serial number and access token are required for print_3mf.");
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
const threeMFPath = String(args.three_mf_path);
|
|
943
|
+
|
|
944
|
+
// Define variables needed outside the parse try block
|
|
945
|
+
let implementation: BambuImplementation;
|
|
946
|
+
let threeMfFilename: string;
|
|
947
|
+
let projectName: string;
|
|
948
|
+
let finalAmsMapping: number[] | undefined;
|
|
949
|
+
let useAMS: boolean;
|
|
950
|
+
let printOptions: any; // Use a more specific type later if possible
|
|
951
|
+
|
|
952
|
+
try {
|
|
953
|
+
// --- Parse 3MF ---
|
|
954
|
+
const parsed3MFData = await parse3MF(threeMFPath);
|
|
955
|
+
console.log(`Successfully parsed 3MF file: ${threeMFPath}`);
|
|
956
|
+
let parsedAmsMapping: number[] | undefined = undefined;
|
|
957
|
+
// ... (Extract default AMS mapping logic) ...
|
|
958
|
+
if (parsed3MFData.slicerConfig?.ams_mapping) {
|
|
959
|
+
const slots = Object.values(parsed3MFData.slicerConfig.ams_mapping)
|
|
960
|
+
.filter(v => typeof v === 'number') as number[];
|
|
961
|
+
if (slots.length > 0) {
|
|
962
|
+
parsedAmsMapping = slots.sort((a, b) => a - b);
|
|
963
|
+
console.log("Extracted default AMS mapping from 3MF:", parsedAmsMapping);
|
|
964
|
+
} else {
|
|
965
|
+
console.log("AMS mapping found in 3MF, but no valid slots extracted.");
|
|
966
|
+
}
|
|
967
|
+
} else {
|
|
968
|
+
console.log("No default AMS mapping found in 3MF slicer config.");
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// --- Gather Overrides and Determine Final Options ---
|
|
972
|
+
finalAmsMapping = parsedAmsMapping; // Start with parsed
|
|
973
|
+
useAMS = args?.use_ams !== undefined ? Boolean(args.use_ams) : (!!finalAmsMapping && finalAmsMapping.length > 0);
|
|
974
|
+
// ... (Process user AMS mapping override logic) ...
|
|
975
|
+
if (args?.ams_mapping) {
|
|
976
|
+
let userMappingOverride: number[] | undefined = undefined;
|
|
977
|
+
if (Array.isArray(args.ams_mapping)) {
|
|
978
|
+
userMappingOverride = args.ams_mapping.filter(v => typeof v === 'number');
|
|
979
|
+
} else if (typeof args.ams_mapping === 'object') {
|
|
980
|
+
userMappingOverride = Object.values(args.ams_mapping)
|
|
981
|
+
.filter(v => typeof v === 'number')
|
|
982
|
+
.sort((a, b) => a - b) as number[];
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
if (userMappingOverride && userMappingOverride.length > 0) {
|
|
986
|
+
console.log("Applying user AMS mapping override:", userMappingOverride);
|
|
987
|
+
finalAmsMapping = userMappingOverride;
|
|
988
|
+
useAMS = true; // Force useAMS if override provided
|
|
989
|
+
} else {
|
|
990
|
+
console.warn("Received ams_mapping override, but it was empty or invalid.");
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
// ... (Handle explicit use_ams=false) ...
|
|
994
|
+
if (args?.use_ams === false) {
|
|
995
|
+
console.log("User explicitly disabled AMS.");
|
|
996
|
+
finalAmsMapping = undefined;
|
|
997
|
+
useAMS = false;
|
|
998
|
+
}
|
|
999
|
+
if (!finalAmsMapping || finalAmsMapping.length === 0) {
|
|
1000
|
+
useAMS = false;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// --- Prepare Implementation and Print Options ---
|
|
1004
|
+
const factoryImplementation = this.printerFactory.getImplementation('bambu');
|
|
1005
|
+
if (!(factoryImplementation instanceof BambuImplementation)) {
|
|
1006
|
+
throw new Error("Internal error: Could not get Bambu printer implementation.");
|
|
1007
|
+
}
|
|
1008
|
+
implementation = factoryImplementation; // Assign to outer scope variable
|
|
1009
|
+
|
|
1010
|
+
threeMfFilename = path.basename(threeMFPath); // Assign to outer scope variable
|
|
1011
|
+
projectName = threeMfFilename.replace(/\.3mf$/i, ''); // Assign to outer scope variable
|
|
1012
|
+
|
|
1013
|
+
printOptions = { // Assign to outer scope variable
|
|
1014
|
+
useAMS: useAMS,
|
|
1015
|
+
amsMapping: finalAmsMapping,
|
|
1016
|
+
bedLeveling: args?.bed_leveling !== undefined ? Boolean(args.bed_leveling) : undefined,
|
|
1017
|
+
flowCalibration: args?.flow_calibration !== undefined ? Boolean(args.flow_calibration) : undefined,
|
|
1018
|
+
vibrationCalibration: args?.vibration_calibration !== undefined ? Boolean(args.vibration_calibration) : undefined,
|
|
1019
|
+
layerInspect: args?.layer_inspect !== undefined ? Boolean(args.layer_inspect) : undefined,
|
|
1020
|
+
timelapse: args?.timelapse !== undefined ? Boolean(args.timelapse) : undefined,
|
|
1021
|
+
// md5: parsed3MFData?.metadata?.md5
|
|
1022
|
+
};
|
|
1023
|
+
|
|
1024
|
+
} catch (error) { // Catch parsing or setup errors
|
|
1025
|
+
console.error(`Error processing 3MF or setting up print:`, error);
|
|
1026
|
+
throw new Error(`Failed during 3MF processing: ${(error as Error).message}`);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// --- Call Implementation (Now variables are in scope) ---
|
|
1030
|
+
try {
|
|
1031
|
+
result = await implementation.print3mf(host, bambuSerial, bambuToken, {
|
|
1032
|
+
projectName: projectName,
|
|
1033
|
+
filePath: threeMFPath,
|
|
1034
|
+
plateIndex: 0,
|
|
1035
|
+
...printOptions // Spread the final options
|
|
1036
|
+
});
|
|
1037
|
+
result = `Print command for ${threeMfFilename} sent successfully.`;
|
|
1038
|
+
} catch (printError) {
|
|
1039
|
+
console.error(`Error starting 3MF print for ${threeMfFilename}:`, printError);
|
|
1040
|
+
throw new Error(`Failed to start print: ${(printError as Error).message}`);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
break;
|
|
1044
|
+
|
|
1045
|
+
case "merge_vertices":
|
|
1046
|
+
if (!args?.stl_path) {
|
|
1047
|
+
throw new Error("Missing required parameter: stl_path");
|
|
1048
|
+
}
|
|
1049
|
+
result = await this.stlManipulator.mergeVertices(
|
|
1050
|
+
String(args.stl_path),
|
|
1051
|
+
args.tolerance !== undefined ? Number(args.tolerance) : undefined // Pass tolerance if provided
|
|
1052
|
+
);
|
|
1053
|
+
break;
|
|
1054
|
+
|
|
1055
|
+
case "center_model":
|
|
1056
|
+
if (!args?.stl_path) {
|
|
1057
|
+
throw new Error("Missing required parameter: stl_path");
|
|
1058
|
+
}
|
|
1059
|
+
result = await this.stlManipulator.centerModel(String(args.stl_path));
|
|
1060
|
+
break;
|
|
1061
|
+
|
|
1062
|
+
case "lay_flat":
|
|
1063
|
+
if (!args?.stl_path) {
|
|
1064
|
+
throw new Error("Missing required parameter: stl_path");
|
|
1065
|
+
}
|
|
1066
|
+
result = await this.stlManipulator.layFlat(String(args.stl_path));
|
|
1067
|
+
break;
|
|
1068
|
+
|
|
850
1069
|
default:
|
|
851
1070
|
throw new Error(`Unknown tool: ${name}`);
|
|
852
1071
|
}
|
|
@@ -974,17 +1193,20 @@ class ThreeDPrinterMCPServer {
|
|
|
974
1193
|
apiKey: string,
|
|
975
1194
|
bambuSerial: string,
|
|
976
1195
|
bambuToken: string,
|
|
977
|
-
|
|
1196
|
+
gcodeFilename: string
|
|
978
1197
|
) {
|
|
979
1198
|
const implementation = this.printerFactory.getImplementation(type);
|
|
980
1199
|
|
|
981
1200
|
if (type.toLowerCase() === "bambu") {
|
|
1201
|
+
// Bambu startJob likely uses G-code filename, not 3MF.
|
|
1202
|
+
// Keep this as is for starting pre-sliced G-code files.
|
|
1203
|
+
// The print_3mf tool handles starting 3MF prints.
|
|
982
1204
|
return await (implementation as any).startJob(
|
|
983
|
-
host, port, apiKey, bambuSerial, bambuToken,
|
|
1205
|
+
host, port, apiKey, bambuSerial, bambuToken, gcodeFilename
|
|
984
1206
|
);
|
|
985
1207
|
}
|
|
986
1208
|
|
|
987
|
-
return await implementation.startJob(host, port, apiKey,
|
|
1209
|
+
return await implementation.startJob(host, port, apiKey, gcodeFilename);
|
|
988
1210
|
}
|
|
989
1211
|
|
|
990
1212
|
async cancelPrint(
|
|
@@ -1019,12 +1241,19 @@ class ThreeDPrinterMCPServer {
|
|
|
1019
1241
|
const implementation = this.printerFactory.getImplementation(type);
|
|
1020
1242
|
|
|
1021
1243
|
if (type.toLowerCase() === "bambu") {
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
)
|
|
1244
|
+
const bambuImplementation = this.printerFactory.getImplementation('bambu');
|
|
1245
|
+
|
|
1246
|
+
if (bambuImplementation instanceof BambuImplementation && typeof bambuImplementation.setTemperature === 'function') {
|
|
1247
|
+
return await bambuImplementation.setTemperature(
|
|
1248
|
+
host, bambuSerial, bambuToken, component, temperature
|
|
1249
|
+
);
|
|
1250
|
+
} else {
|
|
1251
|
+
console.warn('setTemperature not fully implemented for Bambu via direct commands yet.');
|
|
1252
|
+
return { status: 'Command sent (implementation pending)'}; // Avoid throwing error if method doesn't exist
|
|
1253
|
+
}
|
|
1025
1254
|
}
|
|
1026
1255
|
|
|
1027
|
-
return
|
|
1256
|
+
return implementation.setTemperature(host, port, apiKey, component, temperature);
|
|
1028
1257
|
}
|
|
1029
1258
|
|
|
1030
1259
|
async run() {
|