tree-sitter-analyzer 1.8.4__py3-none-any.whl → 1.9.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of tree-sitter-analyzer might be problematic. Click here for more details.

Files changed (64) hide show
  1. tree_sitter_analyzer/__init__.py +1 -1
  2. tree_sitter_analyzer/api.py +4 -4
  3. tree_sitter_analyzer/cli/argument_validator.py +29 -17
  4. tree_sitter_analyzer/cli/commands/advanced_command.py +7 -5
  5. tree_sitter_analyzer/cli/commands/structure_command.py +7 -5
  6. tree_sitter_analyzer/cli/commands/summary_command.py +10 -6
  7. tree_sitter_analyzer/cli/commands/table_command.py +8 -7
  8. tree_sitter_analyzer/cli/info_commands.py +1 -1
  9. tree_sitter_analyzer/cli_main.py +3 -2
  10. tree_sitter_analyzer/core/analysis_engine.py +5 -5
  11. tree_sitter_analyzer/core/cache_service.py +3 -1
  12. tree_sitter_analyzer/core/query.py +17 -5
  13. tree_sitter_analyzer/core/query_service.py +1 -1
  14. tree_sitter_analyzer/encoding_utils.py +3 -3
  15. tree_sitter_analyzer/exceptions.py +61 -50
  16. tree_sitter_analyzer/file_handler.py +3 -0
  17. tree_sitter_analyzer/formatters/base_formatter.py +10 -5
  18. tree_sitter_analyzer/formatters/formatter_registry.py +83 -68
  19. tree_sitter_analyzer/formatters/html_formatter.py +90 -64
  20. tree_sitter_analyzer/formatters/javascript_formatter.py +21 -16
  21. tree_sitter_analyzer/formatters/language_formatter_factory.py +7 -6
  22. tree_sitter_analyzer/formatters/markdown_formatter.py +247 -124
  23. tree_sitter_analyzer/formatters/python_formatter.py +61 -38
  24. tree_sitter_analyzer/formatters/typescript_formatter.py +113 -45
  25. tree_sitter_analyzer/interfaces/mcp_server.py +2 -2
  26. tree_sitter_analyzer/language_detector.py +6 -6
  27. tree_sitter_analyzer/language_loader.py +3 -1
  28. tree_sitter_analyzer/languages/css_plugin.py +120 -61
  29. tree_sitter_analyzer/languages/html_plugin.py +159 -62
  30. tree_sitter_analyzer/languages/java_plugin.py +42 -34
  31. tree_sitter_analyzer/languages/javascript_plugin.py +59 -30
  32. tree_sitter_analyzer/languages/markdown_plugin.py +402 -368
  33. tree_sitter_analyzer/languages/python_plugin.py +111 -64
  34. tree_sitter_analyzer/languages/typescript_plugin.py +241 -132
  35. tree_sitter_analyzer/mcp/server.py +22 -18
  36. tree_sitter_analyzer/mcp/tools/analyze_scale_tool.py +13 -8
  37. tree_sitter_analyzer/mcp/tools/base_tool.py +2 -2
  38. tree_sitter_analyzer/mcp/tools/fd_rg_utils.py +232 -26
  39. tree_sitter_analyzer/mcp/tools/find_and_grep_tool.py +31 -23
  40. tree_sitter_analyzer/mcp/tools/list_files_tool.py +21 -19
  41. tree_sitter_analyzer/mcp/tools/query_tool.py +17 -18
  42. tree_sitter_analyzer/mcp/tools/read_partial_tool.py +30 -31
  43. tree_sitter_analyzer/mcp/tools/search_content_tool.py +131 -77
  44. tree_sitter_analyzer/mcp/tools/table_format_tool.py +29 -16
  45. tree_sitter_analyzer/mcp/utils/file_output_factory.py +64 -51
  46. tree_sitter_analyzer/mcp/utils/file_output_manager.py +34 -24
  47. tree_sitter_analyzer/mcp/utils/gitignore_detector.py +8 -4
  48. tree_sitter_analyzer/models.py +7 -5
  49. tree_sitter_analyzer/plugins/base.py +9 -7
  50. tree_sitter_analyzer/plugins/manager.py +1 -0
  51. tree_sitter_analyzer/queries/css.py +2 -21
  52. tree_sitter_analyzer/queries/html.py +2 -15
  53. tree_sitter_analyzer/queries/markdown.py +30 -41
  54. tree_sitter_analyzer/queries/python.py +20 -5
  55. tree_sitter_analyzer/query_loader.py +5 -5
  56. tree_sitter_analyzer/security/validator.py +114 -86
  57. tree_sitter_analyzer/utils/__init__.py +58 -28
  58. tree_sitter_analyzer/utils/tree_sitter_compat.py +72 -65
  59. tree_sitter_analyzer/utils.py +26 -15
  60. {tree_sitter_analyzer-1.8.4.dist-info → tree_sitter_analyzer-1.9.1.dist-info}/METADATA +23 -6
  61. tree_sitter_analyzer-1.9.1.dist-info/RECORD +109 -0
  62. tree_sitter_analyzer-1.8.4.dist-info/RECORD +0 -109
  63. {tree_sitter_analyzer-1.8.4.dist-info → tree_sitter_analyzer-1.9.1.dist-info}/WHEEL +0 -0
  64. {tree_sitter_analyzer-1.8.4.dist-info → tree_sitter_analyzer-1.9.1.dist-info}/entry_points.txt +0 -0
