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
Behavior3/5

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

With no annotations provided, the description carries the full burden of behavioral disclosure. It does reveal some behavioral traits: the tool can search recursively, skip large files, and support pagination. However, it doesn't mention important aspects like performance characteristics, memory usage, error conditions, or whether the search is destructive (though 'grep' implies read-only).

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

Conciseness3/5

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

The description is well-structured with clear sections for arguments and returns, but it's quite lengthy due to documenting all 18 parameters. While each parameter explanation earns its place, the front-loaded purpose statement could be more prominent. The structure is functional but not optimally concise.

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

Completeness4/5

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

Given the tool's complexity (18 parameters, no annotations, no output schema), the description does a reasonably complete job. It thoroughly documents all parameters and mentions the return type ('Search results'). However, without annotations or output schema, it could benefit from more detail about result format, error handling, and performance considerations.

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

Parameters5/5

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

The description provides comprehensive parameter documentation that fully compensates for the 0% schema description coverage. Each of the 18 parameters is clearly explained with meaningful context (e.g., 'like grep -C', 'for pagination', 'Skip files larger than this size'). This adds substantial value beyond what the bare schema provides.

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

Purpose4/5

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

The description clearly states the tool's purpose: 'Search for pattern in files, similar to grep.' This specifies the verb ('search') and resource ('files'), making the function immediately understandable. However, it doesn't explicitly differentiate from sibling tools like 'search_files' or 'find_duplicate_files', which prevents a perfect score.

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

Usage Guidelines2/5

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

The description provides no guidance on when to use this tool versus alternatives. With many sibling tools for file operations (like 'search_files', 'find_duplicate_files', 'read_file_lines'), there's no indication of when grep_files is the appropriate choice versus other search or file examination tools.

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

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