hanzo-mcp 0.7.7__py3-none-any.whl → 0.8.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of hanzo-mcp might be problematic. Click here for more details.

Files changed (178) hide show
  1. hanzo_mcp/__init__.py +6 -0
  2. hanzo_mcp/__main__.py +1 -1
  3. hanzo_mcp/analytics/__init__.py +2 -2
  4. hanzo_mcp/analytics/posthog_analytics.py +76 -82
  5. hanzo_mcp/cli.py +31 -36
  6. hanzo_mcp/cli_enhanced.py +94 -72
  7. hanzo_mcp/cli_plugin.py +27 -17
  8. hanzo_mcp/config/__init__.py +2 -2
  9. hanzo_mcp/config/settings.py +112 -88
  10. hanzo_mcp/config/tool_config.py +32 -34
  11. hanzo_mcp/dev_server.py +66 -67
  12. hanzo_mcp/prompts/__init__.py +94 -12
  13. hanzo_mcp/prompts/enhanced_prompts.py +809 -0
  14. hanzo_mcp/prompts/example_custom_prompt.py +6 -5
  15. hanzo_mcp/prompts/project_todo_reminder.py +0 -1
  16. hanzo_mcp/prompts/tool_explorer.py +10 -7
  17. hanzo_mcp/server.py +17 -21
  18. hanzo_mcp/server_enhanced.py +15 -22
  19. hanzo_mcp/tools/__init__.py +56 -28
  20. hanzo_mcp/tools/agent/__init__.py +16 -19
  21. hanzo_mcp/tools/agent/agent.py +82 -65
  22. hanzo_mcp/tools/agent/agent_tool.py +152 -122
  23. hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +66 -62
  24. hanzo_mcp/tools/agent/clarification_protocol.py +55 -50
  25. hanzo_mcp/tools/agent/clarification_tool.py +11 -10
  26. hanzo_mcp/tools/agent/claude_cli_tool.py +21 -20
  27. hanzo_mcp/tools/agent/claude_desktop_auth.py +130 -144
  28. hanzo_mcp/tools/agent/cli_agent_base.py +59 -53
  29. hanzo_mcp/tools/agent/code_auth.py +102 -107
  30. hanzo_mcp/tools/agent/code_auth_tool.py +28 -27
  31. hanzo_mcp/tools/agent/codex_cli_tool.py +20 -19
  32. hanzo_mcp/tools/agent/critic_tool.py +86 -73
  33. hanzo_mcp/tools/agent/gemini_cli_tool.py +21 -20
  34. hanzo_mcp/tools/agent/grok_cli_tool.py +21 -20
  35. hanzo_mcp/tools/agent/iching_tool.py +404 -139
  36. hanzo_mcp/tools/agent/network_tool.py +89 -73
  37. hanzo_mcp/tools/agent/prompt.py +2 -1
  38. hanzo_mcp/tools/agent/review_tool.py +101 -98
  39. hanzo_mcp/tools/agent/swarm_alias.py +87 -0
  40. hanzo_mcp/tools/agent/swarm_tool.py +246 -161
  41. hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +134 -92
  42. hanzo_mcp/tools/agent/tool_adapter.py +21 -11
  43. hanzo_mcp/tools/common/__init__.py +1 -1
  44. hanzo_mcp/tools/common/base.py +3 -5
  45. hanzo_mcp/tools/common/batch_tool.py +46 -39
  46. hanzo_mcp/tools/common/config_tool.py +120 -84
  47. hanzo_mcp/tools/common/context.py +1 -5
  48. hanzo_mcp/tools/common/context_fix.py +5 -3
  49. hanzo_mcp/tools/common/critic_tool.py +4 -8
  50. hanzo_mcp/tools/common/decorators.py +58 -56
  51. hanzo_mcp/tools/common/enhanced_base.py +29 -32
  52. hanzo_mcp/tools/common/fastmcp_pagination.py +91 -94
  53. hanzo_mcp/tools/common/forgiving_edit.py +91 -87
  54. hanzo_mcp/tools/common/mode.py +15 -17
  55. hanzo_mcp/tools/common/mode_loader.py +27 -24
  56. hanzo_mcp/tools/common/paginated_base.py +61 -53
  57. hanzo_mcp/tools/common/paginated_response.py +72 -79
  58. hanzo_mcp/tools/common/pagination.py +50 -53
  59. hanzo_mcp/tools/common/permissions.py +4 -4
  60. hanzo_mcp/tools/common/personality.py +186 -138
  61. hanzo_mcp/tools/common/plugin_loader.py +54 -54
  62. hanzo_mcp/tools/common/stats.py +65 -47
  63. hanzo_mcp/tools/common/test_helpers.py +31 -0
  64. hanzo_mcp/tools/common/thinking_tool.py +4 -8
  65. hanzo_mcp/tools/common/tool_disable.py +17 -12
  66. hanzo_mcp/tools/common/tool_enable.py +13 -14
  67. hanzo_mcp/tools/common/tool_list.py +36 -28
  68. hanzo_mcp/tools/common/truncate.py +23 -23
  69. hanzo_mcp/tools/config/__init__.py +4 -4
  70. hanzo_mcp/tools/config/config_tool.py +42 -29
  71. hanzo_mcp/tools/config/index_config.py +37 -34
  72. hanzo_mcp/tools/config/mode_tool.py +175 -55
  73. hanzo_mcp/tools/database/__init__.py +15 -12
  74. hanzo_mcp/tools/database/database_manager.py +77 -75
  75. hanzo_mcp/tools/database/graph.py +137 -91
  76. hanzo_mcp/tools/database/graph_add.py +30 -18
  77. hanzo_mcp/tools/database/graph_query.py +178 -102
  78. hanzo_mcp/tools/database/graph_remove.py +33 -28
  79. hanzo_mcp/tools/database/graph_search.py +97 -75
  80. hanzo_mcp/tools/database/graph_stats.py +91 -59
  81. hanzo_mcp/tools/database/sql.py +107 -79
  82. hanzo_mcp/tools/database/sql_query.py +30 -24
  83. hanzo_mcp/tools/database/sql_search.py +29 -25
  84. hanzo_mcp/tools/database/sql_stats.py +47 -35
  85. hanzo_mcp/tools/editor/neovim_command.py +25 -28
  86. hanzo_mcp/tools/editor/neovim_edit.py +21 -23
  87. hanzo_mcp/tools/editor/neovim_session.py +60 -54
  88. hanzo_mcp/tools/filesystem/__init__.py +31 -30
  89. hanzo_mcp/tools/filesystem/ast_multi_edit.py +329 -249
  90. hanzo_mcp/tools/filesystem/ast_tool.py +4 -4
  91. hanzo_mcp/tools/filesystem/base.py +1 -1
  92. hanzo_mcp/tools/filesystem/batch_search.py +316 -224
  93. hanzo_mcp/tools/filesystem/content_replace.py +4 -4
  94. hanzo_mcp/tools/filesystem/diff.py +71 -59
  95. hanzo_mcp/tools/filesystem/directory_tree.py +7 -7
  96. hanzo_mcp/tools/filesystem/directory_tree_paginated.py +49 -37
  97. hanzo_mcp/tools/filesystem/edit.py +4 -4
  98. hanzo_mcp/tools/filesystem/find.py +173 -80
  99. hanzo_mcp/tools/filesystem/find_files.py +73 -52
  100. hanzo_mcp/tools/filesystem/git_search.py +157 -104
  101. hanzo_mcp/tools/filesystem/grep.py +8 -8
  102. hanzo_mcp/tools/filesystem/multi_edit.py +4 -8
  103. hanzo_mcp/tools/filesystem/read.py +12 -10
  104. hanzo_mcp/tools/filesystem/rules_tool.py +59 -43
  105. hanzo_mcp/tools/filesystem/search_tool.py +263 -207
  106. hanzo_mcp/tools/filesystem/symbols_tool.py +94 -54
  107. hanzo_mcp/tools/filesystem/tree.py +35 -33
  108. hanzo_mcp/tools/filesystem/unix_aliases.py +13 -18
  109. hanzo_mcp/tools/filesystem/watch.py +37 -36
  110. hanzo_mcp/tools/filesystem/write.py +4 -8
  111. hanzo_mcp/tools/jupyter/__init__.py +4 -4
  112. hanzo_mcp/tools/jupyter/base.py +4 -5
  113. hanzo_mcp/tools/jupyter/jupyter.py +67 -47
  114. hanzo_mcp/tools/jupyter/notebook_edit.py +4 -4
  115. hanzo_mcp/tools/jupyter/notebook_read.py +4 -7
  116. hanzo_mcp/tools/llm/__init__.py +5 -7
  117. hanzo_mcp/tools/llm/consensus_tool.py +72 -52
  118. hanzo_mcp/tools/llm/llm_manage.py +101 -60
  119. hanzo_mcp/tools/llm/llm_tool.py +226 -166
  120. hanzo_mcp/tools/llm/provider_tools.py +25 -26
  121. hanzo_mcp/tools/lsp/__init__.py +1 -1
  122. hanzo_mcp/tools/lsp/lsp_tool.py +228 -143
  123. hanzo_mcp/tools/mcp/__init__.py +2 -3
  124. hanzo_mcp/tools/mcp/mcp_add.py +27 -25
  125. hanzo_mcp/tools/mcp/mcp_remove.py +7 -8
  126. hanzo_mcp/tools/mcp/mcp_stats.py +23 -22
  127. hanzo_mcp/tools/mcp/mcp_tool.py +129 -98
  128. hanzo_mcp/tools/memory/__init__.py +39 -21
  129. hanzo_mcp/tools/memory/knowledge_tools.py +124 -99
  130. hanzo_mcp/tools/memory/memory_tools.py +90 -108
  131. hanzo_mcp/tools/search/__init__.py +7 -2
  132. hanzo_mcp/tools/search/find_tool.py +297 -212
  133. hanzo_mcp/tools/search/unified_search.py +366 -314
  134. hanzo_mcp/tools/shell/__init__.py +8 -7
  135. hanzo_mcp/tools/shell/auto_background.py +56 -49
  136. hanzo_mcp/tools/shell/base.py +1 -1
  137. hanzo_mcp/tools/shell/base_process.py +75 -75
  138. hanzo_mcp/tools/shell/bash_session.py +2 -2
  139. hanzo_mcp/tools/shell/bash_session_executor.py +4 -4
  140. hanzo_mcp/tools/shell/bash_tool.py +24 -31
  141. hanzo_mcp/tools/shell/command_executor.py +12 -12
  142. hanzo_mcp/tools/shell/logs.py +43 -33
  143. hanzo_mcp/tools/shell/npx.py +13 -13
  144. hanzo_mcp/tools/shell/npx_background.py +24 -21
  145. hanzo_mcp/tools/shell/npx_tool.py +18 -22
  146. hanzo_mcp/tools/shell/open.py +19 -21
  147. hanzo_mcp/tools/shell/pkill.py +31 -26
  148. hanzo_mcp/tools/shell/process_tool.py +32 -32
  149. hanzo_mcp/tools/shell/processes.py +57 -58
  150. hanzo_mcp/tools/shell/run_background.py +24 -25
  151. hanzo_mcp/tools/shell/run_command.py +5 -5
  152. hanzo_mcp/tools/shell/run_command_windows.py +5 -5
  153. hanzo_mcp/tools/shell/session_storage.py +3 -3
  154. hanzo_mcp/tools/shell/streaming_command.py +141 -126
  155. hanzo_mcp/tools/shell/uvx.py +24 -25
  156. hanzo_mcp/tools/shell/uvx_background.py +35 -33
  157. hanzo_mcp/tools/shell/uvx_tool.py +18 -22
  158. hanzo_mcp/tools/todo/__init__.py +6 -2
  159. hanzo_mcp/tools/todo/todo.py +50 -37
  160. hanzo_mcp/tools/todo/todo_read.py +5 -8
  161. hanzo_mcp/tools/todo/todo_write.py +5 -7
  162. hanzo_mcp/tools/vector/__init__.py +40 -28
  163. hanzo_mcp/tools/vector/ast_analyzer.py +176 -143
  164. hanzo_mcp/tools/vector/git_ingester.py +170 -179
  165. hanzo_mcp/tools/vector/index_tool.py +96 -44
  166. hanzo_mcp/tools/vector/infinity_store.py +283 -228
  167. hanzo_mcp/tools/vector/mock_infinity.py +39 -40
  168. hanzo_mcp/tools/vector/project_manager.py +88 -78
  169. hanzo_mcp/tools/vector/vector.py +59 -42
  170. hanzo_mcp/tools/vector/vector_index.py +30 -27
  171. hanzo_mcp/tools/vector/vector_search.py +64 -45
  172. hanzo_mcp/types.py +6 -4
  173. {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/METADATA +1 -1
  174. hanzo_mcp-0.8.1.dist-info/RECORD +185 -0
  175. hanzo_mcp-0.7.7.dist-info/RECORD +0 -182
  176. {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/WHEEL +0 -0
  177. {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/entry_points.txt +0 -0
  178. {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/top_level.txt +0 -0
@@ -1,17 +1,16 @@
1
1
  """SQL search tool for text search in database."""
2
2
 
3
3
  import sqlite3
4
- from typing import Annotated, Optional, TypedDict, Unpack, final, override
4
+ from typing import Unpack, Optional, Annotated, TypedDict, final, override
5
5
 
6
- from mcp.server.fastmcp import Context as MCPContext
7
6
  from pydantic import Field
7
+ from mcp.server.fastmcp import Context as MCPContext
8
8
 
9
9
  from hanzo_mcp.tools.common.base import BaseTool
10
10
  from hanzo_mcp.tools.common.context import create_tool_context
11
11
  from hanzo_mcp.tools.common.permissions import PermissionManager
12
12
  from hanzo_mcp.tools.database.database_manager import DatabaseManager
13
13
 
14
-
15
14
  SearchPattern = Annotated[
16
15
  str,
17
16
  Field(
@@ -67,7 +66,9 @@ class SqlSearchParams(TypedDict, total=False):
67
66
  class SqlSearchTool(BaseTool):
68
67
  """Tool for searching text in SQLite database."""
69
68
 
70
- def __init__(self, permission_manager: PermissionManager, db_manager: DatabaseManager):
69
+ def __init__(
70
+ self, permission_manager: PermissionManager, db_manager: DatabaseManager
71
+ ):
71
72
  """Initialize the SQL search tool.
72
73
 
73
74
  Args:
@@ -146,8 +147,9 @@ Use sql_query for complex queries with joins, conditions, etc."""
146
147
  project_db = self.db_manager.get_project_db(project_path)
147
148
  else:
148
149
  import os
150
+
149
151
  project_db = self.db_manager.get_project_for_path(os.getcwd())
150
-
152
+
151
153
  if not project_db:
152
154
  return "Error: Could not find project database"
153
155
 
@@ -163,17 +165,17 @@ Use sql_query for complex queries with joins, conditions, etc."""
163
165
  try:
164
166
  conn = project_db.get_sqlite_connection()
165
167
  cursor = conn.cursor()
166
-
168
+
167
169
  # Get searchable columns for the table
168
170
  if column:
169
171
  # Validate column exists
170
172
  cursor.execute(f"PRAGMA table_info({table})")
171
173
  columns_info = cursor.fetchall()
172
174
  column_names = [col[1] for col in columns_info]
173
-
175
+
174
176
  if column not in column_names:
175
177
  return f"Error: Column '{column}' not found in table '{table}'. Available columns: {', '.join(column_names)}"
176
-
178
+
177
179
  search_columns = [column]
178
180
  else:
179
181
  # Get all text columns
@@ -184,7 +186,7 @@ Use sql_query for complex queries with joins, conditions, etc."""
184
186
  # Build WHERE clause
185
187
  where_conditions = [f"{col} LIKE ?" for col in search_columns]
186
188
  where_clause = " OR ".join(where_conditions)
187
-
189
+
188
190
  # Build query
189
191
  if table == "files":
190
192
  query = f"""
@@ -194,7 +196,7 @@ Use sql_query for complex queries with joins, conditions, etc."""
194
196
  LIMIT ?
195
197
  """
196
198
  params_list = [pattern] * len(search_columns) + [max_results]
197
-
199
+
198
200
  elif table == "symbols":
199
201
  query = f"""
200
202
  SELECT name, type, file_path, line_start, signature
@@ -204,7 +206,7 @@ Use sql_query for complex queries with joins, conditions, etc."""
204
206
  LIMIT ?
205
207
  """
206
208
  params_list = [pattern] * len(search_columns) + [max_results]
207
-
209
+
208
210
  else: # metadata
209
211
  query = f"""
210
212
  SELECT key, value, updated_at
@@ -213,17 +215,17 @@ Use sql_query for complex queries with joins, conditions, etc."""
213
215
  LIMIT ?
214
216
  """
215
217
  params_list = [pattern] * len(search_columns) + [max_results]
216
-
218
+
217
219
  # Execute search
218
220
  cursor.execute(query, params_list)
219
221
  results = cursor.fetchall()
220
-
222
+
221
223
  if not results:
222
224
  return f"No results found for pattern '{pattern}' in {table}"
223
-
225
+
224
226
  # Format results
225
227
  output = self._format_results(table, results, pattern, search_columns)
226
-
228
+
227
229
  return f"Found {len(results)} result(s) in {table}:\n\n{output}"
228
230
 
229
231
  except sqlite3.Error as e:
@@ -240,21 +242,23 @@ Use sql_query for complex queries with joins, conditions, etc."""
240
242
  """Get text columns for a table."""
241
243
  cursor.execute(f"PRAGMA table_info({table})")
242
244
  columns_info = cursor.fetchall()
243
-
245
+
244
246
  # Get TEXT columns
245
247
  text_columns = []
246
248
  for col in columns_info:
247
249
  col_name = col[1]
248
250
  col_type = col[2].upper()
249
- if 'TEXT' in col_type or 'CHAR' in col_type or col_type == '':
251
+ if "TEXT" in col_type or "CHAR" in col_type or col_type == "":
250
252
  text_columns.append(col_name)
251
-
253
+
252
254
  return text_columns
253
255
 
254
- def _format_results(self, table: str, results: list, pattern: str, search_columns: list[str]) -> str:
256
+ def _format_results(
257
+ self, table: str, results: list, pattern: str, search_columns: list[str]
258
+ ) -> str:
255
259
  """Format search results based on table type."""
256
260
  output = []
257
-
261
+
258
262
  if table == "files":
259
263
  output.append(f"Searched columns: {', '.join(search_columns)}\n")
260
264
  for row in results:
@@ -264,12 +268,12 @@ Use sql_query for complex queries with joins, conditions, etc."""
264
268
  output.append(f"Modified: {modified}")
265
269
  if snippet:
266
270
  # Highlight pattern in snippet
267
- snippet = snippet.replace('\n', ' ')
271
+ snippet = snippet.replace("\n", " ")
268
272
  if len(snippet) > 150:
269
273
  snippet = snippet[:150] + "..."
270
274
  output.append(f"Content: {snippet}")
271
275
  output.append("-" * 60)
272
-
276
+
273
277
  elif table == "symbols":
274
278
  output.append(f"Searched columns: {', '.join(search_columns)}\n")
275
279
  for row in results:
@@ -279,7 +283,7 @@ Use sql_query for complex queries with joins, conditions, etc."""
279
283
  if signature:
280
284
  output.append(f"Signature: {signature}")
281
285
  output.append("-" * 60)
282
-
286
+
283
287
  else: # metadata
284
288
  output.append(f"Searched columns: {', '.join(search_columns)}\n")
285
289
  for row in results:
@@ -288,9 +292,9 @@ Use sql_query for complex queries with joins, conditions, etc."""
288
292
  output.append(f"Value: {value}")
289
293
  output.append(f"Updated: {updated}")
290
294
  output.append("-" * 60)
291
-
295
+
292
296
  return "\n".join(output)
293
297
 
294
298
  def register(self, mcp_server) -> None:
295
299
  """Register this tool with the MCP server."""
296
- pass
300
+ pass
@@ -1,17 +1,16 @@
1
1
  """SQL statistics tool for database insights."""
2
2
 
3
3
  import sqlite3
4
- from typing import Annotated, Optional, TypedDict, Unpack, final, override
4
+ from typing import Unpack, Optional, Annotated, TypedDict, final, override
5
5
 
6
- from mcp.server.fastmcp import Context as MCPContext
7
6
  from pydantic import Field
7
+ from mcp.server.fastmcp import Context as MCPContext
8
8
 
9
9
  from hanzo_mcp.tools.common.base import BaseTool
10
10
  from hanzo_mcp.tools.common.context import create_tool_context
11
11
  from hanzo_mcp.tools.common.permissions import PermissionManager
12
12
  from hanzo_mcp.tools.database.database_manager import DatabaseManager
13
13
 
14
-
15
14
  ProjectPath = Annotated[
16
15
  Optional[str],
17
16
  Field(
@@ -40,7 +39,9 @@ class SqlStatsParams(TypedDict, total=False):
40
39
  class SqlStatsTool(BaseTool):
41
40
  """Tool for getting SQLite database statistics."""
42
41
 
43
- def __init__(self, permission_manager: PermissionManager, db_manager: DatabaseManager):
42
+ def __init__(
43
+ self, permission_manager: PermissionManager, db_manager: DatabaseManager
44
+ ):
44
45
  """Initialize the SQL stats tool.
45
46
 
46
47
  Args:
@@ -103,8 +104,9 @@ Examples:
103
104
  project_db = self.db_manager.get_project_db(project_path)
104
105
  else:
105
106
  import os
107
+
106
108
  project_db = self.db_manager.get_project_for_path(os.getcwd())
107
-
109
+
108
110
  if not project_db:
109
111
  return "Error: Could not find project database"
110
112
 
@@ -113,49 +115,53 @@ Examples:
113
115
  except Exception as e:
114
116
  return f"Error accessing project database: {str(e)}"
115
117
 
116
- await tool_ctx.info(f"Getting statistics for project: {project_db.project_path}")
118
+ await tool_ctx.info(
119
+ f"Getting statistics for project: {project_db.project_path}"
120
+ )
117
121
 
118
122
  # Collect statistics
119
123
  conn = None
120
124
  try:
121
125
  conn = project_db.get_sqlite_connection()
122
126
  cursor = conn.cursor()
123
-
127
+
124
128
  output = []
125
129
  output.append(f"=== SQLite Database Statistics ===")
126
130
  output.append(f"Project: {project_db.project_path}")
127
131
  output.append(f"Database: {project_db.sqlite_path}")
128
-
132
+
129
133
  # Get database size
130
134
  db_size = project_db.sqlite_path.stat().st_size
131
135
  output.append(f"Database Size: {self._format_size(db_size)}")
132
136
  output.append("")
133
-
137
+
134
138
  # Get table statistics
135
- cursor.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
139
+ cursor.execute(
140
+ "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
141
+ )
136
142
  tables = cursor.fetchall()
137
-
143
+
138
144
  output.append("=== Tables ===")
139
145
  total_rows = 0
140
-
146
+
141
147
  for (table_name,) in tables:
142
- if table_name.startswith('sqlite_'):
148
+ if table_name.startswith("sqlite_"):
143
149
  continue
144
-
150
+
145
151
  # Get row count
146
152
  cursor.execute(f"SELECT COUNT(*) FROM {table_name}")
147
153
  row_count = cursor.fetchone()[0]
148
154
  total_rows += row_count
149
-
155
+
150
156
  # Get table info
151
157
  cursor.execute(f"PRAGMA table_info({table_name})")
152
158
  columns = cursor.fetchall()
153
159
  col_count = len(columns)
154
-
160
+
155
161
  output.append(f"\n{table_name}:")
156
162
  output.append(f" Rows: {row_count:,}")
157
163
  output.append(f" Columns: {col_count}")
158
-
164
+
159
165
  if detailed and row_count > 0:
160
166
  # Show column details
161
167
  output.append(" Columns:")
@@ -164,44 +170,50 @@ Examples:
164
170
  col_type = col[2]
165
171
  is_pk = col[5]
166
172
  not_null = col[3]
167
-
173
+
168
174
  flags = []
169
175
  if is_pk:
170
176
  flags.append("PRIMARY KEY")
171
177
  if not_null:
172
178
  flags.append("NOT NULL")
173
-
179
+
174
180
  flag_str = f" ({', '.join(flags)})" if flags else ""
175
181
  output.append(f" - {col_name}: {col_type}{flag_str}")
176
-
182
+
177
183
  # Show sample data for specific tables
178
184
  if table_name == "files" and row_count > 0:
179
- cursor.execute(f"SELECT COUNT(DISTINCT SUBSTR(path, -3)) as ext_count FROM {table_name}")
185
+ cursor.execute(
186
+ f"SELECT COUNT(DISTINCT SUBSTR(path, -3)) as ext_count FROM {table_name}"
187
+ )
180
188
  ext_count = cursor.fetchone()[0]
181
189
  output.append(f" File types: ~{ext_count}")
182
-
190
+
183
191
  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")
192
+ cursor.execute(
193
+ f"SELECT type, COUNT(*) as count FROM {table_name} GROUP BY type ORDER BY count DESC LIMIT 5"
194
+ )
185
195
  symbol_types = cursor.fetchall()
186
196
  output.append(" Symbol types:")
187
197
  for sym_type, count in symbol_types:
188
198
  output.append(f" - {sym_type}: {count}")
189
-
199
+
190
200
  # Get indexes
191
201
  cursor.execute(f"PRAGMA index_list({table_name})")
192
202
  indexes = cursor.fetchall()
193
203
  if indexes:
194
204
  output.append(f" Indexes: {len(indexes)}")
195
-
205
+
196
206
  output.append(f"\nTotal Rows: {total_rows:,}")
197
-
207
+
198
208
  # Get index statistics
199
- cursor.execute("SELECT name FROM sqlite_master WHERE type='index' AND sql IS NOT NULL ORDER BY name")
209
+ cursor.execute(
210
+ "SELECT name FROM sqlite_master WHERE type='index' AND sql IS NOT NULL ORDER BY name"
211
+ )
200
212
  indexes = cursor.fetchall()
201
213
  if indexes:
202
214
  output.append(f"\n=== Indexes ===")
203
215
  output.append(f"Total Indexes: {len(indexes)}")
204
-
216
+
205
217
  if detailed:
206
218
  for (idx_name,) in indexes:
207
219
  cursor.execute(f"PRAGMA index_info({idx_name})")
@@ -209,26 +221,26 @@ Examples:
209
221
  if idx_info:
210
222
  cols = [info[2] for info in idx_info]
211
223
  output.append(f" {idx_name}: ({', '.join(cols)})")
212
-
224
+
213
225
  # Database properties
214
226
  if detailed:
215
227
  output.append("\n=== Database Properties ===")
216
-
228
+
217
229
  # Page size
218
230
  cursor.execute("PRAGMA page_size")
219
231
  page_size = cursor.fetchone()[0]
220
232
  output.append(f"Page Size: {page_size:,} bytes")
221
-
233
+
222
234
  # Page count
223
235
  cursor.execute("PRAGMA page_count")
224
236
  page_count = cursor.fetchone()[0]
225
237
  output.append(f"Page Count: {page_count:,}")
226
-
238
+
227
239
  # Cache size
228
240
  cursor.execute("PRAGMA cache_size")
229
241
  cache_size = cursor.fetchone()[0]
230
242
  output.append(f"Cache Size: {abs(cache_size):,} pages")
231
-
243
+
232
244
  return "\n".join(output)
233
245
 
234
246
  except sqlite3.Error as e:
@@ -243,7 +255,7 @@ Examples:
243
255
 
244
256
  def _format_size(self, size: int) -> str:
245
257
  """Format file size in human-readable format."""
246
- for unit in ['B', 'KB', 'MB', 'GB']:
258
+ for unit in ["B", "KB", "MB", "GB"]:
247
259
  if size < 1024.0:
248
260
  return f"{size:.1f} {unit}"
249
261
  size /= 1024.0
@@ -251,4 +263,4 @@ Examples:
251
263
 
252
264
  def register(self, mcp_server) -> None:
253
265
  """Register this tool with the MCP server."""
254
- pass
266
+ pass
@@ -1,19 +1,18 @@
1
1
  """Execute Neovim commands and macros."""
2
2
 
3
3
  import os
4
- import subprocess
5
4
  import shutil
6
5
  import tempfile
7
- from typing import Annotated, Optional, TypedDict, Unpack, final, override, List
6
+ import subprocess
7
+ from typing import List, Unpack, Optional, Annotated, TypedDict, final, override
8
8
 
9
- from mcp.server.fastmcp import Context as MCPContext
10
9
  from pydantic import Field
10
+ from mcp.server.fastmcp import Context as MCPContext
11
11
 
12
12
  from hanzo_mcp.tools.common.base import BaseTool
13
13
  from hanzo_mcp.tools.common.context import create_tool_context
14
14
  from hanzo_mcp.tools.common.permissions import PermissionManager
15
15
 
16
-
17
16
  Command = Annotated[
18
17
  Optional[str],
19
18
  Field(
@@ -159,7 +158,9 @@ Note: Requires Neovim to be installed.
159
158
  return "Error: Must provide either 'command', 'commands', or 'macro'"
160
159
 
161
160
  if sum(bool(x) for x in [command, commands, macro]) > 1:
162
- return "Error: Can only use one of 'command', 'commands', or 'macro' at a time"
161
+ return (
162
+ "Error: Can only use one of 'command', 'commands', or 'macro' at a time"
163
+ )
163
164
 
164
165
  # Check if Neovim is available
165
166
  nvim_cmd = shutil.which("nvim")
@@ -168,7 +169,7 @@ Note: Requires Neovim to be installed.
168
169
 
169
170
  # Prepare commands list
170
171
  nvim_commands = []
171
-
172
+
172
173
  if command:
173
174
  nvim_commands.append(command)
174
175
  elif commands:
@@ -178,65 +179,61 @@ Note: Requires Neovim to be installed.
178
179
  # Escape special characters
179
180
  escaped_macro = macro.replace('"', '\\"')
180
181
  nvim_commands.append(f':normal "{escaped_macro}"')
181
-
182
+
182
183
  # Add save command if requested
183
184
  if save_after:
184
185
  nvim_commands.append(":w")
185
-
186
+
186
187
  # Always quit at the end
187
188
  nvim_commands.append(":q")
188
189
 
189
190
  # Build Neovim command line
190
191
  cmd = [nvim_cmd, "-n", "-i", "NONE"] # No swap file, no shada file
191
-
192
+
192
193
  # Add commands
193
194
  for vim_cmd in nvim_commands:
194
195
  cmd.extend(["-c", vim_cmd])
195
-
196
+
196
197
  # Add file if specified
197
198
  if file_path:
198
199
  file_path = os.path.abspath(file_path)
199
-
200
+
200
201
  # Check permissions
201
202
  if not self.permission_manager.has_permission(file_path):
202
203
  return f"Error: No permission to access {file_path}"
203
-
204
+
204
205
  if not os.path.exists(file_path):
205
206
  return f"Error: File not found: {file_path}"
206
-
207
+
207
208
  cmd.append(file_path)
208
209
  else:
209
210
  # Create empty buffer
210
211
  cmd.append("-")
211
-
212
+
212
213
  await tool_ctx.info(f"Executing Neovim commands: {nvim_commands}")
213
-
214
+
214
215
  try:
215
216
  # Execute Neovim
216
217
  if return_output:
217
218
  # Capture output by redirecting messages
218
- output_file = tempfile.NamedTemporaryFile(mode='w+', delete=False)
219
+ output_file = tempfile.NamedTemporaryFile(mode="w+", delete=False)
219
220
  output_file.close()
220
-
221
+
221
222
  # Add command to redirect messages
222
223
  cmd.insert(3, "-c")
223
224
  cmd.insert(4, f":redir! > {output_file.name}")
224
-
225
+
225
226
  # Execute
226
- result = subprocess.run(
227
- cmd,
228
- capture_output=True,
229
- text=True
230
- )
231
-
227
+ result = subprocess.run(cmd, capture_output=True, text=True)
228
+
232
229
  # Read output
233
230
  output_content = ""
234
231
  try:
235
- with open(output_file.name, 'r') as f:
232
+ with open(output_file.name, "r") as f:
236
233
  output_content = f.read().strip()
237
234
  finally:
238
235
  os.unlink(output_file.name)
239
-
236
+
240
237
  if result.returncode == 0:
241
238
  response = "Commands executed successfully"
242
239
  if file_path:
@@ -254,7 +251,7 @@ Note: Requires Neovim to be installed.
254
251
  else:
255
252
  # Just execute without capturing output
256
253
  result = subprocess.run(cmd)
257
-
254
+
258
255
  if result.returncode == 0:
259
256
  response = "Commands executed successfully"
260
257
  if file_path:
@@ -262,7 +259,7 @@ Note: Requires Neovim to be installed.
262
259
  return response
263
260
  else:
264
261
  return f"Neovim exited with code {result.returncode}"
265
-
262
+
266
263
  except Exception as e:
267
264
  await tool_ctx.error(f"Failed to execute Neovim commands: {str(e)}")
268
265
  return f"Error executing Neovim commands: {str(e)}"
@@ -1,19 +1,17 @@
1
1
  """Open files in Neovim editor."""
2
2
 
3
3
  import os
4
- import subprocess
5
4
  import shutil
6
- from typing import Annotated, Optional, TypedDict, Unpack, final, override
7
- from pathlib import Path
5
+ import subprocess
6
+ from typing import Unpack, Optional, Annotated, TypedDict, final, override
8
7
 
9
- from mcp.server.fastmcp import Context as MCPContext
10
8
  from pydantic import Field
9
+ from mcp.server.fastmcp import Context as MCPContext
11
10
 
12
11
  from hanzo_mcp.tools.common.base import BaseTool
13
12
  from hanzo_mcp.tools.common.context import create_tool_context
14
13
  from hanzo_mcp.tools.common.permissions import PermissionManager
15
14
 
16
-
17
15
  FilePath = Annotated[
18
16
  str,
19
17
  Field(
@@ -170,7 +168,7 @@ Note: Requires Neovim to be installed and available in PATH.
170
168
  if os.path.exists(path):
171
169
  nvim_cmd = path
172
170
  break
173
-
171
+
174
172
  if not nvim_cmd:
175
173
  return """Error: Neovim (nvim) not found. Install it with:
176
174
 
@@ -187,18 +185,18 @@ Or visit: https://neovim.io/"""
187
185
 
188
186
  # Convert to absolute path
189
187
  file_path = os.path.abspath(file_path)
190
-
188
+
191
189
  # Check permissions
192
190
  if not self.permission_manager.has_permission(file_path):
193
191
  return f"Error: No permission to access {file_path}"
194
192
 
195
193
  # Build Neovim command
196
194
  cmd = [nvim_cmd]
197
-
195
+
198
196
  # Add read-only flag
199
197
  if read_only:
200
198
  cmd.append("-R")
201
-
199
+
202
200
  # Add split mode
203
201
  if split:
204
202
  if split == "vsplit":
@@ -209,10 +207,10 @@ Or visit: https://neovim.io/"""
209
207
  cmd.extend(["-c", "tabnew"])
210
208
  else:
211
209
  return f"Error: Invalid split mode '{split}'. Use 'vsplit', 'split', or 'tab'"
212
-
210
+
213
211
  # Add file path
214
212
  cmd.append(file_path)
215
-
213
+
216
214
  # Add line/column positioning
217
215
  if line_number:
218
216
  if column_number:
@@ -221,9 +219,9 @@ Or visit: https://neovim.io/"""
221
219
  else:
222
220
  # Go to specific line
223
221
  cmd.append(f"+{line_number}")
224
-
222
+
225
223
  await tool_ctx.info(f"Opening {file_path} in Neovim")
226
-
224
+
227
225
  try:
228
226
  # Determine how to run Neovim
229
227
  if in_terminal and not wait:
@@ -233,9 +231,9 @@ Or visit: https://neovim.io/"""
233
231
  if shutil.which("osascript"):
234
232
  # Build AppleScript to open in iTerm2 or Terminal
235
233
  nvim_cmd_str = " ".join(f'"{arg}"' for arg in cmd)
236
-
234
+
237
235
  # Try iTerm2 first
238
- applescript = f'''tell application "System Events"
236
+ applescript = f"""tell application "System Events"
239
237
  if exists application process "iTerm2" then
240
238
  tell application "iTerm"
241
239
  activate
@@ -252,35 +250,35 @@ Or visit: https://neovim.io/"""
252
250
  do script "{nvim_cmd_str}"
253
251
  end tell
254
252
  end if
255
- end tell'''
256
-
253
+ end tell"""
254
+
257
255
  subprocess.run(["osascript", "-e", applescript])
258
256
  return f"Opened {file_path} in Neovim (new terminal window)"
259
-
257
+
260
258
  elif shutil.which("gnome-terminal"):
261
259
  # Linux with GNOME
262
260
  subprocess.Popen(["gnome-terminal", "--"] + cmd)
263
261
  return f"Opened {file_path} in Neovim (new terminal window)"
264
-
262
+
265
263
  elif shutil.which("xterm"):
266
264
  # Fallback to xterm
267
265
  subprocess.Popen(["xterm", "-e"] + cmd)
268
266
  return f"Opened {file_path} in Neovim (new terminal window)"
269
-
267
+
270
268
  else:
271
269
  # Can't open in terminal, fall back to subprocess
272
270
  subprocess.Popen(cmd)
273
271
  return f"Opened {file_path} in Neovim (background process)"
274
-
272
+
275
273
  else:
276
274
  # Run and wait for completion
277
275
  result = subprocess.run(cmd)
278
-
276
+
279
277
  if result.returncode == 0:
280
278
  return f"Successfully edited {file_path} in Neovim"
281
279
  else:
282
280
  return f"Neovim exited with code {result.returncode}"
283
-
281
+
284
282
  except Exception as e:
285
283
  await tool_ctx.error(f"Failed to open Neovim: {str(e)}")
286
284
  return f"Error opening Neovim: {str(e)}"