grep
Search file contents using regular expressions to find specific patterns, with options to filter by file type and show surrounding context lines.
Instructions
Search file contents using regular expressions with ripgrep-style output
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| pattern | Yes | The regular expression pattern to search for in file contents | |
| path | No | The directory to search in. Defaults to the current working directory. | |
| include | No | File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}", "!**/node_modules/**") | |
| beforeContext | No | Show NUM lines before each match | |
| afterContext | No | Show NUM lines after each match | |
| context | No | Show NUM lines before and after each match | |
| multiline | No | Enable multiline pattern matching | |
| maxCount | No | Maximum number of matches per file (default: 5) |
Implementation Reference
- servers/file-mcp/tools/grep.ts:69-214 (handler)The primary handler function for the 'grep' tool. It validates inputs using Zod, handles single file or directory searches, performs regex matching with configurable context lines, limits matches and output size, and formats results in a ripgrep-like style with summaries.
async execute(args: any): Promise<any> { try { // Validate and parse input using Zod schema const validatedArgs = GrepToolInputSchema.parse(args); const { pattern, path, include, beforeContext, afterContext, context, multiline, maxCount } = validatedArgs; // Reset output length for each execution this.outputLength = 0; // Calculate actual before/after context const actualBefore = context !== undefined ? context : beforeContext; const actualAfter = context !== undefined ? context : afterContext; // Path validation is handled by DirectoryUtils // Create regex pattern with enhanced error handling const regexFlags = multiline ? "gm" : "g"; let regex: RegExp; try { regex = new RegExp(pattern, regexFlags); } catch (error: any) { throw new Error(this.createFriendlyRegexError(pattern, error.message)); } let result = ""; let totalMatches = 0; let filesWithMatches = 0; let filesSearched = 0; // Check if path is a single file or directory let filesToSearch: string[] = []; let results: any[] = []; const MAX_FILES = 20; try { const stats = await FileUtils.getFileStats(path); if (stats.isFile) { // Single file case filesToSearch = [path]; filesSearched = 1; } else { throw new Error("Not a file"); } } catch { // Not a file, treat as directory results = await DirectoryUtils.findFiles(path, include, { maxDepth: 3, includeIgnored: false, includeFiles: true, includeDirectories: false, }); // Limit number of files to search for performance filesToSearch = results.slice(0, MAX_FILES).map((r) => r.path); } for (const file of filesToSearch) { if (filesSearched === 1 && filesToSearch.length === 1) { // Single file case, already counted } else { filesSearched++; } try { // Use FileUtils for safe file reading with built-in security checks const content = await FileUtils.safeReadFile(file, { maxSize: TOOL_CONSTANTS.OUTPUT_LIMIT, checkIsBinary: true, }); // Search for matches const matches = this.searchContent(content, regex, actualBefore, actualAfter, maxCount); if (matches.length > 0) { filesWithMatches++; const relativePath = relative(process.cwd(), file); // Add file header const header = `${relativePath}\n`; result += header; this.addToOutput(header); // Add matches with context let displayedMatches = 0; let totalFileMatches = 0; for (const match of matches) { if (displayedMatches >= maxCount) { totalFileMatches = this.countTotalMatches(content, regex); break; } const line = `${match.lineNumber}${match.isMatch ? ":" : "-"}${match.content}\n`; result += line; this.addToOutput(line); if (match.isMatch) { displayedMatches++; totalMatches++; } } // Show continuation message if there are more matches if (totalFileMatches > maxCount) { const remaining = totalFileMatches - maxCount; const continuation = `\n... and ${remaining} more matches (showing first ${maxCount})\nUse maxCount=${Math.min(totalFileMatches, maxCount * 2)} to see more matches\n`; result += continuation; this.addToOutput(continuation); } const separator = "\n"; result += separator; this.addToOutput(separator); } } catch (_error) {} } // Add summary let summary = ""; if (totalMatches === 0) { summary = "No matches found"; } else { summary = `${totalMatches} match${totalMatches === 1 ? "" : "es"} found in ${filesWithMatches} file${filesWithMatches === 1 ? "" : "s"}`; if (results.length > MAX_FILES) { summary += ` (limited to first ${MAX_FILES} files)`; } } summary += "\n"; result += summary; this.addToOutput(summary); return ResultFormatter.createResponse(result); } catch (error: any) { // Handle Zod validation errors if (error instanceof Error && error.name === "ZodError") { throw ToolError.createValidationError("input", args, `Invalid input: ${error.message}`); } // If it's already our friendly regex error, don't wrap it if (error.message.includes("Regex pattern error:")) { throw error; } throw ToolError.wrapError("Grep search", error); } } - Zod input schema for the grep tool used for runtime validation of tool arguments including pattern, path, glob include, context lines, multiline flag, and max matches.
export const GrepToolInputSchema = z.object({ pattern: z.string().min(1, "Pattern cannot be empty"), path: FilePathSchema.default("."), include: z.string().optional(), beforeContext: z.number().int().min(0).default(0), afterContext: z.number().int().min(0).default(0), context: z.number().int().min(0).optional(), multiline: BooleanFlagSchema, maxCount: z.number().int().min(1).default(5), }); - servers/file-mcp/index.ts:90-100 (registration)Tool registration: adds the grep tool's definition to the list of available MCP tools returned by listTools.
protected getTools(): Tool[] { return [ this.readTool.getDefinition(), this.findTool.getDefinition(), this.grepTool.getDefinition(), this.writeTool.getDefinition(), this.editTool.getDefinition(), this.moveTool.getDefinition(), this.copyTool.getDefinition(), ]; } - servers/file-mcp/index.ts:114-115 (registration)Tool dispatch registration: handles 'grep' tool calls by invoking the GrepTool instance's execute method.
case "grep": return await this.grepTool.execute(args); - Helper function that performs the actual content search: finds matching lines, adds before/after context without duplication, and returns structured matches for output formatting.
private searchContent( content: string, regex: RegExp, beforeContext: number, afterContext: number, maxCount: number ): GrepMatch[] { const lines = content.split("\n"); const matches: GrepMatch[] = []; const matchedLines = new Set<number>(); let matchCount = 0; // Find all matches for (let i = 0; i < lines.length && matchCount < maxCount; i++) { const line = lines[i]; if (line !== undefined && regex.test(line)) { matchedLines.add(i); matchCount++; } } // Build result with context const processedLines = new Set<number>(); for (const matchLine of Array.from(matchedLines).sort((a, b) => a - b)) { // Add before context for (let i = Math.max(0, matchLine - beforeContext); i < matchLine; i++) { if (!processedLines.has(i)) { const line = lines[i]; if (line !== undefined) { matches.push({ lineNumber: i + 1, content: line, isMatch: false, }); processedLines.add(i); } } } // Add match line if (!processedLines.has(matchLine)) { const line = lines[matchLine]; if (line !== undefined) { matches.push({ lineNumber: matchLine + 1, content: line, isMatch: true, }); processedLines.add(matchLine); } } // Add after context for (let i = matchLine + 1; i <= Math.min(lines.length - 1, matchLine + afterContext); i++) { if (!processedLines.has(i)) { const line = lines[i]; if (line !== undefined) { matches.push({ lineNumber: i + 1, content: line, isMatch: false, }); processedLines.add(i); } } } } return matches; }