hanzo-mcp 0.8.11__py3-none-any.whl → 0.8.13__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 (153) hide show
  1. hanzo_mcp/__init__.py +1 -3
  2. hanzo_mcp/analytics/posthog_analytics.py +3 -9
  3. hanzo_mcp/bridge.py +9 -25
  4. hanzo_mcp/cli.py +6 -15
  5. hanzo_mcp/cli_enhanced.py +5 -14
  6. hanzo_mcp/cli_plugin.py +3 -9
  7. hanzo_mcp/config/settings.py +6 -20
  8. hanzo_mcp/config/tool_config.py +1 -3
  9. hanzo_mcp/core/base_agent.py +88 -88
  10. hanzo_mcp/core/model_registry.py +238 -210
  11. hanzo_mcp/dev_server.py +5 -15
  12. hanzo_mcp/prompts/__init__.py +2 -6
  13. hanzo_mcp/prompts/project_todo_reminder.py +3 -9
  14. hanzo_mcp/prompts/tool_explorer.py +1 -3
  15. hanzo_mcp/prompts/utils.py +7 -21
  16. hanzo_mcp/server.py +2 -6
  17. hanzo_mcp/tools/__init__.py +10 -24
  18. hanzo_mcp/tools/agent/__init__.py +2 -1
  19. hanzo_mcp/tools/agent/agent.py +10 -30
  20. hanzo_mcp/tools/agent/agent_tool.py +5 -15
  21. hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +14 -41
  22. hanzo_mcp/tools/agent/claude_desktop_auth.py +3 -9
  23. hanzo_mcp/tools/agent/cli_agent_base.py +7 -24
  24. hanzo_mcp/tools/agent/cli_tools.py +75 -74
  25. hanzo_mcp/tools/agent/code_auth.py +1 -3
  26. hanzo_mcp/tools/agent/code_auth_tool.py +2 -6
  27. hanzo_mcp/tools/agent/critic_tool.py +8 -24
  28. hanzo_mcp/tools/agent/iching_tool.py +12 -36
  29. hanzo_mcp/tools/agent/network_tool.py +7 -18
  30. hanzo_mcp/tools/agent/prompt.py +1 -5
  31. hanzo_mcp/tools/agent/review_tool.py +10 -25
  32. hanzo_mcp/tools/agent/swarm_alias.py +1 -3
  33. hanzo_mcp/tools/agent/swarm_tool.py +9 -29
  34. hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +11 -39
  35. hanzo_mcp/tools/agent/unified_cli_tools.py +38 -38
  36. hanzo_mcp/tools/common/batch_tool.py +15 -45
  37. hanzo_mcp/tools/common/config_tool.py +9 -28
  38. hanzo_mcp/tools/common/context.py +1 -3
  39. hanzo_mcp/tools/common/critic_tool.py +1 -3
  40. hanzo_mcp/tools/common/decorators.py +2 -6
  41. hanzo_mcp/tools/common/enhanced_base.py +2 -6
  42. hanzo_mcp/tools/common/fastmcp_pagination.py +4 -12
  43. hanzo_mcp/tools/common/forgiving_edit.py +9 -28
  44. hanzo_mcp/tools/common/mode.py +1 -5
  45. hanzo_mcp/tools/common/paginated_base.py +3 -11
  46. hanzo_mcp/tools/common/paginated_response.py +10 -30
  47. hanzo_mcp/tools/common/pagination.py +3 -9
  48. hanzo_mcp/tools/common/permissions.py +3 -9
  49. hanzo_mcp/tools/common/personality.py +9 -34
  50. hanzo_mcp/tools/common/plugin_loader.py +3 -15
  51. hanzo_mcp/tools/common/stats.py +6 -18
  52. hanzo_mcp/tools/common/thinking_tool.py +1 -3
  53. hanzo_mcp/tools/common/tool_disable.py +2 -6
  54. hanzo_mcp/tools/common/tool_list.py +2 -6
  55. hanzo_mcp/tools/common/validation.py +1 -3
  56. hanzo_mcp/tools/config/config_tool.py +7 -13
  57. hanzo_mcp/tools/config/index_config.py +1 -3
  58. hanzo_mcp/tools/config/mode_tool.py +5 -15
  59. hanzo_mcp/tools/database/database_manager.py +3 -9
  60. hanzo_mcp/tools/database/graph.py +1 -3
  61. hanzo_mcp/tools/database/graph_add.py +3 -9
  62. hanzo_mcp/tools/database/graph_query.py +11 -34
  63. hanzo_mcp/tools/database/graph_remove.py +3 -9
  64. hanzo_mcp/tools/database/graph_search.py +6 -20
  65. hanzo_mcp/tools/database/graph_stats.py +11 -33
  66. hanzo_mcp/tools/database/sql.py +4 -12
  67. hanzo_mcp/tools/database/sql_query.py +6 -10
  68. hanzo_mcp/tools/database/sql_search.py +2 -6
  69. hanzo_mcp/tools/database/sql_stats.py +5 -15
  70. hanzo_mcp/tools/editor/neovim_command.py +1 -3
  71. hanzo_mcp/tools/editor/neovim_session.py +7 -13
  72. hanzo_mcp/tools/filesystem/__init__.py +2 -3
  73. hanzo_mcp/tools/filesystem/ast_multi_edit.py +14 -43
  74. hanzo_mcp/tools/filesystem/base.py +4 -12
  75. hanzo_mcp/tools/filesystem/batch_search.py +35 -115
  76. hanzo_mcp/tools/filesystem/content_replace.py +4 -12
  77. hanzo_mcp/tools/filesystem/diff.py +2 -10
  78. hanzo_mcp/tools/filesystem/directory_tree.py +9 -27
  79. hanzo_mcp/tools/filesystem/directory_tree_paginated.py +5 -15
  80. hanzo_mcp/tools/filesystem/edit.py +6 -18
  81. hanzo_mcp/tools/filesystem/find.py +3 -9
  82. hanzo_mcp/tools/filesystem/find_files.py +2 -6
  83. hanzo_mcp/tools/filesystem/git_search.py +9 -24
  84. hanzo_mcp/tools/filesystem/grep.py +9 -27
  85. hanzo_mcp/tools/filesystem/multi_edit.py +6 -18
  86. hanzo_mcp/tools/filesystem/read.py +8 -26
  87. hanzo_mcp/tools/filesystem/rules_tool.py +6 -17
  88. hanzo_mcp/tools/filesystem/search_tool.py +18 -62
  89. hanzo_mcp/tools/filesystem/symbols_tool.py +5 -15
  90. hanzo_mcp/tools/filesystem/tree.py +1 -3
  91. hanzo_mcp/tools/filesystem/watch.py +1 -3
  92. hanzo_mcp/tools/filesystem/write.py +1 -3
  93. hanzo_mcp/tools/jupyter/base.py +6 -20
  94. hanzo_mcp/tools/jupyter/jupyter.py +4 -12
  95. hanzo_mcp/tools/jupyter/notebook_edit.py +11 -35
  96. hanzo_mcp/tools/jupyter/notebook_read.py +2 -6
  97. hanzo_mcp/tools/llm/consensus_tool.py +8 -24
  98. hanzo_mcp/tools/llm/llm_manage.py +2 -6
  99. hanzo_mcp/tools/llm/llm_tool.py +17 -58
  100. hanzo_mcp/tools/llm/llm_unified.py +18 -59
  101. hanzo_mcp/tools/llm/provider_tools.py +1 -3
  102. hanzo_mcp/tools/lsp/lsp_tool.py +5 -17
  103. hanzo_mcp/tools/mcp/mcp_add.py +1 -3
  104. hanzo_mcp/tools/mcp/mcp_stats.py +1 -3
  105. hanzo_mcp/tools/mcp/mcp_tool.py +9 -23
  106. hanzo_mcp/tools/memory/__init__.py +10 -27
  107. hanzo_mcp/tools/memory/knowledge_tools.py +7 -25
  108. hanzo_mcp/tools/memory/memory_tools.py +6 -18
  109. hanzo_mcp/tools/search/find_tool.py +10 -32
  110. hanzo_mcp/tools/search/unified_search.py +24 -78
  111. hanzo_mcp/tools/shell/__init__.py +2 -2
  112. hanzo_mcp/tools/shell/auto_background.py +2 -6
  113. hanzo_mcp/tools/shell/base.py +1 -5
  114. hanzo_mcp/tools/shell/base_process.py +5 -7
  115. hanzo_mcp/tools/shell/bash_session.py +7 -24
  116. hanzo_mcp/tools/shell/bash_session_executor.py +5 -15
  117. hanzo_mcp/tools/shell/bash_tool.py +3 -7
  118. hanzo_mcp/tools/shell/command_executor.py +26 -79
  119. hanzo_mcp/tools/shell/logs.py +4 -16
  120. hanzo_mcp/tools/shell/npx.py +2 -8
  121. hanzo_mcp/tools/shell/npx_tool.py +1 -3
  122. hanzo_mcp/tools/shell/pkill.py +4 -12
  123. hanzo_mcp/tools/shell/process_tool.py +2 -8
  124. hanzo_mcp/tools/shell/processes.py +5 -17
  125. hanzo_mcp/tools/shell/run_background.py +1 -3
  126. hanzo_mcp/tools/shell/run_command.py +1 -3
  127. hanzo_mcp/tools/shell/run_command_windows.py +1 -3
  128. hanzo_mcp/tools/shell/session_manager.py +2 -6
  129. hanzo_mcp/tools/shell/session_storage.py +2 -6
  130. hanzo_mcp/tools/shell/streaming_command.py +7 -23
  131. hanzo_mcp/tools/shell/uvx.py +4 -14
  132. hanzo_mcp/tools/shell/uvx_background.py +2 -6
  133. hanzo_mcp/tools/shell/uvx_tool.py +1 -3
  134. hanzo_mcp/tools/shell/zsh_tool.py +12 -20
  135. hanzo_mcp/tools/todo/todo.py +1 -3
  136. hanzo_mcp/tools/todo/todo_read.py +3 -9
  137. hanzo_mcp/tools/todo/todo_write.py +6 -18
  138. hanzo_mcp/tools/vector/__init__.py +3 -9
  139. hanzo_mcp/tools/vector/ast_analyzer.py +6 -20
  140. hanzo_mcp/tools/vector/git_ingester.py +10 -30
  141. hanzo_mcp/tools/vector/index_tool.py +3 -9
  142. hanzo_mcp/tools/vector/infinity_store.py +7 -27
  143. hanzo_mcp/tools/vector/mock_infinity.py +1 -3
  144. hanzo_mcp/tools/vector/project_manager.py +4 -12
  145. hanzo_mcp/tools/vector/vector.py +2 -6
  146. hanzo_mcp/tools/vector/vector_index.py +8 -8
  147. hanzo_mcp/tools/vector/vector_search.py +7 -21
  148. {hanzo_mcp-0.8.11.dist-info → hanzo_mcp-0.8.13.dist-info}/METADATA +2 -2
  149. hanzo_mcp-0.8.13.dist-info/RECORD +193 -0
  150. hanzo_mcp-0.8.11.dist-info/RECORD +0 -193
  151. {hanzo_mcp-0.8.11.dist-info → hanzo_mcp-0.8.13.dist-info}/WHEEL +0 -0
  152. {hanzo_mcp-0.8.11.dist-info → hanzo_mcp-0.8.13.dist-info}/entry_points.txt +0 -0
  153. {hanzo_mcp-0.8.11.dist-info → hanzo_mcp-0.8.13.dist-info}/top_level.txt +0 -0