@@ -257,9 +257,9 @@ class FindAndGrepTool(BaseMCPTool):
257
257
  "success": False,
258
258
  "error": f"Required commands not found: {', '.join(missing_commands)}. Please install fd (https://github.com/sharkdp/fd) and ripgrep (https://github.com/BurntSushi/ripgrep) to use this tool.",
259
259
  "count": 0,
260
- "results": []
260
+ "results": [],
261
261
  }
262
-
262
+
263
263
  self.validate_arguments(arguments)
264
264
  roots = self._validate_roots(arguments["roots"]) # absolute validated
265
265
 
@@ -459,7 +459,7 @@ class FindAndGrepTool(BaseMCPTool):
459
459
  else:
460
460
  # Parse full match details
461
461
  matches = fd_rg_utils.parse_rg_json_lines_to_matches(rg_out)
462
-
462
+
463
463
  # Apply user-specified max_count limit if provided
464
464
  # Note: ripgrep's -m option limits matches per file, not total matches
465
465
  # So we need to apply the total limit here in post-processing
@@ -503,25 +503,29 @@ class FindAndGrepTool(BaseMCPTool):
503
503
  try:
504
504
  # Save full result to file
505
505
  import json
506
- json_content = json.dumps(grouped_result, indent=2, ensure_ascii=False)
506
+
507
+ json_content = json.dumps(
508
+ grouped_result, indent=2, ensure_ascii=False
509
+ )
507
510
  file_path = self.file_output_manager.save_to_file(
508
- content=json_content,
509
- base_name=output_file
511
+ content=json_content, base_name=output_file
510
512
  )
511
-
513
+
512
514
  # If suppress_output is True, return minimal response
513
515
  if suppress_output:
514
516
  minimal_result = {
515
517
  "success": grouped_result.get("success", True),
516
518
  "count": grouped_result.get("count", 0),
517
519
  "output_file": output_file,
518
- "file_saved": f"Results saved to {file_path}"
520
+ "file_saved": f"Results saved to {file_path}",
519
521
  }
520
522
  return minimal_result
521
523
  else:
522
524
  # Include file info in full response
523
525
  grouped_result["output_file"] = output_file
524
- grouped_result["file_saved"] = f"Results saved to {file_path}"
526
+ grouped_result["file_saved"] = (
527
+ f"Results saved to {file_path}"
528
+ )
525
529
  except Exception as e:
