hanzo-mcp 0.7.6__py3-none-any.whl → 0.8.0__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 (178) hide show
  1. hanzo_mcp/__init__.py +7 -1
  2. hanzo_mcp/__main__.py +1 -1
  3. hanzo_mcp/analytics/__init__.py +2 -2
  4. hanzo_mcp/analytics/posthog_analytics.py +76 -82
  5. hanzo_mcp/cli.py +31 -36
  6. hanzo_mcp/cli_enhanced.py +94 -72
  7. hanzo_mcp/cli_plugin.py +27 -17
  8. hanzo_mcp/config/__init__.py +2 -2
  9. hanzo_mcp/config/settings.py +112 -88
  10. hanzo_mcp/config/tool_config.py +32 -34
  11. hanzo_mcp/dev_server.py +66 -67
  12. hanzo_mcp/prompts/__init__.py +94 -12
  13. hanzo_mcp/prompts/enhanced_prompts.py +809 -0
  14. hanzo_mcp/prompts/example_custom_prompt.py +6 -5
  15. hanzo_mcp/prompts/project_todo_reminder.py +0 -1
  16. hanzo_mcp/prompts/tool_explorer.py +10 -7
  17. hanzo_mcp/server.py +17 -21
  18. hanzo_mcp/server_enhanced.py +15 -22
  19. hanzo_mcp/tools/__init__.py +56 -28
  20. hanzo_mcp/tools/agent/__init__.py +16 -19
  21. hanzo_mcp/tools/agent/agent.py +82 -65
  22. hanzo_mcp/tools/agent/agent_tool.py +152 -122
  23. hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +66 -62
  24. hanzo_mcp/tools/agent/clarification_protocol.py +55 -50
  25. hanzo_mcp/tools/agent/clarification_tool.py +11 -10
  26. hanzo_mcp/tools/agent/claude_cli_tool.py +21 -20
  27. hanzo_mcp/tools/agent/claude_desktop_auth.py +130 -144
  28. hanzo_mcp/tools/agent/cli_agent_base.py +59 -53
  29. hanzo_mcp/tools/agent/code_auth.py +102 -107
  30. hanzo_mcp/tools/agent/code_auth_tool.py +28 -27
  31. hanzo_mcp/tools/agent/codex_cli_tool.py +20 -19
  32. hanzo_mcp/tools/agent/critic_tool.py +86 -73
  33. hanzo_mcp/tools/agent/gemini_cli_tool.py +21 -20
  34. hanzo_mcp/tools/agent/grok_cli_tool.py +21 -20
  35. hanzo_mcp/tools/agent/iching_tool.py +404 -139
  36. hanzo_mcp/tools/agent/network_tool.py +89 -73
  37. hanzo_mcp/tools/agent/prompt.py +2 -1
  38. hanzo_mcp/tools/agent/review_tool.py +101 -98
  39. hanzo_mcp/tools/agent/swarm_alias.py +87 -0
  40. hanzo_mcp/tools/agent/swarm_tool.py +246 -161
  41. hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +134 -92
  42. hanzo_mcp/tools/agent/tool_adapter.py +21 -11
  43. hanzo_mcp/tools/common/__init__.py +1 -1
  44. hanzo_mcp/tools/common/base.py +3 -5
  45. hanzo_mcp/tools/common/batch_tool.py +46 -39
  46. hanzo_mcp/tools/common/config_tool.py +120 -84
  47. hanzo_mcp/tools/common/context.py +1 -5
  48. hanzo_mcp/tools/common/context_fix.py +5 -3
  49. hanzo_mcp/tools/common/critic_tool.py +4 -8
  50. hanzo_mcp/tools/common/decorators.py +58 -56
  51. hanzo_mcp/tools/common/enhanced_base.py +29 -32
  52. hanzo_mcp/tools/common/fastmcp_pagination.py +91 -94
  53. hanzo_mcp/tools/common/forgiving_edit.py +91 -87
  54. hanzo_mcp/tools/common/mode.py +15 -17
  55. hanzo_mcp/tools/common/mode_loader.py +27 -24
  56. hanzo_mcp/tools/common/paginated_base.py +61 -53
  57. hanzo_mcp/tools/common/paginated_response.py +72 -79
  58. hanzo_mcp/tools/common/pagination.py +50 -53
  59. hanzo_mcp/tools/common/permissions.py +4 -4
  60. hanzo_mcp/tools/common/personality.py +186 -138
  61. hanzo_mcp/tools/common/plugin_loader.py +54 -54
  62. hanzo_mcp/tools/common/stats.py +65 -47
  63. hanzo_mcp/tools/common/test_helpers.py +31 -0
  64. hanzo_mcp/tools/common/thinking_tool.py +4 -8
  65. hanzo_mcp/tools/common/tool_disable.py +17 -12
  66. hanzo_mcp/tools/common/tool_enable.py +13 -14
  67. hanzo_mcp/tools/common/tool_list.py +36 -28
  68. hanzo_mcp/tools/common/truncate.py +23 -23
  69. hanzo_mcp/tools/config/__init__.py +4 -4
  70. hanzo_mcp/tools/config/config_tool.py +42 -29
  71. hanzo_mcp/tools/config/index_config.py +37 -34
  72. hanzo_mcp/tools/config/mode_tool.py +175 -55
  73. hanzo_mcp/tools/database/__init__.py +15 -12
  74. hanzo_mcp/tools/database/database_manager.py +77 -75
  75. hanzo_mcp/tools/database/graph.py +137 -91
  76. hanzo_mcp/tools/database/graph_add.py +30 -18
  77. hanzo_mcp/tools/database/graph_query.py +178 -102
  78. hanzo_mcp/tools/database/graph_remove.py +33 -28
  79. hanzo_mcp/tools/database/graph_search.py +97 -75
  80. hanzo_mcp/tools/database/graph_stats.py +91 -59
  81. hanzo_mcp/tools/database/sql.py +107 -79
  82. hanzo_mcp/tools/database/sql_query.py +30 -24
  83. hanzo_mcp/tools/database/sql_search.py +29 -25
  84. hanzo_mcp/tools/database/sql_stats.py +47 -35
  85. hanzo_mcp/tools/editor/neovim_command.py +25 -28
  86. hanzo_mcp/tools/editor/neovim_edit.py +21 -23
  87. hanzo_mcp/tools/editor/neovim_session.py +60 -54
  88. hanzo_mcp/tools/filesystem/__init__.py +31 -30
  89. hanzo_mcp/tools/filesystem/ast_multi_edit.py +329 -249
  90. hanzo_mcp/tools/filesystem/ast_tool.py +4 -4
  91. hanzo_mcp/tools/filesystem/base.py +1 -1
  92. hanzo_mcp/tools/filesystem/batch_search.py +316 -224
  93. hanzo_mcp/tools/filesystem/content_replace.py +4 -4
  94. hanzo_mcp/tools/filesystem/diff.py +71 -59
  95. hanzo_mcp/tools/filesystem/directory_tree.py +7 -7
  96. hanzo_mcp/tools/filesystem/directory_tree_paginated.py +49 -37
  97. hanzo_mcp/tools/filesystem/edit.py +4 -4
  98. hanzo_mcp/tools/filesystem/find.py +173 -80
  99. hanzo_mcp/tools/filesystem/find_files.py +73 -52
  100. hanzo_mcp/tools/filesystem/git_search.py +157 -104
  101. hanzo_mcp/tools/filesystem/grep.py +8 -8
  102. hanzo_mcp/tools/filesystem/multi_edit.py +4 -8
  103. hanzo_mcp/tools/filesystem/read.py +12 -10
  104. hanzo_mcp/tools/filesystem/rules_tool.py +59 -43
  105. hanzo_mcp/tools/filesystem/search_tool.py +263 -207
  106. hanzo_mcp/tools/filesystem/symbols_tool.py +94 -54
  107. hanzo_mcp/tools/filesystem/tree.py +35 -33
  108. hanzo_mcp/tools/filesystem/unix_aliases.py +13 -18
  109. hanzo_mcp/tools/filesystem/watch.py +37 -36
  110. hanzo_mcp/tools/filesystem/write.py +4 -8
  111. hanzo_mcp/tools/jupyter/__init__.py +4 -4
  112. hanzo_mcp/tools/jupyter/base.py +4 -5
  113. hanzo_mcp/tools/jupyter/jupyter.py +67 -47
  114. hanzo_mcp/tools/jupyter/notebook_edit.py +4 -4
  115. hanzo_mcp/tools/jupyter/notebook_read.py +4 -7
  116. hanzo_mcp/tools/llm/__init__.py +5 -7
  117. hanzo_mcp/tools/llm/consensus_tool.py +72 -52
  118. hanzo_mcp/tools/llm/llm_manage.py +101 -60
  119. hanzo_mcp/tools/llm/llm_tool.py +226 -166
  120. hanzo_mcp/tools/llm/provider_tools.py +25 -26
  121. hanzo_mcp/tools/lsp/__init__.py +1 -1
  122. hanzo_mcp/tools/lsp/lsp_tool.py +228 -143
  123. hanzo_mcp/tools/mcp/__init__.py +2 -3
  124. hanzo_mcp/tools/mcp/mcp_add.py +27 -25
  125. hanzo_mcp/tools/mcp/mcp_remove.py +7 -8
  126. hanzo_mcp/tools/mcp/mcp_stats.py +23 -22
  127. hanzo_mcp/tools/mcp/mcp_tool.py +129 -98
  128. hanzo_mcp/tools/memory/__init__.py +39 -21
  129. hanzo_mcp/tools/memory/knowledge_tools.py +124 -99
  130. hanzo_mcp/tools/memory/memory_tools.py +90 -108
  131. hanzo_mcp/tools/search/__init__.py +7 -2
  132. hanzo_mcp/tools/search/find_tool.py +297 -212
  133. hanzo_mcp/tools/search/unified_search.py +366 -314
  134. hanzo_mcp/tools/shell/__init__.py +8 -7
  135. hanzo_mcp/tools/shell/auto_background.py +56 -49
  136. hanzo_mcp/tools/shell/base.py +1 -1
  137. hanzo_mcp/tools/shell/base_process.py +75 -75
  138. hanzo_mcp/tools/shell/bash_session.py +2 -2
  139. hanzo_mcp/tools/shell/bash_session_executor.py +4 -4
  140. hanzo_mcp/tools/shell/bash_tool.py +24 -31
  141. hanzo_mcp/tools/shell/command_executor.py +12 -12
  142. hanzo_mcp/tools/shell/logs.py +43 -33
  143. hanzo_mcp/tools/shell/npx.py +13 -13
  144. hanzo_mcp/tools/shell/npx_background.py +24 -21
  145. hanzo_mcp/tools/shell/npx_tool.py +18 -22
  146. hanzo_mcp/tools/shell/open.py +19 -21
  147. hanzo_mcp/tools/shell/pkill.py +31 -26
  148. hanzo_mcp/tools/shell/process_tool.py +32 -32
  149. hanzo_mcp/tools/shell/processes.py +57 -58
  150. hanzo_mcp/tools/shell/run_background.py +24 -25
  151. hanzo_mcp/tools/shell/run_command.py +5 -5
  152. hanzo_mcp/tools/shell/run_command_windows.py +5 -5
  153. hanzo_mcp/tools/shell/session_storage.py +3 -3
  154. hanzo_mcp/tools/shell/streaming_command.py +141 -126
  155. hanzo_mcp/tools/shell/uvx.py +24 -25
  156. hanzo_mcp/tools/shell/uvx_background.py +35 -33
  157. hanzo_mcp/tools/shell/uvx_tool.py +18 -22
  158. hanzo_mcp/tools/todo/__init__.py +6 -2
  159. hanzo_mcp/tools/todo/todo.py +50 -37
  160. hanzo_mcp/tools/todo/todo_read.py +5 -8
  161. hanzo_mcp/tools/todo/todo_write.py +5 -7
  162. hanzo_mcp/tools/vector/__init__.py +40 -28
  163. hanzo_mcp/tools/vector/ast_analyzer.py +176 -143
  164. hanzo_mcp/tools/vector/git_ingester.py +170 -179
  165. hanzo_mcp/tools/vector/index_tool.py +96 -44
  166. hanzo_mcp/tools/vector/infinity_store.py +283 -228
  167. hanzo_mcp/tools/vector/mock_infinity.py +39 -40
  168. hanzo_mcp/tools/vector/project_manager.py +88 -78
  169. hanzo_mcp/tools/vector/vector.py +59 -42
  170. hanzo_mcp/tools/vector/vector_index.py +30 -27
  171. hanzo_mcp/tools/vector/vector_search.py +64 -45
  172. hanzo_mcp/types.py +6 -4
  173. {hanzo_mcp-0.7.6.dist-info → hanzo_mcp-0.8.0.dist-info}/METADATA +1 -1
  174. hanzo_mcp-0.8.0.dist-info/RECORD +185 -0
  175. hanzo_mcp-0.7.6.dist-info/RECORD +0 -182
  176. {hanzo_mcp-0.7.6.dist-info → hanzo_mcp-0.8.0.dist-info}/WHEEL +0 -0
  177. {hanzo_mcp-0.7.6.dist-info → hanzo_mcp-0.8.0.dist-info}/entry_points.txt +0 -0
  178. {hanzo_mcp-0.7.6.dist-info → hanzo_mcp-0.8.0.dist-info}/top_level.txt +0 -0
