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.
    """

    expose_directly = True

    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 _TOOL_NAME

    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 _failure_result(
                _ReadFileError.MISSING_ARG,
                "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 _failure_result(_ReadFileError.BAD_RANGE, range_error)

        # Read content
        text = _read_text(resolved, file_rel)
        if isinstance(text, ToolResult):
            return text

        all_lines = text.splitlines(keepends=True)
        total_lines = len(all_lines)
        if start_line is not None and start_line > total_lines:
            error = (
                f"Invalid range: start_line ({start_line}) exceeds "
                f"file length ({total_lines})"
            )
            return _failure_result(_ReadFileError.BAD_RANGE, error)

        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.
        selected, capped = _cap_selection(selected, start_line, end_line)

        content = _format_numbered(selected, first_line_num)

        logger.debug("read %s: %d/%d lines", file_rel, len(selected), 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 _failure_result(
            _ReadFileError.MISSING_ARG,
            "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 _failure_result(_ReadFileError.BAD_RANGE, range_error)

    # Read content
    text = _read_text(resolved, file_rel)
    if isinstance(text, ToolResult):
        return text

    all_lines = text.splitlines(keepends=True)
    total_lines = len(all_lines)
    if start_line is not None and start_line > total_lines:
        error = (
            f"Invalid range: start_line ({start_line}) exceeds "
            f"file length ({total_lines})"
        )
        return _failure_result(_ReadFileError.BAD_RANGE, error)

    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.
    selected, capped = _cap_selection(selected, start_line, end_line)

    content = _format_numbered(selected, first_line_num)

    logger.debug("read %s: %d/%d lines", file_rel, len(selected), 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}"