526
530
  logger.error(f"Failed to save output to file: {e}")
527
531
  grouped_result["file_save_error"] = str(e)
@@ -532,7 +536,7 @@ class FindAndGrepTool(BaseMCPTool):
532
536
  "success": grouped_result.get("success", True),
533
537
  "count": grouped_result.get("count", 0),
534
538
  "summary": grouped_result.get("summary", {}),
535
- "meta": grouped_result.get("meta", {})
539
+ "meta": grouped_result.get("meta", {}),
536
540
  }
537
541
  return minimal_result
538
542
 
@@ -562,19 +566,19 @@ class FindAndGrepTool(BaseMCPTool):
562
566
  try:
563
567
  # Save full result to file
564
568
  import json
569
+
565
570
  json_content = json.dumps(result, indent=2, ensure_ascii=False)
566
571
  file_path = self.file_output_manager.save_to_file(
567
- content=json_content,
568
- base_name=output_file
572
+ content=json_content, base_name=output_file
569
573
  )
570
-
574
+
571
575
  # If suppress_output is True, return minimal response
572
576
  if suppress_output:
573
577
  minimal_result = {
574
578
  "success": result.get("success", True),
575
579
  "count": len(matches),
576
580
  "output_file": output_file,
577
- "file_saved": f"Results saved to {file_path}"
581
+ "file_saved": f"Results saved to {file_path}",
578
582
  }
579
583
  return minimal_result
580
584
  else:
@@ -591,7 +595,7 @@ class FindAndGrepTool(BaseMCPTool):
591
595
  "success": result.get("success", True),
592
596
  "count": len(matches),
593
597
  "summary": result.get("summary", {}),
594
- "meta": result.get("meta", {})
598
+ "meta": result.get("meta", {}),
595
599
  }
596
600
  return minimal_result
597
601
 
@@ -624,20 +628,24 @@ class FindAndGrepTool(BaseMCPTool):
624
628
  "success": True,
625
629
  "results": matches,
626
630
  "count": len(matches),
627
- "files": fd_rg_utils.group_matches_by_file(matches)["files"] if matches else [],
631
+ "files": fd_rg_utils.group_matches_by_file(matches)["files"]
632
+ if matches
633
+ else [],
628
634
  "summary": fd_rg_utils.summarize_search_results(matches),
629
- "meta": result["meta"]
635
+ "meta": result["meta"],
630
636
  }
631
637
 
632
638
  # Convert to JSON for file output
633
639
  # Save full result to file using FileOutputManager
634
640
  import json
635
- json_content = json.dumps(file_content, indent=2, ensure_ascii=False)
641
+
642
+ json_content = json.dumps(
643
+ file_content, indent=2, ensure_ascii=False
644
+ )
636
645
  file_path = self.file_output_manager.save_to_file(
637
- content=json_content,
638
- base_name=output_file
646
+ content=json_content, base_name=output_file
639
647
  )
640
-
648
+
641
649
  # Check if suppress_output is enabled
642
650
  suppress_output = arguments.get("suppress_output", False)
643
651
  if suppress_output:
@@ -646,7 +654,7 @@ class FindAndGrepTool(BaseMCPTool):
646
654
  "success": result.get("success", True),
647
655
  "count": result.get("count", 0),
648
656
  "output_file": output_file,
649
- "file_saved": f"Results saved to {file_path}"
657
+ "file_saved": f"Results saved to {file_path}",
650
658
  }
651
659
  return minimal_result
652
660
  else:
@@ -669,7 +677,7 @@ class FindAndGrepTool(BaseMCPTool):
669
677
  "success": result.get("success", True),
670
678
  "count": result.get("count", 0),
671
679
  "summary": result.get("summary", {}),
672
- "meta": result.get("meta", {})
680
+ "meta": result.get("meta", {}),
673
681
  }
674
682
  return minimal_result
675
683
 
@@ -113,12 +113,12 @@ class ListFilesTool(BaseMCPTool):
113
113
  },
