tree-sitter-analyzer 0.9.5__py3-none-any.whl → 0.9.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of tree-sitter-analyzer might be problematic. Click here for more details.
- tree_sitter_analyzer/__init__.py +1 -1
- tree_sitter_analyzer/cli/commands/query_command.py +50 -35
- tree_sitter_analyzer/cli_main.py +21 -0
- tree_sitter_analyzer/core/query_filter.py +200 -0
- tree_sitter_analyzer/core/query_service.py +162 -0
- tree_sitter_analyzer/mcp/resources/code_file_resource.py +1 -2
- tree_sitter_analyzer/mcp/server.py +7 -1
- tree_sitter_analyzer/mcp/tools/query_tool.py +242 -0
- tree_sitter_analyzer/mcp/utils/error_handler.py +569 -569
- tree_sitter_analyzer/queries/java.py +5 -0
- {tree_sitter_analyzer-0.9.5.dist-info → tree_sitter_analyzer-0.9.7.dist-info}/METADATA +83 -13
- {tree_sitter_analyzer-0.9.5.dist-info → tree_sitter_analyzer-0.9.7.dist-info}/RECORD +14 -11
- {tree_sitter_analyzer-0.9.5.dist-info → tree_sitter_analyzer-0.9.7.dist-info}/WHEEL +0 -0
- {tree_sitter_analyzer-0.9.5.dist-info → tree_sitter_analyzer-0.9.7.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Query Tool for MCP
|
|
4
|
+
|
|
5
|
+
MCP tool providing tree-sitter query functionality using unified QueryService.
|
|
6
|
+
Supports both predefined query keys and custom query strings.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from ...core.query_service import QueryService
|
|
13
|
+
from ...language_detector import detect_language_from_file
|
|
14
|
+
from ...security import SecurityValidator
|
|
15
|
+
from ..utils.error_handler import handle_mcp_errors
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class QueryTool:
|
|
21
|
+
"""MCP query tool providing tree-sitter query functionality"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, project_root: str | None = None) -> None:
|
|
24
|
+
"""Initialize query tool"""
|
|
25
|
+
self.project_root = project_root
|
|
26
|
+
self.query_service = QueryService(project_root)
|
|
27
|
+
self.security_validator = SecurityValidator(project_root)
|
|
28
|
+
|
|
29
|
+
def get_tool_definition(self) -> dict[str, Any]:
|
|
30
|
+
"""
|
|
31
|
+
Get MCP tool definition
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Tool definition dictionary
|
|
35
|
+
"""
|
|
36
|
+
return {
|
|
37
|
+
"name": "query_code",
|
|
38
|
+
"description": "Execute tree-sitter queries on code files to extract specific code elements",
|
|
39
|
+
"inputSchema": {
|
|
40
|
+
"type": "object",
|
|
41
|
+
"properties": {
|
|
42
|
+
"file_path": {
|
|
43
|
+
"type": "string",
|
|
44
|
+
"description": "Path to the code file to query (relative to project root)",
|
|
45
|
+
},
|
|
46
|
+
"language": {
|
|
47
|
+
"type": "string",
|
|
48
|
+
"description": "Programming language (optional, auto-detected if not provided)",
|
|
49
|
+
},
|
|
50
|
+
"query_key": {
|
|
51
|
+
"type": "string",
|
|
52
|
+
"description": "Predefined query key (e.g., 'methods', 'class', 'functions')",
|
|
53
|
+
},
|
|
54
|
+
"query_string": {
|
|
55
|
+
"type": "string",
|
|
56
|
+
"description": "Custom tree-sitter query string (e.g., '(method_declaration) @method')",
|
|
57
|
+
},
|
|
58
|
+
"filter": {
|
|
59
|
+
"type": "string",
|
|
60
|
+
"description": "Filter expression to refine results (e.g., 'name=main', 'name=~get*,public=true')",
|
|
61
|
+
},
|
|
62
|
+
"output_format": {
|
|
63
|
+
"type": "string",
|
|
64
|
+
"enum": ["json", "summary"],
|
|
65
|
+
"default": "json",
|
|
66
|
+
"description": "Output format",
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
"required": ["file_path"],
|
|
70
|
+
"anyOf": [
|
|
71
|
+
{"required": ["query_key"]},
|
|
72
|
+
{"required": ["query_string"]},
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@handle_mcp_errors("query_code")
|
|
78
|
+
async def execute(self, arguments: dict[str, Any]) -> dict[str, Any]:
|
|
79
|
+
"""
|
|
80
|
+
Execute query tool
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
arguments: Tool arguments
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Query results
|
|
87
|
+
"""
|
|
88
|
+
# Validate input parameters
|
|
89
|
+
file_path = arguments.get("file_path")
|
|
90
|
+
if not file_path:
|
|
91
|
+
raise ValueError("file_path is required")
|
|
92
|
+
|
|
93
|
+
# Security validation
|
|
94
|
+
is_valid, error_msg = self.security_validator.validate_file_path(file_path)
|
|
95
|
+
if not is_valid:
|
|
96
|
+
raise ValueError(f"Invalid or unsafe file path: {error_msg or file_path}")
|
|
97
|
+
# Use the original path as validated path after checks
|
|
98
|
+
validated_path = file_path
|
|
99
|
+
|
|
100
|
+
# Get query parameters
|
|
101
|
+
query_key = arguments.get("query_key")
|
|
102
|
+
query_string = arguments.get("query_string")
|
|
103
|
+
filter_expression = arguments.get("filter")
|
|
104
|
+
output_format = arguments.get("output_format", "json")
|
|
105
|
+
|
|
106
|
+
if not query_key and not query_string:
|
|
107
|
+
raise ValueError("Either query_key or query_string must be provided")
|
|
108
|
+
|
|
109
|
+
if query_key and query_string:
|
|
110
|
+
raise ValueError("Cannot provide both query_key and query_string")
|
|
111
|
+
|
|
112
|
+
# Detect language
|
|
113
|
+
language = arguments.get("language")
|
|
114
|
+
if not language:
|
|
115
|
+
language = detect_language_from_file(validated_path)
|
|
116
|
+
if not language:
|
|
117
|
+
raise ValueError(f"Could not detect language for file: {file_path}")
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
# Execute query
|
|
121
|
+
results = await self.query_service.execute_query(
|
|
122
|
+
validated_path, language, query_key, query_string, filter_expression
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
if not results:
|
|
126
|
+
return {
|
|
127
|
+
"success": True,
|
|
128
|
+
"message": "No results found matching the query",
|
|
129
|
+
"results": [],
|
|
130
|
+
"count": 0,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
# Format output
|
|
134
|
+
if output_format == "summary":
|
|
135
|
+
return self._format_summary(results, query_key or "custom", language)
|
|
136
|
+
else:
|
|
137
|
+
return {
|
|
138
|
+
"success": True,
|
|
139
|
+
"results": results,
|
|
140
|
+
"count": len(results),
|
|
141
|
+
"file_path": file_path,
|
|
142
|
+
"language": language,
|
|
143
|
+
"query": query_key or query_string,
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
except Exception as e:
|
|
147
|
+
logger.error(f"Query execution failed: {e}")
|
|
148
|
+
return {
|
|
149
|
+
"success": False,
|
|
150
|
+
"error": str(e),
|
|
151
|
+
"file_path": file_path,
|
|
152
|
+
"language": language,
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
def _format_summary(
|
|
156
|
+
self, results: list[dict[str, Any]], query_type: str, language: str
|
|
157
|
+
) -> dict[str, Any]:
|
|
158
|
+
"""
|
|
159
|
+
Format summary output
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
results: Query results
|
|
163
|
+
query_type: Query type
|
|
164
|
+
language: Programming language
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Summary formatted results
|
|
168
|
+
"""
|
|
169
|
+
# Group by capture name
|
|
170
|
+
by_capture = {}
|
|
171
|
+
for result in results:
|
|
172
|
+
capture_name = result["capture_name"]
|
|
173
|
+
if capture_name not in by_capture:
|
|
174
|
+
by_capture[capture_name] = []
|
|
175
|
+
by_capture[capture_name].append(result)
|
|
176
|
+
|
|
177
|
+
# Create summary
|
|
178
|
+
summary = {
|
|
179
|
+
"success": True,
|
|
180
|
+
"query_type": query_type,
|
|
181
|
+
"language": language,
|
|
182
|
+
"total_count": len(results),
|
|
183
|
+
"captures": {},
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
for capture_name, items in by_capture.items():
|
|
187
|
+
summary["captures"][capture_name] = {
|
|
188
|
+
"count": len(items),
|
|
189
|
+
"items": [
|
|
190
|
+
{
|
|
191
|
+
"name": self._extract_name_from_content(item["content"]),
|
|
192
|
+
"line_range": f"{item['start_line']}-{item['end_line']}",
|
|
193
|
+
"node_type": item["node_type"],
|
|
194
|
+
}
|
|
195
|
+
for item in items
|
|
196
|
+
],
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return summary
|
|
200
|
+
|
|
201
|
+
def _extract_name_from_content(self, content: str) -> str:
|
|
202
|
+
"""
|
|
203
|
+
Extract name from content (simple heuristic method)
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
content: Code content
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
Extracted name
|
|
210
|
+
"""
|
|
211
|
+
# Simple name extraction logic, can be improved as needed
|
|
212
|
+
lines = content.strip().split("\n")
|
|
213
|
+
if lines:
|
|
214
|
+
first_line = lines[0].strip()
|
|
215
|
+
# Extract method names, class names, etc.
|
|
216
|
+
import re
|
|
217
|
+
|
|
218
|
+
# Match common declaration patterns
|
|
219
|
+
patterns = [
|
|
220
|
+
r"(?:public|private|protected)?\s*(?:static)?\s*(?:class|interface)\s+(\w+)", # class/interface
|
|
221
|
+
r"(?:public|private|protected)?\s*(?:static)?\s*\w+\s+(\w+)\s*\(", # method
|
|
222
|
+
r"(\w+)\s*\(", # simple function call
|
|
223
|
+
]
|
|
224
|
+
|
|
225
|
+
for pattern in patterns:
|
|
226
|
+
match = re.search(pattern, first_line)
|
|
227
|
+
if match:
|
|
228
|
+
return match.group(1)
|
|
229
|
+
|
|
230
|
+
return "unnamed"
|
|
231
|
+
|
|
232
|
+
def get_available_queries(self, language: str) -> list[str]:
|
|
233
|
+
"""
|
|
234
|
+
Get available query keys
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
language: Programming language
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
List of available query keys
|
|
241
|
+
"""
|
|
242
|
+
return self.query_service.get_available_queries(language)
|