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