114
114
  "output_file": {
115
115
  "type": "string",
116
- "description": "Optional filename to save output to file (extension auto-detected based on content)"
116
+ "description": "Optional filename to save output to file (extension auto-detected based on content)",
117
117
  },
118
118
  "suppress_output": {
119
119
  "type": "boolean",
120
120
  "description": "When true and output_file is specified, suppress detailed output in response to save tokens",
121
- "default": False
121
+ "default": False,
122
122
  },
123
123
  },
124
124
  "required": ["roots"],
@@ -187,9 +187,9 @@ class ListFilesTool(BaseMCPTool):
187
187
  "success": False,
188
188
  "error": "fd command not found. Please install fd (https://github.com/sharkdp/fd) to use this tool.",
189
189
  "count": 0,
190
- "results": []
190
+ "results": [],
191
191
  }
192
-
192
+
193
193
  self.validate_arguments(arguments)
194
194
  roots = self._validate_roots(arguments["roots"]) # normalized absolutes
195
195
 
@@ -273,7 +273,7 @@ class ListFilesTool(BaseMCPTool):
273
273
  # Handle file output for count_only mode
274
274
  output_file = arguments.get("output_file")
275
275
  suppress_output = arguments.get("suppress_output", False)
276
-
276
+
277
277
  if output_file:
278
278
  file_manager = FileOutputManager(self.project_root)
279
279
  file_content = {
@@ -289,18 +289,20 @@ class ListFilesTool(BaseMCPTool):
289
289
  "extensions": arguments.get("extensions"),
290
290
  "exclude": arguments.get("exclude"),
291
291
  "limit": limit,
292
- }
292
+ },
293
293
  }
294
-
294
+
295
295
  try:
296
296
  import json
297
- json_content = json.dumps(file_content, indent=2, ensure_ascii=False)
297
+
298
+ json_content = json.dumps(
299
+ file_content, indent=2, ensure_ascii=False
300
+ )
298
301
  saved_path = file_manager.save_to_file(
299
- content=json_content,
300
- base_name=output_file
302
+ content=json_content, base_name=output_file
301
303
  )
302
304
  result["output_file"] = saved_path
303
-
305
+
304
306
  if suppress_output:
305
307
  # Return minimal response to save tokens
306
308
  return {
@@ -308,7 +310,7 @@ class ListFilesTool(BaseMCPTool):
308
310
  "count_only": True,
309
311
  "total_count": total_count,
310
312
  "output_file": saved_path,
311
- "message": f"Count results saved to {saved_path}"
313
+ "message": f"Count results saved to {saved_path}",
312
314
  }
313
315
  except Exception as e:
314
316
  logger.warning(f"Failed to save output file: {e}")
@@ -359,7 +361,7 @@ class ListFilesTool(BaseMCPTool):
359
361
  # Handle file output for detailed results
360
362
  output_file = arguments.get("output_file")
361
363
  suppress_output = arguments.get("suppress_output", False)
362
-
364
+
363
365
  if output_file:
364
366
  file_manager = FileOutputManager(self.project_root)
365
367
  file_content = {
@@ -384,25 +386,25 @@ class ListFilesTool(BaseMCPTool):
384
386
  "full_path_match": arguments.get("full_path_match", False),
385
387
  "absolute": arguments.get("absolute", True),
386
388
  "limit": limit,
387
- }
389
+ },
388
390
  }
389
-
391
+
390
392
  try:
391
393
  import json
394
+
392
395
  json_content = json.dumps(file_content, indent=2, ensure_ascii=False)
393
396
  saved_path = file_manager.save_to_file(
394
- content=json_content,
395
- base_name=output_file
397
+ content=json_content, base_name=output_file
396
398
  )
397
399
  result["output_file"] = saved_path
398
-
400
+
399
401
  if suppress_output:
400
402
  # Return minimal response to save tokens
