Skip to main content
Glama
jbroll

MCP Build Environment Service

by jbroll

read_file

Read file contents from repositories with optional line range selection and branch isolation for secure build environment access.

Instructions

Read the contents of a file in a repository. Supports reading specific line ranges for large files. If branch is specified, creates/uses a hidden worktree (.repo@branch) for isolation.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
repoYesRepository name (required)
pathYesAbsolute path to the file to read (required)
start_lineNoStarting line number (1-indexed, optional). If provided, only lines from start_line to end_line will be returned.
end_lineNoEnding line number (1-indexed, optional). If provided, only lines from start_line to end_line will be returned.
branchNoGit branch name (optional). If provided, uses isolated worktree.

Implementation Reference

  • The core handler function `handle_read_file` that implements the read_file tool logic: extracts arguments, gets repo worktree path with locking, validates file path using helper, reads file content with optional start_line/end_line range, formats output with line numbers, total lines, and file metadata.
    async def handle_read_file(self, args: Dict[str, Any]) -> List[TextContent]:
        """Handle read_file command"""
        repo = args.get("repo")
        branch = args.get("branch")
        file_path = args.get("path")
        start_line = args.get("start_line")
        end_line = args.get("end_line")
    
        if not file_path:
            raise ValueError("File path is required")
    
        # Get working path with locking
        lock_key = self._get_lock_key(repo, branch)
        async with self.worktree_locks[lock_key]:
            repo_path = await self.get_working_path(repo, branch)
    
            # Validate and resolve the file path
            validated_path = validate_file_path(file_path, repo_path)
    
        # Read the file
        try:
            with open(validated_path, 'r', encoding='utf-8', errors='replace') as f:
                if start_line is not None or end_line is not None:
                    # Read specific line range
                    lines = f.readlines()
                    total_lines = len(lines)
    
                    # Validate line numbers
                    if start_line is not None and start_line < 1:
                        raise ValueError(f"start_line must be >= 1, got {start_line}")
                    if end_line is not None and end_line < 1:
                        raise ValueError(f"end_line must be >= 1, got {end_line}")
                    if start_line is not None and end_line is not None and start_line > end_line:
                        raise ValueError(f"start_line ({start_line}) must be <= end_line ({end_line})")
    
                    # Default values
                    start_idx = (start_line - 1) if start_line is not None else 0
                    end_idx = end_line if end_line is not None else total_lines
    
                    # Clamp to valid range
                    start_idx = max(0, min(start_idx, total_lines))
                    end_idx = max(0, min(end_idx, total_lines))
    
                    # Extract the requested lines
                    selected_lines = lines[start_idx:end_idx]
    
                    # Format output with line numbers
                    output = f"File: {validated_path}\n"
                    output += f"Lines {start_idx + 1}-{end_idx} of {total_lines}\n"
                    output += "=" * 80 + "\n"
                    for i, line in enumerate(selected_lines, start=start_idx + 1):
                        output += f"{i:6d}: {line.rstrip()}\n"
    
                    return [TextContent(type="text", text=output)]
                else:
                    # Read entire file
                    content = f.read()
                    lines_count = content.count('\n') + (1 if content and not content.endswith('\n') else 0)
    
                    output = f"File: {validated_path}\n"
                    output += f"Total lines: {lines_count}\n"
                    output += "=" * 80 + "\n"
    
                    # Add line numbers to entire file
                    for i, line in enumerate(content.splitlines(), start=1):
                        output += f"{i:6d}: {line}\n"
    
                    return [TextContent(type="text", text=output)]
    
        except FileNotFoundError:
            raise FileNotFoundError(f"File not found: {validated_path}")
        except PermissionError:
            raise PermissionError(f"Permission denied reading file: {validated_path}")
        except Exception as e:
            raise Exception(f"Error reading file {validated_path}: {str(e)}")
  • The JSON inputSchema and Tool metadata definition for the read_file tool, returned by the list_tools() MCP method to register the tool's schema and description.
    Tool(
        name="env",
        description="Show environment information including environment variables "
                   "and versions of key build tools (gcc, g++, python, make, cmake, etc.). "
                   "If branch is specified, creates/uses a hidden worktree (.repo@branch) for isolation.",
        inputSchema={
            "type": "object",
            "properties": {
                "repo": {
                    "type": "string",
                    "description": "Repository name (required)"
                },
                "branch": {
                    "type": "string",
                    "description": "Git branch name (optional). If provided, uses isolated worktree."
                }
            },
            "required": ["repo"]
        }
    ),
    Tool(
        name="read_file",
        description="Read the contents of a file in a repository. "
                   "Supports reading specific line ranges for large files. "
                   "If branch is specified, creates/uses a hidden worktree (.repo@branch) for isolation.",
        inputSchema={
            "type": "object",
            "properties": {
                "repo": {
                    "type": "string",
                    "description": "Repository name (required)"
                },
                "path": {
                    "type": "string",
                    "description": "Absolute path to the file to read (required)"
                },
                "start_line": {
                    "type": "integer",
                    "description": "Starting line number (1-indexed, optional). If provided, only lines from start_line to end_line will be returned."
                },
                "end_line": {
                    "type": "integer",
                    "description": "Ending line number (1-indexed, optional). If provided, only lines from start_line to end_line will be returned."
                },
                "branch": {
                    "type": "string",
                    "description": "Git branch name (optional). If provided, uses isolated worktree."
                }
            },
            "required": ["repo", "path"]
        }
    )
  • src/server.py:463-466 (registration)
    Dispatch/registration logic in the call_tool() MCP handler that routes calls to the 'read_file' tool to its specific handler function.
    elif name == "read_file":
        return await self.handle_read_file(arguments)
    else:
        raise ValueError(f"Unknown tool: {name}")
  • Supporting helper `validate_file_path` that ensures the requested file path resolves within the repository root, blocks command injection patterns and path traversal attempts (used at line 569 in handler).
    def validate_file_path(file_path: str, repo_path: Path) -> Path:
        """
        Validate a file path for reading and ensure it's within the repository.
    
        This validator accepts both absolute and relative paths but ensures that
        the final resolved path is within the repository directory.
    
        Args:
            file_path: The file path to validate (can be absolute or relative)
            repo_path: The repository root path
    
        Returns:
            Path: The validated absolute path to the file
    
        Raises:
            ValueError: If path contains dangerous patterns or escapes the repository
        """
        if not file_path:
            raise ValueError("File path cannot be empty")
    
        # Check for dangerous command injection patterns
        # We're less strict than validate_path since we're only reading files
        dangerous_for_file_read = [
            r";",      # Command chaining
            r"\|",     # Pipes
            r"&",      # Background/chaining
            r"`",      # Command substitution
            r"\$\(",   # Command substitution
        ]
    
        for pattern in dangerous_for_file_read:
            if re.search(pattern, file_path):
                raise ValueError(f"File path contains dangerous pattern: {file_path}")
    
        # Convert to Path object
        path_obj = Path(file_path)
    
        # Resolve the path to absolute
        if path_obj.is_absolute():
            # Absolute path - resolve it
            resolved_path = path_obj.resolve()
        else:
            # Relative path - resolve relative to repo
            resolved_path = (repo_path / path_obj).resolve()
    
        # Ensure the resolved path is within the repository
        repo_path_resolved = repo_path.resolve()
        try:
            # This will raise ValueError if resolved_path is not relative to repo_path
            resolved_path.relative_to(repo_path_resolved)
        except ValueError:
            raise ValueError(
                f"Access denied: Path '{file_path}' resolves to '{resolved_path}' "
                f"which is outside repository '{repo_path_resolved}'"
            )
    
        # Additional check: ensure no parent directory traversal in original path
        # This catches things like "foo/../../etc/passwd" even if they resolve safely
        if ".." in path_obj.parts:
            # But we need to verify it doesn't escape
            try:
                if path_obj.is_absolute():
                    check_path = path_obj.resolve()
                else:
                    check_path = (repo_path / path_obj).resolve()
                check_path.relative_to(repo_path_resolved)
            except ValueError:
                raise ValueError(f"Path traversal not allowed: {file_path}")
    
        return resolved_path
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries full burden. It discloses key behavioral traits: the ability to read specific line ranges and the creation/use of hidden worktrees for branch isolation. However, it doesn't mention error conditions, permissions needed, or what happens with non-existent files/branches.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two well-structured sentences with zero waste. First sentence states core purpose with key feature. Second sentence explains branch isolation behavior. Every word earns its place.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 5-parameter tool with no annotations and no output schema, the description provides adequate context about what the tool does but lacks information about return format, error handling, or performance characteristics. It covers the basic operation but leaves gaps in full behavioral understanding.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, providing complete parameter documentation. The description adds minimal value beyond the schema, mentioning line range support and branch isolation worktree behavior, but doesn't provide additional semantic context about parameter interactions or usage patterns.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the specific action ('Read the contents of a file') and resource ('in a repository'), with additional functionality ('Supports reading specific line ranges for large files'). It distinguishes from potential siblings by specifying file reading rather than listing or other operations.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies usage for reading file contents with optional line range filtering and branch isolation, but doesn't explicitly state when to use this tool versus alternatives like 'list' or 'ls' for directory contents. No explicit exclusions or comparisons to sibling tools are provided.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jbroll/mcp-build'

If you have feedback or need assistance with the MCP directory API, please join our Discord server