Skip to main content
Glama

FileWriteOrEdit

DestructiveIdempotent

Write or edit files by specifying the percentage of changes needed, using either full content or search/replace blocks based on the modification scope.

Instructions

  • Writes or edits a file based on the percentage of changes.

  • Use absolute path only (~ allowed).

  • First write down percentage of lines that need to be replaced in the file (between 0-100) in percentage_to_change

  • percentage_to_change should be low if mostly new code is to be added. It should be high if a lot of things are to be replaced.

  • If percentage_to_change > 50, provide full file content in text_or_search_replace_blocks

  • If percentage_to_change <= 50, text_or_search_replace_blocks should be search/replace blocks.

Instructions for editing files.

Example

Input file

import numpy as np
from impls import impl1, impl2

def hello():
    "print a greeting"

    print("hello")

def call_hello():
    "call hello"

    hello()
    print("Called")
    impl1()
    hello()
    impl2()

Edit format on the input file

<<<<<<< SEARCH
from impls import impl1, impl2
=======
from impls import impl1, impl2
from hello import hello as hello_renamed
>>>>>>> REPLACE
<<<<<<< SEARCH
def hello():
    "print a greeting"

    print("hello")
=======
>>>>>>> REPLACE
<<<<<<< SEARCH
def call_hello():
    "call hello"

    hello()
=======
def call_hello_renamed():
    "call hello renamed"

    hello_renamed()
>>>>>>> REPLACE
<<<<<<< SEARCH
    impl1()
    hello()
    impl2()
=======
    impl1()
    hello_renamed()
    impl2()
>>>>>>> REPLACE

SEARCH/REPLACE block Rules:

Every "<<<<<<< SEARCH" section must EXACTLY MATCH the existing file content, character for character, including all comments, docstrings, whitespaces, etc.

Including multiple unique SEARCH/REPLACE blocks if needed. Include enough and only enough lines in each SEARCH section to uniquely match each set of lines that need to change.

Keep SEARCH/REPLACE blocks concise. Break large SEARCH/REPLACE blocks into a series of smaller blocks that each change a small portion of the file. Include just the changing lines, and a few surrounding lines (0-3 lines) if needed for uniqueness. Other than for uniqueness, avoid including those lines which do not change in search (and replace) blocks. Target 0-3 non trivial extra lines per block.

Preserve leading spaces and indentations in both SEARCH and REPLACE blocks.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
file_pathYes#1: absolute file path
percentage_to_changeYes#2: predict this percentage, calculated as number of existing lines that will have some diff divided by total existing lines.
text_or_search_replace_blocksYes#3: content/edit blocks. Must be after #2 in the tool xml
thread_idYes#4: thread_id