@@ -94,11 +94,7 @@ def register_default_modes():
94
94
  def get_mode_from_env() -> Optional[str]:
95
95
  """Get mode name from environment variables."""
96
96
  # Check for HANZO_MODE, PERSONALITY, or MODE env vars
97
- return (
98
- os.environ.get("HANZO_MODE")
99
- or os.environ.get("PERSONALITY")
100
- or os.environ.get("MODE")
101
- )
97
+ return os.environ.get("HANZO_MODE") or os.environ.get("PERSONALITY") or os.environ.get("MODE")
102
98
 
103
99
 
104
100
  def activate_mode_from_env():
@@ -68,11 +68,7 @@ class PaginatedBaseTool(BaseTool):
68
68
  if cursor:
69
69
  # For continuation, check if we have cached results
70
70
  cursor_data = CursorManager.parse_cursor(cursor)
71
- if (
72
- cursor_data
73
- and "tool" in cursor_data
74
- and cursor_data["tool"] != self.name
75
- ):
71
+ if cursor_data and "tool" in cursor_data and cursor_data["tool"] != self.name:
76
72
  return {"error": "Cursor is for a different tool", "code": -32602}
77
73
 
78
74
  # Execute the tool
@@ -90,15 +86,11 @@ class PaginatedBaseTool(BaseTool):
90
86
  if isinstance(paginated_result, dict) and "nextCursor" in paginated_result:
