tree-sitter-analyzer 0.7.0__py3-none-any.whl → 0.8.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.

Potentially problematic release.


This version of tree-sitter-analyzer might be problematic. Click here for more details.

Files changed (70) hide show
  1. tree_sitter_analyzer/__init__.py +132 -132
  2. tree_sitter_analyzer/__main__.py +11 -11
  3. tree_sitter_analyzer/api.py +533 -533
  4. tree_sitter_analyzer/cli/__init__.py +39 -39
  5. tree_sitter_analyzer/cli/__main__.py +12 -12
  6. tree_sitter_analyzer/cli/commands/__init__.py +26 -26
  7. tree_sitter_analyzer/cli/commands/advanced_command.py +88 -88
  8. tree_sitter_analyzer/cli/commands/base_command.py +178 -160
  9. tree_sitter_analyzer/cli/commands/default_command.py +18 -18
  10. tree_sitter_analyzer/cli/commands/partial_read_command.py +141 -141
  11. tree_sitter_analyzer/cli/commands/query_command.py +88 -81
  12. tree_sitter_analyzer/cli/commands/structure_command.py +138 -138
  13. tree_sitter_analyzer/cli/commands/summary_command.py +101 -101
  14. tree_sitter_analyzer/cli/commands/table_command.py +235 -235
  15. tree_sitter_analyzer/cli/info_commands.py +121 -121
  16. tree_sitter_analyzer/cli_main.py +303 -297
  17. tree_sitter_analyzer/core/__init__.py +15 -15
  18. tree_sitter_analyzer/core/analysis_engine.py +580 -555
  19. tree_sitter_analyzer/core/cache_service.py +320 -320
  20. tree_sitter_analyzer/core/engine.py +566 -566
  21. tree_sitter_analyzer/core/parser.py +293 -293
  22. tree_sitter_analyzer/encoding_utils.py +459 -459
  23. tree_sitter_analyzer/exceptions.py +406 -337
  24. tree_sitter_analyzer/file_handler.py +210 -210
  25. tree_sitter_analyzer/formatters/__init__.py +1 -1
  26. tree_sitter_analyzer/formatters/base_formatter.py +167 -167
  27. tree_sitter_analyzer/formatters/formatter_factory.py +78 -78
  28. tree_sitter_analyzer/interfaces/__init__.py +9 -9
  29. tree_sitter_analyzer/interfaces/cli.py +528 -528
  30. tree_sitter_analyzer/interfaces/cli_adapter.py +343 -343
  31. tree_sitter_analyzer/interfaces/mcp_adapter.py +206 -206
  32. tree_sitter_analyzer/interfaces/mcp_server.py +425 -405
  33. tree_sitter_analyzer/languages/__init__.py +10 -10
  34. tree_sitter_analyzer/languages/javascript_plugin.py +446 -446
  35. tree_sitter_analyzer/languages/python_plugin.py +755 -755
  36. tree_sitter_analyzer/mcp/__init__.py +31 -31
  37. tree_sitter_analyzer/mcp/resources/__init__.py +44 -44
  38. tree_sitter_analyzer/mcp/resources/code_file_resource.py +209 -209
  39. tree_sitter_analyzer/mcp/server.py +408 -333
  40. tree_sitter_analyzer/mcp/tools/__init__.py +30 -30
  41. tree_sitter_analyzer/mcp/tools/analyze_scale_tool.py +673 -654
  42. tree_sitter_analyzer/mcp/tools/analyze_scale_tool_cli_compatible.py +247 -247
  43. tree_sitter_analyzer/mcp/tools/base_tool.py +54 -54
  44. tree_sitter_analyzer/mcp/tools/read_partial_tool.py +308 -300
  45. tree_sitter_analyzer/mcp/tools/table_format_tool.py +379 -362
  46. tree_sitter_analyzer/mcp/tools/universal_analyze_tool.py +559 -543
  47. tree_sitter_analyzer/mcp/utils/__init__.py +107 -107
  48. tree_sitter_analyzer/mcp/utils/error_handler.py +549 -549
  49. tree_sitter_analyzer/output_manager.py +253 -253
  50. tree_sitter_analyzer/plugins/__init__.py +280 -280
  51. tree_sitter_analyzer/plugins/base.py +529 -529
  52. tree_sitter_analyzer/plugins/manager.py +379 -379
  53. tree_sitter_analyzer/project_detector.py +317 -0
  54. tree_sitter_analyzer/queries/__init__.py +26 -26
  55. tree_sitter_analyzer/queries/java.py +391 -391
  56. tree_sitter_analyzer/queries/javascript.py +148 -148
  57. tree_sitter_analyzer/queries/python.py +285 -285
  58. tree_sitter_analyzer/queries/typescript.py +229 -229
  59. tree_sitter_analyzer/query_loader.py +257 -257
  60. tree_sitter_analyzer/security/__init__.py +22 -0
  61. tree_sitter_analyzer/security/boundary_manager.py +237 -0
  62. tree_sitter_analyzer/security/regex_checker.py +292 -0
  63. tree_sitter_analyzer/security/validator.py +241 -0
  64. tree_sitter_analyzer/table_formatter.py +652 -589
  65. tree_sitter_analyzer/utils.py +277 -277
  66. {tree_sitter_analyzer-0.7.0.dist-info → tree_sitter_analyzer-0.8.1.dist-info}/METADATA +27 -1
  67. tree_sitter_analyzer-0.8.1.dist-info/RECORD +77 -0
  68. tree_sitter_analyzer-0.7.0.dist-info/RECORD +0 -72
  69. {tree_sitter_analyzer-0.7.0.dist-info → tree_sitter_analyzer-0.8.1.dist-info}/WHEEL +0 -0
  70. {tree_sitter_analyzer-0.7.0.dist-info → tree_sitter_analyzer-0.8.1.dist-info}/entry_points.txt +0 -0
