diff --git a/.devcontainer/Dockerfile.dev b/.devcontainer/Dockerfile.dev
index ea04d0aae..fec32fd95 100644
--- a/.devcontainer/Dockerfile.dev
+++ b/.devcontainer/Dockerfile.dev
@@ -40,7 +40,7 @@ RUN uv venv $VIRTUAL_ENV
 # https://setuptools.pypa.io/en/latest/userguide/development_mode.html
 # This allows for live reloading of the code during development
 RUN uv pip install -e .
-# Install "extras" (development dependencies) in pyproject.toml
-RUN uv sync --group dev
+# Install optional cloud development dependencies in pyproject.toml
+RUN uv sync --extra cloud --group dev
 # Now one can run:
 #    pre-commit run --all-files
diff --git a/src/mdio/commands/copy.py b/src/mdio/commands/copy.py
index 8a206d09c..6cbde48b1 100644
--- a/src/mdio/commands/copy.py
+++ b/src/mdio/commands/copy.py
@@ -64,9 +64,9 @@ def copy(  # noqa: PLR0913
     compatibility with various filesystems via FSSpec.
     """
     # Lazy import to reduce CLI startup time
-    from mdio.api.convenience import copy_mdio  # noqa: PLC0415
+    from mdio.converters.mdio import copy_mdio_cli  # noqa: PLC0415
 
-    copy_mdio(
+    copy_mdio_cli(
         source_mdio_path,
         target_mdio_path,
         overwrite,
diff --git a/src/mdio/commands/info.py b/src/mdio/commands/info.py
index b08de40da..48e9d1f1b 100644
--- a/src/mdio/commands/info.py
+++ b/src/mdio/commands/info.py
@@ -2,156 +2,33 @@
 
 from __future__ import annotations
 
-from typing import TYPE_CHECKING
 from typing import Any
 
+import xarray as xr
 from click import STRING
-from click import Choice
 from click import argument
 from click import command
 from click import option
-
-if TYPE_CHECKING:
-    from mdio import MDIOReader
-    from mdio.core import Grid
+from click_params import JSON
 
 
 @command(name="info")
 @argument("mdio-path", type=STRING)
 @option(
-    "-access",
-    "--access-pattern",
-    required=False,
-    default="012",
-    help="Access pattern of the file",
-    type=STRING,
-    show_default=True,
-)
-@option(
-    "-format",
-    "--output-format",
+    "-storage",
+    "--storage-options",
     required=False,
-    default="pretty",
-    help="Output format. Pretty console or JSON.",
-    type=Choice(["pretty", "json"]),
-    show_default=True,
-    show_choices=True,
+    help="Storage options for SEG-Y input file.",
+    type=JSON,
 )
-def info(mdio_path: str, output_format: str, access_pattern: str) -> None:
+def info(mdio_path: str, storage_options: dict[str, Any]) -> None:
     """Provide information on a MDIO dataset.
 
     By default, this returns human-readable information about the grid and stats for the dataset.
     If output-format is set to 'json' then a JSON is returned to facilitate parsing.
     """
-    # Lazy import to reduce CLI startup time
-    from mdio import MDIOReader  # noqa: PLC0415
-
-    reader = MDIOReader(mdio_path, access_pattern=access_pattern, return_metadata=True)
-
-    grid_dict = parse_grid(reader.grid)
-    stats_dict = cast_stats(reader.stats)
-    access_pattern_dict = parse_access_patterns(reader)
-
-    mdio_info = {
-        "path": mdio_path,
-        "stats": stats_dict,
-        "grid": grid_dict,
-        "access_patterns": access_pattern_dict,
-    }
-
-    if output_format == "pretty":
-        pretty_print(mdio_info)
-
-    if output_format == "json":
-        json_print(mdio_info)
-
-
-def cast_stats(stats_dict: dict[str, Any]) -> dict[str, float]:
-    """Normalize all floats to JSON serializable floats."""
-    return {k: float(v) for k, v in stats_dict.items()}
-
-
-def parse_grid(grid: Grid) -> dict[str, dict[str, int | str]]:
-    """Extract grid information per dimension."""
-    grid_dict = {}
-    for dim_name in grid.dim_names:
-        dim = grid.select_dim(dim_name)
-        min_ = str(dim.coords[0])
-        max_ = str(dim.coords[-1])
-        size = str(dim.coords.shape[0])
-        grid_dict[dim_name] = {"name": dim_name, "min": min_, "max": max_, "size": size}
-    return grid_dict
-
-
-def parse_access_patterns(reader: MDIOReader) -> dict[str, Any]:
-    """Extract access patterns and their info."""
-    access_pattern_dict = {}
-    for name, array in reader._data_group.arrays():
-        pattern = name.replace("chunked_", "")
-        chunks = str(array.chunks)
-        format_ = str(array.dtype)
-        compressors = str(array.compressors)
-        access_pattern_dict[pattern] = {
-            "chunks": chunks,
-            "format": format_,
-            "compressor(s)": compressors,
-        }
-
-    return access_pattern_dict
-
-
-def json_print(mdio_info: dict[str, Any]) -> None:
-    """Convert MDIO Info to JSON and pretty print."""
-    # Lazy import to reduce CLI startup time
-    from json import dumps as json_dumps  # noqa: PLC0415
-
-    from rich import print  # noqa: A004, PLC0415
-
-    print(json_dumps(mdio_info, indent=2))
-
-
-def pretty_print(mdio_info: dict[str, Any]) -> None:
-    """Print pretty MDIO Info table to console."""
-    # Lazy import to reduce CLI startup time
-    from rich.console import Console  # noqa: PLC0415
-    from rich.table import Table  # noqa: PLC0415
-
-    console = Console()
-
-    grid_table = Table(show_edge=False)
-    grid_table.add_column("Dimension", justify="right", style="cyan", no_wrap=True)
-    grid_table.add_column("Min", justify="left", style="magenta")
-    grid_table.add_column("Max", justify="left", style="magenta")
-    grid_table.add_column("Size", justify="left", style="green")
-
-    for axis_dict in mdio_info["grid"].values():
-        name, min_, max_, size = axis_dict.values()
-        grid_table.add_row(name, min_, max_, size)
-
-    stat_table = Table(show_edge=False)
-    stat_table.add_column("Stat", justify="right", style="cyan", no_wrap=True)
-    stat_table.add_column("Value", justify="left", style="magenta")
-
-    for stat, value in mdio_info["stats"].items():
-        stat_table.add_row(stat, f"{value:.4f}")
-
-    access_patter_table = Table(show_edge=False)
-    access_patter_table.add_column("Pattern", justify="right", style="cyan", no_wrap=True)
-    access_patter_table.add_column("Chunks", justify="left", style="magenta")
-    access_patter_table.add_column("Format", justify="left", style="magenta")
-    access_patter_table.add_column("Compressor", justify="left", style="magenta")
-
-    for name, pattern_info in mdio_info["access_patterns"].items():
-        chunks, format_, compressor = pattern_info.values()
-        access_patter_table.add_row(name, chunks, format_, compressor)
-
-    master_table = Table(title=f"File Information for {mdio_info['path']}")
-    master_table.add_column("Grid", justify="center")
-    master_table.add_column("Statistics", justify="center")
-    master_table.add_column("Access Patterns", justify="center")
-    master_table.add_row(grid_table, stat_table, access_patter_table)
-
-    console.print(master_table)
+    ds = xr.open_zarr(mdio_path, mask_and_scale=False, storage_options=storage_options)
+    print(ds)
 
 
 cli = info
diff --git a/src/mdio/commands/segy.py b/src/mdio/commands/segy.py
index 71e1c4d4e..c9a5494cb 100644
--- a/src/mdio/commands/segy.py
+++ b/src/mdio/commands/segy.py
@@ -2,8 +2,6 @@
 
 from typing import Any
 
-from click import BOOL
-from click import FLOAT
 from click import STRING
 from click import Choice
 from click import Group
@@ -62,6 +60,7 @@
 @cli.command(name="import")
 @argument("segy-path", type=STRING)
 @argument("mdio-path", type=STRING)
+@argument("mdio-template-name", type=STRING)
 @option(
     "-loc",
     "--header-locations",
@@ -83,31 +82,6 @@
     help="Names of the index attributes",
     type=StringListParamType(),
 )
-@option(
-    "-chunks",
-    "--chunk-size",
-    required=False,
-    help="Custom chunk size for bricked storage",
-    type=IntListParamType(),
-)
-@option(
-    "-lossless",
-    "--lossless",
-    required=False,
-    default=True,
-    help="Toggle lossless, and perceptually lossless compression",
-    type=BOOL,
-    show_default=True,
-)
-@option(
-    "-tolerance",
-    "--compression-tolerance",
-    required=False,
-    default=0.01,
-    help="Lossy compression tolerance in ZFP.",
-    type=FLOAT,
-    show_default=True,
-)
 @option(
     "-storage-input",
     "--storage-options-input",
@@ -139,28 +113,26 @@
 def segy_import(  # noqa: PLR0913
     segy_path: str,
     mdio_path: str,
+    mdio_template_name: str,
     header_locations: list[int],
     header_types: list[str],
     header_names: list[str],
-    chunk_size: list[int],
-    lossless: bool,
-    compression_tolerance: float,
     storage_options_input: dict[str, Any],
     storage_options_output: dict[str, Any],
     overwrite: bool,
     grid_overrides: dict[str, Any],
 ) -> None:
-    r"""Ingest SEG-Y file to MDIO.
+    r"""Ingest SEG-Y file to MDIO v1.
 
     SEG-Y format is explained in the "segy" group of the command line interface. To see additional
     information run:
 
     mdio segy --help
 
-    MDIO allows ingesting flattened seismic surveys in SEG-Y format into a multidimensional
+    MDIO v1 allows ingesting flattened seismic surveys in SEG-Y format into a multidimensional
     tensor that represents the correct geometry of the seismic dataset.
 
-    The output MDIO file can be local or on the cloud. For local files, a UNIX or Windows path is
+    The output MDIO v1 file can be local or on the cloud. For local files, a UNIX or Windows path is
     sufficient. However, for cloud stores, an appropriate protocol must be provided. Some examples:
 
     File Path Patterns:
@@ -190,20 +162,6 @@ def segy_import(  # noqa: PLR0913
     names to the index dimensions, and if needed providing the header types if they are not
     standard. By default, all header entries are assumed to be 4-byte long (int32).
 
-    The chunk size depends on the data type, however, it can be chosen to accommodate any
-    workflow's access patterns. See examples below for some common use cases.
-
-    By default, the data is ingested with LOSSLESS compression. This saves disk space in the range
-    of 20% to 40%. MDIO also allows data to be compressed using the ZFP compressor's fixed
-    accuracy lossy compression. If lossless parameter is set to False and MDIO was installed using
-    the lossy extra; then the data will be compressed to approximately 30% of its original size and
-    will be perceptually lossless. The compression amount can be adjusted using the option
-    compression_tolerance (float). Values less than 1 gives good results. The higher the value, the
-    more compression, but will introduce artifacts. The default value is 0.01 tolerance, however we
-    get good results up to 0.5; where data is almost compressed to 10% of its original size. NOTE:
-    This assumes data has amplitudes normalized to have approximately standard deviation of 1. If
-    dataset has values smaller than this tolerance, a lot of loss may occur.
-
     Usage:
 
         Below are some examples of ingesting standard SEG-Y files per the SEG-Y Revision 1 and 2.
@@ -220,14 +178,12 @@ def segy_import(  # noqa: PLR0913
         Chunks: 16 inlines x 16 crosslines x 16 offsets x 512 samples
         --header-locations 189,193,37
         --header-names inline,crossline,offset
-        --chunk-size 16,16,16,512
 
         \b
         2D Seismic Shot Data (Byte Locations Vary):
         Chunks: 16 shots x 256 channels x 512 samples
         --header-locations 9,13
         --header-names shot,chan
-        --chunk-size 16,256,512
 
         \b
         3D Seismic Shot Data (Byte Locations Vary):
@@ -237,7 +193,6 @@ def segy_import(  # noqa: PLR0913
         --header-locations 9,213,13
         --header-names shot,cable,chan
         --header-types int32,int16,int32
-        --chunk-size 8,2,256,512
 
     We can override the dataset grid by the `grid_overrides` parameter. This allows us to ingest
     files that don't conform to the true geometry of the seismic acquisition.
@@ -290,7 +245,6 @@ def segy_import(  # noqa: PLR0913
         --header-locations 21
         --header-names cdp
         --header-types int32
-        --chunk-size 4,1024
         --grid-overrides '{"NonBinned": True, "chunksize": 64}'
 
         \b
@@ -301,7 +255,6 @@ def segy_import(  # noqa: PLR0913
         --header-locations 189,193
         --header-names inline,crossline
         --header-types int32,int32
-        --chunk-size 4,4,1024
         --grid-overrides '{"NonBinned": True, "chunksize": 16}'
 
         \b
@@ -312,21 +265,18 @@ def segy_import(  # noqa: PLR0913
         --header-locations 9,213,13
         --header-names shot,cable,chan
         --header-types int32,int16,int32
-        --chunk-size 8,2,256,512
         --grid-overrides '{"HasDuplicates": True}'
     """
     # Lazy import to reduce CLI startup time
-    from mdio import segy_to_mdio  # noqa: PLC0415
+    from mdio.converters.segy import segy_to_mdio_cli  # noqa: PLC0415
 
-    segy_to_mdio(
+    segy_to_mdio_cli(
         segy_path=segy_path,
         mdio_path_or_buffer=mdio_path,
+        mdio_template_name=mdio_template_name,
         index_bytes=header_locations,
         index_types=header_types,
         index_names=header_names,
-        chunksize=chunk_size,
-        lossless=lossless,
-        compression_tolerance=compression_tolerance,
         storage_options_input=storage_options_input,
         storage_options_output=storage_options_output,
         overwrite=overwrite,
diff --git a/src/mdio/converters/mdio.py b/src/mdio/converters/mdio.py
index 9726cd6b8..25d35e574 100644
--- a/src/mdio/converters/mdio.py
+++ b/src/mdio/converters/mdio.py
@@ -6,11 +6,14 @@
 from pathlib import Path
 from tempfile import TemporaryDirectory
 
+import dask.array as da
 import numpy as np
+import xarray as xr
 from psutil import cpu_count
 from tqdm.dask import TqdmCallback
 
 from mdio import MDIOReader
+from mdio.core.storage_location import StorageLocation
 from mdio.segy.blocked_io import to_segy
 from mdio.segy.creation import concat_files
 from mdio.segy.creation import mdio_spec_to_segy
@@ -171,3 +174,100 @@ def mdio_to_segy(  # noqa: PLR0912, PLR0913
             _ = client.submit(concat_files, paths=ordered_files).result()
         else:
             concat_files(paths=ordered_files, progress=True)
+
+
+def _zero_fill_variable(src_ds: xr.Dataset, trace_variable_name: str) -> None:
+    var_to_zero_fill = src_ds[trace_variable_name]
+    if not hasattr(var_to_zero_fill.data, "chunks"):
+        err = "The source dataset is not dask-backed."
+        raise RuntimeError(err)
+
+    fill_value = var_to_zero_fill.attrs.get("_FillValue", 0)
+    # Create a Dask array with the same shape, dtype, and chunks,
+    # but filled with a constant value.
+    # This operation is lazy and doesn't compute any data yet.
+    empty_array = da.full(
+        var_to_zero_fill.shape, fill_value=fill_value, dtype=var_to_zero_fill.dtype, chunks=var_to_zero_fill.chunks
+    )
+
+    # Replace the 'traceVariableName' DataArray in the dataset with the new dummy array.
+    # This happens in memory (for the Dask graph, not the data itself).
+    src_ds[trace_variable_name] = (var_to_zero_fill.dims, empty_array, var_to_zero_fill.attrs)
+
+
+def copy_mdio(  # noqa: PLR0913
+    source: StorageLocation,
+    destination: StorageLocation,
+    with_traces: bool = True,
+    with_headers: bool = True,
+    overwrite: bool = False,
+) -> None:
+    """Copy an MDIO dataset to another location.
+
+    Copies an MDIO dataset to another location, optionally setting headers and / or
+    traces to the fill values (fill values are not storage-persisted).
+
+    Args:
+        source: The source MDIO dataset location.
+        destination: The destination MDIO dataset location.
+        with_traces: Whether to include traces data in the copy.
+        with_headers: Whether to include headers data in the copy.
+        overwrite: Whether to overwrite the destination if it exists.
+
+    Raises:
+        FileExistsError: If the destination exists and overwrite is False.
+    """
+    # Open source dataset with dask-backed arrays
+    # NOTE: Xarray will convert int to float and replace _FillValue with NaN
+    # NOTE: 'chunks={}' instructs xarray to load the dataset into dask arrays using the engine's
+    # preferred chunk sizes if they are exposed by the backend. 'chunks="auto"' instructs xarray
+    # to use dask's automatic chunking algorithm. This algorithm attempts to determine optimal
+    # chunk sizes based on factors like array size and available memory, while also taking
+    # into account any engine-preferred chunks if they exist.
+    src_ds = xr.open_dataset(source.uri, engine="zarr", chunks="auto", mask_and_scale=False)
+
+    if not overwrite and destination.exists():
+        err = f"Output location '{destination.uri}' exists. Set `overwrite=True` if intended."
+        raise FileExistsError(err)
+
+    if not with_headers:
+        _zero_fill_variable(src_ds, "headers")
+
+    if not with_traces:
+        # The "traceVariableName" must be in attributes for the later versions
+        # HACK: Using a "amplitude" as the default is a temporary workaround since it
+        # might not be defined in early versions
+        trace_variable_name = src_ds.attrs["attributes"].get("traceVariableName", "amplitude")
+        _zero_fill_variable(src_ds, trace_variable_name)
+
+    # Save to destination Zarr store, writing chunk by chunk
+    src_ds.to_zarr(destination.uri, mode="w", compute=True, zarr_format=2)
+
+
+def copy_mdio_cli(  # noqa PLR0913
+    source_mdio_path: str,
+    target_mdio_path: str,
+    overwrite: bool,
+    with_traces: bool,
+    with_headers: bool,
+    storage_options_input: dict | None,
+    storage_options_output: dict | None,
+) -> None:
+    """CLI wrapper for copy_mdio function.
+
+    Args:
+        source_mdio_path: Path to the source MDIO dataset location.
+        target_mdio_path: Path to the destination MDIO dataset location.
+        overwrite: Whether to overwrite the destination if it exists.
+        with_traces: Whether to include traces in the copy.
+        with_headers: Whether to include headers in the copy.
+        storage_options_input: Cloud storage options for the source MDIO dataset.
+        storage_options_output: Cloud storage options for the destination MDIO dataset.
+    """
+    copy_mdio(
+        source=StorageLocation(source_mdio_path, storage_options_input),
+        destination=StorageLocation(target_mdio_path, storage_options_output),
+        with_traces=with_traces,
+        with_headers=with_headers,
+        overwrite=overwrite,
+    )
diff --git a/src/mdio/converters/segy.py b/src/mdio/converters/segy.py
index 6a1d7df7e..31090b376 100644
--- a/src/mdio/converters/segy.py
+++ b/src/mdio/converters/segy.py
@@ -5,10 +5,13 @@
 import logging
 import os
 from typing import TYPE_CHECKING
+from typing import Any
 
 import numpy as np
 from segy import SegyFile
 from segy.config import SegySettings
+from segy.schema import HeaderField
+from segy.standards import get_segy_standard
 from segy.standards.codes import MeasurementSystem as segy_MeasurementSystem
 from segy.standards.fields.trace import Rev0 as TraceHeaderFieldsRev0
 
@@ -18,7 +21,9 @@
 from mdio.converters.exceptions import GridTraceSparsityError
 from mdio.converters.type_converter import to_structured_type
 from mdio.core.grid import Grid
+from mdio.core.storage_location import StorageLocation
 from mdio.schemas.v1.dataset_serializer import to_xarray_dataset
+from mdio.schemas.v1.templates.template_registry import TemplateRegistry
 from mdio.schemas.v1.units import AllUnits
 from mdio.schemas.v1.units import LengthUnitEnum
 from mdio.schemas.v1.units import LengthUnitModel
@@ -26,12 +31,13 @@
 from mdio.segy.utilities import get_grid_plan
 
 if TYPE_CHECKING:
+    from collections.abc import Sequence
+
     from segy.arrays import HeaderArray as SegyHeaderArray
     from segy.schema import SegySpec
     from xarray import Dataset as xr_Dataset
 
     from mdio.core.dimension import Dimension
-    from mdio.core.storage_location import StorageLocation
     from mdio.schemas.v1.dataset import Dataset
     from mdio.schemas.v1.templates.abstract_dataset_template import AbstractDatasetTemplate
 
@@ -397,3 +403,63 @@ def segy_to_mdio(
         dataset=xr_dataset,
         data_variable_name=data_variable_name,
     )
+
+
+def segy_to_mdio_cli(  # noqa PLR0913
+    segy_path: str,
+    mdio_path_or_buffer: str,
+    mdio_template_name: str,
+    index_bytes: Sequence[int],
+    index_names: Sequence[str] | None = None,
+    index_types: Sequence[str] | None = None,
+    storage_options_input: dict[str, Any] | None = None,
+    storage_options_output: dict[str, Any] | None = None,
+    overwrite: bool = False,
+    grid_overrides: dict | None = None,  # NOOP for now
+) -> None:
+    """Convert SEG-Y to MDIO format invoked from the command line.
+
+    Args:
+        segy_path: Path to the input SEG-Y file.
+        mdio_path_or_buffer: Path to the output MDIO file or a buffer.
+        mdio_template_name: Name of the MDIO template to use.
+        index_bytes: Tuple of the byte location for the index attributes
+        index_names: List of names for the index dimensions. If not provided, defaults to `dim_0`,
+            `dim_1`, ..., with the last dimension named `sample`.
+        index_types: Tuple of the data-types for the index attributes. Must be in {"int16, int32,
+            float16, float32, ibm32"}. Default is 4-byte integers for each index key.
+        storage_options_input: Additional cloud storage options for the input location.
+        storage_options_output: Additional cloud storage options for the output location.
+        overwrite: If True, overwrite the output file if it exists.
+        grid_overrides: Optional dictionary to override grid parameters.
+
+    Raises:
+        ValueError: If the index fields are not consistent.
+    """
+    mdio_template = TemplateRegistry.get_instance().get(mdio_template_name)
+
+    segy_spec = get_segy_standard(1.0)
+    if index_bytes:
+        index_names = index_names or [f"dim_{i}" for i in range(len(index_bytes))]
+        index_types = index_types or ["int32"] * len(index_bytes)
+        if not (len(index_names) == len(index_bytes) == len(index_types)):
+            err = "All index fields must have the same length."
+            raise ValueError(err)
+        # Index the dataset using a spec that interprets the user provided index headers.
+        index_fields = []
+        for name, byte, format_ in zip(index_names, index_bytes, index_types, strict=True):
+            index_fields.append(HeaderField(name=name, byte=byte, format=format_))
+        segy_spec = segy_spec.customize(trace_header_fields=index_fields)
+
+    # Remove the following when grid overrides are implemented
+    # TODO(Dmitriy): implement grid overrides
+    # https://github.com/TGSAI/mdio-python/issues/585
+    _ = grid_overrides
+    segy_to_mdio(
+        segy_spec=segy_spec,
+        mdio_template=mdio_template,
+        input_location=StorageLocation(uri=segy_path, options=storage_options_input),
+        output_location=StorageLocation(uri=mdio_path_or_buffer, options=storage_options_output),
+        overwrite=overwrite,
+        # grid_overrides=grid_overrides
+    )
diff --git a/src/mdio/core/storage_location.py b/src/mdio/core/storage_location.py
index 3a5adb2a1..511c64a84 100644
--- a/src/mdio/core/storage_location.py
+++ b/src/mdio/core/storage_location.py
@@ -30,6 +30,9 @@ def __init__(self, uri: str = "", options: dict[str, Any] = None):
         if uri.startswith(("s3://", "gs://")):
             return
 
+        if uri.startswith(("http://", "https://")):
+            return
+
         if uri.startswith("file://"):
             self._uri = self._uri.removeprefix("file://")
         # For local paths, ensure they are absolute and resolved
diff --git a/tests/conftest.py b/tests/conftest.py
index 6f4f2d1f3..0bf03032c 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -49,6 +49,12 @@ def zarr_tmp(tmp_path_factory: pytest.TempPathFactory) -> Path:
     return tmp_path_factory.mktemp(r"mdio")
 
 
+@pytest.fixture(scope="module")
+def mdio_copy_tmp(tmp_path_factory: pytest.TempPathFactory) -> Path:
+    """Make a temp file for the copy of the MDIO. dataset."""
+    return tmp_path_factory.mktemp(r"mdio_copy")
+
+
 @pytest.fixture(scope="module")
 def zarr_tmp2(tmp_path_factory: pytest.TempPathFactory) -> Path:
     """Make a temp file for the output MDIO."""
diff --git a/tests/integration/test_segy_import_export.py b/tests/integration/test_segy_import_export.py
index e80028df1..5a8ce157e 100644
--- a/tests/integration/test_segy_import_export.py
+++ b/tests/integration/test_segy_import_export.py
@@ -4,7 +4,7 @@
 
 import json
 import os
-from typing import TYPE_CHECKING
+from pathlib import Path
 
 import dask
 import numpy as np
@@ -23,6 +23,7 @@
 from mdio import MDIOReader
 from mdio import mdio_to_segy
 from mdio.converters.exceptions import GridTraceSparsityError
+from mdio.converters.mdio import copy_mdio
 from mdio.converters.segy import segy_to_mdio
 from mdio.core import Dimension
 from mdio.core.storage_location import StorageLocation
@@ -30,9 +31,6 @@
 from mdio.segy.compat import mdio_segy_spec
 from mdio.segy.geometry import StreamerShotGeometryType
 
-if TYPE_CHECKING:
-    from pathlib import Path
-
 dask.config.set(scheduler="synchronous")
 
 
@@ -403,6 +401,25 @@ def test_zslice(self, zarr_tmp: Path) -> None:
         npt.assert_allclose([mean, std], [0.005236923, 0.61279935])
 
 
+@pytest.mark.dependency("test_3d_import")
+@pytest.mark.parametrize("with_headers", [True, False], ids=["with_headers", "no_headers"])
+@pytest.mark.parametrize("with_traces", [True, False], ids=["with_traces", "no_traces"])
+class TestCopy:
+    """Test MDIO copy functionality."""
+
+    def test_mdio_copy(self, zarr_tmp: Path, mdio_copy_tmp: Path, with_headers: str, with_traces: str) -> None:
+        """Test mdio copy."""
+        copy_mdio(  # noqa: PLR0913
+            source=StorageLocation(str(zarr_tmp)),
+            destination=StorageLocation(str(mdio_copy_tmp)),
+            with_headers=with_headers,
+            with_traces=with_traces,
+            overwrite=True,
+        )
+        assert with_traces == Path(f"{mdio_copy_tmp}/amplitude/0.0.0").exists()
+        assert with_headers == Path(f"{mdio_copy_tmp}/headers/0.0").exists()
+
+
 @pytest.mark.dependency("test_3d_import")
 class TestExport:
     """Test SEG-Y exporting functionaliy."""
diff --git a/tests/test_main.py b/tests/test_main.py
index 1685c0110..b7e24b28f 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -18,9 +18,10 @@ def runner() -> CliRunner:
 @pytest.mark.dependency
 def test_main_succeeds(runner: CliRunner, segy_input: Path, zarr_tmp: Path) -> None:
     """It exits with a status code of zero."""
-    cli_args = ["segy", "import", str(segy_input), str(zarr_tmp)]
-    cli_args.extend(["--header-locations", "181,185"])
-    cli_args.extend(["--header-names", "inline,crossline"])
+    cli_args = ["segy", "import", str(segy_input), str(zarr_tmp), "PostStack3DTime"]
+    cli_args.extend(["--header-locations", "17,13,81,85"])
+    cli_args.extend(["--header-names", "inline,crossline,cdp_x,cdp_y"])
+    cli_args.extend(["--overwrite"])
 
     result = runner.invoke(__main__.main, args=cli_args)
     assert result.exit_code == 0
@@ -30,9 +31,9 @@ def test_main_succeeds(runner: CliRunner, segy_input: Path, zarr_tmp: Path) -> N
 def test_main_cloud(runner: CliRunner, segy_input_uri: str, zarr_tmp: Path) -> None:
     """It exits with a status code of zero."""
     os.environ["MDIO__IMPORT__CLOUD_NATIVE"] = "true"
-    cli_args = ["segy", "import", segy_input_uri, str(zarr_tmp)]
-    cli_args.extend(["--header-locations", "181,185"])
-    cli_args.extend(["--header-names", "inline,crossline"])
+    cli_args = ["segy", "import", segy_input_uri, str(zarr_tmp), "PostStack3DTime"]
+    cli_args.extend(["--header-locations", "17,13,81,85"])
+    cli_args.extend(["--header-names", "inline,crossline,cdp_x,cdp_y"])
     cli_args.extend(["--overwrite"])
 
     result = runner.invoke(__main__.main, args=cli_args)
@@ -47,15 +48,45 @@ def test_main_info_succeeds(runner: CliRunner, zarr_tmp: Path) -> None:
 
     result = runner.invoke(__main__.main, args=cli_args)
     assert result.exit_code == 0
+    exp_output = """\
+<xarray.Dataset> Size: 392MB
+Dimensions:     (inline: 345, crossline: 188, time: 1501)
+Coordinates:
+    cdp_x       (inline, crossline) float64 519kB dask.array<chunksize=(345, 188), meta=np.ndarray>
+    cdp_y       (inline, crossline) float64 519kB dask.array<chunksize=(345, 188), meta=np.ndarray>
+  * crossline   (crossline) int32 752B 1 2 3 4 5 6 7 ... 183 184 185 186 187 188
+  * inline      (inline) int32 1kB 1 2 3 4 5 6 7 ... 339 340 341 342 343 344 345
+  * time        (time) int32 6kB 0 2 4 6 8 10 ... 2990 2992 2994 2996 2998 3000
+Data variables:
+    amplitude   (inline, crossline, time) float32 389MB dask.array<chunksize=(128, 128, 128), meta=np.ndarray>
+    headers     (inline, crossline) [('inline', '<i4'), ('crossline', '<i4'), ('cdp_x', '<i4'), ('cdp_y', '<i4')] 1MB dask.array<chunksize=(128, 128), meta=np.ndarray>
+    trace_mask  (inline, crossline) bool 65kB dask.array<chunksize=(345, 188), meta=np.ndarray>
+Attributes:
+    apiVersion:  1.0.0a1
+    createdOn:   2025-09-03 04:07:20.025836+00:00
+    name:        PostStack3DTime
+    attributes:  {'surveyDimensionality': '3D', 'ensembleType': 'line', 'proc...
+"""  # noqa: E501
+    # Remove the 'createdOn:' line that changes at every test run
+    expected = [item for item in exp_output.splitlines() if not item.startswith("    createdOn:")]
+    actual = [item for item in result.output.splitlines() if not item.startswith("    createdOn:")]
+    assert actual == expected
 
 
 @pytest.mark.dependency(depends=["test_main_succeeds"])
 def test_main_copy(runner: CliRunner, zarr_tmp: Path, zarr_tmp2: Path) -> None:
-    """It exits with a status code of zero."""
-    cli_args = ["copy", str(zarr_tmp), str(zarr_tmp2), "-headers", "-traces"]
+    """It exits with a status code of zero.
+
+    This tests depends on the test_main_succeeds to generate the input file
+    """
+    # We do not specify "-headers", "-traces" flags here,
+    # Thus, the dataset should be copied with FillValues in the traces and header.
+    cli_args = ["copy", str(zarr_tmp), str(zarr_tmp2), "--overwrite"]
 
     result = runner.invoke(__main__.main, args=cli_args)
     assert result.exit_code == 0
+    assert not Path(f"{zarr_tmp2}/amplitude/0.0.0").exists()
+    assert not Path(f"{zarr_tmp2}/headers/0.0").exists()
 
 
 def test_cli_version(runner: CliRunner) -> None:
