regex_search_content
Search file content recursively using a regex pattern. Scan subdirectories from a specified path, return matching files with line details. Optional filters: file pattern, depth, size, and result limits. Works within secure directories.
Instructions
Recursively search file content using a regex pattern. Searches through subdirectories from the starting path. Returns a list of files containing matches, including line numbers and matching lines. Requires regex pattern. Optional: path, filePattern, maxDepth, maxFileSize, maxResults. Only searches within allowed directories.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| filePattern | No | Glob pattern to filter files to search within (e.g., "*.ts", "data/**.json"). Defaults to searching all files. | * |
| maxDepth | No | Maximum directory depth to search recursively. Defaults to 2. | |
| maxFileSize | No | Maximum file size in bytes to read for searching. Defaults to 10MB. | |
| maxResults | No | Maximum number of files with matches to return. Defaults to 50. | |
| path | Yes | Directory path to start the search from. | |
| regex | Yes | The regular expression pattern to search for within file content. |
Implementation Reference
- src/handlers/utility-handlers.ts:249-296 (handler)The main handler function that parses arguments, validates path, calls the regexSearchContent helper, formats results, and returns the response.export async function handleRegexSearchContent( args: unknown, allowedDirectories: string[], symlinksMap: Map<string, string>, noFollowSymlinks: boolean ) { const parsed = parseArgs(RegexSearchContentArgsSchema, args, 'regex_search_content'); const { path: startPath, regex, filePattern, maxDepth, maxFileSize, maxResults } = parsed; const validPath = await validatePath(startPath, allowedDirectories, symlinksMap, noFollowSymlinks); try { const results = await regexSearchContent( validPath, regex, filePattern, maxDepth, maxFileSize, maxResults ); if (results.length === 0) { return { content: [{ type: "text", text: "No matches found for the given regex pattern." }] }; } // Format the output const formattedResults = results.map(fileResult => { const matchesText = fileResult.matches .map(match => ` Line ${match.lineNumber}: ${match.lineContent.trim()}`) .join('\n'); return `File: ${fileResult.path}\n${matchesText}`; }).join('\n\n'); return { content: [{ type: "text", text: formattedResults }], }; } catch (error: any) { // Catch errors from regexSearchContent (e.g., invalid regex) throw new Error(`Error during regex content search: ${error.message}`); } }
- TypeBox schema definition for the tool's input arguments and corresponding TypeScript type.export const RegexSearchContentArgsSchema = Type.Object({ path: Type.String({ description: 'Directory path to start the search from.' }), regex: Type.String({ description: 'The regular expression pattern to search for within file content.' }), filePattern: Type.Optional(Type.String({ default: '*', description: 'Glob pattern to filter files to search within (e.g., "*.ts", "data/**.json"). Defaults to searching all files.' })), maxDepth: Type.Optional(Type.Integer({ minimum: 1, default: 2, description: 'Maximum directory depth to search recursively. Defaults to 2.' })), maxFileSize: Type.Optional(Type.Integer({ minimum: 1, default: 10 * 1024 * 1024, description: 'Maximum file size in bytes to read for searching. Defaults to 10MB.' })), maxResults: Type.Optional(Type.Integer({ minimum: 1, default: 50, description: 'Maximum number of files with matches to return. Defaults to 50.' })) }); export type RegexSearchContentArgs = Static<typeof RegexSearchContentArgsSchema>;
- src/utils/file-utils.ts:264-370 (helper)Core utility function that performs the recursive regex search on file contents using streaming readline for large files, respects limits, and returns structured match results.export async function regexSearchContent( rootPath: string, regexPattern: string, filePattern: string = '*', maxDepth: number = 2, maxFileSize: number = 10 * 1024 * 1024, // 10MB default maxResults: number = 50 ): Promise<ReadonlyArray<RegexSearchResult>> { const results: RegexSearchResult[] = []; let regex: RegExp; try { regex = new RegExp(regexPattern, 'g'); // Global flag to find all matches } catch (error: any) { throw new Error(`Invalid regex pattern provided: ${error.message}`); } async function search(currentPath: string, currentDepth: number) { if (currentDepth >= maxDepth || results.length >= maxResults) { return; } let entries; try { entries = await fsPromises.readdir(currentPath, { withFileTypes: true }); } catch (error: any) { console.warn(`Skipping directory ${currentPath}: ${error.message}`); return; // Skip directories we can't read } for (const entry of entries) { if (results.length >= maxResults) return; // Check results limit again const fullPath = path.join(currentPath, entry.name); const relativePath = path.relative(rootPath, fullPath); if (entry.isDirectory()) { await search(fullPath, currentDepth + 1); } else if (entry.isFile()) { // Check if file matches the filePattern glob // Match file pattern against the relative path (removed matchBase: true) if (!minimatch(relativePath, filePattern, { dot: true })) { continue; } try { const stats = await fsPromises.stat(fullPath); if (stats.size > maxFileSize) { console.warn(`Skipping large file ${fullPath}: size ${stats.size} > max ${maxFileSize}`); continue; } // Use streaming approach for large files const fileStream = createReadStream(fullPath, { encoding: 'utf-8' }); const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity, // Handle different line endings }); const fileMatches: { lineNumber: number; lineContent: string }[] = []; let currentLineNumber = 0; // Wrap readline processing in a promise await new Promise<void>((resolve, reject) => { rl.on('line', (line) => { currentLineNumber++; // Reset regex lastIndex before each test if using global flag regex.lastIndex = 0; if (regex.test(line)) { fileMatches.push({ lineNumber: currentLineNumber, lineContent: line }); } }); rl.on('close', () => { resolve(); }); rl.on('error', (err) => { // Don't reject, just warn and resolve to continue processing other files console.warn(`Error reading file ${fullPath}: ${err.message}`); resolve(); }); fileStream.on('error', (err) => { // Handle stream errors (e.g., file not found during read) console.warn(`Error reading file stream ${fullPath}: ${err.message}`); resolve(); // Resolve to allow processing to continue }); }); if (fileMatches.length > 0) { if (results.length < maxResults) { results.push({ path: fullPath, matches: fileMatches }); } if (results.length >= maxResults) return; // Stop searching this branch } } catch (error: any) { console.warn(`Skipping file ${fullPath}: ${error.message}`); // Continue searching other files even if one fails } } } } await search(rootPath, 0); return results; }
- index.ts:295-301 (registration)Registers the tool handler in the toolHandlers object, binding it to the FastMCP server execution.regex_search_content: (a: unknown) => handleRegexSearchContent( a, allowedDirectories, symlinksMap, noFollowSymlinks, ),
- index.ts:334-337 (registration)Defines the tool metadata (name and description) in the allTools array used for server.addTool.{ name: "regex_search_content", description: "Search file content with regex", },