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.

@@ -11,7 +11,7 @@ Architecture:
11
11
  - Data Models: Generic and language-specific code element representations
12
12
  """
13
13
 
14
- __version__ = "0.4.0"
14
+ __version__ = "0.8.2"
15
15
  __author__ = "aisheng.yu"
16
16
  __email__ = "aimasteracc@gmail.com"
17
17
 
@@ -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
- self.analysis_engine = get_analysis_engine()
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("ERROR: File path not specified.")
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"ERROR: File not found: {self.args.file_path}")
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
- target_language = self.args.language.lower()
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("ERROR: Failed to read file partially")
115
+ output_error("Failed to read file partially")
98
116
  return None
99
117
  except Exception as e:
100
- output_error(f"ERROR: Failed to read file partially: {e}")
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"ERROR: Analysis failed: {error_msg}")
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"ERROR: An error occurred during analysis: {e}")
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"ERROR: An error occurred during command execution: {e}")
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("ERROR: Please specify a query or --advanced option")
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("ERROR: File path not specified.")
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"ERROR: File not found: {self.args.file_path}")
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("ERROR: --start-line is required")
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("ERROR: --start-line must be 1 or greater")
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
- "ERROR: --end-line must be greater than or equal to --start-line"
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("ERROR: Failed to read file partially")
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"ERROR: Failed to read file partially: {e}")
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, self.args.query_key)
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"ERROR: Query '{self.args.query_key}' not found for language '{language}'"
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"ERROR: {e}")
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("ERROR: No query specified.")
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"ERROR: An error occurred during table format analysis: {e}")
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"ERROR: Query '{self.args.describe_query}' not found for language '{language}'"
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"ERROR: {e}")
90
+ output_error(f"{e}")
91
91
  return 1
92
92
  return 0
93
93
 
@@ -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("ERROR: --start-line is required")
210
+ output_error("--start-line is required")
205
211
  return 1
206
212
 
207
213
  if args.start_line < 1:
208
- output_error("ERROR: --start-line must be 1 or greater")
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
- "ERROR: --end-line must be greater than or equal to --start-line"
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("ERROR: --start-column must be 0 or greater")
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("ERROR: --end-column must be 0 or greater")
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("ERROR: File path not specified.")
288
+ output_error("File path not specified.")
283
289
  else:
284
- output_error("ERROR: No executable command specified.")
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
- _instance: Optional["UnifiedAnalysisEngine"] = None
208
+ _instances: Dict[str, "UnifiedAnalysisEngine"] = {}
208
209
  _lock: threading.Lock = threading.Lock()
209
210
 
210
- def __new__(cls) -> "UnifiedAnalysisEngine":
211
- """シングルトンパターンでインスタンス共有"""
212
- if cls._instance is None:
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._instance is None:
215
- cls._instance = super().__new__(cls)
216
- return cls._instance
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
- def __init__(self) -> None:
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.analysis_engine = get_analysis_engine()
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
- logger.info(f"Initializing {self.name} v{self.version}")
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
- if name == "analyze_code_scale":
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 name == "read_code_partial":
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 name == "format_table":
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 name == "analyze_code_universal":
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
- server = TreeSitterAnalyzerMCPServer()
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.analysis_engine = get_analysis_engine()
35
- logger.info("AnalyzeScaleTool initialized")
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
- logger.info("ReadPartialTool initialized")
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}")