grep_files
Search files for text patterns using regex, filters, and context lines to find specific content within directories.
Instructions
Search for pattern in files, similar to grep.
Args:
path: Starting directory or file path
pattern: Text or regex pattern to search for
is_regex: Whether to treat pattern as regex
case_sensitive: Whether search is case sensitive
whole_word: Match whole words only
include_patterns: Only include files matching these patterns
exclude_patterns: Exclude files matching these patterns
context_lines: Number of lines to show before AND after matches (like grep -C)
context_before: Number of lines to show BEFORE matches (like grep -B)
context_after: Number of lines to show AFTER matches (like grep -A)
results_offset: Start at Nth match (0-based, for pagination)
results_limit: Return at most this many matches (for pagination)
max_results: Maximum total matches to find during search
max_file_size_mb: Skip files larger than this size
recursive: Whether to search subdirectories
max_depth: Maximum directory depth to recurse
count_only: Only show match counts per file
format: Output format ('text' or 'json')
ctx: MCP context
Returns:
Search results
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| path | Yes | ||
| pattern | Yes | ||
| is_regex | No | ||
| case_sensitive | No | ||
| whole_word | No | ||
| include_patterns | No | ||
| exclude_patterns | No | ||
| context_lines | No | ||
| context_before | No | ||
| context_after | No | ||
| results_offset | No | ||
| results_limit | No | ||
| max_results | No | ||
| max_file_size_mb | No | ||
| recursive | No | ||
| max_depth | No | ||
| count_only | No | ||
| format | No | text |
Implementation Reference
- mcp_filesystem/server.py:707-803 (registration)Registration of the 'grep_files' MCP tool. This decorator defines the tool schema via function parameters and serves as the entrypoint, instantiating GrepTools via get_components() and calling its grep_files method.async def grep_files( path: str, pattern: str, ctx: Context, is_regex: bool = False, case_sensitive: bool = True, whole_word: bool = False, include_patterns: Optional[List[str]] = None, exclude_patterns: Optional[List[str]] = None, context_lines: int = 0, context_before: int = 0, context_after: int = 0, results_offset: int = 0, results_limit: Optional[int] = None, max_results: int = 1000, max_file_size_mb: float = 10, recursive: bool = True, max_depth: Optional[int] = None, count_only: bool = False, format: str = "text", ) -> str: """Search for pattern in files, similar to grep. Args: path: Starting directory or file path pattern: Text or regex pattern to search for is_regex: Whether to treat pattern as regex case_sensitive: Whether search is case sensitive whole_word: Match whole words only include_patterns: Only include files matching these patterns exclude_patterns: Exclude files matching these patterns context_lines: Number of lines to show before AND after matches (like grep -C) context_before: Number of lines to show BEFORE matches (like grep -B) context_after: Number of lines to show AFTER matches (like grep -A) results_offset: Start at Nth match (0-based, for pagination) results_limit: Return at most this many matches (for pagination) max_results: Maximum total matches to find during search max_file_size_mb: Skip files larger than this size recursive: Whether to search subdirectories max_depth: Maximum directory depth to recurse count_only: Only show match counts per file format: Output format ('text' or 'json') ctx: MCP context Returns: Search results """ try: components = get_components() # Fix regex escaping - if is_regex is True, handle backslash escaping pattern_fixed = pattern if is_regex and "\\" in pattern: # For patterns coming from JSON where backslashes are escaped, # we need to convert double backslashes to single backslashes pattern_fixed = pattern.replace("\\\\", "\\") results = await components["grep"].grep_files( path, pattern_fixed, is_regex, case_sensitive, whole_word, include_patterns, exclude_patterns, context_lines, context_before, context_after, max_results, max_file_size_mb, recursive, max_depth, count_only, results_offset=results_offset, results_limit=results_limit, ) if format.lower() == "json": return json.dumps(results.to_dict(), indent=2) else: # Format as text with appropriate options show_line_numbers = True show_file_names = True show_context = context_lines > 0 highlight = True return results.format_text( show_line_numbers=show_line_numbers, show_file_names=show_file_names, count_only=count_only, show_context=show_context, highlight=highlight, ) except Exception as e: return f"Error searching files: {str(e)}"
- mcp_filesystem/grep.py:251-351 (handler)Core handler implementation: GrepTools.grep_files method. Validates path, checks for ripgrep availability, and dispatches to ripgrep or Python fallback implementations.async def grep_files( self, path: Union[str, Path], pattern: str, is_regex: bool = False, case_sensitive: bool = True, whole_word: bool = False, include_patterns: Optional[List[str]] = None, exclude_patterns: Optional[List[str]] = None, context_lines: int = 0, context_before: int = 0, context_after: int = 0, max_results: int = 1000, max_file_size_mb: float = 10, recursive: bool = True, max_depth: Optional[int] = None, count_only: bool = False, results_offset: int = 0, results_limit: Optional[int] = None, show_progress: bool = False, progress_callback: Optional[Callable[[int, int], Any]] = None, ) -> GrepResult: """Search for pattern in files, similar to grep. Args: path: Starting directory or file path pattern: Text or regex pattern to search for is_regex: Whether to treat pattern as regex case_sensitive: Whether search is case sensitive whole_word: Match whole words only include_patterns: Only include files matching these patterns exclude_patterns: Exclude files matching these patterns context_lines: Number of lines to show before AND after matches (like grep -C) context_before: Number of lines to show BEFORE matches (like grep -B) context_after: Number of lines to show AFTER matches (like grep -A) max_results: Maximum total matches to find during search max_file_size_mb: Skip files larger than this size recursive: Whether to search subdirectories max_depth: Maximum directory depth to recurse count_only: Only show match counts per file results_offset: Start at Nth match (0-based, for pagination) results_limit: Return at most this many matches (for pagination) show_progress: Whether to show progress progress_callback: Optional callback for progress updates Returns: GrepResult object with matches and statistics Raises: ValueError: If path is outside allowed directories """ abs_path, allowed = await self.validator.validate_path(path) if not allowed: raise ValueError(f"Path outside allowed directories: {path}") if self._ripgrep_available and not count_only: # Use ripgrep for better performance try: return await self._grep_with_ripgrep( abs_path, pattern, is_regex, case_sensitive, whole_word, include_patterns, exclude_patterns, context_lines, context_before, context_after, max_results, recursive, max_depth, results_offset, results_limit, ) except Exception as e: logger.warning(f"Ripgrep failed, falling back to Python: {e}") # Fall back to Python implementation return await self._grep_with_python( abs_path, pattern, is_regex, case_sensitive, whole_word, include_patterns, exclude_patterns, context_lines, context_before, context_after, max_results, max_file_size_mb, recursive, max_depth, count_only, show_progress, progress_callback, results_offset, results_limit, )
- mcp_filesystem/grep.py:79-218 (helper)GrepResult class: Manages grep search results, including matches, statistics, errors, and provides serialization and text formatting methods.class GrepResult: """Result of a grep operation.""" def __init__(self): """Initialize an empty grep result.""" self.matches: List[GrepMatch] = [] self.file_counts: Dict[str, int] = {} self.total_matches = 0 self.files_searched = 0 self.errors: Dict[str, str] = {} def add_match(self, match: GrepMatch) -> None: """Add a match to the results. Args: match: GrepMatch to add """ self.matches.append(match) self.total_matches += 1 # Update file counts if match.file_path in self.file_counts: self.file_counts[match.file_path] += 1 else: self.file_counts[match.file_path] = 1 def add_file_error(self, file_path: str, error: str) -> None: """Add a file error to the results. Args: file_path: Path to the file with the error error: Error message """ self.errors[file_path] = error def increment_files_searched(self) -> None: """Increment the count of files searched.""" self.files_searched += 1 def to_dict(self) -> Dict: """Convert to dictionary representation. Returns: Dictionary with all results """ return { "matches": [match.to_dict() for match in self.matches], "file_counts": self.file_counts, "total_matches": self.total_matches, "files_searched": self.files_searched, "errors": self.errors, } def format_text( self, show_line_numbers: bool = True, show_file_names: bool = True, count_only: bool = False, show_context: bool = True, highlight: bool = True, ) -> str: """Format results as text. Args: show_line_numbers: Include line numbers in output show_file_names: Include file names in output count_only: Only show match counts per file show_context: Show context lines if available highlight: Highlight matches Returns: Formatted string with results """ if count_only: lines = [ f"Found {self.total_matches} matches in {len(self.file_counts)} files:" ] for file_path, count in sorted(self.file_counts.items()): lines.append(f"{file_path}: {count} matches") return "\n".join(lines) if not self.matches: return "No matches found" lines = [] current_file = None for match in self.matches: # Add file header if changed if show_file_names and match.file_path != current_file: current_file = match.file_path lines.append(f"\n{current_file}:") # Add context before if show_context and match.context_before: for i, context in enumerate(match.context_before): context_line_num = match.line_number - len(match.context_before) + i if show_line_numbers: lines.append(f"{context_line_num:>6}: {context}") else: lines.append(f"{context}") # Add matching line line_prefix = "" if show_line_numbers: line_prefix = f"{match.line_number:>6}: " if highlight: # Highlight the match in the line line = match.line_content highlighted = ( line[: match.match_start] + ">>>" + line[match.match_start : match.match_end] + "<<<" + line[match.match_end :] ) lines.append(f"{line_prefix}{highlighted}") else: lines.append(f"{line_prefix}{match.line_content}") # Add context after if show_context and match.context_after: for i, context in enumerate(match.context_after): context_line_num = match.line_number + i + 1 if show_line_numbers: lines.append(f"{context_line_num:>6}: {context}") else: lines.append(f"{context}") # Add summary summary = ( f"\nFound {self.total_matches} matches in {len(self.file_counts)} files" ) if self.errors: summary += f" ({len(self.errors)} files had errors)" lines.append(summary) return "\n".join(lines)
- mcp_filesystem/grep.py:22-77 (helper)GrepMatch class: Data structure for individual search matches, including file path, line details, match position, and context lines.class GrepMatch: """Represents a single grep match.""" def __init__( self, file_path: str, line_number: int, line_content: str, match_start: int, match_end: int, context_before: Optional[List[str]] = None, context_after: Optional[List[str]] = None, ): """Initialize a grep match. Args: file_path: Path to the file containing the match line_number: Line number of the match (1-based) line_content: Content of the matching line match_start: Start index of the match within the line match_end: End index of the match within the line context_before: Lines before the match context_after: Lines after the match """ self.file_path = file_path self.line_number = line_number self.line_content = line_content self.match_start = match_start self.match_end = match_end self.context_before = context_before or [] self.context_after = context_after or [] def to_dict(self) -> Dict: """Convert to dictionary representation. Returns: Dictionary with match information """ return { "file_path": self.file_path, "line_number": self.line_number, "line_content": self.line_content, "match_start": self.match_start, "match_end": self.match_end, "context_before": self.context_before, "context_after": self.context_after, } def __str__(self) -> str: """Get string representation. Returns: Formatted string with match information """ return f"{self.file_path}:{self.line_number}: {self.line_content}"
- mcp_filesystem/server.py:48-84 (helper)get_components function instantiates GrepTools with PathValidator for security and caches shared components used by all tools.def get_components() -> Dict[str, Any]: """Initialize and return shared components. Returns cached components if already initialized. Returns: Dictionary with initialized components """ # Return cached components if available if _components_cache: return _components_cache # Initialize components allowed_dirs_typed: List[Union[str, Path]] = get_allowed_dirs() validator = PathValidator(allowed_dirs_typed) operations = FileOperations(validator) advanced = AdvancedFileOperations(validator, operations) grep = GrepTools(validator) # Store in cache _components = { "validator": validator, "operations": operations, "advanced": advanced, "grep": grep, "allowed_dirs": validator.get_allowed_dirs(), } # Update cache _components_cache.update(_components) logger.info( f"Initialized filesystem components with allowed directories: {validator.get_allowed_dirs()}" ) return _components