Skip to main content
Glama
safurrier

MCP Filesystem Server

edit_file_at_line

Modify specific lines in text files by replacing, inserting, or deleting content at precise line positions, with verification options to ensure accurate file editing.

Instructions

Edit specific lines in a text file.

Args: path: Path to the file line_edits: List of edits to apply. Each edit is a dict with: - line_number: Line number to edit (0-based if relative_line_numbers=True, otherwise 1-based) - action: "replace", "insert_before", "insert_after", "delete" - content: New content for replace/insert operations (optional for delete) - expected_content: (Optional) Expected content of the line being edited for verification offset: Line offset (0-based) to start considering lines limit: Maximum number of lines to consider relative_line_numbers: Whether line numbers in edits are relative to offset abort_on_verification_failure: Whether to abort all edits if any verification fails encoding: Text encoding (default: utf-8) dry_run: If True, returns what would be changed without modifying the file ctx: MCP context Returns: Edit results summary

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
pathYes
line_editsYes
offsetNo
limitNo
relative_line_numbersNo
abort_on_verification_failureNo
encodingNoutf-8
dry_runNo

Implementation Reference

  • Primary MCP tool handler for 'edit_file_at_line'. Decorated with @mcp.tool() for registration. Parses arguments, delegates to Operations.edit_file_at_line, and returns formatted string response with edit summary.
    @mcp.tool() async def edit_file_at_line( path: str, line_edits: List[Dict[str, Any]], ctx: Context, offset: int = 0, limit: Optional[int] = None, relative_line_numbers: bool = False, abort_on_verification_failure: bool = False, encoding: str = "utf-8", dry_run: bool = False, ) -> str: """Edit specific lines in a text file. Args: path: Path to the file line_edits: List of edits to apply. Each edit is a dict with: - line_number: Line number to edit (0-based if relative_line_numbers=True, otherwise 1-based) - action: "replace", "insert_before", "insert_after", "delete" - content: New content for replace/insert operations (optional for delete) - expected_content: (Optional) Expected content of the line being edited for verification offset: Line offset (0-based) to start considering lines limit: Maximum number of lines to consider relative_line_numbers: Whether line numbers in edits are relative to offset abort_on_verification_failure: Whether to abort all edits if any verification fails encoding: Text encoding (default: utf-8) dry_run: If True, returns what would be changed without modifying the file ctx: MCP context Returns: Edit results summary """ try: components = get_components() results = await components["operations"].edit_file_at_line( path, line_edits, offset, limit, relative_line_numbers, abort_on_verification_failure, encoding, dry_run, ) # Format as text summary mode = "Would apply" if dry_run else "Applied" # Check if we had verification failures that prevented editing if "success" in results and not results["success"]: summary = [ f"Failed to edit {results['path']} due to content verification failures:", "", ] if "verification_failures" in results: for failure in results["verification_failures"]: line_num = failure.get("line", "?") action = failure.get("action", "?") summary.append(f"Line {line_num}: {action} - Verification failed") summary.append(f" Expected: {failure.get('expected', '').strip()}") summary.append(f" Actual: {failure.get('actual', '').strip()}") summary.append("") if "message" in results: summary.append(f"Error: {results['message']}") return "\n".join(summary) # Normal success case summary = [ f"{mode} {results['edits_applied']} edits to {results['path']}:", "", ] # Add verification warnings if any if "verification_failures" in results and results["verification_failures"]: summary.append( "Warning: Some content verification checks failed but edits were applied:" ) for failure in results["verification_failures"]: line_num = failure.get("line", "?") summary.append(f" Line {line_num}: Content did not match expected") summary.append("") for change in results["changes"]: line_num = change.get("line", "?") action = change.get("action", "?") orig_line_num = change.get("original_line_number", "") line_info = f"Line {line_num}" if relative_line_numbers and orig_line_num != "": line_info = f"Line {line_num} (relative: {orig_line_num})" if action == "replace": summary.append(f"{line_info}: Replaced") summary.append(f" - {change.get('before', '').strip()}") summary.append(f" + {change.get('after', '').strip()}") elif action == "insert_before": summary.append(f"{line_info}: Inserted before") summary.append(f" + {change.get('content', '').strip()}") elif action == "insert_after": summary.append(f"{line_info}: Inserted after") summary.append(f" + {change.get('content', '').strip()}") elif action == "delete": summary.append(f"{line_info}: Deleted") summary.append(f" - {change.get('content', '').strip()}") if "error" in change: summary.append(f" ! Error: {change['error']}") summary.append("") return "\n".join(summary) except Exception as e: return f"Error editing file: {str(e)}"
  • Core helper function in Operations class implementing the file editing logic: reads file into lines, validates and adjusts line numbers, performs verification if specified, applies edits in reverse order to preserve indices, handles dry_run, and returns detailed results dict.
    async def edit_file_at_line( self, path: Union[str, Path], line_edits: List[Dict[str, Any]], offset: int = 0, limit: Optional[int] = None, relative_line_numbers: bool = False, abort_on_verification_failure: bool = False, encoding: str = "utf-8", dry_run: bool = False, ) -> Dict[str, Any]: """Edit specific lines in a text file. Args: path: Path to the file line_edits: List of edits to apply. Each edit is a dict with: - line_number: Line number to edit (0-based if relative_line_numbers=True, otherwise 1-based) - action: "replace", "insert_before", "insert_after", "delete" - content: New content for replace/insert operations (optional for delete) - expected_content: (Optional) Expected content of the line being edited for verification offset: Line offset (0-based) to start considering lines limit: Maximum number of lines to consider relative_line_numbers: Whether line numbers in edits are relative to offset abort_on_verification_failure: Whether to abort all edits if any verification fails encoding: Text encoding (default: utf-8) dry_run: If True, returns what would be changed without modifying the file Returns: Dict with edit results including verification information Raises: ValueError: If path is outside allowed directories FileNotFoundError: If file does not exist """ abs_path, allowed = await self.validator.validate_path(path) if not allowed: raise ValueError(f"Path outside allowed directories: {path}") # Parameter validation if offset < 0: raise ValueError("offset must be non-negative") if limit is not None and limit < 0: raise ValueError("limit must be non-negative") try: # Read the entire file content = await anyio.to_thread.run_sync( partial(abs_path.read_text, encoding=encoding) ) lines = content.splitlines(keepends=True) # Calculate effective range end_offset = None if limit is not None: end_offset = offset + limit - 1 else: end_offset = len(lines) - 1 # Ensure end_offset is within bounds if end_offset >= len(lines): end_offset = len(lines) - 1 # Track verification failures verification_failures = [] # Validate line_edits and adjust line numbers if needed for i, edit in enumerate(line_edits): if "line_number" not in edit: raise ValueError(f"Edit {i} is missing line_number") if "action" not in edit: raise ValueError(f"Edit {i} is missing action") line_num = edit["line_number"] action = edit["action"] # Handle relative line numbers absolute_line_num = line_num if relative_line_numbers: # If relative, line_num is 0-based and relative to offset if line_num < 0: raise ValueError( f"Relative line number {line_num} must be non-negative" ) absolute_line_num = offset + line_num + 1 else: # Not relative, line_num is 1-based (standard line numbers) absolute_line_num = line_num # Store the adjusted line number for later use edit["_absolute_line_num"] = absolute_line_num # Check if line is within the considered range if ( absolute_line_num < 1 or absolute_line_num > len(lines) + 1 ): # +1 to allow appending at the end raise ValueError( f"Line number {absolute_line_num} is outside file bounds (1-{len(lines)})" ) if offset > 0 and absolute_line_num < offset + 1: raise ValueError( f"Line number {absolute_line_num} is before offset {offset}" ) if limit is not None and absolute_line_num > offset + limit: raise ValueError( f"Line number {absolute_line_num} is beyond limit (offset {offset} + limit {limit})" ) if action not in ["replace", "insert_before", "insert_after", "delete"]: raise ValueError(f"Invalid action '{action}' in edit {i}") if action != "delete" and "content" not in edit: raise ValueError( f"Edit {i} with action '{action}' is missing content" ) # Verify expected content if provided if "expected_content" in edit and absolute_line_num <= len(lines): expected = edit["expected_content"] actual = lines[absolute_line_num - 1].rstrip("\r\n") if expected.rstrip("\r\n") != actual: failure = { "edit_index": i, "line": absolute_line_num, "action": action, "expected": expected, "actual": actual, } verification_failures.append(failure) if abort_on_verification_failure: return { "success": False, "path": str(abs_path), "verification_failures": verification_failures, "message": f"Content verification failed at line {absolute_line_num}", "edits_applied": 0, "changes": [], "dry_run": dry_run, } # If there are verification failures but we're not aborting, # we'll continue with the edits and report the failures # Sort edits by absolute line number in reverse order to avoid line number changes line_edits = sorted( line_edits, key=lambda e: e["_absolute_line_num"], reverse=True ) # Apply edits results = [] for edit in line_edits: # Use the adjusted absolute line number line_num = edit["_absolute_line_num"] action = edit["action"] content_before = lines[line_num - 1] if line_num <= len(lines) else "" if action == "replace": # Ensure proper line endings new_content = edit["content"] if not new_content.endswith("\n") and content_before.endswith("\n"): new_content += "\n" if line_num <= len(lines): lines[line_num - 1] = new_content else: # If replacing beyond the end, append with any necessary newlines while len(lines) < line_num - 1: lines.append("\n") lines.append(new_content) results.append( { "line": line_num, "original_line_number": edit["line_number"], "action": "replace", "before": content_before, "after": new_content, } ) elif action == "insert_before": # Ensure proper line endings new_content = edit["content"] if not new_content.endswith("\n"): new_content += "\n" if line_num <= len(lines): lines.insert(line_num - 1, new_content) else: # If inserting beyond the end, append with any necessary newlines while len(lines) < line_num - 1: lines.append("\n") lines.append(new_content) results.append( { "line": line_num, "original_line_number": edit["line_number"], "action": "insert_before", "content": new_content, } ) elif action == "insert_after": # Ensure proper line endings new_content = edit["content"] if not new_content.endswith("\n"): new_content += "\n" if line_num <= len(lines): lines.insert(line_num, new_content) else: # If inserting beyond the end, append with any necessary newlines while len(lines) < line_num: lines.append("\n") lines.append(new_content) results.append( { "line": line_num, "action": "insert_after", "content": new_content, } ) elif action == "delete": if line_num <= len(lines): deleted_content = lines.pop(line_num - 1) results.append( { "line": line_num, "action": "delete", "content": deleted_content, } ) else: results.append( { "line": line_num, "action": "delete", "error": "Line does not exist", } ) # Write back the file if not a dry run if not dry_run: new_content = "".join(lines) await anyio.to_thread.run_sync( partial(abs_path.write_text, new_content, encoding=encoding) ) return { "path": str(abs_path), "edits_applied": len(results), "dry_run": dry_run, "changes": results, }
  • The @mcp.tool() decorator registers the edit_file_at_line tool with the MCP server.
    @mcp.tool()
  • Type annotations and docstring Args section define the input schema for the tool.
    Args: path: Path to the file line_edits: List of edits to apply. Each edit is a dict with: - line_number: Line number to edit (0-based if relative_line_numbers=True, otherwise 1-based) - action: "replace", "insert_before", "insert_after", "delete" - content: New content for replace/insert operations (optional for delete) - expected_content: (Optional) Expected content of the line being edited for verification offset: Line offset (0-based) to start considering lines limit: Maximum number of lines to consider relative_line_numbers: Whether line numbers in edits are relative to offset abort_on_verification_failure: Whether to abort all edits if any verification fails encoding: Text encoding (default: utf-8) dry_run: If True, returns what would be changed without modifying the file ctx: MCP context Returns: Edit results summary """

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

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