Skip to content

Commit bfd3d41

Browse files
committed
Merge branch 'feature/error-handling'
1 parent 37bee30 commit bfd3d41

8 files changed

+747
-183
lines changed

src/mcp_text_editor/errors.py

+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
"""Error handling for MCP Text Editor."""
2+
from dataclasses import dataclass
3+
from enum import Enum
4+
from typing import Any, Dict, Optional
5+
6+
7+
class ErrorCode(str, Enum):
8+
"""Error codes for MCP Text Editor."""
9+
10+
# Protocol level errors (1000-1999)
11+
INVALID_REQUEST = "INVALID_REQUEST"
12+
INVALID_SCHEMA = "INVALID_SCHEMA"
13+
INVALID_FIELD = "INVALID_FIELD"
14+
15+
# File operation errors (2000-2999)
16+
FILE_NOT_FOUND = "FILE_NOT_FOUND"
17+
FILE_ALREADY_EXISTS = "FILE_ALREADY_EXISTS"
18+
FILE_ACCESS_DENIED = "FILE_ACCESS_DENIED"
19+
FILE_HASH_MISMATCH = "FILE_HASH_MISMATCH"
20+
21+
# Content operation errors (3000-3999)
22+
CONTENT_HASH_MISMATCH = "CONTENT_HASH_MISMATCH"
23+
INVALID_LINE_RANGE = "INVALID_LINE_RANGE"
24+
CONTENT_TOO_LARGE = "CONTENT_TOO_LARGE"
25+
26+
# Internal errors (9000-9999)
27+
INTERNAL_ERROR = "INTERNAL_ERROR"
28+
29+
30+
@dataclass
31+
class MCPError(Exception):
32+
"""Base exception class for MCP Text Editor."""
33+
34+
code: ErrorCode
35+
message: str
36+
details: Optional[Dict[str, Any]] = None
37+
38+
def to_dict(self) -> Dict[str, Any]:
39+
"""Convert error to dictionary format for JSON response."""
40+
error_dict = {
41+
"error": {
42+
"code": self.code,
43+
"message": self.message,
44+
}
45+
}
46+
if self.details:
47+
error_dict["error"]["details"] = self.details
48+
return error_dict
49+
50+
51+
class ValidationError(MCPError):
52+
"""Raised when input validation fails."""
53+
54+
def __init__(
55+
self,
56+
message: str,
57+
details: Optional[Dict[str, Any]] = None,
58+
code: ErrorCode = ErrorCode.INVALID_SCHEMA,
59+
):
60+
super().__init__(code=code, message=message, details=details)
61+
62+
63+
class FileOperationError(MCPError):
64+
"""Raised when file operations fail."""
65+
66+
def __init__(
67+
self,
68+
message: str,
69+
details: Optional[Dict[str, Any]] = None,
70+
code: ErrorCode = ErrorCode.FILE_NOT_FOUND,
71+
):
72+
super().__init__(code=code, message=message, details=details)
73+
74+
75+
class ContentOperationError(MCPError):
76+
"""Raised when content operations fail."""
77+
78+
def __init__(
79+
self,
80+
message: str,
81+
details: Optional[Dict[str, Any]] = None,
82+
code: ErrorCode = ErrorCode.CONTENT_HASH_MISMATCH,
83+
):
84+
super().__init__(code=code, message=message, details=details)
85+
86+
87+
class InternalError(MCPError):
88+
"""Raised when internal errors occur."""
89+
90+
def __init__(
91+
self,
92+
message: str = "An internal error occurred",
93+
details: Optional[Dict[str, Any]] = None,
94+
):
95+
super().__init__(
96+
code=ErrorCode.INTERNAL_ERROR,
97+
message=message,
98+
details=details,
99+
)

src/mcp_text_editor/handlers/base.py

+69-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,36 @@
11
"""Base handler for MCP Text Editor."""
22

3-
from typing import Any, Dict, Sequence
3+
from functools import wraps
4+
from typing import Any, Dict, Sequence, TypeVar
45

56
from mcp.types import TextContent, Tool
67

8+
from ..errors import MCPError, InternalError, ValidationError
79
from ..text_editor import TextEditor
810

