hanzo-mcp 0.5.0__py3-none-any.whl → 0.5.2__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 (60) hide show
  1. hanzo_mcp/__init__.py +1 -1
  2. hanzo_mcp/config/settings.py +61 -0
  3. hanzo_mcp/tools/__init__.py +158 -12
  4. hanzo_mcp/tools/common/base.py +7 -2
  5. hanzo_mcp/tools/common/config_tool.py +396 -0
  6. hanzo_mcp/tools/common/stats.py +261 -0
  7. hanzo_mcp/tools/common/tool_disable.py +144 -0
  8. hanzo_mcp/tools/common/tool_enable.py +182 -0
  9. hanzo_mcp/tools/common/tool_list.py +263 -0
  10. hanzo_mcp/tools/database/__init__.py +71 -0
  11. hanzo_mcp/tools/database/database_manager.py +246 -0
  12. hanzo_mcp/tools/database/graph_add.py +257 -0
  13. hanzo_mcp/tools/database/graph_query.py +536 -0
  14. hanzo_mcp/tools/database/graph_remove.py +267 -0
  15. hanzo_mcp/tools/database/graph_search.py +348 -0
  16. hanzo_mcp/tools/database/graph_stats.py +345 -0
  17. hanzo_mcp/tools/database/sql_query.py +229 -0
  18. hanzo_mcp/tools/database/sql_search.py +296 -0
  19. hanzo_mcp/tools/database/sql_stats.py +254 -0
  20. hanzo_mcp/tools/editor/__init__.py +11 -0
  21. hanzo_mcp/tools/editor/neovim_command.py +272 -0
  22. hanzo_mcp/tools/editor/neovim_edit.py +290 -0
  23. hanzo_mcp/tools/editor/neovim_session.py +356 -0
  24. hanzo_mcp/tools/filesystem/__init__.py +20 -1
  25. hanzo_mcp/tools/filesystem/batch_search.py +812 -0
  26. hanzo_mcp/tools/filesystem/find_files.py +348 -0
  27. hanzo_mcp/tools/filesystem/git_search.py +505 -0
  28. hanzo_mcp/tools/llm/__init__.py +27 -0
  29. hanzo_mcp/tools/llm/consensus_tool.py +351 -0
  30. hanzo_mcp/tools/llm/llm_manage.py +413 -0
  31. hanzo_mcp/tools/llm/llm_tool.py +346 -0
  32. hanzo_mcp/tools/llm/provider_tools.py +412 -0
  33. hanzo_mcp/tools/mcp/__init__.py +11 -0
  34. hanzo_mcp/tools/mcp/mcp_add.py +263 -0
  35. hanzo_mcp/tools/mcp/mcp_remove.py +127 -0
  36. hanzo_mcp/tools/mcp/mcp_stats.py +165 -0
  37. hanzo_mcp/tools/shell/__init__.py +27 -7
  38. hanzo_mcp/tools/shell/logs.py +265 -0
  39. hanzo_mcp/tools/shell/npx.py +194 -0
  40. hanzo_mcp/tools/shell/npx_background.py +254 -0
  41. hanzo_mcp/tools/shell/pkill.py +262 -0
  42. hanzo_mcp/tools/shell/processes.py +279 -0
  43. hanzo_mcp/tools/shell/run_background.py +326 -0
  44. hanzo_mcp/tools/shell/uvx.py +187 -0
  45. hanzo_mcp/tools/shell/uvx_background.py +249 -0
  46. hanzo_mcp/tools/vector/__init__.py +21 -12
  47. hanzo_mcp/tools/vector/ast_analyzer.py +459 -0
  48. hanzo_mcp/tools/vector/git_ingester.py +485 -0
  49. hanzo_mcp/tools/vector/index_tool.py +358 -0
  50. hanzo_mcp/tools/vector/infinity_store.py +465 -1
  51. hanzo_mcp/tools/vector/mock_infinity.py +162 -0
  52. hanzo_mcp/tools/vector/vector_index.py +7 -6
  53. hanzo_mcp/tools/vector/vector_search.py +22 -7
  54. {hanzo_mcp-0.5.0.dist-info → hanzo_mcp-0.5.2.dist-info}/METADATA +68 -20
  55. hanzo_mcp-0.5.2.dist-info/RECORD +106 -0
  56. hanzo_mcp-0.5.0.dist-info/RECORD +0 -63
  57. {hanzo_mcp-0.5.0.dist-info → hanzo_mcp-0.5.2.dist-info}/WHEEL +0 -0
  58. {hanzo_mcp-0.5.0.dist-info → hanzo_mcp-0.5.2.dist-info}/entry_points.txt +0 -0
  59. {hanzo_mcp-0.5.0.dist-info → hanzo_mcp-0.5.2.dist-info}/licenses/LICENSE +0 -0
  60. {hanzo_mcp-0.5.0.dist-info → hanzo_mcp-0.5.2.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 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 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
+ ]