Skip to main content
Glama

Search files

search_files

Search for a literal or regex pattern across files in your dev environment, with control over case sensitivity, number of context lines, hidden files, and maximum results.

Instructions

Search for a pattern (literal or regex) across files inside the configured scope.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
patternYes
pathNo.
globNo
regexNoTreat `pattern` as a regular expression
caseInsensitiveNo
contextLinesNo
maxResultsNo
includeHiddenNo

Implementation Reference

  • The main handler function for the 'search_files' tool. Resolves the search root, compiles the pattern matcher, and walks the directory tree calling scanFile on each file. Returns matches, truncated flag, and filesScanned count.
    export async function searchFilesHandler(
      input: SearchFilesInput,
      config: McpDevtoolsConfig,
    ): Promise<ToolResult<SearchFilesOutput>> {
      const realRoot = await resolveWithinScope(config.scope, input.path);
      const stats = await stat(realRoot);
      if (!stats.isDirectory()) {
        throw new FileSystemError("Search path is not a directory", {
          path: input.path,
          code: "ENOTDIR",
        });
      }
    
      const matcher = compilePattern(input);
      const globMatcher =
        input.glob !== undefined && input.glob.length > 0
          ? picomatch(input.glob, { dot: input.includeHidden })
          : null;
    
      const state: WalkState = {
        matches: [],
        filesScanned: 0,
        truncated: false,
        visited: new Set([realRoot]),
      };
    
      await walk(realRoot, "", input, state, matcher, globMatcher);
    
      return ok({
        matches: state.matches,
        truncated: state.truncated,
        filesScanned: state.filesScanned,
      });
    }
  • Helper function compilePattern: converts the input pattern (literal or regex) into a RegExp with optional case-insensitivity.
    function compilePattern(input: SearchFilesInput): RegExp {
      const flags = input.caseInsensitive ? "i" : "";
      if (input.regex) {
        try {
          return new RegExp(input.pattern, flags);
        } catch (error) {
          throw new ValidationError(`Invalid regex: ${(error as Error).message}`, {
            pattern: input.pattern,
          });
        }
      }
      const escaped = input.pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
      return new RegExp(escaped, flags);
    }
  • walk function: recursively traverses directories, respecting hidden file filtering, ignore dirs (node_modules, .git, etc.), symlink cycle detection via visited set, and optional glob filtering. Calls scanFile on each matching file.
    async function walk(
      absoluteDir: string,
      relativeDir: string,
      input: SearchFilesInput,
      state: WalkState,
      matcher: RegExp,
      globMatcher: ReturnType<typeof picomatch> | null,
    ): Promise<void> {
      if (state.truncated) {
        return;
      }
    
      let dir;
      try {
        dir = await opendir(absoluteDir);
      } catch (error) {
        throw mapFsError(error, absoluteDir);
      }
    
      try {
        for await (const entry of dir) {
          if (state.truncated) {
            return;
          }
          const name = entry.name;
          if (!input.includeHidden && pathIsHidden(name)) {
            continue;
          }
    
          const relPath = relativeDir === "" ? name : `${relativeDir}/${name}`;
          const absPath = path.join(absoluteDir, name);
    
          if (entry.isDirectory()) {
            if (DEFAULT_IGNORE_DIRS.has(name)) {
              continue;
            }
            let realChild: string;
            try {
              realChild = await realpath(absPath);
            } catch {
              continue;
            }
            if (state.visited.has(realChild)) {
              continue;
            }
            state.visited.add(realChild);
            await walk(realChild, relPath, input, state, matcher, globMatcher);
            continue;
          }
    
          if (!entry.isFile()) {
            continue;
          }
    
          if (globMatcher !== null && !globMatcher(relPath)) {
            continue;
          }
    
          await scanFile(absPath, relPath, input, state, matcher);
        }
      } finally {
        await dir.close().catch(() => undefined);
      }
    }
  • scanFile function: reads a file line-by-line, skips binary files and oversized files (>10MB). For each matching line, captures context lines (before and after) and collects SearchMatch results. Stops early when maxResults is reached.
    async function scanFile(
      absolutePath: string,
      relativePath: string,
      input: SearchFilesInput,
      state: WalkState,
      matcher: RegExp,
    ): Promise<void> {
      let stats;
      try {
        stats = await stat(absolutePath);
      } catch {
        return;
      }
      if (stats.size > MAX_FILE_SIZE_FOR_SEARCH) {
        return;
      }
    
      if (await isBinary(absolutePath)) {
        return;
      }
    
      state.filesScanned += 1;
    
      const buffer: string[] = [];
      const stream = createReadStream(absolutePath, { encoding: "utf-8" });
      const rl = readline.createInterface({ input: stream, crlfDelay: Number.POSITIVE_INFINITY });
    
      let lineNo = 0;
      const pendingAfter: { match: SearchMatch; remaining: number }[] = [];
    
      try {
        for await (const rawLine of rl) {
          lineNo += 1;
          const line =
            rawLine.length > MAX_LINE_LENGTH ? `${rawLine.slice(0, MAX_LINE_LENGTH)}…` : rawLine;
    
          for (const pending of pendingAfter) {
            pending.match.after.push(line);
            pending.remaining -= 1;
          }
          while (pendingAfter.length > 0 && pendingAfter[0]!.remaining <= 0) {
            pendingAfter.shift();
          }
    
          if (matcher.test(line)) {
            const before = buffer.slice(-input.contextLines);
            const match: SearchMatch = {
              file: relativePath,
              line: lineNo,
              match: line,
              before,
              after: [],
            };
            state.matches.push(match);
            if (input.contextLines > 0) {
              pendingAfter.push({ match, remaining: input.contextLines });
            }
    
            if (state.matches.length >= input.maxResults) {
              state.truncated = true;
              return;
            }
          }
    
          buffer.push(line);
          if (buffer.length > input.contextLines) {
            buffer.shift();
          }
        }
      } finally {
        rl.close();
        stream.destroy();
      }
    }
  • Zod schema (SearchFilesInput) defining the input: pattern (string), path (default '.'), optional glob, regex flag, caseInsensitive, contextLines (0-20), maxResults (1-1000), includeHidden.
    export const SearchFilesInput = z.object({
      pattern: z.string().min(1),
      path: z.string().default("."),
      glob: z.string().optional(),
      regex: z.boolean().default(false).describe("Treat `pattern` as a regular expression"),
      caseInsensitive: z.boolean().default(false),
      contextLines: z.number().int().min(0).max(20).default(2),
      maxResults: z.number().int().positive().max(1000).default(100),
      includeHidden: z.boolean().default(false),
    });
  • Tool registration in the allTools array: defines the tool with name 'search_files', title 'Search files', description, inputSchema, and handler.
    defineTool({
      name: "search_files",
      title: "Search files",
      description:
        "Search for a pattern (literal or regex) across files inside the configured scope.",
      inputSchema: SearchFilesInput,
      handler: searchFilesHandler,
    }),
  • pathIsHidden helper used by walk to skip dotfiles unless includeHidden is true.
    export function pathIsHidden(name: string): boolean {
      return name.length > 1 && name.startsWith(".");
    }
  • resolveWithinScope helper used to verify the search path is within the configured scope, following symlinks.
    export async function resolveWithinScope(
      scopeRoot: string,
      relativeOrAbsolute: string,
    ): Promise<string> {
      const candidate = assertWithinScope(scopeRoot, relativeOrAbsolute);
      // Resolve the scope itself through symlinks so the post-realpath comparison
      // works on platforms where the temp dir or workspace lives behind a symlink
      // (e.g. macOS `/var` -> `/private/var`).
      const realScope = await safeRealpath(path.resolve(scopeRoot));
    
      try {
        const real = await realpath(candidate);
        if (!isWithinPath(realScope, real)) {
          throw new ScopeViolationError(`Symlink target escapes scope: ${relativeOrAbsolute}`, {
            scope: realScope,
            target: real,
          });
        }
        return real;
      } catch (error) {
        if (error instanceof ScopeViolationError) {
          throw error;
        }
        const errno = error as NodeJS.ErrnoException;
        if (errno.code === "ENOENT") {
          return candidate;
        }
        if (errno.code === "ELOOP") {
          throw new FileSystemError(`Symlink loop detected: ${relativeOrAbsolute}`, {
            path: candidate,
            cause: errno.code,
          });
        }
        throw new FileSystemError(`Failed to resolve path: ${relativeOrAbsolute}`, {
          path: candidate,
          cause: errno.code ?? "UNKNOWN",
        });
      }
    }
