How to Modify MCP 3D Relief to Support Different STL Output Formats

You can modify the MCP 3D Relief codebase to support both ASCII and binary STL output formats by refactoring the hard-coded ASCII writer in relief.py into separate format-specific functions and exposing a new format parameter through the CLI.

The bigchx/mcp_3d_relief repository currently generates 3D relief models exclusively as ASCII STL files. If you need to modify the code to support different STL output formats—such as the more compact binary variant—you must restructure the file-writing logic in relief.py. This guide walks through the exact source code changes required to implement multi-format support while preserving the existing depth-map processing pipeline.

Understanding the Current STL Implementation in relief.py

The current implementation hard-codes ASCII STL generation inside the generate_stl coroutine. Located in relief.py, this function opens the output file in text mode ("w") and iterates through computed facets, writing human-readable "solid", "facet normal", and "vertex" records.

This design limits the tool to ASCII output only. The write_facet helper function (also in relief.py) specifically formats strings for the ASCII specification, making it unsuitable for binary serialization without modification.

Refactoring the Code to Support Multiple STL Formats

To modify the code to support different STL output formats, you need to extract the writing logic into dedicated functions, update the function signatures to accept a format specifier, and propagate that parameter through the CLI.

Creating Format-Specific Writer Functions

First, create separate writer functions for ASCII and binary formats. Place these in relief.py near the existing write_facet definition.

The ASCII writer remains straightforward string formatting:

def _write_ascii_stl(file, facets):
    """Write facets to an ASCII STL file."""
    file.write("solid mcp_3d_relief\n")
    for normal, v1, v2, v3 in facets:
        file.write(f"  facet normal {normal[0]:.6f} {normal[1]:.6f} {normal[2]:.6f}\n")
        file.write("    outer loop\n")
        file.write(f"      vertex {v1[0]:.6f} {v1[1]:.6f} {v1[2]:.6f}\n")
        file.write(f"      vertex {v2[0]:.6f} {v2[1]:.6f} {v2[2]:.6f}\n")
        file.write(f"      vertex {v3[0]:.6f} {v3[1]:.6f} {v3[2]:.6f}\n")
        file.write("    endloop\n")
        file.write("  endfacet\n")
    file.write("endsolid mcp_3d_relief\n")

The binary writer requires Python's struct module to pack data according to the STL binary specification: an 80-byte header, a 4-byte unsigned integer for the triangle count, and for each triangle, 12 floats (normal + 3 vertices) followed by a 2-byte attribute count (typically zero).

import struct

def _write_binary_stl(file, facets):
    """Write facets to a binary STL file."""
    # 80-byte header (can contain description)

    header = b'Binary STL generated by MCP 3D Relief'
    file.write(header + b' ' * (80 - len(header)))
    
    # Number of triangles (uint32, little-endian)

    file.write(struct.pack("<I", len(facets)))
    
    # Each triangle: normal (3 floats), vertices (9 floats), attribute byte count (uint16)

    for normal, v1, v2, v3 in facets:
        data = struct.pack("<12fH",
            normal[0], normal[1], normal[2],
            v1[0], v1[1], v1[2],
            v2[0], v2[1], v2[2],
            v3[0], v3[1], v3[2],
            0  # attribute byte count

        )
        file.write(data)

Updating the generate_stl Function Signature

Next, modify the generate_stl coroutine in relief.py to accept a format parameter and dispatch to the appropriate writer. Change the function signature to include fmt: str = "ascii" and replace the hard-coded file-writing block with conditional logic.

async def generate_stl(
    depth_map,
    output_path,
    model_width=50,
    model_thickness=5,
    base_thickness=2.0,
    fmt: str = "ascii",  # New parameter for STL output format

):
    """
    Generate an STL file from a depth map.
    
    Args:
        depth_map: 2D array of depth values
        output_path: Path to write the STL file
        model_width: Width of the model in mm
        model_thickness: Maximum thickness of the relief
        base_thickness: Thickness of the base layer
        fmt: Output format - "ascii" or "binary"
    """
    # ... existing vertex/facet computation logic ...

    facets = []  # Computed list of (normal, v1, v2, v3) tuples

    
    if fmt == "binary":
        with open(output_path, "wb") as f:
            _write_binary_stl(f, facets)
    else:  # default to ASCII

        with open(output_path, "w") as f:
            _write_ascii_stl(f, facets)
    
    return output_path

Propagating the Format Parameter Through the API

You must thread the format parameter through the call stack. Update the high-level relief coroutine (also in relief.py) to accept stl_format: str = "ascii" and pass it to generate_stl.

async def relief(
    input_image_path,
    detail_level=0.5,
    model_width=50,
    model_thickness=5,
    base_thickness=2.0,
    output_dir=None,
    skip_depth=False,
    invert_depth=False,
    stl_format: str = "ascii",  # New parameter

):
    """
    Process an image into a 3D relief model.
    
    Args:
        ...
        stl_format: STL output format - "ascii" or "binary"
    """
    # ... existing processing logic ...

    
    stl_path = await generate_stl(
        depth_map,
        stl_path,
        model_width,
        model_thickness,
        base_thickness,
        fmt=stl_format,  # Pass format to generator

    )
    
    return stl_path

