Skip to main content
Glama

MCP Claude Code

by SDGLBL
edit.py10.7 kB
"""Edit tool implementation. This module provides the Edit tool for making precise text replacements in files. """ from difflib import unified_diff from pathlib import Path from typing import Annotated, TypedDict, Unpack, final, override from fastmcp import Context as MCPContext from fastmcp import FastMCP from fastmcp.server.dependencies import get_context from pydantic import Field from mcp_claude_code.tools.filesystem.base import FilesystemBaseTool FilePath = Annotated[ str, Field( description="The absolute path to the file to modify (must be absolute, not relative)", ), ] OldString = Annotated[ str, Field( description="The text to replace (must match the file contents exactly, including all whitespace and indentation)", ), ] NewString = Annotated[ str, Field( description="The edited text to replace the old_string", ), ] ExpectedReplacements = Annotated[ int, Field( default=1, description="The expected number of replacements to perform. Defaults to 1 if not specified.", ), ] class EditToolParams(TypedDict): """Parameters for the Edit tool. Attributes: file_path: The absolute path to the file to modify (must be absolute, not relative) old_string: The text to replace (must match the file contents exactly, including all whitespace and indentation) new_string: The edited text to replace the old_string expected_replacements: The expected number of replacements to perform. Defaults to 1 if not specified. """ file_path: FilePath old_string: OldString new_string: NewString expected_replacements: ExpectedReplacements @final class Edit(FilesystemBaseTool): """Tool for making precise text replacements in files.""" @property @override def name(self) -> str: """Get the tool name. Returns: Tool name """ return "edit" @property @override def description(self) -> str: """Get the tool description. Returns: Tool description """ return """Performs exact string replacements in files with strict occurrence count validation. Usage: - When editing text from Read tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: spaces + line number + tab. Everything after that tab is the actual file content to match. Never include any part of the line number prefix in the old_string or new_string. - ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.""" @override async def call( self, ctx: MCPContext, **params: Unpack[EditToolParams], ) -> str: """Execute the tool with the given parameters. Args: ctx: MCP context **params: Tool parameters Returns: Tool result """ tool_ctx = self.create_tool_context(ctx) self.set_tool_context_info(tool_ctx) # Extract parameters file_path: FilePath = params["file_path"] old_string: OldString = params["old_string"] new_string: NewString = params["new_string"] expected_replacements = params.get("expected_replacements", 1) # Validate parameters path_validation = self.validate_path(file_path) if path_validation.is_error: await tool_ctx.error(path_validation.error_message) return f"Error: {path_validation.error_message}" # Only validate old_string for non-empty if we're not creating a new file # Empty old_string is valid when creating a new file file_exists = Path(file_path).exists() if file_exists and old_string.strip() == "": await tool_ctx.error( "Parameter 'old_string' cannot be empty for existing files" ) return "Error: Parameter 'old_string' cannot be empty for existing files" if ( expected_replacements is None or not isinstance(expected_replacements, (int, float)) or expected_replacements < 0 ): await tool_ctx.error( "Parameter 'expected_replacements' must be a non-negative number" ) return ( "Error: Parameter 'expected_replacements' must be a non-negative number" ) await tool_ctx.info(f"Editing file: {file_path}") # Check if file is allowed to be edited allowed, error_msg = await self.check_path_allowed(file_path, tool_ctx) if not allowed: return error_msg try: file_path_obj = Path(file_path) # If the file doesn't exist and old_string is empty, create a new file if not file_path_obj.exists() and old_string == "": # Check if parent directory is allowed parent_dir = str(file_path_obj.parent) if not self.is_path_allowed(parent_dir): await tool_ctx.error(f"Parent directory not allowed: {parent_dir}") return f"Error: Parent directory not allowed: {parent_dir}" # Create parent directories if they don't exist file_path_obj.parent.mkdir(parents=True, exist_ok=True) # Create the new file with the new_string content with open(file_path_obj, "w", encoding="utf-8") as f: f.write(new_string) await tool_ctx.info(f"Successfully created file: {file_path}") return ( f"Successfully created file: {file_path} ({len(new_string)} bytes)" ) # Check file exists for non-creation operations exists, error_msg = await self.check_path_exists(file_path, tool_ctx) if not exists: return error_msg # Check is a file is_file, error_msg = await self.check_is_file(file_path, tool_ctx) if not is_file: return error_msg # Read the file try: with open(file_path_obj, "r", encoding="utf-8") as f: original_content = f.read() # Apply edit if old_string in original_content: # Count occurrences of the old_string in the content occurrences = original_content.count(old_string) # Check if the number of occurrences matches expected_replacements if occurrences != expected_replacements: await tool_ctx.error( f"Found {occurrences} occurrences of the specified old_string, but expected {expected_replacements}" ) return f"Error: Found {occurrences} occurrences of the specified old_string, but expected {expected_replacements}. Change your old_string to uniquely identify the target text, or set expected_replacements={occurrences} to replace all occurrences." # Replace all occurrences since the count matches expectations modified_content = original_content.replace(old_string, new_string) else: # If we can't find the exact string, report an error await tool_ctx.error( "The specified old_string was not found in the file content" ) return "Error: The specified old_string was not found in the file content. Please check that it matches exactly, including all whitespace and indentation." # Generate diff original_lines = original_content.splitlines(keepends=True) modified_lines = modified_content.splitlines(keepends=True) diff_lines = list( unified_diff( original_lines, modified_lines, fromfile=f"{file_path} (original)", tofile=f"{file_path} (modified)", n=3, ) ) diff_text = "".join(diff_lines) # Determine the number of backticks needed num_backticks = 3 while f"```{num_backticks}" in diff_text: num_backticks += 1 # Format diff with appropriate number of backticks formatted_diff = ( f"```{num_backticks}diff\n{diff_text}```{num_backticks}\n" ) # Write the file if there are changes if diff_text: with open(file_path_obj, "w", encoding="utf-8") as f: f.write(modified_content) await tool_ctx.info( f"Successfully edited file: {file_path} ({expected_replacements} replacements applied)" ) return f"Successfully edited file: {file_path} ({expected_replacements} replacements applied)\n\n{formatted_diff}" else: return f"No changes made to file: {file_path}" except UnicodeDecodeError: await tool_ctx.error(f"Cannot edit binary file: {file_path}") return f"Error: Cannot edit binary file: {file_path}" except Exception as e: await tool_ctx.error(f"Error editing file: {str(e)}") return f"Error editing file: {str(e)}" @override def register(self, mcp_server: FastMCP) -> None: """Register this edit tool with the MCP server. Creates a wrapper function with explicitly defined parameters that match the tool's parameter schema and registers it with the MCP server. Args: mcp_server: The FastMCP server instance """ tool_self = self # Create a reference to self for use in the closure @mcp_server.tool(name=self.name, description=self.description) async def edit( ctx: MCPContext, file_path: FilePath, old_string: OldString, new_string: NewString, expected_replacements: ExpectedReplacements, ) -> str: ctx = get_context() return await tool_self.call( ctx, file_path=file_path, old_string=old_string, new_string=new_string, expected_replacements=expected_replacements, )

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/SDGLBL/mcp-claude-code'

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