Skip to main content
Glama
pzfreo

build123d-mcp

render_view

Render a 3D model as PNG, SVG, or both to confirm its appearance visually, with control over direction, objects, quality, clipping, and camera angles.

Instructions

Render model. format: 'png' (raster, default), 'svg' (HLR line drawing — works without a display, no shading but precise edges), or 'both' (returns the PNG and SVG together — useful when you want shaded depth cues plus crisp edge geometry). If the raster path fails (typically headless host with no display backend) and format='png', the server falls back to SVG automatically. Renders confirm appearance, not geometry — verify boolean operations with measure() before rendering. direction: top, front, side, iso. objects: comma-separated names or name:color pairs e.g. 'u_frame:blue,roller:red' (default: all, auto-coloured). quality: standard, high. clip_plane: x, y, z to slice; clip_at: absolute world coordinate along that axis (default: each mesh's midpoint). azimuth/elevation: camera rotation in degrees applied after the direction preset. save_to: optional file path; for format='both' the PNG and SVG are written as <save_to>.png and <save_to>.svg.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
directionNoiso
objectsNo
qualityNostandard
clip_planeNo
clip_atNo
azimuthNo
elevationNo
save_toNo
formatNopng

Implementation Reference

  • MCP tool registration via @mcp.tool() decorator. The render_view function is registered as an MCP tool with its schema defined via type hints. It delegates to WorkerSession.render_view().
    @mcp.tool()
    def render_view(direction: str = "iso", objects: str = "", quality: str = "standard", clip_plane: str = "", clip_at: float | None = None, azimuth: float = 0.0, elevation: float = 0.0, save_to: str = "", format: str = "png") -> list:
        """Render model. format: 'png' (raster, default), 'svg' (HLR line drawing — works without a display, no shading but precise edges), or 'both' (returns the PNG and SVG together — useful when you want shaded depth cues plus crisp edge geometry). If the raster path fails (typically headless host with no display backend) and format='png', the server falls back to SVG automatically. Renders confirm appearance, not geometry — verify boolean operations with measure() before rendering. direction: top, front, side, iso. objects: comma-separated names or name:color pairs e.g. 'u_frame:blue,roller:red' (default: all, auto-coloured). quality: standard, high. clip_plane: x, y, z to slice; clip_at: absolute world coordinate along that axis (default: each mesh's midpoint). azimuth/elevation: camera rotation in degrees applied after the direction preset. save_to: optional file path; for format='both' the PNG and SVG are written as <save_to>.png and <save_to>.svg."""
        result = _session.render_view(
            direction=direction, objects=objects, quality=quality,
            clip_plane=clip_plane, clip_at=clip_at, azimuth=azimuth,
            elevation=elevation, save_to=save_to, format=format,
        )
    
        contents: list = []
        for key, suffix, mime in (("png", ".png", "image/png"), ("svg", ".svg", "image/svg+xml")):
            if key in result:
                data = result[key]
                fd, path = tempfile.mkstemp(suffix=suffix, prefix="build123d_")
                os.close(fd)
                with open(path, "wb") as f:
                    f.write(data)
                contents.append(ImageContent(
                    type="image",
                    data=base64.b64encode(data).decode(),
                    mimeType=mime,
                ))
                contents.append(TextContent(type="text", text=f"[SEND: {path}]"))
        if result.get("fallback"):
            contents.append(TextContent(type="text", text=result["fallback"]))
        if result.get("png_error"):
            contents.append(TextContent(type="text", text=f"PNG render failed: {result['png_error']}"))
        if result.get("png_warnings"):
            for w in result["png_warnings"]:
                contents.append(TextContent(type="text", text=f"Warning: {w}"))
        return contents
  • Core render_view handler function. Validates inputs, resolves shapes, renders PNG (VTK) with fallback to SVG (build123d HLR projection), and optionally saves to file. Returns dict with png/svg bytes.
    def render_view(
        session,
        direction: str = "iso",
        objects: str = "",
        quality: str = "standard",
        clip_plane: str = "",
        clip_at: float | None = None,
        azimuth: float = 0.0,
        elevation: float = 0.0,
        save_to: str = "",
        format: str = "png",
    ) -> dict:
        """Render the active session geometry.
    
        Returns a dict with optional keys:
          - "png": bytes of the rasterised PNG (when format in ("png","both")
            or as automatic fallback failed)
          - "svg": bytes of the SVG document (when format in ("svg","both") or
            as automatic fallback when raster rendering failed)
          - "fallback": str, present when SVG was returned in place of a
            requested PNG because the VTK renderer failed
        """
        direction = direction.lower()
        if direction not in ("top", "front", "side", "iso"):
            raise ValueError(f"Unknown direction '{direction}'. Use: top, front, side, iso")
    
        quality = quality.lower()
        if quality not in _QUALITY:
            raise ValueError(f"Unknown quality '{quality}'. Use: standard, high")
    
        clip_plane = clip_plane.lower()
        if clip_plane and clip_plane not in ("x", "y", "z"):
            raise ValueError(f"Unknown clip_plane '{clip_plane}'. Use: x, y, z")
    
        format = format.lower()
        if format not in _VALID_FORMATS:
            raise ValueError(f"Unknown format '{format}'. Use: png, svg, both")
    
        shapes = _resolve_shapes(session, objects)
        tess = _QUALITY[quality]
    
        result: dict = {}
    
        if format in ("png", "both"):
            try:
                png_bytes, png_failed = _do_render_png(
                    shapes, tess, direction, clip_plane, clip_at, azimuth, elevation,
                )
                result["png"] = png_bytes
                if png_failed:
                    result["png_warnings"] = [
                        f"Skipped shapes (tessellation failed): {', '.join(png_failed)}"
                    ]
            except Exception as exc:
                if format == "png":
                    # Auto-fallback: produce SVG so the AI still gets a visual.
                    result["svg"] = _do_render_svg(
                        shapes, direction, clip_plane, clip_at, azimuth, elevation,
                    )
                    result["format"] = "svg"
                    result["fallback"] = (
                        f"VTK raster render failed ({type(exc).__name__}: {exc}). "
                        f"Returning SVG via build123d HLR projection. "
                        f"Common causes: no DISPLAY and no OSMesa/EGL backend on a headless host."
                    )
                else:
                    # 'both' was requested. Record the PNG failure and continue to SVG.
                    result["png_error"] = f"{type(exc).__name__}: {exc}"
    
        if format in ("svg", "both") and "svg" not in result:
            result["svg"] = _do_render_svg(
                shapes, direction, clip_plane, clip_at, azimuth, elevation,
            )
    
        if save_to:
            from build123d_mcp.tools._paths import safe_output_path
            # Strip a known extension so format='both' produces consistent <base>.png and <base>.svg
            base, ext = os.path.splitext(save_to)
            if ext.lower() in (".png", ".svg"):
                save_to = base
            if "png" in result:
                with open(safe_output_path(save_to + ".png"), "wb") as f:
                    f.write(result["png"])
            if "svg" in result:
                with open(safe_output_path(save_to + ".svg"), "wb") as f:
                    f.write(result["svg"])
    
        return result
  • WorkerSession.render_view() proxy method. Sends a 'render_view' operation over the multiprocessing pipe to the worker subprocess, which dispatches to the actual handler in render.py.
    def render_view(
        self,
        direction: str = "iso",
        objects: str = "",
        quality: str = "standard",
        clip_plane: str = "",
        clip_at: float | None = None,
        azimuth: float = 0.0,
        elevation: float = 0.0,
        save_to: str = "",
        format: str = "png",
    ) -> dict:
        return self._call(
            "render_view",
            {
                "direction": direction,
                "objects": objects,
                "quality": quality,
                "clip_plane": clip_plane,
                "clip_at": clip_at,
                "azimuth": azimuth,
                "elevation": elevation,
                "save_to": save_to,
                "format": format,
            },
            self._RENDER_TIMEOUT,
        )
  • Worker dispatch: routes the 'render_view' operation string to the render_view function from build123d_mcp.tools.render.
    def _dispatch(session: Any, op: str, args: dict, library_index: Any) -> Any:
        if op == "execute":
            return session.execute(args["code"])
    
        if op == "render_view":
            from build123d_mcp.tools.render import render_view
            return render_view(session, **args)
  • PNG rendering helper (_do_render_png): uses VTK offscreen rendering to rasterize shapes with tessellation, clipping, camera positioning, and PNG output via vtkPNGWriter.
    def _do_render_png(shapes, tess, direction, clip_plane, clip_at, azimuth, elevation) -> tuple[bytes, list[str]]:
        import tempfile
        import vtk
    
        _ensure_display()
    
        renderer = vtk.vtkRenderer()
        renderer.SetBackground(1.0, 1.0, 1.0)
    
        render_window = vtk.vtkRenderWindow()
        render_window.SetOffScreenRendering(1)
        render_window.SetSize(800, 600)
        render_window.AddRenderer(renderer)
    
        failed: list[str] = []
        actor_count = 0
    
        for i, (name, shape, obj_color) in enumerate(shapes):
            try:
                verts, tris = shape.tessellate(
                    tess["linear_deflection"], tess["angular_deflection"]
                )
            except Exception as exc:
                failed.append(f"{name}: {exc}")
                continue
    
            points = vtk.vtkPoints()
            for v in verts:
                points.InsertNextPoint(v.X, v.Y, v.Z)
    
            cells = vtk.vtkCellArray()
            for tri in tris:
                cells.InsertNextCell(3)
                cells.InsertCellPoint(tri[0])
                cells.InsertCellPoint(tri[1])
                cells.InsertCellPoint(tri[2])
    
            poly = vtk.vtkPolyData()
            poly.SetPoints(points)
            poly.SetPolys(cells)
    
            if clip_plane:
                if clip_at is not None:
                    origin = {"x": (clip_at, 0, 0), "y": (0, clip_at, 0), "z": (0, 0, clip_at)}[clip_plane]
                else:
                    bounds = poly.GetBounds()  # xmin, xmax, ymin, ymax, zmin, zmax
                    cx = (bounds[0] + bounds[1]) / 2
                    cy = (bounds[2] + bounds[3]) / 2
                    cz = (bounds[4] + bounds[5]) / 2
                    origin = {"x": (cx, 0, 0), "y": (0, cy, 0), "z": (0, 0, cz)}[clip_plane]
                normal = {"x": (1, 0, 0), "y": (0, 1, 0), "z": (0, 0, 1)}[clip_plane]
    
                plane = vtk.vtkPlane()
                plane.SetOrigin(*origin)
                plane.SetNormal(*normal)
    
                clipper = vtk.vtkClipPolyData()
                clipper.SetInputData(poly)
                clipper.SetClipFunction(plane)
                clipper.SetInsideOut(False)
                clipper.Update()
                poly = clipper.GetOutput()
    
            mapper = vtk.vtkPolyDataMapper()
            mapper.SetInputData(poly)
    
            actor = vtk.vtkActor()
            actor.SetMapper(mapper)
    
            r, g, b = _color_to_rgb(obj_color if obj_color else _PALETTE[i % len(_PALETTE)])
            prop = actor.GetProperty()
            prop.SetColor(r, g, b)
            prop.SetAmbient(0.3)
            prop.SetDiffuse(0.7)
            prop.SetSpecular(0.2)
            prop.SetInterpolationToPhong()  # smooth shading
    
            renderer.AddActor(actor)
            actor_count += 1
    
        if actor_count == 0:
            msg = "All shapes failed to tessellate: " + "; ".join(failed) if failed else "No geometry to render"
            raise RuntimeError(msg)
    
        # Camera setup
        camera = renderer.GetActiveCamera()
        camera.SetParallelProjection(False)
        pos, up = _camera_direction(direction)
        camera.SetPosition(*pos)
        camera.SetFocalPoint(0.0, 0.0, 0.0)
        camera.SetViewUp(*up)
        renderer.ResetCamera()
    
        if azimuth != 0.0 or elevation != 0.0:
            camera.Azimuth(azimuth)
            camera.Elevation(elevation)
            camera.OrthogonalizeViewUp()
            renderer.ResetCameraClippingRange()
    
        render_window.Render()
    
        w2i = vtk.vtkWindowToImageFilter()
        w2i.SetInput(render_window)
        w2i.Update()
    
        with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir:
            png_path = os.path.join(tmpdir, "render.png")
            writer = vtk.vtkPNGWriter()
            writer.SetFileName(png_path)
            writer.SetInputConnection(w2i.GetOutputPort())
            writer.Write()
    
            with open(png_path, "rb") as f:
                return f.read(), failed
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description covers key behaviors: fallback to SVG when raster fails, that SVG works headless, and that rendering confirms appearance not geometry. It lacks details on performance or side effects, but these are minimal for a render tool.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single dense paragraph, well-organized with parameter explanations and usage hints. It is somewhat long but each sentence adds value; minor improvement could be breaking into sections for readability.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given no output schema, the description explains output formats (png, svg, both) and file saving. It covers input, behavior, fallback, and hints for use. It does not explicitly state the return type (e.g., image data), but the context is sufficient for an agent to invoke correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 0%, so the description fully explains all 9 parameters with defaults, examples (e.g., 'u_frame:blue,roller:red' for objects), and behavior (e.g., clip_at default is mesh midpoint). This adds substantial meaning beyond the schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states it renders a model in formats like png, svg, or both. It distinguishes from sibling tools by noting that renders confirm appearance, not geometry, directing users to measure() for geometry verification. This differentiates it effectively.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides clear guidance on when to use this tool (for rendering) and when not to (for geometry verification, use measure). It explains format options and fallback behavior, but does not explicitly list alternative tools or exclusion scenarios beyond measure.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

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/pzfreo/build123d-mcp'

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