Files
zeekay 16d1277bc3 refactor: rename hanzo-tools-filesystem to hanzo-tools-fs
- Shorter package name for brevity
- hanzo_tools.filesystem -> hanzo_tools.fs
- Updated all dependencies and CI
2025-12-26 16:56:44 -08:00

109 lines
3.4 KiB
Python

"""Edit tool - find and replace in files."""
from typing import Optional, Annotated
from pathlib import Path
from pydantic import Field
from mcp.server import FastMCP
from mcp.server.fastmcp import Context as MCPContext
from hanzo_tools.core import FileSystemTool, PermissionManager, auto_timeout
class EditTool(FileSystemTool):
"""Edit files with find and replace."""
name = "edit"
@property
def description(self) -> str:
return """Edit a file by replacing text.
Args:
file_path: Absolute path to the file
old_string: Text to find (must be unique)
new_string: Text to replace with
expected_replacements: Expected number of replacements (default 1)
Returns:
Success message with diff or error
"""
def __init__(self, permission_manager: Optional[PermissionManager] = None):
super().__init__(permission_manager)
@auto_timeout("edit")
async def call(
self,
ctx: MCPContext,
file_path: str,
old_string: str,
new_string: str,
expected_replacements: int = 1,
**kwargs,
) -> str:
"""Edit file with find/replace."""
validation = self.validate_path(file_path)
if not validation:
return validation.error_message
if not self.is_path_allowed(file_path):
return f"Error: Access denied to path: {file_path}"
path = Path(file_path)
if not path.exists():
return f"Error: File does not exist: {file_path}"
if old_string == new_string:
return "Error: old_string and new_string are identical"
try:
with open(path, "r", encoding="utf-8") as f:
content = f.read()
# Count occurrences
count = content.count(old_string)
if count == 0:
return f"Error: old_string not found in file"
if count != expected_replacements:
return (
f"Error: Found {count} occurrences of old_string, "
f"expected {expected_replacements}. "
f"Use expected_replacements={count} or make old_string more specific."
)
# Perform replacement
new_content = content.replace(old_string, new_string, expected_replacements)
with open(path, "w", encoding="utf-8") as f:
f.write(new_content)
return f"Successfully edited {file_path}\nReplaced {count} occurrence(s)"
except Exception as e:
return f"Error editing file: {e}"
def register(self, mcp_server: FastMCP) -> None:
"""Register with MCP server."""
tool_instance = self
@mcp_server.tool()
async def edit(
file_path: Annotated[str, Field(description="Absolute path to the file")],
old_string: Annotated[str, Field(description="Text to find")],
new_string: Annotated[str, Field(description="Text to replace with")],
expected_replacements: Annotated[int, Field(description="Expected replacements")] = 1,
ctx: MCPContext = None,
) -> str:
"""Edit a file by replacing text."""
return await tool_instance.call(
ctx,
file_path=file_path,
old_string=old_string,
new_string=new_string,
expected_replacements=expected_replacements,
)