find_text
Search for specific text patterns within project files using customizable filters like file type, regex, case sensitivity, and whole-word matching. Supports context lines for better result understanding.
Instructions
Search for text pattern in project files.
Args:
project: Project name
pattern: Text pattern to search for
file_pattern: Optional glob pattern (e.g., "**/*.py")
max_results: Maximum number of results
case_sensitive: Whether to do case-sensitive matching
whole_word: Whether to match whole words only
use_regex: Whether to treat pattern as a regular expression
context_lines: Number of context lines to include
Returns:
List of matches with file, line number, and text
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| case_sensitive | No | ||
| context_lines | No | ||
| file_pattern | No | ||
| max_results | No | ||
| pattern | Yes | ||
| project | Yes | ||
| use_regex | No | ||
| whole_word | No |
Implementation Reference
- The MCP tool handler function for 'find_text', decorated with @mcp_server.tool() for registration. It handles dependency injection, project resolution, and delegates to the core search_text helper.def find_text( project: str, pattern: str, file_pattern: Optional[str] = None, max_results: int = 100, case_sensitive: bool = False, whole_word: bool = False, use_regex: bool = False, context_lines: int = 2, ) -> List[Dict[str, Any]]: """Search for text pattern in project files. Args: project: Project name pattern: Text pattern to search for file_pattern: Optional glob pattern (e.g., "**/*.py") max_results: Maximum number of results case_sensitive: Whether to do case-sensitive matching whole_word: Whether to match whole words only use_regex: Whether to treat pattern as a regular expression context_lines: Number of context lines to include Returns: List of matches with file, line number, and text """ from ..tools.search import search_text config = config_manager.get_config() return search_text( project_registry.get_project(project), pattern, file_pattern, max_results if max_results is not None else config.max_results_default, case_sensitive, whole_word, use_regex, context_lines, )
- Core helper function implementing the text search logic: prepares regex or string patterns, scans files using glob, processes matches with context lines in parallel using ThreadPoolExecutor, and returns structured results.def search_text( project: Any, pattern: str, file_pattern: Optional[str] = None, max_results: int = 100, case_sensitive: bool = False, whole_word: bool = False, use_regex: bool = False, context_lines: int = 0, ) -> List[Dict[str, Any]]: """ Search for text pattern in project files. Args: project: Project object pattern: Text pattern to search for file_pattern: Optional glob pattern to filter files (e.g. "**/*.py") max_results: Maximum number of results to return case_sensitive: Whether to do case-sensitive matching whole_word: Whether to match whole words only use_regex: Whether to treat pattern as a regular expression context_lines: Number of context lines to include before/after matches Returns: List of matches with file, line number, and text """ root = project.root_path results: List[Dict[str, Any]] = [] pattern_obj = None # Prepare the pattern if use_regex: try: flags = 0 if case_sensitive else re.IGNORECASE pattern_obj = re.compile(pattern, flags) except re.error as e: raise ValueError(f"Invalid regular expression: {e}") from e elif whole_word: # Escape pattern for use in regex and add word boundary markers pattern_escaped = re.escape(pattern) flags = 0 if case_sensitive else re.IGNORECASE pattern_obj = re.compile(rf"\b{pattern_escaped}\b", flags) elif not case_sensitive: # For simple case-insensitive search pattern = pattern.lower() file_pattern = file_pattern or "**/*" # Process files in parallel def process_file(file_path: Path) -> List[Dict[str, Any]]: file_results = [] try: validate_file_access(file_path, root) with open(file_path, "r", encoding="utf-8", errors="replace") as f: lines = f.readlines() for i, line in enumerate(lines, 1): match = False if pattern_obj: # Using regex pattern match_result = pattern_obj.search(line) match = bool(match_result) elif case_sensitive: # Simple case-sensitive search - check both original and stripped versions match = pattern in line or pattern.strip() in line.strip() else: # Simple case-insensitive search - check both original and stripped versions line_lower = line.lower() pattern_lower = pattern.lower() match = pattern_lower in line_lower or pattern_lower.strip() in line_lower.strip() if match: # Calculate context lines start = max(0, i - 1 - context_lines) end = min(len(lines), i + context_lines) context = [] for ctx_i in range(start, end): ctx_line = lines[ctx_i].rstrip("\n") context.append( { "line": ctx_i + 1, "text": ctx_line, "is_match": ctx_i == i - 1, } ) file_results.append( { "file": str(file_path.relative_to(root)), "line": i, "text": line.rstrip("\n"), "context": context, } ) if len(file_results) >= max_results: break except Exception: # Skip files that can't be read pass return file_results # Collect files to process files_to_process = [] for path in root.glob(file_pattern): if path.is_file(): files_to_process.append(path) # Process files in parallel with concurrent.futures.ThreadPoolExecutor() as executor: futures = [executor.submit(process_file, f) for f in files_to_process] for future in concurrent.futures.as_completed(futures): results.extend(future.result()) if len(results) >= max_results: # Cancel any pending futures for f in futures: f.cancel() break return results[:max_results]