Skip to main content
Glama
quellant

OpenSCAD MCP Server

by quellant

render_single

Generate a 3D model image from OpenSCAD code or file by rendering a single view with customizable camera angles, image size, and output format.

Instructions

Render a single view from OpenSCAD code or file.

Args: scad_content: OpenSCAD code to render (mutually exclusive with scad_file) scad_file: Path to OpenSCAD file (mutually exclusive with scad_content)
view: Predefined view name ("front", "back", "left", "right", "top", "bottom", "isometric", "dimetric") camera_position: Camera position - accepts [x,y,z] list, JSON string "[x,y,z]", or dict {"x":x,"y":y,"z":z} (default: [70, 70, 70]) camera_target: Camera look-at point - accepts [x,y,z] list, JSON string, or dict (default: [0, 0, 0]) camera_up: Camera up vector - accepts [x,y,z] list, JSON string, or dict (default: [0, 0, 1]) image_size: Image dimensions - accepts [width, height] list, JSON string "[width, height]", "widthxheight", or tuple (default: [800, 600]) color_scheme: OpenSCAD color scheme (default: "Cornfield") variables: Variables to pass to OpenSCAD auto_center: Auto-center the model output_format: Output format - "auto", "base64", "file_path", or "compressed" (default: "auto") ctx: MCP context for logging

Returns: Dict with base64-encoded PNG image or file path

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
scad_contentNo
scad_fileNo
viewNo
camera_positionNo
camera_targetNo
camera_upNo
image_sizeNo
color_schemeNoCornfield
variablesNo
auto_centerNo
output_formatNoauto

