tree-sitter-analyzer 1.8.3__py3-none-any.whl → 1.9.0__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.
- tree_sitter_analyzer/__init__.py +1 -1
- tree_sitter_analyzer/api.py +4 -4
- tree_sitter_analyzer/cli/argument_validator.py +29 -17
- tree_sitter_analyzer/cli/commands/advanced_command.py +7 -5
- tree_sitter_analyzer/cli/commands/structure_command.py +7 -5
- tree_sitter_analyzer/cli/commands/summary_command.py +10 -6
- tree_sitter_analyzer/cli/commands/table_command.py +8 -7
- tree_sitter_analyzer/cli/info_commands.py +1 -1
- tree_sitter_analyzer/cli_main.py +3 -2
- tree_sitter_analyzer/core/analysis_engine.py +5 -5
- tree_sitter_analyzer/core/cache_service.py +3 -1
- tree_sitter_analyzer/core/query.py +17 -5
- tree_sitter_analyzer/core/query_service.py +1 -1
- tree_sitter_analyzer/encoding_utils.py +3 -3
- tree_sitter_analyzer/exceptions.py +61 -50
- tree_sitter_analyzer/file_handler.py +3 -0
- tree_sitter_analyzer/formatters/base_formatter.py +10 -5
- tree_sitter_analyzer/formatters/formatter_registry.py +83 -68
- tree_sitter_analyzer/formatters/html_formatter.py +90 -54
- tree_sitter_analyzer/formatters/javascript_formatter.py +21 -16
- tree_sitter_analyzer/formatters/language_formatter_factory.py +7 -6
- tree_sitter_analyzer/formatters/markdown_formatter.py +247 -124
- tree_sitter_analyzer/formatters/python_formatter.py +61 -38
- tree_sitter_analyzer/formatters/typescript_formatter.py +113 -45
- tree_sitter_analyzer/interfaces/mcp_server.py +2 -2
- tree_sitter_analyzer/language_detector.py +6 -6
- tree_sitter_analyzer/language_loader.py +3 -1
- tree_sitter_analyzer/languages/css_plugin.py +120 -61
- tree_sitter_analyzer/languages/html_plugin.py +159 -62
- tree_sitter_analyzer/languages/java_plugin.py +42 -34
- tree_sitter_analyzer/languages/javascript_plugin.py +59 -30
- tree_sitter_analyzer/languages/markdown_plugin.py +402 -368
- tree_sitter_analyzer/languages/python_plugin.py +111 -64
- tree_sitter_analyzer/languages/typescript_plugin.py +241 -132
- tree_sitter_analyzer/mcp/server.py +22 -18
- tree_sitter_analyzer/mcp/tools/analyze_scale_tool.py +13 -8
- tree_sitter_analyzer/mcp/tools/base_tool.py +2 -2
- tree_sitter_analyzer/mcp/tools/fd_rg_utils.py +232 -26
- tree_sitter_analyzer/mcp/tools/find_and_grep_tool.py +31 -23
- tree_sitter_analyzer/mcp/tools/list_files_tool.py +21 -19
- tree_sitter_analyzer/mcp/tools/query_tool.py +17 -18
- tree_sitter_analyzer/mcp/tools/read_partial_tool.py +30 -31
- tree_sitter_analyzer/mcp/tools/search_content_tool.py +131 -77
- tree_sitter_analyzer/mcp/tools/table_format_tool.py +29 -16
- tree_sitter_analyzer/mcp/utils/file_output_factory.py +64 -51
- tree_sitter_analyzer/mcp/utils/file_output_manager.py +34 -24
- tree_sitter_analyzer/mcp/utils/gitignore_detector.py +8 -4
- tree_sitter_analyzer/models.py +7 -5
- tree_sitter_analyzer/plugins/base.py +9 -7
- tree_sitter_analyzer/plugins/manager.py +1 -0
- tree_sitter_analyzer/queries/css.py +2 -21
- tree_sitter_analyzer/queries/html.py +2 -15
- tree_sitter_analyzer/queries/markdown.py +30 -41
- tree_sitter_analyzer/queries/python.py +20 -5
- tree_sitter_analyzer/query_loader.py +5 -5
- tree_sitter_analyzer/security/validator.py +114 -86
- tree_sitter_analyzer/utils/__init__.py +58 -28
- tree_sitter_analyzer/utils/tree_sitter_compat.py +72 -65
- tree_sitter_analyzer/utils.py +83 -25
- {tree_sitter_analyzer-1.8.3.dist-info → tree_sitter_analyzer-1.9.0.dist-info}/METADATA +19 -5
- tree_sitter_analyzer-1.9.0.dist-info/RECORD +109 -0
- tree_sitter_analyzer-1.8.3.dist-info/RECORD +0 -109
- {tree_sitter_analyzer-1.8.3.dist-info → tree_sitter_analyzer-1.9.0.dist-info}/WHEEL +0 -0
- {tree_sitter_analyzer-1.8.3.dist-info → tree_sitter_analyzer-1.9.0.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
|
-
|
|
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"] =
|
|
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"]
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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([
|
|
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,
|