Skip to main content
Glama
safurrier

MCP Filesystem Server

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
NameRequiredDescriptionDefault
pathYes
patternYes
is_regexNo
case_sensitiveNo
whole_wordNo
include_patternsNo
exclude_patternsNo
context_linesNo
context_beforeNo
context_afterNo
results_offsetNo
results_limitNo
max_resultsNo
max_file_size_mbNo
recursiveNo
max_depthNo
count_onlyNo
formatNotext

Implementation Reference

  • 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)}"
  • 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,
        )
  • 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)
  • 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}"
  • 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

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/safurrier/mcp-filesystem'

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