edit_block
Perform precise text replacements in files by specifying the exact context and replacements needed. Ensure unique, focused edits with minimal changes per call. Use absolute paths for reliability and manage edits effectively by handling small changes separately.
Instructions
Apply surgical text replacements 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.
Takes:
- file_path: Path to the file to edit
- old_string: Text to replace
- new_string: Replacement text
- expected_replacements: Optional parameter for number of replacements
By default, replaces only ONE occurrence of the search text.
To replace multiple occurrences, provide the expected_replacements parameter 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
| Name | Required | Description | Default |
|---|---|---|---|
| expected_replacements | No | ||
| file_path | Yes | ||
| new_string | Yes | ||
| old_string | Yes |
Implementation Reference
- src/tools/edit.ts:345-354 (handler)The handleEditBlock function: entry point for the tool execution. Parses input arguments using EditBlockArgsSchema and delegates to performSearchReplace for the core editing logic.export async function handleEditBlock(args: unknown): Promise<ServerResult> { const parsed = EditBlockArgsSchema.parse(args); const searchReplace = { search: parsed.old_string, replace: parsed.new_string }; return performSearchReplace(parsed.file_path, searchReplace, parsed.expected_replacements); }
- src/tools/edit.ts:96-300 (handler)Core implementation of the edit operation: handles exact string replacement with occurrence counting, fuzzy matching fallback, line ending normalization, file validation, and detailed error reporting.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."); }
- src/tools/schemas.ts:120-125 (schema)Zod schema for validating input arguments to the edit_block tool: file_path, old_string, new_string, and optional expected_replacements.export const EditBlockArgsSchema = z.object({ file_path: z.string(), old_string: z.string(), new_string: z.string(), expected_replacements: z.number().optional().default(1), });
- src/server.ts:647-689 (registration)Tool registration in the list_tools handler: defines the tool name 'edit_block', description, input schema from EditBlockArgsSchema, and annotations.{ name: "edit_block", description: ` Apply surgical text replacements 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. Takes: - file_path: Path to the file to edit - old_string: Text to replace - new_string: Replacement text - expected_replacements: Optional parameter for number of replacements By default, replaces only ONE occurrence of the search text. To replace multiple occurrences, provide the expected_replacements parameter 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 Text Block", readOnlyHint: false, destructiveHint: true, openWorldHint: false, }, },
- src/server.ts:1284-1286 (registration)Dispatch registration in the call_tool handler switch statement: routes 'edit_block' calls to handlers.handleEditBlock.case "edit_block": result = await handlers.handleEditBlock(args); break;