91
87
  # Enhance the cursor with tool information
92
88
  if "nextCursor" in paginated_result:
93
- cursor_data = CursorManager.parse_cursor(
94
- paginated_result["nextCursor"]
95
- )
89
+ cursor_data = CursorManager.parse_cursor(paginated_result["nextCursor"])
96
90
  if cursor_data:
97
91
  cursor_data["tool"] = self.name
98
92
  cursor_data["params"] = params # Store params for continuation
99
- paginated_result["nextCursor"] = CursorManager.create_cursor(
100
- cursor_data
101
- )
93
+ paginated_result["nextCursor"] = CursorManager.create_cursor(cursor_data)
102
94
 
103
95
  return paginated_result
104
96
  else:
@@ -35,31 +35,21 @@ class AutoPaginatedResponse:
35
35
  """
36
36
  # Handle different content types
37
37
  if isinstance(content, str):
38
- return AutoPaginatedResponse._handle_string_response(
39
- content, cursor, max_tokens
40
- )
38
+ return AutoPaginatedResponse._handle_string_response(content, cursor, max_tokens)
41
39
  elif isinstance(content, list):
42
- return AutoPaginatedResponse._handle_list_response(
43
- content, cursor, max_tokens
44
- )
40
+ return AutoPaginatedResponse._handle_list_response(content, cursor, max_tokens)
45
41
  elif isinstance(content, dict):
46
42
  # If dict already has pagination info, return as-is
47
43
  if "nextCursor" in content or "cursor" in content:
48
44
  return content
49
45
  # Otherwise treat as single item
50
- return AutoPaginatedResponse._handle_dict_response(
51
- content, cursor, max_tokens
52
- )
46
+ return AutoPaginatedResponse._handle_dict_response(content, cursor, max_tokens)
53
47
  else:
54
48
  # Convert to string for other types
55
- return AutoPaginatedResponse._handle_string_response(
56
- str(content), cursor, max_tokens
57
- )
49
+ return AutoPaginatedResponse._handle_string_response(str(content), cursor, max_tokens)
58
50
 
59
51
  @staticmethod
60
- def _handle_string_response(
61
- content: str, cursor: Optional[str], max_tokens: int
62
- ) -> Dict[str, Any]:
52
+ def _handle_string_response(content: str, cursor: Optional[str], max_tokens: int) -> Dict[str, Any]:
63
53
  """Handle pagination for string responses."""
64
54
  # Parse cursor to get offset
65
55
  offset = 0
@@ -124,9 +114,7 @@ class AutoPaginatedResponse:
124
114
  return response
125
115
 
126
116
  @staticmethod
127
- def _handle_list_response(
128
- items: List[Any], cursor: Optional[str], max_tokens: int
129
- ) -> Dict[str, Any]:
117
+ def _handle_list_response(items: List[Any], cursor: Optional[str], max_tokens: int) -> Dict[str, Any]:
130
118
  """Handle pagination for list responses."""
131
119
  # Parse cursor to get offset
132
120
  offset = 0
@@ -164,9 +152,7 @@ class AutoPaginatedResponse:
164
152
  truncated = item[:5000] + "... [truncated]"
165
153
  result_items.append(truncated)
166
154
  else:
167
- result_items.append(
168
- {"error": "Item too large", "index": item_index}
169
- )
155
+ result_items.append({"error": "Item too large", "index": item_index})
170
156
  item_index += 1
171
157
  break
172
158
 
@@ -199,9 +185,7 @@ class AutoPaginatedResponse:
199
185
  return response
200
186
 
201
187
  @staticmethod
202
- def _handle_dict_response(
203
- content: Dict[str, Any], cursor: Optional[str], max_tokens: int
204
- ) -> Dict[str, Any]:
188
+ def _handle_dict_response(content: Dict[str, Any], cursor: Optional[str], max_tokens: int) -> Dict[str, Any]:
205
189
  """Handle pagination for dict responses."""
206
190
  # For dicts, check if it's too large as-is
207
191
  content_str = json.dumps(content, indent=2)
@@ -280,17 +264,13 @@ def paginate_if_needed(
280
264
  Original response if small enough, otherwise paginated dict
281
265
  """
