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.
- 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 +168 -6
- 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 +9 -4
- 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 +261 -0
- hanzo_mcp/tools/common/thinking_tool.py +3 -5
- hanzo_mcp/tools/common/tool_disable.py +144 -0
- hanzo_mcp/tools/common/tool_enable.py +182 -0
- hanzo_mcp/tools/common/tool_list.py +260 -0
- 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 +71 -0
- hanzo_mcp/tools/database/database_manager.py +246 -0
- hanzo_mcp/tools/database/graph.py +482 -0
- hanzo_mcp/tools/database/graph_add.py +257 -0
- hanzo_mcp/tools/database/graph_query.py +536 -0
- hanzo_mcp/tools/database/graph_remove.py +267 -0
- hanzo_mcp/tools/database/graph_search.py +348 -0
- hanzo_mcp/tools/database/graph_stats.py +345 -0
- hanzo_mcp/tools/database/sql.py +411 -0
- hanzo_mcp/tools/database/sql_query.py +229 -0
- hanzo_mcp/tools/database/sql_search.py +296 -0
- hanzo_mcp/tools/database/sql_stats.py +254 -0
- hanzo_mcp/tools/editor/__init__.py +11 -0
- hanzo_mcp/tools/editor/neovim_command.py +272 -0
- hanzo_mcp/tools/editor/neovim_edit.py +290 -0
- hanzo_mcp/tools/editor/neovim_session.py +356 -0
- hanzo_mcp/tools/filesystem/__init__.py +52 -13
- hanzo_mcp/tools/filesystem/base.py +1 -1
- hanzo_mcp/tools/filesystem/batch_search.py +812 -0
- 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 +348 -0
- hanzo_mcp/tools/filesystem/git_search.py +505 -0
- 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 +465 -443
- 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 +31 -0
- hanzo_mcp/tools/llm/consensus_tool.py +351 -0
- hanzo_mcp/tools/llm/llm_manage.py +413 -0
- hanzo_mcp/tools/llm/llm_tool.py +346 -0
- hanzo_mcp/tools/llm/llm_unified.py +851 -0
- hanzo_mcp/tools/llm/provider_tools.py +412 -0
- hanzo_mcp/tools/mcp/__init__.py +15 -0
- hanzo_mcp/tools/mcp/mcp_add.py +263 -0
- hanzo_mcp/tools/mcp/mcp_remove.py +127 -0
- hanzo_mcp/tools/mcp/mcp_stats.py +165 -0
- hanzo_mcp/tools/mcp/mcp_unified.py +503 -0
- hanzo_mcp/tools/shell/__init__.py +21 -23
- 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 +265 -0
- hanzo_mcp/tools/shell/npx.py +194 -0
- hanzo_mcp/tools/shell/npx_background.py +254 -0
- hanzo_mcp/tools/shell/npx_unified.py +101 -0
- hanzo_mcp/tools/shell/open.py +107 -0
- hanzo_mcp/tools/shell/pkill.py +262 -0
- hanzo_mcp/tools/shell/process_unified.py +131 -0
- hanzo_mcp/tools/shell/processes.py +279 -0
- hanzo_mcp/tools/shell/run_background.py +326 -0
- 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 +187 -0
- hanzo_mcp/tools/shell/uvx_background.py +249 -0
- 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 +6 -1
- hanzo_mcp/tools/vector/git_ingester.py +3 -0
- hanzo_mcp/tools/vector/index_tool.py +358 -0
- hanzo_mcp/tools/vector/infinity_store.py +98 -0
- 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 +12 -7
- 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.1.dist-info/METADATA +0 -276
- hanzo_mcp-0.5.1.dist-info/RECORD +0 -68
- hanzo_mcp-0.5.1.dist-info/entry_points.txt +0 -2
- {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.6.1.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.6.1.dist-info}/licenses/LICENSE +0 -0
- {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.6.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""SQL search tool for text search in database."""
|
|
2
|
+
|
|
3
|
+
import sqlite3
|
|
4
|
+
from typing import Annotated, Optional, TypedDict, Unpack, final, override
|
|
5
|
+
|
|
6
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
7
|
+
from pydantic import Field
|
|
8
|
+
|
|
9
|
+
from hanzo_mcp.tools.common.base import BaseTool
|
|
10
|
+
from hanzo_mcp.tools.common.context import create_tool_context
|
|
11
|
+
from hanzo_mcp.tools.common.permissions import PermissionManager
|
|
12
|
+
from hanzo_mcp.tools.database.database_manager import DatabaseManager
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
SearchPattern = Annotated[
|
|
16
|
+
str,
|
|
17
|
+
Field(
|
|
18
|
+
description="Search pattern (SQL LIKE syntax, % for wildcard)",
|
|
19
|
+
min_length=1,
|
|
20
|
+
),
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
Table = Annotated[
|
|
24
|
+
str,
|
|
25
|
+
Field(
|
|
26
|
+
description="Table to search in (files, symbols, metadata)",
|
|
27
|
+
default="files",
|
|
28
|
+
),
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
Column = Annotated[
|
|
32
|
+
Optional[str],
|
|
33
|
+
Field(
|
|
34
|
+
description="Specific column to search (searches all text columns if not specified)",
|
|
35
|
+
default=None,
|
|
36
|
+
),
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
ProjectPath = Annotated[
|
|
40
|
+
Optional[str],
|
|
41
|
+
Field(
|
|
42
|
+
description="Project path (defaults to current directory)",
|
|
43
|
+
default=None,
|
|
44
|
+
),
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
MaxResults = Annotated[
|
|
48
|
+
int,
|
|
49
|
+
Field(
|
|
50
|
+
description="Maximum number of results",
|
|
51
|
+
default=50,
|
|
52
|
+
),
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class SqlSearchParams(TypedDict, total=False):
|
|
57
|
+
"""Parameters for SQL search tool."""
|
|
58
|
+
|
|
59
|
+
pattern: str
|
|
60
|
+
table: str
|
|
61
|
+
column: Optional[str]
|
|
62
|
+
project_path: Optional[str]
|
|
63
|
+
max_results: int
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@final
|
|
67
|
+
class SqlSearchTool(BaseTool):
|
|
68
|
+
"""Tool for searching text in SQLite database."""
|
|
69
|
+
|
|
70
|
+
def __init__(self, permission_manager: PermissionManager, db_manager: DatabaseManager):
|
|
71
|
+
"""Initialize the SQL search tool.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
permission_manager: Permission manager for access control
|
|
75
|
+
db_manager: Database manager instance
|
|
76
|
+
"""
|
|
77
|
+
self.permission_manager = permission_manager
|
|
78
|
+
self.db_manager = db_manager
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
@override
|
|
82
|
+
def name(self) -> str:
|
|
83
|
+
"""Get the tool name."""
|
|
84
|
+
return "sql_search"
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
@override
|
|
88
|
+
def description(self) -> str:
|
|
89
|
+
"""Get the tool description."""
|
|
90
|
+
return """Search for text patterns in the project's SQLite database.
|
|
91
|
+
|
|
92
|
+
Supports SQL LIKE pattern matching:
|
|
93
|
+
- % matches any sequence of characters
|
|
94
|
+
- _ matches any single character
|
|
95
|
+
- Use %pattern% to search for pattern anywhere
|
|
96
|
+
|
|
97
|
+
Tables available:
|
|
98
|
+
- files: Search in file paths and content
|
|
99
|
+
- symbols: Search in symbol names, types, and signatures
|
|
100
|
+
- metadata: Search in key-value metadata
|
|
101
|
+
|
|
102
|
+
Examples:
|
|
103
|
+
- sql_search --pattern "%TODO%" --table files
|
|
104
|
+
- sql_search --pattern "test_%" --table symbols --column name
|
|
105
|
+
- sql_search --pattern "%config%" --table metadata
|
|
106
|
+
- sql_search --pattern "%.py" --table files --column path
|
|
107
|
+
|
|
108
|
+
Use sql_query for complex queries with joins, conditions, etc."""
|
|
109
|
+
|
|
110
|
+
@override
|
|
111
|
+
async def call(
|
|
112
|
+
self,
|
|
113
|
+
ctx: MCPContext,
|
|
114
|
+
**params: Unpack[SqlSearchParams],
|
|
115
|
+
) -> str:
|
|
116
|
+
"""Execute SQL search.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
ctx: MCP context
|
|
120
|
+
**params: Tool parameters
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Search results
|
|
124
|
+
"""
|
|
125
|
+
tool_ctx = create_tool_context(ctx)
|
|
126
|
+
await tool_ctx.set_tool_info(self.name)
|
|
127
|
+
|
|
128
|
+
# Extract parameters
|
|
129
|
+
pattern = params.get("pattern")
|
|
130
|
+
if not pattern:
|
|
131
|
+
return "Error: pattern is required"
|
|
132
|
+
|
|
133
|
+
table = params.get("table", "files")
|
|
134
|
+
column = params.get("column")
|
|
135
|
+
project_path = params.get("project_path")
|
|
136
|
+
max_results = params.get("max_results", 50)
|
|
137
|
+
|
|
138
|
+
# Validate table
|
|
139
|
+
valid_tables = ["files", "symbols", "metadata"]
|
|
140
|
+
if table not in valid_tables:
|
|
141
|
+
return f"Error: Invalid table '{table}'. Must be one of: {', '.join(valid_tables)}"
|
|
142
|
+
|
|
143
|
+
# Get project database
|
|
144
|
+
try:
|
|
145
|
+
if project_path:
|
|
146
|
+
project_db = self.db_manager.get_project_db(project_path)
|
|
147
|
+
else:
|
|
148
|
+
import os
|
|
149
|
+
project_db = self.db_manager.get_project_for_path(os.getcwd())
|
|
150
|
+
|
|
151
|
+
if not project_db:
|
|
152
|
+
return "Error: Could not find project database"
|
|
153
|
+
|
|
154
|
+
except PermissionError as e:
|
|
155
|
+
return str(e)
|
|
156
|
+
except Exception as e:
|
|
157
|
+
return f"Error accessing project database: {str(e)}"
|
|
158
|
+
|
|
159
|
+
await tool_ctx.info(f"Searching in {table} table for pattern: {pattern}")
|
|
160
|
+
|
|
161
|
+
# Build search query
|
|
162
|
+
conn = None
|
|
163
|
+
try:
|
|
164
|
+
conn = project_db.get_sqlite_connection()
|
|
165
|
+
cursor = conn.cursor()
|
|
166
|
+
|
|
167
|
+
# Get searchable columns for the table
|
|
168
|
+
if column:
|
|
169
|
+
# Validate column exists
|
|
170
|
+
cursor.execute(f"PRAGMA table_info({table})")
|
|
171
|
+
columns_info = cursor.fetchall()
|
|
172
|
+
column_names = [col[1] for col in columns_info]
|
|
173
|
+
|
|
174
|
+
if column not in column_names:
|
|
175
|
+
return f"Error: Column '{column}' not found in table '{table}'. Available columns: {', '.join(column_names)}"
|
|
176
|
+
|
|
177
|
+
search_columns = [column]
|
|
178
|
+
else:
|
|
179
|
+
# Get all text columns
|
|
180
|
+
search_columns = self._get_text_columns(cursor, table)
|
|
181
|
+
if not search_columns:
|
|
182
|
+
return f"Error: No searchable text columns in table '{table}'"
|
|
183
|
+
|
|
184
|
+
# Build WHERE clause
|
|
185
|
+
where_conditions = [f"{col} LIKE ?" for col in search_columns]
|
|
186
|
+
where_clause = " OR ".join(where_conditions)
|
|
187
|
+
|
|
188
|
+
# Build query
|
|
189
|
+
if table == "files":
|
|
190
|
+
query = f"""
|
|
191
|
+
SELECT path, SUBSTR(content, 1, 200) as snippet, size, modified_at
|
|
192
|
+
FROM {table}
|
|
193
|
+
WHERE {where_clause}
|
|
194
|
+
LIMIT ?
|
|
195
|
+
"""
|
|
196
|
+
params_list = [pattern] * len(search_columns) + [max_results]
|
|
197
|
+
|
|
198
|
+
elif table == "symbols":
|
|
199
|
+
query = f"""
|
|
200
|
+
SELECT name, type, file_path, line_start, signature
|
|
201
|
+
FROM {table}
|
|
202
|
+
WHERE {where_clause}
|
|
203
|
+
ORDER BY type, name
|
|
204
|
+
LIMIT ?
|
|
205
|
+
"""
|
|
206
|
+
params_list = [pattern] * len(search_columns) + [max_results]
|
|
207
|
+
|
|
208
|
+
else: # metadata
|
|
209
|
+
query = f"""
|
|
210
|
+
SELECT key, value, updated_at
|
|
211
|
+
FROM {table}
|
|
212
|
+
WHERE {where_clause}
|
|
213
|
+
LIMIT ?
|
|
214
|
+
"""
|
|
215
|
+
params_list = [pattern] * len(search_columns) + [max_results]
|
|
216
|
+
|
|
217
|
+
# Execute search
|
|
218
|
+
cursor.execute(query, params_list)
|
|
219
|
+
results = cursor.fetchall()
|
|
220
|
+
|
|
221
|
+
if not results:
|
|
222
|
+
return f"No results found for pattern '{pattern}' in {table}"
|
|
223
|
+
|
|
224
|
+
# Format results
|
|
225
|
+
output = self._format_results(table, results, pattern, search_columns)
|
|
226
|
+
|
|
227
|
+
return f"Found {len(results)} result(s) in {table}:\n\n{output}"
|
|
228
|
+
|
|
229
|
+
except sqlite3.Error as e:
|
|
230
|
+
await tool_ctx.error(f"SQL error: {str(e)}")
|
|
231
|
+
return f"SQL error: {str(e)}"
|
|
232
|
+
except Exception as e:
|
|
233
|
+
await tool_ctx.error(f"Unexpected error: {str(e)}")
|
|
234
|
+
return f"Error executing search: {str(e)}"
|
|
235
|
+
finally:
|
|
236
|
+
if conn:
|
|
237
|
+
conn.close()
|
|
238
|
+
|
|
239
|
+
def _get_text_columns(self, cursor: sqlite3.Cursor, table: str) -> list[str]:
|
|
240
|
+
"""Get text columns for a table."""
|
|
241
|
+
cursor.execute(f"PRAGMA table_info({table})")
|
|
242
|
+
columns_info = cursor.fetchall()
|
|
243
|
+
|
|
244
|
+
# Get TEXT columns
|
|
245
|
+
text_columns = []
|
|
246
|
+
for col in columns_info:
|
|
247
|
+
col_name = col[1]
|
|
248
|
+
col_type = col[2].upper()
|
|
249
|
+
if 'TEXT' in col_type or 'CHAR' in col_type or col_type == '':
|
|
250
|
+
text_columns.append(col_name)
|
|
251
|
+
|
|
252
|
+
return text_columns
|
|
253
|
+
|
|
254
|
+
def _format_results(self, table: str, results: list, pattern: str, search_columns: list[str]) -> str:
|
|
255
|
+
"""Format search results based on table type."""
|
|
256
|
+
output = []
|
|
257
|
+
|
|
258
|
+
if table == "files":
|
|
259
|
+
output.append(f"Searched columns: {', '.join(search_columns)}\n")
|
|
260
|
+
for row in results:
|
|
261
|
+
path, snippet, size, modified = row
|
|
262
|
+
output.append(f"File: {path}")
|
|
263
|
+
output.append(f"Size: {size} bytes")
|
|
264
|
+
output.append(f"Modified: {modified}")
|
|
265
|
+
if snippet:
|
|
266
|
+
# Highlight pattern in snippet
|
|
267
|
+
snippet = snippet.replace('\n', ' ')
|
|
268
|
+
if len(snippet) > 150:
|
|
269
|
+
snippet = snippet[:150] + "..."
|
|
270
|
+
output.append(f"Content: {snippet}")
|
|
271
|
+
output.append("-" * 60)
|
|
272
|
+
|
|
273
|
+
elif table == "symbols":
|
|
274
|
+
output.append(f"Searched columns: {', '.join(search_columns)}\n")
|
|
275
|
+
for row in results:
|
|
276
|
+
name, type_, file_path, line_start, signature = row
|
|
277
|
+
output.append(f"{type_}: {name}")
|
|
278
|
+
output.append(f"File: {file_path}:{line_start}")
|
|
279
|
+
if signature:
|
|
280
|
+
output.append(f"Signature: {signature}")
|
|
281
|
+
output.append("-" * 60)
|
|
282
|
+
|
|
283
|
+
else: # metadata
|
|
284
|
+
output.append(f"Searched columns: {', '.join(search_columns)}\n")
|
|
285
|
+
for row in results:
|
|
286
|
+
key, value, updated = row
|
|
287
|
+
output.append(f"Key: {key}")
|
|
288
|
+
output.append(f"Value: {value}")
|
|
289
|
+
output.append(f"Updated: {updated}")
|
|
290
|
+
output.append("-" * 60)
|
|
291
|
+
|
|
292
|
+
return "\n".join(output)
|
|
293
|
+
|
|
294
|
+
def register(self, mcp_server) -> None:
|
|
295
|
+
"""Register this tool with the MCP server."""
|
|
296
|
+
pass
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""SQL statistics tool for database insights."""
|
|
2
|
+
|
|
3
|
+
import sqlite3
|
|
4
|
+
from typing import Annotated, Optional, TypedDict, Unpack, final, override
|
|
5
|
+
|
|
6
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
7
|
+
from pydantic import Field
|
|
8
|
+
|
|
9
|
+
from hanzo_mcp.tools.common.base import BaseTool
|
|
10
|
+
from hanzo_mcp.tools.common.context import create_tool_context
|
|
11
|
+
from hanzo_mcp.tools.common.permissions import PermissionManager
|
|
12
|
+
from hanzo_mcp.tools.database.database_manager import DatabaseManager
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
ProjectPath = Annotated[
|
|
16
|
+
Optional[str],
|
|
17
|
+
Field(
|
|
18
|
+
description="Project path (defaults to current directory)",
|
|
19
|
+
default=None,
|
|
20
|
+
),
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
Detailed = Annotated[
|
|
24
|
+
bool,
|
|
25
|
+
Field(
|
|
26
|
+
description="Show detailed statistics",
|
|
27
|
+
default=False,
|
|
28
|
+
),
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SqlStatsParams(TypedDict, total=False):
|
|
33
|
+
"""Parameters for SQL stats tool."""
|
|
34
|
+
|
|
35
|
+
project_path: Optional[str]
|
|
36
|
+
detailed: bool
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@final
|
|
40
|
+
class SqlStatsTool(BaseTool):
|
|
41
|
+
"""Tool for getting SQLite database statistics."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, permission_manager: PermissionManager, db_manager: DatabaseManager):
|
|
44
|
+
"""Initialize the SQL stats tool.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
permission_manager: Permission manager for access control
|
|
48
|
+
db_manager: Database manager instance
|
|
49
|
+
"""
|
|
50
|
+
self.permission_manager = permission_manager
|
|
51
|
+
self.db_manager = db_manager
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
@override
|
|
55
|
+
def name(self) -> str:
|
|
56
|
+
"""Get the tool name."""
|
|
57
|
+
return "sql_stats"
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
@override
|
|
61
|
+
def description(self) -> str:
|
|
62
|
+
"""Get the tool description."""
|
|
63
|
+
return """Get statistics about the project's SQLite database.
|
|
64
|
+
|
|
65
|
+
Shows:
|
|
66
|
+
- Database size and location
|
|
67
|
+
- Table information (row counts, sizes)
|
|
68
|
+
- Index information
|
|
69
|
+
- Column statistics
|
|
70
|
+
- Most common values (with --detailed)
|
|
71
|
+
|
|
72
|
+
Examples:
|
|
73
|
+
- sql_stats # Basic stats for current project
|
|
74
|
+
- sql_stats --detailed # Detailed statistics
|
|
75
|
+
- sql_stats --project-path /path/to/project
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
@override
|
|
79
|
+
async def call(
|
|
80
|
+
self,
|
|
81
|
+
ctx: MCPContext,
|
|
82
|
+
**params: Unpack[SqlStatsParams],
|
|
83
|
+
) -> str:
|
|
84
|
+
"""Get database statistics.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
ctx: MCP context
|
|
88
|
+
**params: Tool parameters
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Database statistics
|
|
92
|
+
"""
|
|
93
|
+
tool_ctx = create_tool_context(ctx)
|
|
94
|
+
await tool_ctx.set_tool_info(self.name)
|
|
95
|
+
|
|
96
|
+
# Extract parameters
|
|
97
|
+
project_path = params.get("project_path")
|
|
98
|
+
detailed = params.get("detailed", False)
|
|
99
|
+
|
|
100
|
+
# Get project database
|
|
101
|
+
try:
|
|
102
|
+
if project_path:
|
|
103
|
+
project_db = self.db_manager.get_project_db(project_path)
|
|
104
|
+
else:
|
|
105
|
+
import os
|
|
106
|
+
project_db = self.db_manager.get_project_for_path(os.getcwd())
|
|
107
|
+
|
|
108
|
+
if not project_db:
|
|
109
|
+
return "Error: Could not find project database"
|
|
110
|
+
|
|
111
|
+
except PermissionError as e:
|
|
112
|
+
return str(e)
|
|
113
|
+
except Exception as e:
|
|
114
|
+
return f"Error accessing project database: {str(e)}"
|
|
115
|
+
|
|
116
|
+
await tool_ctx.info(f"Getting statistics for project: {project_db.project_path}")
|
|
117
|
+
|
|
118
|
+
# Collect statistics
|
|
119
|
+
conn = None
|
|
120
|
+
try:
|
|
121
|
+
conn = project_db.get_sqlite_connection()
|
|
122
|
+
cursor = conn.cursor()
|
|
123
|
+
|
|
124
|
+
output = []
|
|
125
|
+
output.append(f"=== SQLite Database Statistics ===")
|
|
126
|
+
output.append(f"Project: {project_db.project_path}")
|
|
127
|
+
output.append(f"Database: {project_db.sqlite_path}")
|
|
128
|
+
|
|
129
|
+
# Get database size
|
|
130
|
+
db_size = project_db.sqlite_path.stat().st_size
|
|
131
|
+
output.append(f"Database Size: {self._format_size(db_size)}")
|
|
132
|
+
output.append("")
|
|
133
|
+
|
|
134
|
+
# Get table statistics
|
|
135
|
+
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
|
|
136
|
+
tables = cursor.fetchall()
|
|
137
|
+
|
|
138
|
+
output.append("=== Tables ===")
|
|
139
|
+
total_rows = 0
|
|
140
|
+
|
|
141
|
+
for (table_name,) in tables:
|
|
142
|
+
if table_name.startswith('sqlite_'):
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
# Get row count
|
|
146
|
+
cursor.execute(f"SELECT COUNT(*) FROM {table_name}")
|
|
147
|
+
row_count = cursor.fetchone()[0]
|
|
148
|
+
total_rows += row_count
|
|
149
|
+
|
|
150
|
+
# Get table info
|
|
151
|
+
cursor.execute(f"PRAGMA table_info({table_name})")
|
|
152
|
+
columns = cursor.fetchall()
|
|
153
|
+
col_count = len(columns)
|
|
154
|
+
|
|
155
|
+
output.append(f"\n{table_name}:")
|
|
156
|
+
output.append(f" Rows: {row_count:,}")
|
|
157
|
+
output.append(f" Columns: {col_count}")
|
|
158
|
+
|
|
159
|
+
if detailed and row_count > 0:
|
|
160
|
+
# Show column details
|
|
161
|
+
output.append(" Columns:")
|
|
162
|
+
for col in columns:
|
|
163
|
+
col_name = col[1]
|
|
164
|
+
col_type = col[2]
|
|
165
|
+
is_pk = col[5]
|
|
166
|
+
not_null = col[3]
|
|
167
|
+
|
|
168
|
+
flags = []
|
|
169
|
+
if is_pk:
|
|
170
|
+
flags.append("PRIMARY KEY")
|
|
171
|
+
if not_null:
|
|
172
|
+
flags.append("NOT NULL")
|
|
173
|
+
|
|
174
|
+
flag_str = f" ({', '.join(flags)})" if flags else ""
|
|
175
|
+
output.append(f" - {col_name}: {col_type}{flag_str}")
|
|
176
|
+
|
|
177
|
+
# Show sample data for specific tables
|
|
178
|
+
if table_name == "files" and row_count > 0:
|
|
179
|
+
cursor.execute(f"SELECT COUNT(DISTINCT SUBSTR(path, -3)) as ext_count FROM {table_name}")
|
|
180
|
+
ext_count = cursor.fetchone()[0]
|
|
181
|
+
output.append(f" File types: ~{ext_count}")
|
|
182
|
+
|
|
183
|
+
elif table_name == "symbols" and row_count > 0:
|
|
184
|
+
cursor.execute(f"SELECT type, COUNT(*) as count FROM {table_name} GROUP BY type ORDER BY count DESC LIMIT 5")
|
|
185
|
+
symbol_types = cursor.fetchall()
|
|
186
|
+
output.append(" Symbol types:")
|
|
187
|
+
for sym_type, count in symbol_types:
|
|
188
|
+
output.append(f" - {sym_type}: {count}")
|
|
189
|
+
|
|
190
|
+
# Get indexes
|
|
191
|
+
cursor.execute(f"PRAGMA index_list({table_name})")
|
|
192
|
+
indexes = cursor.fetchall()
|
|
193
|
+
if indexes:
|
|
194
|
+
output.append(f" Indexes: {len(indexes)}")
|
|
195
|
+
|
|
196
|
+
output.append(f"\nTotal Rows: {total_rows:,}")
|
|
197
|
+
|
|
198
|
+
# Get index statistics
|
|
199
|
+
cursor.execute("SELECT name FROM sqlite_master WHERE type='index' AND sql IS NOT NULL ORDER BY name")
|
|
200
|
+
indexes = cursor.fetchall()
|
|
201
|
+
if indexes:
|
|
202
|
+
output.append(f"\n=== Indexes ===")
|
|
203
|
+
output.append(f"Total Indexes: {len(indexes)}")
|
|
204
|
+
|
|
205
|
+
if detailed:
|
|
206
|
+
for (idx_name,) in indexes:
|
|
207
|
+
cursor.execute(f"PRAGMA index_info({idx_name})")
|
|
208
|
+
idx_info = cursor.fetchall()
|
|
209
|
+
if idx_info:
|
|
210
|
+
cols = [info[2] for info in idx_info]
|
|
211
|
+
output.append(f" {idx_name}: ({', '.join(cols)})")
|
|
212
|
+
|
|
213
|
+
# Database properties
|
|
214
|
+
if detailed:
|
|
215
|
+
output.append("\n=== Database Properties ===")
|
|
216
|
+
|
|
217
|
+
# Page size
|
|
218
|
+
cursor.execute("PRAGMA page_size")
|
|
219
|
+
page_size = cursor.fetchone()[0]
|
|
220
|
+
output.append(f"Page Size: {page_size:,} bytes")
|
|
221
|
+
|
|
222
|
+
# Page count
|
|
223
|
+
cursor.execute("PRAGMA page_count")
|
|
224
|
+
page_count = cursor.fetchone()[0]
|
|
225
|
+
output.append(f"Page Count: {page_count:,}")
|
|
226
|
+
|
|
227
|
+
# Cache size
|
|
228
|
+
cursor.execute("PRAGMA cache_size")
|
|
229
|
+
cache_size = cursor.fetchone()[0]
|
|
230
|
+
output.append(f"Cache Size: {abs(cache_size):,} pages")
|
|
231
|
+
|
|
232
|
+
return "\n".join(output)
|
|
233
|
+
|
|
234
|
+
except sqlite3.Error as e:
|
|
235
|
+
await tool_ctx.error(f"SQL error: {str(e)}")
|
|
236
|
+
return f"SQL error: {str(e)}"
|
|
237
|
+
except Exception as e:
|
|
238
|
+
await tool_ctx.error(f"Unexpected error: {str(e)}")
|
|
239
|
+
return f"Error getting statistics: {str(e)}"
|
|
240
|
+
finally:
|
|
241
|
+
if conn:
|
|
242
|
+
conn.close()
|
|
243
|
+
|
|
244
|
+
def _format_size(self, size: int) -> str:
|
|
245
|
+
"""Format file size in human-readable format."""
|
|
246
|
+
for unit in ['B', 'KB', 'MB', 'GB']:
|
|
247
|
+
if size < 1024.0:
|
|
248
|
+
return f"{size:.1f} {unit}"
|
|
249
|
+
size /= 1024.0
|
|
250
|
+
return f"{size:.1f} TB"
|
|
251
|
+
|
|
252
|
+
def register(self, mcp_server) -> None:
|
|
253
|
+
"""Register this tool with the MCP server."""
|
|
254
|
+
pass
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Editor integration tools for Hanzo MCP."""
|
|
2
|
+
|
|
3
|
+
from hanzo_mcp.tools.editor.neovim_edit import NeovimEditTool
|
|
4
|
+
from hanzo_mcp.tools.editor.neovim_command import NeovimCommandTool
|
|
5
|
+
from hanzo_mcp.tools.editor.neovim_session import NeovimSessionTool
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"NeovimEditTool",
|
|
9
|
+
"NeovimCommandTool",
|
|
10
|
+
"NeovimSessionTool",
|
|
11
|
+
]
|