Skip to content

Commit d79c3ec

Browse files
committed
Merge branch 'develop'
2 parents 176c1c8 + eb77392 commit d79c3ec

6 files changed

+448
-78
lines changed

CHANGELOG.md

+29
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,34 @@
11
# Changelog
22

3+
## [1.1.0] - 2024-12-23
4+
5+
### Added
6+
7+
- New text file manipulation operations:
8+
- `insert_text_file_contents`: Insert content at specific positions
9+
- `create_text_file`: Create new text files
10+
- `append_text_file_contents`: Append content to existing files
11+
- `delete_text_file_contents`: Delete specified ranges of text
12+
- `patch_text_file_contents`: Apply multiple patches to text files
13+
- Enhanced error messages with useful suggestions for alternative editing methods
14+
15+
### Changed
16+
17+
- Unified parameter naming: renamed 'path' to 'file_path' across all APIs
18+
- Improved handler organization by moving them to separate directory
19+
- Made 'end' parameter required when not in append mode
20+
- Enhanced validation for required parameters and file path checks
21+
- Removed 'edit_text_file_contents' tool in favor of more specific operations
22+
- Improved JSON serialization for handler responses
23+
24+
### Fixed
25+
26+
- Delete operation now uses dedicated deletion method instead of empty content replacement
27+
- Improved range validation in delete operations
28+
- Enhanced error handling across all operations
29+
- Removed file hash from error responses for better clarity
30+
- Fixed concurrency control with proper hash validation
31+
332
## [1.0.2] - 2024-12-22
433

534
### Fixed

src/mcp_text_editor/text_editor.py

+92-64
Original file line numberDiff line numberDiff line change
@@ -11,32 +11,6 @@
1111
logger = logging.getLogger(__name__)
1212

1313

14-
def _create_error_response(
15-
error_message: str,
16-
content_hash: Optional[str] = None,
17-
file_path: Optional[str] = None,
18-
) -> Dict[str, Any]:
19-
"""Create a standardized error response.
20-
21-
Args:
22-
error_message (str): The error message to include
23-
content_hash (Optional[str], optional): Hash of the current content if available
24-
file_path (Optional[str], optional): File path to use as dictionary key
25-
26-
Returns:
27-
Dict[str, Any]: Standardized error response structure
28-
"""
29-
error_response = {
30-
"result": "error",
31-
"reason": error_message,
32-
"hash": content_hash,
33-
}
34-
35-
if file_path:
36-
return {file_path: error_response}
37-
return error_response
38-
39-
4014
class TextEditor:
4115
"""Handles text file operations with security checks and conflict detection."""
4216

@@ -45,6 +19,44 @@ def __init__(self):
4519
self._validate_environment()
4620
self.service = TextEditorService()
4721