Implementation Reference

  • Primary handler for the render_single MCP tool. Handles flexible parameter parsing, calls core renderer, manages output formats, and returns results.
    @mcp.tool
    async def render_single(
        scad_content: Optional[str] = None,
        scad_file: Optional[str] = None,
        view: Optional[str] = None,
        camera_position: Union[str, List[float], Dict[str, float], None] = None,
        camera_target: Union[str, List[float], Dict[str, float], None] = None,
        camera_up: Union[str, List[float], Dict[str, float], None] = None,
        image_size: Union[str, List[int], tuple, None] = None,
        color_scheme: str = "Cornfield",
        variables: Optional[Dict[str, Any]] = None,
        auto_center: bool = False,
        output_format: Optional[str] = "auto",
        ctx: Optional[Context] = None,
    ) -> Dict[str, Any]:
        """
        Render a single view from OpenSCAD code or file.
        
        Args:
            scad_content: OpenSCAD code to render (mutually exclusive with scad_file)
            scad_file: Path to OpenSCAD file (mutually exclusive with scad_content)  
            view: Predefined view name ("front", "back", "left", "right", "top", "bottom", "isometric", "dimetric")
            camera_position: Camera position - accepts [x,y,z] list, JSON string "[x,y,z]", or dict {"x":x,"y":y,"z":z} (default: [70, 70, 70])
            camera_target: Camera look-at point - accepts [x,y,z] list, JSON string, or dict (default: [0, 0, 0])
            camera_up: Camera up vector - accepts [x,y,z] list, JSON string, or dict (default: [0, 0, 1])
            image_size: Image dimensions - accepts [width, height] list, JSON string "[width, height]", "widthxheight", or tuple (default: [800, 600])
            color_scheme: OpenSCAD color scheme (default: "Cornfield")
            variables: Variables to pass to OpenSCAD
            auto_center: Auto-center the model
            output_format: Output format - "auto", "base64", "file_path", or "compressed" (default: "auto")
            ctx: MCP context for logging
        
        Returns:
            Dict with base64-encoded PNG image or file path
        """
        if ctx:
            await ctx.info("Starting OpenSCAD render...")
        
        # Validate input
        if bool(scad_content) == bool(scad_file):
            raise ValueError("Exactly one of scad_content or scad_file must be provided")
        
        # If view keyword is provided, use preset camera settings
        if view:
            if view not in VIEW_PRESETS:
                raise ValueError(f"Invalid view name '{view}'. Must be one of: {', '.join(VIEW_PRESETS.keys())}")
            
            # Get preset camera settings
            preset_pos, preset_target, preset_up = VIEW_PRESETS[view]
            
            # Override camera parameters with preset values
            camera_position = list(preset_pos)
            camera_target = list(preset_target)
            camera_up = list(preset_up)
            
            # Auto-center is typically enabled for standard views
            if not auto_center:
                auto_center = True
                
            if ctx:
                await ctx.info(f"Using preset view '{view}' with camera position {camera_position}")
        else:
            # Parse camera parameters with proper defaults
            camera_position = parse_camera_param(camera_position, [70, 70, 70])
            camera_target = parse_camera_param(camera_target, [0, 0, 0])
            camera_up = parse_camera_param(camera_up, [0, 0, 1])
        
        # Parse image size with flexible formats
        image_size = parse_image_size_param(image_size, [800, 600])
        
        # Parse variables with flexible formats
        variables = parse_dict_param(variables, {})
        
        try:
            # Run rendering (simplified synchronous version)
            # In production, this should use asyncio.run_in_executor
            image_data = await asyncio.get_event_loop().run_in_executor(
                None,
                render_scad_to_png,
                scad_content,
                scad_file,
                camera_position,
                camera_target,
                camera_up,
                image_size,
                color_scheme,
                variables,
                auto_center,
            )
            
            if ctx:
                await ctx.info("Rendering completed successfully")
            
            # Apply response size management for single image
            if output_format and output_format != "base64":
                managed_result = manage_response_size(
                    {"render": image_data},
                    output_format=output_format,
                    max_size=20000,
                    ctx=ctx
                )
                
                # Check if we got extended format
                if isinstance(managed_result, dict) and "render" in managed_result:
                    result_data = managed_result["render"]
                    if isinstance(result_data, dict):
                        # Extended format with metadata
                        return {
                            "success": True,
                            **result_data,  # Include type, data/path, mime_type
                            "operation_id": str(uuid.uuid4()),
                            "output_format": output_format if output_format != "auto" else "optimized"
                        }
            
            # Default base64 response (backwards compatible)
            return {
                "success": True,
                "data": image_data,
                "mime_type": "image/png",
                "operation_id": str(uuid.uuid4()),
            }
        
        except Exception as e:
            if ctx:
                await ctx.error(f"Rendering failed: {str(e)}")
            return {
                "success": False,
                "error": str(e),
                "operation_id": str(uuid.uuid4()),
            }
  • Core rendering helper that invokes OpenSCAD CLI to generate PNG image from SCAD input and returns base64-encoded data.
    def render_scad_to_png(
        scad_content: Optional[str] = None,
        scad_file: Optional[str] = None,
        camera_position: List[float] = [70, 70, 70],
        camera_target: List[float] = [0, 0, 0],
        camera_up: List[float] = [0, 0, 1],
        image_size: List[int] = [800, 600],
        color_scheme: str = "Cornfield",
        variables: Optional[Dict[str, Any]] = None,
        auto_center: bool = False,
    ) -> str:
        """
        Render OpenSCAD code or file to PNG and return as base64.
        
        This is a simplified synchronous implementation for the MVP.
        """
        openscad_cmd = find_openscad()
        if not openscad_cmd:
            raise RuntimeError("OpenSCAD not found. Please install OpenSCAD first.")
        
        config = get_config()
        
        # Ensure temp directory exists
        temp_dir_path = Path(config.temp_dir)
        temp_dir_path.mkdir(parents=True, exist_ok=True)
        
        # Create temporary files
        with tempfile.TemporaryDirectory(dir=config.temp_dir) as temp_dir:
            temp_path = Path(temp_dir)
            
            # Handle input source
            if scad_content:
                scad_path = temp_path / "input.scad"
                scad_path.write_text(scad_content)
            elif scad_file:
                scad_path = Path(scad_file)
                if not scad_path.exists():
                    raise FileNotFoundError(f"SCAD file not found: {scad_file}")
            else:
                raise ValueError("Either scad_content or scad_file must be provided")
            
            # Output path
            output_path = temp_path / "output.png"
            
            # Build OpenSCAD command
            cmd = [
                openscad_cmd,
                "--hardwarnings",
                "-o", str(output_path),
                "--imgsize", f"{image_size[0]},{image_size[1]}",
                "--colorscheme", color_scheme,
            ]
            
            # Calculate camera distance
            import math
            dx = camera_position[0] - camera_target[0]
            dy = camera_position[1] - camera_target[1]
            dz = camera_position[2] - camera_target[2]
            distance = math.sqrt(dx*dx + dy*dy + dz*dz)
            
            # Add camera parameters
            camera_str = (
                f"--camera="
                f"{camera_position[0]},{camera_position[1]},{camera_position[2]},"
                f"{camera_target[0]},{camera_target[1]},{camera_target[2]},"
                f"{distance}"
            )
            cmd.append(camera_str)
            
            if auto_center:
                cmd.append("--autocenter")
                cmd.append("--viewall")
            
            # Add variables
            if variables:
                for key, value in variables.items():
                    if isinstance(value, str):
                        val_str = f'"{value}"'
                    elif isinstance(value, bool):
                        val_str = "true" if value else "false"
                    else:
                        val_str = str(value)
                    cmd.extend(["-D", f"{key}={val_str}"])
            
            # Add the SCAD file
            cmd.append(str(scad_path))
            
            # Run OpenSCAD
            result = subprocess.run(cmd, capture_output=True, text=True, check=False)
            
            if result.returncode != 0:
                raise RuntimeError(f"OpenSCAD rendering failed: {result.stderr}")
            
            if not output_path.exists():
                raise RuntimeError("OpenSCAD did not produce output file")
            
            # Read and encode the image
            with open(output_path, "rb") as f:
                image_data = f.read()
            
            # Return base64-encoded PNG
            return base64.b64encode(image_data).decode("utf-8")
  • Pydantic model defining structured input parameters and validation for the render_single tool, including flexible parsing for camera positions and image sizes.
    class SingleRenderParams(RenderParams):
        """Parameters for single view rendering."""
    
        view: Optional[Union[PredefinedView, str]] = Field(
            None, 
            description="Predefined view name (front, back, left, right, top, bottom, isometric, dimetric) - overrides camera settings if provided"
        )
        camera_position: Union[Vector3D, List[float], str] = Field(
            default_factory=lambda: Vector3D(x=70, y=70, z=70),
            description="Camera position in 3D space (accepts Vector3D, list [x,y,z], or JSON string)",
        )
        camera_target: Union[Vector3D, List[float], str] = Field(
            default_factory=lambda: Vector3D(x=0, y=0, z=0),
            description="Point camera looks at (accepts Vector3D, list [x,y,z], or JSON string)",
        )
        camera_up: Union[Vector3D, List[float], str] = Field(
            default_factory=lambda: Vector3D(x=0, y=0, z=1),
            description="Camera up vector (accepts Vector3D, list [x,y,z], or JSON string)",
        )
    
        @field_validator("camera_position", "camera_target", "camera_up", mode="before")
        @classmethod
        def parse_camera_params(cls, v: Any) -> Vector3D:
            """Parse camera parameters into Vector3D objects.
            
            Accepts:
            - Vector3D objects
            - Lists/tuples of 3 floats: [x, y, z]
            - JSON string representation: "[x, y, z]"
            - Dict format: {"x": x, "y": y, "z": z}
            """
            # If it's already a Vector3D, return it
            if isinstance(v, Vector3D):
                return v
            
            # Use Vector3D's validator to handle the conversion
            return Vector3D.model_validate(v)
  • Predefined camera configurations for standard views used when 'view' parameter is specified in render_single.
    # View presets for common perspectives with distance=200
    VIEW_PRESETS = {
        "front": ([0, -200, 0], [0, 0, 0], [0, 0, 1]),
        "back": ([0, 200, 0], [0, 0, 0], [0, 0, 1]),
        "left": ([-200, 0, 0], [0, 0, 0], [0, 0, 1]),
        "right": ([200, 0, 0], [0, 0, 0], [0, 0, 1]),
        "top": ([0, 0, 200], [0, 0, 0], [0, 1, 0]),
        "bottom": ([0, 0, -200], [0, 0, 0], [0, -1, 0]),
        "isometric": ([200, 200, 200], [0, 0, 0], [0, 0, 1]),
        "dimetric": ([200, 100, 200], [0, 0, 0], [0, 0, 1]),
    }
  • Pydantic model for 3D vectors with flexible input parsing (list, dict, JSON string) used in camera parameters for render_single.
    class Vector3D(BaseModel):
        """3D vector for positions and directions."""
    
        x: float = Field(..., description="X coordinate")
        y: float = Field(..., description="Y coordinate")
        z: float = Field(..., description="Z coordinate")
    
        @model_validator(mode="before")
        @classmethod
        def parse_vector_input(cls, data: Any) -> Any:
            """Parse various input formats for Vector3D.
            
            Handles:
            - Dict format: {"x": 1, "y": 2, "z": 3}
            - List format: [1, 2, 3]
            - String representation of list: "[1, 2, 3]"
            - String representation of dict: '{"x": 1, "y": 2, "z": 3}'
            """
            # If it's already a dict with x, y, z keys, return as is
            if isinstance(data, dict) and "x" in data and "y" in data and "z" in data:
                return data
            
            # If it's a string, try to parse it as JSON
            if isinstance(data, str):
                try:
                    # Remove any whitespace and try to parse
                    data = data.strip()
                    parsed = json.loads(data)
                    
                    # If parsed result is a list
                    if isinstance(parsed, list) and len(parsed) == 3:
                        return {"x": parsed[0], "y": parsed[1], "z": parsed[2]}
                    # If parsed result is a dict
                    elif isinstance(parsed, dict):
                        return parsed
                except (json.JSONDecodeError, ValueError):
                    # If JSON parsing fails, it might be a malformed string
                    raise ValueError(f"Cannot parse '{data}' as a valid Vector3D")
            
            # If it's a list or tuple with 3 elements
            if isinstance(data, (list, tuple)) and len(data) == 3:
                return {"x": data[0], "y": data[1], "z": data[2]}
            
            # If none of the above, return as is and let Pydantic handle it
            return data
    
        def to_tuple(self) -> Tuple[float, float, float]:
            """Convert to tuple format."""
            return (self.x, self.y, self.z)
    
        @classmethod
        def from_tuple(cls, values: Tuple[float, float, float]) -> "Vector3D":
            """Create from tuple."""
            return cls(x=values[0], y=values[1], z=values[2])
Install Server

Other Tools

Latest Blog Posts

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/quellant/openscad-mcp'

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