Skip to content

Commit 79770fa

Browse files
committed
feat(server.py): add PatchTextFileContentsHandler for applying patches to text files with hash-based validation
test(server.py): update test_list_tools to reflect the addition of the PatchTextFileContentsHandler
1 parent a9294be commit 79770fa

File tree

2 files changed

+102
-2
lines changed

2 files changed

+102
-2
lines changed

src/mcp_text_editor/server.py

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,103 @@ async def run_tool(self, arguments: Dict[str, Any]) -> Sequence[TextContent]:
249249
raise RuntimeError(f"Error processing request: {str(e)}") from e
250250

251251

252+
class PatchTextFileContentsHandler:
253+
"""Handler for patching a text file."""
254+
255+
name = "patch_text_file_contents"
256+
description = "Apply patches to text files with hash-based validation for concurrency control."
257+
258+
def __init__(self):
259+
self.editor = TextEditor()
260+
261+
def get_tool_description(self) -> Tool:
262+
"""Get the tool description."""
263+
return Tool(
264+
name=self.name,
265+
description=self.description,
266+
inputSchema={
267+
"type": "object",
268+
"properties": {
269+
"file_path": {
270+
"type": "string",
271+
"description": "Path to the text file. File path must be absolute.",
272+
},
273+
"file_hash": {
274+
"type": "string",
275+
"description": "Hash of the file contents for concurrency control.",
276+
},
277+
"patches": {
278+
"type": "array",
279+
"description": "List of patches to apply",
280+
"items": {
281+
"type": "object",
282+
"properties": {
283+
"start": {
284+
"type": "integer",
285+
"description": "Starting line number (1-based)",
286+
},
287+
"end": {
288+
"type": ["integer", "null"],
289+
"description": "Ending line number (null for end of file)",
290+
},
291+
"contents": {
292+
"type": "string",
293+
"description": "New content to replace the range with",
294+
},
295+
"range_hash": {
296+
"type": "string",
297+
"description": "Hash of the content being replaced",
298+
},
299+
},
300+
"required": ["start", "contents", "range_hash"],
301+
},
302+
},
303+
"encoding": {
304+
"type": "string",
305+
"description": "Text encoding (default: 'utf-8')",
306+
"default": "utf-8",
307+
},
308+
},
309+
"required": ["file_path", "file_hash", "patches"],
310+
},
311+
)
312+
313+
async def run_tool(self, arguments: Dict[str, Any]) -> Sequence[TextContent]:
314+
"""Execute the tool with given arguments."""
315+
try:
316+
if "file_path" not in arguments:
317+
raise RuntimeError("Missing required argument: file_path")
318+
if "file_hash" not in arguments:
319+
raise RuntimeError("Missing required argument: file_hash")
320+
if "patches" not in arguments:
321+
raise RuntimeError("Missing required argument: patches")
322+
323+
file_path = arguments["file_path"]
324+
if not os.path.isabs(file_path):
325+
raise RuntimeError(f"File path must be absolute: {file_path}")
326+
327+
# Check if file exists
328+
if not os.path.exists(file_path):
329+
raise RuntimeError(f"File does not exist: {file_path}")
330+
331+
encoding = arguments.get("encoding", "utf-8")
332+
333+
# Apply patches using editor.edit_file_contents
334+
result = await self.editor.edit_file_contents(
335+
file_path=file_path,
336+
expected_hash=arguments["file_hash"],
337+
patches=arguments["patches"],
338+
encoding=encoding,
339+
)
340+
341+
return [TextContent(type="text", text=json.dumps(result, indent=2))]
342+
343+
except Exception as e:
344+
logger.error(f"Error processing request: {str(e)}")
345+
logger.error(traceback.format_exc())
346+
raise RuntimeError(f"Error processing request: {str(e)}") from e
347+
348+
252349
class CreateTextFileHandler:
253350
"""Handler for creating a new text file."""
254351

@@ -611,7 +708,6 @@ async def run_tool(self, arguments: Dict[str, Any]) -> Sequence[TextContent]:
611708
line_number=line_number,
612709
encoding=encoding,
613710
)
614-
615711
return [TextContent(type="text", text=json.dumps(result, indent=2))]
616712

617713
except Exception as e:
@@ -627,6 +723,7 @@ async def run_tool(self, arguments: Dict[str, Any]) -> Sequence[TextContent]:
627723
append_file_handler = AppendTextFileContentsHandler()
628724
delete_contents_handler = DeleteTextFileContentsHandler()
629725
insert_file_handler = InsertTextFileContentsHandler()
726+
patch_file_handler = PatchTextFileContentsHandler()
630727

631728

632729
@app.list_tools()
@@ -639,6 +736,7 @@ async def list_tools() -> List[Tool]:
639736
append_file_handler.get_tool_description(),
640737
delete_contents_handler.get_tool_description(),
641738
insert_file_handler.get_tool_description(),
739+
patch_file_handler.get_tool_description(),
642740
]
643741

644742

@@ -659,6 +757,8 @@ async def call_tool(name: str, arguments: Any) -> Sequence[TextContent]:
659757
return await delete_contents_handler.run_tool(arguments)
660758
elif name == insert_file_handler.name:
661759
return await insert_file_handler.run_tool(arguments)
760+
elif name == patch_file_handler.name:
761+
return await patch_file_handler.run_tool(arguments)
662762
else:
663763
raise ValueError(f"Unknown tool: {name}")
664764
except ValueError:

tests/test_server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
async def test_list_tools():
2626
"""Test tool listing."""
2727
tools: List[Tool] = await list_tools()
28-
assert len(tools) == 6
28+
assert len(tools) == 7
2929

3030
# Verify GetTextFileContents tool
3131
get_contents_tool = next(

0 commit comments

Comments
 (0)