Finally, expose the option via the CLI. Locate the argparse configuration in the if __name__ == "__main__" block of relief.py and add the --stl_format argument.

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Generate 3D relief models from images")
    parser.add_argument("input_image_path", help="Path to input image")
    parser.add_argument("--detail_level", type=float, default=0.5)
    parser.add_argument("--model_width", type=float, default=50)
    parser.add_argument("--model_thickness", type=float, default=5)
    parser.add_argument("--base_thickness", type=float, default=2.0)
    parser.add_argument("--output_dir", default=None)
    parser.add_argument("--skip_depth", action="store_true")
    parser.add_argument("--invert_depth", action="store_true")
    
    # New CLI argument for STL output format

    parser.add_argument(
        "--stl_format",
        choices=["ascii", "binary"],
        default="ascii",
        help="Select STL output format (ascii or binary)",
    )
    
    args = parser.parse_args()
    
    asyncio.run(
        relief(
            input_image_path=args.input_image_path,
            detail_level=args.detail_level,
            model_width=args.model_width,
            model_thickness=args.model_thickness,
            base_thickness=args.base_thickness,
            output_dir=args.output_dir,
            skip_depth=args.skip_depth,
            invert_depth=args.invert_depth,
            stl_format=args.stl_format,  # Pass CLI value to function

        )
    )

Alternative Approaches for Binary STL Generation

While the struct-based approach gives you full control over the binary layout, you can simplify maintenance by using the numpy-stl library (pip install numpy-stl). This library handles the binary packing internally and reduces the risk of endianness or alignment errors.

To integrate numpy-stl, replace the custom _write_binary_stl function with:

import numpy as np
from stl import mesh

def _write_binary_stl_numpy(file_path, facets):
    """Write binary STL using numpy-stl library."""
    # Create empty mesh

    stl_mesh = mesh.Mesh(np.zeros(len(facets), dtype=mesh.Mesh.dtype))
    
    # Fill mesh with facet data

    for i, (normal, v1, v2, v3) in enumerate(facets):
        stl_mesh.normals[i] = normal
        stl_mesh.vectors[i] = np.array([v1, v2, v3])
    
    # Save as binary

    stl_mesh.save(file_path, mode=mesh.Mode.BINARY)

Then update the generate_stl function to call this alternative when fmt == "binary" and the numpy-stl backend is preferred.

Summary

To modify the MCP 3D Relief code to support different STL output formats, implement these architectural changes:

  • Extract writer functions: Move the ASCII formatting logic from generate_stl into a dedicated _write_ascii_stl helper, and create a parallel _write_binary_stl function using Python's struct module to pack binary data according to the STL specification.
  • Extend the API signature: Add a fmt: str = "ascii" parameter to generate_stl and stl_format: str = "ascii" to the high-level relief coroutine, enabling format selection at every layer of the application.
  • Update the CLI interface: Add a --stl_format argument with choices=["ascii", "binary"] to the argparse configuration in the __main__ block, ensuring users can specify their preferred format when running the tool from the command line.
  • Consider external libraries: For production environments, evaluate replacing the manual binary writer with the numpy-stl library to reduce maintenance overhead and ensure spec-compliant binary output.

Frequently Asked Questions

What is the difference between ASCII and binary STL formats?

ASCII STL files contain human-readable text with keywords like solid, facet normal, and vertex, making them easy to debug but significantly larger in file size. Binary STL files store the same geometric data in a compact 80-byte header followed by 32-bit floating-point numbers and unsigned integers, resulting in files that are roughly 70% smaller and faster to parse by most 3D printing slicers.

How do I add support for additional mesh formats like OBJ or PLY?

To extend MCP 3D Relief beyond STL, create new writer functions (e.g., _write_obj or _write_ply) that translate the internal facets list—containing tuples of (normal, v1, v2, v3)—into the target format's syntax. Update the fmt parameter to accept new enum values (e.g., "obj", "ply"), and modify the conditional logic in generate_stl (or rename it to export_mesh) to dispatch to the appropriate writer based on the file extension or explicit format argument.

Will modifying the STL output format affect the 3D relief generation quality?

No, changing the output format between ASCII and binary does not affect the geometric quality or resolution of the generated 3D relief model. Both formats encode the exact same vertex coordinates and normal vectors; only the storage representation differs. The depth-map processing, mesh triangulation, and dimensional calculations in generate_stl remain identical regardless of whether you choose ASCII or binary output.

Where should I place the new writer functions in the codebase?

Place the new _write_ascii_stl and _write_binary_stl helper functions in relief.py, either immediately before the generate_stl coroutine or in a new stl_writer.py module if you prefer to separate I/O logic from the mesh generation algorithm. If you create a separate module, import the writer functions in relief.py to maintain the existing architecture while keeping the code organized and testable.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →