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.

Files changed (114) hide show
  1. hanzo_mcp/__init__.py +1 -1
  2. hanzo_mcp/cli.py +32 -0
  3. hanzo_mcp/dev_server.py +246 -0
  4. hanzo_mcp/prompts/__init__.py +1 -1
  5. hanzo_mcp/prompts/project_system.py +43 -7
  6. hanzo_mcp/server.py +5 -1
  7. hanzo_mcp/tools/__init__.py +66 -35
  8. hanzo_mcp/tools/agent/__init__.py +1 -1
  9. hanzo_mcp/tools/agent/agent.py +401 -0
  10. hanzo_mcp/tools/agent/agent_tool.py +3 -4
  11. hanzo_mcp/tools/common/__init__.py +1 -1
  12. hanzo_mcp/tools/common/base.py +2 -2
  13. hanzo_mcp/tools/common/batch_tool.py +3 -5
  14. hanzo_mcp/tools/common/config_tool.py +1 -1
  15. hanzo_mcp/tools/common/context.py +1 -1
  16. hanzo_mcp/tools/common/palette.py +344 -0
  17. hanzo_mcp/tools/common/palette_loader.py +108 -0
  18. hanzo_mcp/tools/common/stats.py +1 -1
  19. hanzo_mcp/tools/common/thinking_tool.py +3 -5
  20. hanzo_mcp/tools/common/tool_disable.py +1 -1
  21. hanzo_mcp/tools/common/tool_enable.py +1 -1
  22. hanzo_mcp/tools/common/tool_list.py +49 -52
  23. hanzo_mcp/tools/config/__init__.py +10 -0
  24. hanzo_mcp/tools/config/config_tool.py +212 -0
  25. hanzo_mcp/tools/config/index_config.py +176 -0
  26. hanzo_mcp/tools/config/palette_tool.py +166 -0
  27. hanzo_mcp/tools/database/__init__.py +1 -1
  28. hanzo_mcp/tools/database/graph.py +482 -0
  29. hanzo_mcp/tools/database/graph_add.py +1 -1
  30. hanzo_mcp/tools/database/graph_query.py +1 -1
  31. hanzo_mcp/tools/database/graph_remove.py +1 -1
  32. hanzo_mcp/tools/database/graph_search.py +1 -1
  33. hanzo_mcp/tools/database/graph_stats.py +1 -1
  34. hanzo_mcp/tools/database/sql.py +411 -0
  35. hanzo_mcp/tools/database/sql_query.py +1 -1
  36. hanzo_mcp/tools/database/sql_search.py +1 -1
  37. hanzo_mcp/tools/database/sql_stats.py +1 -1
  38. hanzo_mcp/tools/editor/neovim_command.py +1 -1
  39. hanzo_mcp/tools/editor/neovim_edit.py +1 -1
  40. hanzo_mcp/tools/editor/neovim_session.py +1 -1
  41. hanzo_mcp/tools/filesystem/__init__.py +42 -13
  42. hanzo_mcp/tools/filesystem/base.py +1 -1
  43. hanzo_mcp/tools/filesystem/batch_search.py +4 -4
  44. hanzo_mcp/tools/filesystem/content_replace.py +3 -5
  45. hanzo_mcp/tools/filesystem/diff.py +193 -0
  46. hanzo_mcp/tools/filesystem/directory_tree.py +3 -5
  47. hanzo_mcp/tools/filesystem/edit.py +3 -5
  48. hanzo_mcp/tools/filesystem/find.py +443 -0
  49. hanzo_mcp/tools/filesystem/find_files.py +1 -1
  50. hanzo_mcp/tools/filesystem/git_search.py +1 -1
  51. hanzo_mcp/tools/filesystem/grep.py +2 -2
  52. hanzo_mcp/tools/filesystem/multi_edit.py +3 -5
  53. hanzo_mcp/tools/filesystem/read.py +17 -5
  54. hanzo_mcp/tools/filesystem/{grep_ast_tool.py → symbols.py} +17 -27
  55. hanzo_mcp/tools/filesystem/symbols_unified.py +376 -0
  56. hanzo_mcp/tools/filesystem/tree.py +268 -0
  57. hanzo_mcp/tools/filesystem/unified_search.py +711 -0
  58. hanzo_mcp/tools/filesystem/unix_aliases.py +99 -0
  59. hanzo_mcp/tools/filesystem/watch.py +174 -0
  60. hanzo_mcp/tools/filesystem/write.py +3 -5
  61. hanzo_mcp/tools/jupyter/__init__.py +9 -12
  62. hanzo_mcp/tools/jupyter/base.py +1 -1
  63. hanzo_mcp/tools/jupyter/jupyter.py +326 -0
  64. hanzo_mcp/tools/jupyter/notebook_edit.py +3 -4
  65. hanzo_mcp/tools/jupyter/notebook_read.py +3 -5
  66. hanzo_mcp/tools/llm/__init__.py +4 -0
  67. hanzo_mcp/tools/llm/consensus_tool.py +1 -1
  68. hanzo_mcp/tools/llm/llm_manage.py +1 -1
  69. hanzo_mcp/tools/llm/llm_tool.py +1 -1
  70. hanzo_mcp/tools/llm/llm_unified.py +851 -0
  71. hanzo_mcp/tools/llm/provider_tools.py +1 -1
  72. hanzo_mcp/tools/mcp/__init__.py +4 -0
  73. hanzo_mcp/tools/mcp/mcp_add.py +1 -1
  74. hanzo_mcp/tools/mcp/mcp_remove.py +1 -1
  75. hanzo_mcp/tools/mcp/mcp_stats.py +1 -1
  76. hanzo_mcp/tools/mcp/mcp_unified.py +503 -0
  77. hanzo_mcp/tools/shell/__init__.py +20 -42
  78. hanzo_mcp/tools/shell/base.py +1 -1
  79. hanzo_mcp/tools/shell/base_process.py +303 -0
  80. hanzo_mcp/tools/shell/bash_unified.py +134 -0
  81. hanzo_mcp/tools/shell/logs.py +1 -1
  82. hanzo_mcp/tools/shell/npx.py +1 -1
  83. hanzo_mcp/tools/shell/npx_background.py +1 -1
  84. hanzo_mcp/tools/shell/npx_unified.py +101 -0
  85. hanzo_mcp/tools/shell/open.py +107 -0
  86. hanzo_mcp/tools/shell/pkill.py +1 -1
  87. hanzo_mcp/tools/shell/process_unified.py +131 -0
  88. hanzo_mcp/tools/shell/processes.py +1 -1
  89. hanzo_mcp/tools/shell/run_background.py +1 -1
  90. hanzo_mcp/tools/shell/run_command.py +3 -4
  91. hanzo_mcp/tools/shell/run_command_windows.py +3 -4
  92. hanzo_mcp/tools/shell/uvx.py +1 -1
  93. hanzo_mcp/tools/shell/uvx_background.py +1 -1
  94. hanzo_mcp/tools/shell/uvx_unified.py +101 -0
  95. hanzo_mcp/tools/todo/__init__.py +1 -1
  96. hanzo_mcp/tools/todo/base.py +1 -1
  97. hanzo_mcp/tools/todo/todo.py +265 -0
  98. hanzo_mcp/tools/todo/todo_read.py +3 -5
  99. hanzo_mcp/tools/todo/todo_write.py +3 -5
  100. hanzo_mcp/tools/vector/__init__.py +1 -1
  101. hanzo_mcp/tools/vector/index_tool.py +1 -1
  102. hanzo_mcp/tools/vector/project_manager.py +27 -5
  103. hanzo_mcp/tools/vector/vector.py +311 -0
  104. hanzo_mcp/tools/vector/vector_index.py +1 -1
  105. hanzo_mcp/tools/vector/vector_search.py +1 -1
  106. hanzo_mcp-0.6.1.dist-info/METADATA +336 -0
  107. hanzo_mcp-0.6.1.dist-info/RECORD +134 -0
  108. hanzo_mcp-0.6.1.dist-info/entry_points.txt +3 -0
  109. hanzo_mcp-0.5.2.dist-info/METADATA +0 -276
  110. hanzo_mcp-0.5.2.dist-info/RECORD +0 -106
  111. hanzo_mcp-0.5.2.dist-info/entry_points.txt +0 -2
  112. {hanzo_mcp-0.5.2.dist-info → hanzo_mcp-0.6.1.dist-info}/WHEEL +0 -0
  113. {hanzo_mcp-0.5.2.dist-info → hanzo_mcp-0.6.1.dist-info}/licenses/LICENSE +0 -0
  114. {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 fastmcp import FastMCP
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.grep_ast_tool import GrepAstTool
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
- "GrepAstTool",
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
- return [
59
+ tools = [
54
60
  ReadTool(permission_manager),
55
61
  DirectoryTreeTool(permission_manager),
56
62
  Grep(permission_manager),
57
- GrepAstTool(permission_manager),
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
- return [
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
- GrepAstTool(permission_manager),
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": GrepAstTool,
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 == "batch_search":
130
- # Batch search requires project_manager
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 fastmcp import FastMCP
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.grep_ast_tool import GrepAstTool
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 = GrepAstTool(permission_manager)
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