Implementation Reference

  • Main handler function for the FileWriteOrEdit tool. Determines whether to perform a full file write or a targeted edit based on the predicted percentage_to_change, delegating to write_file or do_diff_edit accordingly.
    def file_writing(
        file_writing_args: FileWriteOrEdit,
        coding_max_tokens: Optional[int],
        noncoding_max_tokens: Optional[int],
        context: Context,
    ) -> tuple[
        str, dict[str, list[tuple[int, int]]]
    ]:  # Updated to return message and file paths with line ranges
        """
        Write or edit a file based on percentage of changes.
        If percentage_changed > 50%, treat content as direct file content.
        Otherwise, treat content as search/replace blocks.
        """
        # Check if the thread_id matches current
        if file_writing_args.thread_id != context.bash_state.current_thread_id:
            # Try to load state from the thread_id
            if not context.bash_state.load_state_from_thread_id(
                file_writing_args.thread_id
            ):
                return (
                    f"Error: No saved bash state found for thread_id `{file_writing_args.thread_id}`. Please re-initialize to get a new id or use correct id.",
                    {},
                )
    
        # Expand the path before checking if it's absolute
        path_ = expand_user(file_writing_args.file_path)
        if not os.path.isabs(path_):
            return (
                f"Failure: file_path should be absolute path, current working directory is {context.bash_state.cwd}",
                {},  # Return empty dict instead of empty list for type consistency
            )
    
        # If file doesn't exist, always use direct file_content mode
        content = file_writing_args.text_or_search_replace_blocks
    
        if not _is_edit(content, file_writing_args.percentage_to_change):
            # Use direct content mode (same as WriteIfEmpty)
            result, paths = write_file(
                WriteIfEmpty(
                    file_path=path_,
                    file_content=file_writing_args.text_or_search_replace_blocks,
                ),
                True,
                coding_max_tokens,
                noncoding_max_tokens,
                context,
            )
            return result, paths
        else:
            # File exists and percentage <= 50, use search/replace mode
            result, paths = do_diff_edit(
                FileEdit(
                    file_path=path_,
                    file_edit_using_search_replace_blocks=file_writing_args.text_or_search_replace_blocks,
                ),
                coding_max_tokens,
                noncoding_max_tokens,
                context,
            )
            return result, paths
  • Pydantic BaseModel defining the input schema for FileWriteOrEdit, including fields for file_path, percentage_to_change, text_or_search_replace_blocks, and thread_id.
    class FileWriteOrEdit(BaseModel):
        # Naming should be in sorted order otherwise it gets changed in LLM backend.
        file_path: str = Field(description="#1: absolute file path")
        percentage_to_change: int = Field(
            description="#2: predict this percentage, calculated as number of existing lines that will have some diff divided by total existing lines."
        )
        text_or_search_replace_blocks: str = Field(
            description="#3: content/edit blocks. Must be after #2 in the tool xml"
        )
        thread_id: str = Field(description="#4: thread_id")
    
        def model_post_init(self, __context: Any) -> None:
            self.thread_id = normalize_thread_id(self.thread_id)
            return super().model_post_init(__context)
  • MCP tool registration for FileWriteOrEdit, providing the JSON schema, name, detailed description, and annotations indicating it's destructive but idempotent.
            inputSchema=remove_titles_from_schema(FileWriteOrEdit.model_json_schema()),
            name="FileWriteOrEdit",
            description="""
    - Writes or edits a file based on the percentage of changes.
    - Use absolute path only (~ allowed).
    - First write down percentage of lines that need to be replaced in the file (between 0-100) in percentage_to_change
    - percentage_to_change should be low if mostly new code is to be added. It should be high if a lot of things are to be replaced.
    - If percentage_to_change > 50, provide full file content in text_or_search_replace_blocks
    - If percentage_to_change <= 50, text_or_search_replace_blocks should be search/replace blocks.
    """
            + diffinstructions,
            annotations=ToolAnnotations(
                destructiveHint=True, idempotentHint=True, openWorldHint=False
            ),
        ),
  • Dispatch logic within get_tool_output that invokes the file_writing handler specifically for FileWriteOrEdit tool calls and handles the output and file tracking.
    elif isinstance(arg, FileWriteOrEdit):
        context.console.print("Calling file writing tool")
    
        result, write_edit_paths = file_writing(
            arg, coding_max_tokens, noncoding_max_tokens, context
        )
        output = result, 0.0
        # Add write/edit paths with their ranges to our tracking dictionary
        for path, ranges in write_edit_paths.items():
            if path in file_paths_with_ranges:
                file_paths_with_ranges[path].extend(ranges)
            else:
                file_paths_with_ranges[path] = ranges.copy()
  • MCP server handler for all tool calls, including FileWriteOrEdit. Uses which_tool_name and parse_tool_by_name to instantiate the tool and delegates to get_tool_output.
    async def handle_call_tool(
        name: str, arguments: dict[str, Any] | None
    ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
        global BASH_STATE
        if not arguments:
            raise ValueError("Missing arguments")
    
        tool_type = which_tool_name(name)
        tool_call = parse_tool_by_name(name, arguments)
    
        try:
            assert BASH_STATE
            output_or_dones, _ = get_tool_output(
                Context(BASH_STATE, BASH_STATE.console),
                tool_call,
                default_enc,
                0.0,
                lambda x, y: ("", 0),
                24000,  # coding_max_tokens
                8000,  # noncoding_max_tokens
            )
    
        except Exception as e:
            output_or_dones = [f"GOT EXCEPTION while calling tool. Error: {e}"]
    
        content: list[types.TextContent | types.ImageContent | types.EmbeddedResource] = []
        for output_or_done in output_or_dones:
            if isinstance(output_or_done, str):
                if issubclass(tool_type, Initialize):
                    # Prepare the original hardcoded message
                    original_message = """
    - Additional important note: as soon as you encounter "The user has chosen to disallow the tool call.", immediately stop doing everything and ask user for the reason.
    
    Initialize call done.
        """
    
                    # If custom instructions exist, prepend them to the original message
                    if CUSTOM_INSTRUCTIONS:
                        output_or_done += f"\n{CUSTOM_INSTRUCTIONS}\n{original_message}"
                    else:
                        output_or_done += original_message
    
                content.append(types.TextContent(type="text", text=output_or_done))
            else:
                content.append(
                    types.ImageContent(
                        type="image",
                        data=output_or_done.data,
                        mimeType=output_or_done.media_type,
                    )
                )
    
        return content
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations indicate destructiveHint=true and idempotentHint=true, which the description aligns with by describing file editing/writing. The description adds valuable behavioral context beyond annotations, such as the requirement for exact matching in SEARCH blocks, rules for block conciseness, and handling of changes based on percentage thresholds, enhancing the agent's understanding of how the tool behaves.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness2/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is overly verbose and poorly structured, with extensive examples and rules that could be condensed. It includes redundant details like full example code and repetitive block formatting instructions, making it less front-loaded and efficient. Sentences like 'Include just the changing lines...' are necessary but could be more concise.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity (destructive file editing) and lack of output schema, the description is fairly complete. It covers key behavioral aspects, parameter usage, and provides examples. However, it could improve by summarizing rules more succinctly and clarifying error handling or edge cases, keeping it at 4.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already documents all parameters. The description adds some meaning by explaining how to calculate percentage_to_change and the format for text_or_search_replace_blocks, but it doesn't provide additional syntax or format details beyond what the schema implies. This meets the baseline of 3 for high schema coverage.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool 'writes or edits a file based on the percentage of changes,' which is a specific verb+resource combination. It distinguishes from sibling tools like ReadFiles by focusing on modification rather than reading. However, it doesn't explicitly differentiate from other potential write tools (though none are present in siblings), keeping it at 4.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides implied usage through rules like 'Use absolute path only' and conditions for when to use full content vs. search/replace blocks based on percentage_to_change. It doesn't explicitly state when to use this tool vs. alternatives like BashCommand or ContextSave, nor does it mention exclusions or prerequisites, resulting in a 3.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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/rusiaaman/wcgw'

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