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
| Name | Required | Description | Default |
|---|---|---|---|
| repo | Yes | Repository name (required) | |
| path | Yes | Absolute path to the file to read (required) | |
| start_line | No | Starting line number (1-indexed, optional). If provided, only lines from start_line to end_line will be returned. | |
| end_line | No | Ending line number (1-indexed, optional). If provided, only lines from start_line to end_line will be returned. | |
| branch | No | Git branch name (optional). If provided, uses isolated worktree. |
Implementation Reference
- src/server.py:552-627 (handler)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)}")
- src/server.py:396-447 (schema)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}")
- src/validators.py:172-242 (helper)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