tree-sitter-analyzer 1.9.17.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.
Files changed (149) hide show
  1. tree_sitter_analyzer/__init__.py +132 -0
  2. tree_sitter_analyzer/__main__.py +11 -0
  3. tree_sitter_analyzer/api.py +853 -0
  4. tree_sitter_analyzer/cli/__init__.py +39 -0
  5. tree_sitter_analyzer/cli/__main__.py +12 -0
  6. tree_sitter_analyzer/cli/argument_validator.py +89 -0
  7. tree_sitter_analyzer/cli/commands/__init__.py +26 -0
  8. tree_sitter_analyzer/cli/commands/advanced_command.py +226 -0
  9. tree_sitter_analyzer/cli/commands/base_command.py +181 -0
  10. tree_sitter_analyzer/cli/commands/default_command.py +18 -0
  11. tree_sitter_analyzer/cli/commands/find_and_grep_cli.py +188 -0
  12. tree_sitter_analyzer/cli/commands/list_files_cli.py +133 -0
  13. tree_sitter_analyzer/cli/commands/partial_read_command.py +139 -0
  14. tree_sitter_analyzer/cli/commands/query_command.py +109 -0
  15. tree_sitter_analyzer/cli/commands/search_content_cli.py +161 -0
  16. tree_sitter_analyzer/cli/commands/structure_command.py +156 -0
  17. tree_sitter_analyzer/cli/commands/summary_command.py +116 -0
  18. tree_sitter_analyzer/cli/commands/table_command.py +414 -0
  19. tree_sitter_analyzer/cli/info_commands.py +124 -0
  20. tree_sitter_analyzer/cli_main.py +472 -0
  21. tree_sitter_analyzer/constants.py +85 -0
  22. tree_sitter_analyzer/core/__init__.py +15 -0
  23. tree_sitter_analyzer/core/analysis_engine.py +580 -0
  24. tree_sitter_analyzer/core/cache_service.py +333 -0
  25. tree_sitter_analyzer/core/engine.py +585 -0
  26. tree_sitter_analyzer/core/parser.py +293 -0
  27. tree_sitter_analyzer/core/query.py +605 -0
  28. tree_sitter_analyzer/core/query_filter.py +200 -0
  29. tree_sitter_analyzer/core/query_service.py +340 -0
  30. tree_sitter_analyzer/encoding_utils.py +530 -0
  31. tree_sitter_analyzer/exceptions.py +747 -0
  32. tree_sitter_analyzer/file_handler.py +246 -0
  33. tree_sitter_analyzer/formatters/__init__.py +1 -0
  34. tree_sitter_analyzer/formatters/base_formatter.py +201 -0
  35. tree_sitter_analyzer/formatters/csharp_formatter.py +367 -0
  36. tree_sitter_analyzer/formatters/formatter_config.py +197 -0
  37. tree_sitter_analyzer/formatters/formatter_factory.py +84 -0
  38. tree_sitter_analyzer/formatters/formatter_registry.py +377 -0
  39. tree_sitter_analyzer/formatters/formatter_selector.py +96 -0
  40. tree_sitter_analyzer/formatters/go_formatter.py +368 -0
  41. tree_sitter_analyzer/formatters/html_formatter.py +498 -0
  42. tree_sitter_analyzer/formatters/java_formatter.py +423 -0
  43. tree_sitter_analyzer/formatters/javascript_formatter.py +611 -0
  44. tree_sitter_analyzer/formatters/kotlin_formatter.py +268 -0
  45. tree_sitter_analyzer/formatters/language_formatter_factory.py +123 -0
  46. tree_sitter_analyzer/formatters/legacy_formatter_adapters.py +228 -0
  47. tree_sitter_analyzer/formatters/markdown_formatter.py +725 -0
  48. tree_sitter_analyzer/formatters/php_formatter.py +301 -0
  49. tree_sitter_analyzer/formatters/python_formatter.py +830 -0
  50. tree_sitter_analyzer/formatters/ruby_formatter.py +278 -0
  51. tree_sitter_analyzer/formatters/rust_formatter.py +233 -0
  52. tree_sitter_analyzer/formatters/sql_formatter_wrapper.py +689 -0
  53. tree_sitter_analyzer/formatters/sql_formatters.py +536 -0
  54. tree_sitter_analyzer/formatters/typescript_formatter.py +543 -0
  55. tree_sitter_analyzer/formatters/yaml_formatter.py +462 -0
  56. tree_sitter_analyzer/interfaces/__init__.py +9 -0
  57. tree_sitter_analyzer/interfaces/cli.py +535 -0
  58. tree_sitter_analyzer/interfaces/cli_adapter.py +359 -0
  59. tree_sitter_analyzer/interfaces/mcp_adapter.py +224 -0
  60. tree_sitter_analyzer/interfaces/mcp_server.py +428 -0
  61. tree_sitter_analyzer/language_detector.py +553 -0
  62. tree_sitter_analyzer/language_loader.py +271 -0
  63. tree_sitter_analyzer/languages/__init__.py +10 -0
  64. tree_sitter_analyzer/languages/csharp_plugin.py +1076 -0
  65. tree_sitter_analyzer/languages/css_plugin.py +449 -0
  66. tree_sitter_analyzer/languages/go_plugin.py +836 -0
  67. tree_sitter_analyzer/languages/html_plugin.py +496 -0
  68. tree_sitter_analyzer/languages/java_plugin.py +1299 -0
  69. tree_sitter_analyzer/languages/javascript_plugin.py +1622 -0
  70. tree_sitter_analyzer/languages/kotlin_plugin.py +656 -0
  71. tree_sitter_analyzer/languages/markdown_plugin.py +1928 -0
  72. tree_sitter_analyzer/languages/php_plugin.py +862 -0
  73. tree_sitter_analyzer/languages/python_plugin.py +1636 -0
  74. tree_sitter_analyzer/languages/ruby_plugin.py +757 -0
  75. tree_sitter_analyzer/languages/rust_plugin.py +673 -0
  76. tree_sitter_analyzer/languages/sql_plugin.py +2444 -0
  77. tree_sitter_analyzer/languages/typescript_plugin.py +1892 -0
  78. tree_sitter_analyzer/languages/yaml_plugin.py +695 -0
  79. tree_sitter_analyzer/legacy_table_formatter.py +860 -0
  80. tree_sitter_analyzer/mcp/__init__.py +34 -0
  81. tree_sitter_analyzer/mcp/resources/__init__.py +43 -0
  82. tree_sitter_analyzer/mcp/resources/code_file_resource.py +208 -0
  83. tree_sitter_analyzer/mcp/resources/project_stats_resource.py +586 -0
  84. tree_sitter_analyzer/mcp/server.py +869 -0
  85. tree_sitter_analyzer/mcp/tools/__init__.py +28 -0
  86. tree_sitter_analyzer/mcp/tools/analyze_scale_tool.py +779 -0
  87. tree_sitter_analyzer/mcp/tools/analyze_scale_tool_cli_compatible.py +291 -0
  88. tree_sitter_analyzer/mcp/tools/base_tool.py +139 -0
  89. tree_sitter_analyzer/mcp/tools/fd_rg_utils.py +816 -0
  90. tree_sitter_analyzer/mcp/tools/find_and_grep_tool.py +686 -0
  91. tree_sitter_analyzer/mcp/tools/list_files_tool.py +413 -0
  92. tree_sitter_analyzer/mcp/tools/output_format_validator.py +148 -0
  93. tree_sitter_analyzer/mcp/tools/query_tool.py +443 -0
  94. tree_sitter_analyzer/mcp/tools/read_partial_tool.py +464 -0
  95. tree_sitter_analyzer/mcp/tools/search_content_tool.py +836 -0
  96. tree_sitter_analyzer/mcp/tools/table_format_tool.py +572 -0
  97. tree_sitter_analyzer/mcp/tools/universal_analyze_tool.py +653 -0
  98. tree_sitter_analyzer/mcp/utils/__init__.py +113 -0
  99. tree_sitter_analyzer/mcp/utils/error_handler.py +569 -0
  100. tree_sitter_analyzer/mcp/utils/file_output_factory.py +217 -0
  101. tree_sitter_analyzer/mcp/utils/file_output_manager.py +322 -0
  102. tree_sitter_analyzer/mcp/utils/gitignore_detector.py +358 -0
  103. tree_sitter_analyzer/mcp/utils/path_resolver.py +414 -0
  104. tree_sitter_analyzer/mcp/utils/search_cache.py +343 -0
  105. tree_sitter_analyzer/models.py +840 -0
  106. tree_sitter_analyzer/mypy_current_errors.txt +2 -0
  107. tree_sitter_analyzer/output_manager.py +255 -0
  108. tree_sitter_analyzer/platform_compat/__init__.py +3 -0
  109. tree_sitter_analyzer/platform_compat/adapter.py +324 -0
  110. tree_sitter_analyzer/platform_compat/compare.py +224 -0
  111. tree_sitter_analyzer/platform_compat/detector.py +67 -0
  112. tree_sitter_analyzer/platform_compat/fixtures.py +228 -0
  113. tree_sitter_analyzer/platform_compat/profiles.py +217 -0
  114. tree_sitter_analyzer/platform_compat/record.py +55 -0
  115. tree_sitter_analyzer/platform_compat/recorder.py +155 -0
  116. tree_sitter_analyzer/platform_compat/report.py +92 -0
  117. tree_sitter_analyzer/plugins/__init__.py +280 -0
  118. tree_sitter_analyzer/plugins/base.py +647 -0
  119. tree_sitter_analyzer/plugins/manager.py +384 -0
  120. tree_sitter_analyzer/project_detector.py +328 -0
  121. tree_sitter_analyzer/queries/__init__.py +27 -0
  122. tree_sitter_analyzer/queries/csharp.py +216 -0
  123. tree_sitter_analyzer/queries/css.py +615 -0
  124. tree_sitter_analyzer/queries/go.py +275 -0
  125. tree_sitter_analyzer/queries/html.py +543 -0
  126. tree_sitter_analyzer/queries/java.py +402 -0
  127. tree_sitter_analyzer/queries/javascript.py +724 -0
  128. tree_sitter_analyzer/queries/kotlin.py +192 -0
  129. tree_sitter_analyzer/queries/markdown.py +258 -0
  130. tree_sitter_analyzer/queries/php.py +95 -0
  131. tree_sitter_analyzer/queries/python.py +859 -0
  132. tree_sitter_analyzer/queries/ruby.py +92 -0
  133. tree_sitter_analyzer/queries/rust.py +223 -0
  134. tree_sitter_analyzer/queries/sql.py +555 -0
  135. tree_sitter_analyzer/queries/typescript.py +871 -0
  136. tree_sitter_analyzer/queries/yaml.py +236 -0
  137. tree_sitter_analyzer/query_loader.py +272 -0
  138. tree_sitter_analyzer/security/__init__.py +22 -0
  139. tree_sitter_analyzer/security/boundary_manager.py +277 -0
  140. tree_sitter_analyzer/security/regex_checker.py +297 -0
  141. tree_sitter_analyzer/security/validator.py +599 -0
  142. tree_sitter_analyzer/table_formatter.py +782 -0
  143. tree_sitter_analyzer/utils/__init__.py +53 -0
  144. tree_sitter_analyzer/utils/logging.py +433 -0
  145. tree_sitter_analyzer/utils/tree_sitter_compat.py +289 -0
  146. tree_sitter_analyzer-1.9.17.1.dist-info/METADATA +485 -0
  147. tree_sitter_analyzer-1.9.17.1.dist-info/RECORD +149 -0
  148. tree_sitter_analyzer-1.9.17.1.dist-info/WHEEL +4 -0
  149. tree_sitter_analyzer-1.9.17.1.dist-info/entry_points.txt +25 -0
