Skip to main content
Glama

search_and_replace

Perform bulk search-and-replace operations across multiple files using glob patterns. Preview changes with dry runs, support literal or regex matching, and apply replacements to all occurrences in each file.

Instructions

Bulk search-and-replace across files matching a glob. Replaces ALL occurrences per file (unlike edit: first only). Always dryRun:true first — returns a unified diff. Literal matching by default; isRegex:true enables RE2 with capture groups ($1, $2).

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
pathNoBase directory (default: root). Absolute path required if multiple roots.
filePatternNoGlob to filter files. Default: **/***/*
searchPatternYesText to search for. Literal by default; RE2 regex when `isRegex=true`.
replacementYesReplacement text
isRegexNoTreat searchPattern as RE2 regex. Supports capture groups ($1, $2) in replacement.
caseSensitiveNoCase-sensitive matching. Default: true.
dryRunNoPreview matches without writing. Check changedFiles and matches in the response before committing.
includeHiddenNoInclude hidden files/directories (starting with .). Default: false.
includeIgnoredNoInclude .gitignore-ignored files (node_modules, dist). Default: false.
returnDiffNoReturn unified diff of changes even if dryRun is false. Default: false.
maxFilesNoMax files to process before stopping

Implementation Reference

  • The main handler function that executes the bulk search-and-replace operation across files.
    async function handleSearchAndReplace(
      args: SearchAndReplaceArgs,
      signal?: AbortSignal,
      onProgress: (progress: { total?: number; current: number }) => void = () => {}
    ): Promise<ToolResponse<SearchAndReplaceOutput>> {
      const maxFileSize = MAX_TEXT_FILE_SIZE;
      const root = await resolveSearchRoot(args.path, signal);
      const matcher = createReplacementMatcher(args);
    
      const entries = globEntries({
        cwd: root,
        pattern: args.filePattern,
        excludePatterns: args.includeIgnored ? [] : DEFAULT_EXCLUDE_PATTERNS,
        includeHidden: args.includeHidden ?? false,
        baseNameMatch: false,
        caseSensitiveMatch: true, // Default to sensitive for file paths
        followSymbolicLinks: false,
        onlyFiles: true,
        stats: false,
        suppressErrors: true,
      });
    
      const summary = createReplaceSummary(root);
    
      const t0 = performance.now();
    
      const context: ReplaceContext = {
        options: {
          dryRun: args.dryRun,
          returnDiff: args.returnDiff ?? false,
        },
        replacement: args.replacement,
        matcher,
        maxFileSize,
        signal,
        summary,
      };
    
      const { stoppedByLimit } = await processEntriesConcurrently(entries, {
        signal,
        concurrency: REPLACE_CONCURRENCY,
        ...(args.maxFiles !== undefined ? { maxEntries: args.maxFiles } : {}),
        onEntry: () => {
          summary.processedFiles++;
          reportPeriodicProgress(onProgress, summary.processedFiles, {
            throttleModulo: 25,
          });
        },
        runEntry: (entryPath) => processEntry(entryPath, context),
      });
    
      summary.perfTimeMs = performance.now() - t0;
    
      if (stoppedByLimit) {
        summary.stoppedReason = 'maxFiles';
      }
    
      reportPeriodicProgress(onProgress, summary.processedFiles, {
        throttleModulo: 25,
        force: true,
      });
    
      return buildToolResponse(
        buildSearchAndReplaceText(summary, args.dryRun),
        buildSearchAndReplaceStructuredResult(summary, args)
      );
    }
  • Registration function for the 'search_and_replace' tool.
    export function registerSearchAndReplaceTool(
      server: McpServer,
      options: ToolRegistrationOptions = {}
    ): void {
      const handler = (
        args: z.infer<typeof SearchAndReplaceInputSchema>,
        extra: ToolExtra
      ): Promise<ToolResult<z.infer<typeof SearchAndReplaceOutputSchema>>> =>
        executeToolWithDiagnostics({
          toolName: 'search_and_replace',
          extra,
          outputSchema: SearchAndReplaceOutputSchema,
          timedSignal: {},
          ...(args.path ? { context: { path: args.path } } : {}),
          run: async (signal) => {
            const dryLabel = args.dryRun ? ' [dry run]' : '';
            const truncatedPattern = truncateProgressPattern(args.searchPattern);
            const context = `"${truncatedPattern}" in ${args.filePattern}${dryLabel}`;
            const progress = createToolProgressSession(
              extra,
              `🛠 replace: ${context}`
            );
            const progressWithMessage = ({
              current,
              total,
            }: {
              total?: number;
              current: number;
            }): void => {
              progress.update({
                current,
                ...(total !== undefined ? { total } : {}),
                message: `🛠 replace: ${truncatedPattern} [${current} files]`,
              });
            };
    
            try {
              const result = await handleSearchAndReplace(
                args,
                signal,
                progressWithMessage
              );
              const sc = result.structuredContent;
              const finalCurrent = resolveFinalProgressCurrent(
                progress,
                (sc.processedFiles ?? 0) + 1
              );
              const matchWord = (sc.matches ?? 0) === 1 ? 'match' : 'matches';
              const fileWord = (sc.filesChanged ?? 0) === 1 ? 'file' : 'files';
              let endSuffix = `${sc.matches ?? 0} ${matchWord} in ${sc.filesChanged ?? 0} ${fileWord}`;
              if (sc.failedFiles) endSuffix += `, ${sc.failedFiles} failed`;
              progress.complete(
                `🛠 replace: ${context} • ${endSuffix}`,
                finalCurrent
              );
              return result;
            } catch (error) {
              progress.fail(`🛠 replace: ${context} • failed`);
              throw error;
            }
          },
          onError: (error) =>
            buildToolErrorResponse(error, ErrorCode.E_UNKNOWN, args.path),
        });
    
      const { isInitialized } = options;
    
      const wrappedHandler = wrapToolHandler(handler, {
        guard: isInitialized,
      });
    
      const validatedHandler = withValidatedArgs(
        SearchAndReplaceInputSchema,
        wrappedHandler
      );
    
      if (
        registerToolTaskIfAvailable(
          server,
          'search_and_replace',
          SEARCH_AND_REPLACE_TOOL,
          validatedHandler,
          options.iconInfo,
          isInitialized
        )
      )
        return;
      server.registerTool(
        'search_and_replace',
        withDefaultIcons({ ...SEARCH_AND_REPLACE_TOOL }, options.iconInfo),
        validatedHandler
      );
    }
  • Zod schema definitions for the 'search_and_replace' tool input and output.
    export const SearchAndReplaceInputSchema = z.strictObject({
      path: OptionalPathSchema.describe(DESC_PATH_ROOT),
      filePattern: z
        .string()
        .min(1, 'Pattern required')
        .max(1000, 'Max 1000 chars')
        .optional()
        .default('**/*')
        .refine((val) => isSafeGlobPattern(val), {
          error: 'Invalid glob or unsafe path (absolute/.. forbidden)',
        })
        .describe('Glob to filter files. Default: **/*'),
      searchPattern: z
        .string()
        .min(1, 'Search pattern required')
        .max(1000, 'Max 1000 chars')
        .describe(
          'Text to search for. Literal by default; RE2 regex when `isRegex=true`.'
        ),
      replacement: z.string().describe('Replacement text'),
      isRegex: defaultFalseBoolean(
        'Treat searchPattern as RE2 regex. Supports capture groups ($1, $2) in replacement.'
      ),
      caseSensitive: z
        .boolean()
        .optional()
        .default(true)
        .describe('Case-sensitive matching. Default: true.'),
      dryRun: defaultFalseBoolean(
        'Preview matches without writing. Check changedFiles and matches in the response before committing.'
      ),
      includeHidden: z
        .boolean()
        .optional()
        .describe(
          'Include hidden files/directories (starting with .). Default: false.'
        ),
      includeIgnored: z
        .boolean()
        .optional()
        .describe(
          'Include .gitignore-ignored files (node_modules, dist). Default: false.'
        ),
      returnDiff: z
        .boolean()
        .optional()
        .describe(
          'Return unified diff of changes even if dryRun is false. Default: false.'
        ),
      maxFiles: z
        .int({ error: 'Must be integer' })
        .min(1, 'Min: 1')
        .max(10000, 'Max: 10,000')
        .optional()
        .describe('Max files to process before stopping'),
    });
    
    export const SearchAndReplaceOutputSchema = z.strictObject({
      ok: SuccessFlagSchema,
      matches: NonNegativeIntegerSchema.optional().describe('Total matches found'),
      filesChanged: NonNegativeIntegerSchema.optional().describe('Files modified'),

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/j0hanz/filesystem-mcp'

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