282
266
  # Quick check - if response is already paginated, return as-is
283
- if isinstance(response, dict) and (
284
- "nextCursor" in response or "pagination_info" in response
285
- ):
267
+ if isinstance(response, dict) and ("nextCursor" in response or "pagination_info" in response):
286
268
  return response
287
269
 
288
270
  # For small responses, don't paginate unless forced
289
271
  if not force_pagination:
290
272
  try:
291
- response_str = (
292
- json.dumps(response) if not isinstance(response, str) else response
293
- )
273
+ response_str = json.dumps(response) if not isinstance(response, str) else response
294
274
  if len(response_str) < 10000: # Quick heuristic
295
275
  return response
296
276
  except Exception:
@@ -154,9 +154,7 @@ class StreamPaginator(Generic[T]):
154
154
  """
155
155
  self.page_size = page_size
156
156
 
157
- def paginate_stream(
158
- self, stream_generator, cursor: Optional[str] = None
159
- ) -> PaginatedResponse[T]:
157
+ def paginate_stream(self, stream_generator, cursor: Optional[str] = None) -> PaginatedResponse[T]:
160
158
  """Paginate results from a stream/generator.
161
159
 
162
160
  Args:
@@ -185,18 +183,14 @@ class StreamPaginator(Generic[T]):
185
183
  items.append(item)
186
184
  if len(items) >= self.page_size:
187
185
  # We have a full page, create cursor for next page
188
- next_cursor = CursorManager.create_cursor(
189
- {"skip": skip_count + len(items)}
190
- )
186
+ next_cursor = CursorManager.create_cursor({"skip": skip_count + len(items)})
191
187
  return PaginatedResponse(items=items, next_cursor=next_cursor)
192
188
 
193
189
  # No more items
194
190
  return PaginatedResponse(items=items, next_cursor=None)
195
191
 
196
192
 
197
- def paginate_list(
198
- items: List[T], cursor: Optional[str] = None, page_size: int = 100
199
- ) -> PaginatedResponse[T]:
193
+ def paginate_list(items: List[T], cursor: Optional[str] = None, page_size: int = 100) -> PaginatedResponse[T]:
200
194
  """Convenience function to paginate a list.
201
195
 
202
196
  Args:
@@ -225,13 +225,9 @@ class PermissibleOperation:
225
225
  """
226
226
  self.permission_manager: PermissionManager = permission_manager
227
227
  self.operation: str = operation
228
- self.get_path_fn: Callable[[list[Any], dict[str, Any]], str] | None = (
229
- get_path_fn
230
- )
228
+ self.get_path_fn: Callable[[list[Any], dict[str, Any]], str] | None = get_path_fn
231
229
 
