hanzo-mcp 0.9.0__py3-none-any.whl → 0.9.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 hanzo-mcp might be problematic. Click here for more details.

Files changed (135) hide show
  1. hanzo_mcp/__init__.py +1 -1
  2. hanzo_mcp/analytics/posthog_analytics.py +14 -1
  3. hanzo_mcp/cli.py +108 -4
  4. hanzo_mcp/server.py +11 -0
  5. hanzo_mcp/tools/__init__.py +3 -16
  6. hanzo_mcp/tools/agent/__init__.py +5 -0
  7. hanzo_mcp/tools/agent/agent.py +5 -0
  8. hanzo_mcp/tools/agent/agent_tool.py +3 -17
  9. hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +623 -0
  10. hanzo_mcp/tools/agent/clarification_tool.py +7 -1
  11. hanzo_mcp/tools/agent/claude_desktop_auth.py +16 -6
  12. hanzo_mcp/tools/agent/cli_agent_base.py +5 -0
  13. hanzo_mcp/tools/agent/cli_tools.py +26 -0
  14. hanzo_mcp/tools/agent/code_auth_tool.py +5 -0
  15. hanzo_mcp/tools/agent/critic_tool.py +7 -1
  16. hanzo_mcp/tools/agent/iching_tool.py +5 -0
  17. hanzo_mcp/tools/agent/network_tool.py +5 -0
  18. hanzo_mcp/tools/agent/review_tool.py +7 -1
  19. hanzo_mcp/tools/agent/swarm_alias.py +5 -0
  20. hanzo_mcp/tools/agent/swarm_tool.py +701 -0
  21. hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +554 -0
  22. hanzo_mcp/tools/agent/unified_cli_tools.py +5 -0
  23. hanzo_mcp/tools/common/auto_timeout.py +234 -0
  24. hanzo_mcp/tools/common/base.py +4 -0
  25. hanzo_mcp/tools/common/batch_tool.py +5 -0
  26. hanzo_mcp/tools/common/config_tool.py +5 -0
  27. hanzo_mcp/tools/common/critic_tool.py +5 -0
  28. hanzo_mcp/tools/common/paginated_base.py +4 -0
  29. hanzo_mcp/tools/common/permissions.py +38 -12
  30. hanzo_mcp/tools/common/personality.py +673 -980
  31. hanzo_mcp/tools/common/stats.py +5 -0
  32. hanzo_mcp/tools/common/thinking_tool.py +5 -0
  33. hanzo_mcp/tools/common/timeout_parser.py +103 -0
  34. hanzo_mcp/tools/common/tool_disable.py +5 -0
  35. hanzo_mcp/tools/common/tool_enable.py +5 -0
  36. hanzo_mcp/tools/common/tool_list.py +5 -0
  37. hanzo_mcp/tools/config/config_tool.py +5 -0
  38. hanzo_mcp/tools/config/mode_tool.py +5 -0
  39. hanzo_mcp/tools/database/graph.py +5 -0
  40. hanzo_mcp/tools/database/graph_add.py +5 -0
  41. hanzo_mcp/tools/database/graph_query.py +5 -0
  42. hanzo_mcp/tools/database/graph_remove.py +5 -0
  43. hanzo_mcp/tools/database/graph_search.py +5 -0
  44. hanzo_mcp/tools/database/graph_stats.py +5 -0
  45. hanzo_mcp/tools/database/sql.py +5 -0
  46. hanzo_mcp/tools/database/sql_query.py +2 -0
  47. hanzo_mcp/tools/database/sql_search.py +5 -0
  48. hanzo_mcp/tools/database/sql_stats.py +5 -0
  49. hanzo_mcp/tools/editor/neovim_command.py +5 -0
  50. hanzo_mcp/tools/editor/neovim_edit.py +7 -2
  51. hanzo_mcp/tools/editor/neovim_session.py +5 -0
  52. hanzo_mcp/tools/filesystem/__init__.py +23 -26
  53. hanzo_mcp/tools/filesystem/ast_tool.py +2 -3
  54. hanzo_mcp/tools/filesystem/base.py +0 -16
  55. hanzo_mcp/tools/filesystem/batch_search.py +825 -0
  56. hanzo_mcp/tools/filesystem/content_replace.py +5 -3
  57. hanzo_mcp/tools/filesystem/diff.py +5 -0
  58. hanzo_mcp/tools/filesystem/directory_tree.py +34 -281
  59. hanzo_mcp/tools/filesystem/directory_tree_paginated.py +345 -0
  60. hanzo_mcp/tools/filesystem/edit.py +5 -4
  61. hanzo_mcp/tools/filesystem/find.py +177 -311
  62. hanzo_mcp/tools/filesystem/find_files.py +370 -0
  63. hanzo_mcp/tools/filesystem/git_search.py +5 -3
  64. hanzo_mcp/tools/filesystem/grep.py +454 -0
  65. hanzo_mcp/tools/filesystem/multi_edit.py +5 -4
  66. hanzo_mcp/tools/filesystem/read.py +11 -8
  67. hanzo_mcp/tools/filesystem/rules_tool.py +5 -3
  68. hanzo_mcp/tools/filesystem/search_tool.py +728 -0
  69. hanzo_mcp/tools/filesystem/symbols_tool.py +510 -0
  70. hanzo_mcp/tools/filesystem/tree.py +273 -0
  71. hanzo_mcp/tools/filesystem/watch.py +6 -1
  72. hanzo_mcp/tools/filesystem/write.py +12 -6
  73. hanzo_mcp/tools/jupyter/jupyter.py +30 -2
  74. hanzo_mcp/tools/jupyter/notebook_edit.py +298 -0
  75. hanzo_mcp/tools/jupyter/notebook_read.py +148 -0
  76. hanzo_mcp/tools/llm/consensus_tool.py +8 -6
  77. hanzo_mcp/tools/llm/llm_manage.py +5 -0
  78. hanzo_mcp/tools/llm/llm_tool.py +2 -0
  79. hanzo_mcp/tools/llm/llm_unified.py +5 -0
  80. hanzo_mcp/tools/llm/provider_tools.py +5 -0
  81. hanzo_mcp/tools/lsp/lsp_tool.py +475 -622
  82. hanzo_mcp/tools/mcp/mcp_add.py +7 -2
  83. hanzo_mcp/tools/mcp/mcp_remove.py +15 -2
  84. hanzo_mcp/tools/mcp/mcp_stats.py +5 -0
  85. hanzo_mcp/tools/mcp/mcp_tool.py +5 -0
  86. hanzo_mcp/tools/memory/knowledge_tools.py +14 -0
  87. hanzo_mcp/tools/memory/memory_tools.py +17 -0
  88. hanzo_mcp/tools/search/find_tool.py +5 -3
  89. hanzo_mcp/tools/search/unified_search.py +3 -1
  90. hanzo_mcp/tools/shell/__init__.py +2 -14
  91. hanzo_mcp/tools/shell/base_process.py +4 -2
  92. hanzo_mcp/tools/shell/bash_tool.py +2 -0
  93. hanzo_mcp/tools/shell/command_executor.py +7 -7
  94. hanzo_mcp/tools/shell/logs.py +5 -0
  95. hanzo_mcp/tools/shell/npx.py +5 -0
  96. hanzo_mcp/tools/shell/npx_background.py +5 -0
  97. hanzo_mcp/tools/shell/npx_tool.py +5 -0
  98. hanzo_mcp/tools/shell/open.py +5 -0
  99. hanzo_mcp/tools/shell/pkill.py +5 -0
  100. hanzo_mcp/tools/shell/process_tool.py +5 -0
  101. hanzo_mcp/tools/shell/processes.py +5 -0
  102. hanzo_mcp/tools/shell/run_background.py +5 -0
  103. hanzo_mcp/tools/shell/run_command.py +2 -0
  104. hanzo_mcp/tools/shell/run_command_windows.py +5 -0
  105. hanzo_mcp/tools/shell/streaming_command.py +5 -0
  106. hanzo_mcp/tools/shell/uvx.py +5 -0
  107. hanzo_mcp/tools/shell/uvx_background.py +5 -0
  108. hanzo_mcp/tools/shell/uvx_tool.py +5 -0
  109. hanzo_mcp/tools/shell/zsh_tool.py +3 -0
  110. hanzo_mcp/tools/todo/todo.py +5 -0
  111. hanzo_mcp/tools/todo/todo_read.py +142 -0
  112. hanzo_mcp/tools/todo/todo_write.py +367 -0
  113. hanzo_mcp/tools/vector/__init__.py +42 -95
  114. hanzo_mcp/tools/vector/index_tool.py +5 -0
  115. hanzo_mcp/tools/vector/vector.py +5 -0
  116. hanzo_mcp/tools/vector/vector_index.py +5 -0
  117. hanzo_mcp/tools/vector/vector_search.py +5 -0
  118. {hanzo_mcp-0.9.0.dist-info → hanzo_mcp-0.9.1.dist-info}/METADATA +1 -1
  119. hanzo_mcp-0.9.1.dist-info/RECORD +195 -0
  120. hanzo_mcp/tools/common/path_utils.py +0 -34
  121. hanzo_mcp/tools/compiler/__init__.py +0 -8
  122. hanzo_mcp/tools/compiler/sandboxed_compiler.py +0 -681
  123. hanzo_mcp/tools/environment/__init__.py +0 -8
  124. hanzo_mcp/tools/environment/environment_detector.py +0 -594
  125. hanzo_mcp/tools/filesystem/search.py +0 -1160
  126. hanzo_mcp/tools/framework/__init__.py +0 -8
  127. hanzo_mcp/tools/framework/framework_modes.py +0 -714
  128. hanzo_mcp/tools/memory/conversation_memory.py +0 -636
  129. hanzo_mcp/tools/shell/run_tool.py +0 -56
  130. hanzo_mcp/tools/vector/node_tool.py +0 -538
  131. hanzo_mcp/tools/vector/unified_vector.py +0 -384
  132. hanzo_mcp-0.9.0.dist-info/RECORD +0 -191
  133. {hanzo_mcp-0.9.0.dist-info → hanzo_mcp-0.9.1.dist-info}/WHEEL +0 -0
  134. {hanzo_mcp-0.9.0.dist-info → hanzo_mcp-0.9.1.dist-info}/entry_points.txt +0 -0
  135. {hanzo_mcp-0.9.0.dist-info → hanzo_mcp-0.9.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,510 @@
1
+ """Unified symbols tool implementation.
2
+
3
+ This module provides the SymbolsTool for searching, indexing, and querying code symbols
4
+ using tree-sitter AST parsing. It can find function definitions, class declarations,
5
+ and other code structures with full context.
6
+ """
7
+
8
+ import os
9
+ from typing import (
10
+ Any,
11
+ Dict,
12
+ List,
13
+ Unpack,
14
+ Optional,
15
+ Annotated,
16
+ TypedDict,
17
+ final,
18
+ override,
19
+ )
20
+ from pathlib import Path
21
+
22
+ from pydantic import Field
23
+ from grep_ast.grep_ast import TreeContext
24
+ from mcp.server.fastmcp import Context as MCPContext
25
+
26
+ from hanzo_mcp.tools.common.auto_timeout import auto_timeout
27
+
28
+ from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
29
+
30
+ # Parameter types
31
+ Action = Annotated[
32
+ str,
33
+ Field(
34
+ description="Action: search (default), ast, index, query, list",
35
+ default="search",
36
+ ),
37
+ ]
38
+
39
+ Pattern = Annotated[
40
+ Optional[str],
41
+ Field(
42
+ description="Pattern to search for in code",
43
+ default=None,
44
+ ),
45
+ ]
46
+
47
+ SearchPath = Annotated[
48
+ str,
49
+ Field(
50
+ description="Path to search/index (file or directory)",
51
+ default=".",
52
+ ),
53
+ ]
54
+
55
+ SymbolType = Annotated[
56
+ Optional[str],
57
+ Field(
58
+ description="Symbol type: function, class, method, variable",
59
+ default=None,
60
+ ),
61
+ ]
62
+
63
+ IgnoreCase = Annotated[
64
+ bool,
65
+ Field(
66
+ description="Ignore case when matching",
67
+ default=False,
68
+ ),
69
+ ]
70
+
71
+ ShowContext = Annotated[
72
+ bool,
73
+ Field(
74
+ description="Show AST context around matches",
75
+ default=True,
76
+ ),
77
+ ]
78
+
79
+ Limit = Annotated[
80
+ int,
81
+ Field(
82
+ description="Maximum results to return",
83
+ default=50,
84
+ ),
85
+ ]
86
+
87
+
88
+ class SymbolsParams(TypedDict, total=False):
89
+ """Parameters for symbols tool."""
90
+
91
+ action: str
92
+ pattern: Optional[str]
93
+ path: str
94
+ symbol_type: Optional[str]
95
+ ignore_case: bool
96
+ show_context: bool
97
+ limit: int
98
+
99
+
100
+ @final
101
+ class SymbolsTool(FilesystemBaseTool):
102
+ """Tool for code symbol operations using tree-sitter."""
103
+
104
+ def __init__(self, permission_manager):
105
+ """Initialize the symbols tool."""
106
+ super().__init__(permission_manager)
107
+ self._symbol_cache = {} # Cache for indexed symbols
108
+
109
+ @property
110
+ @override
111
+ def name(self) -> str:
112
+ """Get the tool name."""
113
+ return "symbols"
114
+
115
+ @property
116
+ @override
117
+ def description(self) -> str:
118
+ """Get the tool description."""
119
+ return """Code symbols search with tree-sitter AST. Actions: search (default), ast, index, query, list.
120
+
121
+ Usage:
122
+ symbols "function_name"
123
+ symbols --action ast --pattern "TODO" --path ./src
124
+ symbols --action query --symbol-type function --path ./src
125
+ symbols --action index --path ./project
126
+ symbols --action list --path ./src --symbol-type class
127
+
128
+ Finds code structures (functions, classes, methods) with full context."""
129
+
130
+ @override
131
+ @auto_timeout("symbols")
132
+
133
+
134
+ async def call(
135
+ self,
136
+ ctx: MCPContext,
137
+ **params: Unpack[SymbolsParams],
138
+ ) -> str:
139
+ """Execute symbols operation."""
140
+ tool_ctx = self.create_tool_context(ctx)
141
+ self.set_tool_context_info(tool_ctx)
142
+
143
+ # Extract action
144
+ action = params.get("action", "search")
145
+
146
+ # Route to appropriate handler
147
+ if action == "search":
148
+ return await self._handle_search(params, tool_ctx)
149
+ elif action == "ast" or action == "grep_ast": # Support both for backward compatibility
150
+ return await self._handle_ast(params, tool_ctx)
151
+ elif action == "index":
152
+ return await self._handle_index(params, tool_ctx)
153
+ elif action == "query":
154
+ return await self._handle_query(params, tool_ctx)
155
+ elif action == "list":
156
+ return await self._handle_list(params, tool_ctx)
157
+ else:
158
+ return f"Error: Unknown action '{action}'. Valid actions: search, ast, index, query, list"
159
+
160
+ async def _handle_search(self, params: Dict[str, Any], tool_ctx) -> str:
161
+ """Search for pattern in code with AST context."""
162
+ pattern = params.get("pattern")
163
+ if not pattern:
164
+ return "Error: pattern required for search action"
165
+
166
+ path = params.get("path", ".")
167
+ ignore_case = params.get("ignore_case", False)
168
+ show_context = params.get("show_context", True)
169
+ limit = params.get("limit", 50)
170
+
171
+ # Validate path
172
+ path_validation = self.validate_path(path)
173
+ if not path_validation.is_valid:
174
+ await tool_ctx.error(f"Invalid path: {path_validation.error_message}")
175
+ return f"Error: Invalid path: {path_validation.error_message}"
176
+
177
+ # Check permissions
178
+ is_allowed, error_message = await self.check_path_allowed(path, tool_ctx)
179
+ if not is_allowed:
180
+ return error_message
181
+
182
+ # Check existence
183
+ is_exists, error_message = await self.check_path_exists(path, tool_ctx)
184
+ if not is_exists:
185
+ return error_message
186
+
187
+ await tool_ctx.info(f"Searching for '{pattern}' in {path}")
188
+
189
+ # Get files to process
190
+ files_to_process = self._get_source_files(path)
191
+ if not files_to_process:
192
+ return f"No source code files found in {path}"
193
+
194
+ # Process files
195
+ results = []
196
+ match_count = 0
197
+
198
+ for file_path in files_to_process:
199
+ if match_count >= limit:
200
+ break
201
+
202
+ try:
203
+ with open(file_path, "r", encoding="utf-8") as f:
204
+ code = f.read()
205
+
206
+ tc = TreeContext(
207
+ file_path,
208
+ code,
209
+ color=False,
210
+ verbose=False,
211
+ line_number=True,
212
+ )
213
+
214
+ # Find matches
215
+ loi = tc.grep(pattern, ignore_case)
216
+
217
+ if loi:
218
+ if show_context:
219
+ tc.add_lines_of_interest(loi)
220
+ tc.add_context()
221
+ output = tc.format()
222
+ else:
223
+ # Just show matching lines
224
+ output = "\n".join([f"{line}: {code.splitlines()[line - 1]}" for line in loi])
225
+
226
+ results.append(f"\n{file_path}:\n{output}\n")
227
+ match_count += len(loi)
228
+
229
+ except Exception as e:
230
+ await tool_ctx.warning(f"Could not parse {file_path}: {str(e)}")
231
+
232
+ if not results:
233
+ return f"No matches found for '{pattern}' in {path}"
234
+
235
+ output = [f"=== Symbol Search Results for '{pattern}' ==="]
236
+ output.append(f"Found {match_count} matches in {len(results)} files\n")
237
+ output.extend(results)
238
+
239
+ if match_count >= limit:
240
+ output.append(f"\n(Results limited to {limit} matches)")
241
+
242
+ return "\n".join(output)
243
+
244
+ async def _handle_ast(self, params: Dict[str, Any], tool_ctx) -> str:
245
+ """AST-aware grep - shows code structure context around matches."""
246
+ pattern = params.get("pattern")
247
+ if not pattern:
248
+ return "Error: pattern required for ast action"
249
+
250
+ path = params.get("path", ".")
251
+ ignore_case = params.get("ignore_case", False)
252
+ show_context = params.get("show_context", True)
253
+ limit = params.get("limit", 50)
254
+
255
+ # Validate path
256
+ path_validation = self.validate_path(path)
257
+ if not path_validation.is_valid:
258
+ await tool_ctx.error(f"Invalid path: {path_validation.error_message}")
259
+ return f"Error: Invalid path: {path_validation.error_message}"
260
+
261
+ # Check permissions
262
+ is_allowed, error_message = await self.check_path_allowed(path, tool_ctx)
263
+ if not is_allowed:
264
+ return error_message
265
+
266
+ # Check existence
267
+ is_exists, error_message = await self.check_path_exists(path, tool_ctx)
268
+ if not is_exists:
269
+ return error_message
270
+
271
+ await tool_ctx.info(f"Running AST-aware grep for '{pattern}' in {path}")
272
+
273
+ # Get files to process
274
+ files_to_process = self._get_source_files(path)
275
+ if not files_to_process:
276
+ return f"No source code files found in {path}"
277
+
278
+ # Process files
279
+ results = []
280
+ match_count = 0
281
+
282
+ for file_path in files_to_process:
283
+ if match_count >= limit:
284
+ break
285
+
286
+ try:
287
+ with open(file_path, "r", encoding="utf-8") as f:
288
+ code = f.read()
289
+
290
+ # Create TreeContext for AST parsing
291
+ tc = TreeContext(
292
+ file_path,
293
+ code,
294
+ color=False,
295
+ verbose=False,
296
+ line_number=True,
297
+ )
298
+
299
+ # Find matches with case sensitivity option
300
+ if ignore_case:
301
+ loi = tc.grep(pattern, ignore_case=True)
302
+ else:
303
+ loi = tc.grep(pattern, ignore_case=False)
304
+
305
+ if loi:
306
+ # Always show AST context for grep_ast
307
+ tc.add_lines_of_interest(loi)
308
+ tc.add_context()
309
+
310
+ # Get the formatted output with structure
311
+ output = tc.format()
312
+
313
+ # Add section separator and file info
314
+ results.append(f"\n{'=' * 60}")
315
+ results.append(f"File: {file_path}")
316
+ results.append(f"Matches: {len(loi)}")
317
+ results.append(f"{'=' * 60}\n")
318
+ results.append(output)
319
+
320
+ match_count += len(loi)
321
+
322
+ except Exception as e:
323
+ await tool_ctx.warning(f"Could not parse {file_path}: {str(e)}")
324
+
325
+ if not results:
326
+ return f"No matches found for '{pattern}' in {path}"
327
+
328
+ output = [f"=== AST-aware Grep Results for '{pattern}' ==="]
329
+ output.append(f"Total matches: {match_count} in {len([r for r in results if '===' in str(r)]) // 4} files\n")
330
+ output.extend(results)
331
+
332
+ if match_count >= limit:
333
+ output.append(f"\n(Results limited to {limit} matches)")
334
+
335
+ return "\n".join(output)
336
+
337
+ async def _handle_index(self, params: Dict[str, Any], tool_ctx) -> str:
338
+ """Index symbols in a codebase."""
339
+ path = params.get("path", ".")
340
+
341
+ # Validate path
342
+ is_allowed, error_message = await self.check_path_allowed(path, tool_ctx)
343
+ if not is_allowed:
344
+ return error_message
345
+
346
+ await tool_ctx.info(f"Indexing symbols in {path}...")
347
+
348
+ files_to_process = self._get_source_files(path)
349
+ if not files_to_process:
350
+ return f"No source code files found in {path}"
351
+
352
+ # Clear cache for this path
353
+ self._symbol_cache[path] = {
354
+ "functions": [],
355
+ "classes": [],
356
+ "methods": [],
357
+ "variables": [],
358
+ }
359
+
360
+ indexed_count = 0
361
+ symbol_count = 0
362
+
363
+ for file_path in files_to_process:
364
+ try:
365
+ with open(file_path, "r", encoding="utf-8") as f:
366
+ code = f.read()
367
+
368
+ tc = TreeContext(file_path, code, color=False, verbose=False)
369
+
370
+ # Extract symbols (simplified - would need proper tree-sitter queries)
371
+ # This is a placeholder for actual symbol extraction
372
+ symbols = self._extract_symbols(tc, file_path)
373
+
374
+ for symbol_type, syms in symbols.items():
375
+ self._symbol_cache[path][symbol_type].extend(syms)
376
+ symbol_count += len(syms)
377
+
378
+ indexed_count += 1
379
+
380
+ except Exception as e:
381
+ await tool_ctx.warning(f"Could not index {file_path}: {str(e)}")
382
+
383
+ output = [f"=== Symbol Indexing Complete ==="]
384
+ output.append(f"Indexed {indexed_count} files")
385
+ output.append(f"Found {symbol_count} total symbols:")
386
+
387
+ for symbol_type, symbols in self._symbol_cache[path].items():
388
+ if symbols:
389
+ output.append(f" {symbol_type}: {len(symbols)}")
390
+
391
+ return "\n".join(output)
392
+
393
+ async def _handle_query(self, params: Dict[str, Any], tool_ctx) -> str:
394
+ """Query indexed symbols."""
395
+ path = params.get("path", ".")
396
+ symbol_type = params.get("symbol_type")
397
+ pattern = params.get("pattern")
398
+ limit = params.get("limit", 50)
399
+
400
+ # Check if we have indexed this path
401
+ if path not in self._symbol_cache:
402
+ return f"No symbols indexed for {path}. Run 'symbols --action index --path {path}' first."
403
+
404
+ symbols = self._symbol_cache[path]
405
+ results = []
406
+
407
+ # Filter by type if specified
408
+ if symbol_type:
409
+ if symbol_type in symbols:
410
+ candidates = symbols[symbol_type]
411
+ else:
412
+ return f"Unknown symbol type: {symbol_type}. Valid types: {', '.join(symbols.keys())}"
413
+ else:
414
+ # Combine all symbol types
415
+ candidates = []
416
+ for syms in symbols.values():
417
+ candidates.extend(syms)
418
+
419
+ # Filter by pattern if specified
420
+ if pattern:
421
+ filtered = []
422
+ for sym in candidates:
423
+ if pattern.lower() in sym["name"].lower():
424
+ filtered.append(sym)
425
+ candidates = filtered
426
+
427
+ # Limit results
428
+ candidates = candidates[:limit]
429
+
430
+ if not candidates:
431
+ return "No symbols found matching criteria"
432
+
433
+ output = [f"=== Symbol Query Results ==="]
434
+ output.append(f"Found {len(candidates)} symbols\n")
435
+
436
+ for sym in candidates:
437
+ output.append(f"{sym['type']}: {sym['name']}")
438
+ output.append(f" File: {sym['file']}:{sym['line']}")
439
+ if sym.get("signature"):
440
+ output.append(f" Signature: {sym['signature']}")
441
+ output.append("")
442
+
443
+ return "\n".join(output)
444
+
445
+ async def _handle_list(self, params: Dict[str, Any], tool_ctx) -> str:
446
+ """List all symbols in a path."""
447
+ # Similar to query but shows all symbols
448
+ params["pattern"] = None
449
+ return await self._handle_query(params, tool_ctx)
450
+
451
+ def _get_source_files(self, path: str) -> List[str]:
452
+ """Get all source code files in a path."""
453
+ path_obj = Path(path)
454
+ files_to_process = []
455
+
456
+ # Common source file extensions
457
+ extensions = {
458
+ ".py",
459
+ ".js",
460
+ ".ts",
461
+ ".jsx",
462
+ ".tsx",
463
+ ".java",
464
+ ".cpp",
465
+ ".c",
466
+ ".h",
467
+ ".hpp",
468
+ ".cs",
469
+ ".rb",
470
+ ".go",
471
+ ".rs",
472
+ ".swift",
473
+ ".kt",
474
+ ".scala",
475
+ ".php",
476
+ ".lua",
477
+ ".r",
478
+ ".jl",
479
+ ".ex",
480
+ ".exs",
481
+ ".clj",
482
+ ".cljs",
483
+ }
484
+
485
+ if path_obj.is_file():
486
+ if path_obj.suffix in extensions:
487
+ files_to_process.append(str(path_obj))
488
+ elif path_obj.is_dir():
489
+ for root, _, files in os.walk(path_obj):
490
+ for file in files:
491
+ file_path = Path(root) / file
492
+ if file_path.suffix in extensions and self.is_path_allowed(str(file_path)):
493
+ files_to_process.append(str(file_path))
494
+
495
+ return files_to_process
496
+
497
+ def _extract_symbols(self, tc: TreeContext, file_path: str) -> Dict[str, List[Dict[str, Any]]]:
498
+ """Extract symbols from a TreeContext (placeholder implementation)."""
499
+ # This would need proper tree-sitter queries to extract symbols
500
+ # For now, return empty structure
501
+ return {
502
+ "functions": [],
503
+ "classes": [],
504
+ "methods": [],
505
+ "variables": [],
506
+ }
507
+
508
+ def register(self, mcp_server) -> None:
509
+ """Register this tool with the MCP server."""
510
+ pass