hanzo-mcp 0.8.8__py3-none-any.whl → 0.9.0__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 (167) hide show
  1. hanzo_mcp/__init__.py +1 -3
  2. hanzo_mcp/analytics/posthog_analytics.py +4 -17
  3. hanzo_mcp/bridge.py +9 -25
  4. hanzo_mcp/cli.py +8 -17
  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 +2 -4
  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 +6 -7
  17. hanzo_mcp/tools/__init__.py +29 -32
  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 +23 -17
  21. hanzo_mcp/tools/agent/claude_desktop_auth.py +3 -9
  22. hanzo_mcp/tools/agent/cli_agent_base.py +7 -24
  23. hanzo_mcp/tools/agent/cli_tools.py +76 -75
  24. hanzo_mcp/tools/agent/code_auth.py +1 -3
  25. hanzo_mcp/tools/agent/code_auth_tool.py +2 -6
  26. hanzo_mcp/tools/agent/critic_tool.py +8 -24
  27. hanzo_mcp/tools/agent/iching_tool.py +12 -36
  28. hanzo_mcp/tools/agent/network_tool.py +7 -18
  29. hanzo_mcp/tools/agent/prompt.py +1 -5
  30. hanzo_mcp/tools/agent/review_tool.py +10 -25
  31. hanzo_mcp/tools/agent/swarm_alias.py +1 -3
  32. hanzo_mcp/tools/agent/unified_cli_tools.py +38 -38
  33. hanzo_mcp/tools/common/batch_tool.py +15 -45
  34. hanzo_mcp/tools/common/config_tool.py +9 -28
  35. hanzo_mcp/tools/common/context.py +1 -3
  36. hanzo_mcp/tools/common/critic_tool.py +1 -3
  37. hanzo_mcp/tools/common/decorators.py +2 -6
  38. hanzo_mcp/tools/common/enhanced_base.py +2 -6
  39. hanzo_mcp/tools/common/fastmcp_pagination.py +4 -12
  40. hanzo_mcp/tools/common/forgiving_edit.py +9 -28
  41. hanzo_mcp/tools/common/mode.py +1 -5
  42. hanzo_mcp/tools/common/paginated_base.py +3 -11
  43. hanzo_mcp/tools/common/paginated_response.py +10 -30
  44. hanzo_mcp/tools/common/pagination.py +3 -9
  45. hanzo_mcp/tools/common/path_utils.py +34 -0
  46. hanzo_mcp/tools/common/permissions.py +14 -13
  47. hanzo_mcp/tools/common/personality.py +983 -701
  48. hanzo_mcp/tools/common/plugin_loader.py +3 -15
  49. hanzo_mcp/tools/common/stats.py +7 -19
  50. hanzo_mcp/tools/common/thinking_tool.py +1 -3
  51. hanzo_mcp/tools/common/tool_disable.py +2 -6
  52. hanzo_mcp/tools/common/tool_list.py +2 -6
  53. hanzo_mcp/tools/common/validation.py +1 -3
  54. hanzo_mcp/tools/compiler/__init__.py +8 -0
  55. hanzo_mcp/tools/compiler/sandboxed_compiler.py +681 -0
  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/environment/__init__.py +8 -0
  73. hanzo_mcp/tools/environment/environment_detector.py +594 -0
  74. hanzo_mcp/tools/filesystem/__init__.py +28 -26
  75. hanzo_mcp/tools/filesystem/ast_multi_edit.py +14 -43
  76. hanzo_mcp/tools/filesystem/ast_tool.py +3 -0
  77. hanzo_mcp/tools/filesystem/base.py +20 -12
  78. hanzo_mcp/tools/filesystem/content_replace.py +7 -12
  79. hanzo_mcp/tools/filesystem/diff.py +2 -10
  80. hanzo_mcp/tools/filesystem/directory_tree.py +285 -51
  81. hanzo_mcp/tools/filesystem/edit.py +10 -18
  82. hanzo_mcp/tools/filesystem/find.py +312 -179
  83. hanzo_mcp/tools/filesystem/git_search.py +12 -24
  84. hanzo_mcp/tools/filesystem/multi_edit.py +10 -18
  85. hanzo_mcp/tools/filesystem/read.py +14 -30
  86. hanzo_mcp/tools/filesystem/rules_tool.py +9 -17
  87. hanzo_mcp/tools/filesystem/search.py +1160 -0
  88. hanzo_mcp/tools/filesystem/watch.py +2 -4
  89. hanzo_mcp/tools/filesystem/write.py +7 -10
  90. hanzo_mcp/tools/framework/__init__.py +8 -0
  91. hanzo_mcp/tools/framework/framework_modes.py +714 -0
  92. hanzo_mcp/tools/jupyter/base.py +6 -20
  93. hanzo_mcp/tools/jupyter/jupyter.py +4 -12
  94. hanzo_mcp/tools/llm/consensus_tool.py +8 -24
  95. hanzo_mcp/tools/llm/llm_manage.py +2 -6
  96. hanzo_mcp/tools/llm/llm_tool.py +17 -58
  97. hanzo_mcp/tools/llm/llm_unified.py +18 -59
  98. hanzo_mcp/tools/llm/provider_tools.py +1 -3
  99. hanzo_mcp/tools/lsp/lsp_tool.py +621 -481
  100. hanzo_mcp/tools/mcp/mcp_add.py +3 -5
  101. hanzo_mcp/tools/mcp/mcp_remove.py +1 -1
  102. hanzo_mcp/tools/mcp/mcp_stats.py +1 -3
  103. hanzo_mcp/tools/mcp/mcp_tool.py +9 -23
  104. hanzo_mcp/tools/memory/__init__.py +33 -40
  105. hanzo_mcp/tools/memory/conversation_memory.py +636 -0
  106. hanzo_mcp/tools/memory/knowledge_tools.py +7 -25
  107. hanzo_mcp/tools/memory/memory_tools.py +7 -19
  108. hanzo_mcp/tools/search/find_tool.py +12 -34
  109. hanzo_mcp/tools/search/unified_search.py +27 -81
  110. hanzo_mcp/tools/shell/__init__.py +16 -4
  111. hanzo_mcp/tools/shell/auto_background.py +2 -6
  112. hanzo_mcp/tools/shell/base.py +1 -5
  113. hanzo_mcp/tools/shell/base_process.py +5 -7
  114. hanzo_mcp/tools/shell/bash_session.py +7 -24
  115. hanzo_mcp/tools/shell/bash_session_executor.py +5 -15
  116. hanzo_mcp/tools/shell/bash_tool.py +3 -7
  117. hanzo_mcp/tools/shell/command_executor.py +26 -79
  118. hanzo_mcp/tools/shell/logs.py +4 -16
  119. hanzo_mcp/tools/shell/npx.py +2 -8
  120. hanzo_mcp/tools/shell/npx_tool.py +1 -3
  121. hanzo_mcp/tools/shell/pkill.py +4 -12
  122. hanzo_mcp/tools/shell/process_tool.py +2 -8
  123. hanzo_mcp/tools/shell/processes.py +5 -17
  124. hanzo_mcp/tools/shell/run_background.py +1 -3
  125. hanzo_mcp/tools/shell/run_command.py +1 -3
  126. hanzo_mcp/tools/shell/run_command_windows.py +1 -3
  127. hanzo_mcp/tools/shell/run_tool.py +56 -0
  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/vector/__init__.py +97 -50
  137. hanzo_mcp/tools/vector/ast_analyzer.py +6 -20
  138. hanzo_mcp/tools/vector/git_ingester.py +10 -30
  139. hanzo_mcp/tools/vector/index_tool.py +3 -9
  140. hanzo_mcp/tools/vector/infinity_store.py +11 -30
  141. hanzo_mcp/tools/vector/mock_infinity.py +159 -0
  142. hanzo_mcp/tools/vector/node_tool.py +538 -0
  143. hanzo_mcp/tools/vector/project_manager.py +4 -12
  144. hanzo_mcp/tools/vector/unified_vector.py +384 -0
  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.8.dist-info → hanzo_mcp-0.9.0.dist-info}/METADATA +2 -2
  149. hanzo_mcp-0.9.0.dist-info/RECORD +191 -0
  150. hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +0 -645
  151. hanzo_mcp/tools/agent/swarm_tool.py +0 -723
  152. hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +0 -577
  153. hanzo_mcp/tools/filesystem/batch_search.py +0 -900
  154. hanzo_mcp/tools/filesystem/directory_tree_paginated.py +0 -350
  155. hanzo_mcp/tools/filesystem/find_files.py +0 -369
  156. hanzo_mcp/tools/filesystem/grep.py +0 -467
  157. hanzo_mcp/tools/filesystem/search_tool.py +0 -767
  158. hanzo_mcp/tools/filesystem/symbols_tool.py +0 -515
  159. hanzo_mcp/tools/filesystem/tree.py +0 -270
  160. hanzo_mcp/tools/jupyter/notebook_edit.py +0 -317
  161. hanzo_mcp/tools/jupyter/notebook_read.py +0 -147
  162. hanzo_mcp/tools/todo/todo_read.py +0 -143
  163. hanzo_mcp/tools/todo/todo_write.py +0 -374
  164. hanzo_mcp-0.8.8.dist-info/RECORD +0 -192
  165. {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/WHEEL +0 -0
  166. {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/entry_points.txt +0 -0
  167. {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/top_level.txt +0 -0
@@ -52,9 +52,7 @@ def create_project_system_prompt(project_path: str):
52
52
  platform, _, os_version = get_os_info()
53
53
 
54
54
  # Get directory structure
55
- directory_structure = get_directory_structure(
56
- working_directory, max_depth=3, include_filtered=False
57
- )
55
+ directory_structure = get_directory_structure(working_directory, max_depth=3, include_filtered=False)
58
56
 
59
57
  # Get git information
60
58
  git_info = get_git_info(working_directory)
@@ -78,9 +76,7 @@ def create_project_system_prompt(project_path: str):
78
76
  return project_system_prompt
79
77
 
80
78
 
81
- def register_all_prompts(
82
- mcp_server: FastMCP, projects: list[str] | None = None
83
- ) -> None:
79
+ def register_all_prompts(mcp_server: FastMCP, projects: list[str] | None = None) -> None:
84
80
  @mcp_server.prompt(name="Compact current conversation")
85
81
  def compact() -> str:
86
82
  """
@@ -49,13 +49,9 @@ def format_todo_list_concise(todos: list[dict[str, Any]]) -> str:
49
49
  }.get(status, "[?]")
50
50
 
51
51
  # Create priority indicator
52
- priority_indicator = {"high": "🔴", "medium": "🟡", "low": "🟢"}.get(
53
- priority, "⚪"
54
- )
52
+ priority_indicator = {"high": "🔴", "medium": "🟡", "low": "🟢"}.get(priority, "⚪")
55
53
 
56
- formatted_lines.append(
57
- f"{status_indicator} {priority_indicator} {content} (id: {todo_id})"
58
- )
54
+ formatted_lines.append(f"{status_indicator} {priority_indicator} {content} (id: {todo_id})")
59
55
 
60
56
  return "\n".join(formatted_lines)
61
57
 
@@ -105,6 +101,4 @@ def get_project_todo_reminder(session_id: str | None = None) -> str:
105
101
 
106
102
  # Format the todo list and return the reminder with content
107
103
  formatted_todos = format_todo_list_concise(todos)
108
- return PROJECT_TODO_REMINDER.format(
109
- session_id=session_id, todo_list=formatted_todos
110
- )
104
+ return PROJECT_TODO_REMINDER.format(session_id=session_id, todo_list=formatted_todos)
@@ -596,9 +596,7 @@ def create_tool_category_prompt(category: str, tools: list[str]):
596
596
  "batch": BATCH_TOOL_EXAMPLES,
597
597
  }
598
598
 
599
- base_prompt = tool_descriptions.get(
600
- category, f"# {category.title()} Tools\n\nAvailable tools in this category:\n"
601
- )
599
+ base_prompt = tool_descriptions.get(category, f"# {category.title()} Tools\n\nAvailable tools in this category:\n")
602
600
 
603
601
  if category not in tool_descriptions:
604
602
  base_prompt += "\n".join(f"- **{tool}**: [Tool description]" for tool in tools)
@@ -51,9 +51,7 @@ def get_os_info() -> tuple[str, str, str]:
51
51
  return system, release, version
52
52
 
53
53
 
54
- def get_directory_structure(
55
- path: str, max_depth: int = 3, include_filtered: bool = False
56
- ) -> str:
54
+ def get_directory_structure(path: str, max_depth: int = 3, include_filtered: bool = False) -> str:
57
55
  """Get a directory structure similar to directory_tree tool.
58
56
 
59
57
  Args:
@@ -102,9 +100,7 @@ def get_directory_structure(
102
100
 
103
101
  try:
104
102
  # Sort entries: directories first, then files alphabetically
105
- entries = sorted(
106
- current_path.iterdir(), key=lambda x: (not x.is_dir(), x.name)
107
- )
103
+ entries = sorted(current_path.iterdir(), key=lambda x: (not x.is_dir(), x.name))
108
104
 
109
105
  for entry in entries:
110
106
  if entry.is_dir():
@@ -147,9 +143,7 @@ def get_directory_structure(
147
143
  # Format based on type
148
144
  if item["type"] == "directory":
149
145
  if "skipped" in item:
150
- lines.append(
151
- f"{indent}{item['name']}/ [skipped - {item['skipped']}]"
152
- )
146
+ lines.append(f"{indent}{item['name']}/ [skipped - {item['skipped']}]")
153
147
  else:
154
148
  lines.append(f"{indent}{item['name']}/")
155
149
  # Add children with increased indentation if present
@@ -228,9 +222,7 @@ def get_git_info(path: str) -> dict[str, str | None]:
228
222
  change_type = item.change_type
229
223
  status_lines.append(f"{change_type[0].upper()} {item.a_path}")
230
224
  if len(staged_files) > 25:
231
- status_lines.append(
232
- f"... and {len(staged_files) - 25} more staged files"
233
- )
225
+ status_lines.append(f"... and {len(staged_files) - 25} more staged files")
234
226
 
235
227
  # Check for unstaged changes
236
228
  unstaged_files = list(repo.index.diff(None))
@@ -238,9 +230,7 @@ def get_git_info(path: str) -> dict[str, str | None]:
238
230
  for item in unstaged_files[:25]: # Limit to first 25
239
231
  status_lines.append(f"M {item.a_path}")
240
232
  if len(unstaged_files) > 25:
241
- status_lines.append(
242
- f"... and {len(unstaged_files) - 25} more modified files"
243
- )
233
+ status_lines.append(f"... and {len(unstaged_files) - 25} more modified files")
244
234
 
245
235
  # Check for untracked files
246
236
  untracked_files = repo.untracked_files
@@ -248,13 +238,9 @@ def get_git_info(path: str) -> dict[str, str | None]:
248
238
  for file in untracked_files[:25]: # Limit to first 25
249
239
  status_lines.append(f"?? {file}")
250
240
  if len(untracked_files) > 25:
251
- status_lines.append(
252
- f"... and {len(untracked_files) - 25} more untracked files"
253
- )
241
+ status_lines.append(f"... and {len(untracked_files) - 25} more untracked files")
254
242
 
255
- git_status = (
256
- "\n".join(status_lines) if status_lines else "Working tree clean"
257
- )
243
+ git_status = "\n".join(status_lines) if status_lines else "Working tree clean"
258
244
 
259
245
  except Exception:
260
246
  git_status = "Unable to get git status"
hanzo_mcp/server.py CHANGED
@@ -9,15 +9,16 @@ import threading
9
9
  from typing import Literal, cast, final
10
10
 
11
11
  # Suppress litellm deprecation warnings about event loop
12
- warnings.filterwarnings(
13
- "ignore", message="There is no current event loop", category=DeprecationWarning
14
- )
12
+ warnings.filterwarnings("ignore", message="There is no current event loop", category=DeprecationWarning)
15
13
 
16
14
  try:
17
15
  from fastmcp import FastMCP
18
16
  except ImportError:
19
17
  # Fallback for older MCP versions
20
- from mcp.server import FastMCP
18
+ try:
19
+ from mcp.server import FastMCP
20
+ except ImportError:
21
+ from mcp import FastMCP
21
22
 
22
23
  # Import our enhanced server
23
24
  from hanzo_mcp.tools import register_all_tools
@@ -173,9 +174,7 @@ class HanzoMCPServer:
173
174
  signal.signal(signal.SIGINT, signal_handler)
174
175
 
175
176
  # Start background cleanup thread for periodic cleanup
176
- self._cleanup_thread = threading.Thread(
177
- target=self._background_cleanup, daemon=True
178
- )
177
+ self._cleanup_thread = threading.Thread(target=self._background_cleanup, daemon=True)
179
178
  self._cleanup_thread.start()
180
179
 
181
180
  self._cleanup_registered = True
@@ -35,6 +35,7 @@ try: # pragma: no cover
35
35
  from hanzo_mcp.tools.todo import register_todo_tools
36
36
  from hanzo_mcp.tools.agent import register_agent_tools
37
37
  from hanzo_mcp.tools.shell import register_shell_tools
38
+ from hanzo_mcp.tools.common import register_batch_tool, register_critic_tool, register_thinking_tool
38
39
  from hanzo_mcp.tools.editor import (
39
40
  NeovimEditTool,
40
41
  NeovimCommandTool,
@@ -64,19 +65,19 @@ try: # pragma: no cover
64
65
  register_memory_tools = None # type: ignore
65
66
  except Exception:
66
67
  # Minimal surface to allow submodule imports elsewhere
67
- # Define fallback functions for required imports
68
+ # Define stub functions for required imports
68
69
  def activate_mode_from_env():
69
- """Fallback: No mode activation when imports fail."""
70
- return None
70
+ pass
71
+
71
72
  class ModeLoader:
72
73
  @staticmethod
73
74
  def get_enabled_tools_from_mode(base_enabled_tools=None, force_mode=None):
74
- """Fallback: Return base tools when imports fail."""
75
75
  return base_enabled_tools or {}
76
+
76
77
  @staticmethod
77
78
  def apply_environment_from_mode():
78
- """Fallback: No environment changes when imports fail."""
79
- return None
79
+ pass
80
+
80
81
 
81
82
  # Try to import LSP tool
82
83
  try:
@@ -136,9 +137,7 @@ def register_all_tools(
136
137
 
137
138
  logger = logging.getLogger(__name__)
138
139
  if plugins:
139
- logger.info(
140
- f"Loaded {len(plugins)} user plugin tools: {', '.join(plugins.keys())}"
141
- )
140
+ logger.info(f"Loaded {len(plugins)} user plugin tools: {', '.join(plugins.keys())}")
142
141
  except Exception as e:
143
142
  import logging
144
143
 
@@ -151,9 +150,7 @@ def register_all_tools(
151
150
  # First check for mode activation from environment
152
151
  activate_mode_from_env()
153
152
 
154
- tool_config = ModeLoader.get_enabled_tools_from_mode(
155
- base_enabled_tools=enabled_tools, force_mode=force_mode
156
- )
153
+ tool_config = ModeLoader.get_enabled_tools_from_mode(base_enabled_tools=enabled_tools, force_mode=force_mode)
157
154
  # Apply mode environment variables
158
155
  ModeLoader.apply_environment_from_mode()
159
156
  else:
@@ -198,9 +195,7 @@ def register_all_tools(
198
195
  search_paths = [str(path) for path in permission_manager.allowed_paths]
199
196
  project_manager = ProjectVectorManager(
200
197
  global_db_path=vector_config.get("data_path"),
201
- embedding_model=vector_config.get(
202
- "embedding_model", "text-embedding-3-small"
203
- ),
198
+ embedding_model=vector_config.get("embedding_model", "text-embedding-3-small"),
204
199
  dimension=vector_config.get("dimension", 1536),
205
200
  )
206
201
  # Auto-detect projects from search paths
@@ -227,24 +222,31 @@ def register_all_tools(
227
222
  }
228
223
 
229
224
  if any(jupyter_enabled.values()):
230
- jupyter_tools = register_jupyter_tools(
231
- mcp_server, permission_manager, enabled_tools=jupyter_enabled
232
- )
225
+ jupyter_tools = register_jupyter_tools(mcp_server, permission_manager, enabled_tools=jupyter_enabled)
233
226
  for tool in jupyter_tools:
234
227
  all_tools[tool.name] = tool
235
228
 
236
- # Register shell tools if enabled
237
- if is_tool_enabled("run_command", True):
238
- shell_tools = register_shell_tools(mcp_server, permission_manager)
229
+ # Register shell tools with individual configuration
230
+ shell_enabled = {
231
+ "run": is_tool_enabled("run", True),
232
+ "shell": is_tool_enabled("shell", True),
233
+ "bash": is_tool_enabled("bash", True),
234
+ "zsh": is_tool_enabled("zsh", True),
235
+ "npx": is_tool_enabled("npx", True),
236
+ "uvx": is_tool_enabled("uvx", True),
237
+ "process": is_tool_enabled("process", True),
238
+ "open": is_tool_enabled("open", True),
239
+ # Legacy name support
240
+ "run_command": is_tool_enabled("run_command", True),
241
+ }
242
+
243
+ if any(shell_enabled.values()):
244
+ shell_tools = register_shell_tools(mcp_server, permission_manager, enabled_tools=shell_enabled)
239
245
  for tool in shell_tools:
240
246
  all_tools[tool.name] = tool
241
247
 
242
248
  # Register agent tools if enabled
243
- agent_enabled = (
244
- enable_agent_tool
245
- or is_tool_enabled("agent", False)
246
- or is_tool_enabled("dispatch_agent", False)
247
- )
249
+ agent_enabled = enable_agent_tool or is_tool_enabled("agent", False) or is_tool_enabled("dispatch_agent", False)
248
250
  swarm_enabled = is_tool_enabled("swarm", False)
249
251
 
250
252
  if agent_enabled or swarm_enabled:
@@ -260,12 +262,7 @@ def register_all_tools(
260
262
  )
261
263
  # Filter based on what's enabled
262
264
  for tool in agent_tools:
263
- if (
264
- tool.name == "agent"
265
- and agent_enabled
266
- or tool.name == "swarm"
267
- and swarm_enabled
268
- ):
265
+ if tool.name == "agent" and agent_enabled or tool.name == "swarm" and swarm_enabled:
269
266
  all_tools[tool.name] = tool
270
267
  elif tool.name in ["claude", "codex", "gemini", "grok", "code_auth"]:
271
268
  # CLI tools and auth are always included when agent tools are enabled
@@ -108,6 +108,7 @@ def register_agent_tools(
108
108
  if memory_backend is not None:
109
109
  p["memory_backend"] = memory_backend
110
110
  return await tool_self.call(ctx, **p)
111
+
111
112
  return tool_self
112
113
 
113
114
  # Create auth management tool
@@ -127,6 +128,6 @@ def register_agent_tools(
127
128
 
128
129
  # Register all CLI tools (includes claude, codex, gemini, grok, etc.)
129
130
  cli_tools = register_cli_tools(mcp_server, permission_manager)
130
-
131
+
131
132
  # Return list of registered tools
132
133
  return [agent_tool, network_tool, code_auth_tool] + cli_tools
@@ -118,23 +118,17 @@ class AgentParams(TypedDict, total=False):
118
118
  class RPCAgent:
119
119
  """Long-running RPC agent."""
120
120
 
121
- def __init__(
122
- self, agent_id: str, model: str, system_prompt: str, tools: List[BaseTool]
123
- ):
121
+ def __init__(self, agent_id: str, model: str, system_prompt: str, tools: List[BaseTool]):
124
122
  self.agent_id = agent_id
125
123
  self.model = model
126
124
  self.system_prompt = system_prompt
127
125
  self.tools = tools
128
- self.messages: List[ChatCompletionMessageParam] = [
129
- {"role": "system", "content": system_prompt}
130
- ]
126
+ self.messages: List[ChatCompletionMessageParam] = [{"role": "system", "content": system_prompt}]
131
127
  self.created_at = time.time()
132
128
  self.last_used = time.time()
133
129
  self.call_count = 0
134
130
 
135
- async def call_method(
136
- self, method: str, args: Dict[str, Any], tool_ctx: ToolContext
137
- ) -> str:
131
+ async def call_method(self, method: str, args: Dict[str, Any], tool_ctx: ToolContext) -> str:
138
132
  """Call a method on the RPC agent."""
139
133
  self.last_used = time.time()
140
134
  self.call_count += 1
@@ -189,15 +183,9 @@ class AgentTool(BaseTool):
189
183
 
190
184
  # Available tools
191
185
  self.available_tools: list[BaseTool] = []
192
- self.available_tools.extend(
193
- get_read_only_filesystem_tools(self.permission_manager)
194
- )
195
- self.available_tools.extend(
196
- get_read_only_jupyter_tools(self.permission_manager)
197
- )
198
- self.available_tools.append(
199
- BatchTool({t.name: t for t in self.available_tools})
200
- )
186
+ self.available_tools.extend(get_read_only_filesystem_tools(self.permission_manager))
187
+ self.available_tools.extend(get_read_only_jupyter_tools(self.permission_manager))
188
+ self.available_tools.append(BatchTool({t.name: t for t in self.available_tools}))
201
189
 
202
190
  @property
203
191
  @override
@@ -272,14 +260,10 @@ Modes:
272
260
 
273
261
  if len(prompt_list) == 1:
274
262
  await tool_ctx.info("Launching agent")
275
- result = await self._execute_agent(
276
- prompt_list[0], params.get("model"), tool_ctx
277
- )
263
+ result = await self._execute_agent(prompt_list[0], params.get("model"), tool_ctx)
278
264
  else:
279
265
  await tool_ctx.info(f"Launching {len(prompt_list)} agents in parallel")
280
- result = await self._execute_multiple_agents(
281
- prompt_list, params.get("model"), tool_ctx
282
- )
266
+ result = await self._execute_multiple_agents(prompt_list, params.get("model"), tool_ctx)
283
267
 
284
268
  execution_time = time.time() - start_time
285
269
 
@@ -389,17 +373,13 @@ Use 'agent --action call --agent-id {agent_id} --method <method> --args <args>'
389
373
  absolute_path_pattern = r"/(?:[^/\s]+/)*[^/\s]+"
390
374
  return bool(re.search(absolute_path_pattern, prompt))
391
375
 
392
- async def _execute_agent(
393
- self, prompt: str, model: Optional[str], tool_ctx: ToolContext
394
- ) -> str:
376
+ async def _execute_agent(self, prompt: str, model: Optional[str], tool_ctx: ToolContext) -> str:
395
377
  """Execute a single agent (simplified - would use full logic from agent_tool.py)."""
396
378
  # This would integrate the full agent execution logic from agent_tool.py
397
379
  # For now, return a placeholder
398
380
  return f"Executed agent with prompt: {prompt[:100]}..."
399
381
 
400
- async def _execute_multiple_agents(
401
- self, prompts: List[str], model: Optional[str], tool_ctx: ToolContext
402
- ) -> str:
382
+ async def _execute_multiple_agents(self, prompts: List[str], model: Optional[str], tool_ctx: ToolContext) -> str:
403
383
  """Execute multiple agents in parallel."""
404
384
  tasks = []
405
385
  for prompt in prompts:
@@ -49,8 +49,7 @@ except ImportError:
49
49
  """Stub State class when hanzo-agents is not available."""
50
50
 
51
51
  def __init__(self):
52
- """Initialize empty state."""
53
- self.data = {}
52
+ pass
54
53
 
55
54
  def to_dict(self):
56
55
  return {}
@@ -104,6 +103,23 @@ from hanzo_mcp.tools.agent.clarification_protocol import (
104
103
 
105
104
 
106
105
  class AgentToolParams(TypedDict, total=False):
106
+
107
+ def __init__(self, *args, mode: str = "single", **kwargs):
108
+ """Initialize agent tool with mode support.
109
+
110
+ Modes:
111
+ - single: Single agent execution (default)
112
+ - network: Network of agents (formerly swarm)
113
+ - dispatch: Dispatch to best agent (legacy compatibility)
114
+ """
115
+ super().__init__(*args, **kwargs)
116
+ self.mode = mode
117
+
118
+ # Handle legacy names
119
+ if self.name == "dispatch_agent":
120
+ self.mode = "dispatch"
121
+ elif self.name in ["network", "swarm"]:
122
+ self.mode = "network"
107
123
  """Parameters for the AgentTool."""
108
124
 
109
125
  prompts: str | list[str]
@@ -198,9 +214,7 @@ class MCPAgent(Agent):
198
214
  adapter = MCPToolAdapter(mcp_tool, ctx)
199
215
  self.register_tool(adapter)
200
216
 
201
- async def run(
202
- self, state: MCPAgentState, history: History, network: Network
203
- ) -> InferenceResult:
217
+ async def run(self, state: MCPAgentState, history: History, network: Network) -> InferenceResult:
204
218
  """Execute the agent."""
205
219
  # Get current prompt
206
220
  if state.current_prompt_index >= len(state.prompts):
@@ -325,12 +339,8 @@ Usage notes:
325
339
 
326
340
  # Set up available tools
327
341
  self.available_tools: list[BaseTool] = []
328
- self.available_tools.extend(
329
- get_read_only_filesystem_tools(self.permission_manager)
330
- )
331
- self.available_tools.extend(
332
- get_read_only_jupyter_tools(self.permission_manager)
333
- )
342
+ self.available_tools.extend(get_read_only_filesystem_tools(self.permission_manager))
343
+ self.available_tools.extend(get_read_only_jupyter_tools(self.permission_manager))
334
344
 
335
345
  # Add edit tools
336
346
  self.available_tools.append(Edit(self.permission_manager))
@@ -342,9 +352,7 @@ Usage notes:
342
352
  self.available_tools.append(ReviewTool())
343
353
  self.available_tools.append(IChingTool())
344
354
 
345
- self.available_tools.append(
346
- BatchTool({t.name: t for t in self.available_tools})
347
- )
355
+ self.available_tools.append(BatchTool({t.name: t for t in self.available_tools}))
348
356
 
349
357
  @override
350
358
  async def call(
@@ -400,9 +408,7 @@ Usage notes:
400
408
  times = (concurrency + len(prompt_list) - 1) // len(prompt_list)
401
409
  prompt_list = (prompt_list * times)[:concurrency]
402
410
 
403
- await tool_ctx.info(
404
- f"Launching {len(prompt_list)} agent(s) using hanzo-agents SDK"
405
- )
411
+ await tool_ctx.info(f"Launching {len(prompt_list)} agent(s) using hanzo-agents SDK")
406
412
 
407
413
  # Determine model and agent type
408
414
  model = params.get("model", self.model_override)
@@ -96,9 +96,7 @@ class ClaudeDesktopAuth:
96
96
  except Exception:
97
97
  return None
98
98
 
99
- async def login_interactive(
100
- self, account: Optional[str] = None, headless: bool = False
101
- ) -> Tuple[bool, str]:
99
+ async def login_interactive(self, account: Optional[str] = None, headless: bool = False) -> Tuple[bool, str]:
102
100
  """Login to Claude Desktop interactively.
103
101
 
104
102
  Args:
@@ -192,9 +190,7 @@ class ClaudeDesktopAuth:
192
190
  # For now, return a placeholder
193
191
  return False, "Headless login not yet implemented"
194
192
 
195
- async def _exchange_code_for_session(
196
- self, code: str, account: Optional[str]
197
- ) -> bool:
193
+ async def _exchange_code_for_session(self, code: str, account: Optional[str]) -> bool:
198
194
  """Exchange auth code for session token."""
199
195
  # This would make API calls to exchange the code
200
196
  # For now, create a mock session
@@ -357,9 +353,7 @@ class ClaudeDesktopAuth:
357
353
  # Generate agent-specific account
358
354
  return f"agent_{agent_id}@claude.local"
359
355
 
360
- async def ensure_agent_auth(
361
- self, agent_id: str, force_new: bool = False
362
- ) -> Tuple[bool, str]:
356
+ async def ensure_agent_auth(self, agent_id: str, force_new: bool = False) -> Tuple[bool, str]:
363
357
  """Ensure an agent is authenticated with its own account.
364
358
 
365
359
  Args:
@@ -97,9 +97,7 @@ class CLIAgentBase(BaseTool):
97
97
 
98
98
  # Check if installed
99
99
  if not self.is_installed():
100
- error_msg = (
101
- f"{self.provider_name} CLI ({self.command_name}) is not installed. "
102
- )
100
+ error_msg = f"{self.provider_name} CLI ({self.command_name}) is not installed. "
103
101
  error_msg += f"Please install it first: https://github.com/anthropics/{self.command_name}"
104
102
  await tool_ctx.error(error_msg)
105
103
  return f"Error: {error_msg}"
@@ -115,15 +113,11 @@ class CLIAgentBase(BaseTool):
115
113
  cli_args = self.get_cli_args(prompt, **kwargs)
116
114
 
117
115
  # Log command
118
- await tool_ctx.info(
119
- f"Executing {self.provider_name}: {self.command_name} {' '.join(cli_args[:3])}..."
120
- )
116
+ await tool_ctx.info(f"Executing {self.provider_name}: {self.command_name} {' '.join(cli_args[:3])}...")
121
117
 
122
118
  try:
123
119
  # Create temp file for prompt if needed
124
- with tempfile.NamedTemporaryFile(
125
- mode="w", suffix=".txt", delete=False
126
- ) as f:
120
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
127
121
  f.write(prompt)
128
122
  prompt_file = f.name
129
123
 
@@ -131,12 +125,7 @@ class CLIAgentBase(BaseTool):
131
125
  if "--prompt-file" in cli_args:
132
126
  # Replace placeholder with actual file
133
127
  cli_args = [
134
- (
135
- arg.replace("--prompt-file", prompt_file)
136
- if arg == "--prompt-file"
137
- else arg
138
- )
139
- for arg in cli_args
128
+ (arg.replace("--prompt-file", prompt_file) if arg == "--prompt-file" else arg) for arg in cli_args
140
129
  ]
141
130
 
142
131
  # Execute command
@@ -151,13 +140,9 @@ class CLIAgentBase(BaseTool):
151
140
 
152
141
  # Send prompt via stdin if not using file
153
142
  if "--prompt-file" not in cli_args:
154
- stdout, stderr = await asyncio.wait_for(
155
- process.communicate(input=prompt.encode()), timeout=timeout
156
- )
143
+ stdout, stderr = await asyncio.wait_for(process.communicate(input=prompt.encode()), timeout=timeout)
157
144
  else:
158
- stdout, stderr = await asyncio.wait_for(
159
- process.communicate(), timeout=timeout
160
- )
145
+ stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=timeout)
161
146
 
162
147
  # Clean up temp file
163
148
  try:
@@ -175,9 +160,7 @@ class CLIAgentBase(BaseTool):
175
160
  return result
176
161
 
177
162
  except asyncio.TimeoutError:
178
- await tool_ctx.error(
179
- f"{self.provider_name} timed out after {timeout} seconds"
180
- )
163
+ await tool_ctx.error(f"{self.provider_name} timed out after {timeout} seconds")
181
164
  return f"Error: Command timed out after {timeout} seconds"
182
165
  except Exception as e:
183
166
  await tool_ctx.error(f"{self.provider_name} error: {str(e)}")