232
- def __call__(
233
- self, func: Callable[..., Awaitable[T]]
234
- ) -> Callable[..., Awaitable[T]]:
230
+ def __call__(self, func: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]:
235
231
  """Decorate the function.
236
232
 
237
233
  Args:
@@ -255,9 +251,7 @@ class PermissibleOperation:
255
251
 
256
252
  # Check permission
257
253
  if not self.permission_manager.is_path_allowed(path):
258
- raise PermissionError(
259
- f"Operation '{self.operation}' not allowed for path: {path}"
260
- )
254
+ raise PermissionError(f"Operation '{self.operation}' not allowed for path: {path}")
261
255
 
262
256
  # Call the function
263
257
  return await func(*args, **kwargs)
@@ -88,10 +88,7 @@ personalities = [
88
88
  programmer="Guido van Rossum",
89
89
  description="Python's BDFL - readability counts",
90
90
  philosophy="There should be one-- and preferably only one --obvious way to do it.",
91
- tools=ESSENTIAL_TOOLS
92
- + ["uvx", "jupyter", "multi_edit", "symbols", "rules"]
93
- + AI_TOOLS
94
- + SEARCH_TOOLS,
91
+ tools=ESSENTIAL_TOOLS + ["uvx", "jupyter", "multi_edit", "symbols", "rules"] + AI_TOOLS + SEARCH_TOOLS,
95
92
  environment={"PYTHONPATH": ".", "PYTEST_ARGS": "-xvs"},
96
93
  ),
97
94
  ToolPersonality(
@@ -107,10 +104,7 @@ personalities = [
107
104
  programmer="Brendan Eich",
108
105
  description="JavaScript creator - dynamic and flexible",
109
106
  philosophy="Always bet on JS.",
110
- tools=ESSENTIAL_TOOLS
111
- + ["npx", "watch", "symbols", "todo", "rules"]
112
- + BUILD_TOOLS
113
- + SEARCH_TOOLS,
107
+ tools=ESSENTIAL_TOOLS + ["npx", "watch", "symbols", "todo", "rules"] + BUILD_TOOLS + SEARCH_TOOLS,
114
108
  environment={"NODE_ENV": "development", "NPM_CONFIG_LOGLEVEL": "warn"},
115
109
  ),
116
110
  ToolPersonality(
@@ -126,10 +120,7 @@ personalities = [
126
120
  programmer="Bjarne Stroustrup",
127
121
  description="C++ creator - zero-overhead abstractions",
128
122
  philosophy="C++ is designed to allow you to express ideas.",
129
- tools=ESSENTIAL_TOOLS
130
- + ["symbols", "multi_edit", "content_replace"]
131
- + UNIX_TOOLS
132
- + BUILD_TOOLS,
123
+ tools=ESSENTIAL_TOOLS + ["symbols", "multi_edit", "content_replace"] + UNIX_TOOLS + BUILD_TOOLS,
133
124
  environment={"CXX": "g++", "CXXFLAGS": "-std=c++20 -Wall"},
134
125
  ),
135
126
  ToolPersonality(
@@ -145,10 +136,7 @@ personalities = [
145
136
  programmer="Anders Hejlsberg",
146
137
  description="TypeScript/C# creator - type safety matters",
147
138
  philosophy="TypeScript is JavaScript that scales.",
148
- tools=ESSENTIAL_TOOLS
149
- + ["npx", "symbols", "watch", "rules"]
150
- + BUILD_TOOLS
151
- + SEARCH_TOOLS,
139
+ tools=ESSENTIAL_TOOLS + ["npx", "symbols", "watch", "rules"] + BUILD_TOOLS + SEARCH_TOOLS,
152
140
  environment={"TYPESCRIPT_VERSION": "5.0"},
153
141
  ),
154
142
  ToolPersonality(
@@ -181,9 +169,7 @@ personalities = [
181
169
  programmer="Linus Torvalds",
182
170
  description="Linux & Git creator - pragmatic excellence",
183
171
  philosophy="Talk is cheap. Show me the code.",
184
- tools=ESSENTIAL_TOOLS
185
- + ["git_search", "diff", "content_replace", "critic"]
186
- + UNIX_TOOLS,
172
+ tools=ESSENTIAL_TOOLS + ["git_search", "diff", "content_replace", "critic"] + UNIX_TOOLS,
187
173
  environment={"KERNEL_VERSION": "6.0", "GIT_AUTHOR_NAME": "Linus Torvalds"},
188
174
  ),
189
175
  ToolPersonality(
@@ -191,10 +177,7 @@ personalities = [
191
177
  programmer="Rob Pike",
192
178
  description="Go creator - simplicity and concurrency",
193
179
  philosophy="A little copying is better than a little dependency.",
194
- tools=ESSENTIAL_TOOLS
195
- + ["symbols", "batch", "process"]
196
- + UNIX_TOOLS
197
- + BUILD_TOOLS,
180
+ tools=ESSENTIAL_TOOLS + ["symbols", "batch", "process"] + UNIX_TOOLS + BUILD_TOOLS,
198
181
  environment={"GOPATH": "~/go", "GO111MODULE": "on"},
199
182
  ),
200
183
  ToolPersonality(
@@ -242,9 +225,7 @@ personalities = [
242
225
  programmer="Graydon Hoare",
243
226
  description="Rust creator - memory safety without GC",
244
227
  philosophy="Memory safety without garbage collection, concurrency without data races.",
245
- tools=ESSENTIAL_TOOLS
246
- + ["symbols", "multi_edit", "critic", "todo"]
247
- + BUILD_TOOLS,
228
+ tools=ESSENTIAL_TOOLS + ["symbols", "multi_edit", "critic", "todo"] + BUILD_TOOLS,
248
229
  environment={"RUST_BACKTRACE": "1", "CARGO_HOME": "~/.cargo"},
249
230
  ),
250
231
  ToolPersonality(
@@ -871,10 +852,7 @@ personalities = [
871
852
  programmer="Data Scientist",
872
853
  description="Analyze all the things",
873
854
  philosophy="In God we trust. All others must bring data.",
874
- tools=ESSENTIAL_TOOLS
875
- + ["jupyter", "sql_query", "stats"]
876
- + VECTOR_TOOLS
877
- + AI_TOOLS,
855
+ tools=ESSENTIAL_TOOLS + ["jupyter", "sql_query", "stats"] + VECTOR_TOOLS + AI_TOOLS,
878
856
  environment={"JUPYTER_THEME": "dark"},
879
857
  ),
880
858
  ToolPersonality(
@@ -906,10 +884,7 @@ personalities = [
906
884
  programmer="Startup Founder",
907
885
  description="Move fast and fix things",
908
886
  philosophy="Done is better than perfect.",
909
- tools=ESSENTIAL_TOOLS
910
- + ["todo", "agent", "consensus"]
911
- + BUILD_TOOLS
912
- + DATABASE_TOOLS,
887
+ tools=ESSENTIAL_TOOLS + ["todo", "agent", "consensus"] + BUILD_TOOLS + DATABASE_TOOLS,
913
888
  environment={"STARTUP_MODE": "hustle"},
914
889
  ),
915
890
  ToolPersonality(
@@ -88,12 +88,7 @@ class PluginLoader:
88
88
 
89
89
  # Find tool classes
90
90
  for _name, obj in inspect.getmembers(module):
91
- if (
92
- inspect.isclass(obj)
93
- and issubclass(obj, BaseTool)
94
- and obj != BaseTool
95
- and hasattr(obj, "name")
96
- ):
91
+ if inspect.isclass(obj) and issubclass(obj, BaseTool) and obj != BaseTool and hasattr(obj, "name"):
97
92
  # Load metadata if available
98
93
  metadata = None
99
94
  metadata_file = file_path.with_suffix(".json")
@@ -134,15 +129,8 @@ class PluginLoader:
134
129
  else:
135
130
  # Search for tool classes
136
131
  for _name, obj in inspect.getmembers(module):
137
- if (
138
- inspect.isclass(obj)
139
- and issubclass(obj, BaseTool)
140
- and obj != BaseTool
141
- and hasattr(obj, "name")
142
- ):
143
- plugin = ToolPlugin(
144
- name=obj.name, tool_class=obj, source_path=package_dir
145
- )
132
+ if inspect.isclass(obj) and issubclass(obj, BaseTool) and obj != BaseTool and hasattr(obj, "name"):
133
+ plugin = ToolPlugin(name=obj.name, tool_class=obj, source_path=package_dir)
146
134
  self.plugins[obj.name] = plugin
147
135
  finally:
148
136
  # Remove from path
@@ -97,9 +97,7 @@ Example:
97
97
  memory_used_gb = memory.used / (1024**3)
98
98
  memory_total_gb = memory.total / (1024**3)
99
99
  memory_percent = memory.percent
100
- output.append(
101
- f"Memory: {memory_used_gb:.1f}/{memory_total_gb:.1f} GB ({memory_percent}%)"
102
- )
100
+ output.append(f"Memory: {memory_used_gb:.1f}/{memory_total_gb:.1f} GB ({memory_percent}%)")
103
101
  if memory_percent > 90:
104
102
  warnings.append(f"⚠️ HIGH MEMORY USAGE: {memory_percent}%")
105
103
 
@@ -109,14 +107,10 @@ Example:
109
107
  disk_total_gb = disk.total / (1024**3)
110
108
  disk_percent = disk.percent
111
109
  disk_free_gb = disk.free / (1024**3)
112
- output.append(
113
- f"Disk: {disk_used_gb:.1f}/{disk_total_gb:.1f} GB ({disk_percent}%)"
114
- )
110
+ output.append(f"Disk: {disk_used_gb:.1f}/{disk_total_gb:.1f} GB ({disk_percent}%)")
115
111
  output.append(f"Free Space: {disk_free_gb:.1f} GB")
116
112
  if disk_percent > 90:
117
- warnings.append(
118
- f"⚠️ LOW DISK SPACE: Only {disk_free_gb:.1f} GB free ({100 - disk_percent:.1f}% remaining)"
119
- )
113
+ warnings.append(f"⚠️ LOW DISK SPACE: Only {disk_free_gb:.1f} GB free ({100 - disk_percent:.1f}% remaining)")
120
114
 
121
115
  output.append("")
122
116
 
@@ -173,9 +167,7 @@ Example:
173
167
  size = db_file.stat().st_size
174
168
  total_db_size += size
175
169
 
176
- output.append(
177
- f"Total Database Size: {total_db_size / (1024**2):.1f} MB"
178
- )
170
+ output.append(f"Total Database Size: {total_db_size / (1024**2):.1f} MB")
179
171
  output.append(f"Active Projects: {len(self.db_manager.projects)}")
180
172
 
181
173
  # List largest databases
@@ -201,9 +193,7 @@ Example:
201
193
  output.append("=== MCP Servers ===")
202
194
  mcp_servers = McpAddTool.get_servers()
203
195
  if mcp_servers:
204
- running_mcp = sum(
205
- 1 for s in mcp_servers.values() if s.get("status") == "running"
206
- )
196
+ running_mcp = sum(1 for s in mcp_servers.values() if s.get("status") == "running")
207
197
  total_mcp_tools = sum(len(s.get("tools", [])) for s in mcp_servers.values())
208
198
 
209
199
  output.append(f"Total Servers: {len(mcp_servers)}")
@@ -225,9 +215,7 @@ Example:
225
215
  output.append(f"Log Files: {log_count} ({log_size / (1024**2):.1f} MB)")
226
216
 
227
217
  if log_size > 100 * 1024**2: # > 100MB
228
- warnings.append(
229
- f"⚠️ Large log directory: {log_size / (1024**2):.1f} MB"
230
- )
218
+ warnings.append(f"⚠️ Large log directory: {log_size / (1024**2):.1f} MB")
231
219
 
232
220
  # Config directory
233
221
  config_dir = Path.home() / ".hanzo" / "mcp"
@@ -115,9 +115,7 @@ Feature Implementation Planning
115
115
 
116
116
  # Validate required thought parameter
117
117
  if not thought:
118
- await tool_ctx.error(
119
- "Parameter 'thought' is required but was None or empty"
120
- )
118
+ await tool_ctx.error("Parameter 'thought' is required but was None or empty")
121
119
  return "Error: Parameter 'thought' is required but was None or empty"
122
120
 
123
121
  if thought.strip() == "":
@@ -125,9 +125,7 @@ Use 'tool_enable' to re-enable disabled tools.
125
125
  ]
126
126
 
127
127
  if not persist:
128
- output.append(
129
- "\nNote: This change is temporary and will be lost on restart."
130
- )
128
+ output.append("\nNote: This change is temporary and will be lost on restart.")
131
129
 
132
130
  # Warn about commonly used tools
133
131
  common_tools = {"grep", "read", "write", "bash", "edit"}
@@ -137,9 +135,7 @@ Use 'tool_enable' to re-enable disabled tools.
137
135
  )
138
136
 
139
137
  # Count disabled tools
140
- disabled_count = sum(
141
- 1 for enabled in ToolEnableTool._tool_states.values() if not enabled
142
- )
138
+ disabled_count = sum(1 for enabled in ToolEnableTool._tool_states.values() if not enabled)
143
139
  output.append(f"\nTotal disabled tools: {disabled_count}")
144
140
 
145
141
  return "\n".join(output)
@@ -205,9 +205,7 @@ Use 'tool_enable' and 'tool_disable' to change tool status.
205
205
 
206
206
  # Iterate through categories
207
207
  categories = (
208
- [category_filter]
209
- if category_filter and category_filter in self.TOOL_INFO
210
- else self.TOOL_INFO.keys()
208
+ [category_filter] if category_filter and category_filter in self.TOOL_INFO else self.TOOL_INFO.keys()
211
209
  )
212
210
 
213
211
  for category in categories:
@@ -242,9 +240,7 @@ Use 'tool_enable' and 'tool_disable' to change tool status.
242
240
  max_name_len = max(len(name) for name, _, _ in category_shown)
243
241
 
244
242
  for tool_name, description, status in category_shown:
245
- output.append(
246
- f"{status} {tool_name.ljust(max_name_len)} - {description}"
247
- )
243
+ output.append(f"{status} {tool_name.ljust(max_name_len)} - {description}")
248
244
 
249
245
  output.append("")
250
246
 
@@ -32,9 +32,7 @@ class ValidationResult:
32
32
  return not self.is_valid
33
33
 
34
34
 
35
- def validate_path_parameter(
36
- path: str | None, parameter_name: str = "path"
37
- ) -> ValidationResult:
35
+ def validate_path_parameter(path: str | None, parameter_name: str = "path") -> ValidationResult:
38
36
  """Validate a path parameter.
