Skip to main content
Glama

MCP 3D Printer Server

index.js56 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { ListResourcesRequestSchema, ReadResourceRequestSchema, ListToolsRequestSchema, CallToolRequestSchema, ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js"; import axios from "axios"; import dotenv from "dotenv"; import fs from "fs"; import path from "path"; import * as THREE from 'three'; import { PrinterFactory } from "./printers/printer-factory.js"; import { STLManipulator } from "./stl/stl-manipulator.js"; import { parse3MF } from './3mf_parser.js'; import { BambuImplementation } from "./printers/bambu.js"; // Load environment variables from .env file dotenv.config(); // Default values const DEFAULT_HOST = process.env.PRINTER_HOST || "localhost"; const DEFAULT_PORT = process.env.PRINTER_PORT || "80"; const DEFAULT_API_KEY = process.env.API_KEY || ""; const DEFAULT_TYPE = process.env.PRINTER_TYPE || "octoprint"; // Default to OctoPrint const TEMP_DIR = process.env.TEMP_DIR || path.join(process.cwd(), "temp"); // Slicer configuration const DEFAULT_SLICER_TYPE = process.env.SLICER_TYPE || "prusaslicer"; const DEFAULT_SLICER_PATH = process.env.SLICER_PATH || ""; const DEFAULT_SLICER_PROFILE = process.env.SLICER_PROFILE || ""; // Bambu-specific default values const DEFAULT_BAMBU_SERIAL = process.env.BAMBU_SERIAL || ""; const DEFAULT_BAMBU_TOKEN = process.env.BAMBU_TOKEN || ""; // Ensure temp directory exists if (!fs.existsSync(TEMP_DIR)) { fs.mkdirSync(TEMP_DIR, { recursive: true }); } class ThreeDPrinterMCPServer { constructor() { this.server = new Server({ name: "mcp-3d-printer-server", version: "1.0.0" }, { capabilities: { resources: {}, tools: {} } }); this.printerFactory = new PrinterFactory(); this.stlManipulator = new STLManipulator(TEMP_DIR); this.setupHandlers(); this.setupErrorHandling(); } setupErrorHandling() { this.server.onerror = (error) => { console.error("[MCP Error]", error); }; process.on("SIGINT", async () => { // Disconnect all printers await this.printerFactory.disconnectAll(); await this.server.close(); process.exit(0); }); } setupHandlers() { this.setupResourceHandlers(); this.setupToolHandlers(); } setupResourceHandlers() { // List available resources this.server.setRequestHandler(ListResourcesRequestSchema, async () => { return { resources: [ { uri: `printer://${DEFAULT_HOST}/status`, name: "3D Printer Status", mimeType: "application/json", description: "Current status of the 3D printer including temperatures, print progress, and more" }, { uri: `printer://${DEFAULT_HOST}/files`, name: "3D Printer Files", mimeType: "application/json", description: "List of files available on the 3D printer" } ], templates: [ { uriTemplate: "printer://{host}/status", name: "3D Printer Status", mimeType: "application/json" }, { uriTemplate: "printer://{host}/files", name: "3D Printer Files", mimeType: "application/json" }, { uriTemplate: "printer://{host}/file/{filename}", name: "3D Printer File Content", mimeType: "application/gcode" } ] }; }); // Read resource this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const uri = request.params.uri; const match = uri.match(/^printer:\/\/([^\/]+)\/(.+)$/); if (!match) { throw new McpError(ErrorCode.InvalidRequest, `Invalid resource URI: ${uri}`); } const [, host, resource] = match; let content; try { if (resource === "status") { content = await this.getPrinterStatus(host); } else if (resource === "files") { content = await this.getPrinterFiles(host); } else if (resource.startsWith("file/")) { const filename = resource.substring(5); content = await this.getPrinterFile(host, filename); } else { throw new McpError(ErrorCode.InvalidRequest, `Unknown resource: ${resource}`); } return { contents: [ { uri, mimeType: resource.startsWith("file/") ? "application/gcode" : "application/json", text: typeof content === "string" ? content : JSON.stringify(content, null, 2) } ] }; } catch (error) { if (axios.isAxiosError(error)) { throw new McpError(ErrorCode.InternalError, `API error: ${error.response?.data?.error || error.message}`); } throw error; } }); } setupToolHandlers() { // List available tools this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "get_printer_status", description: "Get the current status of the 3D printer", inputSchema: { type: "object", properties: { host: { type: "string", description: "Hostname or IP address of the printer (default: value from env)" }, port: { type: "string", description: "Port of the printer API (default: value from env)" }, type: { type: "string", description: "Type of printer management system (octoprint, klipper, duet, repetier, bambu, prusa, creality) (default: value from env)" }, api_key: { type: "string", description: "API key for authentication (default: value from env)" }, bambu_serial: { type: "string", description: "Serial number for Bambu Lab printers (default: value from env)" }, bambu_token: { type: "string", description: "Access token for Bambu Lab printers (default: value from env)" } } } }, // New STL manipulation tools { name: "extend_stl_base", description: "Extend the base of an STL file by a specified amount", inputSchema: { type: "object", properties: { stl_path: { type: "string", description: "Path to the STL file to modify" }, extension_inches: { type: "number", description: "Amount to extend the base in inches" } }, required: ["stl_path", "extension_inches"] } }, { name: "slice_stl", description: "Slice an STL file to generate G-code", inputSchema: { type: "object", properties: { stl_path: { type: "string", description: "Path to the STL file to slice" }, slicer_type: { type: "string", description: "Type of slicer to use (prusaslicer, cura, slic3r, orcaslicer) (default: value from env)" }, slicer_path: { type: "string", description: "Path to the slicer executable (default: value from env)" }, slicer_profile: { type: "string", description: "Profile to use for slicing (default: value from env)" } }, required: ["stl_path"] } }, { name: "confirm_temperatures", description: "Confirm temperature settings in a G-code file", inputSchema: { type: "object", properties: { gcode_path: { type: "string", description: "Path to the G-code file" }, extruder_temp: { type: "number", description: "Expected extruder temperature" }, bed_temp: { type: "number", description: "Expected bed temperature" } }, required: ["gcode_path"] } }, { name: "process_and_print_stl", description: "Process an STL file (extend base), slice it, confirm temperatures, and start printing", inputSchema: { type: "object", properties: { stl_path: { type: "string", description: "Path to the STL file to process" }, extension_inches: { type: "number", description: "Amount to extend the base in inches" }, extruder_temp: { type: "number", description: "Expected extruder temperature" }, bed_temp: { type: "number", description: "Expected bed temperature" }, host: { type: "string", description: "Hostname or IP address of the printer (default: value from env)" }, port: { type: "string", description: "Port of the printer API (default: value from env)" }, type: { type: "string", description: "Type of printer management system (default: value from env)" }, api_key: { type: "string", description: "API key for authentication (default: value from env)" } }, required: ["stl_path", "extension_inches"] } }, // New STL manipulation tools { name: "get_stl_info", description: "Get detailed information about an STL file", inputSchema: { type: "object", properties: { stl_path: { type: "string", description: "Path to the STL file" } }, required: ["stl_path"] } }, { name: "scale_stl", description: "Scale an STL model uniformly or along specific axes", inputSchema: { type: "object", properties: { stl_path: { type: "string", description: "Path to the STL file" }, scale_factor: { type: "number", description: "Uniform scaling factor to apply" }, scale_x: { type: "number", description: "X-axis scaling factor (overrides scale_factor for X axis)" }, scale_y: { type: "number", description: "Y-axis scaling factor (overrides scale_factor for Y axis)" }, scale_z: { type: "number", description: "Z-axis scaling factor (overrides scale_factor for Z axis)" } }, required: ["stl_path"] } }, { name: "rotate_stl", description: "Rotate an STL model around specific axes", inputSchema: { type: "object", properties: { stl_path: { type: "string", description: "Path to the STL file" }, rotate_x: { type: "number", description: "Rotation around X-axis in degrees" }, rotate_y: { type: "number", description: "Rotation around Y-axis in degrees" }, rotate_z: { type: "number", description: "Rotation around Z-axis in degrees" } }, required: ["stl_path"] } }, { name: "translate_stl", description: "Move an STL model along specific axes", inputSchema: { type: "object", properties: { stl_path: { type: "string", description: "Path to the STL file" }, translate_x: { type: "number", description: "Translation along X-axis in millimeters" }, translate_y: { type: "number", description: "Translation along Y-axis in millimeters" }, translate_z: { type: "number", description: "Translation along Z-axis in millimeters" } }, required: ["stl_path"] } }, { name: "modify_stl_section", description: "Apply a specific transformation to a selected section of an STL file", inputSchema: { type: "object", properties: { stl_path: { type: "string", description: "Path to the STL file" }, section: { type: "string", description: "Section to modify: 'top', 'bottom', 'center', or custom bounds", enum: ["top", "bottom", "center", "custom"] }, transformation_type: { type: "string", description: "Type of transformation to apply", enum: ["scale", "rotate", "translate"] }, value_x: { type: "number", description: "Transformation value for X axis" }, value_y: { type: "number", description: "Transformation value for Y axis" }, value_z: { type: "number", description: "Transformation value for Z axis" }, custom_min_x: { type: "number", description: "Minimum X for custom section bounds" }, custom_min_y: { type: "number", description: "Minimum Y for custom section bounds" }, custom_min_z: { type: "number", description: "Minimum Z for custom section bounds" }, custom_max_x: { type: "number", description: "Maximum X for custom section bounds" }, custom_max_y: { type: "number", description: "Maximum Y for custom section bounds" }, custom_max_z: { type: "number", description: "Maximum Z for custom section bounds" } }, required: ["stl_path", "section", "transformation_type"] } }, { name: "generate_stl_visualization", description: "Generate an SVG visualization of an STL file from multiple angles", inputSchema: { type: "object", properties: { stl_path: { type: "string", description: "Path to the STL file" }, width: { type: "number", description: "Width of each view in pixels (default: 300)" }, height: { type: "number", description: "Height of each view in pixels (default: 300)" } }, required: ["stl_path"] } }, { name: "print_3mf", description: "Print a 3MF file on a Bambu Lab printer, potentially overriding settings.", inputSchema: { type: "object", properties: { three_mf_path: { type: "string", description: "Path to the 3MF file to print." }, host: { type: "string", description: "Hostname or IP address of the Bambu printer (default: value from env)" }, bambu_serial: { type: "string", description: "Serial number for the Bambu Lab printer (default: value from env)" }, bambu_token: { type: "string", description: "Access token for the Bambu Lab printer (default: value from env)" }, layer_height: { type: "number", description: "Override layer height (mm)." }, nozzle_temperature: { type: "number", description: "Override nozzle temperature (°C)." }, bed_temperature: { type: "number", description: "Override bed temperature (°C)." }, support_enabled: { type: "boolean", description: "Override support generation." }, ams_mapping: { type: "object", description: "Override AMS filament mapping (e.g., {\"Generic PLA\": 0, \"Generic PETG\": 1}).", additionalProperties: { type: "number" } } }, required: ["three_mf_path"] } }, { name: "merge_vertices", description: "Merge vertices in an STL file that are closer than the specified tolerance.", inputSchema: { type: "object", properties: { stl_path: { type: "string", description: "Path to the STL file to modify." }, tolerance: { type: "number", description: "Maximum distance between vertices to merge (in mm, default: 0.01)." } }, required: ["stl_path"] } }, { name: "center_model", description: "Translate the model so its geometric center is at the origin (0,0,0).", inputSchema: { type: "object", properties: { stl_path: { type: "string", description: "Path to the STL file to center." } }, required: ["stl_path"] } }, { name: "lay_flat", description: "Attempt to rotate the model so its largest flat face lies on the XY plane (Z=0).", inputSchema: { type: "object", properties: { stl_path: { type: "string", description: "Path to the STL file to lay flat." } }, required: ["stl_path"] } } ] }; }); // Handle tool calls this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; // Set default values for common parameters const host = String(args?.host || DEFAULT_HOST); const port = String(args?.port || DEFAULT_PORT); const type = String(args?.type || DEFAULT_TYPE); const apiKey = String(args?.api_key || DEFAULT_API_KEY); const bambuSerial = String(args?.bambu_serial || DEFAULT_BAMBU_SERIAL); const bambuToken = String(args?.bambu_token || DEFAULT_BAMBU_TOKEN); const slicerType = String(args?.slicer_type || DEFAULT_SLICER_TYPE); const slicerPath = String(args?.slicer_path || DEFAULT_SLICER_PATH); const slicerProfile = String(args?.slicer_profile || DEFAULT_SLICER_PROFILE); try { let result; switch (name) { case "get_printer_status": result = await this.getPrinterStatus(host, port, type, apiKey, bambuSerial, bambuToken); break; case "list_printer_files": result = await this.getPrinterFiles(host, port, type, apiKey, bambuSerial, bambuToken); break; case "upload_gcode": if (!args?.filename || !args?.gcode) { throw new Error("Missing required parameters: filename and gcode"); } result = await this.uploadGcode(host, port, type, apiKey, bambuSerial, bambuToken, String(args.filename), String(args.gcode), Boolean(args.print || false)); break; case "start_print": if (!args?.filename) { throw new Error("Missing required parameter: filename"); } result = await this.startPrint(host, port, type, apiKey, bambuSerial, bambuToken, String(args.filename)); break; case "cancel_print": result = await this.cancelPrint(host, port, type, apiKey, bambuSerial, bambuToken); break; case "set_printer_temperature": if (!args?.component || args?.temperature === undefined) { throw new Error("Missing required parameters: component and temperature"); } result = await this.setPrinterTemperature(host, port, type, apiKey, bambuSerial, bambuToken, String(args.component), Number(args.temperature)); break; // New STL manipulation tools case "extend_stl_base": if (!args?.stl_path || args?.extension_inches === undefined) { throw new Error("Missing required parameters: stl_path and extension_inches"); } result = await this.stlManipulator.extendBase(String(args.stl_path), Number(args.extension_inches)); break; case "slice_stl": if (!args?.stl_path) { throw new Error("Missing required parameter: stl_path"); } result = await this.stlManipulator.sliceSTL(String(args.stl_path), slicerType, slicerPath, slicerProfile || undefined); break; case "confirm_temperatures": if (!args?.gcode_path) { throw new Error("Missing required parameter: gcode_path"); } result = await this.stlManipulator.confirmTemperatures(String(args.gcode_path), { extruder: args.extruder_temp !== undefined ? Number(args.extruder_temp) : undefined, bed: args.bed_temp !== undefined ? Number(args.bed_temp) : undefined }); break; case "process_and_print_stl": if (!args?.stl_path || args?.extension_inches === undefined) { throw new Error("Missing required parameters: stl_path and extension_inches"); } // Define progress callback for UI updates const processProgressCallback = (progress, message) => { console.log(`Process progress: ${progress}% - ${message || ''}`); }; // 1. Extend the base of the STL file const extendedStlPath = await this.stlManipulator.extendBase(String(args.stl_path), Number(args.extension_inches), processProgressCallback); // 2. Slice the extended STL file const gcodePath = await this.stlManipulator.sliceSTL(extendedStlPath, slicerType, slicerPath, slicerProfile || undefined, processProgressCallback); // 3. Confirm temperatures if specified if (args.extruder_temp !== undefined || args.bed_temp !== undefined) { const tempConfirmation = await this.stlManipulator.confirmTemperatures(gcodePath, { extruder: args.extruder_temp !== undefined ? Number(args.extruder_temp) : undefined, bed: args.bed_temp !== undefined ? Number(args.bed_temp) : undefined }, processProgressCallback); if (!tempConfirmation.match) { console.warn("Temperature mismatch:", tempConfirmation); } } // 4. Upload the G-code file to the printer const gcodeContent = await fs.promises.readFile(gcodePath, 'utf8'); const filename = path.basename(gcodePath); await this.uploadGcode(host, port, type, apiKey, bambuSerial, bambuToken, filename, gcodeContent, true // Start printing immediately ); result = { extended_stl_path: extendedStlPath, gcode_path: gcodePath, filename, status: "Print job started" }; break; // New STL manipulation tool handlers case "get_stl_info": if (!args?.stl_path) { throw new Error("Missing required parameter: stl_path"); } result = await this.stlManipulator.getSTLInfo(String(args.stl_path)); break; case "scale_stl": if (!args?.stl_path) { throw new Error("Missing required parameter: stl_path"); } // Define progress callback for UI updates const scaleProgressCallback = (progress, message) => { console.log(`Scale progress: ${progress}% - ${message || ''}`); }; let scaleFactors; // Check if we have individual axis scaling factors if (args.scale_x !== undefined || args.scale_y !== undefined || args.scale_z !== undefined) { // Use individual axis scaling scaleFactors = [ Number(args.scale_x ?? 1.0), Number(args.scale_y ?? 1.0), Number(args.scale_z ?? 1.0) ]; } else { // Use uniform scaling scaleFactors = Number(args.scale_factor ?? 1.0); } result = await this.stlManipulator.scaleSTL(String(args.stl_path), scaleFactors, scaleProgressCallback); break; case "rotate_stl": if (!args?.stl_path) { throw new Error("Missing required parameter: stl_path"); } // Define progress callback for UI updates const rotateProgressCallback = (progress, message) => { console.log(`Rotate progress: ${progress}% - ${message || ''}`); }; // Get rotation angles, defaulting to 0 for any undefined axis const rotationAngles = [ Number(args.rotate_x ?? 0), Number(args.rotate_y ?? 0), Number(args.rotate_z ?? 0) ]; result = await this.stlManipulator.rotateSTL(String(args.stl_path), rotationAngles, rotateProgressCallback); break; case "translate_stl": if (!args?.stl_path) { throw new Error("Missing required parameter: stl_path"); } // Define progress callback for UI updates const translateProgressCallback = (progress, message) => { console.log(`Translate progress: ${progress}% - ${message || ''}`); }; // Get translation values, defaulting to 0 for any undefined axis const translationValues = [ Number(args.translate_x ?? 0), Number(args.translate_y ?? 0), Number(args.translate_z ?? 0) ]; result = await this.stlManipulator.translateSTL(String(args.stl_path), translationValues, translateProgressCallback); break; case "modify_stl_section": if (!args?.stl_path || !args?.section || !args?.transformation_type) { throw new Error("Missing required parameters: stl_path, section, and transformation_type"); } // Define progress callback for UI updates const modifySectionProgressCallback = (progress, message) => { console.log(`Modify section progress: ${progress}% - ${message || ''}`); }; // Determine the section to modify let sectionBox; if (args.section === 'custom') { // Create a custom bounding box from the provided bounds if (args.custom_min_x === undefined || args.custom_min_y === undefined || args.custom_min_z === undefined || args.custom_max_x === undefined || args.custom_max_y === undefined || args.custom_max_z === undefined) { throw new Error("Custom section requires all min/max bounds to be specified"); } sectionBox = new THREE.Box3(new THREE.Vector3(Number(args.custom_min_x), Number(args.custom_min_y), Number(args.custom_min_z)), new THREE.Vector3(Number(args.custom_max_x), Number(args.custom_max_y), Number(args.custom_max_z))); } else { // Use a predefined section sectionBox = String(args.section); } // Determine the transformation to apply const transformationType = String(args.transformation_type); let transformationValue; if (transformationType === 'scale') { if (args.value_x !== undefined || args.value_y !== undefined || args.value_z !== undefined) { transformationValue = [ Number(args.value_x ?? 1.0), Number(args.value_y ?? 1.0), Number(args.value_z ?? 1.0) ]; } else { transformationValue = 1.0; // Default scale factor } } else if (transformationType === 'rotate') { transformationValue = [ Number(args.value_x ?? 0), Number(args.value_y ?? 0), Number(args.value_z ?? 0) ]; } else { // translate transformationValue = [ Number(args.value_x ?? 0), Number(args.value_y ?? 0), Number(args.value_z ?? 0) ]; } result = await this.stlManipulator.modifySection(String(args.stl_path), sectionBox, { type: transformationType, value: transformationValue }, modifySectionProgressCallback); break; case "generate_stl_visualization": if (!args?.stl_path) { throw new Error("Missing required parameter: stl_path"); } // Define progress callback for UI updates const visualizationProgressCallback = (progress, message) => { console.log(`Visualization progress: ${progress}% - ${message || ''}`); }; // Get width and height parameters, with defaults const width = args.width !== undefined ? Number(args.width) : 300; const height = args.height !== undefined ? Number(args.height) : 300; result = await this.stlManipulator.generateVisualization(String(args.stl_path), width, height, visualizationProgressCallback); break; case "print_3mf": if (!args?.three_mf_path) { throw new Error("Missing required parameter: three_mf_path"); } if (type.toLowerCase() !== 'bambu') { throw new Error("The print_3mf tool currently only supports Bambu printers."); } if (!bambuSerial || !bambuToken) { throw new Error("Bambu serial number and access token are required for print_3mf."); } const threeMFPath = String(args.three_mf_path); // Define variables needed outside the parse try block let implementation; let threeMfFilename; let projectName; let finalAmsMapping; let useAMS; let printOptions; // Use a more specific type later if possible try { // --- Parse 3MF --- const parsed3MFData = await parse3MF(threeMFPath); console.log(`Successfully parsed 3MF file: ${threeMFPath}`); let parsedAmsMapping = undefined; // ... (Extract default AMS mapping logic) ... if (parsed3MFData.slicerConfig?.ams_mapping) { const slots = Object.values(parsed3MFData.slicerConfig.ams_mapping) .filter(v => typeof v === 'number'); if (slots.length > 0) { parsedAmsMapping = slots.sort((a, b) => a - b); console.log("Extracted default AMS mapping from 3MF:", parsedAmsMapping); } else { console.log("AMS mapping found in 3MF, but no valid slots extracted."); } } else { console.log("No default AMS mapping found in 3MF slicer config."); } // --- Gather Overrides and Determine Final Options --- finalAmsMapping = parsedAmsMapping; // Start with parsed useAMS = args?.use_ams !== undefined ? Boolean(args.use_ams) : (!!finalAmsMapping && finalAmsMapping.length > 0); // ... (Process user AMS mapping override logic) ... if (args?.ams_mapping) { let userMappingOverride = undefined; if (Array.isArray(args.ams_mapping)) { userMappingOverride = args.ams_mapping.filter(v => typeof v === 'number'); } else if (typeof args.ams_mapping === 'object') { userMappingOverride = Object.values(args.ams_mapping) .filter(v => typeof v === 'number') .sort((a, b) => a - b); } if (userMappingOverride && userMappingOverride.length > 0) { console.log("Applying user AMS mapping override:", userMappingOverride); finalAmsMapping = userMappingOverride; useAMS = true; // Force useAMS if override provided } else { console.warn("Received ams_mapping override, but it was empty or invalid."); } } // ... (Handle explicit use_ams=false) ... if (args?.use_ams === false) { console.log("User explicitly disabled AMS."); finalAmsMapping = undefined; useAMS = false; } if (!finalAmsMapping || finalAmsMapping.length === 0) { useAMS = false; } // --- Prepare Implementation and Print Options --- const factoryImplementation = this.printerFactory.getImplementation('bambu'); if (!(factoryImplementation instanceof BambuImplementation)) { throw new Error("Internal error: Could not get Bambu printer implementation."); } implementation = factoryImplementation; // Assign to outer scope variable threeMfFilename = path.basename(threeMFPath); // Assign to outer scope variable projectName = threeMfFilename.replace(/\.3mf$/i, ''); // Assign to outer scope variable printOptions = { useAMS: useAMS, amsMapping: finalAmsMapping, bedLeveling: args?.bed_leveling !== undefined ? Boolean(args.bed_leveling) : undefined, flowCalibration: args?.flow_calibration !== undefined ? Boolean(args.flow_calibration) : undefined, vibrationCalibration: args?.vibration_calibration !== undefined ? Boolean(args.vibration_calibration) : undefined, layerInspect: args?.layer_inspect !== undefined ? Boolean(args.layer_inspect) : undefined, timelapse: args?.timelapse !== undefined ? Boolean(args.timelapse) : undefined, // md5: parsed3MFData?.metadata?.md5 }; } catch (error) { // Catch parsing or setup errors console.error(`Error processing 3MF or setting up print:`, error); throw new Error(`Failed during 3MF processing: ${error.message}`); } // --- Call Implementation (Now variables are in scope) --- try { result = await implementation.print3mf(host, bambuSerial, bambuToken, { projectName: projectName, filePath: threeMFPath, plateIndex: 0, ...printOptions // Spread the final options }); result = `Print command for ${threeMfFilename} sent successfully.`; } catch (printError) { console.error(`Error starting 3MF print for ${threeMfFilename}:`, printError); throw new Error(`Failed to start print: ${printError.message}`); } break; case "merge_vertices": if (!args?.stl_path) { throw new Error("Missing required parameter: stl_path"); } result = await this.stlManipulator.mergeVertices(String(args.stl_path), args.tolerance !== undefined ? Number(args.tolerance) : undefined // Pass tolerance if provided ); break; case "center_model": if (!args?.stl_path) { throw new Error("Missing required parameter: stl_path"); } result = await this.stlManipulator.centerModel(String(args.stl_path)); break; case "lay_flat": if (!args?.stl_path) { throw new Error("Missing required parameter: stl_path"); } result = await this.stlManipulator.layFlat(String(args.stl_path)); break; default: throw new Error(`Unknown tool: ${name}`); } return { content: [ { type: "text", text: typeof result === "string" ? result : JSON.stringify(result, null, 2) } ] }; } catch (error) { console.error(`Error calling tool ${name}:`, error); const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: "text", text: `Error: ${errorMessage}` } ], isError: true }; } }); } // Delegating methods to printer implementations async getPrinterStatus(host, port = DEFAULT_PORT, type = DEFAULT_TYPE, apiKey = DEFAULT_API_KEY, bambuSerial = DEFAULT_BAMBU_SERIAL, bambuToken = DEFAULT_BAMBU_TOKEN) { const implementation = this.printerFactory.getImplementation(type); if (type.toLowerCase() === "bambu") { const bambuApiKey = `${bambuSerial}:${bambuToken}`; return implementation.getStatus(host, port, bambuApiKey); } return implementation.getStatus(host, port, apiKey); } async getPrinterFiles(host, port = DEFAULT_PORT, type = DEFAULT_TYPE, apiKey = DEFAULT_API_KEY, bambuSerial = DEFAULT_BAMBU_SERIAL, bambuToken = DEFAULT_BAMBU_TOKEN) { const implementation = this.printerFactory.getImplementation(type); if (type.toLowerCase() === "bambu") { const bambuApiKey = `${bambuSerial}:${bambuToken}`; return implementation.getFiles(host, port, bambuApiKey); } return implementation.getFiles(host, port, apiKey); } async getPrinterFile(host, filename, port = DEFAULT_PORT, type = DEFAULT_TYPE, apiKey = DEFAULT_API_KEY, bambuSerial = DEFAULT_BAMBU_SERIAL, bambuToken = DEFAULT_BAMBU_TOKEN) { const implementation = this.printerFactory.getImplementation(type); if (type.toLowerCase() === "bambu") { return implementation.getFile(host, port, apiKey, bambuSerial, bambuToken, filename); } return implementation.getFile(host, port, apiKey, filename); } async uploadGcode(host, port, type, apiKey, bambuSerial, bambuToken, filename, gcode, print) { const tempFilePath = path.join(TEMP_DIR, filename); // Write gcode to temporary file fs.writeFileSync(tempFilePath, gcode); try { const implementation = this.printerFactory.getImplementation(type); if (type.toLowerCase() === "bambu") { return await implementation.uploadFile(host, port, apiKey, bambuSerial, bambuToken, tempFilePath, filename, print); } return await implementation.uploadFile(host, port, apiKey, tempFilePath, filename, print); } finally { // Clean up temporary file if (fs.existsSync(tempFilePath)) { fs.unlinkSync(tempFilePath); } } } async startPrint(host, port, type, apiKey, bambuSerial, bambuToken, gcodeFilename) { const implementation = this.printerFactory.getImplementation(type); if (type.toLowerCase() === "bambu") { // Bambu startJob likely uses G-code filename, not 3MF. // Keep this as is for starting pre-sliced G-code files. // The print_3mf tool handles starting 3MF prints. return await implementation.startJob(host, port, apiKey, bambuSerial, bambuToken, gcodeFilename); } return await implementation.startJob(host, port, apiKey, gcodeFilename); } async cancelPrint(host, port, type, apiKey, bambuSerial, bambuToken) { const implementation = this.printerFactory.getImplementation(type); if (type.toLowerCase() === "bambu") { return await implementation.cancelJob(host, port, apiKey, bambuSerial, bambuToken); } return await implementation.cancelJob(host, port, apiKey); } async setPrinterTemperature(host, port, type, apiKey, bambuSerial, bambuToken, component, temperature) { const implementation = this.printerFactory.getImplementation(type); if (type.toLowerCase() === "bambu") { const bambuImplementation = this.printerFactory.getImplementation('bambu'); if (bambuImplementation instanceof BambuImplementation && typeof bambuImplementation.setTemperature === 'function') { return await bambuImplementation.setTemperature(host, bambuSerial, bambuToken, component, temperature); } else { console.warn('setTemperature not fully implemented for Bambu via direct commands yet.'); return { status: 'Command sent (implementation pending)' }; // Avoid throwing error if method doesn't exist } } return implementation.setTemperature(host, port, apiKey, component, temperature); } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error("3D Printer MCP server running on stdio transport"); } } const server = new ThreeDPrinterMCPServer(); server.run().catch(console.error);

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/DMontgomery40/mcp-3D-printer-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server