22+
def create_error_response(
23+
self,
24+
error_message: str,
25+
content_hash: Optional[str] = None,
26+
file_path: Optional[str] = None,
27+
suggestion: Optional[str] = None,
28+
hint: Optional[str] = None,
29+
) -> Dict[str, Any]:
30+
"""Create a standardized error response.
31+
32+
Args:
33+
error_message (str): The error message to include
34+
content_hash (Optional[str], optional): Hash of the current content if available
35+
file_path (Optional[str], optional): File path to use as dictionary key
36+
suggestion (Optional[str], optional): Suggested operation type
37+
hint (Optional[str], optional): Hint message for users
38+
39+
Returns:
40+
Dict[str, Any]: Standardized error response structure
41+
"""
42+
error_response = {
43+
"result": "error",
44+
"reason": error_message,
45+
"file_hash": content_hash,
46+
}
47+
48+
# Add fields if provided
49+
if content_hash is not None:
50+
error_response["file_hash"] = content_hash
51+
if suggestion:
52+
error_response["suggestion"] = suggestion
53+
if hint:
54+
error_response["hint"] = hint
55+
56+
if file_path:
57+
return {file_path: error_response}
58+
return error_response
59+
4860
def _validate_environment(self) -> None:
4961
"""
5062
Validate environment variables and setup.
@@ -241,20 +253,22 @@ async def edit_file_contents(
241253
try:
242254
if not os.path.exists(file_path):
243255
if expected_hash not in ["", None]: # Allow null hash
244-
return {
245-
"result": "error",
246-
"reason": "File not found and non-empty hash provided",
247-
}
256+
return self.create_error_response(
257+
"File not found and non-empty hash provided",
258+
suggestion="append",
259+
hint="For new files, please consider using append_text_file_contents",
260+
)
248261
# Create parent directories if they don't exist
249262
parent_dir = os.path.dirname(file_path)
250263
if parent_dir:
251264
try:
252265
os.makedirs(parent_dir, exist_ok=True)
253266
except OSError as e:
254-
return {
255-
"result": "error",
256-
"reason": f"Failed to create directory: {str(e)}",
257-
}
267+
return self.create_error_response(
268+
f"Failed to create directory: {str(e)}",
269+
suggestion="patch",
270+
hint="Please check file permissions and try again",
271+
)
258272
# Initialize empty state for new file
259273
current_content = ""
260274
current_hash = ""
@@ -277,17 +291,21 @@ async def edit_file_contents(
277291
current_hash = ""
278292
lines = []
279293
elif current_content and expected_hash == "":
280-
return {
281-
"result": "error",
282-
"reason": "Unexpected error - Cannot treat existing file as new",
283-
}
294+
return self.create_error_response(
295+
"Unexpected error - Cannot treat existing file as new",
296+
)
284297
elif current_hash != expected_hash:
285-
return {
286-
"result": "error",
287-
"reason": "FileHash mismatch - Please use get_text_file_contents tool to get current content and hashes, then retry with the updated hashes.",
288-
}
298+
suggestion = "patch"
299+
hint = "Please use get_text_file_contents tool to get the current content and hash"
300+
301+
return self.create_error_response(
302+
"FileHash mismatch - Please use get_text_file_contents tool to get current content and hashes, then retry with the updated hashes.",
303+
suggestion=suggestion,
304+
hint=hint,
305+
)
289306
else:
290307
lines = current_content.splitlines(keepends=True)
308+
lines = current_content.splitlines(keepends=True)
291309

292310
# Convert patches to EditPatch objects
293311
patch_objects = [EditPatch.model_validate(p) for p in patches]
@@ -314,10 +332,11 @@ async def edit_file_contents(
314332
if (start1 <= end2 and end1 >= start2) or (
315333
start2 <= end1 and end2 >= start1
316334
):
317-
return {
318-
"result": "error",
319-
"reason": "Overlapping patches detected",
320-
}
335+
return self.create_error_response(
336+
"Overlapping patches detected",
337+
suggestion="patch",
338+
hint="Please ensure your patches do not overlap",
339+
)
321340

322341
# Apply patches
323342
for patch in sorted_patches:
@@ -364,7 +383,6 @@ async def edit_file_contents(
364383
# New file or empty file - treat as insertion
365384
is_insertion = True
366385
elif start_zero >= len(lines):
367-
# Append mode - start exceeds total lines
368386
is_insertion = True
369387
else:
370388
# For modification mode, check the range_hash
@@ -386,6 +404,8 @@ async def edit_file_contents(
386404
return {
387405
"result": "error",
388406
"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.",
407+
"suggestion": "get",
408+
"hint": "Please run get_text_file_contents first to get current content and hashes",
389409
}
390410

391411
# Prepare new content
@@ -404,18 +424,18 @@ async def edit_file_contents(
404424
}
405425

406426
# Set suggestions for alternative tools
407-
suggestion = None
408-
hint = None
427+
suggestion_text: Optional[str] = None
428+
hint_text: Optional[str] = None
409429
if not os.path.exists(file_path) or not current_content:
410-
suggestion = "append"
411-
hint = "For new or empty files, please consider using append_text_file_contents instead"
430+
suggestion_text = "append"
431+
hint_text = "For new or empty files, please consider using append_text_file_contents instead"
412432
elif is_insertion:
413433
if start_zero >= len(lines):
414-
suggestion = "append"
415-
hint = "For adding content at the end of file, please consider using append_text_file_contents instead"
434+
suggestion_text = "append"
435+
hint_text = "For adding content at the end of file, please consider using append_text_file_contents instead"
416436
else:
417-
suggestion = "insert"
418-
hint = "For inserting content within file, please consider using insert_text_file_contents instead"
437+
suggestion_text = "insert"
438+
hint_text = "For inserting content within file, please consider using insert_text_file_contents instead"
419439

420440
# Prepare the content
421441
new_content = contents if contents.endswith("\n") else contents + "\n"
@@ -441,24 +461,32 @@ async def edit_file_contents(
441461
"result": "ok",
442462
"file_hash": new_hash,
443463
"reason": None,
444-
"suggestion": suggestion,
445-
"hint": hint,
464+
"suggestion": suggestion_text,
465+
"hint": hint_text,
446466
}
447467

448468
except FileNotFoundError:
449-
return {"result": "error", "reason": f"File not found: {file_path}"}
469+
return self.create_error_response(
470+
f"File not found: {file_path}",
471+
suggestion="append",
472+
hint="For new files, please use append_text_file_contents",
473+
)
450474
except (IOError, UnicodeError, PermissionError) as e:
451-
return {"result": "error", "reason": f"Error editing file: {str(e)}"}
475+
return self.create_error_response(
476+
f"Error editing file: {str(e)}",
477+
suggestion="patch",
478+
hint="Please check file permissions and try again",
479+
)
452480
except Exception as e:
453481
import traceback
454482

455483
logger.error(f"Error: {str(e)}")
456484
logger.error(f"Traceback:\n{traceback.format_exc()}")
457-
return {
458-
"result": "error",
459-
"reason": "Unexpected error occurred",
460-
"file_hash": None,
461-
}
485+
return self.create_error_response(
486+
"Unexpected error occurred",
487+
suggestion="patch",
488+
hint="Please try again or report the issue if it persists",
489+
)
462490

463491
async def insert_text_file_contents(
464492
self,

tests/test_create_error_response.py

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"""Tests for error response creation and hint/suggestion functionality."""
2+
3+
import pytest
4+
5+
from mcp_text_editor.text_editor import TextEditor
6+
7+
8+
@pytest.fixture
9+
def editor():
10+
"""Create TextEditor instance."""
11+
return TextEditor()
12+
13+
14+
def test_create_error_response_basic(editor):
15+
"""Test basic error response without hint/suggestion."""
16+
response = editor.create_error_response("Test error")
17+
assert response["result"] == "error"
18+
assert response["reason"] == "Test error"
19+
assert response["file_hash"] is None
20+
assert "hint" not in response
21+
assert "suggestion" not in response
22+
23+
24+
def test_create_error_response_with_hint_suggestion(editor):
25+
"""Test error response with hint and suggestion."""
26+
response = editor.create_error_response(
27+
"Test error", suggestion="append", hint="Please use append_text_file_contents"
28+
)
29+
assert response["result"] == "error"
30+
assert response["reason"] == "Test error"
31+
assert response["suggestion"] == "append"
32+
assert response["hint"] == "Please use append_text_file_contents"
33+
34+
35+
def test_create_error_response_with_file_path(editor):
36+
"""Test error response with file path."""
37+
response = editor.create_error_response(
38+
"Test error",
39+
file_path="/test/file.txt",
40+
suggestion="patch",
41+
hint="Please try again",
42+
)
43+
assert "/test/file.txt" in response
44+
assert response["/test/file.txt"]["result"] == "error"
45+
assert response["/test/file.txt"]["reason"] == "Test error"
46+
assert response["/test/file.txt"]["suggestion"] == "patch"
47+
assert response["/test/file.txt"]["hint"] == "Please try again"
48+
49+
50+
def test_create_error_response_with_hash(editor):
51+
"""Test error response with content hash."""
52+
test_hash = "test_hash_value"
53+
response = editor.create_error_response("Test error", content_hash=test_hash)
54+
assert response["result"] == "error"
55+
assert response["reason"] == "Test error"
56+
assert response["file_hash"] == test_hash

0 commit comments

Comments
 (0)