39
37
 
40
38
  Args:
@@ -131,9 +131,7 @@ config --action toggle index.scope --path ./project"""
131
131
  else:
132
132
  return f"Error: Unknown action '{action}'. Valid actions: get, set, list, toggle"
133
133
 
134
- async def _handle_get(
135
- self, key: Optional[str], scope: str, path: Optional[str], tool_ctx
136
- ) -> str:
134
+ async def _handle_get(self, key: Optional[str], scope: str, path: Optional[str], tool_ctx) -> str:
137
135
  """Get configuration value."""
138
136
  if not key:
139
137
  return "Error: key required for get action"
@@ -173,7 +171,9 @@ config --action toggle index.scope --path ./project"""
173
171
  project_path = Path(project_dir)
174
172
  project_path.mkdir(parents=True, exist_ok=True)
175
173
  cfg = project_path / ".hanzo-mcp.json"
176
- cfg.write_text(__import__("json").dumps(settings.__dict__ if hasattr(settings, "__dict__") else {}, indent=2))
174
+ cfg.write_text(
175
+ __import__("json").dumps(settings.__dict__ if hasattr(settings, "__dict__") else {}, indent=2)
176
+ )
177
177
  return cfg
178
178
  # Fallback to global handler
179
179
  return save_settings(settings, global_config=True)
