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

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault

No arguments

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])

Tool Definition Quality

Score is being calculated. Check back soon.

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