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