Skip to main content
Glama

OpenSCAD MCP Server

by jhacksman
cad_exporter.py9.89 kB
import os import logging import subprocess from typing import Dict, Any, Optional, Tuple, List logger = logging.getLogger(__name__) class CADExporter: """ Exports OpenSCAD models to various CAD formats that preserve parametric properties. """ def __init__(self, openscad_path: str = "openscad"): """ Initialize the CAD exporter. Args: openscad_path: Path to the OpenSCAD executable """ self.openscad_path = openscad_path # Supported export formats self.supported_formats = { "csg": "OpenSCAD CSG format (preserves all parametric properties)", "amf": "Additive Manufacturing File Format (preserves some metadata)", "3mf": "3D Manufacturing Format (modern replacement for STL with metadata)", "scad": "OpenSCAD source code (fully parametric)", "dxf": "Drawing Exchange Format (for 2D designs)", "svg": "Scalable Vector Graphics (for 2D designs)" } def export_model(self, scad_file: str, output_format: str = "csg", parameters: Optional[Dict[str, Any]] = None, metadata: Optional[Dict[str, Any]] = None) -> Tuple[bool, str, Optional[str]]: """ Export an OpenSCAD model to the specified format. Args: scad_file: Path to the SCAD file output_format: Format to export to (csg, amf, 3mf, etc.) parameters: Optional parameters to override in the SCAD file metadata: Optional metadata to include in the export Returns: Tuple of (success, output_file_path, error_message) """ if not os.path.exists(scad_file): return False, "", f"SCAD file not found: {scad_file}" # Create output file path output_dir = os.path.dirname(scad_file) model_id = os.path.basename(scad_file).split('.')[0] # Special case for SCAD format - just copy the file with parameters embedded if output_format.lower() == "scad" and parameters: return self._export_parametric_scad(scad_file, parameters, metadata) # For native OpenSCAD formats output_file = os.path.join(output_dir, f"{model_id}.{output_format.lower()}") # Build command cmd = [self.openscad_path, "-o", output_file] # Add parameters if provided if parameters: for key, value in parameters.items(): cmd.extend(["-D", f"{key}={value}"]) # Add input file cmd.append(scad_file) try: # Run OpenSCAD result = subprocess.run(cmd, check=True, capture_output=True, text=True) # Check if file was created if os.path.exists(output_file) and os.path.getsize(output_file) > 0: logger.info(f"Exported model to {output_format}: {output_file}") # Add metadata if supported and provided if metadata and output_format.lower() in ["amf", "3mf"]: self._add_metadata_to_file(output_file, metadata, output_format) return True, output_file, None else: error_msg = f"Failed to export model to {output_format}" logger.error(error_msg) logger.error(f"OpenSCAD output: {result.stdout}") logger.error(f"OpenSCAD error: {result.stderr}") return False, "", error_msg except subprocess.CalledProcessError as e: error_msg = f"Error exporting model to {output_format}: {e.stderr}" logger.error(error_msg) return False, "", error_msg except Exception as e: error_msg = f"Error exporting model to {output_format}: {str(e)}" logger.error(error_msg) return False, "", error_msg def _export_parametric_scad(self, scad_file: str, parameters: Dict[str, Any], metadata: Optional[Dict[str, Any]] = None) -> Tuple[bool, str, Optional[str]]: """ Create a new SCAD file with parameters embedded as variables. Args: scad_file: Path to the original SCAD file parameters: Parameters to embed in the SCAD file metadata: Optional metadata to include as comments Returns: Tuple of (success, output_file_path, error_message) """ try: # Read the original SCAD file with open(scad_file, 'r') as f: content = f.read() # Create output file path output_dir = os.path.dirname(scad_file) model_id = os.path.basename(scad_file).split('.')[0] output_file = os.path.join(output_dir, f"{model_id}_parametric.scad") # Create parameter declarations param_declarations = [] for key, value in parameters.items(): if isinstance(value, str): param_declarations.append(f'{key} = "{value}";') else: param_declarations.append(f'{key} = {value};') # Create metadata comments metadata_comments = [] if metadata: metadata_comments.append("// Metadata:") for key, value in metadata.items(): metadata_comments.append(f"// {key}: {value}") # Combine everything new_content = "// Parametric model generated by OpenSCAD MCP Server\n" new_content += "\n".join(metadata_comments) + "\n\n" if metadata_comments else "\n" new_content += "// Parameters:\n" new_content += "\n".join(param_declarations) + "\n\n" new_content += content # Write to the new file with open(output_file, 'w') as f: f.write(new_content) logger.info(f"Exported parametric SCAD file: {output_file}") return True, output_file, None except Exception as e: error_msg = f"Error creating parametric SCAD file: {str(e)}" logger.error(error_msg) return False, "", error_msg def _add_metadata_to_file(self, file_path: str, metadata: Dict[str, Any], format_type: str) -> None: """ Add metadata to supported file formats. Args: file_path: Path to the file metadata: Metadata to add format_type: File format """ if format_type.lower() == "amf": self._add_metadata_to_amf(file_path, metadata) elif format_type.lower() == "3mf": self._add_metadata_to_3mf(file_path, metadata) def _add_metadata_to_amf(self, file_path: str, metadata: Dict[str, Any]) -> None: """Add metadata to AMF file.""" try: import xml.etree.ElementTree as ET # Parse the AMF file tree = ET.parse(file_path) root = tree.getroot() # Find or create metadata element metadata_elem = root.find("metadata") if metadata_elem is None: metadata_elem = ET.SubElement(root, "metadata") # Add metadata for key, value in metadata.items(): meta = ET.SubElement(metadata_elem, "meta", name=key) meta.text = str(value) # Write back to file tree.write(file_path) logger.info(f"Added metadata to AMF file: {file_path}") except Exception as e: logger.error(f"Error adding metadata to AMF file: {str(e)}") def _add_metadata_to_3mf(self, file_path: str, metadata: Dict[str, Any]) -> None: """Add metadata to 3MF file.""" try: import zipfile import xml.etree.ElementTree as ET # 3MF files are ZIP archives with zipfile.ZipFile(file_path, 'a') as z: # Check if metadata file exists metadata_path = "Metadata/model_metadata.xml" try: z.getinfo(metadata_path) # Extract existing metadata with z.open(metadata_path) as f: tree = ET.parse(f) root = tree.getroot() except KeyError: # Create new metadata file root = ET.Element("metadata") tree = ET.ElementTree(root) # Add metadata for key, value in metadata.items(): meta = ET.SubElement(root, "meta", name=key) meta.text = str(value) # Write metadata to a temporary file temp_path = file_path + ".metadata.tmp" tree.write(temp_path) # Add to ZIP z.write(temp_path, metadata_path) # Remove temporary file os.remove(temp_path) logger.info(f"Added metadata to 3MF file: {file_path}") except Exception as e: logger.error(f"Error adding metadata to 3MF file: {str(e)}") def get_supported_formats(self) -> List[str]: """Get list of supported export formats.""" return list(self.supported_formats.keys()) def get_format_description(self, format_name: str) -> str: """Get description of a format.""" return self.supported_formats.get(format_name.lower(), "Unknown format")

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/jhacksman/OpenSCAD-MCP-Server'

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