401
403
  return {
402
404
  "success": True,
403
405
  "count": len(results),
404
406
  "output_file": saved_path,
405
- "message": f"File list results saved to {saved_path}"
407
+ "message": f"File list results saved to {saved_path}",
406
408
  }
407
409
  except Exception as e:
408
410
  logger.warning(f"Failed to save output file: {e}")
@@ -12,7 +12,6 @@ from typing import Any
12
12
 
13
13
  from ...core.query_service import QueryService
14
14
  from ...language_detector import detect_language_from_file
15
- from ..utils.error_handler import handle_mcp_errors
16
15
  from ..utils.file_output_manager import FileOutputManager
17
16
  from .base_tool import BaseMCPTool
18
17
 
@@ -111,28 +110,25 @@ class QueryTool(BaseMCPTool):
111
110
  # Validate input parameters - check for empty arguments first
112
111
  if not arguments:
113
112
  from ..utils.error_handler import AnalysisError
114
- raise AnalysisError(
115
- "file_path is required",
116
- operation="query_code"
117
- )
118
-
113
+
114
+ raise AnalysisError("file_path is required", operation="query_code")
115
+
119
116
  file_path = arguments.get("file_path")
120
117
  if not file_path:
121
118
  from ..utils.error_handler import AnalysisError
122
- raise AnalysisError(
123
- "file_path is required",
124
- operation="query_code"
125
- )
119
+
120
+ raise AnalysisError("file_path is required", operation="query_code")
126
121
 
127
122
  # Check that either query_key or query_string is provided early
128
123
  query_key = arguments.get("query_key")
129
124
  query_string = arguments.get("query_string")
130
-
125
+
131
126
  if not query_key and not query_string:
132
127
  from ..utils.error_handler import AnalysisError
128
+
133
129
  raise AnalysisError(
134
130
  "Either query_key or query_string must be provided",
135
- operation="query_code"
131
+ operation="query_code",
136
132
  )
137
133
 
138
134
  # Security validation BEFORE path resolution to catch symlinks
@@ -140,12 +136,14 @@ class QueryTool(BaseMCPTool):
140
136
  if not is_valid:
141
137
  return {
142
138
  "success": False,
143
- "error": f"Invalid or unsafe file path: {error_msg or file_path}"
139
+ "error": f"Invalid or unsafe file path: {error_msg or file_path}",
144
140
  }
145
141
 
146
142
  # Resolve file path to absolute path
147
143
  resolved_file_path = self.path_resolver.resolve(file_path)
148
- logger.info(f"Querying file: {file_path} (resolved to: {resolved_file_path})")
144
+ logger.info(
145
+ f"Querying file: {file_path} (resolved to: {resolved_file_path})"
146
+ )
149
147
 
150
148
  # Additional security validation on resolved path
