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.
- tree_sitter_analyzer/__init__.py +132 -0
- tree_sitter_analyzer/__main__.py +11 -0
- tree_sitter_analyzer/api.py +853 -0
- tree_sitter_analyzer/cli/__init__.py +39 -0
- tree_sitter_analyzer/cli/__main__.py +12 -0
- tree_sitter_analyzer/cli/argument_validator.py +89 -0
- tree_sitter_analyzer/cli/commands/__init__.py +26 -0
- tree_sitter_analyzer/cli/commands/advanced_command.py +226 -0
- tree_sitter_analyzer/cli/commands/base_command.py +181 -0
- tree_sitter_analyzer/cli/commands/default_command.py +18 -0
- tree_sitter_analyzer/cli/commands/find_and_grep_cli.py +188 -0
- tree_sitter_analyzer/cli/commands/list_files_cli.py +133 -0
- tree_sitter_analyzer/cli/commands/partial_read_command.py +139 -0
- tree_sitter_analyzer/cli/commands/query_command.py +109 -0
- tree_sitter_analyzer/cli/commands/search_content_cli.py +161 -0
- tree_sitter_analyzer/cli/commands/structure_command.py +156 -0
- tree_sitter_analyzer/cli/commands/summary_command.py +116 -0
- tree_sitter_analyzer/cli/commands/table_command.py +414 -0
- tree_sitter_analyzer/cli/info_commands.py +124 -0
- tree_sitter_analyzer/cli_main.py +472 -0
- tree_sitter_analyzer/constants.py +85 -0
- tree_sitter_analyzer/core/__init__.py +15 -0
- tree_sitter_analyzer/core/analysis_engine.py +580 -0
- tree_sitter_analyzer/core/cache_service.py +333 -0
- tree_sitter_analyzer/core/engine.py +585 -0
- tree_sitter_analyzer/core/parser.py +293 -0
- tree_sitter_analyzer/core/query.py +605 -0
- tree_sitter_analyzer/core/query_filter.py +200 -0
- tree_sitter_analyzer/core/query_service.py +340 -0
- tree_sitter_analyzer/encoding_utils.py +530 -0
- tree_sitter_analyzer/exceptions.py +747 -0
- tree_sitter_analyzer/file_handler.py +246 -0
- tree_sitter_analyzer/formatters/__init__.py +1 -0
- tree_sitter_analyzer/formatters/base_formatter.py +201 -0
- tree_sitter_analyzer/formatters/csharp_formatter.py +367 -0
- tree_sitter_analyzer/formatters/formatter_config.py +197 -0
- tree_sitter_analyzer/formatters/formatter_factory.py +84 -0
- tree_sitter_analyzer/formatters/formatter_registry.py +377 -0
- tree_sitter_analyzer/formatters/formatter_selector.py +96 -0
- tree_sitter_analyzer/formatters/go_formatter.py +368 -0
- tree_sitter_analyzer/formatters/html_formatter.py +498 -0
- tree_sitter_analyzer/formatters/java_formatter.py +423 -0
- tree_sitter_analyzer/formatters/javascript_formatter.py +611 -0
- tree_sitter_analyzer/formatters/kotlin_formatter.py +268 -0
- tree_sitter_analyzer/formatters/language_formatter_factory.py +123 -0
- tree_sitter_analyzer/formatters/legacy_formatter_adapters.py +228 -0
- tree_sitter_analyzer/formatters/markdown_formatter.py +725 -0
- tree_sitter_analyzer/formatters/php_formatter.py +301 -0
- tree_sitter_analyzer/formatters/python_formatter.py +830 -0
- tree_sitter_analyzer/formatters/ruby_formatter.py +278 -0
- tree_sitter_analyzer/formatters/rust_formatter.py +233 -0
- tree_sitter_analyzer/formatters/sql_formatter_wrapper.py +689 -0
- tree_sitter_analyzer/formatters/sql_formatters.py +536 -0
- tree_sitter_analyzer/formatters/typescript_formatter.py +543 -0
- tree_sitter_analyzer/formatters/yaml_formatter.py +462 -0
- tree_sitter_analyzer/interfaces/__init__.py +9 -0
- tree_sitter_analyzer/interfaces/cli.py +535 -0
- tree_sitter_analyzer/interfaces/cli_adapter.py +359 -0
- tree_sitter_analyzer/interfaces/mcp_adapter.py +224 -0
- tree_sitter_analyzer/interfaces/mcp_server.py +428 -0
- tree_sitter_analyzer/language_detector.py +553 -0
- tree_sitter_analyzer/language_loader.py +271 -0
- tree_sitter_analyzer/languages/__init__.py +10 -0
- tree_sitter_analyzer/languages/csharp_plugin.py +1076 -0
- tree_sitter_analyzer/languages/css_plugin.py +449 -0
- tree_sitter_analyzer/languages/go_plugin.py +836 -0
- tree_sitter_analyzer/languages/html_plugin.py +496 -0
- tree_sitter_analyzer/languages/java_plugin.py +1299 -0
- tree_sitter_analyzer/languages/javascript_plugin.py +1622 -0
- tree_sitter_analyzer/languages/kotlin_plugin.py +656 -0
- tree_sitter_analyzer/languages/markdown_plugin.py +1928 -0
- tree_sitter_analyzer/languages/php_plugin.py +862 -0
- tree_sitter_analyzer/languages/python_plugin.py +1636 -0
- tree_sitter_analyzer/languages/ruby_plugin.py +757 -0
- tree_sitter_analyzer/languages/rust_plugin.py +673 -0
- tree_sitter_analyzer/languages/sql_plugin.py +2444 -0
- tree_sitter_analyzer/languages/typescript_plugin.py +1892 -0
- tree_sitter_analyzer/languages/yaml_plugin.py +695 -0
- tree_sitter_analyzer/legacy_table_formatter.py +860 -0
- tree_sitter_analyzer/mcp/__init__.py +34 -0
- tree_sitter_analyzer/mcp/resources/__init__.py +43 -0
- tree_sitter_analyzer/mcp/resources/code_file_resource.py +208 -0
- tree_sitter_analyzer/mcp/resources/project_stats_resource.py +586 -0
- tree_sitter_analyzer/mcp/server.py +869 -0
- tree_sitter_analyzer/mcp/tools/__init__.py +28 -0
- tree_sitter_analyzer/mcp/tools/analyze_scale_tool.py +779 -0
- tree_sitter_analyzer/mcp/tools/analyze_scale_tool_cli_compatible.py +291 -0
- tree_sitter_analyzer/mcp/tools/base_tool.py +139 -0
- tree_sitter_analyzer/mcp/tools/fd_rg_utils.py +816 -0
- tree_sitter_analyzer/mcp/tools/find_and_grep_tool.py +686 -0
- tree_sitter_analyzer/mcp/tools/list_files_tool.py +413 -0
- tree_sitter_analyzer/mcp/tools/output_format_validator.py +148 -0
- tree_sitter_analyzer/mcp/tools/query_tool.py +443 -0
- tree_sitter_analyzer/mcp/tools/read_partial_tool.py +464 -0
- tree_sitter_analyzer/mcp/tools/search_content_tool.py +836 -0
- tree_sitter_analyzer/mcp/tools/table_format_tool.py +572 -0
- tree_sitter_analyzer/mcp/tools/universal_analyze_tool.py +653 -0
- tree_sitter_analyzer/mcp/utils/__init__.py +113 -0
- tree_sitter_analyzer/mcp/utils/error_handler.py +569 -0
- tree_sitter_analyzer/mcp/utils/file_output_factory.py +217 -0
- tree_sitter_analyzer/mcp/utils/file_output_manager.py +322 -0
- tree_sitter_analyzer/mcp/utils/gitignore_detector.py +358 -0
- tree_sitter_analyzer/mcp/utils/path_resolver.py +414 -0
- tree_sitter_analyzer/mcp/utils/search_cache.py +343 -0
- tree_sitter_analyzer/models.py +840 -0
- tree_sitter_analyzer/mypy_current_errors.txt +2 -0
- tree_sitter_analyzer/output_manager.py +255 -0
- tree_sitter_analyzer/platform_compat/__init__.py +3 -0
- tree_sitter_analyzer/platform_compat/adapter.py +324 -0
- tree_sitter_analyzer/platform_compat/compare.py +224 -0
- tree_sitter_analyzer/platform_compat/detector.py +67 -0
- tree_sitter_analyzer/platform_compat/fixtures.py +228 -0
- tree_sitter_analyzer/platform_compat/profiles.py +217 -0
- tree_sitter_analyzer/platform_compat/record.py +55 -0
- tree_sitter_analyzer/platform_compat/recorder.py +155 -0
- tree_sitter_analyzer/platform_compat/report.py +92 -0
- tree_sitter_analyzer/plugins/__init__.py +280 -0
- tree_sitter_analyzer/plugins/base.py +647 -0
- tree_sitter_analyzer/plugins/manager.py +384 -0
- tree_sitter_analyzer/project_detector.py +328 -0
- tree_sitter_analyzer/queries/__init__.py +27 -0
- tree_sitter_analyzer/queries/csharp.py +216 -0
- tree_sitter_analyzer/queries/css.py +615 -0
- tree_sitter_analyzer/queries/go.py +275 -0
- tree_sitter_analyzer/queries/html.py +543 -0
- tree_sitter_analyzer/queries/java.py +402 -0
- tree_sitter_analyzer/queries/javascript.py +724 -0
- tree_sitter_analyzer/queries/kotlin.py +192 -0
- tree_sitter_analyzer/queries/markdown.py +258 -0
- tree_sitter_analyzer/queries/php.py +95 -0
- tree_sitter_analyzer/queries/python.py +859 -0
- tree_sitter_analyzer/queries/ruby.py +92 -0
- tree_sitter_analyzer/queries/rust.py +223 -0
- tree_sitter_analyzer/queries/sql.py +555 -0
- tree_sitter_analyzer/queries/typescript.py +871 -0
- tree_sitter_analyzer/queries/yaml.py +236 -0
- tree_sitter_analyzer/query_loader.py +272 -0
- tree_sitter_analyzer/security/__init__.py +22 -0
- tree_sitter_analyzer/security/boundary_manager.py +277 -0
- tree_sitter_analyzer/security/regex_checker.py +297 -0
- tree_sitter_analyzer/security/validator.py +599 -0
- tree_sitter_analyzer/table_formatter.py +782 -0
- tree_sitter_analyzer/utils/__init__.py +53 -0
- tree_sitter_analyzer/utils/logging.py +433 -0
- tree_sitter_analyzer/utils/tree_sitter_compat.py +289 -0
- tree_sitter_analyzer-1.9.17.1.dist-info/METADATA +485 -0
- tree_sitter_analyzer-1.9.17.1.dist-info/RECORD +149 -0
- tree_sitter_analyzer-1.9.17.1.dist-info/WHEEL +4 -0
- 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()
|