@@ -196,9 +196,7 @@ config --action toggle index.scope --path ./project"""
196
196
  if key == "index.scope":
197
197
  try:
198
198
  new_scope = IndexScope(value)
199
- self.index_config.set_scope(
200
- new_scope, path if scope == "local" else None
201
- )
199
+ self.index_config.set_scope(new_scope, path if scope == "local" else None)
202
200
  return f"Set {key}={value} ({'project' if path else 'global'})"
203
201
  except ValueError:
204
202
  return f"Error: Invalid scope value '{value}'. Valid: project, global, auto"
@@ -281,18 +279,14 @@ config --action toggle index.scope --path ./project"""
281
279
 
282
280
  return "\n".join(output)
283
281
 
284
- async def _handle_toggle(
285
- self, key: Optional[str], scope: str, path: Optional[str], tool_ctx
286
- ) -> str:
282
+ async def _handle_toggle(self, key: Optional[str], scope: str, path: Optional[str], tool_ctx) -> str:
287
283
  """Toggle configuration value."""
288
284
  if not key:
289
285
  return "Error: key required for toggle action"
290
286
 
291
287
  # Handle index scope toggle
292
288
  if key == "index.scope":
293
- new_scope = self.index_config.toggle_scope(
294
- path if scope == "local" else None
295
- )
289
+ new_scope = self.index_config.toggle_scope(path if scope == "local" else None)
296
290
  return f"Toggled index.scope to {new_scope.value}"
297
291
 
298
292
  # Handle execution tool enable/disable: tools.<name>.enabled or enabled_tools.<name>
@@ -99,9 +99,7 @@ class IndexConfig:
99
99
  if project_root:
100
100
  if str(project_root) not in self._config["project_configs"]:
101
101
  self._config["project_configs"][str(project_root)] = {}
102
- self._config["project_configs"][str(project_root)][
103
- "scope"
104
- ] = scope.value
102
+ self._config["project_configs"][str(project_root)]["scope"] = scope.value
105
103
  else:
106
104
  # Set global default
107
105
  self._config["default_scope"] = scope.value