Behavior2/5

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

No annotations are provided, so the description carries the full burden. It mentions searching across files but does not disclose key behaviors like the default case-insensitivity (caseInsensitive defaults false), exclusion of hidden files (includeHidden defaults false), or the maximum file size or scope limitations. The agent is left unaware of critical behavioral traits.

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

Conciseness4/5

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

The description is a single sentence, concise and front-loaded. It avoids redundancy, but could benefit from a bit more detail without losing conciseness.

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

Completeness2/5

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

Given 8 parameters, no output schema, and no annotations, the description is insufficient. It does not explain the return format, behavior of parameters, or edge cases (e.g., no matches), leaving the agent with incomplete information to correctly invoke the tool.

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

Parameters2/5

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

Schema description coverage is only 13%, yet the description only mentions 'pattern' and the choice of literal or regex. It does not explain 'path', 'glob', 'caseInsensitive', 'contextLines', 'maxResults', or 'includeHidden', leaving the agent to infer their meanings from names alone, which may be ambiguous.

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: searching for a pattern (literal or regex) across files. However, 'inside the configured scope' is vague and does not differentiate well from sibling tools like 'list_directory' or 'read_file', which also involve file access.

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?

No guidance is provided on when to use this tool versus alternatives such as 'read_file' or 'list_directory'. The description lacks any context about preferred scenarios or exclusions.

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/marin1321/mcp-devtools'

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