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

> Learn to modify MCP 3D Relief to support ASCII and binary STL output formats. Refactor the writer and add a format parameter to your CLI for flexible STL generation.

- Repository: [bigchx/mcp_3d_relief](https://github.com/bigchx/mcp_3d_relief)
- Tags: how-to-guide
- Published: 2026-02-26

---

**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`](https://github.com/bigchx/mcp_3d_relief/blob/main/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`](https://github.com/bigchx/mcp_3d_relief/blob/main/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`](https://github.com/bigchx/mcp_3d_relief/blob/main/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`](https://github.com/bigchx/mcp_3d_relief/blob/main/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`](https://github.com/bigchx/mcp_3d_relief/blob/main/relief.py) near the existing `write_facet` definition.

The ASCII writer remains straightforward string formatting:

```python
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).

```python
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`](https://github.com/bigchx/mcp_3d_relief/blob/main/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.

```python
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`](https://github.com/bigchx/mcp_3d_relief/blob/main/relief.py)) to accept `stl_format: str = "ascii"` and pass it to `generate_stl`.

```python
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`](https://github.com/bigchx/mcp_3d_relief/blob/main/relief.py) and add the `--stl_format` argument.

```python
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:

```python
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`](https://github.com/bigchx/mcp_3d_relief/blob/main/relief.py), either immediately before the `generate_stl` coroutine or in a new [`stl_writer.py`](https://github.com/bigchx/mcp_3d_relief/blob/main/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`](https://github.com/bigchx/mcp_3d_relief/blob/main/relief.py) to maintain the existing architecture while keeping the code organized and testable.