151
149
  is_valid, error_msg = self.security_validator.validate_file_path(
@@ -154,7 +152,7 @@ class QueryTool(BaseMCPTool):
154
152
  if not is_valid:
155
153
  return {
156
154
  "success": False,
157
- "error": f"Invalid or unsafe resolved path: {error_msg or resolved_file_path}"
155
+ "error": f"Invalid or unsafe resolved path: {error_msg or resolved_file_path}",
158
156
  }
159
157
 
160
158
  # Get query parameters (already validated above)
@@ -166,7 +164,7 @@ class QueryTool(BaseMCPTool):
166
164
  if query_key and query_string:
167
165
  return {
168
166
  "success": False,
169
- "error": "Cannot provide both query_key and query_string"
167
+ "error": "Cannot provide both query_key and query_string",
170
168
  }
171
169
 
172
170
  # Detect language
@@ -176,7 +174,7 @@ class QueryTool(BaseMCPTool):
176
174
  if not language:
177
175
  return {
178
176
  "success": False,
179
- "error": f"Could not detect language for file: {file_path}"
177
+ "error": f"Could not detect language for file: {file_path}",
180
178
  }
181
179
 
182
180
  # Execute query
@@ -270,10 +268,11 @@ class QueryTool(BaseMCPTool):
270
268
 
271
269
  except Exception as e:
272
270
  from ..utils.error_handler import AnalysisError
271
+
273
272
  # Re-raise AnalysisError to maintain proper error handling
274
273
  if isinstance(e, AnalysisError):
275
274
  raise
276
-
275
+
277
276
  logger.error(f"Query execution failed: {e}")
278
277
  return {
279
278
  "success": False,
@@ -27,7 +27,7 @@ class ReadPartialTool(BaseMCPTool):
27
27
  selective file content reading through the MCP protocol.
28
28
  """
29
29
 
30
- def __init__(self, project_root: str = None) -> None:
30
+ def __init__(self, project_root: str | None = None) -> None:
31
31
  """Initialize the read partial tool."""
32
32
  super().__init__(project_root)
33
33
  self.file_output_manager = FileOutputManager(project_root)
@@ -117,7 +117,9 @@ class ReadPartialTool(BaseMCPTool):
117
117
  output_format = arguments.get("format", "text")
118
118
 
119
119
  # Security validation BEFORE path resolution to catch symlinks
120
- is_valid, error_msg = self.security_validator.validate_file_path(file_path, self.project_root)
120
+ is_valid, error_msg = self.security_validator.validate_file_path(
121
+ file_path, self.project_root
122
+ )
121
123
  if not is_valid:
122
124
  logger.warning(
123
125
  f"Security validation failed for file path: {file_path} - {error_msg}"
@@ -125,14 +127,16 @@ class ReadPartialTool(BaseMCPTool):
125
127
  return {
126
128
  "success": False,
127
129
  "error": f"Security validation failed: {error_msg}",
128
- "file_path": file_path
130
+ "file_path": file_path,
129
131
  }
130
132
 
131
133
  # Resolve file path using common path resolver
132
134
  resolved_path = self.path_resolver.resolve(file_path)
133
135
 
134
136
  # Additional security validation on resolved path
135
- is_valid, error_msg = self.security_validator.validate_file_path(resolved_path, self.project_root)
137
+ is_valid, error_msg = self.security_validator.validate_file_path(
138
+ resolved_path, self.project_root
139
+ )
136
140
  if not is_valid:
137
141
  logger.warning(
138
142
  f"Security validation failed for resolved path: {resolved_path} - {error_msg}"
@@ -140,7 +144,7 @@ class ReadPartialTool(BaseMCPTool):
140
144
  return {
141
145
  "success": False,
142
146
  "error": f"Security validation failed for resolved path: {error_msg}",
143
- "file_path": file_path
147
+ "file_path": file_path,
144
148
  }
145
149
 
146
150
  # Validate file exists
@@ -148,7 +152,7 @@ class ReadPartialTool(BaseMCPTool):
148
152
  return {
149
153
  "success": False,
150
154
  "error": "Invalid file path: file does not exist",
151
- "file_path": file_path
155
+ "file_path": file_path,
152
156
  }
153
157
 
154
158
  # Validate line numbers
@@ -156,14 +160,14 @@ class ReadPartialTool(BaseMCPTool):
156
160
  return {
157
161
  "success": False,
158
162
  "error": "start_line must be >= 1",
159
- "file_path": file_path
163
+ "file_path": file_path,
160
164
  }
161
165
 
162
166
  if end_line is not None and end_line < start_line:
163
167
  return {
164
168
  "success": False,
165
169
  "error": "end_line must be >= start_line",
166
- "file_path": file_path
170
+ "file_path": file_path,
167
171
  }
168
172
 
169
173
  # Validate column numbers
@@ -171,14 +175,14 @@ class ReadPartialTool(BaseMCPTool):
171
175
  return {
172
176
  "success": False,
173
177
  "error": "start_column must be >= 0",
174
- "file_path": file_path
178
+ "file_path": file_path,
175
179
  }
176
180
 
177
181
  if end_column is not None and end_column < 0:
178
182
  return {
179
183
  "success": False,
180
184
  "error": "end_column must be >= 0",
181
- "file_path": file_path
185
+ "file_path": file_path,
182
186
  }
183
187
 
184
188
  logger.info(
@@ -199,15 +203,15 @@ class ReadPartialTool(BaseMCPTool):
199
203
  return {
200
204
  "success": False,
201
205
  "error": f"Failed to read partial content from file: {file_path}",
202
- "file_path": file_path
206
+ "file_path": file_path,
203
207
  }
204
-
208
+
205
209
  # Check if content is empty or invalid range
206
210
  if not content or content.strip() == "":
207
211
  return {
208
212
  "success": False,
209
213
  "error": f"Invalid line range or empty content: start_line={start_line}, end_line={end_line}",
210
- "file_path": file_path
214
+ "file_path": file_path,
211
215
  }
212
216
 
213
217
  # Build result structure compatible with CLI --partial-read format
@@ -245,7 +249,7 @@ class ReadPartialTool(BaseMCPTool):
245
249
  )
246
250
 
247
251
  # Calculate lines extracted
248
- lines_extracted = len(content.split('\n')) if content else 0
252
+ lines_extracted = len(content.split("\n")) if content else 0
249
253
  if end_line:
250
254
  lines_extracted = end_line - start_line + 1
251
255
 
@@ -267,15 +271,15 @@ class ReadPartialTool(BaseMCPTool):
267
271
  if not suppress_output or not output_file:
268
272
  if output_format == "json":
269
273
  # For JSON format, return structured data with exact line count
270
- lines = content.split('\n') if content else []
271
-
274
+ lines = content.split("\n") if content else []
275
+
272
276
  # If end_line is specified, ensure we return exactly the requested number of lines
273
277
  if end_line and len(lines) > lines_extracted:
274
278
  lines = lines[:lines_extracted]
275
279
  elif end_line and len(lines) < lines_extracted:
276
280
  # Pad with empty lines if needed (shouldn't normally happen)
277
- lines.extend([''] * (lines_extracted - len(lines)))
278
-
281
+ lines.extend([""] * (lines_extracted - len(lines)))
282
+
279
283
  result["partial_content_result"] = {
280
284
  "lines": lines,
281
285
  "metadata": {
@@ -287,8 +291,8 @@ class ReadPartialTool(BaseMCPTool):
287
291
  "end_column": end_column,
288
292
  },
289
293
  "content_length": len(content),
290
- "lines_count": len(lines)
291
- }
294
+ "lines_count": len(lines),
295
+ },
292
296
  }
293
297
  else:
294
298
  # For text/raw format, return CLI-compatible string
@@ -313,18 +317,17 @@ class ReadPartialTool(BaseMCPTool):
313
317
  else: # format == "text" (default)
314
318
  # Save CLI-compatible format with headers
315
319
  content_to_save = cli_output
316
-
320
+
317
321
  # Save to file with automatic extension detection
318
322
  saved_file_path = self.file_output_manager.save_to_file(
319
- content=content_to_save,
320
- base_name=base_name
323
+ content=content_to_save, base_name=base_name
321
324
  )
322
-
325
+
323
326
  result["output_file_path"] = saved_file_path
324
327
  result["file_saved"] = True
325
-
328
+
326
329
  logger.info(f"Extract output saved to: {saved_file_path}")
327
-
330
+
328
331
  except Exception as e:
329
332
  logger.error(f"Failed to save output to file: {e}")
330
333
  result["file_save_error"] = str(e)
@@ -334,11 +337,7 @@ class ReadPartialTool(BaseMCPTool):
334
337
 
335
338
  except Exception as e:
336
339
  logger.error(f"Error reading partial content from {file_path}: {e}")
337
- return {
338
- "success": False,
339
- "error": str(e),
340
- "file_path": file_path
341
- }
340
+ return {"success": False, "error": str(e), "file_path": file_path}
342
341
 
343
342
  def _read_file_partial(
344
343
  self,