@@ -187,12 +187,8 @@ mode --action current"""
187
187
  for mode_name in mode_names:
188
188
  mode = next((m for m in modes if m.name == mode_name), None)
189
189
  if mode:
190
- marker = (
191
- " (active)" if active and active.name == mode.name else ""
192
- )
193
- output.append(
194
- f" {mode.name}{marker}: {mode.programmer} - {mode.description}"
195
- )
190
+ marker = " (active)" if active and active.name == mode.name else ""
191
+ output.append(f" {mode.name}{marker}: {mode.programmer} - {mode.description}")
196
192
 
197
193
  output.append("\nUse 'mode --action activate <name>' to activate a mode")
198
194
 
@@ -256,9 +252,7 @@ mode --action current"""
256
252
  for key, value in mode.environment.items():
257
253
  output.append(f" {key}={value}")
258
254
 
259
- output.append(
260
- "\nNote: Restart MCP session for changes to take full effect"
261
- )
255
+ output.append("\nNote: Restart MCP session for changes to take full effect")
262
256
 
263
257
  return "\n".join(output)
264
258
 
@@ -312,17 +306,13 @@ mode --action current"""
312
306
  tool_self = self
313
307
 
314
308
  @server.tool(name=self.name, description=self.description)
315
- async def mode_handler(
316
- ctx: MCPContext, action: str = "list", name: Optional[str] = None
317
- ) -> str:
309
+ async def mode_handler(ctx: MCPContext, action: str = "list", name: Optional[str] = None) -> str:
318
310
  """Handle mode tool calls."""
319
311
  return await tool_self.run(ctx, action=action, name=name)
320
312
 
321
313
  async def call(self, ctx: MCPContext, **params) -> str:
322
314
  """Call the tool with arguments."""
323
- return await self.run(
324
- ctx, action=params.get("action", "list"), name=params.get("name")
325
- )
315
+ return await self.run(ctx, action=params.get("action", "list"), name=params.get("name"))
326
316
 
327
317
 
328
318
  # Create tool instance
@@ -127,9 +127,7 @@ class ProjectDatabase:
127
127
  # Indexes for graph traversal
128
128
  conn.execute("CREATE INDEX IF NOT EXISTS idx_edges_source ON edges(source)")
129
129
  conn.execute("CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target)")
130
- conn.execute(
131
- "CREATE INDEX IF NOT EXISTS idx_edges_relationship ON edges(relationship)"
132
- )
130
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_edges_relationship ON edges(relationship)")
133
131
  conn.execute("CREATE INDEX IF NOT EXISTS idx_nodes_type ON nodes(type)")
134
132
 
135
133
  def _load_graph_from_disk(self):
@@ -138,15 +136,11 @@ class ProjectDatabase:
138
136
  try:
139
137
  # Copy nodes
140
138
  nodes = disk_conn.execute("SELECT * FROM nodes").fetchall()
141
- self.graph_conn.executemany(
142
- "INSERT OR REPLACE INTO nodes VALUES (?, ?, ?, ?)", nodes
143
- )
139
+ self.graph_conn.executemany("INSERT OR REPLACE INTO nodes VALUES (?, ?, ?, ?)", nodes)
144
140
 
145
141
  # Copy edges
146
142
  edges = disk_conn.execute("SELECT * FROM edges").fetchall()
147
- self.graph_conn.executemany(
148
- "INSERT OR REPLACE INTO edges VALUES (?, ?, ?, ?, ?, ?)", edges
149
- )
143
+ self.graph_conn.executemany("INSERT OR REPLACE INTO edges VALUES (?, ?, ?, ?, ?, ?)", edges)
150
144
 
151
145
  self.graph_conn.commit()
152
146
  finally:
@@ -120,9 +120,7 @@ class GraphParams(TypedDict, total=False):
120
120
  class GraphTool(BaseTool):
121
121
  """Unified graph database tool."""
122
122
 
123
- def __init__(
124
- self, permission_manager: PermissionManager, db_manager: DatabaseManager
125
- ):
123
+ def __init__(self, permission_manager: PermissionManager, db_manager: DatabaseManager):
126
124
  """Initialize the graph tool."""
127
125
  super().__init__(permission_manager)
128
126
  self.db_manager = db_manager
@@ -93,9 +93,7 @@ class GraphAddParams(TypedDict, total=False):
93
93
  class GraphAddTool(BaseTool):
94
94
  """Tool for adding nodes and edges to graph database."""
95
95
 
96
- def __init__(
97
- self, permission_manager: PermissionManager, db_manager: DatabaseManager
98
- ):
96
+ def __init__(self, permission_manager: PermissionManager, db_manager: DatabaseManager):
99
97
  """Initialize the graph add tool.
100
98
 
101
99
  Args:
@@ -225,15 +223,11 @@ Examples:
225
223
  if not relationship:
226
224
  return "Error: relationship is required when adding an edge"
227
225
 
228
- await tool_ctx.info(
229
- f"Adding edge: {source} --[{relationship}]--> {target}"
230
- )
226
+ await tool_ctx.info(f"Adding edge: {source} --[{relationship}]--> {target}")
231
227
 
232
228
  # Check if nodes exist
233
229
  cursor = graph_conn.cursor()
234
- cursor.execute(
235
- "SELECT id FROM nodes WHERE id IN (?, ?)", (source, target)
236
- )
230
+ cursor.execute("SELECT id FROM nodes WHERE id IN (?, ?)", (source, target))
237
231
  existing = [row[0] for row in cursor.fetchall()]
238
232
 
239
233
  if source not in existing: