hanzo-mcp 0.5.0__py3-none-any.whl → 0.5.2__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 (60) hide show
  1. hanzo_mcp/__init__.py +1 -1
  2. hanzo_mcp/config/settings.py +61 -0
  3. hanzo_mcp/tools/__init__.py +158 -12
  4. hanzo_mcp/tools/common/base.py +7 -2
  5. hanzo_mcp/tools/common/config_tool.py +396 -0
  6. hanzo_mcp/tools/common/stats.py +261 -0
  7. hanzo_mcp/tools/common/tool_disable.py +144 -0
  8. hanzo_mcp/tools/common/tool_enable.py +182 -0
  9. hanzo_mcp/tools/common/tool_list.py +263 -0
  10. hanzo_mcp/tools/database/__init__.py +71 -0
  11. hanzo_mcp/tools/database/database_manager.py +246 -0
  12. hanzo_mcp/tools/database/graph_add.py +257 -0
  13. hanzo_mcp/tools/database/graph_query.py +536 -0
  14. hanzo_mcp/tools/database/graph_remove.py +267 -0
  15. hanzo_mcp/tools/database/graph_search.py +348 -0
  16. hanzo_mcp/tools/database/graph_stats.py +345 -0
  17. hanzo_mcp/tools/database/sql_query.py +229 -0
  18. hanzo_mcp/tools/database/sql_search.py +296 -0
  19. hanzo_mcp/tools/database/sql_stats.py +254 -0
  20. hanzo_mcp/tools/editor/__init__.py +11 -0
  21. hanzo_mcp/tools/editor/neovim_command.py +272 -0
  22. hanzo_mcp/tools/editor/neovim_edit.py +290 -0
  23. hanzo_mcp/tools/editor/neovim_session.py +356 -0
  24. hanzo_mcp/tools/filesystem/__init__.py +20 -1
  25. hanzo_mcp/tools/filesystem/batch_search.py +812 -0
  26. hanzo_mcp/tools/filesystem/find_files.py +348 -0
  27. hanzo_mcp/tools/filesystem/git_search.py +505 -0
  28. hanzo_mcp/tools/llm/__init__.py +27 -0
  29. hanzo_mcp/tools/llm/consensus_tool.py +351 -0
  30. hanzo_mcp/tools/llm/llm_manage.py +413 -0
  31. hanzo_mcp/tools/llm/llm_tool.py +346 -0
  32. hanzo_mcp/tools/llm/provider_tools.py +412 -0
  33. hanzo_mcp/tools/mcp/__init__.py +11 -0
  34. hanzo_mcp/tools/mcp/mcp_add.py +263 -0
  35. hanzo_mcp/tools/mcp/mcp_remove.py +127 -0
  36. hanzo_mcp/tools/mcp/mcp_stats.py +165 -0
  37. hanzo_mcp/tools/shell/__init__.py +27 -7
  38. hanzo_mcp/tools/shell/logs.py +265 -0
  39. hanzo_mcp/tools/shell/npx.py +194 -0
  40. hanzo_mcp/tools/shell/npx_background.py +254 -0
  41. hanzo_mcp/tools/shell/pkill.py +262 -0
  42. hanzo_mcp/tools/shell/processes.py +279 -0
  43. hanzo_mcp/tools/shell/run_background.py +326 -0
  44. hanzo_mcp/tools/shell/uvx.py +187 -0
  45. hanzo_mcp/tools/shell/uvx_background.py +249 -0
  46. hanzo_mcp/tools/vector/__init__.py +21 -12
  47. hanzo_mcp/tools/vector/ast_analyzer.py +459 -0
  48. hanzo_mcp/tools/vector/git_ingester.py +485 -0
  49. hanzo_mcp/tools/vector/index_tool.py +358 -0
  50. hanzo_mcp/tools/vector/infinity_store.py +465 -1
  51. hanzo_mcp/tools/vector/mock_infinity.py +162 -0
  52. hanzo_mcp/tools/vector/vector_index.py +7 -6
  53. hanzo_mcp/tools/vector/vector_search.py +22 -7
  54. {hanzo_mcp-0.5.0.dist-info → hanzo_mcp-0.5.2.dist-info}/METADATA +68 -20
  55. hanzo_mcp-0.5.2.dist-info/RECORD +106 -0
  56. hanzo_mcp-0.5.0.dist-info/RECORD +0 -63
  57. {hanzo_mcp-0.5.0.dist-info → hanzo_mcp-0.5.2.dist-info}/WHEEL +0 -0
  58. {hanzo_mcp-0.5.0.dist-info → hanzo_mcp-0.5.2.dist-info}/entry_points.txt +0 -0
  59. {hanzo_mcp-0.5.0.dist-info → hanzo_mcp-0.5.2.dist-info}/licenses/LICENSE +0 -0
  60. {hanzo_mcp-0.5.0.dist-info → hanzo_mcp-0.5.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,459 @@
1
+ """AST analysis and symbol extraction for code understanding."""
2
+
3
+ import ast
4
+ import json
5
+ from pathlib import Path
6
+ from typing import Dict, List, Any, Optional, Set, Tuple
7
+ from dataclasses import dataclass, asdict
8
+ import hashlib
9
+
10
+ try:
11
+ import tree_sitter_python as tspython
12
+ import tree_sitter
13
+ TREE_SITTER_AVAILABLE = True
14
+ except ImportError:
15
+ TREE_SITTER_AVAILABLE = False
16
+
17
+
18
+ @dataclass
19
+ class Symbol:
20
+ """Represents a code symbol (function, class, variable, etc.)."""
21
+ name: str
22
+ type: str # function, class, variable, import, etc.
23
+ file_path: str
24
+ line_start: int
25
+ line_end: int
26
+ column_start: int
27
+ column_end: int
28
+ scope: str # global, class, function
29
+ parent: Optional[str] = None # parent class/function
30
+ docstring: Optional[str] = None
31
+ signature: Optional[str] = None
32
+ references: List[str] = None # Files that reference this symbol
33
+
34
+ def __post_init__(self):
35
+ if self.references is None:
36
+ self.references = []
37
+
38
+
39
+ @dataclass
40
+ class ASTNode:
41
+ """Represents an AST node with metadata."""
42
+ type: str
43
+ name: Optional[str]
44
+ line_start: int
45
+ line_end: int
46
+ column_start: int
47
+ column_end: int
48
+ children: List['ASTNode'] = None
49
+ parent: Optional[str] = None
50
+
51
+ def __post_init__(self):
52
+ if self.children is None:
53
+ self.children = []
54
+
55
+
56
+ @dataclass
57
+ class FileAST:
58
+ """Complete AST representation of a file."""
59
+ file_path: str
60
+ file_hash: str
61
+ language: str
62
+ symbols: List[Symbol]
63
+ ast_nodes: List[ASTNode]
64
+ imports: List[str]
65
+ exports: List[str]
66
+ dependencies: List[str] # Files this file depends on
67
+
68
+ def to_dict(self) -> Dict[str, Any]:
69
+ """Convert to dictionary for storage."""
70
+ return {
71
+ "file_path": self.file_path,
72
+ "file_hash": self.file_hash,
73
+ "language": self.language,
74
+ "symbols": [asdict(s) for s in self.symbols],
75
+ "ast_nodes": [asdict(n) for n in self.ast_nodes],
76
+ "imports": self.imports,
77
+ "exports": self.exports,
78
+ "dependencies": self.dependencies,
79
+ }
80
+
81
+
82
+ class ASTAnalyzer:
83
+ """Analyzes code files and extracts AST information and symbols."""
84
+
85
+ def __init__(self):
86
+ """Initialize the AST analyzer."""
87
+ self.parsers = {}
88
+ self._setup_parsers()
89
+
90
+ def _setup_parsers(self):
91
+ """Set up tree-sitter parsers for different languages."""
92
+ if TREE_SITTER_AVAILABLE:
93
+ try:
94
+ # Python parser
95
+ self.parsers['python'] = tree_sitter.Language(tspython.language())
96
+ except Exception as e:
97
+ print(f"Warning: Could not initialize Python parser: {e}")
98
+
99
+ def analyze_file(self, file_path: str) -> Optional[FileAST]:
100
+ """Analyze a file and extract AST information and symbols.
101
+
102
+ Args:
103
+ file_path: Path to the file to analyze
104
+
105
+ Returns:
106
+ FileAST object with extracted information, or None if analysis fails
107
+ """
108
+ path = Path(file_path)
109
+ if not path.exists():
110
+ return None
111
+
112
+ # Determine language
113
+ language = self._detect_language(path)
114
+ if not language:
115
+ return None
116
+
117
+ try:
118
+ # Read file content
119
+ content = path.read_text(encoding='utf-8')
120
+ file_hash = hashlib.sha256(content.encode()).hexdigest()
121
+
122
+ # Extract symbols and AST
123
+ if language == 'python':
124
+ return self._analyze_python_file(file_path, content, file_hash)
125
+ else:
126
+ # Generic analysis for other languages
127
+ return self._analyze_generic_file(file_path, content, file_hash, language)
128
+
129
+ except Exception as e:
130
+ print(f"Error analyzing file {file_path}: {e}")
131
+ return None
132
+
133
+ def _detect_language(self, path: Path) -> Optional[str]:
134
+ """Detect programming language from file extension."""
135
+ extension = path.suffix.lower()
136
+
137
+ language_map = {
138
+ '.py': 'python',
139
+ '.js': 'javascript',
140
+ '.ts': 'typescript',
141
+ '.jsx': 'javascript',
142
+ '.tsx': 'typescript',
143
+ '.java': 'java',
144
+ '.cpp': 'cpp',
145
+ '.c': 'c',
146
+ '.h': 'c',
147
+ '.hpp': 'cpp',
148
+ '.rs': 'rust',
149
+ '.go': 'go',
150
+ '.rb': 'ruby',
151
+ '.php': 'php',
152
+ '.cs': 'csharp',
153
+ '.swift': 'swift',
154
+ '.kt': 'kotlin',
155
+ '.scala': 'scala',
156
+ '.clj': 'clojure',
157
+ '.hs': 'haskell',
158
+ '.ml': 'ocaml',
159
+ '.elm': 'elm',
160
+ '.dart': 'dart',
161
+ '.lua': 'lua',
162
+ '.r': 'r',
163
+ '.m': 'objective-c',
164
+ '.mm': 'objective-cpp',
165
+ }
166
+
167
+ return language_map.get(extension)
168
+
169
+ def _analyze_python_file(self, file_path: str, content: str, file_hash: str) -> FileAST:
170
+ """Analyze Python file using both AST and tree-sitter."""
171
+ symbols = []
172
+ ast_nodes = []
173
+ imports = []
174
+ exports = []
175
+ dependencies = []
176
+
177
+ try:
178
+ # Parse with Python AST
179
+ tree = ast.parse(content)
180
+
181
+ # Extract symbols using AST visitor
182
+ visitor = PythonSymbolExtractor(file_path)
183
+ visitor.visit(tree)
184
+
185
+ symbols.extend(visitor.symbols)
186
+ imports.extend(visitor.imports)
187
+ exports.extend(visitor.exports)
188
+ dependencies.extend(visitor.dependencies)
189
+
190
+ # If tree-sitter is available, get more detailed AST
191
+ if TREE_SITTER_AVAILABLE and 'python' in self.parsers:
192
+ parser = tree_sitter.Parser(self.parsers['python'])
193
+ ts_tree = parser.parse(content.encode())
194
+ ast_nodes = self._extract_tree_sitter_nodes(ts_tree.root_node, content)
195
+
196
+ except SyntaxError as e:
197
+ print(f"Syntax error in {file_path}: {e}")
198
+ except Exception as e:
199
+ print(f"Error parsing Python file {file_path}: {e}")
200
+
201
+ return FileAST(
202
+ file_path=file_path,
203
+ file_hash=file_hash,
204
+ language='python',
205
+ symbols=symbols,
206
+ ast_nodes=ast_nodes,
207
+ imports=imports,
208
+ exports=exports,
209
+ dependencies=dependencies,
210
+ )
211
+
212
+ def _analyze_generic_file(self, file_path: str, content: str, file_hash: str, language: str) -> FileAST:
213
+ """Generic analysis for non-Python files."""
214
+ # For now, just basic line-based analysis
215
+ # Could be enhanced with language-specific parsers
216
+
217
+ symbols = []
218
+ ast_nodes = []
219
+ imports = []
220
+ exports = []
221
+ dependencies = []
222
+
223
+ # Basic pattern matching for common constructs
224
+ lines = content.split('\n')
225
+ for i, line in enumerate(lines, 1):
226
+ line = line.strip()
227
+
228
+ # Basic function detection (works for many C-style languages)
229
+ if language in ['javascript', 'typescript', 'java', 'cpp', 'c']:
230
+ if 'function ' in line or line.startswith('def ') or ' function(' in line:
231
+ # Extract function name
232
+ parts = line.split()
233
+ for j, part in enumerate(parts):
234
+ if part == 'function' and j + 1 < len(parts):
235
+ func_name = parts[j + 1].split('(')[0]
236
+ symbols.append(Symbol(
237
+ name=func_name,
238
+ type='function',
239
+ file_path=file_path,
240
+ line_start=i,
241
+ line_end=i,
242
+ column_start=0,
243
+ column_end=len(line),
244
+ scope='global',
245
+ ))
246
+ break
247
+
248
+ # Basic import detection
249
+ if 'import ' in line or '#include ' in line or 'require(' in line:
250
+ imports.append(line)
251
+
252
+ return FileAST(
253
+ file_path=file_path,
254
+ file_hash=file_hash,
255
+ language=language,
256
+ symbols=symbols,
257
+ ast_nodes=ast_nodes,
258
+ imports=imports,
259
+ exports=exports,
260
+ dependencies=dependencies,
261
+ )
262
+
263
+ def _extract_tree_sitter_nodes(self, node, content: str) -> List[ASTNode]:
264
+ """Extract AST nodes from tree-sitter parse tree."""
265
+ nodes = []
266
+
267
+ def traverse(ts_node, parent_name=None):
268
+ node_name = None
269
+
270
+ # Try to extract node name for named nodes
271
+ if ts_node.type in ['function_definition', 'class_definition', 'identifier']:
272
+ for child in ts_node.children:
273
+ if child.type == 'identifier':
274
+ start_byte = child.start_byte
275
+ end_byte = child.end_byte
276
+ node_name = content[start_byte:end_byte]
277
+ break
278
+
279
+ ast_node = ASTNode(
280
+ type=ts_node.type,
281
+ name=node_name,
282
+ line_start=ts_node.start_point[0] + 1,
283
+ line_end=ts_node.end_point[0] + 1,
284
+ column_start=ts_node.start_point[1],
285
+ column_end=ts_node.end_point[1],
286
+ parent=parent_name,
287
+ )
288
+
289
+ nodes.append(ast_node)
290
+
291
+ # Recursively process children
292
+ for child in ts_node.children:
293
+ traverse(child, node_name or parent_name)
294
+
295
+ traverse(node)
296
+ return nodes
297
+
298
+
299
+ class PythonSymbolExtractor(ast.NodeVisitor):
300
+ """AST visitor for extracting Python symbols."""
301
+
302
+ def __init__(self, file_path: str):
303
+ self.file_path = file_path
304
+ self.symbols = []
305
+ self.imports = []
306
+ self.exports = []
307
+ self.dependencies = []
308
+ self.scope_stack = ['global']
309
+
310
+ def visit_FunctionDef(self, node):
311
+ """Visit function definitions."""
312
+ scope = '.'.join(self.scope_stack)
313
+ parent = self.scope_stack[-1] if len(self.scope_stack) > 1 else None
314
+
315
+ # Extract docstring
316
+ docstring = None
317
+ if (node.body and isinstance(node.body[0], ast.Expr) and
318
+ isinstance(node.body[0].value, ast.Constant) and
319
+ isinstance(node.body[0].value.value, str)):
320
+ docstring = node.body[0].value.value
321
+
322
+ # Create function signature
323
+ args = [arg.arg for arg in node.args.args]
324
+ signature = f"{node.name}({', '.join(args)})"
325
+
326
+ symbol = Symbol(
327
+ name=node.name,
328
+ type='function',
329
+ file_path=self.file_path,
330
+ line_start=node.lineno,
331
+ line_end=node.end_lineno or node.lineno,
332
+ column_start=node.col_offset,
333
+ column_end=node.end_col_offset or 0,
334
+ scope=scope,
335
+ parent=parent if parent != 'global' else None,
336
+ docstring=docstring,
337
+ signature=signature,
338
+ )
339
+
340
+ self.symbols.append(symbol)
341
+
342
+ # Enter function scope
343
+ self.scope_stack.append(node.name)
344
+ self.generic_visit(node)
345
+ self.scope_stack.pop()
346
+
347
+ def visit_AsyncFunctionDef(self, node):
348
+ """Visit async function definitions."""
349
+ self.visit_FunctionDef(node) # Same logic
350
+
351
+ def visit_ClassDef(self, node):
352
+ """Visit class definitions."""
353
+ scope = '.'.join(self.scope_stack)
354
+ parent = self.scope_stack[-1] if len(self.scope_stack) > 1 else None
355
+
356
+ # Extract docstring
357
+ docstring = None
358
+ if (node.body and isinstance(node.body[0], ast.Expr) and
359
+ isinstance(node.body[0].value, ast.Constant) and
360
+ isinstance(node.body[0].value.value, str)):
361
+ docstring = node.body[0].value.value
362
+
363
+ # Extract base classes
364
+ bases = [self._get_name(base) for base in node.bases]
365
+ signature = f"class {node.name}({', '.join(bases)})" if bases else f"class {node.name}"
366
+
367
+ symbol = Symbol(
368
+ name=node.name,
369
+ type='class',
370
+ file_path=self.file_path,
371
+ line_start=node.lineno,
372
+ line_end=node.end_lineno or node.lineno,
373
+ column_start=node.col_offset,
374
+ column_end=node.end_col_offset or 0,
375
+ scope=scope,
376
+ parent=parent if parent != 'global' else None,
377
+ docstring=docstring,
378
+ signature=signature,
379
+ )
380
+
381
+ self.symbols.append(symbol)
382
+
383
+ # Enter class scope
384
+ self.scope_stack.append(node.name)
385
+ self.generic_visit(node)
386
+ self.scope_stack.pop()
387
+
388
+ def visit_Import(self, node):
389
+ """Visit import statements."""
390
+ for alias in node.names:
391
+ import_name = alias.name
392
+ self.imports.append(import_name)
393
+ if '.' not in import_name: # Top-level module
394
+ self.dependencies.append(import_name)
395
+
396
+ def visit_ImportFrom(self, node):
397
+ """Visit from...import statements."""
398
+ if node.module:
399
+ self.imports.append(node.module)
400
+ if '.' not in node.module: # Top-level module
401
+ self.dependencies.append(node.module)
402
+
403
+ for alias in node.names:
404
+ if alias.name != '*':
405
+ import_item = f"{node.module}.{alias.name}" if node.module else alias.name
406
+ self.imports.append(import_item)
407
+
408
+ def visit_Assign(self, node):
409
+ """Visit variable assignments."""
410
+ scope = '.'.join(self.scope_stack)
411
+ parent = self.scope_stack[-1] if len(self.scope_stack) > 1 else None
412
+
413
+ for target in node.targets:
414
+ if isinstance(target, ast.Name):
415
+ symbol = Symbol(
416
+ name=target.id,
417
+ type='variable',
418
+ file_path=self.file_path,
419
+ line_start=node.lineno,
420
+ line_end=node.end_lineno or node.lineno,
421
+ column_start=node.col_offset,
422
+ column_end=node.end_col_offset or 0,
423
+ scope=scope,
424
+ parent=parent if parent != 'global' else None,
425
+ )
426
+ self.symbols.append(symbol)
427
+
428
+ self.generic_visit(node)
429
+
430
+ def _get_name(self, node):
431
+ """Extract name from AST node."""
432
+ if isinstance(node, ast.Name):
433
+ return node.id
434
+ elif isinstance(node, ast.Attribute):
435
+ return f"{self._get_name(node.value)}.{node.attr}"
436
+ elif isinstance(node, ast.Constant):
437
+ return str(node.value)
438
+ else:
439
+ return str(node)
440
+
441
+
442
+ def create_symbol_embedding_text(symbol: Symbol) -> str:
443
+ """Create text representation of symbol for vector embedding."""
444
+ parts = [
445
+ f"Symbol: {symbol.name}",
446
+ f"Type: {symbol.type}",
447
+ f"Scope: {symbol.scope}",
448
+ ]
449
+
450
+ if symbol.parent:
451
+ parts.append(f"Parent: {symbol.parent}")
452
+
453
+ if symbol.signature:
454
+ parts.append(f"Signature: {symbol.signature}")
455
+
456
+ if symbol.docstring:
457
+ parts.append(f"Documentation: {symbol.docstring}")
458
+
459
+ return " | ".join(parts)