@@ -1,333 +1,408 @@
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 asyncio
10
- import json
11
- import sys
12
- from typing import Any
13
-
14
- try:
15
- from mcp.server import Server
16
- from mcp.server.models import InitializationOptions
17
- from mcp.server.stdio import stdio_server
18
- from mcp.types import Resource, TextContent, Tool
19
-
20
- MCP_AVAILABLE = True
21
- except ImportError:
22
- MCP_AVAILABLE = False
23
-
24
- # Fallback types for development without MCP
25
- class FallbackServer:
26
- pass
27
-
28
- class FallbackInitializationOptions:
29
- pass
30
-
31
-
32
- from ..core.analysis_engine import get_analysis_engine
33
- from ..utils import setup_logger
34
- from . import MCP_INFO
35
- from .resources import CodeFileResource, ProjectStatsResource
36
- from .tools.base_tool import MCPTool
37
- from .tools.read_partial_tool import ReadPartialTool
38
- from .tools.table_format_tool import TableFormatTool
39
- from .tools.universal_analyze_tool import UniversalAnalyzeTool
40
- from .utils.error_handler import handle_mcp_errors
41
-
42
- # Set up logging
43
- logger = setup_logger(__name__)
44
-
45
-
46
- class TreeSitterAnalyzerMCPServer:
47
- """
48
- MCP Server for Tree-sitter Analyzer
49
-
50
- Provides code analysis capabilities through the Model Context Protocol,
51
- integrating with existing analyzer components.
52
- """
53
-
54
- def __init__(self) -> None:
55
- """Initialize the MCP server with analyzer components."""
56
- self.server: Server | None = None
57
- self.analysis_engine = get_analysis_engine()
58
- # Use unified analysis engine instead of deprecated AdvancedAnalyzer
59
-
60
- # Initialize MCP tools
61
- self.read_partial_tool: MCPTool = ReadPartialTool()
62
- self.universal_analyze_tool: MCPTool = UniversalAnalyzeTool()
63
- self.table_format_tool: MCPTool = TableFormatTool()
64
-
65
- # Initialize MCP resources
66
- self.code_file_resource = CodeFileResource()
67
- self.project_stats_resource = ProjectStatsResource()
68
-
69
- # Server metadata
70
- self.name = MCP_INFO["name"]
71
- self.version = MCP_INFO["version"]
72
-
73
- logger.info(f"Initializing {self.name} v{self.version}")
74
-
75
- @handle_mcp_errors("analyze_code_scale")
76
- async def _analyze_code_scale(self, arguments: dict[str, Any]) -> dict[str, Any]:
77
- """
78
- Analyze code scale and complexity metrics by delegating to the universal_analyze_tool.
79
- """
80
- # Delegate the execution to the already initialized tool
81
- return await self.universal_analyze_tool.execute(arguments)
82
-
83
- def create_server(self) -> Server:
84
- """
85
- Create and configure the MCP server.
86
-
87
- Returns:
88
- Configured MCP Server instance
89
- """
90
- if not MCP_AVAILABLE:
91
- raise RuntimeError("MCP library not available. Please install mcp package.")
92
-
93
- server: Server = Server(self.name)
94
-
95
- # Register tools
96
- @server.list_tools() # type: ignore
97
- async def handle_list_tools() -> list[Tool]:
98
- """List available tools."""
99
- tools = [
100
- Tool(
101
- name="analyze_code_scale",
102
- description="Analyze code scale, complexity, and structure metrics",
103
- inputSchema={
104
- "type": "object",
105
- "properties": {
106
- "file_path": {
107
- "type": "string",
108
- "description": "Path to the code file to analyze",
109
- },
110
- "language": {
111
- "type": "string",
112
- "description": "Programming language (optional, auto-detected if not specified)",
113
- },
114
- "include_complexity": {
115
- "type": "boolean",
116
- "description": "Include complexity metrics in the analysis",
117
- "default": True,
118
- },
119
- "include_details": {
120
- "type": "boolean",
121
- "description": "Include detailed element information",
122
- "default": False,
123
- },
124
- },
125
- "required": ["file_path"],
126
- "additionalProperties": False,
127
- },
128
- )
129
- ]
130
-
131
- # Add tools from tool classes - FIXED VERSION
132
- for tool_instance in [
133
- self.read_partial_tool,
134
- self.table_format_tool,
135
- self.universal_analyze_tool,
136
- ]:
137
- tool_def = tool_instance.get_tool_definition()
138
- if isinstance(tool_def, dict):
139
- # Convert dict to Tool object
140
- tools.append(Tool(**tool_def))
141
- else:
142
- # Already a Tool object
143
- tools.append(tool_def)
144
-
145
- return tools
146
-
147
- @server.call_tool() # type: ignore
148
- async def handle_call_tool(
149
- name: str, arguments: dict[str, Any]
150
- ) -> list[TextContent]:
151
- """Handle tool calls."""
152
- try:
153
- if name == "analyze_code_scale":
154
- result = await self._analyze_code_scale(arguments)
155
- return [
156
- TextContent(
157
- type="text",
158
- text=json.dumps(result, indent=2, ensure_ascii=False),
159
- )
160
- ]
161
- elif name == "read_code_partial":
162
- result = await self.read_partial_tool.execute(arguments)
163
- return [
164
- TextContent(
165
- type="text",
166
- text=json.dumps(result, indent=2, ensure_ascii=False),
167
- )
168
- ]
169
- elif name == "format_table":
170
- result = await self.table_format_tool.execute(arguments)
171
- return [
172
- TextContent(
173
- type="text",
174
- text=json.dumps(result, indent=2, ensure_ascii=False),
175
- )
176
- ]
177
- elif name == "analyze_code_universal":
178
- result = await self.universal_analyze_tool.execute(arguments)
179
- return [
180
- TextContent(
181
- type="text",
182
- text=json.dumps(result, indent=2, ensure_ascii=False),
183
- )
184
- ]
185
- else:
186
- raise ValueError(f"Unknown tool: {name}")
187
-
188
- except Exception as e:
189
- try:
190
- logger.error(f"Tool call error for {name}: {e}")
191
- except (ValueError, OSError):
192
- pass # Silently ignore logging errors during shutdown
193
- return [
194
- TextContent(
195
- type="text",
196
- text=json.dumps(
197
- {"error": str(e), "tool": name, "arguments": arguments},
198
- indent=2,
199
- ),
200
- )
201
- ]
202
-
203
- # Register resources
204
- @server.list_resources() # type: ignore
205
- async def handle_list_resources() -> list[Resource]:
206
- """List available resources."""
207
- return [
208
- Resource(
209
- uri=self.code_file_resource.get_resource_info()["uri_template"],
210
- name=self.code_file_resource.get_resource_info()["name"],
211
- description=self.code_file_resource.get_resource_info()[
212
- "description"
213
- ],
214
- mimeType=self.code_file_resource.get_resource_info()["mime_type"],
215
- ),
216
- Resource(
217
- uri=self.project_stats_resource.get_resource_info()["uri_template"],
218
- name=self.project_stats_resource.get_resource_info()["name"],
219
- description=self.project_stats_resource.get_resource_info()[
220
- "description"
221
- ],
222
- mimeType=self.project_stats_resource.get_resource_info()[
223
- "mime_type"
224
- ],
225
- ),
226
- ]
227
-
228
- @server.read_resource() # type: ignore
229
- async def handle_read_resource(uri: str) -> str:
230
- """Read resource content."""
231
- try:
232
- # Check which resource matches the URI
233
- if self.code_file_resource.matches_uri(uri):
234
- return await self.code_file_resource.read_resource(uri)
235
- elif self.project_stats_resource.matches_uri(uri):
236
- return await self.project_stats_resource.read_resource(uri)
237
- else:
238
- raise ValueError(f"Resource not found: {uri}")
239
-
240
- except Exception as e:
241
- try:
242
- logger.error(f"Resource read error for {uri}: {e}")
243
- except (ValueError, OSError):
244
- pass # Silently ignore logging errors during shutdown
245
- raise
246
-
247
- self.server = server
248
- try:
249
- logger.info("MCP server created successfully")
250
- except (ValueError, OSError):
251
- pass # Silently ignore logging errors during shutdown
252
- return server
253
-
254
- def set_project_path(self, project_path: str) -> None:
255
- """
256
- Set the project path for statistics resource
257
-
258
- Args:
259
- project_path: Path to the project directory
260
- """
261
- self.project_stats_resource.set_project_path(project_path)
262
- try:
263
- logger.info(f"Set project path to: {project_path}")
264
- except (ValueError, OSError):
265
- pass # Silently ignore logging errors during shutdown
266
-
267
- async def run(self) -> None:
268
- """
269
- Run the MCP server.
270
-
271
- This method starts the server and handles stdio communication.
272
- """
273
- if not MCP_AVAILABLE:
274
- raise RuntimeError("MCP library not available. Please install mcp package.")
275
-
276
- server = self.create_server()
277
-
278
- # Initialize server options
279
- options = InitializationOptions(
280
- server_name=self.name,
281
- server_version=self.version,
282
- capabilities=MCP_INFO["capabilities"],
283
- )
284
-
285
- try:
286
- logger.info(f"Starting MCP server: {self.name} v{self.version}")
287
- except (ValueError, OSError):
288
- pass # Silently ignore logging errors during shutdown
289
-
290
- try:
291
- async with stdio_server() as (read_stream, write_stream):
292
- await server.run(read_stream, write_stream, options)
293
- except Exception as e:
294
- # Use safe logging to avoid I/O errors during shutdown
295
- try:
296
- logger.error(f"Server error: {e}")
297
- except (ValueError, OSError):
298
- pass # Silently ignore logging errors during shutdown
299
- raise
300
- finally:
301
- # Safe cleanup
302
- try:
303
- logger.info("MCP server shutting down")
304
- except (ValueError, OSError):
305
- pass # Silently ignore logging errors during shutdown
306
-
307
-
308
- async def main() -> None:
309
- """Main entry point for the MCP server."""
310
- try:
311
- server = TreeSitterAnalyzerMCPServer()
312
- await server.run()
313
- except KeyboardInterrupt:
314
- try:
315
- logger.info("Server stopped by user")
316
- except (ValueError, OSError):
317
- pass # Silently ignore logging errors during shutdown
318
- except Exception as e:
319
- try:
320
- logger.error(f"Server failed: {e}")
321
- except (ValueError, OSError):
322
- pass # Silently ignore logging errors during shutdown
323
- sys.exit(1)
324
- finally:
325
- # Ensure clean shutdown
326
- try:
327
- logger.info("MCP server shutdown complete")
328
- except (ValueError, OSError):
329
- pass # Silently ignore logging errors during shutdown
330
-
331
-
332
- if __name__ == "__main__":
333
- asyncio.run(main())
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 typing import Any
15
+
16
+ try:
17
+ from mcp.server import Server
18
+ from mcp.server.models import InitializationOptions
19
+ from mcp.server.stdio import stdio_server
20
+ from mcp.types import Resource, TextContent, Tool
21
+
22
+ MCP_AVAILABLE = True
23
+ except ImportError:
24
+ MCP_AVAILABLE = False
25
+
26
+ # Fallback types for development without MCP
27
+ class Server:
28
+ pass
29
+
30
+ class InitializationOptions:
31
+ def __init__(self, **kwargs):
32
+ pass
33
+
34
+ class Tool:
35
+ pass
36
+
37
+ class Resource:
38
+ pass
39
+
40
+ class TextContent:
41
+ pass
42
+
43
+ def stdio_server():
44
+ pass
45
+
46
+
47
+ from ..core.analysis_engine import get_analysis_engine
48
+ from ..project_detector import detect_project_root
49
+ from ..security import SecurityValidator
50
+ from ..utils import setup_logger
51
+ from . import MCP_INFO
52
+ from .resources import CodeFileResource, ProjectStatsResource
53
+ from .tools.base_tool import MCPTool
54
+ from .tools.read_partial_tool import ReadPartialTool
55
+ from .tools.table_format_tool import TableFormatTool
56
+ from .tools.universal_analyze_tool import UniversalAnalyzeTool
57
+ from .utils.error_handler import handle_mcp_errors
58
+
59
+ # Set up logging
60
+ logger = setup_logger(__name__)
61
+
62
+
63
+ class TreeSitterAnalyzerMCPServer:
64
+ """
65
+ MCP Server for Tree-sitter Analyzer
66
+
67
+ Provides code analysis capabilities through the Model Context Protocol,
68
+ integrating with existing analyzer components.
69
+ """
70
+
71
+ def __init__(self, project_root: str = None) -> None:
72
+ """Initialize the MCP server with analyzer components."""
73
+ self.server: Server | None = None
74
+ self.analysis_engine = get_analysis_engine(project_root)
75
+ self.security_validator = SecurityValidator(project_root)
76
+ # Use unified analysis engine instead of deprecated AdvancedAnalyzer
77
+
78
+ # Initialize MCP tools with security validation
79
+ self.read_partial_tool: MCPTool = ReadPartialTool(project_root)
80
+ self.universal_analyze_tool: MCPTool = UniversalAnalyzeTool(project_root)
81
+ self.table_format_tool: MCPTool = TableFormatTool(project_root)
82
+
83
+ # Initialize MCP resources
84
+ self.code_file_resource = CodeFileResource()
85
+ self.project_stats_resource = ProjectStatsResource()
86
+
87
+ # Server metadata
88
+ self.name = MCP_INFO["name"]
89
+ self.version = MCP_INFO["version"]
90
+
91
+ logger.info(f"Initializing {self.name} v{self.version}")
92
+
93
+ @handle_mcp_errors("analyze_code_scale")
94
+ async def _analyze_code_scale(self, arguments: dict[str, Any]) -> dict[str, Any]:
95
+ """
96
+ Analyze code scale and complexity metrics by delegating to the universal_analyze_tool.
97
+ """
98
+ # Delegate the execution to the already initialized tool
99
+ return await self.universal_analyze_tool.execute(arguments)
100
+
101
+ def create_server(self) -> Server:
102
+ """
103
+ Create and configure the MCP server.
104
+
105
+ Returns:
106
+ Configured MCP Server instance
107
+ """
108
+ if not MCP_AVAILABLE:
109
+ raise RuntimeError("MCP library not available. Please install mcp package.")
110
+
111
+ server: Server = Server(self.name)
112
+
113
+ # Register tools
114
+ @server.list_tools() # type: ignore
115
+ async def handle_list_tools() -> list[Tool]:
116
+ """List available tools."""
117
+ tools = [
118
+ Tool(
119
+ name="analyze_code_scale",
120
+ description="Analyze code scale, complexity, and structure metrics",
121
+ inputSchema={
122
+ "type": "object",
123
+ "properties": {
124
+ "file_path": {
125
+ "type": "string",
126
+ "description": "Path to the code file to analyze",
127
+ },
128
+ "language": {
129
+ "type": "string",
130
+ "description": "Programming language (optional, auto-detected if not specified)",
131
+ },
132
+ "include_complexity": {
133
+ "type": "boolean",
134
+ "description": "Include complexity metrics in the analysis",
135
+ "default": True,
136
+ },
137
+ "include_details": {
138
+ "type": "boolean",
139
+ "description": "Include detailed element information",
140
+ "default": False,
141
+ },
142
+ },
143
+ "required": ["file_path"],
144
+ "additionalProperties": False,
145
+ },
146
+ )
147
+ ]
148
+
149
+ # Add tools from tool classes - FIXED VERSION
150
+ for tool_instance in [
151
+ self.read_partial_tool,
152
+ self.table_format_tool,
153
+ self.universal_analyze_tool,
154
+ ]:
155
+ tool_def = tool_instance.get_tool_definition()
156
+ if isinstance(tool_def, dict):
157
+ # Convert dict to Tool object
158
+ tools.append(Tool(**tool_def))
159
+ else:
160
+ # Already a Tool object
161
+ tools.append(tool_def)
162
+
163
+ return tools
164
+
165
+ @server.call_tool() # type: ignore
166
+ async def handle_call_tool(
167
+ name: str, arguments: dict[str, Any]
168
+ ) -> list[TextContent]:
169
+ """Handle tool calls with security validation."""
170
+ try:
171
+ # Security validation for tool name
172
+ sanitized_name = self.security_validator.sanitize_input(name, max_length=100)
173
+
174
+ # Log tool call for audit
175
+ logger.info(f"MCP tool call: {sanitized_name} with args: {list(arguments.keys())}")
176
+
177
+ # Validate arguments contain no malicious content
178
+ for key, value in arguments.items():
179
+ if isinstance(value, str):
180
+ # Check for potential injection attempts
181
+ if len(value) > 10000: # Prevent extremely large inputs
182
+ raise ValueError(f"Input too large for parameter {key}")
183
+
184
+ # Basic sanitization for string inputs
185
+ sanitized_value = self.security_validator.sanitize_input(value, max_length=10000)
186
+ arguments[key] = sanitized_value
187
+ if sanitized_name == "analyze_code_scale":
188
+ result = await self._analyze_code_scale(arguments)
189
+ return [
190
+ TextContent(
191
+ type="text",
192
+ text=json.dumps(result, indent=2, ensure_ascii=False),
193
+ )
194
+ ]
195
+ elif sanitized_name == "read_code_partial":
196
+ result = await self.read_partial_tool.execute(arguments)
197
+ return [
198
+ TextContent(
199
+ type="text",
200
+ text=json.dumps(result, indent=2, ensure_ascii=False),
201
+ )
202
+ ]
203
+ elif sanitized_name == "format_table":
204
+ result = await self.table_format_tool.execute(arguments)
205
+ return [
206
+ TextContent(
207
+ type="text",
208
+ text=json.dumps(result, indent=2, ensure_ascii=False),
209
+ )
210
+ ]
211
+ elif sanitized_name == "analyze_code_universal":
212
+ result = await self.universal_analyze_tool.execute(arguments)
213
+ return [
214
+ TextContent(
215
+ type="text",
216
+ text=json.dumps(result, indent=2, ensure_ascii=False),
217
+ )
218
+ ]
219
+ else:
220
+ raise ValueError(f"Unknown tool: {name}")
221
+
222
+ except Exception as e:
223
+ try:
224
+ logger.error(f"Tool call error for {name}: {e}")
225
+ except (ValueError, OSError):
226
+ pass # Silently ignore logging errors during shutdown
227
+ return [
228
+ TextContent(
229
+ type="text",
230
+ text=json.dumps(
231
+ {"error": str(e), "tool": name, "arguments": arguments},
232
+ indent=2,
233
+ ),
234
+ )
235
+ ]
236
+
237
+ # Register resources
238
+ @server.list_resources() # type: ignore
239
+ async def handle_list_resources() -> list[Resource]:
240
+ """List available resources."""
241
+ return [
242
+ Resource(
243
+ uri=self.code_file_resource.get_resource_info()["uri_template"],
244
+ name=self.code_file_resource.get_resource_info()["name"],
245
+ description=self.code_file_resource.get_resource_info()[
246
+ "description"
247
+ ],
248
+ mimeType=self.code_file_resource.get_resource_info()["mime_type"],
249
+ ),
250
+ Resource(
251
+ uri=self.project_stats_resource.get_resource_info()["uri_template"],
252
+ name=self.project_stats_resource.get_resource_info()["name"],
253
+ description=self.project_stats_resource.get_resource_info()[
254
+ "description"
255
+ ],
256
+ mimeType=self.project_stats_resource.get_resource_info()[
257
+ "mime_type"
258
+ ],
259
+ ),
260
+ ]
261
+
262
+ @server.read_resource() # type: ignore
263
+ async def handle_read_resource(uri: str) -> str:
264
+ """Read resource content."""
265
+ try:
266
+ # Check which resource matches the URI
267
+ if self.code_file_resource.matches_uri(uri):
268
+ return await self.code_file_resource.read_resource(uri)
269
+ elif self.project_stats_resource.matches_uri(uri):
270
+ return await self.project_stats_resource.read_resource(uri)
271
+ else:
272
+ raise ValueError(f"Resource not found: {uri}")
273
+
274
+ except Exception as e:
275
+ try:
276
+ logger.error(f"Resource read error for {uri}: {e}")
277
+ except (ValueError, OSError):
278
+ pass # Silently ignore logging errors during shutdown
279
+ raise
280
+
281
+ self.server = server
282
+ try:
283
+ logger.info("MCP server created successfully")
284
+ except (ValueError, OSError):
285
+ pass # Silently ignore logging errors during shutdown
286
+ return server
287
+
288
+ def set_project_path(self, project_path: str) -> None:
289
+ """
290
+ Set the project path for statistics resource
291
+
292
+ Args:
293
+ project_path: Path to the project directory
294
+ """
295
+ self.project_stats_resource.set_project_path(project_path)
296
+ try:
297
+ logger.info(f"Set project path to: {project_path}")
298
+ except (ValueError, OSError):
299
+ pass # Silently ignore logging errors during shutdown
300
+
301
+ async def run(self) -> None:
302
+ """
303
+ Run the MCP server.
304
+
305
+ This method starts the server and handles stdio communication.
306
+ """
307
+ if not MCP_AVAILABLE:
308
+ raise RuntimeError("MCP library not available. Please install mcp package.")
309
+
310
+ server = self.create_server()
311
+
312
+ # Initialize server options
313
+ options = InitializationOptions(
314
+ server_name=self.name,
315
+ server_version=self.version,
316
+ capabilities=MCP_INFO["capabilities"],
317
+ )
318
+
319
+ try:
320
+ logger.info(f"Starting MCP server: {self.name} v{self.version}")
321
+ except (ValueError, OSError):
322
+ pass # Silently ignore logging errors during shutdown
323
+
324
+ try:
325
+ async with stdio_server() as (read_stream, write_stream):
326
+ await server.run(read_stream, write_stream, options)
327
+ except Exception as e:
328
+ # Use safe logging to avoid I/O errors during shutdown
329
+ try:
330
+ logger.error(f"Server error: {e}")
331
+ except (ValueError, OSError):
332
+ pass # Silently ignore logging errors during shutdown
333
+ raise
334
+ finally:
335
+ # Safe cleanup
336
+ try:
337
+ logger.info("MCP server shutting down")
338
+ except (ValueError, OSError):
339
+ pass # Silently ignore logging errors during shutdown
340
+
341
+
342
+ def parse_mcp_args(args=None) -> argparse.Namespace:
343
+ """Parse command line arguments for MCP server."""
344
+ parser = argparse.ArgumentParser(
345
+ description="Tree-sitter Analyzer MCP Server",
346
+ formatter_class=argparse.RawDescriptionHelpFormatter,
347
+ epilog="""
348
+ Environment Variables:
349
+ TREE_SITTER_PROJECT_ROOT Project root directory (alternative to --project-root)
350
+
351
+ Examples:
352
+ python -m tree_sitter_analyzer.mcp.server
353
+ python -m tree_sitter_analyzer.mcp.server --project-root /path/to/project
354
+ """
355
+ )
356
+
357
+ parser.add_argument(
358
+ "--project-root",
359
+ help="Project root directory for security validation (auto-detected if not specified)"
360
+ )
361
+
362
+ return parser.parse_args(args)
363
+
364
+
365
+ async def main() -> None:
366
+ """Main entry point for the MCP server."""
367
+ try:
368
+ # Parse command line arguments (empty list for testing)
369
+ args = parse_mcp_args([])
370
+
371
+ # Determine project root with priority handling
372
+ project_root = None
373
+
374
+ # Priority 1: Command line argument
375
+ if args.project_root:
376
+ project_root = args.project_root
377
+ # Priority 2: Environment variable
378
+ elif os.getenv('TREE_SITTER_PROJECT_ROOT'):
379
+ project_root = os.getenv('TREE_SITTER_PROJECT_ROOT')
380
+ # Priority 3: Auto-detection from current directory
381
+ else:
382
+ project_root = detect_project_root()
383
+
384
+ logger.info(f"MCP server starting with project root: {project_root}")
385
+
386
+ server = TreeSitterAnalyzerMCPServer(project_root)
387
+ await server.run()
388
+ except KeyboardInterrupt:
389
+ try:
390
+ logger.info("Server stopped by user")
391
+ except (ValueError, OSError):
392
+ pass # Silently ignore logging errors during shutdown
393
+ except Exception as e:
394
+ try:
395
+ logger.error(f"Server failed: {e}")
396
+ except (ValueError, OSError):
397
+ pass # Silently ignore logging errors during shutdown
398
+ sys.exit(1)
399
+ finally:
400
+ # Ensure clean shutdown
401
+ try:
402
+ logger.info("MCP server shutdown complete")
403
+ except (ValueError, OSError):
404
+ pass # Silently ignore logging errors during shutdown
405
+
406
+
407
+ if __name__ == "__main__":
408
+ asyncio.run(main())