11+
T = TypeVar("T")
12+
13+
14+
def handle_errors(func):
15+
"""Decorator to handle errors in handler methods."""
16+
17+
@wraps(func)
18+
async def wrapper(*args, **kwargs) -> Sequence[TextContent]:
19+
try:
20+
return await func(*args, **kwargs)
21+
except MCPError as e:
22+
# Known application errors
23+
return [TextContent(text=str(e.to_dict()))]
24+
except Exception as e:
25+
# Unexpected errors
26+
internal_error = InternalError(
27+
message="An unexpected error occurred",
28+
details={"error": str(e), "type": e.__class__.__name__},
29+
)
30+
return [TextContent(text=str(internal_error.to_dict()))]
31+
32+
return wrapper
33+
934

1035
class BaseHandler:
1136
"""Base class for handlers."""
@@ -17,10 +42,52 @@ def __init__(self, editor: TextEditor | None = None):
1742
"""Initialize the handler."""
1843
self.editor = editor if editor is not None else TextEditor()
1944

45+
def validate_arguments(self, arguments: Dict[str, Any]) -> None:
46+
"""Validate the input arguments.
47+
48+
Args:
49+
arguments: The arguments to validate
50+
51+
Raises:
52+
ValidationError: If the arguments are invalid
53+
"""
54+
raise NotImplementedError
55+
2056
def get_tool_description(self) -> Tool:
2157
"""Get the tool description."""
2258
raise NotImplementedError
2359

60+
@handle_errors
2461
async def run_tool(self, arguments: Dict[str, Any]) -> Sequence[TextContent]:
25-
"""Execute the tool with given arguments."""
62+
"""Execute the tool with given arguments.
63+
64+
This method is decorated with handle_errors to provide consistent
65+
error handling across all handlers.
66+
67+
Args:
68+
arguments: The arguments for the tool
69+
70+
Returns:
71+
Sequence[TextContent]: The tool's output
72+
73+
Raises:
74+
MCPError: For known application errors
75+
Exception: For unexpected errors
76+
"""
77+
# Validate arguments before execution
78+
self.validate_arguments(arguments)
79+
return await self._execute_tool(arguments)
80+
81+
async def _execute_tool(self, arguments: Dict[str, Any]) -> Sequence[TextContent]:
82+
"""Internal method to execute the tool.
83+
84+
This should be implemented by each handler to provide the actual
85+
tool functionality.
86+
87+
Args:
88+
arguments: The validated arguments for the tool
89+
90+
Returns:
91+
Sequence[TextContent]: The tool's output
92+
"""
2693
raise NotImplementedError

src/mcp_text_editor/handlers/delete_text_file_contents.py

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

2020
name = "delete_text_file_contents"
21-
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."
21+
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." # noqa: E501
2222

2323
def get_tool_description(self) -> Tool:
2424
"""Get the tool description."""
@@ -34,7 +34,7 @@ def get_tool_description(self) -> Tool:
3434
},
3535
"file_hash": {
3636
"type": "string",
37-
"description": "Hash of the file contents for concurrency control. it should be matched with the file_hash when get_text_file_contents is called.",
37+
"description": "Hash of the file contents for concurrency control. it should be matched with the file_hash when get_text_file_contents is called.", # noqa: E501
3838
},
3939
"ranges": {
4040
"type": "array",
@@ -52,7 +52,7 @@ def get_tool_description(self) -> Tool:
5252
},
5353
"range_hash": {
5454
"type": "string",
55-
"description": "Hash of the content being deleted. it should be matched with the range_hash when get_text_file_contents is called with the same range.",
55+
"description": "Hash of the content being deleted. it should be matched with the range_hash when get_text_file_contents is called with the same range.", # noqa: E501
5656
},
5757
},
5858
"required": ["start", "range_hash"],

src/mcp_text_editor/handlers/insert_text_file_contents.py

+2-2
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. You need to provide the file_hash comes from get_text_file_contents."
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." # noqa: E501
2121

2222
def get_tool_description(self) -> Tool:
2323
"""Get the tool description."""
@@ -33,7 +33,7 @@ def get_tool_description(self) -> Tool:
3333
},
3434
"file_hash": {
3535
"type": "string",
36-
"description": "Hash of the file contents for concurrency control. it should be matched with the file_hash when get_text_file_contents is called.",
36+
"description": "Hash of the file contents for concurrency control. it should be matched with the file_hash when get_text_file_contents is called.", # noqa: E501
3737
},
3838
"contents": {
3939
"type": "string",

0 commit comments

Comments
 (0)