@@ -0,0 +1,869 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ MCP Server implementation for Tree-sitter Analyzer (Fixed Version)
4
+
5
+ This module provides the main MCP server that exposes tree-sitter analyzer
6
+ functionality through the Model Context Protocol.
7
+ """
8
+
9
+ import argparse
10
+ import asyncio
11
+ import json
12
+ import os
13
+ import sys
14
+ from pathlib import Path as PathClass
15
+ from typing import Any
16
+
17
+ try:
18
+ from mcp.server import Server
19
+ from mcp.server.models import InitializationOptions
20
+ from mcp.server.stdio import stdio_server
21
+ from mcp.types import Resource, TextContent, Tool
22
+
23
+ MCP_AVAILABLE = True
24
+ except ImportError:
25
+ MCP_AVAILABLE = False
26
+
27
+ # Fallback types for development without MCP
28
+ class Server: # type: ignore
29
+ pass
30
+
31
+ class InitializationOptions: # type: ignore
32
+ def __init__(self, **kwargs: Any) -> None:
33
+ pass
34
+
35
+ class Tool: # type: ignore
36
+ pass
37
+
38
+ class Resource: # type: ignore
39
+ pass
40
+
41
+ class TextContent: # type: ignore
42
+ pass
43
+
44
+ def stdio_server() -> None: # type: ignore[misc]
45
+ pass
46
+
47
+
48
+ import contextlib
49
+
50
+ from ..constants import (
51
+ ELEMENT_TYPE_CLASS,
52
+ ELEMENT_TYPE_FUNCTION,
53
+ ELEMENT_TYPE_IMPORT,
54
+ ELEMENT_TYPE_PACKAGE,
55
+ ELEMENT_TYPE_VARIABLE,
56
+ is_element_of_type,
57
+ )
58
+ from ..core.analysis_engine import get_analysis_engine
59
+ from ..platform_compat.detector import PlatformDetector
60
+ from ..project_detector import detect_project_root
61
+ from ..security import SecurityValidator
62
+ from ..utils import setup_logger
63
+ from . import MCP_INFO
64
+ from .resources import CodeFileResource, ProjectStatsResource
65
+ from .tools.analyze_scale_tool import AnalyzeScaleTool
66
+ from .tools.find_and_grep_tool import FindAndGrepTool
67
+ from .tools.list_files_tool import ListFilesTool
68
+ from .tools.query_tool import QueryTool
69
+ from .tools.read_partial_tool import ReadPartialTool
70
+ from .tools.search_content_tool import SearchContentTool
71
+ from .tools.table_format_tool import TableFormatTool
72
+
73
+ # Import UniversalAnalyzeTool at module level for test compatibility
74
+ try:
75
+ from .tools.universal_analyze_tool import UniversalAnalyzeTool
76
+
77
+ UNIVERSAL_TOOL_AVAILABLE = True
78
+ except ImportError:
79
+ UniversalAnalyzeTool = None # type: ignore
80
+ UNIVERSAL_TOOL_AVAILABLE = False
81
+
82
+ # Set up logging
83
+ logger = setup_logger(__name__)
84
+
85
+
86
+ class TreeSitterAnalyzerMCPServer:
87
+ """
88
+ MCP Server for Tree-sitter Analyzer
89
+
90
+ Provides code analysis capabilities through the Model Context Protocol,
91
+ integrating with existing analyzer components.
92
+ """
93
+
94
+ def __init__(self, project_root: str | None = None) -> None:
95
+ """Initialize the MCP server with analyzer components."""
96
+ self.server: Server | None = None
97
+ self._initialization_complete = False
98
+
99
+ try:
100
+ logger.info("Starting MCP server initialization...")
101
+ except Exception: # nosec
102
+ # Gracefully handle logging failures during initialization
103
+ pass
104
+
105
+ self.analysis_engine = get_analysis_engine(project_root)
106
+ self.security_validator = SecurityValidator(project_root)
107
+ # Use unified analysis engine instead of deprecated AdvancedAnalyzer
108
+
109
+ # Initialize MCP tools with security validation (core tools + fd/rg tools)
110
+ self.query_tool = QueryTool(project_root) # query_code
111
+ self.read_partial_tool = ReadPartialTool(project_root) # extract_code_section
112
+ self.table_format_tool = TableFormatTool(project_root) # analyze_code_structure
113
+ self.analyze_scale_tool = AnalyzeScaleTool(project_root) # check_code_scale
114
+ # New fd/rg tools
115
+ self.list_files_tool = ListFilesTool(project_root) # list_files
116
+ self.search_content_tool = SearchContentTool(project_root) # search_content
117
+ self.find_and_grep_tool = FindAndGrepTool(project_root) # find_and_grep
118
+
119
+ # Optional universal tool to satisfy initialization tests
120
+ # Allow tests to control initialization by checking if UniversalAnalyzeTool is available
121
+ if UNIVERSAL_TOOL_AVAILABLE and UniversalAnalyzeTool is not None:
122
+ try:
123
+ self.universal_analyze_tool: UniversalAnalyzeTool | None = (
124
+ UniversalAnalyzeTool(project_root)
125
+ )
126
+ except Exception:
127
+ self.universal_analyze_tool = None
128
+ else:
129
+ self.universal_analyze_tool = None
130
+
131
+ # Initialize MCP resources
132
+ self.code_file_resource = CodeFileResource()
133
+ self.project_stats_resource = ProjectStatsResource()
134
+ # Add project_root attribute for test compatibility
135
+ self.project_stats_resource.project_root = project_root
136
+
137
+ # Server metadata
138
+ self.name = MCP_INFO["name"]
139
+ self.version = MCP_INFO["version"]
140
+
141
+ # Add platform info to version for better diagnostics
142
+ try:
143
+ platform_info = PlatformDetector.detect()
144
+ self.version = f"{self.version} ({platform_info.platform_key})"
145
+ try:
146
+ logger.info(f"Running on platform: {platform_info}")
147
+ except Exception: # nosec
148
+ pass
149
+ except Exception as e:
150
+ try:
151
+ logger.warning(f"Failed to detect platform: {e}")
152
+ except Exception: # nosec
153
+ pass
154
+
155
+ self._initialization_complete = True
156
+ try:
157
+ logger.info(
158
+ f"MCP server initialization complete: {self.name} v{self.version}"
159
+ )
160
+ except Exception: # nosec
161
+ # Gracefully handle logging failures during initialization
162
+ pass
163
+
164
+ def is_initialized(self) -> bool:
165
+ """Check if the server is fully initialized."""
166
+ return self._initialization_complete
167
+
168
+ def _ensure_initialized(self) -> None:
169
+ """Ensure the server is initialized before processing requests."""
170
+ if not self._initialization_complete:
171
+ raise RuntimeError(
172
+ "Server not fully initialized. Please wait for initialization to complete."
173
+ )
174
+
175
+ async def _analyze_code_scale(self, arguments: dict[str, Any]) -> dict[str, Any]:
176
+ """
177
+ Analyze code scale and complexity metrics using the analysis engine directly.
178
+ """
179
+ # For initialization-specific tests, we should raise MCPError instead of RuntimeError
180
+ if not self._initialization_complete:
181
+ from .utils.error_handler import MCPError
182
+
183
+ raise MCPError("Server is still initializing")
184
+
185
+ # For specific initialization tests we allow delegating to universal tool
186
+ if "file_path" not in arguments:
187
+ universal_tool = getattr(self, "universal_analyze_tool", None)
188
+ if universal_tool is not None:
189
+ try:
190
+ result = await universal_tool.execute(arguments)
191
+ return dict(result) # Ensure proper type casting
192
+ except ValueError:
193
+ # Re-raise ValueError as-is for test compatibility
194
+ raise
195
+ else:
196
+ raise ValueError("file_path is required")
197
+
198
+ file_path = arguments["file_path"]
199
+ language = arguments.get("language")
200
+ include_complexity = arguments.get("include_complexity", True)
201
+ include_details = arguments.get("include_details", False)
202
+
203
+ # Resolve relative path against project root for consistent behavior
204
+ base_root = getattr(
205
+ getattr(self.security_validator, "boundary_manager", None),
206
+ "project_root",
207
+ None,
208
+ )
209
+ if not PathClass(file_path).is_absolute() and base_root:
210
+ resolved_path = str((PathClass(base_root) / file_path).resolve())
211
+ else:
212
+ resolved_path = file_path
213
+
214
+ # Security validation
215
+ is_valid, error_msg = self.security_validator.validate_file_path(resolved_path)
216
+ if not is_valid:
217
+ raise ValueError(f"Invalid file path: {error_msg}")
218
+
219
+ # Use analysis engine directly
220
+ from ..core.analysis_engine import AnalysisRequest
221
+ from ..language_detector import detect_language_from_file
222
+
223
+ # Validate file exists
224
+ if not PathClass(resolved_path).exists():
225
+ raise FileNotFoundError(f"File not found: {file_path}")
226
+
227
+ # Detect language if not specified
228
+ if not language:
229
+ language = detect_language_from_file(resolved_path)
230
+
231
+ # Create analysis request
232
+ request = AnalysisRequest(
233
+ file_path=resolved_path,
234
+ language=language,
235
+ include_complexity=include_complexity,
236
+ include_details=include_details,
237
+ )
238
+
239
+ # Perform analysis
240
+ analysis_result = await self.analysis_engine.analyze(request)
241
+
242
+ if analysis_result is None or not analysis_result.success:
243
+ error_msg = (
244
+ analysis_result.error_message or "Unknown error"
245
+ if analysis_result
246
+ else "Unknown error"
247
+ )
248
+ raise RuntimeError(f"Failed to analyze file: {file_path} - {error_msg}")
249
+
250
+ # Get element counts from the unified elements list
251
+ elements = analysis_result.elements or []
252
+
253
+ # Count elements by type using the new unified system
254
+ classes_count = len(
255
+ [e for e in elements if is_element_of_type(e, ELEMENT_TYPE_CLASS)]
256
+ )
257
+ methods_count = len(
258
+ [e for e in elements if is_element_of_type(e, ELEMENT_TYPE_FUNCTION)]
259
+ )
260
+ fields_count = len(
261
+ [e for e in elements if is_element_of_type(e, ELEMENT_TYPE_VARIABLE)]
262
+ )
263
+ imports_count = len(
264
+ [e for e in elements if is_element_of_type(e, ELEMENT_TYPE_IMPORT)]
265
+ )
266
+ packages_count = len(
267
+ [e for e in elements if is_element_of_type(e, ELEMENT_TYPE_PACKAGE)]
268
+ )
269
+ total_elements = (
270
+ classes_count
271
+ + methods_count
272
+ + fields_count
273
+ + imports_count
274
+ + packages_count
275
+ )
276
+
277
+ # Calculate accurate file metrics including comments and blank lines
278
+ file_metrics = self._calculate_file_metrics(resolved_path, language)
279
+ lines_code = file_metrics["code_lines"]
280
+ lines_comment = file_metrics["comment_lines"]
281
+ lines_blank = file_metrics["blank_lines"]
282
+
283
+ result = {
284
+ "file_path": file_path,
285
+ "language": language,
286
+ "metrics": {
287
+ "lines_total": analysis_result.line_count,
288
+ "lines_code": lines_code,
289
+ "lines_comment": lines_comment,
290
+ "lines_blank": lines_blank,
291
+ "elements": {
292
+ "classes": classes_count,
293
+ "methods": methods_count,
294
+ "fields": fields_count,
295
+ "imports": imports_count,
296
+ "packages": packages_count,
297
+ "total": total_elements,
298
+ },
299
+ },
300
+ }
301
+
302
+ if include_complexity:
303
+ # Add complexity metrics if available
304
+ methods = [
305
+ e for e in elements if is_element_of_type(e, ELEMENT_TYPE_FUNCTION)
306
+ ]
307
+ if methods:
308
+ complexities = [getattr(m, "complexity_score", 0) for m in methods]
309
+ result["metrics"]["complexity"] = {
310
+ "total": sum(complexities),
311
+ "average": round(
312
+ sum(complexities) / len(complexities) if complexities else 0, 2
313
+ ),
314
+ "max": max(complexities) if complexities else 0,
315
+ }
316
+
317
+ if include_details:
318
+ # Convert elements to serializable format
319
+ detailed_elements = []
320
+ for elem in elements:
321
+ if hasattr(elem, "__dict__"):
322
+ detailed_elements.append(elem.__dict__)
323
+ else:
324
+ detailed_elements.append({"element": str(elem)})
325
+ result["detailed_elements"] = detailed_elements
326
+
327
+ return result
328
+
329
+ async def _read_resource(self, uri: str) -> dict[str, Any]:
330
+ """
331
+ Read a resource by URI.
332
+
333
+ Args:
334
+ uri: Resource URI to read
335
+
336
+ Returns:
337
+ Resource content
338
+
339
+ Raises:
340
+ ValueError: If URI is invalid or resource not found
341
+ """
342
+ if uri.startswith("code://file/"):
343
+ # Extract file path from URI
344
+ result = await self.code_file_resource.read_resource(uri)
345
+ return {"content": result}
346
+ elif uri.startswith("code://stats/"):
347
+ # Extract stats type from URI
348
+ result = await self.project_stats_resource.read_resource(uri)
349
+ return {"content": result}
350
+ else:
351
+ raise ValueError(f"Unknown resource URI: {uri}")
352
+
353
+ def _calculate_file_metrics(self, file_path: str, language: str) -> dict[str, Any]:
354
+ """
355
+ Calculate accurate file metrics including line counts, comments, and blank lines.
356
+
357
+ Args:
358
+ file_path: Path to the file to analyze
359
+ language: Programming language
360
+
361
+ Returns:
362
+ Dictionary containing file metrics
363
+ """
364
+ try:
365
+ from ..encoding_utils import read_file_safe
366
+
367
+ content, _ = read_file_safe(file_path)
368
+
369
+ lines = content.split("\n")
370
+ total_lines = len(lines)
371
+
372
+ # Remove empty line at the end if file ends with newline
373
+ if lines and not lines[-1]:
374
+ total_lines -= 1
375
+
376
+ # Count different types of lines
377
+ code_lines = 0
378
+ comment_lines = 0
379
+ blank_lines = 0
380
+ in_multiline_comment = False
381
+
382
+ for line in lines:
383
+ stripped = line.strip()
384
+
385
+ # Check for blank lines first
386
+ if not stripped:
387
+ blank_lines += 1
388
+ continue
389
+
390
+ # Check if we're in a multi-line comment
391
+ if in_multiline_comment:
392
+ comment_lines += 1
393
+ # Check if this line ends the multi-line comment
394
+ if "*/" in stripped:
395
+ in_multiline_comment = False
396
+ continue
397
+
398
+ # Check for multi-line comment start
399
+ if stripped.startswith("/**") or stripped.startswith("/*"):
400
+ comment_lines += 1
401
+ # Check if this line also ends the comment
402
+ if "*/" not in stripped:
403
+ in_multiline_comment = True
404
+ continue
405
+
406
+ # Check for single-line comments
407
+ if stripped.startswith("//"):
408
+ comment_lines += 1
409
+ continue
410
+
411
+ # Check for JavaDoc continuation lines (lines starting with * but not */)
412
+ if stripped.startswith("*") and not stripped.startswith("*/"):
413
+ comment_lines += 1
414
+ continue
415
+
416
+ # Check for other comment types based on language
417
+ if (
418
+ language == "python"
419
+ and stripped.startswith("#")
420
+ or language == "sql"
421
+ and stripped.startswith("--")
422
+ ):
423
+ comment_lines += 1
424
+ continue
425
+ elif language in ["html", "xml"] and stripped.startswith("<!--"):
426
+ comment_lines += 1
427
+ if "-->" not in stripped:
428
+ in_multiline_comment = True
429
+ continue
430
+
431
+ # If not a comment, it's code
432
+ code_lines += 1
433
+
434
+ # Ensure the sum equals total_lines (handle any rounding errors)
435
+ calculated_total = code_lines + comment_lines + blank_lines
436
+ if calculated_total != total_lines:
437
+ # Adjust code_lines to match total
438
+ code_lines = total_lines - comment_lines - blank_lines
439
+ # Ensure code_lines is not negative
440
+ code_lines = max(0, code_lines)
441
+
442
+ return {
443
+ "total_lines": total_lines,
444
+ "code_lines": code_lines,
445
+ "comment_lines": comment_lines,
446
+ "blank_lines": blank_lines,
447
+ }
448
+ except Exception as e:
449
+ logger.error(f"Error calculating file metrics for {file_path}: {e}")
450
+ return {
451
+ "total_lines": 0,
452
+ "code_lines": 0,
453
+ "comment_lines": 0,
454
+ "blank_lines": 0,
455
+ }
456
+
457
+ def create_server(self) -> Server:
458
+ """
459
+ Create and configure the MCP server.
460
+
461
+ Returns:
462
+ Configured MCP Server instance
463
+ """
464
+ if not MCP_AVAILABLE:
465
+ raise RuntimeError("MCP library not available. Please install mcp package.")
466
+
467
+ server: Server = Server(self.name)
468
+
469
+ # Register tools using @server decorators (standard MCP pattern)
470
+ @server.list_tools() # type: ignore[misc]
471
+ async def handle_list_tools() -> list[Tool]:
472
+ """List all available tools."""
473
+ logger.info("Client requesting tools list")
474
+
475
+ tools = [
476
+ Tool(**self.analyze_scale_tool.get_tool_definition()),
477
+ Tool(**self.table_format_tool.get_tool_definition()),
478
+ Tool(**self.read_partial_tool.get_tool_definition()),
479
+ Tool(
480
+ name="set_project_path",
481
+ description="Set or override the project root path used for security boundaries",
482
+ inputSchema={
483
+ "type": "object",
484
+ "properties": {
485
+ "project_path": {
486
+ "type": "string",
487
+ "description": "Absolute path to the project root",
488
+ }
489
+ },
490
+ "required": ["project_path"],
491
+ "additionalProperties": False,
492
+ },
493
+ ),
494
+ Tool(**self.query_tool.get_tool_definition()),
495
+ Tool(**self.list_files_tool.get_tool_definition()),
496
+ Tool(**self.search_content_tool.get_tool_definition()),
497
+ Tool(**self.find_and_grep_tool.get_tool_definition()),
498
+ ]
499
+
500
+ logger.info(f"Returning {len(tools)} tools: {[t.name for t in tools]}")
501
+ return tools
502
+
503
+ @server.call_tool() # type: ignore[misc]
504
+ async def handle_call_tool(
505
+ name: str, arguments: dict[str, Any]
506
+ ) -> list[TextContent]:
507
+ try:
508
+ # Ensure server is fully initialized
509
+ self._ensure_initialized()
510
+
511
+ # Log tool call
512
+ logger.info(
513
+ f"MCP tool call: {name} with args: {list(arguments.keys())}"
514
+ )
515
+
516
+ # Validate file path security
517
+ if "file_path" in arguments:
518
+ file_path = arguments["file_path"]
519
+ is_valid, error_msg = self.security_validator.validate_file_path(
520
+ file_path
521
+ )
522
+ if not is_valid:
523
+ raise ValueError(
524
+ f"Invalid or unsafe file path: {error_msg or file_path}"
525
+ )
526
+
527
+ # Handle tool calls with simplified parameter handling
528
+ if name == "check_code_scale":
529
+ # Ensure file_path is provided
530
+ if "file_path" not in arguments:
531
+ raise ValueError("file_path parameter is required")
532
+
533
+ # Use the original _analyze_code_scale method for backward compatibility
534
+ result = await self._analyze_code_scale(arguments)
535
+
536
+ elif name == "analyze_code_structure":
537
+ if "file_path" not in arguments:
538
+ raise ValueError("file_path parameter is required")
539
+
540
+ full_args = {
541
+ "file_path": arguments["file_path"],
542
+ "format_type": arguments.get("format_type", "full"),
543
+ "language": arguments.get("language"),
544
+ "output_file": arguments.get("output_file"),
545
+ "suppress_output": arguments.get("suppress_output", False),
546
+ }
547
+ result = await self.table_format_tool.execute(full_args)
548
+
549
+ elif name == "extract_code_section":
550
+ if "file_path" not in arguments or "start_line" not in arguments:
551
+ raise ValueError(
552
+ "file_path and start_line parameters are required"
553
+ )
554
+
555
+ full_args = {
556
+ "file_path": arguments["file_path"],
557
+ "start_line": arguments["start_line"],
558
+ "end_line": arguments.get("end_line"),
559
+ "start_column": arguments.get("start_column"),
560
+ "end_column": arguments.get("end_column"),
561
+ "format": arguments.get("format", "text"),
562
+ "output_file": arguments.get("output_file"),
563
+ "suppress_output": arguments.get("suppress_output", False),
564
+ }
565
+ result = await self.read_partial_tool.execute(full_args)
566
+
567
+ elif name == "set_project_path":
568
+ project_path = arguments.get("project_path")
569
+ if not project_path or not isinstance(project_path, str):
570
+ raise ValueError(
571
+ "project_path parameter is required and must be a string"
572
+ )
573
+ if not PathClass(project_path).is_dir():
574
+ raise ValueError(f"Project path does not exist: {project_path}")
575
+ self.set_project_path(project_path)
576
+ result = {"status": "success", "project_root": project_path}
577
+
578
+ elif name == "query_code":
579
+ result = await self.query_tool.execute(arguments)
580
+
581
+ elif name == "list_files":
582
+ result = await self.list_files_tool.execute(arguments)
583
+
584
+ elif name == "search_content":
585
+ result = await self.search_content_tool.execute(arguments)
586
+
587
+ elif name == "find_and_grep":
588
+ result = await self.find_and_grep_tool.execute(arguments)
589
+
590
+ else:
591
+ raise ValueError(f"Unknown tool: {name}")
592
+
593
+ # Return result
594
+ return [
595
+ TextContent(
596
+ type="text",
597
+ text=json.dumps(result, indent=2, ensure_ascii=False),
598
+ )
599
+ ]
600
+
601
+ except Exception as e:
602
+ try:
603
+ logger.error(f"Tool call error for {name}: {e}")
604
+ except (ValueError, OSError):
605
+ pass # Silently ignore logging errors during shutdown
606
+ return [
607
+ TextContent(
608
+ type="text",
609
+ text=json.dumps(
610
+ {"error": str(e), "tool": name, "arguments": arguments},
611
+ indent=2,
612
+ ),
613
+ )
614
+ ]
615
+
616
+ # Register resources
617
+ @server.list_resources() # type: ignore
618
+ async def handle_list_resources() -> list[Resource]:
619
+ """List available resources."""
620
+ return [
621
+ Resource(
622
+ uri=self.code_file_resource.get_resource_info()["uri_template"],
623
+ name=self.code_file_resource.get_resource_info()["name"],
624
+ description=self.code_file_resource.get_resource_info()[
625
+ "description"
626
+ ],
627
+ mimeType=self.code_file_resource.get_resource_info()["mime_type"],
628
+ ),
629
+ Resource(
630
+ uri=self.project_stats_resource.get_resource_info()["uri_template"],
631
+ name=self.project_stats_resource.get_resource_info()["name"],
632
+ description=self.project_stats_resource.get_resource_info()[
633
+ "description"
634
+ ],
635
+ mimeType=self.project_stats_resource.get_resource_info()[
636
+ "mime_type"
637
+ ],
638
+ ),
639
+ ]
640
+
641
+ @server.read_resource() # type: ignore
642
+ async def handle_read_resource(uri: str) -> str:
643
+ """Read resource content."""
644
+ try:
645
+ # Check which resource matches the URI
646
+ if self.code_file_resource.matches_uri(uri):
647
+ return await self.code_file_resource.read_resource(uri)
648
+ elif self.project_stats_resource.matches_uri(uri):
649
+ return await self.project_stats_resource.read_resource(uri)
650
+ else:
651
+ raise ValueError(f"Resource not found: {uri}")
652
+
653
+ except Exception as e:
654
+ try:
655
+ logger.error(f"Resource read error for {uri}: {e}")
656
+ except (ValueError, OSError):
657
+ pass # Silently ignore logging errors during shutdown
658
+ raise
659
+
660
+ # Some clients may request prompts; explicitly return empty list
661
+ # Some clients may request prompts; explicitly return empty list
662
+ try:
663
+ from mcp.types import Prompt
664
+
665
+ @server.list_prompts() # type: ignore
666
+ async def handle_list_prompts() -> list[Prompt]:
667
+ logger.info("Client requested prompts list (returning empty)")
668
+ return []
669
+
670
+ except Exception as e:
671
+ # If Prompt type is unavailable, log at debug level and continue safely
672
+ with contextlib.suppress(ValueError, OSError):
673
+ logger.debug(f"Prompts API unavailable or incompatible: {e}")
674
+
675
+ self.server = server
676
+ try:
677
+ logger.info("MCP server created successfully")
678
+ except (ValueError, OSError):
679
+ pass # Silently ignore logging errors during shutdown
680
+ return server
681
+
682
+ def set_project_path(self, project_path: str) -> None:
683
+ """
684
+ Set the project path for all components
685
+
686
+ Args:
687
+ project_path: Path to the project directory
688
+ """
689
+ # Update project stats resource
690
+ self.project_stats_resource.set_project_path(project_path)
691
+
692
+ # Update all MCP tools (all inherit from BaseMCPTool)
693
+ self.query_tool.set_project_path(project_path)
694
+ self.read_partial_tool.set_project_path(project_path)
695
+ self.table_format_tool.set_project_path(project_path)
696
+ self.analyze_scale_tool.set_project_path(project_path)
697
+ self.list_files_tool.set_project_path(project_path)
698
+ self.search_content_tool.set_project_path(project_path)
699
+ self.find_and_grep_tool.set_project_path(project_path)
700
+
701
+ # Update universal tool if available
702
+ if hasattr(self, "universal_analyze_tool") and self.universal_analyze_tool:
703
+ self.universal_analyze_tool.set_project_path(project_path)
704
+
705
+ # Update analysis engine and security validator
706
+ self.analysis_engine = get_analysis_engine(project_path)
707
+ self.security_validator = SecurityValidator(project_path)
708
+
709
+ try:
710
+ logger.info(f"Set project path to: {project_path}")
711
+ except (ValueError, OSError):
712
+ pass # Silently ignore logging errors during shutdown
713
+
714
+ async def run(self) -> None:
715
+ """
716
+ Run the MCP server.
717
+
718
+ This method starts the server and handles stdio communication.
719
+ """
720
+ if not MCP_AVAILABLE:
721
+ raise RuntimeError("MCP library not available. Please install mcp package.")
722
+
723
+ server = self.create_server()
724
+
725
+ # Initialize server options with required capabilities field
726
+ from mcp.server.models import ServerCapabilities
727
+ from mcp.types import (
728
+ LoggingCapability,
729
+ PromptsCapability,
730
+ ResourcesCapability,
731
+ ToolsCapability,
732
+ )
733
+
734
+ capabilities = ServerCapabilities(
735
+ tools=ToolsCapability(listChanged=True),
736
+ resources=ResourcesCapability(subscribe=True, listChanged=True),
737
+ prompts=PromptsCapability(listChanged=True),
738
+ logging=LoggingCapability(),
739
+ )
740
+
741
+ options = InitializationOptions(
742
+ server_name=self.name,
743
+ server_version=self.version,
744
+ capabilities=capabilities,
745
+ )
746
+
747
+ try:
748
+ logger.info(f"Starting MCP server: {self.name} v{self.version}")
749
+ except (ValueError, OSError):
750
+ pass # Silently ignore logging errors during shutdown
751
+
752
+ try:
753
+ async with stdio_server() as (read_stream, write_stream):
754
+ logger.info("Server running, waiting for requests...")
755
+ await server.run(read_stream, write_stream, options)
756
+ except Exception as e:
757
+ # Use safe logging to avoid I/O errors during shutdown
758
+ try:
759
+ logger.error(f"Server error: {e}")
760
+ except (ValueError, OSError):
761
+ pass # Silently ignore logging errors during shutdown
762
+ raise
763
+ finally:
764
+ # Safe cleanup
765
+ try:
766
+ logger.info("MCP server shutting down")
767
+ except (ValueError, OSError):
768
+ pass # Silently ignore logging errors during shutdown
769
+
770
+
771
+ def parse_mcp_args(args: list[str] | None = None) -> argparse.Namespace:
772
+ """Parse command line arguments for MCP server."""
773
+ parser = argparse.ArgumentParser(
774
+ description="Tree-sitter Analyzer MCP Server",
775
+ formatter_class=argparse.RawDescriptionHelpFormatter,
776
+ epilog="""
777
+ Environment Variables:
778
+ TREE_SITTER_PROJECT_ROOT Project root directory (alternative to --project-root)
779
+
780
+ Examples:
781
+ python -m tree_sitter_analyzer.mcp.server
782
+ python -m tree_sitter_analyzer.mcp.server --project-root /path/to/project
783
+ """,
784
+ )
785
+
786
+ parser.add_argument(
787
+ "--project-root",
788
+ help="Project root directory for security validation (auto-detected if not specified)",
789
+ )
790
+
791
+ return parser.parse_args(args)
792
+
793
+
794
+ async def main() -> None:
795
+ """Main entry point for the MCP server."""
796
+ try:
797
+ # Parse command line arguments (ignore unknown so pytest flags won't crash)
798
+ args = parse_mcp_args([] if "pytest" in sys.argv[0] else None)
799
+
800
+ # Determine project root with robust priority handling and fallbacks
801
+ project_root = None
802
+
803
+ # Priority 1: Command line argument
804
+ if args.project_root:
805
+ project_root = args.project_root
806
+ # Priority 2: Environment variable
807
+ elif (
808
+ PathClass.cwd()
809
+ .joinpath(os.environ.get("TREE_SITTER_PROJECT_ROOT", ""))
810
+ .exists()
811
+ ):
812
+ project_root = os.environ.get("TREE_SITTER_PROJECT_ROOT")
813
+ # Priority 3: Auto-detection from current directory
814
+ else:
815
+ project_root = detect_project_root()
816
+
817
+ # Handle unresolved placeholders from clients (e.g., "${workspaceFolder}")
818
+ invalid_placeholder = isinstance(project_root, str) and (
819
+ "${" in project_root or "}" in project_root or "$" in project_root
820
+ )
821
+
822
+ # Validate existence; if invalid, fall back to current working directory
823
+ if (
824
+ not project_root
825
+ or invalid_placeholder
826
+ or (isinstance(project_root, str) and not PathClass(project_root).is_dir())
827
+ ):
828
+ # Use current working directory as final fallback
829
+ fallback_root = str(PathClass.cwd())
830
+ with contextlib.suppress(ValueError, OSError):
831
+ logger.warning(
832
+ f"Invalid project root '{project_root}', falling back to current directory: {fallback_root}"
833
+ )
834
+ project_root = fallback_root
835
+
836
+ logger.info(f"MCP server starting with project root: {project_root}")
837
+
838
+ server = TreeSitterAnalyzerMCPServer(project_root)
839
+ await server.run()
840
+
841
+ # Exit successfully after server run completes
842
+ sys.exit(0)
843
+ except KeyboardInterrupt:
844
+ try:
845
+ logger.info("Server stopped by user")
846
+ except (ValueError, OSError):
847
+ pass # Silently ignore logging errors during shutdown
848
+ sys.exit(0)
849
+ except Exception as e:
850
+ try:
851
+ logger.error(f"Server failed: {e}")
852
+ except (ValueError, OSError):
853
+ pass # Silently ignore logging errors during shutdown
854
+ sys.exit(1)
855
+ finally:
856
+ # Ensure clean shutdown
857
+ try:
858
+ logger.info("MCP server shutdown complete")
859
+ except (ValueError, OSError):
860
+ pass # Silently ignore logging errors during shutdown
861
+
862
+
863
+ def main_sync() -> None:
864
+ """Synchronous entry point for setuptools scripts."""
865
+ asyncio.run(main())
866
+
867
+
868
+ if __name__ == "__main__":
869
+ main_sync()