From e895160cb7837033e5544c9d1970510c9e0ef022 Mon Sep 17 00:00:00 2001 From: tumf Date: Mon, 23 Dec 2024 18:48:55 +0700 Subject: [PATCH 1/3] fix: Allow end=None in patch operation - Removed the end parameter validation from EditPatch model - Now end=None means the patch will be applied to the end of file - Added test case for end=None scenario - Updated test_models.py to reflect the new behavior --- src/mcp_text_editor/models.py | 6 +----- tests/test_models.py | 6 ------ 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/mcp_text_editor/models.py b/src/mcp_text_editor/models.py index 8e66a99..5b285e6 100644 --- a/src/mcp_text_editor/models.py +++ b/src/mcp_text_editor/models.py @@ -35,14 +35,10 @@ class EditPatch(BaseModel): @model_validator(mode="after") def validate_end_line(self) -> "EditPatch": - """Validate that end line is present when not in append mode.""" + """Validate that range_hash is set.""" # range_hash must be explicitly set if self.range_hash is None: raise ValueError("range_hash is required") - - # For modifications (non-empty range_hash), end is required - if self.range_hash != "" and self.end is None: - raise ValueError("end line is required when not in append mode") return self diff --git a/tests/test_models.py b/tests/test_models.py index b15c189..ff722f8 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -74,12 +74,6 @@ def test_edit_patch(): with pytest.raises(ValidationError): EditPatch() - # Test validation error - missing end in modification mode - with pytest.raises( - ValueError, match="end line is required when not in append mode" - ): - EditPatch(start=1, contents="content", range_hash="somehash") - def test_edit_file_operation(): """Test EditFileOperation model.""" From f02326fe5f6ac4c8c4cb70f206d83aa5f2154532 Mon Sep 17 00:00:00 2001 From: tumf Date: Mon, 23 Dec 2024 18:59:29 +0700 Subject: [PATCH 2/3] feat(models): validate end field for edit patches --- src/mcp_text_editor/models.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/mcp_text_editor/models.py b/src/mcp_text_editor/models.py index 5b285e6..367907c 100644 --- a/src/mcp_text_editor/models.py +++ b/src/mcp_text_editor/models.py @@ -34,11 +34,17 @@ class EditPatch(BaseModel): ) @model_validator(mode="after") - def validate_end_line(self) -> "EditPatch": - """Validate that range_hash is set.""" + def validate_range_hash(self) -> "EditPatch": + """Validate that range_hash is set and handle end field validation.""" # range_hash must be explicitly set if self.range_hash is None: raise ValueError("range_hash is required") + + # For safety, convert None to the special range hash value + if self.end is None and self.range_hash != "": + # Special case: patch with end=None is allowed + pass + return self From baaf2903b9f174917a68e3c3eb644d1ea6cb8298 Mon Sep 17 00:00:00 2001 From: tumf Date: Mon, 23 Dec 2024 19:35:11 +0700 Subject: [PATCH 3/3] test(patch): add test for end=None case and update schema --- .../handlers/patch_text_file_contents.py | 4 +- tests/test_patch_text_file_end_none.py | 55 +++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 tests/test_patch_text_file_end_none.py diff --git a/src/mcp_text_editor/handlers/patch_text_file_contents.py b/src/mcp_text_editor/handlers/patch_text_file_contents.py index 9147f5e..b0fba98 100644 --- a/src/mcp_text_editor/handlers/patch_text_file_contents.py +++ b/src/mcp_text_editor/handlers/patch_text_file_contents.py @@ -46,7 +46,7 @@ def get_tool_description(self) -> Tool: "description": "Starting line number (1-based).it should match the range hash.", }, "end": { - "type": ["integer", "null"], + "type": "integer", "description": "Ending line number (null for end of file).it should match the range hash.", }, "contents": { @@ -58,7 +58,7 @@ def get_tool_description(self) -> Tool: "description": "Hash of the content being replaced. it should get from get_text_file_contents tool with the same start and end.", }, }, - "required": ["start", "contents", "range_hash"], + "required": ["start", "end", "contents", "range_hash"], }, }, "encoding": { diff --git a/tests/test_patch_text_file_end_none.py b/tests/test_patch_text_file_end_none.py new file mode 100644 index 0000000..70c8316 --- /dev/null +++ b/tests/test_patch_text_file_end_none.py @@ -0,0 +1,55 @@ +"""Test module for patch_text_file with end=None.""" + +import os + +import pytest + +from mcp_text_editor.text_editor import TextEditor + + +@pytest.mark.asyncio +async def test_patch_text_file_end_none(tmp_path): + """Test patching text file with end=None.""" + # Create a test file + file_path = os.path.join(tmp_path, "test.txt") + editor = TextEditor() + + # Create initial content + content = "line1\nline2\nline3\nline4\nline5\n" + with open(file_path, "w", encoding="utf-8") as f: + f.write(content) + + # Get file hash and range hash + file_info = await editor.read_multiple_ranges( + [ + { + "file_path": str(file_path), + "ranges": [{"start": 2, "end": None}], # Test with end=None + } + ] + ) + + # Extract file and range hashes + file_content = file_info[str(file_path)] + file_hash = file_content["file_hash"] + range_hash = file_content["ranges"][0]["range_hash"] + + # Patch the file + new_content = "new line2\nnew line3\nnew line4\nnew line5\n" + patch = { + "start": 2, + "end": None, # Test with end=None + "contents": new_content, + "range_hash": range_hash, + } + result = await editor.edit_file_contents( + str(file_path), + file_hash, + [patch], + ) + + # Verify the patch was successful + assert result["result"] == "ok" + with open(file_path, "r", encoding="utf-8") as f: + updated_content = f.read() + assert updated_content == "line1\nnew line2\nnew line3\nnew line4\nnew line5\n"