@@ -249,6 +249,103 @@ async def run_tool(self, arguments: Dict[str, Any]) -> Sequence[TextContent]:
249
249
raise RuntimeError (f"Error processing request: { str (e )} " ) from e
250
250
251
251
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
+
252
349
class CreateTextFileHandler :
253
350
"""Handler for creating a new text file."""
254
351
@@ -611,7 +708,6 @@ async def run_tool(self, arguments: Dict[str, Any]) -> Sequence[TextContent]:
611
708
line_number = line_number ,
612
709
encoding = encoding ,
613
710
)
614
-
615
711
return [TextContent (type = "text" , text = json .dumps (result , indent = 2 ))]
616
712
617
713
except Exception as e :
@@ -627,6 +723,7 @@ async def run_tool(self, arguments: Dict[str, Any]) -> Sequence[TextContent]:
627
723
append_file_handler = AppendTextFileContentsHandler ()
628
724
delete_contents_handler = DeleteTextFileContentsHandler ()
629
725
insert_file_handler = InsertTextFileContentsHandler ()
726
+ patch_file_handler = PatchTextFileContentsHandler ()
630
727
631
728
632
729
@app .list_tools ()
@@ -639,6 +736,7 @@ async def list_tools() -> List[Tool]:
639
736
append_file_handler .get_tool_description (),
640
737
delete_contents_handler .get_tool_description (),
641
738
insert_file_handler .get_tool_description (),
739
+ patch_file_handler .get_tool_description (),
642
740
]
643
741
644
742
@@ -659,6 +757,8 @@ async def call_tool(name: str, arguments: Any) -> Sequence[TextContent]:
659
757
return await delete_contents_handler .run_tool (arguments )
660
758
elif name == insert_file_handler .name :
661
759
return await insert_file_handler .run_tool (arguments )
760
+ elif name == patch_file_handler .name :
761
+ return await patch_file_handler .run_tool (arguments )
662
762
else :
663
763
raise ValueError (f"Unknown tool: { name } " )
664
764
except ValueError :
0 commit comments