hanzo-mcp 0.7.7__py3-none-any.whl → 0.8.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.
- hanzo_mcp/__init__.py +6 -0
- hanzo_mcp/__main__.py +1 -1
- hanzo_mcp/analytics/__init__.py +2 -2
- hanzo_mcp/analytics/posthog_analytics.py +76 -82
- hanzo_mcp/cli.py +31 -36
- hanzo_mcp/cli_enhanced.py +94 -72
- hanzo_mcp/cli_plugin.py +27 -17
- hanzo_mcp/config/__init__.py +2 -2
- hanzo_mcp/config/settings.py +112 -88
- hanzo_mcp/config/tool_config.py +32 -34
- hanzo_mcp/dev_server.py +66 -67
- hanzo_mcp/prompts/__init__.py +94 -12
- hanzo_mcp/prompts/enhanced_prompts.py +809 -0
- hanzo_mcp/prompts/example_custom_prompt.py +6 -5
- hanzo_mcp/prompts/project_todo_reminder.py +0 -1
- hanzo_mcp/prompts/tool_explorer.py +10 -7
- hanzo_mcp/server.py +17 -21
- hanzo_mcp/server_enhanced.py +15 -22
- hanzo_mcp/tools/__init__.py +56 -28
- hanzo_mcp/tools/agent/__init__.py +16 -19
- hanzo_mcp/tools/agent/agent.py +82 -65
- hanzo_mcp/tools/agent/agent_tool.py +152 -122
- hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +66 -62
- hanzo_mcp/tools/agent/clarification_protocol.py +55 -50
- hanzo_mcp/tools/agent/clarification_tool.py +11 -10
- hanzo_mcp/tools/agent/claude_cli_tool.py +21 -20
- hanzo_mcp/tools/agent/claude_desktop_auth.py +130 -144
- hanzo_mcp/tools/agent/cli_agent_base.py +59 -53
- hanzo_mcp/tools/agent/code_auth.py +102 -107
- hanzo_mcp/tools/agent/code_auth_tool.py +28 -27
- hanzo_mcp/tools/agent/codex_cli_tool.py +20 -19
- hanzo_mcp/tools/agent/critic_tool.py +86 -73
- hanzo_mcp/tools/agent/gemini_cli_tool.py +21 -20
- hanzo_mcp/tools/agent/grok_cli_tool.py +21 -20
- hanzo_mcp/tools/agent/iching_tool.py +404 -139
- hanzo_mcp/tools/agent/network_tool.py +89 -73
- hanzo_mcp/tools/agent/prompt.py +2 -1
- hanzo_mcp/tools/agent/review_tool.py +101 -98
- hanzo_mcp/tools/agent/swarm_alias.py +87 -0
- hanzo_mcp/tools/agent/swarm_tool.py +246 -161
- hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +134 -92
- hanzo_mcp/tools/agent/tool_adapter.py +21 -11
- hanzo_mcp/tools/common/__init__.py +1 -1
- hanzo_mcp/tools/common/base.py +3 -5
- hanzo_mcp/tools/common/batch_tool.py +46 -39
- hanzo_mcp/tools/common/config_tool.py +120 -84
- hanzo_mcp/tools/common/context.py +1 -5
- hanzo_mcp/tools/common/context_fix.py +5 -3
- hanzo_mcp/tools/common/critic_tool.py +4 -8
- hanzo_mcp/tools/common/decorators.py +58 -56
- hanzo_mcp/tools/common/enhanced_base.py +29 -32
- hanzo_mcp/tools/common/fastmcp_pagination.py +91 -94
- hanzo_mcp/tools/common/forgiving_edit.py +91 -87
- hanzo_mcp/tools/common/mode.py +15 -17
- hanzo_mcp/tools/common/mode_loader.py +27 -24
- hanzo_mcp/tools/common/paginated_base.py +61 -53
- hanzo_mcp/tools/common/paginated_response.py +72 -79
- hanzo_mcp/tools/common/pagination.py +50 -53
- hanzo_mcp/tools/common/permissions.py +4 -4
- hanzo_mcp/tools/common/personality.py +186 -138
- hanzo_mcp/tools/common/plugin_loader.py +54 -54
- hanzo_mcp/tools/common/stats.py +65 -47
- hanzo_mcp/tools/common/test_helpers.py +31 -0
- hanzo_mcp/tools/common/thinking_tool.py +4 -8
- hanzo_mcp/tools/common/tool_disable.py +17 -12
- hanzo_mcp/tools/common/tool_enable.py +13 -14
- hanzo_mcp/tools/common/tool_list.py +36 -28
- hanzo_mcp/tools/common/truncate.py +23 -23
- hanzo_mcp/tools/config/__init__.py +4 -4
- hanzo_mcp/tools/config/config_tool.py +42 -29
- hanzo_mcp/tools/config/index_config.py +37 -34
- hanzo_mcp/tools/config/mode_tool.py +175 -55
- hanzo_mcp/tools/database/__init__.py +15 -12
- hanzo_mcp/tools/database/database_manager.py +77 -75
- hanzo_mcp/tools/database/graph.py +137 -91
- hanzo_mcp/tools/database/graph_add.py +30 -18
- hanzo_mcp/tools/database/graph_query.py +178 -102
- hanzo_mcp/tools/database/graph_remove.py +33 -28
- hanzo_mcp/tools/database/graph_search.py +97 -75
- hanzo_mcp/tools/database/graph_stats.py +91 -59
- hanzo_mcp/tools/database/sql.py +107 -79
- hanzo_mcp/tools/database/sql_query.py +30 -24
- hanzo_mcp/tools/database/sql_search.py +29 -25
- hanzo_mcp/tools/database/sql_stats.py +47 -35
- hanzo_mcp/tools/editor/neovim_command.py +25 -28
- hanzo_mcp/tools/editor/neovim_edit.py +21 -23
- hanzo_mcp/tools/editor/neovim_session.py +60 -54
- hanzo_mcp/tools/filesystem/__init__.py +31 -30
- hanzo_mcp/tools/filesystem/ast_multi_edit.py +329 -249
- hanzo_mcp/tools/filesystem/ast_tool.py +4 -4
- hanzo_mcp/tools/filesystem/base.py +1 -1
- hanzo_mcp/tools/filesystem/batch_search.py +316 -224
- hanzo_mcp/tools/filesystem/content_replace.py +4 -4
- hanzo_mcp/tools/filesystem/diff.py +71 -59
- hanzo_mcp/tools/filesystem/directory_tree.py +7 -7
- hanzo_mcp/tools/filesystem/directory_tree_paginated.py +49 -37
- hanzo_mcp/tools/filesystem/edit.py +4 -4
- hanzo_mcp/tools/filesystem/find.py +173 -80
- hanzo_mcp/tools/filesystem/find_files.py +73 -52
- hanzo_mcp/tools/filesystem/git_search.py +157 -104
- hanzo_mcp/tools/filesystem/grep.py +8 -8
- hanzo_mcp/tools/filesystem/multi_edit.py +4 -8
- hanzo_mcp/tools/filesystem/read.py +12 -10
- hanzo_mcp/tools/filesystem/rules_tool.py +59 -43
- hanzo_mcp/tools/filesystem/search_tool.py +263 -207
- hanzo_mcp/tools/filesystem/symbols_tool.py +94 -54
- hanzo_mcp/tools/filesystem/tree.py +35 -33
- hanzo_mcp/tools/filesystem/unix_aliases.py +13 -18
- hanzo_mcp/tools/filesystem/watch.py +37 -36
- hanzo_mcp/tools/filesystem/write.py +4 -8
- hanzo_mcp/tools/jupyter/__init__.py +4 -4
- hanzo_mcp/tools/jupyter/base.py +4 -5
- hanzo_mcp/tools/jupyter/jupyter.py +67 -47
- hanzo_mcp/tools/jupyter/notebook_edit.py +4 -4
- hanzo_mcp/tools/jupyter/notebook_read.py +4 -7
- hanzo_mcp/tools/llm/__init__.py +5 -7
- hanzo_mcp/tools/llm/consensus_tool.py +72 -52
- hanzo_mcp/tools/llm/llm_manage.py +101 -60
- hanzo_mcp/tools/llm/llm_tool.py +226 -166
- hanzo_mcp/tools/llm/provider_tools.py +25 -26
- hanzo_mcp/tools/lsp/__init__.py +1 -1
- hanzo_mcp/tools/lsp/lsp_tool.py +228 -143
- hanzo_mcp/tools/mcp/__init__.py +2 -3
- hanzo_mcp/tools/mcp/mcp_add.py +27 -25
- hanzo_mcp/tools/mcp/mcp_remove.py +7 -8
- hanzo_mcp/tools/mcp/mcp_stats.py +23 -22
- hanzo_mcp/tools/mcp/mcp_tool.py +129 -98
- hanzo_mcp/tools/memory/__init__.py +39 -21
- hanzo_mcp/tools/memory/knowledge_tools.py +124 -99
- hanzo_mcp/tools/memory/memory_tools.py +90 -108
- hanzo_mcp/tools/search/__init__.py +7 -2
- hanzo_mcp/tools/search/find_tool.py +297 -212
- hanzo_mcp/tools/search/unified_search.py +366 -314
- hanzo_mcp/tools/shell/__init__.py +8 -7
- hanzo_mcp/tools/shell/auto_background.py +56 -49
- hanzo_mcp/tools/shell/base.py +1 -1
- hanzo_mcp/tools/shell/base_process.py +75 -75
- hanzo_mcp/tools/shell/bash_session.py +2 -2
- hanzo_mcp/tools/shell/bash_session_executor.py +4 -4
- hanzo_mcp/tools/shell/bash_tool.py +24 -31
- hanzo_mcp/tools/shell/command_executor.py +12 -12
- hanzo_mcp/tools/shell/logs.py +43 -33
- hanzo_mcp/tools/shell/npx.py +13 -13
- hanzo_mcp/tools/shell/npx_background.py +24 -21
- hanzo_mcp/tools/shell/npx_tool.py +18 -22
- hanzo_mcp/tools/shell/open.py +19 -21
- hanzo_mcp/tools/shell/pkill.py +31 -26
- hanzo_mcp/tools/shell/process_tool.py +32 -32
- hanzo_mcp/tools/shell/processes.py +57 -58
- hanzo_mcp/tools/shell/run_background.py +24 -25
- hanzo_mcp/tools/shell/run_command.py +5 -5
- hanzo_mcp/tools/shell/run_command_windows.py +5 -5
- hanzo_mcp/tools/shell/session_storage.py +3 -3
- hanzo_mcp/tools/shell/streaming_command.py +141 -126
- hanzo_mcp/tools/shell/uvx.py +24 -25
- hanzo_mcp/tools/shell/uvx_background.py +35 -33
- hanzo_mcp/tools/shell/uvx_tool.py +18 -22
- hanzo_mcp/tools/todo/__init__.py +6 -2
- hanzo_mcp/tools/todo/todo.py +50 -37
- hanzo_mcp/tools/todo/todo_read.py +5 -8
- hanzo_mcp/tools/todo/todo_write.py +5 -7
- hanzo_mcp/tools/vector/__init__.py +40 -28
- hanzo_mcp/tools/vector/ast_analyzer.py +176 -143
- hanzo_mcp/tools/vector/git_ingester.py +170 -179
- hanzo_mcp/tools/vector/index_tool.py +96 -44
- hanzo_mcp/tools/vector/infinity_store.py +283 -228
- hanzo_mcp/tools/vector/mock_infinity.py +39 -40
- hanzo_mcp/tools/vector/project_manager.py +88 -78
- hanzo_mcp/tools/vector/vector.py +59 -42
- hanzo_mcp/tools/vector/vector_index.py +30 -27
- hanzo_mcp/tools/vector/vector_search.py +64 -45
- hanzo_mcp/types.py +6 -4
- {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/METADATA +1 -1
- hanzo_mcp-0.8.1.dist-info/RECORD +185 -0
- hanzo_mcp-0.7.7.dist-info/RECORD +0 -182
- {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/top_level.txt +0 -0
hanzo_mcp/tools/database/sql.py
CHANGED
|
@@ -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
|
|
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__(
|
|
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(
|
|
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,
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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__(
|
|
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 = [
|
|
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(
|
|
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(
|
|
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(
|
|
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
|