Skip to main content
Glama

edit_block

Apply surgical edits to files by replacing specific text or updating Excel ranges with precise control over changes.

Instructions

                    Apply surgical edits to files.

                    BEST PRACTICE: Make multiple small, focused edits rather than one large edit.
                    Each edit_block call should change only what needs to be changed - include just enough
                    context to uniquely identify the text being modified.

                    FORMAT HANDLING (by extension):

                    EXCEL FILES (.xlsx, .xls, .xlsm) - Range Update mode:
                    Takes:
                    - file_path: Path to the Excel file
                    - range: ALWAYS use FROM:TO format - "SheetName!A1:C10" or "SheetName!C1:C1"
                    - content: 2D array, e.g., [["H1","H2"],["R1","R2"]]

                    TEXT FILES - Find/Replace mode:
                    Takes:
                    - file_path: Path to the file to edit
                    - old_string: Text to replace
                    - new_string: Replacement text
                    - expected_replacements: Optional number of replacements (default: 1)

                    By default, replaces only ONE occurrence of the search text.
                    To replace multiple occurrences, provide expected_replacements with
                    the exact number of matches expected.

                    UNIQUENESS REQUIREMENT: When expected_replacements=1 (default), include the minimal
                    amount of context necessary (typically 1-3 lines) before and after the change point,
                    with exact whitespace and indentation.

                    When editing multiple sections, make separate edit_block calls for each distinct change
                    rather than one large replacement.

                    When a close but non-exact match is found, a character-level diff is shown in the format:
                    common_prefix{-removed-}{+added+}common_suffix to help you identify what's different.

                    Similar to write_file, there is a configurable line limit (fileWriteLineLimit) that warns
                    if the edited file exceeds this limit. If this happens, consider breaking your edits into
                    smaller, more focused changes.

                    IMPORTANT: Always use absolute paths for reliability. Paths are automatically normalized regardless of slash direction. Relative paths may fail as they depend on the current working directory. Tilde paths (~/...) might not work in all contexts. Unless the user explicitly asks for relative paths, use absolute paths.
                    This command can be referenced as "DC: ..." or "use Desktop Commander to ..." in your instructions.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
file_pathYes
old_stringNo
new_stringNo
expected_replacementsNo
rangeNo
contentNo
optionsNo

