Skip to content

Commit 6597fc4

Browse files
committed
Merge branch 'remove-file-hash-from-error' into develop
2 parents b16b6f4 + 8669c93 commit 6597fc4

12 files changed

+153
-96
lines changed

src/mcp_text_editor/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ async def get_text_file_contents(
2929
async def insert_text_file_contents(request: Dict[str, Any]) -> Dict[str, Any]:
3030
"""Insert text content before or after a specific line in a file."""
3131
return await _text_editor.insert_text_file_contents(
32-
file_path=request["path"],
32+
file_path=request["file_path"],
3333
file_hash=request["file_hash"],
3434
after=request.get("after"),
3535
before=request.get("before"),

src/mcp_text_editor/handlers/append_text_file_contents.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,14 @@ def get_tool_description(self) -> Tool:
5252
async def run_tool(self, arguments: Dict[str, Any]) -> Sequence[TextContent]:
5353
"""Execute the tool with given arguments."""
5454
try:
55-
if "path" not in arguments:
56-
raise RuntimeError("Missing required argument: path")
55+
if "file_path" not in arguments:
56+
raise RuntimeError("Missing required argument: file_path")
5757
if "contents" not in arguments:
5858
raise RuntimeError("Missing required argument: contents")
5959
if "file_hash" not in arguments:
6060
raise RuntimeError("Missing required argument: file_hash")
6161

62-
file_path = arguments["path"]
62+
file_path = arguments["file_path"]
6363
if not os.path.isabs(file_path):
6464
raise RuntimeError(f"File path must be absolute: {file_path}")
6565

src/mcp_text_editor/handlers/create_text_file.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def get_tool_description(self) -> Tool:
2929
inputSchema={
3030
"type": "object",
3131
"properties": {
32-
"path": {
32+
"file_path": {
3333
"type": "string",
3434
"description": "Path to the text file. File path must be absolute.",
3535
},
@@ -50,12 +50,12 @@ def get_tool_description(self) -> Tool:
5050
async def run_tool(self, arguments: Dict[str, Any]) -> Sequence[TextContent]:
5151
"""Execute the tool with given arguments."""
5252
try:
53-
if "path" not in arguments:
54-
raise RuntimeError("Missing required argument: path")
53+
if "file_path" not in arguments:
54+
raise RuntimeError("Missing required argument: file_path")
5555
if "contents" not in arguments:
5656
raise RuntimeError("Missing required argument: contents")
5757

58-
file_path = arguments["path"]
58+
file_path = arguments["file_path"]
5959
if not os.path.isabs(file_path):
6060
raise RuntimeError(f"File path must be absolute: {file_path}")
6161

src/mcp_text_editor/handlers/delete_text_file_contents.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class DeleteTextFileContentsHandler(BaseHandler):
1717
"""Handler for deleting content from a text file."""
1818

1919
name = "delete_text_file_contents"
20-
description = "Delete specified content ranges from a text file. The file must exist. File paths must be absolute."
20+
description = "Delete specified content ranges from a text file. The file must exist. File paths must be absolute. You need to provide the file_hash comes from get_text_file_contents."
2121

2222
def get_tool_description(self) -> Tool:
2323
"""Get the tool description."""

src/mcp_text_editor/handlers/insert_text_file_contents.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class InsertTextFileContentsHandler(BaseHandler):
1717
"""Handler for inserting content before or after a specific line in a text file."""
1818

1919
name = "insert_text_file_contents"
20-
description = "Insert content before or after a specific line in a text file. Uses hash-based validation for concurrency control."
20+
description = "Insert content before or after a specific line in a text file. Uses hash-based validation for concurrency control. You need to provide the file_hash comes from get_text_file_contents."
2121

2222
def get_tool_description(self) -> Tool:
2323
"""Get the tool description."""
@@ -27,7 +27,7 @@ def get_tool_description(self) -> Tool:
2727
inputSchema={
2828
"type": "object",
2929
"properties": {
30-
"path": {
30+
"file_path": {
3131
"type": "string",
3232
"description": "Path to the text file. File path must be absolute.",
3333
},
@@ -53,21 +53,21 @@ def get_tool_description(self) -> Tool:
5353
"default": "utf-8",
5454
},
5555
},
56-
"required": ["path", "file_hash", "contents"],
56+
"required": ["file_path", "file_hash", "contents"],
5757
},
5858
)
5959

6060
async def run_tool(self, arguments: Dict[str, Any]) -> Sequence[TextContent]:
6161
"""Execute the tool with given arguments."""
6262
try:
63-
if "path" not in arguments:
64-
raise RuntimeError("Missing required argument: path")
63+
if "file_path" not in arguments:
64+
raise RuntimeError("Missing required argument: file_path")
6565
if "file_hash" not in arguments:
6666
raise RuntimeError("Missing required argument: file_hash")
6767
if "contents" not in arguments:
6868
raise RuntimeError("Missing required argument: contents")
6969

70-
file_path = arguments["path"]
70+
file_path = arguments["file_path"]
7171
if not os.path.isabs(file_path):
7272
raise RuntimeError(f"File path must be absolute: {file_path}")
7373

src/mcp_text_editor/text_editor.py

+60-50
Original file line numberDiff line numberDiff line change
@@ -219,27 +219,18 @@ async def edit_file_contents(
219219
Args:
220220
file_path (str): Path to the file to edit
221221
expected_hash (str): Expected hash of the file before editing
222-
patches (List[EditPatch]): List of patches to apply
223-
- start (int): Starting line number (1-based, optional, default: 1)
224-
- end (Optional[int]): Ending line number (inclusive)
225-
- contents (str): New content to insert
226-
Edit file contents with hash-based conflict detection and multiple patches (supporting new file creation).
227-
228-
Args:
229-
file_path (str): Path to the file to edit (parent directories are created automatically)
230-
expected_hash (str): Expected hash of the file before editing (empty string for new files)
231222
patches (List[Dict[str, Any]]): List of patches to apply, each containing:
232223
- start (int): Starting line number (1-based)
233224
- end (Optional[int]): Ending line number (inclusive)
234-
- contents (str): New content to insert
225+
- contents (str): New content to insert (if empty string, consider using delete_text_file_contents instead)
235226
- range_hash (str): Expected hash of the content being replaced
236227
237228
Returns:
238229
Dict[str, Any]: Results of the operation containing:
239230
- result: "ok" or "error"
240231
- hash: New file hash if successful, None if error
241232
- reason: Error message if result is "error"
242-
"content": None,
233+
"file_hash": None,
243234
}
244235
245236
# Read current file content and verify hash
@@ -251,7 +242,6 @@ async def edit_file_contents(
251242
return {
252243
"result": "error",
253244
"reason": "File not found and non-empty hash provided",
254-
"content": None,
255245
}
256246
# Create parent directories if they don't exist
257247
parent_dir = os.path.dirname(file_path)
@@ -262,7 +252,6 @@ async def edit_file_contents(
262252
return {
263253
"result": "error",
264254
"reason": f"Failed to create directory: {str(e)}",
265-
"content": None,
266255
}
267256
# Initialize empty state for new file
268257
current_content = ""
@@ -271,9 +260,14 @@ async def edit_file_contents(
271260
encoding = "utf-8"
272261
else:
273262
# Read current file content and verify hash
274-
current_content, _, _, current_hash, total_lines, _ = (
275-
await self.read_file_contents(file_path, encoding=encoding)
276-
)
263+
(
264+
current_content,
265+
_,
266+
_,
267+
current_hash,
268+
total_lines,
269+
_,
270+
) = await self.read_file_contents(file_path, encoding=encoding)
277271

278272
# Treat empty file as new file
279273
if not current_content:
@@ -284,14 +278,11 @@ async def edit_file_contents(
284278
return {
285279
"result": "error",
286280
"reason": "Unexpected error - Cannot treat existing file as new",
287-
"file_hash": None,
288-
"content": None,
289281
}
290282
elif current_hash != expected_hash:
291283
return {
292284
"result": "error",
293285
"reason": "FileHash mismatch - Please use get_text_file_contents tool to get current content and hashes, then retry with the updated hashes.",
294-
"content": None,
295286
}
296287
else:
297288
lines = current_content.splitlines(keepends=True)
@@ -324,8 +315,6 @@ async def edit_file_contents(
324315
return {
325316
"result": "error",
326317
"reason": "Overlapping patches detected",
327-
"hash": None,
328-
"content": None,
329318
}
330319

331320
# Apply patches
@@ -355,12 +344,7 @@ async def edit_file_contents(
355344
and current_content
356345
and expected_hash == ""
357346
):
358-
return {
359-
"result": "error",
360-
"reason": "Unexpected error",
361-
"file_hash": None,
362-
"content": None,
363-
}
347+
return {"result": "error", "reason": "Unexpected error"}
364348

365349
# Calculate line ranges for zero-based indexing
366350
start_zero = start - 1
@@ -400,7 +384,6 @@ async def edit_file_contents(
400384
return {
401385
"result": "error",
402386
"reason": "Content range hash mismatch - Please use get_text_file_contents tool with the same start and end to get current content and hashes, then retry with the updated hashes.",
403-
"content": current_content,
404387
}
405388

406389
# Prepare new content
@@ -409,6 +392,30 @@ async def edit_file_contents(
409392
else:
410393
contents = patch["contents"]
411394

395+
# Check if this is a deletion (empty content)
396+
if not contents.strip():
397+
return {
398+
"result": "ok",
399+
"file_hash": current_hash, # Return current hash since no changes made
400+
"hint": "For content deletion, please consider using delete_text_file_contents instead of patch with empty content",
401+
"suggestion": "delete",
402+
}
403+
404+
# Set suggestions for alternative tools
405+
suggestion = None
406+
hint = None
407+
if not os.path.exists(file_path) or not current_content:
408+
suggestion = "append"
409+
hint = "For new or empty files, please consider using append_text_file_contents instead"
410+
elif is_insertion:
411+
if start_zero >= len(lines):
412+
suggestion = "append"
413+
hint = "For adding content at the end of file, please consider using append_text_file_contents instead"
414+
else:
415+
suggestion = "insert"
416+
hint = "For inserting content within file, please consider using insert_text_file_contents instead"
417+
418+
# Prepare the content
412419
new_content = contents if contents.endswith("\n") else contents + "\n"
413420
new_lines = new_content.splitlines(keepends=True)
414421

@@ -432,21 +439,14 @@ async def edit_file_contents(
432439
"result": "ok",
433440
"file_hash": new_hash,
434441
"reason": None,
442+
"suggestion": suggestion,
443+
"hint": hint,
435444
}
436445

437446
except FileNotFoundError:
438-
return {
439-
"result": "error",
440-
"reason": f"File not found: {file_path}",
441-
"file_hash": None,
442-
"content": None,
443-
}
447+
return {"result": "error", "reason": f"File not found: {file_path}"}
444448
except (IOError, UnicodeError, PermissionError) as e:
445-
return {
446-
"result": "error",
447-
"reason": f"Error editing file: {str(e)}",
448-
"content": None,
449-
}
449+
return {"result": "error", "reason": f"Error editing file: {str(e)}"}
450450
except Exception as e:
451451
import traceback
452452

@@ -455,7 +455,7 @@ async def edit_file_contents(
455455
return {
456456
"result": "error",
457457
"reason": "Unexpected error occurred",
458-
"content": None,
458+
"file_hash": None,
459459
}
460460

461461
async def insert_text_file_contents(
@@ -493,11 +493,16 @@ async def insert_text_file_contents(
493493
}
494494

495495
try:
496-
current_content, _, _, current_hash, total_lines, _ = (
497-
await self.read_file_contents(
498-
file_path,
499-
encoding=encoding,
500-
)
496+
(
497+
current_content,
498+
_,
499+
_,
500+
current_hash,
501+
total_lines,
502+
_,
503+
) = await self.read_file_contents(
504+
file_path,
505+
encoding=encoding,
501506
)
502507

503508
if current_hash != file_hash:
@@ -585,11 +590,16 @@ async def delete_text_file_contents(
585590
self._validate_file_path(request.file_path)
586591

587592
try:
588-
current_content, _, _, current_hash, total_lines, _ = (
589-
await self.read_file_contents(
590-
request.file_path,
591-
encoding=request.encoding or "utf-8",
592-
)
593+
(
594+
current_content,
595+
_,
596+
_,
597+
current_hash,
598+
total_lines,
599+
_,
600+
) = await self.read_file_contents(
601+
request.file_path,
602+
encoding=request.encoding or "utf-8",
593603
)
594604

595605
# Check for conflicts

tests/test_append_text_file.py

+8-8
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ async def test_append_text_file_success(test_dir: str, cleanup_files: None) -> N
4242

4343
# Append content using handler
4444
arguments: Dict[str, Any] = {
45-
"path": test_file,
45+
"file_path": test_file,
4646
"contents": append_content,
4747
"file_hash": file_hash,
4848
}
@@ -66,7 +66,7 @@ async def test_append_text_file_not_exists(test_dir: str, cleanup_files: None) -
6666

6767
# Try to append to non-existent file
6868
arguments: Dict[str, Any] = {
69-
"path": test_file,
69+
"file_path": test_file,
7070
"contents": "Some content\n",
7171
"file_hash": "dummy_hash",
7272
}
@@ -91,7 +91,7 @@ async def test_append_text_file_hash_mismatch(
9191

9292
# Try to append with incorrect hash
9393
arguments: Dict[str, Any] = {
94-
"path": test_file,
94+
"file_path": test_file,
9595
"contents": "New content\n",
9696
"file_hash": "incorrect_hash",
9797
}
@@ -108,7 +108,7 @@ async def test_append_text_file_relative_path(
108108
) -> None:
109109
"""Test attempting to append using a relative path."""
110110
arguments: Dict[str, Any] = {
111-
"path": "relative_path.txt",
111+
"file_path": "relative_path.txt",
112112
"contents": "Some content\n",
113113
"file_hash": "dummy_hash",
114114
}
@@ -125,19 +125,19 @@ async def test_append_text_file_missing_args() -> None:
125125
# Test missing path
126126
with pytest.raises(RuntimeError) as exc_info:
127127
await append_handler.run_tool({"contents": "content\n", "file_hash": "hash"})
128-
assert "Missing required argument: path" in str(exc_info.value)
128+
assert "Missing required argument: file_path" in str(exc_info.value)
129129

130130
# Test missing contents
131131
with pytest.raises(RuntimeError) as exc_info:
132132
await append_handler.run_tool(
133-
{"path": "/absolute/path.txt", "file_hash": "hash"}
133+
{"file_path": "/absolute/path.txt", "file_hash": "hash"}
134134
)
135135
assert "Missing required argument: contents" in str(exc_info.value)
136136

137137
# Test missing file_hash
138138
with pytest.raises(RuntimeError) as exc_info:
139139
await append_handler.run_tool(
140-
{"path": "/absolute/path.txt", "contents": "content\n"}
140+
{"file_path": "/absolute/path.txt", "contents": "content\n"}
141141
)
142142
assert "Missing required argument: file_hash" in str(exc_info.value)
143143

@@ -163,7 +163,7 @@ async def test_append_text_file_custom_encoding(
163163

164164
# Append content using handler with specified encoding
165165
arguments: Dict[str, Any] = {
166-
"path": test_file,
166+
"file_path": test_file,
167167
"contents": append_content,
168168
"file_hash": file_hash,
169169
"encoding": "utf-8",

0 commit comments

Comments
 (0)