Skip to content

Read file

read_file

ReadFileTool — read file content with optional line-range support.

Registered as read_file via the axm.tools entry point.

ReadFileTool

Read file content with optional line-range support.

Returns file content with line numbers. Supports partial reads via start_line / end_line (1-indexed, inclusive). Registered as read_file via axm.tools entry point.

Source code in packages/axm-edit/src/axm_edit/tools/read_file.py
Python
class ReadFileTool:
    """Read file content with optional line-range support.

    Returns file content with line numbers. Supports partial reads
    via ``start_line`` / ``end_line`` (1-indexed, inclusive).
    Registered as ``read_file`` via axm.tools entry point.
    """

    agent_hint: str = (
        "Read file content with line numbers. Supports partial reads"
        " via start_line/end_line (1-indexed)."
        " Use for raw source when ast_inspect is insufficient."
    )

    @property
    def name(self) -> str:
        """Tool name used for MCP registration."""
        return "read_file"

    def execute(
        self,
        *,
        path: str = ".",
        file: str | None = None,
        start_line: int | None = None,
        end_line: int | None = None,
        **kwargs: object,
    ) -> ToolResult:
        """Read a file, optionally restricting to a line range.

        Args:
            path: Project root directory.
            file: Relative path to the file to read.
            start_line: Optional 1-indexed start line (inclusive).
            end_line: Optional 1-indexed end line (inclusive).

        Returns:
            ToolResult with file content, line numbers, and metadata.
        """
        root_str = path
        file_rel = file

        if not file_rel:
            return ToolResult(success=False, error="Missing required argument: file")

        # Resolve and validate file path
        result = _resolve_file(root_str, file_rel)
        if isinstance(result, ToolResult):
            return result
        resolved = result

        # Validate line range
        range_error = _validate_line_range(start_line, end_line)
        if range_error:
            return ToolResult(success=False, error=range_error)

        # Read content
        try:
            text = resolved.read_text(encoding="utf-8")
        except UnicodeDecodeError:
            return ToolResult(
                success=False,
                error=f"Cannot decode file as UTF-8: {file_rel}",
            )
        except OSError as exc:
            return ToolResult(
                success=False,
                error=f"Read failed: {file_rel}: {exc}",
            )

        all_lines = text.splitlines(keepends=True)
        selected, first_line_num = _select_lines(all_lines, start_line, end_line)

        # Cap unbounded reads so a large file cannot exceed the MCP
        # transport limit. Only applies when no explicit range was given.
        capped = False
        if (
            start_line is None
            and end_line is None
            and len(selected) > _DEFAULT_MAX_LINES
        ):
            selected = selected[:_DEFAULT_MAX_LINES]
            capped = True

        content = _format_numbered(selected, first_line_num)

        logger.debug("read %s: %d/%d lines", file_rel, len(selected), len(all_lines))

        total_lines = len(all_lines)
        start = first_line_num
        end = first_line_num + len(selected) - 1

        if capped:
            content += (
                f"\n[truncated: {total_lines} lines total, showing first "
                f"{_DEFAULT_MAX_LINES}; use start_line/end_line for more]"
            )

        return ToolResult(
            success=True,
            data={
                "content": content,
                "file": file_rel,
                "total_lines": total_lines,
                "truncated": capped,
                "showing": {
                    "start": start,
                    "end": end,
                    "count": len(selected),
                },
            },
            text=render_text(
                file_rel=file_rel,
                content=content,
                total_lines=total_lines,
                start=start,
                end=end,
            ),
        )
name property

Tool name used for MCP registration.

execute(*, path='.', file=None, start_line=None, end_line=None, **kwargs)

Read a file, optionally restricting to a line range.

Parameters:

Name Type Description Default
path str

Project root directory.

'.'
file str | None

Relative path to the file to read.

None
start_line int | None

Optional 1-indexed start line (inclusive).

None
end_line int | None

Optional 1-indexed end line (inclusive).

None

Returns:

Type Description
ToolResult

ToolResult with file content, line numbers, and metadata.

Source code in packages/axm-edit/src/axm_edit/tools/read_file.py
Python
def execute(
    self,
    *,
    path: str = ".",
    file: str | None = None,
    start_line: int | None = None,
    end_line: int | None = None,
    **kwargs: object,
) -> ToolResult:
    """Read a file, optionally restricting to a line range.

    Args:
        path: Project root directory.
        file: Relative path to the file to read.
        start_line: Optional 1-indexed start line (inclusive).
        end_line: Optional 1-indexed end line (inclusive).

    Returns:
        ToolResult with file content, line numbers, and metadata.
    """
    root_str = path
    file_rel = file

    if not file_rel:
        return ToolResult(success=False, error="Missing required argument: file")

    # Resolve and validate file path
    result = _resolve_file(root_str, file_rel)
    if isinstance(result, ToolResult):
        return result
    resolved = result

    # Validate line range
    range_error = _validate_line_range(start_line, end_line)
    if range_error:
        return ToolResult(success=False, error=range_error)

    # Read content
    try:
        text = resolved.read_text(encoding="utf-8")
    except UnicodeDecodeError:
        return ToolResult(
            success=False,
            error=f"Cannot decode file as UTF-8: {file_rel}",
        )
    except OSError as exc:
        return ToolResult(
            success=False,
            error=f"Read failed: {file_rel}: {exc}",
        )

    all_lines = text.splitlines(keepends=True)
    selected, first_line_num = _select_lines(all_lines, start_line, end_line)

    # Cap unbounded reads so a large file cannot exceed the MCP
    # transport limit. Only applies when no explicit range was given.
    capped = False
    if (
        start_line is None
        and end_line is None
        and len(selected) > _DEFAULT_MAX_LINES
    ):
        selected = selected[:_DEFAULT_MAX_LINES]
        capped = True

    content = _format_numbered(selected, first_line_num)

    logger.debug("read %s: %d/%d lines", file_rel, len(selected), len(all_lines))

    total_lines = len(all_lines)
    start = first_line_num
    end = first_line_num + len(selected) - 1

    if capped:
        content += (
            f"\n[truncated: {total_lines} lines total, showing first "
            f"{_DEFAULT_MAX_LINES}; use start_line/end_line for more]"
        )

    return ToolResult(
        success=True,
        data={
            "content": content,
            "file": file_rel,
            "total_lines": total_lines,
            "truncated": capped,
            "showing": {
                "start": start,
                "end": end,
                "count": len(selected),
            },
        },
        text=render_text(
            file_rel=file_rel,
            content=content,
            total_lines=total_lines,
            start=start,
            end=end,
        ),
    )

render_text(*, file_rel, content, total_lines, start, end)

Render a compact LLM-facing view: header + verbatim numbered content.

The header carries the path and the line range actually shown so the reader can tell whether the full file was returned or only a slice. content is embedded verbatim (already line-numbered) so no file content is ever lost relative to data['content'].

Source code in packages/axm-edit/src/axm_edit/tools/read_file.py
Python
def render_text(
    *,
    file_rel: str,
    content: str,
    total_lines: int,
    start: int,
    end: int,
) -> str:
    """Render a compact LLM-facing view: header + verbatim numbered content.

    The header carries the path and the line range actually shown so the
    reader can tell whether the full file was returned or only a slice.
    ``content`` is embedded verbatim (already line-numbered) so no file
    content is ever lost relative to ``data['content']``.
    """
    is_partial = not (start == 1 and end == total_lines)
    span = f"L{start}-{end} of {total_lines}" if is_partial else f"{total_lines} lines"
    return f"{file_rel} ({span})\n{content}"