regex_search
Match a JavaScript regex against file contents across projects, knowledge base, or all files, returning line-numbered hits. Use when full-text search fails to find substrings or identifiers.
Instructions
Match a JS regex against the body of every file in scope (project, KB, or all) and return per-file hits with line numbers. Slower than FTS search because it reads each file's content; use only when FTS misses substrings, URLs, or code identifiers. Read-only; no side effects, auth, or rate limits. Capped at 500 files / 10 hits per file by default; the response reports files_truncated and per-file truncation so the agent can re-scope. project_id: null = KB only; omit = everywhere. Invalid regex throws.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| pattern | Yes | JavaScript RegExp source | |
| project_id | No | Scope to one project, null for KB-only, omit for everything | |
| case_insensitive | No | ||
| max_files | No | Cap on files scanned (default 500) | |
| max_matches_per_file | No | Per-file hit cap (default 10) |
Implementation Reference
- apps/mcp/src/index.ts:441-520 (registration)The tool is registered via server.tool() with name 'regex_search'. The registration includes the description, Zod schema for parameters, and the async handler function.
server.tool( "regex_search", "Match a JS regex against the body of every file in scope (project, KB, or all) and return per-file hits with line numbers. Slower than FTS `search` because it reads each file's content; use only when FTS misses substrings, URLs, or code identifiers. Read-only; no side effects, auth, or rate limits. Capped at 500 files / 10 hits per file by default; the response reports `files_truncated` and per-file truncation so the agent can re-scope. `project_id: null` = KB only; omit = everywhere. Invalid regex throws.", { pattern: z.string().describe("JavaScript RegExp source"), project_id: z.number().nullable().optional().describe("Scope to one project, null for KB-only, omit for everything"), case_insensitive: z.boolean().optional(), max_files: z.number().int().positive().max(2000).optional().describe("Cap on files scanned (default 500)"), max_matches_per_file: z.number().int().positive().max(100).optional().describe("Per-file hit cap (default 10)"), }, async ({ pattern, project_id, case_insensitive, max_files, max_matches_per_file }) => { try { let re: RegExp; try { re = new RegExp(pattern, case_insensitive ? "i" : ""); } catch (e: any) { throw new Error(`invalid regex: ${e?.message ?? e}`); } const fileCap = max_files ?? 500; const perFileCap = max_matches_per_file ?? 10; const db = getDatabase(); let where = ""; const params: any[] = []; if (project_id === null) { where = "WHERE project_id IS NULL"; } else if (typeof project_id === "number") { where = "WHERE project_id = ?"; params.push(project_id); } const rows = db .prepare(`SELECT id, path, title FROM files ${where} LIMIT ?`) .all(...params, fileCap) as { id: number; path: string; title: string }[]; const hits: any[] = []; let scanned = 0; for (const r of rows) { scanned++; let content: string; try { content = readFileSync(r.path, "utf8"); } catch { continue; } const lines = content.split("\n"); const fileHits: { line: number; text: string }[] = []; for (let i = 0; i < lines.length; i++) { if (re.test(lines[i])) { fileHits.push({ line: i + 1, text: lines[i] }); if (fileHits.length >= perFileCap) break; } } if (fileHits.length > 0) { hits.push({ file_id: r.id, path: r.path, title: r.title, match_count: fileHits.length, matches: fileHits }); } } return { content: [ { type: "text", text: JSON.stringify( { pattern, files_scanned: scanned, files_truncated: rows.length === fileCap, file_hit_count: hits.length, total_match_count: hits.reduce((s, h) => s + h.match_count, 0), hits, }, null, 2 ), }, ], }; } catch (e: any) { return { isError: true, content: [{ type: "text", text: JSON.stringify({ error: e?.message ?? String(e) }, null, 2) }], }; } } ); - apps/mcp/src/index.ts:444-450 (schema)Zod schema defining the input parameters: pattern (string), project_id (nullable number optional), case_insensitive (boolean optional), max_files (positive int up to 2000 optional), max_matches_per_file (positive int up to 100 optional).
{ pattern: z.string().describe("JavaScript RegExp source"), project_id: z.number().nullable().optional().describe("Scope to one project, null for KB-only, omit for everything"), case_insensitive: z.boolean().optional(), max_files: z.number().int().positive().max(2000).optional().describe("Cap on files scanned (default 500)"), max_matches_per_file: z.number().int().positive().max(100).optional().describe("Per-file hit cap (default 10)"), }, - apps/mcp/src/index.ts:451-519 (handler)The handler function that executes regex_search: compiles the JS regex, queries the DB for files (scoped by project_id), reads each file from disk, tests each line against the regex, collects hits per file capped at max_matches_per_file, and returns a JSON response with files_scanned, file_hit_count, total_match_count and hits array.
async ({ pattern, project_id, case_insensitive, max_files, max_matches_per_file }) => { try { let re: RegExp; try { re = new RegExp(pattern, case_insensitive ? "i" : ""); } catch (e: any) { throw new Error(`invalid regex: ${e?.message ?? e}`); } const fileCap = max_files ?? 500; const perFileCap = max_matches_per_file ?? 10; const db = getDatabase(); let where = ""; const params: any[] = []; if (project_id === null) { where = "WHERE project_id IS NULL"; } else if (typeof project_id === "number") { where = "WHERE project_id = ?"; params.push(project_id); } const rows = db .prepare(`SELECT id, path, title FROM files ${where} LIMIT ?`) .all(...params, fileCap) as { id: number; path: string; title: string }[]; const hits: any[] = []; let scanned = 0; for (const r of rows) { scanned++; let content: string; try { content = readFileSync(r.path, "utf8"); } catch { continue; } const lines = content.split("\n"); const fileHits: { line: number; text: string }[] = []; for (let i = 0; i < lines.length; i++) { if (re.test(lines[i])) { fileHits.push({ line: i + 1, text: lines[i] }); if (fileHits.length >= perFileCap) break; } } if (fileHits.length > 0) { hits.push({ file_id: r.id, path: r.path, title: r.title, match_count: fileHits.length, matches: fileHits }); } } return { content: [ { type: "text", text: JSON.stringify( { pattern, files_scanned: scanned, files_truncated: rows.length === fileCap, file_hit_count: hits.length, total_match_count: hits.reduce((s, h) => s + h.match_count, 0), hits, }, null, 2 ), }, ], }; } catch (e: any) { return { isError: true, content: [{ type: "text", text: JSON.stringify({ error: e?.message ?? String(e) }, null, 2) }], }; } } - Categorization of regex_search under the 'Search' category in the UI tool category mapping.
regex_search: "Search",