hanzo-mcp 0.5.2__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.
- hanzo_mcp/__init__.py +1 -1
- hanzo_mcp/cli.py +32 -0
- hanzo_mcp/dev_server.py +246 -0
- hanzo_mcp/prompts/__init__.py +1 -1
- hanzo_mcp/prompts/project_system.py +43 -7
- hanzo_mcp/server.py +5 -1
- hanzo_mcp/tools/__init__.py +66 -35
- hanzo_mcp/tools/agent/__init__.py +1 -1
- hanzo_mcp/tools/agent/agent.py +401 -0
- hanzo_mcp/tools/agent/agent_tool.py +3 -4
- hanzo_mcp/tools/common/__init__.py +1 -1
- hanzo_mcp/tools/common/base.py +2 -2
- hanzo_mcp/tools/common/batch_tool.py +3 -5
- hanzo_mcp/tools/common/config_tool.py +1 -1
- hanzo_mcp/tools/common/context.py +1 -1
- hanzo_mcp/tools/common/palette.py +344 -0
- hanzo_mcp/tools/common/palette_loader.py +108 -0
- hanzo_mcp/tools/common/stats.py +1 -1
- hanzo_mcp/tools/common/thinking_tool.py +3 -5
- hanzo_mcp/tools/common/tool_disable.py +1 -1
- hanzo_mcp/tools/common/tool_enable.py +1 -1
- hanzo_mcp/tools/common/tool_list.py +49 -52
- hanzo_mcp/tools/config/__init__.py +10 -0
- hanzo_mcp/tools/config/config_tool.py +212 -0
- hanzo_mcp/tools/config/index_config.py +176 -0
- hanzo_mcp/tools/config/palette_tool.py +166 -0
- hanzo_mcp/tools/database/__init__.py +1 -1
- hanzo_mcp/tools/database/graph.py +482 -0
- hanzo_mcp/tools/database/graph_add.py +1 -1
- hanzo_mcp/tools/database/graph_query.py +1 -1
- hanzo_mcp/tools/database/graph_remove.py +1 -1
- hanzo_mcp/tools/database/graph_search.py +1 -1
- hanzo_mcp/tools/database/graph_stats.py +1 -1
- hanzo_mcp/tools/database/sql.py +411 -0
- hanzo_mcp/tools/database/sql_query.py +1 -1
- hanzo_mcp/tools/database/sql_search.py +1 -1
- hanzo_mcp/tools/database/sql_stats.py +1 -1
- hanzo_mcp/tools/editor/neovim_command.py +1 -1
- hanzo_mcp/tools/editor/neovim_edit.py +1 -1
- hanzo_mcp/tools/editor/neovim_session.py +1 -1
- hanzo_mcp/tools/filesystem/__init__.py +42 -13
- hanzo_mcp/tools/filesystem/base.py +1 -1
- hanzo_mcp/tools/filesystem/batch_search.py +4 -4
- hanzo_mcp/tools/filesystem/content_replace.py +3 -5
- hanzo_mcp/tools/filesystem/diff.py +193 -0
- hanzo_mcp/tools/filesystem/directory_tree.py +3 -5
- hanzo_mcp/tools/filesystem/edit.py +3 -5
- hanzo_mcp/tools/filesystem/find.py +443 -0
- hanzo_mcp/tools/filesystem/find_files.py +1 -1
- hanzo_mcp/tools/filesystem/git_search.py +1 -1
- hanzo_mcp/tools/filesystem/grep.py +2 -2
- hanzo_mcp/tools/filesystem/multi_edit.py +3 -5
- hanzo_mcp/tools/filesystem/read.py +17 -5
- hanzo_mcp/tools/filesystem/{grep_ast_tool.py → symbols.py} +17 -27
- hanzo_mcp/tools/filesystem/symbols_unified.py +376 -0
- hanzo_mcp/tools/filesystem/tree.py +268 -0
- hanzo_mcp/tools/filesystem/unified_search.py +711 -0
- hanzo_mcp/tools/filesystem/unix_aliases.py +99 -0
- hanzo_mcp/tools/filesystem/watch.py +174 -0
- hanzo_mcp/tools/filesystem/write.py +3 -5
- hanzo_mcp/tools/jupyter/__init__.py +9 -12
- hanzo_mcp/tools/jupyter/base.py +1 -1
- hanzo_mcp/tools/jupyter/jupyter.py +326 -0
- hanzo_mcp/tools/jupyter/notebook_edit.py +3 -4
- hanzo_mcp/tools/jupyter/notebook_read.py +3 -5
- hanzo_mcp/tools/llm/__init__.py +4 -0
- hanzo_mcp/tools/llm/consensus_tool.py +1 -1
- hanzo_mcp/tools/llm/llm_manage.py +1 -1
- hanzo_mcp/tools/llm/llm_tool.py +1 -1
- hanzo_mcp/tools/llm/llm_unified.py +851 -0
- hanzo_mcp/tools/llm/provider_tools.py +1 -1
- hanzo_mcp/tools/mcp/__init__.py +4 -0
- hanzo_mcp/tools/mcp/mcp_add.py +1 -1
- hanzo_mcp/tools/mcp/mcp_remove.py +1 -1
- hanzo_mcp/tools/mcp/mcp_stats.py +1 -1
- hanzo_mcp/tools/mcp/mcp_unified.py +503 -0
- hanzo_mcp/tools/shell/__init__.py +20 -42
- hanzo_mcp/tools/shell/base.py +1 -1
- hanzo_mcp/tools/shell/base_process.py +303 -0
- hanzo_mcp/tools/shell/bash_unified.py +134 -0
- hanzo_mcp/tools/shell/logs.py +1 -1
- hanzo_mcp/tools/shell/npx.py +1 -1
- hanzo_mcp/tools/shell/npx_background.py +1 -1
- hanzo_mcp/tools/shell/npx_unified.py +101 -0
- hanzo_mcp/tools/shell/open.py +107 -0
- hanzo_mcp/tools/shell/pkill.py +1 -1
- hanzo_mcp/tools/shell/process_unified.py +131 -0
- hanzo_mcp/tools/shell/processes.py +1 -1
- hanzo_mcp/tools/shell/run_background.py +1 -1
- hanzo_mcp/tools/shell/run_command.py +3 -4
- hanzo_mcp/tools/shell/run_command_windows.py +3 -4
- hanzo_mcp/tools/shell/uvx.py +1 -1
- hanzo_mcp/tools/shell/uvx_background.py +1 -1
- hanzo_mcp/tools/shell/uvx_unified.py +101 -0
- hanzo_mcp/tools/todo/__init__.py +1 -1
- hanzo_mcp/tools/todo/base.py +1 -1
- hanzo_mcp/tools/todo/todo.py +265 -0
- hanzo_mcp/tools/todo/todo_read.py +3 -5
- hanzo_mcp/tools/todo/todo_write.py +3 -5
- hanzo_mcp/tools/vector/__init__.py +1 -1
- hanzo_mcp/tools/vector/index_tool.py +1 -1
- hanzo_mcp/tools/vector/project_manager.py +27 -5
- hanzo_mcp/tools/vector/vector.py +311 -0
- hanzo_mcp/tools/vector/vector_index.py +1 -1
- hanzo_mcp/tools/vector/vector_search.py +1 -1
- hanzo_mcp-0.6.1.dist-info/METADATA +336 -0
- hanzo_mcp-0.6.1.dist-info/RECORD +134 -0
- hanzo_mcp-0.6.1.dist-info/entry_points.txt +3 -0
- hanzo_mcp-0.5.2.dist-info/METADATA +0 -276
- hanzo_mcp-0.5.2.dist-info/RECORD +0 -106
- hanzo_mcp-0.5.2.dist-info/entry_points.txt +0 -2
- {hanzo_mcp-0.5.2.dist-info → hanzo_mcp-0.6.1.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.5.2.dist-info → hanzo_mcp-0.6.1.dist-info}/licenses/LICENSE +0 -0
- {hanzo_mcp-0.5.2.dist-info → hanzo_mcp-0.6.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
"""Unified SQL database tool."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated, TypedDict, Unpack, final, override, Optional, List, Dict, Any
|
|
4
|
+
import sqlite3
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
8
|
+
from pydantic import Field
|
|
9
|
+
|
|
10
|
+
from hanzo_mcp.tools.common.base import BaseTool
|
|
11
|
+
from hanzo_mcp.tools.common.permissions import PermissionManager
|
|
12
|
+
from hanzo_mcp.tools.database.database_manager import DatabaseManager
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Parameter types
|
|
16
|
+
Query = Annotated[
|
|
17
|
+
Optional[str],
|
|
18
|
+
Field(
|
|
19
|
+
description="SQL query to execute",
|
|
20
|
+
default=None,
|
|
21
|
+
),
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
Pattern = Annotated[
|
|
25
|
+
Optional[str],
|
|
26
|
+
Field(
|
|
27
|
+
description="Search pattern for table/column names or data",
|
|
28
|
+
default=None,
|
|
29
|
+
),
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
Table = Annotated[
|
|
33
|
+
Optional[str],
|
|
34
|
+
Field(
|
|
35
|
+
description="Table name for operations",
|
|
36
|
+
default=None,
|
|
37
|
+
),
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
Action = Annotated[
|
|
41
|
+
str,
|
|
42
|
+
Field(
|
|
43
|
+
description="Action: query (default), search, schema, stats",
|
|
44
|
+
default="query",
|
|
45
|
+
),
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
Limit = Annotated[
|
|
49
|
+
int,
|
|
50
|
+
Field(
|
|
51
|
+
description="Maximum rows to return",
|
|
52
|
+
default=100,
|
|
53
|
+
),
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class SQLParams(TypedDict, total=False):
|
|
58
|
+
"""Parameters for SQL tool."""
|
|
59
|
+
query: Optional[str]
|
|
60
|
+
pattern: Optional[str]
|
|
61
|
+
table: Optional[str]
|
|
62
|
+
action: str
|
|
63
|
+
limit: int
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@final
|
|
67
|
+
class SQLTool(BaseTool):
|
|
68
|
+
"""Unified SQL database tool."""
|
|
69
|
+
|
|
70
|
+
def __init__(self, permission_manager: PermissionManager, db_manager: DatabaseManager):
|
|
71
|
+
"""Initialize the SQL tool."""
|
|
72
|
+
super().__init__(permission_manager)
|
|
73
|
+
self.db_manager = db_manager
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
@override
|
|
77
|
+
def name(self) -> str:
|
|
78
|
+
"""Get the tool name."""
|
|
79
|
+
return "sql"
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
@override
|
|
83
|
+
def description(self) -> str:
|
|
84
|
+
"""Get the tool description."""
|
|
85
|
+
return """SQLite database. Actions: query (default), search, schema, stats.
|
|
86
|
+
|
|
87
|
+
Usage:
|
|
88
|
+
sql "SELECT * FROM users WHERE active = 1"
|
|
89
|
+
sql --action schema
|
|
90
|
+
sql --action search --pattern "john"
|
|
91
|
+
sql --action stats --table users
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
@override
|
|
95
|
+
async def call(
|
|
96
|
+
self,
|
|
97
|
+
ctx: MCPContext,
|
|
98
|
+
**params: Unpack[SQLParams],
|
|
99
|
+
) -> str:
|
|
100
|
+
"""Execute SQL operation."""
|
|
101
|
+
tool_ctx = self.create_tool_context(ctx)
|
|
102
|
+
|
|
103
|
+
# Get current project database
|
|
104
|
+
project_db = self.db_manager.get_current_project_db()
|
|
105
|
+
if not project_db:
|
|
106
|
+
return "Error: No project database found. Are you in a project directory?"
|
|
107
|
+
|
|
108
|
+
# Extract action
|
|
109
|
+
action = params.get("action", "query")
|
|
110
|
+
|
|
111
|
+
# Route to appropriate handler
|
|
112
|
+
if action == "query":
|
|
113
|
+
return await self._handle_query(project_db, params, tool_ctx)
|
|
114
|
+
elif action == "search":
|
|
115
|
+
return await self._handle_search(project_db, params, tool_ctx)
|
|
116
|
+
elif action == "schema":
|
|
117
|
+
return await self._handle_schema(project_db, params, tool_ctx)
|
|
118
|
+
elif action == "stats":
|
|
119
|
+
return await self._handle_stats(project_db, params, tool_ctx)
|
|
120
|
+
else:
|
|
121
|
+
return f"Error: Unknown action '{action}'. Valid actions: query, search, schema, stats"
|
|
122
|
+
|
|
123
|
+
async def _handle_query(self, project_db, params: Dict[str, Any], tool_ctx) -> str:
|
|
124
|
+
"""Execute SQL query."""
|
|
125
|
+
query = params.get("query")
|
|
126
|
+
if not query:
|
|
127
|
+
return "Error: query required for query action"
|
|
128
|
+
|
|
129
|
+
limit = params.get("limit", 100)
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
with project_db.get_sqlite_connection() as conn:
|
|
133
|
+
# Enable row factory for dict-like access
|
|
134
|
+
conn.row_factory = sqlite3.Row
|
|
135
|
+
|
|
136
|
+
# Add LIMIT if not present in SELECT queries
|
|
137
|
+
query_upper = query.upper().strip()
|
|
138
|
+
if query_upper.startswith("SELECT") and "LIMIT" not in query_upper:
|
|
139
|
+
query = f"{query} LIMIT {limit}"
|
|
140
|
+
|
|
141
|
+
cursor = conn.execute(query)
|
|
142
|
+
|
|
143
|
+
# Handle different query types
|
|
144
|
+
if query_upper.startswith("SELECT"):
|
|
145
|
+
rows = cursor.fetchall()
|
|
146
|
+
|
|
147
|
+
if not rows:
|
|
148
|
+
return "No results found"
|
|
149
|
+
|
|
150
|
+
# Get column names
|
|
151
|
+
columns = [description[0] for description in cursor.description]
|
|
152
|
+
|
|
153
|
+
# Format as table
|
|
154
|
+
output = ["=== Query Results ==="]
|
|
155
|
+
output.append(f"Columns: {', '.join(columns)}")
|
|
156
|
+
output.append("-" * 60)
|
|
157
|
+
|
|
158
|
+
for row in rows:
|
|
159
|
+
row_data = []
|
|
160
|
+
for col in columns:
|
|
161
|
+
value = row[col]
|
|
162
|
+
if value is None:
|
|
163
|
+
value = "NULL"
|
|
164
|
+
elif isinstance(value, str) and len(value) > 50:
|
|
165
|
+
value = value[:50] + "..."
|
|
166
|
+
row_data.append(str(value))
|
|
167
|
+
output.append(" | ".join(row_data))
|
|
168
|
+
|
|
169
|
+
output.append(f"\nRows returned: {len(rows)}")
|
|
170
|
+
if len(rows) == limit:
|
|
171
|
+
output.append(f"(Limited to {limit} rows)")
|
|
172
|
+
|
|
173
|
+
return "\n".join(output)
|
|
174
|
+
|
|
175
|
+
else:
|
|
176
|
+
# For INSERT, UPDATE, DELETE
|
|
177
|
+
conn.commit()
|
|
178
|
+
rows_affected = cursor.rowcount
|
|
179
|
+
|
|
180
|
+
if query_upper.startswith("INSERT"):
|
|
181
|
+
return f"Inserted {rows_affected} row(s)"
|
|
182
|
+
elif query_upper.startswith("UPDATE"):
|
|
183
|
+
return f"Updated {rows_affected} row(s)"
|
|
184
|
+
elif query_upper.startswith("DELETE"):
|
|
185
|
+
return f"Deleted {rows_affected} row(s)"
|
|
186
|
+
else:
|
|
187
|
+
return f"Query executed successfully. Rows affected: {rows_affected}"
|
|
188
|
+
|
|
189
|
+
except Exception as e:
|
|
190
|
+
await tool_ctx.error(f"Query failed: {str(e)}")
|
|
191
|
+
return f"Error executing query: {str(e)}"
|
|
192
|
+
|
|
193
|
+
async def _handle_search(self, project_db, params: Dict[str, Any], tool_ctx) -> str:
|
|
194
|
+
"""Search for data in tables."""
|
|
195
|
+
pattern = params.get("pattern")
|
|
196
|
+
if not pattern:
|
|
197
|
+
return "Error: pattern required for search action"
|
|
198
|
+
|
|
199
|
+
table = params.get("table")
|
|
200
|
+
limit = params.get("limit", 100)
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
with project_db.get_sqlite_connection() as conn:
|
|
204
|
+
conn.row_factory = sqlite3.Row
|
|
205
|
+
|
|
206
|
+
# Get all tables if not specified
|
|
207
|
+
if not table:
|
|
208
|
+
cursor = conn.execute("""
|
|
209
|
+
SELECT name FROM sqlite_master
|
|
210
|
+
WHERE type='table' AND name NOT LIKE 'sqlite_%'
|
|
211
|
+
""")
|
|
212
|
+
tables = [row[0] for row in cursor.fetchall()]
|
|
213
|
+
else:
|
|
214
|
+
tables = [table]
|
|
215
|
+
|
|
216
|
+
all_results = []
|
|
217
|
+
|
|
218
|
+
for tbl in tables:
|
|
219
|
+
# Get columns
|
|
220
|
+
cursor = conn.execute(f"PRAGMA table_info({tbl})")
|
|
221
|
+
columns = [row[1] for row in cursor.fetchall()]
|
|
222
|
+
|
|
223
|
+
# Build search query
|
|
224
|
+
where_clauses = [f"{col} LIKE ?" for col in columns]
|
|
225
|
+
query = f"SELECT * FROM {tbl} WHERE {' OR '.join(where_clauses)} LIMIT {limit}"
|
|
226
|
+
|
|
227
|
+
# Search
|
|
228
|
+
cursor = conn.execute(query, [f"%{pattern}%"] * len(columns))
|
|
229
|
+
rows = cursor.fetchall()
|
|
230
|
+
|
|
231
|
+
if rows:
|
|
232
|
+
all_results.append((tbl, columns, rows))
|
|
233
|
+
|
|
234
|
+
if not all_results:
|
|
235
|
+
return f"No results found for pattern '{pattern}'"
|
|
236
|
+
|
|
237
|
+
# Format results
|
|
238
|
+
output = [f"=== Search Results for '{pattern}' ==="]
|
|
239
|
+
|
|
240
|
+
for tbl, columns, rows in all_results:
|
|
241
|
+
output.append(f"\nTable: {tbl}")
|
|
242
|
+
output.append(f"Columns: {', '.join(columns)}")
|
|
243
|
+
output.append("-" * 60)
|
|
244
|
+
|
|
245
|
+
for row in rows:
|
|
246
|
+
row_data = []
|
|
247
|
+
for col in columns:
|
|
248
|
+
value = row[col]
|
|
249
|
+
if value is None:
|
|
250
|
+
value = "NULL"
|
|
251
|
+
elif isinstance(value, str):
|
|
252
|
+
# Highlight matches
|
|
253
|
+
if pattern.lower() in str(value).lower():
|
|
254
|
+
value = f"**{value}**"
|
|
255
|
+
if len(value) > 50:
|
|
256
|
+
value = value[:50] + "..."
|
|
257
|
+
row_data.append(str(value))
|
|
258
|
+
output.append(" | ".join(row_data))
|
|
259
|
+
|
|
260
|
+
output.append(f"Found {len(rows)} row(s) in {tbl}")
|
|
261
|
+
|
|
262
|
+
return "\n".join(output)
|
|
263
|
+
|
|
264
|
+
except Exception as e:
|
|
265
|
+
await tool_ctx.error(f"Search failed: {str(e)}")
|
|
266
|
+
return f"Error during search: {str(e)}"
|
|
267
|
+
|
|
268
|
+
async def _handle_schema(self, project_db, params: Dict[str, Any], tool_ctx) -> str:
|
|
269
|
+
"""Show database schema."""
|
|
270
|
+
table = params.get("table")
|
|
271
|
+
|
|
272
|
+
try:
|
|
273
|
+
with project_db.get_sqlite_connection() as conn:
|
|
274
|
+
if table:
|
|
275
|
+
# Show specific table schema
|
|
276
|
+
cursor = conn.execute(f"PRAGMA table_info({table})")
|
|
277
|
+
columns = cursor.fetchall()
|
|
278
|
+
|
|
279
|
+
if not columns:
|
|
280
|
+
return f"Table '{table}' not found"
|
|
281
|
+
|
|
282
|
+
output = [f"=== Schema for table '{table}' ==="]
|
|
283
|
+
output.append("Column | Type | Not Null | Default | Primary Key")
|
|
284
|
+
output.append("-" * 60)
|
|
285
|
+
|
|
286
|
+
for col in columns:
|
|
287
|
+
output.append(f"{col[1]} | {col[2]} | {col[3]} | {col[4]} | {col[5]}")
|
|
288
|
+
|
|
289
|
+
# Get indexes
|
|
290
|
+
cursor = conn.execute(f"PRAGMA index_list({table})")
|
|
291
|
+
indexes = cursor.fetchall()
|
|
292
|
+
|
|
293
|
+
if indexes:
|
|
294
|
+
output.append("\nIndexes:")
|
|
295
|
+
for idx in indexes:
|
|
296
|
+
output.append(f" {idx[1]} (unique: {idx[2]})")
|
|
297
|
+
|
|
298
|
+
else:
|
|
299
|
+
# Show all tables
|
|
300
|
+
cursor = conn.execute("""
|
|
301
|
+
SELECT name, sql FROM sqlite_master
|
|
302
|
+
WHERE type='table' AND name NOT LIKE 'sqlite_%'
|
|
303
|
+
ORDER BY name
|
|
304
|
+
""")
|
|
305
|
+
tables = cursor.fetchall()
|
|
306
|
+
|
|
307
|
+
if not tables:
|
|
308
|
+
return "No tables found in database"
|
|
309
|
+
|
|
310
|
+
output = ["=== Database Schema ==="]
|
|
311
|
+
|
|
312
|
+
for table_name, create_sql in tables:
|
|
313
|
+
output.append(f"\nTable: {table_name}")
|
|
314
|
+
|
|
315
|
+
# Get row count
|
|
316
|
+
cursor = conn.execute(f"SELECT COUNT(*) FROM {table_name}")
|
|
317
|
+
count = cursor.fetchone()[0]
|
|
318
|
+
output.append(f"Rows: {count}")
|
|
319
|
+
|
|
320
|
+
# Get columns
|
|
321
|
+
cursor = conn.execute(f"PRAGMA table_info({table_name})")
|
|
322
|
+
columns = cursor.fetchall()
|
|
323
|
+
output.append(f"Columns: {', '.join([col[1] for col in columns])}")
|
|
324
|
+
|
|
325
|
+
return "\n".join(output)
|
|
326
|
+
|
|
327
|
+
except Exception as e:
|
|
328
|
+
await tool_ctx.error(f"Failed to get schema: {str(e)}")
|
|
329
|
+
return f"Error getting schema: {str(e)}"
|
|
330
|
+
|
|
331
|
+
async def _handle_stats(self, project_db, params: Dict[str, Any], tool_ctx) -> str:
|
|
332
|
+
"""Get database statistics."""
|
|
333
|
+
table = params.get("table")
|
|
334
|
+
|
|
335
|
+
try:
|
|
336
|
+
with project_db.get_sqlite_connection() as conn:
|
|
337
|
+
output = ["=== Database Statistics ==="]
|
|
338
|
+
output.append(f"Database: {project_db.sqlite_path}")
|
|
339
|
+
|
|
340
|
+
# Get file size
|
|
341
|
+
db_size = project_db.sqlite_path.stat().st_size
|
|
342
|
+
output.append(f"Size: {db_size / 1024 / 1024:.2f} MB")
|
|
343
|
+
|
|
344
|
+
if table:
|
|
345
|
+
# Stats for specific table
|
|
346
|
+
cursor = conn.execute(f"SELECT COUNT(*) FROM {table}")
|
|
347
|
+
count = cursor.fetchone()[0]
|
|
348
|
+
|
|
349
|
+
output.append(f"\nTable: {table}")
|
|
350
|
+
output.append(f"Total rows: {count}")
|
|
351
|
+
|
|
352
|
+
# Get column stats
|
|
353
|
+
cursor = conn.execute(f"PRAGMA table_info({table})")
|
|
354
|
+
columns = cursor.fetchall()
|
|
355
|
+
|
|
356
|
+
output.append("\nColumn statistics:")
|
|
357
|
+
for col in columns:
|
|
358
|
+
col_name = col[1]
|
|
359
|
+
col_type = col[2]
|
|
360
|
+
|
|
361
|
+
# Get basic stats based on type
|
|
362
|
+
if "INT" in col_type.upper() or "REAL" in col_type.upper():
|
|
363
|
+
cursor = conn.execute(f"""
|
|
364
|
+
SELECT
|
|
365
|
+
MIN({col_name}) as min_val,
|
|
366
|
+
MAX({col_name}) as max_val,
|
|
367
|
+
AVG({col_name}) as avg_val,
|
|
368
|
+
COUNT(DISTINCT {col_name}) as distinct_count
|
|
369
|
+
FROM {table}
|
|
370
|
+
""")
|
|
371
|
+
stats = cursor.fetchone()
|
|
372
|
+
output.append(f" {col_name}: min={stats[0]}, max={stats[1]}, avg={stats[2]:.2f}, distinct={stats[3]}")
|
|
373
|
+
else:
|
|
374
|
+
cursor = conn.execute(f"""
|
|
375
|
+
SELECT
|
|
376
|
+
COUNT(DISTINCT {col_name}) as distinct_count,
|
|
377
|
+
COUNT(*) - COUNT({col_name}) as null_count
|
|
378
|
+
FROM {table}
|
|
379
|
+
""")
|
|
380
|
+
stats = cursor.fetchone()
|
|
381
|
+
output.append(f" {col_name}: distinct={stats[0]}, nulls={stats[1]}")
|
|
382
|
+
|
|
383
|
+
else:
|
|
384
|
+
# Overall database stats
|
|
385
|
+
cursor = conn.execute("""
|
|
386
|
+
SELECT name FROM sqlite_master
|
|
387
|
+
WHERE type='table' AND name NOT LIKE 'sqlite_%'
|
|
388
|
+
""")
|
|
389
|
+
tables = cursor.fetchall()
|
|
390
|
+
|
|
391
|
+
output.append(f"\nTotal tables: {len(tables)}")
|
|
392
|
+
output.append("\nTable row counts:")
|
|
393
|
+
|
|
394
|
+
total_rows = 0
|
|
395
|
+
for (table_name,) in tables:
|
|
396
|
+
cursor = conn.execute(f"SELECT COUNT(*) FROM {table_name}")
|
|
397
|
+
count = cursor.fetchone()[0]
|
|
398
|
+
total_rows += count
|
|
399
|
+
output.append(f" {table_name}: {count} rows")
|
|
400
|
+
|
|
401
|
+
output.append(f"\nTotal rows across all tables: {total_rows}")
|
|
402
|
+
|
|
403
|
+
return "\n".join(output)
|
|
404
|
+
|
|
405
|
+
except Exception as e:
|
|
406
|
+
await tool_ctx.error(f"Failed to get stats: {str(e)}")
|
|
407
|
+
return f"Error getting stats: {str(e)}"
|
|
408
|
+
|
|
409
|
+
def register(self, mcp_server) -> None:
|
|
410
|
+
"""Register this tool with the MCP server."""
|
|
411
|
+
pass
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import sqlite3
|
|
4
4
|
from typing import Annotated, Optional, TypedDict, Unpack, final, override
|
|
5
5
|
|
|
6
|
-
from fastmcp import Context as MCPContext
|
|
6
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
7
7
|
from pydantic import Field
|
|
8
8
|
|
|
9
9
|
from hanzo_mcp.tools.common.base import BaseTool
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import sqlite3
|
|
4
4
|
from typing import Annotated, Optional, TypedDict, Unpack, final, override
|
|
5
5
|
|
|
6
|
-
from fastmcp import Context as MCPContext
|
|
6
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
7
7
|
from pydantic import Field
|
|
8
8
|
|
|
9
9
|
from hanzo_mcp.tools.common.base import BaseTool
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import sqlite3
|
|
4
4
|
from typing import Annotated, Optional, TypedDict, Unpack, final, override
|
|
5
5
|
|
|
6
|
-
from fastmcp import Context as MCPContext
|
|
6
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
7
7
|
from pydantic import Field
|
|
8
8
|
|
|
9
9
|
from hanzo_mcp.tools.common.base import BaseTool
|
|
@@ -6,7 +6,7 @@ import shutil
|
|
|
6
6
|
import tempfile
|
|
7
7
|
from typing import Annotated, Optional, TypedDict, Unpack, final, override, List
|
|
8
8
|
|
|
9
|
-
from fastmcp import Context as MCPContext
|
|
9
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
10
10
|
from pydantic import Field
|
|
11
11
|
|
|
12
12
|
from hanzo_mcp.tools.common.base import BaseTool
|
|
@@ -6,7 +6,7 @@ import shutil
|
|
|
6
6
|
from typing import Annotated, Optional, TypedDict, Unpack, final, override
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
|
|
9
|
-
from fastmcp import Context as MCPContext
|
|
9
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
10
10
|
from pydantic import Field
|
|
11
11
|
|
|
12
12
|
from hanzo_mcp.tools.common.base import BaseTool
|
|
@@ -8,7 +8,7 @@ from typing import Annotated, Optional, TypedDict, Unpack, final, override, List
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from datetime import datetime
|
|
10
10
|
|
|
11
|
-
from fastmcp import Context as MCPContext
|
|
11
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
12
12
|
from pydantic import Field
|
|
13
13
|
|
|
14
14
|
from hanzo_mcp.tools.common.base import BaseTool
|
|
@@ -4,7 +4,7 @@ This package provides tools for interacting with the filesystem, including readi
|
|
|
4
4
|
and editing files, directory navigation, and content searching.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from
|
|
7
|
+
from mcp.server import FastMCP
|
|
8
8
|
|
|
9
9
|
from hanzo_mcp.tools.common.base import BaseTool, ToolRegistry
|
|
10
10
|
|
|
@@ -13,13 +13,16 @@ from hanzo_mcp.tools.filesystem.content_replace import ContentReplaceTool
|
|
|
13
13
|
from hanzo_mcp.tools.filesystem.directory_tree import DirectoryTreeTool
|
|
14
14
|
from hanzo_mcp.tools.filesystem.edit import Edit
|
|
15
15
|
from hanzo_mcp.tools.filesystem.grep import Grep
|
|
16
|
-
from hanzo_mcp.tools.filesystem.
|
|
16
|
+
from hanzo_mcp.tools.filesystem.symbols import SymbolsTool
|
|
17
17
|
from hanzo_mcp.tools.filesystem.git_search import GitSearchTool
|
|
18
18
|
from hanzo_mcp.tools.filesystem.multi_edit import MultiEdit
|
|
19
19
|
from hanzo_mcp.tools.filesystem.read import ReadTool
|
|
20
20
|
from hanzo_mcp.tools.filesystem.write import Write
|
|
21
21
|
from hanzo_mcp.tools.filesystem.batch_search import BatchSearchTool
|
|
22
22
|
from hanzo_mcp.tools.filesystem.find_files import FindFilesTool
|
|
23
|
+
from hanzo_mcp.tools.filesystem.unified_search import UnifiedSearchTool
|
|
24
|
+
from hanzo_mcp.tools.filesystem.watch import watch_tool
|
|
25
|
+
from hanzo_mcp.tools.filesystem.diff import create_diff_tool
|
|
23
26
|
|
|
24
27
|
# Export all tool classes
|
|
25
28
|
__all__ = [
|
|
@@ -30,10 +33,11 @@ __all__ = [
|
|
|
30
33
|
"DirectoryTreeTool",
|
|
31
34
|
"Grep",
|
|
32
35
|
"ContentReplaceTool",
|
|
33
|
-
"
|
|
36
|
+
"SymbolsTool",
|
|
34
37
|
"GitSearchTool",
|
|
35
38
|
"BatchSearchTool",
|
|
36
39
|
"FindFilesTool",
|
|
40
|
+
"UnifiedSearchTool",
|
|
37
41
|
"get_filesystem_tools",
|
|
38
42
|
"register_filesystem_tools",
|
|
39
43
|
]
|
|
@@ -41,35 +45,46 @@ __all__ = [
|
|
|
41
45
|
|
|
42
46
|
def get_read_only_filesystem_tools(
|
|
43
47
|
permission_manager: PermissionManager,
|
|
48
|
+
project_manager=None,
|
|
44
49
|
) -> list[BaseTool]:
|
|
45
50
|
"""Create instances of read-only filesystem tools.
|
|
46
51
|
|
|
47
52
|
Args:
|
|
48
53
|
permission_manager: Permission manager for access control
|
|
54
|
+
project_manager: Optional project manager for unified search
|
|
49
55
|
|
|
50
56
|
Returns:
|
|
51
57
|
List of read-only filesystem tool instances
|
|
52
58
|
"""
|
|
53
|
-
|
|
59
|
+
tools = [
|
|
54
60
|
ReadTool(permission_manager),
|
|
55
61
|
DirectoryTreeTool(permission_manager),
|
|
56
62
|
Grep(permission_manager),
|
|
57
|
-
|
|
63
|
+
SymbolsTool(permission_manager),
|
|
58
64
|
GitSearchTool(permission_manager),
|
|
59
65
|
FindFilesTool(permission_manager),
|
|
66
|
+
watch_tool,
|
|
67
|
+
create_diff_tool(permission_manager),
|
|
60
68
|
]
|
|
69
|
+
|
|
70
|
+
# Add unified search if project manager is available
|
|
71
|
+
if project_manager:
|
|
72
|
+
tools.append(UnifiedSearchTool(permission_manager, project_manager))
|
|
73
|
+
|
|
74
|
+
return tools
|
|
61
75
|
|
|
62
76
|
|
|
63
|
-
def get_filesystem_tools(permission_manager: PermissionManager) -> list[BaseTool]:
|
|
77
|
+
def get_filesystem_tools(permission_manager: PermissionManager, project_manager=None) -> list[BaseTool]:
|
|
64
78
|
"""Create instances of all filesystem tools.
|
|
65
79
|
|
|
66
80
|
Args:
|
|
67
81
|
permission_manager: Permission manager for access control
|
|
82
|
+
project_manager: Optional project manager for unified search
|
|
68
83
|
|
|
69
84
|
Returns:
|
|
70
85
|
List of filesystem tool instances
|
|
71
86
|
"""
|
|
72
|
-
|
|
87
|
+
tools = [
|
|
73
88
|
ReadTool(permission_manager),
|
|
74
89
|
Write(permission_manager),
|
|
75
90
|
Edit(permission_manager),
|
|
@@ -77,10 +92,18 @@ def get_filesystem_tools(permission_manager: PermissionManager) -> list[BaseTool
|
|
|
77
92
|
DirectoryTreeTool(permission_manager),
|
|
78
93
|
Grep(permission_manager),
|
|
79
94
|
ContentReplaceTool(permission_manager),
|
|
80
|
-
|
|
95
|
+
SymbolsTool(permission_manager),
|
|
81
96
|
GitSearchTool(permission_manager),
|
|
82
97
|
FindFilesTool(permission_manager),
|
|
98
|
+
watch_tool,
|
|
99
|
+
create_diff_tool(permission_manager),
|
|
83
100
|
]
|
|
101
|
+
|
|
102
|
+
# Add unified search if project manager is available
|
|
103
|
+
if project_manager:
|
|
104
|
+
tools.append(UnifiedSearchTool(permission_manager, project_manager))
|
|
105
|
+
|
|
106
|
+
return tools
|
|
84
107
|
|
|
85
108
|
|
|
86
109
|
def register_filesystem_tools(
|
|
@@ -112,11 +135,14 @@ def register_filesystem_tools(
|
|
|
112
135
|
"multi_edit": MultiEdit,
|
|
113
136
|
"directory_tree": DirectoryTreeTool,
|
|
114
137
|
"grep": Grep,
|
|
115
|
-
"grep_ast":
|
|
138
|
+
"grep_ast": SymbolsTool, # Using correct import name
|
|
116
139
|
"git_search": GitSearchTool,
|
|
117
140
|
"content_replace": ContentReplaceTool,
|
|
118
141
|
"batch_search": BatchSearchTool,
|
|
119
142
|
"find_files": FindFilesTool,
|
|
143
|
+
"unified_search": UnifiedSearchTool,
|
|
144
|
+
"watch": lambda pm: watch_tool, # Singleton instance
|
|
145
|
+
"diff": create_diff_tool,
|
|
120
146
|
}
|
|
121
147
|
|
|
122
148
|
tools = []
|
|
@@ -126,9 +152,12 @@ def register_filesystem_tools(
|
|
|
126
152
|
for tool_name, enabled in enabled_tools.items():
|
|
127
153
|
if enabled and tool_name in tool_classes:
|
|
128
154
|
tool_class = tool_classes[tool_name]
|
|
129
|
-
if tool_name
|
|
130
|
-
# Batch search
|
|
155
|
+
if tool_name in ["batch_search", "unified_search"]:
|
|
156
|
+
# Batch search and unified search require project_manager
|
|
131
157
|
tools.append(tool_class(permission_manager, project_manager))
|
|
158
|
+
elif tool_name == "watch":
|
|
159
|
+
# Watch tool is a singleton
|
|
160
|
+
tools.append(tool_class(permission_manager))
|
|
132
161
|
else:
|
|
133
162
|
tools.append(tool_class(permission_manager))
|
|
134
163
|
else:
|
|
@@ -141,7 +170,7 @@ def register_filesystem_tools(
|
|
|
141
170
|
]
|
|
142
171
|
elif disable_write_tools:
|
|
143
172
|
# Read-only tools including search
|
|
144
|
-
tools = get_read_only_filesystem_tools(permission_manager)
|
|
173
|
+
tools = get_read_only_filesystem_tools(permission_manager, project_manager)
|
|
145
174
|
elif disable_search_tools:
|
|
146
175
|
# Write tools but no search
|
|
147
176
|
tools = [
|
|
@@ -154,7 +183,7 @@ def register_filesystem_tools(
|
|
|
154
183
|
]
|
|
155
184
|
else:
|
|
156
185
|
# All tools
|
|
157
|
-
tools = get_filesystem_tools(permission_manager)
|
|
186
|
+
tools = get_filesystem_tools(permission_manager, project_manager)
|
|
158
187
|
|
|
159
188
|
ToolRegistry.register_tools(mcp_server, tools)
|
|
160
189
|
return tools
|
|
@@ -8,7 +8,7 @@ from abc import ABC
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from typing import Any
|
|
10
10
|
|
|
11
|
-
from fastmcp import Context as MCPContext
|
|
11
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
12
12
|
|
|
13
13
|
from hanzo_mcp.tools.common.base import FileSystemTool
|
|
14
14
|
from hanzo_mcp.tools.common.context import ToolContext, create_tool_context
|
|
@@ -19,14 +19,14 @@ from pathlib import Path
|
|
|
19
19
|
from typing import Dict, List, Optional, Set, Tuple, Any, Union
|
|
20
20
|
from enum import Enum
|
|
21
21
|
|
|
22
|
-
from fastmcp import Context as MCPContext
|
|
23
|
-
from
|
|
22
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
23
|
+
from mcp.server import FastMCP
|
|
24
24
|
from pydantic import Field
|
|
25
25
|
from typing_extensions import Annotated, TypedDict, Unpack, final, override
|
|
26
26
|
|
|
27
27
|
from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
|
|
28
28
|
from hanzo_mcp.tools.filesystem.grep import Grep
|
|
29
|
-
from hanzo_mcp.tools.filesystem.
|
|
29
|
+
from hanzo_mcp.tools.filesystem.symbols import SymbolsTool
|
|
30
30
|
from hanzo_mcp.tools.filesystem.git_search import GitSearchTool
|
|
31
31
|
from hanzo_mcp.tools.vector.vector_search import VectorSearchTool
|
|
32
32
|
from hanzo_mcp.tools.vector.ast_analyzer import ASTAnalyzer, Symbol
|
|
@@ -122,7 +122,7 @@ class BatchSearchTool(FilesystemBaseTool):
|
|
|
122
122
|
|
|
123
123
|
# Initialize component search tools
|
|
124
124
|
self.grep_tool = Grep(permission_manager)
|
|
125
|
-
self.grep_ast_tool =
|
|
125
|
+
self.grep_ast_tool = SymbolsTool(permission_manager)
|
|
126
126
|
self.git_search_tool = GitSearchTool(permission_manager)
|
|
127
127
|
self.ast_analyzer = ASTAnalyzer()
|
|
128
128
|
|