Implementation Reference

  • The main handler function for the 'edit_block' tool. It parses arguments using EditBlockArgsSchema and dispatches to either structured file range editing (for Excel etc.) or text file search/replace editing.
    export async function handleEditBlock(args: unknown): Promise<ServerResult> {
        const parsed = EditBlockArgsSchema.parse(args);
    
        // Structured files: Range rewrite
        if (parsed.range !== undefined && parsed.content !== undefined) {
            try {
                // Validate path before any filesystem operations
                const validatedPath = await validatePath(parsed.file_path);
    
                const { getFileHandler } = await import('../utils/files/factory.js');
                const handler = await getFileHandler(validatedPath);
    
                // Parse content if it's a JSON string (AI often sends arrays as JSON strings)
                let content = parsed.content;
                if (typeof content === 'string') {
                    try {
                        content = JSON.parse(content);
                    } catch {
                        // Leave as-is if not valid JSON - let handler decide
                    }
                }
    
                // Check if handler supports range editing
                if ('editRange' in handler && typeof handler.editRange === 'function') {
                    await handler.editRange(validatedPath, parsed.range, content, parsed.options);
                    return {
                        content: [{
                            type: "text",
                            text: `Successfully updated range ${parsed.range} in ${parsed.file_path}`
                        }],
                    };
                } else {
                    return {
                        content: [{
                            type: "text",
                            text: `Error: Range-based editing not supported for ${parsed.file_path}`
                        }],
                        isError: true
                    };
                }
            } catch (error) {
                const errorMessage = error instanceof Error ? error.message : String(error);
                return {
                    content: [{
                        type: "text",
                        text: `Error: ${errorMessage}`
                    }],
                    isError: true
                };
            }
        }
    
        // Text files: String replacement
        // Validate required parameters for text replacement
        if (parsed.old_string === undefined || parsed.new_string === undefined) {
            return {
                content: [{
                    type: "text",
                    text: `Error: Text replacement requires both old_string and new_string parameters`
                }],
                isError: true
            };
        }
    
        const searchReplace = {
            search: parsed.old_string,
            replace: parsed.new_string
        };
    
        return performSearchReplace(parsed.file_path, searchReplace, parsed.expected_replacements);
    }
  • Zod schema defining input parameters for edit_block tool, supporting both text replacement (old_string, new_string, expected_replacements) and structured range edits (range, content).
    export const EditBlockArgsSchema = z.object({
      file_path: z.string(),
      // Text file string replacement
      old_string: z.string().optional(),
      new_string: z.string().optional(),
      expected_replacements: z.number().optional().default(1),
      // Structured file range rewrite (Excel, etc.)
      range: z.string().optional(),
      content: z.any().optional(),
      options: z.record(z.any()).optional()
    }).refine(
      data => (data.old_string !== undefined && data.new_string !== undefined) ||
              (data.range !== undefined && data.content !== undefined),
      { message: "Must provide either (old_string + new_string) or (range + content)" }
    );
  • src/server.ts:674-724 (registration)
    Registration of 'edit_block' tool in the list_tools handler, defining name, description, input schema, and annotations.
        name: "edit_block",
        description: `
                Apply surgical edits to files.
    
                BEST PRACTICE: Make multiple small, focused edits rather than one large edit.
                Each edit_block call should change only what needs to be changed - include just enough
                context to uniquely identify the text being modified.
    
                FORMAT HANDLING (by extension):
    
                EXCEL FILES (.xlsx, .xls, .xlsm) - Range Update mode:
                Takes:
                - file_path: Path to the Excel file
                - range: ALWAYS use FROM:TO format - "SheetName!A1:C10" or "SheetName!C1:C1"
                - content: 2D array, e.g., [["H1","H2"],["R1","R2"]]
    
                TEXT FILES - Find/Replace mode:
                Takes:
                - file_path: Path to the file to edit
                - old_string: Text to replace
                - new_string: Replacement text
                - expected_replacements: Optional number of replacements (default: 1)
    
                By default, replaces only ONE occurrence of the search text.
                To replace multiple occurrences, provide expected_replacements with
                the exact number of matches expected.
    
                UNIQUENESS REQUIREMENT: When expected_replacements=1 (default), include the minimal
                amount of context necessary (typically 1-3 lines) before and after the change point,
                with exact whitespace and indentation.
    
                When editing multiple sections, make separate edit_block calls for each distinct change
                rather than one large replacement.
    
                When a close but non-exact match is found, a character-level diff is shown in the format:
                common_prefix{-removed-}{+added+}common_suffix to help you identify what's different.
    
                Similar to write_file, there is a configurable line limit (fileWriteLineLimit) that warns
                if the edited file exceeds this limit. If this happens, consider breaking your edits into
                smaller, more focused changes.
    
                ${PATH_GUIDANCE}
                ${CMD_PREFIX_DESCRIPTION}`,
        inputSchema: zodToJsonSchema(EditBlockArgsSchema),
        annotations: {
            title: "Edit Block",
            readOnlyHint: false,
            destructiveHint: true,
            openWorldHint: false,
        },
    },
  • Dispatch in CallToolRequestSchema handler that routes 'edit_block' calls to handlers.handleEditBlock.
    case "edit_block":
        result = await handlers.handleEditBlock(args);
        break;
  • Core helper function implementing text file editing logic: exact search/replace, fuzzy matching fallback, line ending handling, occurrence counting, and warnings for large edits.
    export async function performSearchReplace(filePath: string, block: SearchReplace, expectedReplacements: number = 1): Promise<ServerResult> {
        // Get file extension for telemetry using path module
        const fileExtension = path.extname(filePath).toLowerCase();
        
        // Capture file extension and string sizes in telemetry without capturing the file path
        capture('server_edit_block', {
            fileExtension: fileExtension,
            oldStringLength: block.search.length,
            oldStringLines: block.search.split('\n').length,
            newStringLength: block.replace.length,
            newStringLines: block.replace.split('\n').length,
            expectedReplacements: expectedReplacements
        });
        // Check for empty search string to prevent infinite loops
        if (block.search === "") {
        
            // Capture file extension in telemetry without capturing the file path
            capture('server_edit_block_empty_search', {fileExtension: fileExtension, expectedReplacements});
            return {
                content: [{ 
                    type: "text", 
                    text: "Empty search strings are not allowed. Please provide a non-empty string to search for."
                }],
            };
        }
        
    
        // Read file directly to preserve line endings - critical for edit operations
        const validPath = await validatePath(filePath);
        const content = await readFileInternal(validPath, 0, Number.MAX_SAFE_INTEGER);
        
        // Make sure content is a string
        if (typeof content !== 'string') {
            capture('server_edit_block_content_not_string', {fileExtension: fileExtension, expectedReplacements});
            throw new Error('Wrong content for file ' + filePath);
        }
        
        // Get the line limit from configuration
        const config = await configManager.getConfig();
        const MAX_LINES = config.fileWriteLineLimit ?? 50; // Default to 50 if not set
        
        // Detect file's line ending style
        const fileLineEnding = detectLineEnding(content);
        
        // Normalize search string to match file's line endings
        const normalizedSearch = normalizeLineEndings(block.search, fileLineEnding);
        
        // First try exact match
        let tempContent = content;
        let count = 0;
        let pos = tempContent.indexOf(normalizedSearch);
        
        while (pos !== -1) {
            count++;
            pos = tempContent.indexOf(normalizedSearch, pos + 1);
        }
        
        // If exact match found and count matches expected replacements, proceed with exact replacement
        if (count > 0 && count === expectedReplacements) {
            // Replace all occurrences
            let newContent = content;
            
            // If we're only replacing one occurrence, replace it directly
            if (expectedReplacements === 1) {
                const searchIndex = newContent.indexOf(normalizedSearch);
                newContent = 
                    newContent.substring(0, searchIndex) + 
                    normalizeLineEndings(block.replace, fileLineEnding) + 
                    newContent.substring(searchIndex + normalizedSearch.length);
            } else {
                // Replace all occurrences using split and join for multiple replacements
                newContent = newContent.split(normalizedSearch).join(normalizeLineEndings(block.replace, fileLineEnding));
            }
            
            // Check if search or replace text has too many lines
            const searchLines = block.search.split('\n').length;
            const replaceLines = block.replace.split('\n').length;
            const maxLines = Math.max(searchLines, replaceLines);
            let warningMessage = "";
            
            if (maxLines > MAX_LINES) {
                const problemText = searchLines > replaceLines ? 'search text' : 'replacement text';
                warningMessage = `\n\nWARNING: The ${problemText} has ${maxLines} lines (maximum: ${MAX_LINES}).
                
    RECOMMENDATION: For large search/replace operations, consider breaking them into smaller chunks with fewer lines.`;
            }
            
            await writeFile(filePath, newContent);
            capture('server_edit_block_exact_success', {fileExtension: fileExtension, expectedReplacements, hasWarning: warningMessage !== ""});
            return {
                content: [{ 
                    type: "text", 
                    text: `Successfully applied ${expectedReplacements} edit${expectedReplacements > 1 ? 's' : ''} to ${filePath}${warningMessage}` 
                }],
            };
        }
        
        // If exact match found but count doesn't match expected, inform the user
        if (count > 0 && count !== expectedReplacements) {
            capture('server_edit_block_unexpected_count', {fileExtension: fileExtension, expectedReplacements, expectedReplacementsCount: count});
            return {
                content: [{ 
                    type: "text", 
                    text: `Expected ${expectedReplacements} occurrences but found ${count} in ${filePath}. ` + 
                `Double check and make sure you understand all occurencies and if you want to replace all ${count} occurrences, set expected_replacements to ${count}. ` +
                `If there are many occurrancies and you want to change some of them and keep the rest. Do it one by one, by adding more lines around each occurrence.` +
    `If you want to replace a specific occurrence, make your search string more unique by adding more lines around search string.`
                }],
            };
        }
        
        // If exact match not found, try fuzzy search
        if (count === 0) {
            // Track fuzzy search time
            const startTime = performance.now();
            
            // Perform fuzzy search
            const fuzzyResult = recursiveFuzzyIndexOf(content, block.search);
            const similarity = getSimilarityRatio(block.search, fuzzyResult.value);
            
            // Calculate execution time in milliseconds
            const executionTime = performance.now() - startTime;
            
            // Generate diff and gather character code data
            const diff = highlightDifferences(block.search, fuzzyResult.value);
            
            // Count character codes in diff
            const characterCodeData = getCharacterCodeData(block.search, fuzzyResult.value);
            
            // Create comprehensive log entry
            const logEntry: FuzzySearchLogEntry = {
                timestamp: new Date(),
                searchText: block.search,
                foundText: fuzzyResult.value,
                similarity: similarity,
                executionTime: executionTime,
                exactMatchCount: count,
                expectedReplacements: expectedReplacements,
                fuzzyThreshold: FUZZY_THRESHOLD,
                belowThreshold: similarity < FUZZY_THRESHOLD,
                diff: diff,
                searchLength: block.search.length,
                foundLength: fuzzyResult.value.length,
                fileExtension: fileExtension,
                characterCodes: characterCodeData.report,
                uniqueCharacterCount: characterCodeData.uniqueCount,
                diffLength: characterCodeData.diffLength
            };
            
            // Log to file
            await fuzzySearchLogger.log(logEntry);
            
            // Combine all fuzzy search data for single capture
            const fuzzySearchData = {
                similarity: similarity,
                execution_time_ms: executionTime,
                search_length: block.search.length,
                file_size: content.length,
                threshold: FUZZY_THRESHOLD,
                found_text_length: fuzzyResult.value.length,
                character_codes: characterCodeData.report,
                unique_character_count: characterCodeData.uniqueCount,
                total_diff_length: characterCodeData.diffLength
            };
            
            // Check if the fuzzy match is "close enough"
            if (similarity >= FUZZY_THRESHOLD) {
                // Capture the fuzzy search event with all data
                capture('server_fuzzy_search_performed', fuzzySearchData);
                
                // If we allow fuzzy matches, we would make the replacement here
                // For now, we'll return a detailed message about the fuzzy match
                return {
                    content: [{ 
                        type: "text", 
                        text: `Exact match not found, but found a similar text with ${Math.round(similarity * 100)}% similarity (found in ${executionTime.toFixed(2)}ms):\n\n` +
                              `Differences:\n${diff}\n\n` +
                              `To replace this text, use the exact text found in the file.\n\n` +
                              `Log entry saved for analysis. Use the following command to check the log:\n` +
                              `Check log: ${await fuzzySearchLogger.getLogPath()}`
                    }],// TODO
                };
            } else {
                // If the fuzzy match isn't close enough
                // Still capture the fuzzy search event with all data
                capture('server_fuzzy_search_performed', {
                    ...fuzzySearchData,
                    below_threshold: true
                });
                
                return {
                    content: [{ 
                        type: "text", 
                        text: `Search content not found in ${filePath}. The closest match was "${fuzzyResult.value}" ` +
                              `with only ${Math.round(similarity * 100)}% similarity, which is below the ${Math.round(FUZZY_THRESHOLD * 100)}% threshold. ` +
                              `(Fuzzy search completed in ${executionTime.toFixed(2)}ms)\n\n` +
                              `Log entry saved for analysis. Use the following command to check the log:\n` +
                              `Check log: ${await fuzzySearchLogger.getLogPath()}`
                    }],
                };
            }
        }
        
        throw new Error("Unexpected error during search and replace operation.");
    }

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/wonderwhy-er/ClaudeComputerCommander'

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