tree-sitter-analyzer 0.8.0__py3-none-any.whl → 0.8.2__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/cli/commands/base_command.py +27 -9
- tree_sitter_analyzer/cli/commands/default_command.py +1 -1
- tree_sitter_analyzer/cli/commands/partial_read_command.py +7 -7
- tree_sitter_analyzer/cli/commands/query_command.py +11 -4
- tree_sitter_analyzer/cli/commands/table_command.py +1 -1
- tree_sitter_analyzer/cli/info_commands.py +2 -2
- tree_sitter_analyzer/cli_main.py +13 -7
- tree_sitter_analyzer/core/analysis_engine.py +38 -13
- tree_sitter_analyzer/mcp/server.py +93 -13
- tree_sitter_analyzer/mcp/tools/analyze_scale_tool.py +22 -3
- tree_sitter_analyzer/mcp/tools/read_partial_tool.py +10 -2
- tree_sitter_analyzer/mcp/tools/table_format_tool.py +20 -3
- tree_sitter_analyzer/mcp/tools/universal_analyze_tool.py +18 -2
- tree_sitter_analyzer/mcp/utils/error_handler.py +18 -0
- tree_sitter_analyzer/project_detector.py +317 -0
- tree_sitter_analyzer/security/validator.py +24 -7
- {tree_sitter_analyzer-0.8.0.dist-info → tree_sitter_analyzer-0.8.2.dist-info}/METADATA +57 -7
- {tree_sitter_analyzer-0.8.0.dist-info → tree_sitter_analyzer-0.8.2.dist-info}/RECORD +21 -20
- {tree_sitter_analyzer-0.8.0.dist-info → tree_sitter_analyzer-0.8.2.dist-info}/WHEEL +0 -0
- {tree_sitter_analyzer-0.8.0.dist-info → tree_sitter_analyzer-0.8.2.dist-info}/entry_points.txt +0 -0
tree_sitter_analyzer/__init__.py
CHANGED
|
@@ -15,6 +15,8 @@ from ...file_handler import read_file_partial
|
|
|
15
15
|
from ...language_detector import detect_language_from_file, is_language_supported
|
|
16
16
|
from ...models import AnalysisResult
|
|
17
17
|
from ...output_manager import output_error, output_info
|
|
18
|
+
from ...project_detector import detect_project_root
|
|
19
|
+
from ...security import SecurityValidator
|
|
18
20
|
|
|
19
21
|
|
|
20
22
|
class BaseCommand(ABC):
|
|
@@ -28,18 +30,32 @@ class BaseCommand(ABC):
|
|
|
28
30
|
def __init__(self, args: Namespace):
|
|
29
31
|
"""Initialize command with parsed arguments."""
|
|
30
32
|
self.args = args
|
|
31
|
-
|
|
33
|
+
|
|
34
|
+
# Detect project root with priority handling
|
|
35
|
+
file_path = getattr(args, 'file_path', None)
|
|
36
|
+
explicit_root = getattr(args, 'project_root', None)
|
|
37
|
+
self.project_root = detect_project_root(file_path, explicit_root)
|
|
38
|
+
|
|
39
|
+
# Initialize components with project root
|
|
40
|
+
self.analysis_engine = get_analysis_engine(self.project_root)
|
|
41
|
+
self.security_validator = SecurityValidator(self.project_root)
|
|
32
42
|
|
|
33
43
|
def validate_file(self) -> bool:
|
|
34
44
|
"""Validate input file exists and is accessible."""
|
|
35
45
|
if not hasattr(self.args, "file_path") or not self.args.file_path:
|
|
36
|
-
output_error("
|
|
46
|
+
output_error("File path not specified.")
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
# Security validation
|
|
50
|
+
is_valid, error_msg = self.security_validator.validate_file_path(self.args.file_path)
|
|
51
|
+
if not is_valid:
|
|
52
|
+
output_error(f"Invalid file path: {error_msg}")
|
|
37
53
|
return False
|
|
38
54
|
|
|
39
55
|
import os
|
|
40
56
|
|
|
41
57
|
if not os.path.exists(self.args.file_path):
|
|
42
|
-
output_error(f"
|
|
58
|
+
output_error(f"File not found: {self.args.file_path}")
|
|
43
59
|
return False
|
|
44
60
|
|
|
45
61
|
return True
|
|
@@ -47,7 +63,9 @@ class BaseCommand(ABC):
|
|
|
47
63
|
def detect_language(self) -> str | None:
|
|
48
64
|
"""Detect or validate the target language."""
|
|
49
65
|
if hasattr(self.args, "language") and self.args.language:
|
|
50
|
-
|
|
66
|
+
# Sanitize language input
|
|
67
|
+
sanitized_language = self.security_validator.sanitize_input(self.args.language, max_length=50)
|
|
68
|
+
target_language = sanitized_language.lower()
|
|
51
69
|
if (not hasattr(self.args, "table") or not self.args.table) and (
|
|
52
70
|
not hasattr(self.args, "quiet") or not self.args.quiet
|
|
53
71
|
):
|
|
@@ -94,10 +112,10 @@ class BaseCommand(ABC):
|
|
|
94
112
|
end_column=getattr(self.args, "end_column", None),
|
|
95
113
|
)
|
|
96
114
|
if partial_content is None:
|
|
97
|
-
output_error("
|
|
115
|
+
output_error("Failed to read file partially")
|
|
98
116
|
return None
|
|
99
117
|
except Exception as e:
|
|
100
|
-
output_error(f"
|
|
118
|
+
output_error(f"Failed to read file partially: {e}")
|
|
101
119
|
return None
|
|
102
120
|
|
|
103
121
|
request = AnalysisRequest(
|
|
@@ -114,13 +132,13 @@ class BaseCommand(ABC):
|
|
|
114
132
|
if analysis_result
|
|
115
133
|
else "Unknown error"
|
|
116
134
|
)
|
|
117
|
-
output_error(f"
|
|
135
|
+
output_error(f"Analysis failed: {error_msg}")
|
|
118
136
|
return None
|
|
119
137
|
|
|
120
138
|
return analysis_result
|
|
121
139
|
|
|
122
140
|
except Exception as e:
|
|
123
|
-
output_error(f"
|
|
141
|
+
output_error(f"An error occurred during analysis: {e}")
|
|
124
142
|
return None
|
|
125
143
|
|
|
126
144
|
def execute(self) -> int:
|
|
@@ -143,7 +161,7 @@ class BaseCommand(ABC):
|
|
|
143
161
|
try:
|
|
144
162
|
return asyncio.run(self.execute_async(language))
|
|
145
163
|
except Exception as e:
|
|
146
|
-
output_error(f"
|
|
164
|
+
output_error(f"An error occurred during command execution: {e}")
|
|
147
165
|
return 1
|
|
148
166
|
|
|
149
167
|
@abstractmethod
|
|
@@ -14,5 +14,5 @@ class DefaultCommand(BaseCommand):
|
|
|
14
14
|
|
|
15
15
|
async def execute_async(self, language: str) -> int:
|
|
16
16
|
"""Execute default command - show error for missing options."""
|
|
17
|
-
output_error("
|
|
17
|
+
output_error("Please specify a query or --advanced option")
|
|
18
18
|
return 1
|
|
@@ -28,7 +28,7 @@ class PartialReadCommand(BaseCommand):
|
|
|
28
28
|
if not hasattr(self.args, "file_path") or not self.args.file_path:
|
|
29
29
|
from ...output_manager import output_error
|
|
30
30
|
|
|
31
|
-
output_error("
|
|
31
|
+
output_error("File path not specified.")
|
|
32
32
|
return False
|
|
33
33
|
|
|
34
34
|
import os
|
|
@@ -36,7 +36,7 @@ class PartialReadCommand(BaseCommand):
|
|
|
36
36
|
if not os.path.exists(self.args.file_path):
|
|
37
37
|
from ...output_manager import output_error
|
|
38
38
|
|
|
39
|
-
output_error(f"
|
|
39
|
+
output_error(f"File not found: {self.args.file_path}")
|
|
40
40
|
return False
|
|
41
41
|
|
|
42
42
|
return True
|
|
@@ -56,20 +56,20 @@ class PartialReadCommand(BaseCommand):
|
|
|
56
56
|
if not self.args.start_line:
|
|
57
57
|
from ...output_manager import output_error
|
|
58
58
|
|
|
59
|
-
output_error("
|
|
59
|
+
output_error("--start-line is required")
|
|
60
60
|
return 1
|
|
61
61
|
|
|
62
62
|
if self.args.start_line < 1:
|
|
63
63
|
from ...output_manager import output_error
|
|
64
64
|
|
|
65
|
-
output_error("
|
|
65
|
+
output_error("--start-line must be 1 or greater")
|
|
66
66
|
return 1
|
|
67
67
|
|
|
68
68
|
if self.args.end_line and self.args.end_line < self.args.start_line:
|
|
69
69
|
from ...output_manager import output_error
|
|
70
70
|
|
|
71
71
|
output_error(
|
|
72
|
-
"
|
|
72
|
+
"--end-line must be greater than or equal to --start-line"
|
|
73
73
|
)
|
|
74
74
|
return 1
|
|
75
75
|
|
|
@@ -86,7 +86,7 @@ class PartialReadCommand(BaseCommand):
|
|
|
86
86
|
if partial_content is None:
|
|
87
87
|
from ...output_manager import output_error
|
|
88
88
|
|
|
89
|
-
output_error("
|
|
89
|
+
output_error("Failed to read file partially")
|
|
90
90
|
return 1
|
|
91
91
|
|
|
92
92
|
# Output the result
|
|
@@ -96,7 +96,7 @@ class PartialReadCommand(BaseCommand):
|
|
|
96
96
|
except Exception as e:
|
|
97
97
|
from ...output_manager import output_error
|
|
98
98
|
|
|
99
|
-
output_error(f"
|
|
99
|
+
output_error(f"Failed to read file partially: {e}")
|
|
100
100
|
return 1
|
|
101
101
|
|
|
102
102
|
def _output_partial_content(self, content: str) -> None:
|
|
@@ -18,21 +18,28 @@ class QueryCommand(BaseCommand):
|
|
|
18
18
|
query_to_execute = None
|
|
19
19
|
|
|
20
20
|
if hasattr(self.args, "query_key") and self.args.query_key:
|
|
21
|
+
# Sanitize query key input
|
|
22
|
+
sanitized_query_key = self.security_validator.sanitize_input(self.args.query_key, max_length=100)
|
|
21
23
|
try:
|
|
22
|
-
query_to_execute = query_loader.get_query(language,
|
|
24
|
+
query_to_execute = query_loader.get_query(language, sanitized_query_key)
|
|
23
25
|
if query_to_execute is None:
|
|
24
26
|
output_error(
|
|
25
|
-
f"
|
|
27
|
+
f"Query '{sanitized_query_key}' not found for language '{language}'"
|
|
26
28
|
)
|
|
27
29
|
return 1
|
|
28
30
|
except ValueError as e:
|
|
29
|
-
output_error(f"
|
|
31
|
+
output_error(f"{e}")
|
|
30
32
|
return 1
|
|
31
33
|
elif hasattr(self.args, "query_string") and self.args.query_string:
|
|
34
|
+
# Security check for query string (potential regex patterns)
|
|
35
|
+
is_safe, error_msg = self.security_validator.regex_checker.validate_pattern(self.args.query_string)
|
|
36
|
+
if not is_safe:
|
|
37
|
+
output_error(f"Unsafe query pattern: {error_msg}")
|
|
38
|
+
return 1
|
|
32
39
|
query_to_execute = self.args.query_string
|
|
33
40
|
|
|
34
41
|
if not query_to_execute:
|
|
35
|
-
output_error("
|
|
42
|
+
output_error("No query specified.")
|
|
36
43
|
return 1
|
|
37
44
|
|
|
38
45
|
# Perform analysis
|
|
@@ -42,7 +42,7 @@ class TableCommand(BaseCommand):
|
|
|
42
42
|
return 0
|
|
43
43
|
|
|
44
44
|
except Exception as e:
|
|
45
|
-
output_error(f"
|
|
45
|
+
output_error(f"An error occurred during table format analysis: {e}")
|
|
46
46
|
return 1
|
|
47
47
|
|
|
48
48
|
def _convert_to_structure_format(
|
|
@@ -78,7 +78,7 @@ class DescribeQueryCommand(InfoCommand):
|
|
|
78
78
|
|
|
79
79
|
if query_description is None or query_content is None:
|
|
80
80
|
output_error(
|
|
81
|
-
f"
|
|
81
|
+
f"Query '{self.args.describe_query}' not found for language '{language}'"
|
|
82
82
|
)
|
|
83
83
|
return 1
|
|
84
84
|
|
|
@@ -87,7 +87,7 @@ class DescribeQueryCommand(InfoCommand):
|
|
|
87
87
|
)
|
|
88
88
|
output_data(f"Query content:\n{query_content}")
|
|
89
89
|
except ValueError as e:
|
|
90
|
-
output_error(f"
|
|
90
|
+
output_error(f"{e}")
|
|
91
91
|
return 1
|
|
92
92
|
return 0
|
|
93
93
|
|
tree_sitter_analyzer/cli_main.py
CHANGED
|
@@ -166,6 +166,12 @@ def create_argument_parser() -> argparse.ArgumentParser:
|
|
|
166
166
|
help="Explicitly specify language (auto-detected from extension if omitted)",
|
|
167
167
|
)
|
|
168
168
|
|
|
169
|
+
# Project options
|
|
170
|
+
parser.add_argument(
|
|
171
|
+
"--project-root",
|
|
172
|
+
help="Project root directory for security validation (auto-detected if not specified)",
|
|
173
|
+
)
|
|
174
|
+
|
|
169
175
|
# Logging options
|
|
170
176
|
parser.add_argument(
|
|
171
177
|
"--quiet",
|
|
@@ -201,25 +207,25 @@ def handle_special_commands(args: argparse.Namespace) -> int | None:
|
|
|
201
207
|
# Validate partial read options
|
|
202
208
|
if hasattr(args, "partial_read") and args.partial_read:
|
|
203
209
|
if args.start_line is None:
|
|
204
|
-
output_error("
|
|
210
|
+
output_error("--start-line is required")
|
|
205
211
|
return 1
|
|
206
212
|
|
|
207
213
|
if args.start_line < 1:
|
|
208
|
-
output_error("
|
|
214
|
+
output_error("--start-line must be 1 or greater")
|
|
209
215
|
return 1
|
|
210
216
|
|
|
211
217
|
if args.end_line and args.end_line < args.start_line:
|
|
212
218
|
output_error(
|
|
213
|
-
"
|
|
219
|
+
"--end-line must be greater than or equal to --start-line"
|
|
214
220
|
)
|
|
215
221
|
return 1
|
|
216
222
|
|
|
217
223
|
if args.start_column is not None and args.start_column < 0:
|
|
218
|
-
output_error("
|
|
224
|
+
output_error("--start-column must be 0 or greater")
|
|
219
225
|
return 1
|
|
220
226
|
|
|
221
227
|
if args.end_column is not None and args.end_column < 0:
|
|
222
|
-
output_error("
|
|
228
|
+
output_error("--end-column must be 0 or greater")
|
|
223
229
|
return 1
|
|
224
230
|
|
|
225
231
|
# Query language commands
|
|
@@ -279,9 +285,9 @@ def main() -> None:
|
|
|
279
285
|
sys.exit(exit_code)
|
|
280
286
|
else:
|
|
281
287
|
if not args.file_path:
|
|
282
|
-
output_error("
|
|
288
|
+
output_error("File path not specified.")
|
|
283
289
|
else:
|
|
284
|
-
output_error("
|
|
290
|
+
output_error("No executable command specified.")
|
|
285
291
|
parser.print_help()
|
|
286
292
|
sys.exit(1)
|
|
287
293
|
|
|
@@ -15,11 +15,12 @@ Roo Code compliance:
|
|
|
15
15
|
import hashlib
|
|
16
16
|
import threading
|
|
17
17
|
from dataclasses import dataclass
|
|
18
|
-
from typing import Any, Optional, Protocol
|
|
18
|
+
from typing import Any, Dict, Optional, Protocol
|
|
19
19
|
|
|
20
20
|
from ..models import AnalysisResult
|
|
21
21
|
from ..plugins.base import LanguagePlugin as BaseLanguagePlugin
|
|
22
22
|
from ..plugins.manager import PluginManager
|
|
23
|
+
from ..security import SecurityValidator
|
|
23
24
|
from ..utils import log_debug, log_error, log_info, log_performance
|
|
24
25
|
from .cache_service import CacheService
|
|
25
26
|
|
|
@@ -204,32 +205,41 @@ class UnifiedAnalysisEngine:
|
|
|
204
205
|
_performance_monitor: パフォーマンス監視
|
|
205
206
|
"""
|
|
206
207
|
|
|
207
|
-
|
|
208
|
+
_instances: Dict[str, "UnifiedAnalysisEngine"] = {}
|
|
208
209
|
_lock: threading.Lock = threading.Lock()
|
|
209
210
|
|
|
210
|
-
def __new__(cls) -> "UnifiedAnalysisEngine":
|
|
211
|
-
"""シングルトンパターンでインスタンス共有"""
|
|
212
|
-
|
|
211
|
+
def __new__(cls, project_root: str = None) -> "UnifiedAnalysisEngine":
|
|
212
|
+
"""シングルトンパターンでインスタンス共有 (project_root aware)"""
|
|
213
|
+
# Create a key based on project_root for different instances
|
|
214
|
+
instance_key = project_root or "default"
|
|
215
|
+
|
|
216
|
+
if instance_key not in cls._instances:
|
|
213
217
|
with cls._lock:
|
|
214
|
-
if cls.
|
|
215
|
-
|
|
216
|
-
|
|
218
|
+
if instance_key not in cls._instances:
|
|
219
|
+
instance = super().__new__(cls)
|
|
220
|
+
cls._instances[instance_key] = instance
|
|
221
|
+
# Mark as not initialized for this instance
|
|
222
|
+
instance._initialized = False
|
|
217
223
|
|
|
218
|
-
|
|
224
|
+
return cls._instances[instance_key]
|
|
225
|
+
|
|
226
|
+
def __init__(self, project_root: str = None) -> None:
|
|
219
227
|
"""初期化(一度のみ実行)"""
|
|
220
|
-
if hasattr(self, "_initialized"):
|
|
228
|
+
if hasattr(self, "_initialized") and self._initialized:
|
|
221
229
|
return
|
|
222
230
|
|
|
223
231
|
self._cache_service = CacheService()
|
|
224
232
|
self._plugin_manager = PluginManager()
|
|
225
233
|
self._performance_monitor = PerformanceMonitor()
|
|
234
|
+
self._security_validator = SecurityValidator(project_root)
|
|
235
|
+
self._project_root = project_root
|
|
226
236
|
|
|
227
237
|
# プラグインを自動ロード
|
|
228
238
|
self._load_plugins()
|
|
229
239
|
|
|
230
240
|
self._initialized = True
|
|
231
241
|
|
|
232
|
-
log_info("UnifiedAnalysisEngine initialized")
|
|
242
|
+
log_info(f"UnifiedAnalysisEngine initialized with project root: {project_root}")
|
|
233
243
|
|
|
234
244
|
def _load_plugins(self) -> None:
|
|
235
245
|
"""利用可能なプラグインを自動ロード"""
|
|
@@ -265,6 +275,12 @@ class UnifiedAnalysisEngine:
|
|
|
265
275
|
"""
|
|
266
276
|
log_info(f"Starting analysis for {request.file_path}")
|
|
267
277
|
|
|
278
|
+
# Security validation
|
|
279
|
+
is_valid, error_msg = self._security_validator.validate_file_path(request.file_path)
|
|
280
|
+
if not is_valid:
|
|
281
|
+
log_error(f"Security validation failed for file path: {request.file_path} - {error_msg}")
|
|
282
|
+
raise ValueError(f"Invalid file path: {error_msg}")
|
|
283
|
+
|
|
268
284
|
# キャッシュチェック(CLI・MCP間で共有)
|
|
269
285
|
cache_key = self._generate_cache_key(request)
|
|
270
286
|
cached_result = await self._cache_service.get(cache_key)
|
|
@@ -323,6 +339,12 @@ class UnifiedAnalysisEngine:
|
|
|
323
339
|
Returns:
|
|
324
340
|
Analysis result
|
|
325
341
|
"""
|
|
342
|
+
# Security validation
|
|
343
|
+
is_valid, error_msg = self._security_validator.validate_file_path(file_path)
|
|
344
|
+
if not is_valid:
|
|
345
|
+
log_error(f"Security validation failed for file path: {file_path} - {error_msg}")
|
|
346
|
+
raise ValueError(f"Invalid file path: {error_msg}")
|
|
347
|
+
|
|
326
348
|
request = AnalysisRequest(
|
|
327
349
|
file_path=file_path,
|
|
328
350
|
language=None, # Auto-detect
|
|
@@ -545,11 +567,14 @@ class MockLanguagePlugin:
|
|
|
545
567
|
)
|
|
546
568
|
|
|
547
569
|
|
|
548
|
-
def get_analysis_engine() -> UnifiedAnalysisEngine:
|
|
570
|
+
def get_analysis_engine(project_root: str = None) -> UnifiedAnalysisEngine:
|
|
549
571
|
"""
|
|
550
572
|
統一解析エンジンのインスタンスを取得
|
|
551
573
|
|
|
574
|
+
Args:
|
|
575
|
+
project_root: Project root directory for security validation
|
|
576
|
+
|
|
552
577
|
Returns:
|
|
553
578
|
統一解析エンジンのシングルトンインスタンス
|
|
554
579
|
"""
|
|
555
|
-
return UnifiedAnalysisEngine()
|
|
580
|
+
return UnifiedAnalysisEngine(project_root)
|
|
@@ -6,8 +6,10 @@ This module provides the main MCP server that exposes tree-sitter analyzer
|
|
|
6
6
|
functionality through the Model Context Protocol.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
+
import argparse
|
|
9
10
|
import asyncio
|
|
10
11
|
import json
|
|
12
|
+
import os
|
|
11
13
|
import sys
|
|
12
14
|
from typing import Any
|
|
13
15
|
|
|
@@ -43,6 +45,8 @@ except ImportError:
|
|
|
43
45
|
|
|
44
46
|
|
|
45
47
|
from ..core.analysis_engine import get_analysis_engine
|
|
48
|
+
from ..project_detector import detect_project_root
|
|
49
|
+
from ..security import SecurityValidator
|
|
46
50
|
from ..utils import setup_logger
|
|
47
51
|
from . import MCP_INFO
|
|
48
52
|
from .resources import CodeFileResource, ProjectStatsResource
|
|
@@ -64,16 +68,21 @@ class TreeSitterAnalyzerMCPServer:
|
|
|
64
68
|
integrating with existing analyzer components.
|
|
65
69
|
"""
|
|
66
70
|
|
|
67
|
-
def __init__(self) -> None:
|
|
71
|
+
def __init__(self, project_root: str = None) -> None:
|
|
68
72
|
"""Initialize the MCP server with analyzer components."""
|
|
69
73
|
self.server: Server | None = None
|
|
70
|
-
self.
|
|
74
|
+
self._initialization_complete = False
|
|
75
|
+
|
|
76
|
+
logger.info("Starting MCP server initialization...")
|
|
77
|
+
|
|
78
|
+
self.analysis_engine = get_analysis_engine(project_root)
|
|
79
|
+
self.security_validator = SecurityValidator(project_root)
|
|
71
80
|
# Use unified analysis engine instead of deprecated AdvancedAnalyzer
|
|
72
81
|
|
|
73
|
-
# Initialize MCP tools
|
|
74
|
-
self.read_partial_tool: MCPTool = ReadPartialTool()
|
|
75
|
-
self.universal_analyze_tool: MCPTool = UniversalAnalyzeTool()
|
|
76
|
-
self.table_format_tool: MCPTool = TableFormatTool()
|
|
82
|
+
# Initialize MCP tools with security validation
|
|
83
|
+
self.read_partial_tool: MCPTool = ReadPartialTool(project_root)
|
|
84
|
+
self.universal_analyze_tool: MCPTool = UniversalAnalyzeTool(project_root)
|
|
85
|
+
self.table_format_tool: MCPTool = TableFormatTool(project_root)
|
|
77
86
|
|
|
78
87
|
# Initialize MCP resources
|
|
79
88
|
self.code_file_resource = CodeFileResource()
|
|
@@ -83,13 +92,24 @@ class TreeSitterAnalyzerMCPServer:
|
|
|
83
92
|
self.name = MCP_INFO["name"]
|
|
84
93
|
self.version = MCP_INFO["version"]
|
|
85
94
|
|
|
86
|
-
|
|
95
|
+
self._initialization_complete = True
|
|
96
|
+
logger.info(f"MCP server initialization complete: {self.name} v{self.version}")
|
|
97
|
+
|
|
98
|
+
def is_initialized(self) -> bool:
|
|
99
|
+
"""Check if the server is fully initialized."""
|
|
100
|
+
return self._initialization_complete
|
|
101
|
+
|
|
102
|
+
def _ensure_initialized(self) -> None:
|
|
103
|
+
"""Ensure the server is initialized before processing requests."""
|
|
104
|
+
if not self._initialization_complete:
|
|
105
|
+
raise RuntimeError("Server not fully initialized. Please wait for initialization to complete.")
|
|
87
106
|
|
|
88
107
|
@handle_mcp_errors("analyze_code_scale")
|
|
89
108
|
async def _analyze_code_scale(self, arguments: dict[str, Any]) -> dict[str, Any]:
|
|
90
109
|
"""
|
|
91
110
|
Analyze code scale and complexity metrics by delegating to the universal_analyze_tool.
|
|
92
111
|
"""
|
|
112
|
+
self._ensure_initialized()
|
|
93
113
|
# Delegate the execution to the already initialized tool
|
|
94
114
|
return await self.universal_analyze_tool.execute(arguments)
|
|
95
115
|
|
|
@@ -161,9 +181,28 @@ class TreeSitterAnalyzerMCPServer:
|
|
|
161
181
|
async def handle_call_tool(
|
|
162
182
|
name: str, arguments: dict[str, Any]
|
|
163
183
|
) -> list[TextContent]:
|
|
164
|
-
"""Handle tool calls."""
|
|
184
|
+
"""Handle tool calls with security validation."""
|
|
165
185
|
try:
|
|
166
|
-
|
|
186
|
+
# Ensure server is fully initialized
|
|
187
|
+
self._ensure_initialized()
|
|
188
|
+
|
|
189
|
+
# Security validation for tool name
|
|
190
|
+
sanitized_name = self.security_validator.sanitize_input(name, max_length=100)
|
|
191
|
+
|
|
192
|
+
# Log tool call for audit
|
|
193
|
+
logger.info(f"MCP tool call: {sanitized_name} with args: {list(arguments.keys())}")
|
|
194
|
+
|
|
195
|
+
# Validate arguments contain no malicious content
|
|
196
|
+
for key, value in arguments.items():
|
|
197
|
+
if isinstance(value, str):
|
|
198
|
+
# Check for potential injection attempts
|
|
199
|
+
if len(value) > 10000: # Prevent extremely large inputs
|
|
200
|
+
raise ValueError(f"Input too large for parameter {key}")
|
|
201
|
+
|
|
202
|
+
# Basic sanitization for string inputs
|
|
203
|
+
sanitized_value = self.security_validator.sanitize_input(value, max_length=10000)
|
|
204
|
+
arguments[key] = sanitized_value
|
|
205
|
+
if sanitized_name == "analyze_code_scale":
|
|
167
206
|
result = await self._analyze_code_scale(arguments)
|
|
168
207
|
return [
|
|
169
208
|
TextContent(
|
|
@@ -171,7 +210,7 @@ class TreeSitterAnalyzerMCPServer:
|
|
|
171
210
|
text=json.dumps(result, indent=2, ensure_ascii=False),
|
|
172
211
|
)
|
|
173
212
|
]
|
|
174
|
-
elif
|
|
213
|
+
elif sanitized_name == "read_code_partial":
|
|
175
214
|
result = await self.read_partial_tool.execute(arguments)
|
|
176
215
|
return [
|
|
177
216
|
TextContent(
|
|
@@ -179,7 +218,7 @@ class TreeSitterAnalyzerMCPServer:
|
|
|
179
218
|
text=json.dumps(result, indent=2, ensure_ascii=False),
|
|
180
219
|
)
|
|
181
220
|
]
|
|
182
|
-
elif
|
|
221
|
+
elif sanitized_name == "format_table":
|
|
183
222
|
result = await self.table_format_tool.execute(arguments)
|
|
184
223
|
return [
|
|
185
224
|
TextContent(
|
|
@@ -187,7 +226,7 @@ class TreeSitterAnalyzerMCPServer:
|
|
|
187
226
|
text=json.dumps(result, indent=2, ensure_ascii=False),
|
|
188
227
|
)
|
|
189
228
|
]
|
|
190
|
-
elif
|
|
229
|
+
elif sanitized_name == "analyze_code_universal":
|
|
191
230
|
result = await self.universal_analyze_tool.execute(arguments)
|
|
192
231
|
return [
|
|
193
232
|
TextContent(
|
|
@@ -318,10 +357,51 @@ class TreeSitterAnalyzerMCPServer:
|
|
|
318
357
|
pass # Silently ignore logging errors during shutdown
|
|
319
358
|
|
|
320
359
|
|
|
360
|
+
def parse_mcp_args(args=None) -> argparse.Namespace:
|
|
361
|
+
"""Parse command line arguments for MCP server."""
|
|
362
|
+
parser = argparse.ArgumentParser(
|
|
363
|
+
description="Tree-sitter Analyzer MCP Server",
|
|
364
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
365
|
+
epilog="""
|
|
366
|
+
Environment Variables:
|
|
367
|
+
TREE_SITTER_PROJECT_ROOT Project root directory (alternative to --project-root)
|
|
368
|
+
|
|
369
|
+
Examples:
|
|
370
|
+
python -m tree_sitter_analyzer.mcp.server
|
|
371
|
+
python -m tree_sitter_analyzer.mcp.server --project-root /path/to/project
|
|
372
|
+
"""
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
parser.add_argument(
|
|
376
|
+
"--project-root",
|
|
377
|
+
help="Project root directory for security validation (auto-detected if not specified)"
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
return parser.parse_args(args)
|
|
381
|
+
|
|
382
|
+
|
|
321
383
|
async def main() -> None:
|
|
322
384
|
"""Main entry point for the MCP server."""
|
|
323
385
|
try:
|
|
324
|
-
|
|
386
|
+
# Parse command line arguments (empty list for testing)
|
|
387
|
+
args = parse_mcp_args([])
|
|
388
|
+
|
|
389
|
+
# Determine project root with priority handling
|
|
390
|
+
project_root = None
|
|
391
|
+
|
|
392
|
+
# Priority 1: Command line argument
|
|
393
|
+
if args.project_root:
|
|
394
|
+
project_root = args.project_root
|
|
395
|
+
# Priority 2: Environment variable
|
|
396
|
+
elif os.getenv('TREE_SITTER_PROJECT_ROOT'):
|
|
397
|
+
project_root = os.getenv('TREE_SITTER_PROJECT_ROOT')
|
|
398
|
+
# Priority 3: Auto-detection from current directory
|
|
399
|
+
else:
|
|
400
|
+
project_root = detect_project_root()
|
|
401
|
+
|
|
402
|
+
logger.info(f"MCP server starting with project root: {project_root}")
|
|
403
|
+
|
|
404
|
+
server = TreeSitterAnalyzerMCPServer(project_root)
|
|
325
405
|
await server.run()
|
|
326
406
|
except KeyboardInterrupt:
|
|
327
407
|
try:
|
|
@@ -13,6 +13,7 @@ from typing import Any
|
|
|
13
13
|
|
|
14
14
|
from ...core.analysis_engine import AnalysisRequest, get_analysis_engine
|
|
15
15
|
from ...language_detector import detect_language_from_file
|
|
16
|
+
from ...security import SecurityValidator
|
|
16
17
|
from ...utils import setup_logger
|
|
17
18
|
|
|
18
19
|
# Set up logging
|
|
@@ -28,11 +29,13 @@ class AnalyzeScaleTool:
|
|
|
28
29
|
for LLM workflow efficiency.
|
|
29
30
|
"""
|
|
30
31
|
|
|
31
|
-
def __init__(self) -> None:
|
|
32
|
+
def __init__(self, project_root: str = None) -> None:
|
|
32
33
|
"""Initialize the analyze scale tool."""
|
|
33
34
|
# Use unified analysis engine instead of deprecated AdvancedAnalyzer
|
|
34
|
-
self.
|
|
35
|
-
|
|
35
|
+
self.project_root = project_root
|
|
36
|
+
self.analysis_engine = get_analysis_engine(project_root)
|
|
37
|
+
self.security_validator = SecurityValidator(project_root)
|
|
38
|
+
logger.info("AnalyzeScaleTool initialized with security validation")
|
|
36
39
|
|
|
37
40
|
def _calculate_file_metrics(self, file_path: str) -> dict[str, Any]:
|
|
38
41
|
"""
|
|
@@ -251,6 +254,12 @@ class AnalyzeScaleTool:
|
|
|
251
254
|
if total_lines > 200:
|
|
252
255
|
guidance["recommended_tools"].append("read_code_partial")
|
|
253
256
|
|
|
257
|
+
# Ensure all required fields exist
|
|
258
|
+
required_fields = ["complexity_hotspots", "classes", "methods", "fields", "imports"]
|
|
259
|
+
for field in required_fields:
|
|
260
|
+
if field not in structural_overview:
|
|
261
|
+
structural_overview[field] = []
|
|
262
|
+
|
|
254
263
|
if len(structural_overview["complexity_hotspots"]) > 0:
|
|
255
264
|
guidance["recommended_tools"].append("format_table")
|
|
256
265
|
guidance["complexity_assessment"] = (
|
|
@@ -339,6 +348,16 @@ class AnalyzeScaleTool:
|
|
|
339
348
|
include_details = arguments.get("include_details", False)
|
|
340
349
|
include_guidance = arguments.get("include_guidance", True)
|
|
341
350
|
|
|
351
|
+
# Security validation
|
|
352
|
+
is_valid, error_msg = self.security_validator.validate_file_path(file_path)
|
|
353
|
+
if not is_valid:
|
|
354
|
+
logger.warning(f"Security validation failed for file path: {file_path} - {error_msg}")
|
|
355
|
+
raise ValueError(f"Invalid file path: {error_msg}")
|
|
356
|
+
|
|
357
|
+
# Sanitize inputs
|
|
358
|
+
if language:
|
|
359
|
+
language = self.security_validator.sanitize_input(language, max_length=50)
|
|
360
|
+
|
|
342
361
|
# Validate file exists
|
|
343
362
|
if not Path(file_path).exists():
|
|
344
363
|
raise FileNotFoundError(f"File not found: {file_path}")
|
|
@@ -11,6 +11,7 @@ from pathlib import Path
|
|
|
11
11
|
from typing import Any
|
|
12
12
|
|
|
13
13
|
from ...file_handler import read_file_partial
|
|
14
|
+
from ...security import SecurityValidator
|
|
14
15
|
from ...utils import setup_logger
|
|
15
16
|
|
|
16
17
|
# Set up logging
|
|
@@ -25,9 +26,10 @@ class ReadPartialTool:
|
|
|
25
26
|
selective file content reading through the MCP protocol.
|
|
26
27
|
"""
|
|
27
28
|
|
|
28
|
-
def __init__(self) -> None:
|
|
29
|
+
def __init__(self, project_root: str = None) -> None:
|
|
29
30
|
"""Initialize the read partial tool."""
|
|
30
|
-
|
|
31
|
+
self.security_validator = SecurityValidator(project_root)
|
|
32
|
+
logger.info("ReadPartialTool initialized with security validation")
|
|
31
33
|
|
|
32
34
|
def get_tool_schema(self) -> dict[str, Any]:
|
|
33
35
|
"""
|
|
@@ -102,6 +104,12 @@ class ReadPartialTool:
|
|
|
102
104
|
end_column = arguments.get("end_column")
|
|
103
105
|
# output_format = arguments.get("format", "text") # Not used currently
|
|
104
106
|
|
|
107
|
+
# Security validation
|
|
108
|
+
is_valid, error_msg = self.security_validator.validate_file_path(file_path)
|
|
109
|
+
if not is_valid:
|
|
110
|
+
logger.warning(f"Security validation failed for file path: {file_path} - {error_msg}")
|
|
111
|
+
raise ValueError(f"Invalid file path: {error_msg}")
|
|
112
|
+
|
|
105
113
|
# Validate file exists
|
|
106
114
|
if not Path(file_path).exists():
|
|
107
115
|
raise FileNotFoundError(f"File not found: {file_path}")
|