@@ -1,17 +1,24 @@
1
1
  """Unified SQL database tool."""
2
2
 
3
- from typing import Annotated, TypedDict, Unpack, final, override, Optional, List, Dict, Any
4
3
  import sqlite3
5
- from pathlib import Path
4
+ from typing import (
5
+ Any,
6
+ Dict,
7
+ Unpack,
8
+ Optional,
9
+ Annotated,
10
+ TypedDict,
11
+ final,
12
+ override,
13
+ )
6
14
 
7
- from mcp.server.fastmcp import Context as MCPContext
8
15
  from pydantic import Field
16
+ from mcp.server.fastmcp import Context as MCPContext
9
17
 
10
18
  from hanzo_mcp.tools.common.base import BaseTool
11
19
  from hanzo_mcp.tools.common.permissions import PermissionManager
12
20
  from hanzo_mcp.tools.database.database_manager import DatabaseManager
13
21
 
14
-
15
22
  # Parameter types
16
23
  Query = Annotated[
17
24
  Optional[str],
@@ -56,6 +63,7 @@ Limit = Annotated[
56
63
 
57
64
  class SQLParams(TypedDict, total=False):
58
65
  """Parameters for SQL tool."""
66
+
59
67
  query: Optional[str]
60
68
  pattern: Optional[str]
61
69
  table: Optional[str]
@@ -66,8 +74,10 @@ class SQLParams(TypedDict, total=False):
66
74
  @final
67
75
  class SQLTool(BaseTool):
68
76
  """Unified SQL database tool."""
69
-
70
- def __init__(self, permission_manager: PermissionManager, db_manager: DatabaseManager):
77
+
78
+ def __init__(
79
+ self, permission_manager: PermissionManager, db_manager: DatabaseManager
80
+ ):
71
81
  """Initialize the SQL tool."""
72
82
  super().__init__(permission_manager)
73
83
  self.db_manager = db_manager
@@ -107,7 +117,7 @@ sql --action stats --table users
107
117
 
108
118
  # Extract action
109
119
  action = params.get("action", "query")
110
-
120
+
111
121
  # Route to appropriate handler
112
122
  if action == "query":
113
123
  return await self._handle_query(project_db, params, tool_ctx)
@@ -125,36 +135,36 @@ sql --action stats --table users
125
135
  query = params.get("query")
126
136
  if not query:
127
137
  return "Error: query required for query action"
128
-
138
+
129
139
  limit = params.get("limit", 100)
130
-
140
+
131
141
  try:
132
142
  with project_db.get_sqlite_connection() as conn:
133
143
  # Enable row factory for dict-like access
134
144
  conn.row_factory = sqlite3.Row
135
-
145
+
136
146
  # Add LIMIT if not present in SELECT queries
137
147
  query_upper = query.upper().strip()
138
148
  if query_upper.startswith("SELECT") and "LIMIT" not in query_upper:
139
149
  query = f"{query} LIMIT {limit}"
140
-
150
+
141
151
  cursor = conn.execute(query)
142
-
152
+
143
153
  # Handle different query types
144
154
  if query_upper.startswith("SELECT"):
145
155
  rows = cursor.fetchall()
146
-
156
+
147
157
  if not rows:
148
158
  return "No results found"
149
-
159
+
150
160
  # Get column names
151
161
  columns = [description[0] for description in cursor.description]
152
-
162
+
153
163
  # Format as table
154
164
  output = ["=== Query Results ==="]
155
165
  output.append(f"Columns: {', '.join(columns)}")
156
166
  output.append("-" * 60)
157
-
167
+
158
168
  for row in rows:
159
169
  row_data = []
160
170
  for col in columns:
@@ -165,18 +175,18 @@ sql --action stats --table users
165
175
  value = value[:50] + "..."
166
176
  row_data.append(str(value))
167
177
  output.append(" | ".join(row_data))
168
-
178
+
169
179
  output.append(f"\nRows returned: {len(rows)}")
170
180
  if len(rows) == limit:
171
181
  output.append(f"(Limited to {limit} rows)")
172
-
182
+
173
183
  return "\n".join(output)
174
-
184
+
175
185
  else:
176
186
  # For INSERT, UPDATE, DELETE
177
187
  conn.commit()
178
188
  rows_affected = cursor.rowcount
179
-
189
+
180
190
  if query_upper.startswith("INSERT"):
181
191
  return f"Inserted {rows_affected} row(s)"
182
192
  elif query_upper.startswith("UPDATE"):
@@ -185,7 +195,7 @@ sql --action stats --table users
185
195
  return f"Deleted {rows_affected} row(s)"
186
196
  else:
187
197
  return f"Query executed successfully. Rows affected: {rows_affected}"
188
-
198
+
189
199
  except Exception as e:
190
200
  await tool_ctx.error(f"Query failed: {str(e)}")
191
201
  return f"Error executing query: {str(e)}"
@@ -195,53 +205,55 @@ sql --action stats --table users
195
205
  pattern = params.get("pattern")
196
206
  if not pattern:
197
207
  return "Error: pattern required for search action"
198
-
208
+
199
209
  table = params.get("table")
200
210
  limit = params.get("limit", 100)
201
-
211
+
202
212
  try:
203
213
  with project_db.get_sqlite_connection() as conn:
204
214
  conn.row_factory = sqlite3.Row
205
-
215
+
206
216
  # Get all tables if not specified
207
217
  if not table:
208
- cursor = conn.execute("""
218
+ cursor = conn.execute(
219
+ """
209
220
  SELECT name FROM sqlite_master
210
221
  WHERE type='table' AND name NOT LIKE 'sqlite_%'
211
- """)
222
+ """
223
+ )
212
224
  tables = [row[0] for row in cursor.fetchall()]
213
225
  else:
214
226
  tables = [table]
215
-
227
+
216
228
  all_results = []
217
-
229
+
218
230
  for tbl in tables:
219
231
  # Get columns
220
232
  cursor = conn.execute(f"PRAGMA table_info({tbl})")
221
233
  columns = [row[1] for row in cursor.fetchall()]
222
-
234
+
223
235
  # Build search query
224
236
  where_clauses = [f"{col} LIKE ?" for col in columns]
225
237
  query = f"SELECT * FROM {tbl} WHERE {' OR '.join(where_clauses)} LIMIT {limit}"
226
-
238
+
227
239
  # Search
228
240
  cursor = conn.execute(query, [f"%{pattern}%"] * len(columns))
229
241
  rows = cursor.fetchall()
230
-
242
+
231
243
  if rows:
232
244
  all_results.append((tbl, columns, rows))
233
-
245
+
234
246
  if not all_results:
235
247
  return f"No results found for pattern '{pattern}'"
236
-
248
+
237
249
  # Format results
238
250
  output = [f"=== Search Results for '{pattern}' ==="]
239
-
251
+
240
252
  for tbl, columns, rows in all_results:
241
253
  output.append(f"\nTable: {tbl}")
242
254
  output.append(f"Columns: {', '.join(columns)}")
243
255
  output.append("-" * 60)
244
-
256
+
245
257
  for row in rows:
246
258
  row_data = []
247
259
  for col in columns:
@@ -256,11 +268,11 @@ sql --action stats --table users
256
268
  value = value[:50] + "..."
257
269
  row_data.append(str(value))
258
270
  output.append(" | ".join(row_data))
259
-
271
+
260
272
  output.append(f"Found {len(rows)} row(s) in {tbl}")
261
-
273
+
262
274
  return "\n".join(output)
263
-
275
+
264
276
  except Exception as e:
265
277
  await tool_ctx.error(f"Search failed: {str(e)}")
266
278
  return f"Error during search: {str(e)}"
@@ -268,62 +280,68 @@ sql --action stats --table users
268
280
  async def _handle_schema(self, project_db, params: Dict[str, Any], tool_ctx) -> str:
269
281
  """Show database schema."""
270
282
  table = params.get("table")
271
-
283
+
272
284
  try:
273
285
  with project_db.get_sqlite_connection() as conn:
274
286
  if table:
275
287
  # Show specific table schema
276
288
  cursor = conn.execute(f"PRAGMA table_info({table})")
277
289
  columns = cursor.fetchall()
278
-
290
+
279
291
  if not columns:
280
292
  return f"Table '{table}' not found"
281
-
293
+
282
294
  output = [f"=== Schema for table '{table}' ==="]
283
295
  output.append("Column | Type | Not Null | Default | Primary Key")
284
296
  output.append("-" * 60)
285
-
297
+
286
298
  for col in columns:
287
- output.append(f"{col[1]} | {col[2]} | {col[3]} | {col[4]} | {col[5]}")
288
-
299
+ output.append(
300
+ f"{col[1]} | {col[2]} | {col[3]} | {col[4]} | {col[5]}"
301
+ )
302
+
289
303
  # Get indexes
290
304
  cursor = conn.execute(f"PRAGMA index_list({table})")
291
305
  indexes = cursor.fetchall()
292
-
306
+
293
307
  if indexes:
294
308
  output.append("\nIndexes:")
295
309
  for idx in indexes:
296
310
  output.append(f" {idx[1]} (unique: {idx[2]})")
297
-
311
+
298
312
  else:
299
313
  # Show all tables
300
- cursor = conn.execute("""
314
+ cursor = conn.execute(
315
+ """
301
316
  SELECT name, sql FROM sqlite_master
302
317
  WHERE type='table' AND name NOT LIKE 'sqlite_%'
303
318
  ORDER BY name
304
- """)
319
+ """
320
+ )
305
321
  tables = cursor.fetchall()
306
-
322
+
307
323
  if not tables:
308
324
  return "No tables found in database"
309
-
325
+
310
326
  output = ["=== Database Schema ==="]
311
-
312
- for table_name, create_sql in tables:
327
+
328
+ for table_name, _create_sql in tables:
313
329
  output.append(f"\nTable: {table_name}")
314
-
330
+
315
331
  # Get row count
316
332
  cursor = conn.execute(f"SELECT COUNT(*) FROM {table_name}")
317
333
  count = cursor.fetchone()[0]
318
334
  output.append(f"Rows: {count}")
319
-
335
+
320
336
  # Get columns
321
337
  cursor = conn.execute(f"PRAGMA table_info({table_name})")
322
338
  columns = cursor.fetchall()
323
- output.append(f"Columns: {', '.join([col[1] for col in columns])}")
324
-
339
+ output.append(
340
+ f"Columns: {', '.join([col[1] for col in columns])}"
341
+ )
342
+
325
343
  return "\n".join(output)
326
-
344
+
327
345
  except Exception as e:
328
346
  await tool_ctx.error(f"Failed to get schema: {str(e)}")
329
347
  return f"Error getting schema: {str(e)}"
@@ -331,81 +349,91 @@ sql --action stats --table users
331
349
  async def _handle_stats(self, project_db, params: Dict[str, Any], tool_ctx) -> str:
332
350
  """Get database statistics."""
333
351
  table = params.get("table")
334
-
352
+
335
353
  try:
336
354
  with project_db.get_sqlite_connection() as conn:
337
355
  output = ["=== Database Statistics ==="]
338
356
  output.append(f"Database: {project_db.sqlite_path}")
339
-
357
+
340
358
  # Get file size
341
359
  db_size = project_db.sqlite_path.stat().st_size
342
360
  output.append(f"Size: {db_size / 1024 / 1024:.2f} MB")
343
-
361
+
344
362
  if table:
345
363
  # Stats for specific table
346
364
  cursor = conn.execute(f"SELECT COUNT(*) FROM {table}")
347
365
  count = cursor.fetchone()[0]
348
-
366
+
349
367
  output.append(f"\nTable: {table}")
350
368
  output.append(f"Total rows: {count}")
351
-
369
+
352
370
  # Get column stats
353
371
  cursor = conn.execute(f"PRAGMA table_info({table})")
354
372
  columns = cursor.fetchall()
355
-
373
+
356
374
  output.append("\nColumn statistics:")
357
375
  for col in columns:
358
376
  col_name = col[1]
359
377
  col_type = col[2]
360
-
378
+
361
379
  # Get basic stats based on type
362
380
  if "INT" in col_type.upper() or "REAL" in col_type.upper():
363
- cursor = conn.execute(f"""
381
+ cursor = conn.execute(
382
+ f"""
364
383
  SELECT
365
384
  MIN({col_name}) as min_val,
366
385
  MAX({col_name}) as max_val,
367
386
  AVG({col_name}) as avg_val,
368
387
  COUNT(DISTINCT {col_name}) as distinct_count
369
388
  FROM {table}
370
- """)
389
+ """
390
+ )
371
391
  stats = cursor.fetchone()
372
- output.append(f" {col_name}: min={stats[0]}, max={stats[1]}, avg={stats[2]:.2f}, distinct={stats[3]}")
392
+ output.append(
393
+ f" {col_name}: min={stats[0]}, max={stats[1]}, avg={stats[2]:.2f}, distinct={stats[3]}"
394
+ )
373
395
  else:
374
- cursor = conn.execute(f"""
396
+ cursor = conn.execute(
397
+ f"""
375
398
  SELECT
376
399
  COUNT(DISTINCT {col_name}) as distinct_count,
377
400
  COUNT(*) - COUNT({col_name}) as null_count
378
401
  FROM {table}
379
- """)
402
+ """
403
+ )
380
404
  stats = cursor.fetchone()
381
- output.append(f" {col_name}: distinct={stats[0]}, nulls={stats[1]}")
382
-
405
+ output.append(
406
+ f" {col_name}: distinct={stats[0]}, nulls={stats[1]}"
407
+ )
408
+
383
409
  else:
384
410
  # Overall database stats
385
- cursor = conn.execute("""
411
+ cursor = conn.execute(
412
+ """
386
413
  SELECT name FROM sqlite_master
387
414
  WHERE type='table' AND name NOT LIKE 'sqlite_%'
388
- """)
415
+ """
416
+ )
389
417
  tables = cursor.fetchall()
390
-
418
+
391
419
  output.append(f"\nTotal tables: {len(tables)}")
392
420
  output.append("\nTable row counts:")
393
-
421
+
394
422
  total_rows = 0
395
423
  for (table_name,) in tables:
396
424
  cursor = conn.execute(f"SELECT COUNT(*) FROM {table_name}")
397
425
  count = cursor.fetchone()[0]
398
426
  total_rows += count
399
427
  output.append(f" {table_name}: {count} rows")
400
-
428
+
401
429
  output.append(f"\nTotal rows across all tables: {total_rows}")
402
-
430
+
403
431
  return "\n".join(output)
404
-
432
+
405
433
  except Exception as e:
406
434
  await tool_ctx.error(f"Failed to get stats: {str(e)}")
407
435
  return f"Error getting stats: {str(e)}"
408
436
 
409
437
  def register(self, mcp_server) -> None:
410
438
  """Register this tool with the MCP server."""
411
- pass
439
+ pass
@@ -1,17 +1,16 @@
1
1
  """SQL query tool for direct database queries."""
2
2
 
3
3
  import sqlite3
4
- from typing import Annotated, Optional, TypedDict, Unpack, final, override
4
+ from typing import Unpack, Optional, Annotated, TypedDict, final, override
5
5
 
6
- from mcp.server.fastmcp import Context as MCPContext
7
6
  from pydantic import Field
7
+ from mcp.server.fastmcp import Context as MCPContext
8
8
 
9
9
  from hanzo_mcp.tools.common.base import BaseTool
10
10
  from hanzo_mcp.tools.common.context import create_tool_context
11
11
  from hanzo_mcp.tools.common.permissions import PermissionManager
12
12
  from hanzo_mcp.tools.database.database_manager import DatabaseManager
13
13
 
14
-
15
14
  Query = Annotated[
16
15
  str,
17
16
  Field(
@@ -49,7 +48,9 @@ class SqlQueryParams(TypedDict, total=False):
49
48
  class SqlQueryTool(BaseTool):
50
49
  """Tool for executing SQL queries on project databases."""
51
50
 
52
- def __init__(self, permission_manager: PermissionManager, db_manager: DatabaseManager):
51
+ def __init__(
52
+ self, permission_manager: PermissionManager, db_manager: DatabaseManager
53
+ ):
53
54
  """Initialize the SQL query tool.
54
55
 
55
56
  Args:
@@ -121,8 +122,9 @@ Note: Use sql_search for text search operations."""
121
122
  project_db = self.db_manager.get_project_db(project_path)
122
123
  else:
123
124
  import os
125
+
124
126
  project_db = self.db_manager.get_project_for_path(os.getcwd())
125
-
127
+
126
128
  if not project_db:
127
129
  return "Error: Could not find project database"
128
130
 
@@ -134,39 +136,41 @@ Note: Use sql_search for text search operations."""
134
136
  # Check if query is read-only
135
137
  if read_only:
136
138
  # Simple check for write operations
137
- write_keywords = ['INSERT', 'UPDATE', 'DELETE', 'DROP', 'CREATE', 'ALTER']
139
+ write_keywords = ["INSERT", "UPDATE", "DELETE", "DROP", "CREATE", "ALTER"]
138
140
  query_upper = query.upper()
139
141
  for keyword in write_keywords:
140
142
  if keyword in query_upper:
141
143
  return f"Error: Query contains {keyword} operation. Set --read-only false to allow write operations."
142
144
 
143
- await tool_ctx.info(f"Executing SQL query on project: {project_db.project_path}")
145
+ await tool_ctx.info(
146
+ f"Executing SQL query on project: {project_db.project_path}"
147
+ )
144
148
 
145
149
  # Execute query
146
150
  conn = None
147
151
  try:
148
152
  conn = project_db.get_sqlite_connection()
149
153
  cursor = conn.cursor()
150
-
154
+
151
155
  # Execute the query
152
156
  cursor.execute(query)
153
-
157
+
154
158
  # Handle different query types
155
- if query.strip().upper().startswith('SELECT'):
159
+ if query.strip().upper().startswith("SELECT"):
156
160
  # Fetch results
157
161
  results = cursor.fetchall()
158
-
162
+
159
163
  if not results:
160
164
  return "No results found."
161
-
165
+
162
166
  # Get column names
163
167
  columns = [desc[0] for desc in cursor.description]
164
-
168
+
165
169
  # Format as table
166
170
  output = self._format_results_table(columns, results)
167
-
171
+
168
172
  return f"Query executed successfully. Found {len(results)} row(s).\n\n{output}"
169
-
173
+
170
174
  else:
171
175
  # For non-SELECT queries, commit and return affected rows
172
176
  conn.commit()
@@ -187,7 +191,7 @@ Note: Use sql_search for text search operations."""
187
191
  """Format query results as a table."""
188
192
  if not rows:
189
193
  return "No results"
190
-
194
+
191
195
  # Calculate column widths
192
196
  col_widths = []
193
197
  for i, col in enumerate(columns):
@@ -196,34 +200,36 @@ Note: Use sql_search for text search operations."""
196
200
  val_str = str(row[i]) if row[i] is not None else "NULL"
197
201
  max_width = max(max_width, len(val_str))
198
202
  col_widths.append(min(max_width, 50)) # Cap at 50 chars
199
-
203
+
200
204
  # Build header
201
205
  header = " | ".join(col.ljust(width) for col, width in zip(columns, col_widths))
202
206
  separator = "-+-".join("-" * width for width in col_widths)
203
-
207
+
204
208
  # Build rows
205
209
  output_rows = []
206
210
  for row in rows[:1000]: # Limit to 1000 rows
207
211
  row_str = " | ".join(
208
- self._truncate(str(val) if val is not None else "NULL", width).ljust(width)
212
+ self._truncate(str(val) if val is not None else "NULL", width).ljust(
213
+ width
214
+ )
209
215
  for val, width in zip(row, col_widths)
210
216
  )
211
217
  output_rows.append(row_str)
212
-
218
+
213
219
  # Combine
214
220
  output = [header, separator] + output_rows
215
-
221
+
216
222
  if len(rows) > 1000:
217
223
  output.append(f"\n... and {len(rows) - 1000} more rows")
218
-
224
+
219
225
  return "\n".join(output)
220
226
 
221
227
  def _truncate(self, text: str, max_width: int) -> str:
222
228
  """Truncate text to max width."""
223
229
  if len(text) <= max_width:
224
230
  return text
225
- return text[:max_width-3] + "..."
231
+ return text[: max_width - 3] + "..."
226
232
 
227
233
  def register(self, mcp_server) -> None:
228
234
  """Register this tool with the MCP server."""
229
- pass
235
+ pass