hanzo-mcp 0.5.1__py3-none-any.whl → 0.6.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 (118) hide show
  1. hanzo_mcp/__init__.py +1 -1
  2. hanzo_mcp/cli.py +32 -0
  3. hanzo_mcp/dev_server.py +246 -0
  4. hanzo_mcp/prompts/__init__.py +1 -1
  5. hanzo_mcp/prompts/project_system.py +43 -7
  6. hanzo_mcp/server.py +5 -1
  7. hanzo_mcp/tools/__init__.py +168 -6
  8. hanzo_mcp/tools/agent/__init__.py +1 -1
  9. hanzo_mcp/tools/agent/agent.py +401 -0
  10. hanzo_mcp/tools/agent/agent_tool.py +3 -4
  11. hanzo_mcp/tools/common/__init__.py +1 -1
  12. hanzo_mcp/tools/common/base.py +9 -4
  13. hanzo_mcp/tools/common/batch_tool.py +3 -5
  14. hanzo_mcp/tools/common/config_tool.py +1 -1
  15. hanzo_mcp/tools/common/context.py +1 -1
  16. hanzo_mcp/tools/common/palette.py +344 -0
  17. hanzo_mcp/tools/common/palette_loader.py +108 -0
  18. hanzo_mcp/tools/common/stats.py +261 -0
  19. hanzo_mcp/tools/common/thinking_tool.py +3 -5
  20. hanzo_mcp/tools/common/tool_disable.py +144 -0
  21. hanzo_mcp/tools/common/tool_enable.py +182 -0
  22. hanzo_mcp/tools/common/tool_list.py +260 -0
  23. hanzo_mcp/tools/config/__init__.py +10 -0
  24. hanzo_mcp/tools/config/config_tool.py +212 -0
  25. hanzo_mcp/tools/config/index_config.py +176 -0
  26. hanzo_mcp/tools/config/palette_tool.py +166 -0
  27. hanzo_mcp/tools/database/__init__.py +71 -0
  28. hanzo_mcp/tools/database/database_manager.py +246 -0
  29. hanzo_mcp/tools/database/graph.py +482 -0
  30. hanzo_mcp/tools/database/graph_add.py +257 -0
  31. hanzo_mcp/tools/database/graph_query.py +536 -0
  32. hanzo_mcp/tools/database/graph_remove.py +267 -0
  33. hanzo_mcp/tools/database/graph_search.py +348 -0
  34. hanzo_mcp/tools/database/graph_stats.py +345 -0
  35. hanzo_mcp/tools/database/sql.py +411 -0
  36. hanzo_mcp/tools/database/sql_query.py +229 -0
  37. hanzo_mcp/tools/database/sql_search.py +296 -0
  38. hanzo_mcp/tools/database/sql_stats.py +254 -0
  39. hanzo_mcp/tools/editor/__init__.py +11 -0
  40. hanzo_mcp/tools/editor/neovim_command.py +272 -0
  41. hanzo_mcp/tools/editor/neovim_edit.py +290 -0
  42. hanzo_mcp/tools/editor/neovim_session.py +356 -0
  43. hanzo_mcp/tools/filesystem/__init__.py +52 -13
  44. hanzo_mcp/tools/filesystem/base.py +1 -1
  45. hanzo_mcp/tools/filesystem/batch_search.py +812 -0
  46. hanzo_mcp/tools/filesystem/content_replace.py +3 -5
  47. hanzo_mcp/tools/filesystem/diff.py +193 -0
  48. hanzo_mcp/tools/filesystem/directory_tree.py +3 -5
  49. hanzo_mcp/tools/filesystem/edit.py +3 -5
  50. hanzo_mcp/tools/filesystem/find.py +443 -0
  51. hanzo_mcp/tools/filesystem/find_files.py +348 -0
  52. hanzo_mcp/tools/filesystem/git_search.py +505 -0
  53. hanzo_mcp/tools/filesystem/grep.py +2 -2
  54. hanzo_mcp/tools/filesystem/multi_edit.py +3 -5
  55. hanzo_mcp/tools/filesystem/read.py +17 -5
  56. hanzo_mcp/tools/filesystem/{grep_ast_tool.py → symbols.py} +17 -27
  57. hanzo_mcp/tools/filesystem/symbols_unified.py +376 -0
  58. hanzo_mcp/tools/filesystem/tree.py +268 -0
  59. hanzo_mcp/tools/filesystem/unified_search.py +465 -443
  60. hanzo_mcp/tools/filesystem/unix_aliases.py +99 -0
  61. hanzo_mcp/tools/filesystem/watch.py +174 -0
  62. hanzo_mcp/tools/filesystem/write.py +3 -5
  63. hanzo_mcp/tools/jupyter/__init__.py +9 -12
  64. hanzo_mcp/tools/jupyter/base.py +1 -1
  65. hanzo_mcp/tools/jupyter/jupyter.py +326 -0
  66. hanzo_mcp/tools/jupyter/notebook_edit.py +3 -4
  67. hanzo_mcp/tools/jupyter/notebook_read.py +3 -5
  68. hanzo_mcp/tools/llm/__init__.py +31 -0
  69. hanzo_mcp/tools/llm/consensus_tool.py +351 -0
  70. hanzo_mcp/tools/llm/llm_manage.py +413 -0
  71. hanzo_mcp/tools/llm/llm_tool.py +346 -0
  72. hanzo_mcp/tools/llm/llm_unified.py +851 -0
  73. hanzo_mcp/tools/llm/provider_tools.py +412 -0
  74. hanzo_mcp/tools/mcp/__init__.py +15 -0
  75. hanzo_mcp/tools/mcp/mcp_add.py +263 -0
  76. hanzo_mcp/tools/mcp/mcp_remove.py +127 -0
  77. hanzo_mcp/tools/mcp/mcp_stats.py +165 -0
  78. hanzo_mcp/tools/mcp/mcp_unified.py +503 -0
  79. hanzo_mcp/tools/shell/__init__.py +21 -23
  80. hanzo_mcp/tools/shell/base.py +1 -1
  81. hanzo_mcp/tools/shell/base_process.py +303 -0
  82. hanzo_mcp/tools/shell/bash_unified.py +134 -0
  83. hanzo_mcp/tools/shell/logs.py +265 -0
  84. hanzo_mcp/tools/shell/npx.py +194 -0
  85. hanzo_mcp/tools/shell/npx_background.py +254 -0
  86. hanzo_mcp/tools/shell/npx_unified.py +101 -0
  87. hanzo_mcp/tools/shell/open.py +107 -0
  88. hanzo_mcp/tools/shell/pkill.py +262 -0
  89. hanzo_mcp/tools/shell/process_unified.py +131 -0
  90. hanzo_mcp/tools/shell/processes.py +279 -0
  91. hanzo_mcp/tools/shell/run_background.py +326 -0
  92. hanzo_mcp/tools/shell/run_command.py +3 -4
  93. hanzo_mcp/tools/shell/run_command_windows.py +3 -4
  94. hanzo_mcp/tools/shell/uvx.py +187 -0
  95. hanzo_mcp/tools/shell/uvx_background.py +249 -0
  96. hanzo_mcp/tools/shell/uvx_unified.py +101 -0
  97. hanzo_mcp/tools/todo/__init__.py +1 -1
  98. hanzo_mcp/tools/todo/base.py +1 -1
  99. hanzo_mcp/tools/todo/todo.py +265 -0
  100. hanzo_mcp/tools/todo/todo_read.py +3 -5
  101. hanzo_mcp/tools/todo/todo_write.py +3 -5
  102. hanzo_mcp/tools/vector/__init__.py +6 -1
  103. hanzo_mcp/tools/vector/git_ingester.py +3 -0
  104. hanzo_mcp/tools/vector/index_tool.py +358 -0
  105. hanzo_mcp/tools/vector/infinity_store.py +98 -0
  106. hanzo_mcp/tools/vector/project_manager.py +27 -5
  107. hanzo_mcp/tools/vector/vector.py +311 -0
  108. hanzo_mcp/tools/vector/vector_index.py +1 -1
  109. hanzo_mcp/tools/vector/vector_search.py +12 -7
  110. hanzo_mcp-0.6.1.dist-info/METADATA +336 -0
  111. hanzo_mcp-0.6.1.dist-info/RECORD +134 -0
  112. hanzo_mcp-0.6.1.dist-info/entry_points.txt +3 -0
  113. hanzo_mcp-0.5.1.dist-info/METADATA +0 -276
  114. hanzo_mcp-0.5.1.dist-info/RECORD +0 -68
  115. hanzo_mcp-0.5.1.dist-info/entry_points.txt +0 -2
  116. {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.6.1.dist-info}/WHEEL +0 -0
  117. {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.6.1.dist-info}/licenses/LICENSE +0 -0
  118. {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.6.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,345 @@
1
+ """Graph statistics tool for analyzing the graph database."""
2
+
3
+ import json
4
+ import sqlite3
5
+ from typing import Annotated, Optional, TypedDict, Unpack, final, override
6
+ from collections import Counter, defaultdict
7
+
8
+ from mcp.server.fastmcp import Context as MCPContext
9
+ from pydantic import Field
10
+
11
+ from hanzo_mcp.tools.common.base import BaseTool
12
+ from hanzo_mcp.tools.common.context import create_tool_context
13
+ from hanzo_mcp.tools.common.permissions import PermissionManager
14
+ from hanzo_mcp.tools.database.database_manager import DatabaseManager
15
+
16
+
17
+ ProjectPath = Annotated[
18
+ Optional[str],
19
+ Field(
20
+ description="Project path (defaults to current directory)",
21
+ default=None,
22
+ ),
23
+ ]
24
+
25
+ Detailed = Annotated[
26
+ bool,
27
+ Field(
28
+ description="Show detailed statistics",
29
+ default=False,
30
+ ),
31
+ ]
32
+
33
+ NodeType = Annotated[
34
+ Optional[str],
35
+ Field(
36
+ description="Filter stats by node type",
37
+ default=None,
38
+ ),
39
+ ]
40
+
41
+ Relationship = Annotated[
42
+ Optional[str],
43
+ Field(
44
+ description="Filter stats by relationship type",
45
+ default=None,
46
+ ),
47
+ ]
48
+
49
+
50
+ class GraphStatsParams(TypedDict, total=False):
51
+ """Parameters for graph stats tool."""
52
+
53
+ project_path: Optional[str]
54
+ detailed: bool
55
+ node_type: Optional[str]
56
+ relationship: Optional[str]
57
+
58
+
59
+ @final
60
+ class GraphStatsTool(BaseTool):
61
+ """Tool for getting graph database statistics."""
62
+
63
+ def __init__(self, permission_manager: PermissionManager, db_manager: DatabaseManager):
64
+ """Initialize the graph stats tool.
65
+
66
+ Args:
67
+ permission_manager: Permission manager for access control
68
+ db_manager: Database manager instance
69
+ """
70
+ self.permission_manager = permission_manager
71
+ self.db_manager = db_manager
72
+
73
+ @property
74
+ @override
75
+ def name(self) -> str:
76
+ """Get the tool name."""
77
+ return "graph_stats"
78
+
79
+ @property
80
+ @override
81
+ def description(self) -> str:
82
+ """Get the tool description."""
83
+ return """Get statistics about the project's graph database.
84
+
85
+ Shows:
86
+ - Node and edge counts
87
+ - Node type distribution
88
+ - Relationship type distribution
89
+ - Degree statistics (connections per node)
90
+ - Connected components
91
+ - Most connected nodes (hubs)
92
+ - Orphaned nodes
93
+
94
+ Examples:
95
+ - graph_stats # Basic stats
96
+ - graph_stats --detailed # Detailed analysis
97
+ - graph_stats --node-type "class" # Stats for specific node type
98
+ - graph_stats --relationship "calls" # Stats for specific relationship
99
+ """
100
+
101
+ @override
102
+ async def call(
103
+ self,
104
+ ctx: MCPContext,
105
+ **params: Unpack[GraphStatsParams],
106
+ ) -> str:
107
+ """Get graph statistics.
108
+
109
+ Args:
110
+ ctx: MCP context
111
+ **params: Tool parameters
112
+
113
+ Returns:
114
+ Graph statistics
115
+ """
116
+ tool_ctx = create_tool_context(ctx)
117
+ await tool_ctx.set_tool_info(self.name)
118
+
119
+ # Extract parameters
120
+ project_path = params.get("project_path")
121
+ detailed = params.get("detailed", False)
122
+ node_type_filter = params.get("node_type")
123
+ relationship_filter = params.get("relationship")
124
+
125
+ # Get project database
126
+ try:
127
+ if project_path:
128
+ project_db = self.db_manager.get_project_db(project_path)
129
+ else:
130
+ import os
131
+ project_db = self.db_manager.get_project_for_path(os.getcwd())
132
+
133
+ if not project_db:
134
+ return "Error: Could not find project database"
135
+
136
+ except PermissionError as e:
137
+ return str(e)
138
+ except Exception as e:
139
+ return f"Error accessing project database: {str(e)}"
140
+
141
+ await tool_ctx.info(f"Getting graph statistics for project: {project_db.project_path}")
142
+
143
+ # Get graph connection
144
+ graph_conn = project_db.get_graph_connection()
145
+
146
+ try:
147
+ cursor = graph_conn.cursor()
148
+ output = []
149
+ output.append(f"=== Graph Database Statistics ===")
150
+ output.append(f"Project: {project_db.project_path}")
151
+ output.append(f"Database: {project_db.graph_path}")
152
+ output.append("")
153
+
154
+ # Basic counts
155
+ if node_type_filter:
156
+ cursor.execute("SELECT COUNT(*) FROM nodes WHERE type = ?", (node_type_filter,))
157
+ node_count = cursor.fetchone()[0]
158
+ output.append(f"Nodes (type='{node_type_filter}'): {node_count:,}")
159
+ else:
160
+ cursor.execute("SELECT COUNT(*) FROM nodes")
161
+ node_count = cursor.fetchone()[0]
162
+ output.append(f"Total Nodes: {node_count:,}")
163
+
164
+ if relationship_filter:
165
+ cursor.execute("SELECT COUNT(*) FROM edges WHERE relationship = ?", (relationship_filter,))
166
+ edge_count = cursor.fetchone()[0]
167
+ output.append(f"Edges (relationship='{relationship_filter}'): {edge_count:,}")
168
+ else:
169
+ cursor.execute("SELECT COUNT(*) FROM edges")
170
+ edge_count = cursor.fetchone()[0]
171
+ output.append(f"Total Edges: {edge_count:,}")
172
+
173
+ if node_count == 0:
174
+ output.append("\nGraph is empty.")
175
+ return "\n".join(output)
176
+
177
+ output.append("")
178
+
179
+ # Node type distribution
180
+ output.append("=== Node Types ===")
181
+ cursor.execute("SELECT type, COUNT(*) as count FROM nodes GROUP BY type ORDER BY count DESC")
182
+ node_types = cursor.fetchall()
183
+
184
+ for n_type, count in node_types[:10]:
185
+ pct = (count / node_count) * 100
186
+ output.append(f"{n_type}: {count:,} ({pct:.1f}%)")
187
+
188
+ if len(node_types) > 10:
189
+ output.append(f"... and {len(node_types) - 10} more types")
190
+
191
+ output.append("")
192
+
193
+ # Relationship distribution
194
+ output.append("=== Relationship Types ===")
195
+ cursor.execute("SELECT relationship, COUNT(*) as count FROM edges GROUP BY relationship ORDER BY count DESC")
196
+ rel_types = cursor.fetchall()
197
+
198
+ if rel_types:
199
+ for rel, count in rel_types[:10]:
200
+ pct = (count / edge_count) * 100 if edge_count > 0 else 0
201
+ output.append(f"{rel}: {count:,} ({pct:.1f}%)")
202
+
203
+ if len(rel_types) > 10:
204
+ output.append(f"... and {len(rel_types) - 10} more types")
205
+ else:
206
+ output.append("No edges in graph")
207
+
208
+ output.append("")
209
+
210
+ # Degree statistics
211
+ output.append("=== Connectivity ===")
212
+
213
+ # Calculate degrees
214
+ degrees = defaultdict(int)
215
+
216
+ # Out-degree
217
+ query = "SELECT source, COUNT(*) FROM edges"
218
+ if relationship_filter:
219
+ query += " WHERE relationship = ?"
220
+ cursor.execute(query + " GROUP BY source", (relationship_filter,))
221
+ else:
222
+ cursor.execute(query + " GROUP BY source")
223
+
224
+ for node, out_degree in cursor.fetchall():
225
+ degrees[node] += out_degree
226
+
227
+ # In-degree
228
+ query = "SELECT target, COUNT(*) FROM edges"
229
+ if relationship_filter:
230
+ query += " WHERE relationship = ?"
231
+ cursor.execute(query + " GROUP BY target", (relationship_filter,))
232
+ else:
233
+ cursor.execute(query + " GROUP BY target")
234
+
235
+ for node, in_degree in cursor.fetchall():
236
+ degrees[node] += in_degree
237
+
238
+ if degrees:
239
+ degree_values = list(degrees.values())
240
+ avg_degree = sum(degree_values) / len(degree_values)
241
+ max_degree = max(degree_values)
242
+ min_degree = min(degree_values)
243
+
244
+ output.append(f"Average degree: {avg_degree:.2f}")
245
+ output.append(f"Max degree: {max_degree}")
246
+ output.append(f"Min degree: {min_degree}")
247
+
248
+ # Most connected nodes
249
+ output.append("\nMost connected nodes:")
250
+ sorted_nodes = sorted(degrees.items(), key=lambda x: x[1], reverse=True)
251
+
252
+ for node, degree in sorted_nodes[:5]:
253
+ cursor.execute("SELECT type FROM nodes WHERE id = ?", (node,))
254
+ node_type = cursor.fetchone()
255
+ type_str = f" ({node_type[0]})" if node_type else ""
256
+ output.append(f" {node}{type_str}: {degree} connections")
257
+
258
+ # Orphaned nodes
259
+ cursor.execute("""
260
+ SELECT COUNT(*) FROM nodes n
261
+ WHERE NOT EXISTS (SELECT 1 FROM edges WHERE source = n.id OR target = n.id)
262
+ """)
263
+ orphan_count = cursor.fetchone()[0]
264
+ if orphan_count > 0:
265
+ orphan_pct = (orphan_count / node_count) * 100
266
+ output.append(f"\nOrphaned nodes: {orphan_count} ({orphan_pct:.1f}%)")
267
+
268
+ if detailed:
269
+ output.append("\n=== Detailed Analysis ===")
270
+
271
+ # Node properties usage
272
+ cursor.execute("SELECT COUNT(*) FROM nodes WHERE properties IS NOT NULL")
273
+ nodes_with_props = cursor.fetchone()[0]
274
+ if nodes_with_props > 0:
275
+ props_pct = (nodes_with_props / node_count) * 100
276
+ output.append(f"Nodes with properties: {nodes_with_props} ({props_pct:.1f}%)")
277
+
278
+ # Edge properties usage
279
+ cursor.execute("SELECT COUNT(*) FROM edges WHERE properties IS NOT NULL")
280
+ edges_with_props = cursor.fetchone()[0]
281
+ if edges_with_props > 0 and edge_count > 0:
282
+ props_pct = (edges_with_props / edge_count) * 100
283
+ output.append(f"Edges with properties: {edges_with_props} ({props_pct:.1f}%)")
284
+
285
+ # Weight distribution
286
+ cursor.execute("SELECT MIN(weight), MAX(weight), AVG(weight) FROM edges")
287
+ weight_stats = cursor.fetchone()
288
+ if weight_stats[0] is not None:
289
+ output.append(f"\nEdge weights:")
290
+ output.append(f" Min: {weight_stats[0]}")
291
+ output.append(f" Max: {weight_stats[1]}")
292
+ output.append(f" Avg: {weight_stats[2]:.2f}")
293
+
294
+ # Most common patterns
295
+ if not relationship_filter:
296
+ output.append("\n=== Common Patterns ===")
297
+
298
+ # Most common node type connections
299
+ cursor.execute("""
300
+ SELECT n1.type, e.relationship, n2.type, COUNT(*) as count
301
+ FROM edges e
302
+ JOIN nodes n1 ON e.source = n1.id
303
+ JOIN nodes n2 ON e.target = n2.id
304
+ GROUP BY n1.type, e.relationship, n2.type
305
+ ORDER BY count DESC
306
+ LIMIT 10
307
+ """)
308
+
309
+ patterns = cursor.fetchall()
310
+ if patterns:
311
+ output.append("Most common connections:")
312
+ for src_type, rel, tgt_type, count in patterns:
313
+ output.append(f" {src_type} --[{rel}]--> {tgt_type}: {count} times")
314
+
315
+ # Component analysis (simplified)
316
+ output.append("\n=== Graph Structure ===")
317
+
318
+ # Check if graph is fully connected (simplified)
319
+ cursor.execute("""
320
+ SELECT COUNT(DISTINCT node_id) FROM (
321
+ SELECT source as node_id FROM edges
322
+ UNION
323
+ SELECT target as node_id FROM edges
324
+ )
325
+ """)
326
+ connected_nodes = cursor.fetchone()[0]
327
+
328
+ if connected_nodes < node_count:
329
+ output.append(f"Connected nodes: {connected_nodes} / {node_count}")
330
+ output.append("Graph has disconnected components")
331
+ else:
332
+ output.append("All nodes are connected")
333
+
334
+ return "\n".join(output)
335
+
336
+ except sqlite3.Error as e:
337
+ await tool_ctx.error(f"SQL error: {str(e)}")
338
+ return f"SQL error: {str(e)}"
339
+ except Exception as e:
340
+ await tool_ctx.error(f"Unexpected error: {str(e)}")
341
+ return f"Error getting statistics: {str(e)}"
342
+
343
+ def register(self, mcp_server) -> None:
344
+ """Register this tool with the MCP server."""
345
+ pass