edit_file
Apply targeted text replacements in existing files, automatically creating backups to prevent data loss.
Instructions
Apply targeted text replacements in an existing file with backups.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| path | Yes | ||
| old_text | No | ||
| new_text | No | ||
| replace_all | No | ||
| edits | No | ||
| ctx | No |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
| result | Yes |
Implementation Reference
- src/tools/file_tools.py:112-232 (handler)The main implementation of the edit_file tool. Applies targeted text replacements in an existing file with policy checks, backup support, and Script Sentinel scanning.
def edit_file( path: str, old_text: str = "", new_text: str = "", replace_all: bool = False, edits: list[dict[str, Any]] | None = None, ctx: Context | None = None, ) -> str: """Apply targeted text replacements in an existing file with backups.""" context_tokens = activate_runtime_context(ctx) path = str(pathlib.Path(WORKSPACE_ROOT) / path) if not os.path.isabs(path) else path try: refresh_policy_if_changed() path_check = check_path_policy(path, tool="edit_file") if path_check: result = PolicyResult(allowed=False, reason=path_check[0], decision_tier="blocked", matched_rule=path_check[1]) else: result = PolicyResult(allowed=True, reason="allowed", decision_tier="allowed", matched_rule=None) log_entry = build_log_entry("edit_file", result, path=path) append_log_entry(log_entry) if not result.allowed: return f"[POLICY BLOCK] {result.reason}" target = pathlib.Path(path) if not target.exists(): return f"Error: file not found: {path}" if not target.is_file(): return f"Error: '{path}' is not a regular file" try: original = target.read_text(errors="replace") except OSError as e: return f"Error reading file for edit: {e}" operations: list[tuple[str, str, bool]] = [] if edits is not None: if not isinstance(edits, list): return "Error: edits must be a list of {old_text, new_text, replace_all?} objects" for idx, item in enumerate(edits, start=1): if not isinstance(item, dict): return f"Error: edit #{idx} is not an object" item_old = str(item.get("old_text", "")) item_new = str(item.get("new_text", "")) item_replace_all = bool(item.get("replace_all", False)) if not item_old: return f"Error: edit #{idx} has empty old_text" operations.append((item_old, item_new, item_replace_all)) else: if not old_text: return "Error: old_text is required when edits is not provided" operations.append((str(old_text), str(new_text), bool(replace_all))) updated = original total_replacements = 0 for idx, (needle, replacement, replace_everywhere) in enumerate(operations, start=1): matches = updated.count(needle) if matches == 0: return f"Error: edit #{idx} old_text not found in file" if not replace_everywhere and matches > 1: return ( f"Error: edit #{idx} old_text matched {matches} times; " "set replace_all=true for this edit to apply all matches" ) if replace_everywhere: updated = updated.replace(needle, replacement) total_replacements += matches else: updated = updated.replace(needle, replacement, 1) total_replacements += 1 if updated == original: return f"No changes made to {path}" backup_location = None backup_enabled = bool(POLICY.get("audit", {}).get("backup_enabled", True)) if backup_enabled: backup_location = backup_paths([path]) if backup_location: append_log_entry( { **log_entry, "source": "mcp-server", "backup_location": backup_location, "event": "backup_created", } ) try: target.write_text(updated) except OSError as e: return f"Error writing edited file: {e}" sentinel_scan = script_sentinel.scan_and_record_write(path, updated, writer_agent_id=AGENT_ID) if sentinel_scan.get("flagged"): append_log_entry( { **log_entry, "source": "mcp-server", "event": "script_sentinel_flagged", "content_hash": sentinel_scan.get("content_hash", ""), "matched_signatures": sentinel_scan.get("matched_signatures", []), "script_sentinel_mode": POLICY.get("script_sentinel", {}).get("mode", "match_original"), "script_sentinel_scan_mode": sentinel_scan.get("scan_mode", POLICY.get("script_sentinel", {}).get("scan_mode", "exec_context")), } ) msg = ( f"Successfully edited {path} " f"({total_replacements} replacement{'s' if total_replacements != 1 else ''} across {len(operations)} edit operation{'s' if len(operations) != 1 else ''})" ) if backup_location: msg += f" (previous version backed up to {backup_location})" else: msg += " (no content-change backup needed)" if sentinel_scan.get("flagged"): msg += " (Script Sentinel flagged content)" return msg finally: reset_runtime_context(context_tokens) - src/tools/file_tools.py:112-119 (schema)Function signature/parameters: path (str), old_text (str), new_text (str), replace_all (bool), edits (list[dict]), ctx (Context|None).
def edit_file( path: str, old_text: str = "", new_text: str = "", replace_all: bool = False, edits: list[dict[str, Any]] | None = None, ctx: Context | None = None, ) -> str: - src/server.py:21-31 (registration)edit_file is registered as an MCP tool via mcp.tool()(edit_file) in the FastMCP server.
for tool in [ server_info, restore_backup, execute_command, read_file, write_file, edit_file, delete_file, list_directory, ]: mcp.tool()(tool) - src/tools/__init__.py:2-2 (registration)edit_file is re-exported from tools package via from .file_tools import edit_file
from .file_tools import delete_file, edit_file, list_directory, read_file, write_file - src/airg_hook.py:15-16 (helper)Maps native 'Edit' and 'MultiEdit' tool calls to redirect to mcp__ai-runtime-guard__edit_file.
"Edit": "mcp__ai-runtime-guard__edit_file", "MultiEdit": "mcp__ai-runtime-guard__edit_file",