hanzo-mcp 0.9.0__py3-none-any.whl → 0.9.2__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (135) hide show
  1. hanzo_mcp/__init__.py +1 -1
  2. hanzo_mcp/analytics/posthog_analytics.py +14 -1
  3. hanzo_mcp/cli.py +108 -4
  4. hanzo_mcp/server.py +11 -0
  5. hanzo_mcp/tools/__init__.py +3 -16
  6. hanzo_mcp/tools/agent/__init__.py +5 -0
  7. hanzo_mcp/tools/agent/agent.py +5 -0
  8. hanzo_mcp/tools/agent/agent_tool.py +3 -17
  9. hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +623 -0
  10. hanzo_mcp/tools/agent/clarification_tool.py +7 -1
  11. hanzo_mcp/tools/agent/claude_desktop_auth.py +16 -6
  12. hanzo_mcp/tools/agent/cli_agent_base.py +5 -0
  13. hanzo_mcp/tools/agent/cli_tools.py +26 -0
  14. hanzo_mcp/tools/agent/code_auth_tool.py +5 -0
  15. hanzo_mcp/tools/agent/critic_tool.py +7 -1
  16. hanzo_mcp/tools/agent/iching_tool.py +5 -0
  17. hanzo_mcp/tools/agent/network_tool.py +5 -0
  18. hanzo_mcp/tools/agent/review_tool.py +7 -1
  19. hanzo_mcp/tools/agent/swarm_alias.py +5 -0
  20. hanzo_mcp/tools/agent/swarm_tool.py +701 -0
  21. hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +554 -0
  22. hanzo_mcp/tools/agent/unified_cli_tools.py +5 -0
  23. hanzo_mcp/tools/common/auto_timeout.py +254 -0
  24. hanzo_mcp/tools/common/base.py +4 -0
  25. hanzo_mcp/tools/common/batch_tool.py +5 -0
  26. hanzo_mcp/tools/common/config_tool.py +5 -0
  27. hanzo_mcp/tools/common/critic_tool.py +5 -0
  28. hanzo_mcp/tools/common/paginated_base.py +4 -0
  29. hanzo_mcp/tools/common/permissions.py +38 -12
  30. hanzo_mcp/tools/common/personality.py +673 -980
  31. hanzo_mcp/tools/common/stats.py +5 -0
  32. hanzo_mcp/tools/common/thinking_tool.py +5 -0
  33. hanzo_mcp/tools/common/timeout_parser.py +103 -0
  34. hanzo_mcp/tools/common/tool_disable.py +5 -0
  35. hanzo_mcp/tools/common/tool_enable.py +5 -0
  36. hanzo_mcp/tools/common/tool_list.py +5 -0
  37. hanzo_mcp/tools/config/config_tool.py +5 -0
  38. hanzo_mcp/tools/config/mode_tool.py +5 -0
  39. hanzo_mcp/tools/database/graph.py +5 -0
  40. hanzo_mcp/tools/database/graph_add.py +5 -0
  41. hanzo_mcp/tools/database/graph_query.py +5 -0
  42. hanzo_mcp/tools/database/graph_remove.py +5 -0
  43. hanzo_mcp/tools/database/graph_search.py +5 -0
  44. hanzo_mcp/tools/database/graph_stats.py +5 -0
  45. hanzo_mcp/tools/database/sql.py +5 -0
  46. hanzo_mcp/tools/database/sql_query.py +2 -0
  47. hanzo_mcp/tools/database/sql_search.py +5 -0
  48. hanzo_mcp/tools/database/sql_stats.py +5 -0
  49. hanzo_mcp/tools/editor/neovim_command.py +5 -0
  50. hanzo_mcp/tools/editor/neovim_edit.py +7 -2
  51. hanzo_mcp/tools/editor/neovim_session.py +5 -0
  52. hanzo_mcp/tools/filesystem/__init__.py +23 -26
  53. hanzo_mcp/tools/filesystem/ast_tool.py +3 -4
  54. hanzo_mcp/tools/filesystem/base.py +2 -18
  55. hanzo_mcp/tools/filesystem/batch_search.py +825 -0
  56. hanzo_mcp/tools/filesystem/content_replace.py +5 -3
  57. hanzo_mcp/tools/filesystem/diff.py +5 -0
  58. hanzo_mcp/tools/filesystem/directory_tree.py +34 -281
  59. hanzo_mcp/tools/filesystem/directory_tree_paginated.py +345 -0
  60. hanzo_mcp/tools/filesystem/edit.py +6 -5
  61. hanzo_mcp/tools/filesystem/find.py +177 -311
  62. hanzo_mcp/tools/filesystem/find_files.py +370 -0
  63. hanzo_mcp/tools/filesystem/git_search.py +5 -3
  64. hanzo_mcp/tools/filesystem/grep.py +454 -0
  65. hanzo_mcp/tools/filesystem/multi_edit.py +6 -5
  66. hanzo_mcp/tools/filesystem/read.py +10 -9
  67. hanzo_mcp/tools/filesystem/rules_tool.py +6 -4
  68. hanzo_mcp/tools/filesystem/search_tool.py +728 -0
  69. hanzo_mcp/tools/filesystem/symbols_tool.py +510 -0
  70. hanzo_mcp/tools/filesystem/tree.py +273 -0
  71. hanzo_mcp/tools/filesystem/watch.py +6 -1
  72. hanzo_mcp/tools/filesystem/write.py +13 -7
  73. hanzo_mcp/tools/jupyter/jupyter.py +30 -2
  74. hanzo_mcp/tools/jupyter/notebook_edit.py +298 -0
  75. hanzo_mcp/tools/jupyter/notebook_read.py +148 -0
  76. hanzo_mcp/tools/llm/consensus_tool.py +8 -6
  77. hanzo_mcp/tools/llm/llm_manage.py +5 -0
  78. hanzo_mcp/tools/llm/llm_tool.py +2 -0
  79. hanzo_mcp/tools/llm/llm_unified.py +5 -0
  80. hanzo_mcp/tools/llm/provider_tools.py +5 -0
  81. hanzo_mcp/tools/lsp/lsp_tool.py +475 -622
  82. hanzo_mcp/tools/mcp/mcp_add.py +7 -2
  83. hanzo_mcp/tools/mcp/mcp_remove.py +15 -2
  84. hanzo_mcp/tools/mcp/mcp_stats.py +5 -0
  85. hanzo_mcp/tools/mcp/mcp_tool.py +5 -0
  86. hanzo_mcp/tools/memory/knowledge_tools.py +14 -0
  87. hanzo_mcp/tools/memory/memory_tools.py +17 -0
  88. hanzo_mcp/tools/search/find_tool.py +5 -3
  89. hanzo_mcp/tools/search/unified_search.py +3 -1
  90. hanzo_mcp/tools/shell/__init__.py +2 -14
  91. hanzo_mcp/tools/shell/base_process.py +4 -2
  92. hanzo_mcp/tools/shell/bash_tool.py +2 -0
  93. hanzo_mcp/tools/shell/command_executor.py +7 -7
  94. hanzo_mcp/tools/shell/logs.py +5 -0
  95. hanzo_mcp/tools/shell/npx.py +5 -0
  96. hanzo_mcp/tools/shell/npx_background.py +5 -0
  97. hanzo_mcp/tools/shell/npx_tool.py +5 -0
  98. hanzo_mcp/tools/shell/open.py +5 -0
  99. hanzo_mcp/tools/shell/pkill.py +5 -0
  100. hanzo_mcp/tools/shell/process_tool.py +5 -0
  101. hanzo_mcp/tools/shell/processes.py +5 -0
  102. hanzo_mcp/tools/shell/run_background.py +5 -0
  103. hanzo_mcp/tools/shell/run_command.py +2 -0
  104. hanzo_mcp/tools/shell/run_command_windows.py +5 -0
  105. hanzo_mcp/tools/shell/streaming_command.py +5 -0
  106. hanzo_mcp/tools/shell/uvx.py +5 -0
  107. hanzo_mcp/tools/shell/uvx_background.py +5 -0
  108. hanzo_mcp/tools/shell/uvx_tool.py +5 -0
  109. hanzo_mcp/tools/shell/zsh_tool.py +3 -0
  110. hanzo_mcp/tools/todo/todo.py +5 -0
  111. hanzo_mcp/tools/todo/todo_read.py +142 -0
  112. hanzo_mcp/tools/todo/todo_write.py +367 -0
  113. hanzo_mcp/tools/vector/__init__.py +42 -95
  114. hanzo_mcp/tools/vector/index_tool.py +5 -0
  115. hanzo_mcp/tools/vector/vector.py +5 -0
  116. hanzo_mcp/tools/vector/vector_index.py +5 -0
  117. hanzo_mcp/tools/vector/vector_search.py +5 -0
  118. {hanzo_mcp-0.9.0.dist-info → hanzo_mcp-0.9.2.dist-info}/METADATA +1 -1
  119. hanzo_mcp-0.9.2.dist-info/RECORD +195 -0
  120. hanzo_mcp/tools/common/path_utils.py +0 -34
  121. hanzo_mcp/tools/compiler/__init__.py +0 -8
  122. hanzo_mcp/tools/compiler/sandboxed_compiler.py +0 -681
  123. hanzo_mcp/tools/environment/__init__.py +0 -8
  124. hanzo_mcp/tools/environment/environment_detector.py +0 -594
  125. hanzo_mcp/tools/filesystem/search.py +0 -1160
  126. hanzo_mcp/tools/framework/__init__.py +0 -8
  127. hanzo_mcp/tools/framework/framework_modes.py +0 -714
  128. hanzo_mcp/tools/memory/conversation_memory.py +0 -636
  129. hanzo_mcp/tools/shell/run_tool.py +0 -56
  130. hanzo_mcp/tools/vector/node_tool.py +0 -538
  131. hanzo_mcp/tools/vector/unified_vector.py +0 -384
  132. hanzo_mcp-0.9.0.dist-info/RECORD +0 -191
  133. {hanzo_mcp-0.9.0.dist-info → hanzo_mcp-0.9.2.dist-info}/WHEEL +0 -0
  134. {hanzo_mcp-0.9.0.dist-info → hanzo_mcp-0.9.2.dist-info}/entry_points.txt +0 -0
  135. {hanzo_mcp-0.9.0.dist-info → hanzo_mcp-0.9.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,554 @@
1
+ """Swarm tool implementation for parallel and hierarchical agent execution.
2
+
3
+ This module implements the SwarmTool that enables both parallel execution of multiple
4
+ agent instances and hierarchical workflows with specialized roles.
5
+ """
6
+
7
+ import os
8
+ import asyncio
9
+ from typing import (
10
+ Any,
11
+ Dict,
12
+ List,
13
+ Unpack,
14
+ Optional,
15
+ TypedDict,
16
+ final,
17
+ override,
18
+ )
19
+
20
+ from mcp.server import FastMCP
21
+
22
+ from hanzo_mcp.tools.common.auto_timeout import auto_timeout
23
+ from mcp.server.fastmcp import Context as MCPContext
24
+
25
+ from hanzo_mcp.tools.common.base import BaseTool
26
+ from hanzo_mcp.tools.common.context import create_tool_context
27
+ from hanzo_mcp.tools.agent.agent_tool import AgentTool
28
+ from hanzo_mcp.tools.common.permissions import PermissionManager
29
+
30
+
31
+ class AgentNode(TypedDict):
32
+ """Node in the agent network.
33
+
34
+ Attributes:
35
+ id: Unique identifier for this agent
36
+ query: The specific query/task for this agent
37
+ model: Optional model override (e.g., 'claude-3-5-sonnet', 'gpt-4o')
38
+ role: Optional role description (e.g., 'architect', 'frontend', 'reviewer')
39
+ connections: List of agent IDs this agent connects to (sends results to)
40
+ receives_from: Optional list of agent IDs this agent receives input from
41
+ file_path: Optional specific file for the agent to work on
42
+ """
43
+
44
+ id: str
45
+ query: str
46
+ model: Optional[str]
47
+ role: Optional[str]
48
+ connections: Optional[List[str]]
49
+ receives_from: Optional[List[str]]
50
+ file_path: Optional[str]
51
+
52
+
53
+ class SwarmConfig(TypedDict):
54
+ """Configuration for an agent network.
55
+
56
+ Attributes:
57
+ agents: Dictionary of agent configurations keyed by ID
58
+ entry_point: ID of the first agent to execute (optional, defaults to finding roots)
59
+ topology: Optional topology type (tree, dag, pipeline, star, mesh)
60
+ """
61
+
62
+ agents: Dict[str, AgentNode]
63
+ entry_point: Optional[str]
64
+ topology: Optional[str]
65
+
66
+
67
+ class SwarmToolParams(TypedDict):
68
+ """Parameters for the SwarmTool.
69
+
70
+ Attributes:
71
+ config: Agent network configuration
72
+ query: Initial query to send to entry point agent(s)
73
+ context: Optional context shared by all agents
74
+ max_concurrent: Maximum number of concurrent agents (default: 10)
75
+ """
76
+
77
+ config: SwarmConfig
78
+ query: str
79
+ context: Optional[str]
80
+ max_concurrent: Optional[int]
81
+
82
+
83
+ @final
84
+ class SwarmTool(BaseTool):
85
+ """Tool for executing multiple agent tasks in parallel.
86
+
87
+ The SwarmTool enables efficient parallel processing of multiple files or tasks
88
+ by spawning independent agent instances for each task.
89
+ """
90
+
91
+ @property
92
+ @override
93
+ def name(self) -> str:
94
+ """Get the tool name."""
95
+ return "swarm"
96
+
97
+ @property
98
+ @override
99
+ def description(self) -> str:
100
+ """Get the tool description."""
101
+ return """Execute a network of AI agents with flexible connection topologies.
102
+
103
+ This tool enables sophisticated agent orchestration where agents can be connected
104
+ in various network patterns. Each agent can pass results to connected agents,
105
+ enabling complex workflows.
106
+
107
+ Features:
108
+ - Flexible agent networks (tree, DAG, pipeline, star, mesh)
109
+ - Each agent can use different models (Claude, GPT-4, Gemini, etc.)
110
+ - Agents automatically pass results to connected agents
111
+ - Parallel execution with dependency management
112
+ - Full editing capabilities for each agent
113
+
114
+ Common Topologies:
115
+
116
+ 1. Tree (Architect pattern):
117
+ architect → [frontend, backend, database] → reviewer
118
+
119
+ 2. Pipeline (Sequential processing):
120
+ analyzer → planner → implementer → tester → reviewer
121
+
122
+ 3. Star (Central coordinator):
123
+ coordinator ← → [agent1, agent2, agent3, agent4]
124
+
125
+ 4. DAG (Complex dependencies):
126
+ Multiple agents with custom connections
127
+
128
+ Usage Example:
129
+
130
+ swarm(
131
+ config={
132
+ "agents": {
133
+ "architect": {
134
+ "id": "architect",
135
+ "query": "Analyze codebase and create refactoring plan",
136
+ "model": "claude-3-5-sonnet",
137
+ "connections": ["frontend", "backend", "database"]
138
+ },
139
+ "frontend": {
140
+ "id": "frontend",
141
+ "query": "Refactor UI components based on architect's plan",
142
+ "role": "Frontend Developer",
143
+ "connections": ["reviewer"]
144
+ },
145
+ "backend": {
146
+ "id": "backend",
147
+ "query": "Refactor API endpoints based on architect's plan",
148
+ "role": "Backend Developer",
149
+ "connections": ["reviewer"]
150
+ },
151
+ "database": {
152
+ "id": "database",
153
+ "query": "Optimize database schema based on architect's plan",
154
+ "role": "Database Expert",
155
+ "connections": ["reviewer"]
156
+ },
157
+ "reviewer": {
158
+ "id": "reviewer",
159
+ "query": "Review all changes and ensure consistency",
160
+ "model": "gpt-4o",
161
+ "receives_from": ["frontend", "backend", "database"]
162
+ }
163
+ },
164
+ "entry_point": "architect"
165
+ },
166
+ query="Refactor the authentication system for better security and performance"
167
+ )
168
+
169
+ Models can be specified as:
170
+ - Full: 'anthropic/claude-3-5-sonnet-20241022'
171
+ - Short: 'claude-3-5-sonnet', 'gpt-4o', 'gemini-1.5-pro'
172
+ - CLI tools: 'claude_cli', 'codex_cli', 'gemini_cli', 'grok_cli'
173
+ """
174
+
175
+ def __init__(
176
+ self,
177
+ permission_manager: PermissionManager,
178
+ model: str | None = None,
179
+ api_key: str | None = None,
180
+ base_url: str | None = None,
181
+ max_tokens: int | None = None,
182
+ agent_max_iterations: int = 10,
183
+ agent_max_tool_uses: int = 30,
184
+ ):
185
+ """Initialize the swarm tool.
186
+
187
+ Args:
188
+ permission_manager: Permission manager for access control
189
+ model: Optional model name override (defaults to Claude Sonnet)
190
+ api_key: Optional API key for the model provider
191
+ base_url: Optional base URL for the model provider
192
+ max_tokens: Optional maximum tokens for model responses
193
+ agent_max_iterations: Max iterations per agent (default: 10)
194
+ agent_max_tool_uses: Max tool uses per agent (default: 30)
195
+ """
196
+ self.permission_manager = permission_manager
197
+ # Default to latest Claude Sonnet if no model specified
198
+ from hanzo_mcp.tools.agent.code_auth import get_latest_claude_model
199
+
200
+ self.model = model or f"anthropic/{get_latest_claude_model()}"
201
+ self.api_key = api_key or os.environ.get("ANTHROPIC_API_KEY") or os.environ.get("CLAUDE_API_KEY")
202
+ self.base_url = base_url
203
+ self.max_tokens = max_tokens
204
+ self.agent_max_iterations = agent_max_iterations
205
+ self.agent_max_tool_uses = agent_max_tool_uses
206
+
207
+ @override
208
+ @auto_timeout("swarm_tool_v1_deprecated")
209
+
210
+
211
+ async def call(
212
+ self,
213
+ ctx: MCPContext,
214
+ **params: Unpack[SwarmToolParams],
215
+ ) -> str:
216
+ """Execute the swarm tool.
217
+
218
+ Args:
219
+ ctx: MCP context
220
+ **params: Tool parameters
221
+
222
+ Returns:
223
+ Combined results from all agents
224
+ """
225
+ tool_ctx = create_tool_context(ctx)
226
+ await tool_ctx.set_tool_info(self.name)
227
+
228
+ # Extract parameters
229
+ agents = params.get("agents", [])
230
+ manager_query = params.get("manager_query")
231
+ reviewer_query = params.get("reviewer_query")
232
+ common_context = params.get("common_context", "")
233
+ max_concurrent = params.get("max_concurrent", 10)
234
+
235
+ if not agents:
236
+ await tool_ctx.error("No agents provided")
237
+ return "Error: At least one agent must be provided."
238
+
239
+ # Extract parameters
240
+ config = params.get("config", {})
241
+ initial_query = params.get("query", "")
242
+ context = params.get("context", "")
243
+
244
+ agents_config = config.get("agents", {})
245
+ entry_point = config.get("entry_point")
246
+
247
+ await tool_ctx.info(f"Starting swarm execution with {len(agents_config)} agents")
248
+
249
+ # Build agent network
250
+ agent_instances = {}
251
+ agent_results = {}
252
+ execution_queue = asyncio.Queue()
253
+ completed_agents = set()
254
+
255
+ # Create agent instances
256
+ for agent_id, agent_config in agents_config.items():
257
+ model = agent_config.get("model", self.model)
258
+
259
+ # Support CLI tools
260
+ cli_tools = {
261
+ "claude_cli": self._get_cli_tool("claude_cli"),
262
+ "codex_cli": self._get_cli_tool("codex_cli"),
263
+ "gemini_cli": self._get_cli_tool("gemini_cli"),
264
+ "grok_cli": self._get_cli_tool("grok_cli"),
265
+ }
266
+
267
+ if model in cli_tools:
268
+ agent = cli_tools[model]
269
+ else:
270
+ # Regular agent with model
271
+ agent = AgentTool(
272
+ permission_manager=self.permission_manager,
273
+ model=self._normalize_model(model),
274
+ api_key=self.api_key,
275
+ base_url=self.base_url,
276
+ max_tokens=self.max_tokens,
277
+ max_iterations=self.agent_max_iterations,
278
+ max_tool_uses=self.agent_max_tool_uses,
279
+ )
280
+
281
+ agent_instances[agent_id] = agent
282
+
283
+ # Find entry points (agents with no incoming connections)
284
+ if entry_point:
285
+ await execution_queue.put((entry_point, initial_query, {}))
286
+ else:
287
+ # Find root agents (no receives_from)
288
+ roots = []
289
+ for agent_id, agent_config in agents_config.items():
290
+ if not agent_config.get("receives_from"):
291
+ # Check if any other agent connects to this one
292
+ has_incoming = False
293
+ for other_config in agents_config.values():
294
+ if other_config.get("connections") and agent_id in other_config["connections"]:
295
+ has_incoming = True
296
+ break
297
+ if not has_incoming:
298
+ roots.append(agent_id)
299
+
300
+ if not roots:
301
+ await tool_ctx.error("No entry point found in agent network")
302
+ return "Error: Could not determine entry point for agent network"
303
+
304
+ for root in roots:
305
+ await execution_queue.put((root, initial_query, {}))
306
+
307
+ # Execute agents in network order
308
+ async def execute_agent(agent_id: str, query: str, inputs: Dict[str, str]) -> str:
309
+ """Execute a single agent in the network."""
310
+ async with semaphore:
311
+ try:
312
+ agent_config = agents_config[agent_id]
313
+ agent = agent_instances[agent_id]
314
+
315
+ await tool_ctx.info(f"Executing agent: {agent_id} ({agent_config.get('role', 'Agent')})")
316
+
317
+ # Build prompt with context and inputs
318
+ prompt_parts = []
319
+
320
+ # Add role context
321
+ if agent_config.get("role"):
322
+ prompt_parts.append(f"Your role: {agent_config['role']}")
323
+
324
+ # Add shared context
325
+ if context:
326
+ prompt_parts.append(f"Context:\n{context}")
327
+
328
+ # Add inputs from connected agents
329
+ if inputs:
330
+ prompt_parts.append("Input from previous agents:")
331
+ for input_agent, input_result in inputs.items():
332
+ prompt_parts.append(f"\n--- From {input_agent} ---\n{input_result}")
333
+
334
+ # Add file context if specified
335
+ if agent_config.get("file_path"):
336
+ prompt_parts.append(f"\nFile to work on: {agent_config['file_path']}")
337
+
338
+ # Add the main query
339
+ prompt_parts.append(f"\nTask: {agent_config['query']}")
340
+
341
+ # Combine query with initial query if this is entry point
342
+ if query and query != agent_config["query"]:
343
+ prompt_parts.append(f"\nMain objective: {query}")
344
+
345
+ full_prompt = "\n\n".join(prompt_parts)
346
+
347
+ # Execute the agent
348
+ result = await agent.call(ctx, prompts=full_prompt)
349
+
350
+ await tool_ctx.info(f"Agent {agent_id} completed")
351
+ return result
352
+
353
+ except Exception as e:
354
+ error_msg = f"Agent {agent_id} failed: {str(e)}"
355
+ await tool_ctx.error(error_msg)
356
+ return f"Error: {error_msg}"
357
+
358
+ # Process agent network
359
+ running_tasks = set()
360
+
361
+ while not execution_queue.empty() or running_tasks:
362
+ # Start new tasks up to concurrency limit
363
+ while not execution_queue.empty() and len(running_tasks) < max_concurrent:
364
+ agent_id, query, inputs = await execution_queue.get()
365
+
366
+ if agent_id not in completed_agents:
367
+ # Check if all dependencies are met
368
+ agent_config = agents_config[agent_id]
369
+ receives_from = agent_config.get("receives_from", [])
370
+
371
+ # Collect inputs from dependencies
372
+ ready = True
373
+ for dep in receives_from:
374
+ if dep not in agent_results:
375
+ ready = False
376
+ # Re-queue for later
377
+ await execution_queue.put((agent_id, query, inputs))
378
+ break
379
+ else:
380
+ inputs[dep] = agent_results[dep]
381
+
382
+ if ready:
383
+ # Execute agent
384
+ task = asyncio.create_task(execute_agent(agent_id, query, inputs))
385
+ running_tasks.add(task)
386
+
387
+ async def handle_completion(task, agent_id=agent_id):
388
+ result = await task
389
+ agent_results[agent_id] = result
390
+ completed_agents.add(agent_id)
391
+ running_tasks.discard(task)
392
+
393
+ # Queue connected agents
394
+ agent_config = agents_config[agent_id]
395
+ connections = agent_config.get("connections", [])
396
+ for next_agent in connections:
397
+ if next_agent in agents_config:
398
+ await execution_queue.put((next_agent, "", {agent_id: result}))
399
+
400
+ asyncio.create_task(handle_completion(task))
401
+
402
+ # Wait a bit if we're at capacity
403
+ if running_tasks:
404
+ await asyncio.sleep(0.1)
405
+
406
+ # Wait for all tasks to complete
407
+ if running_tasks:
408
+ await asyncio.gather(*running_tasks, return_exceptions=True)
409
+
410
+ # Format results
411
+ return self._format_network_results(agents_config, agent_results, entry_point)
412
+
413
+ def _normalize_model(self, model: str) -> str:
414
+ """Normalize model names to full format."""
415
+ model_map = {
416
+ "claude-3-5-sonnet": "anthropic/claude-3-5-sonnet-20241022",
417
+ "claude-3-opus": "anthropic/claude-3-opus-20240229",
418
+ "gpt-4o": "openai/gpt-4o",
419
+ "gpt-4": "openai/gpt-4",
420
+ "gemini-1.5-pro": "google/gemini-1.5-pro",
421
+ "gemini-1.5-flash": "google/gemini-1.5-flash",
422
+ }
423
+ return model_map.get(model, model)
424
+
425
+ def _get_cli_tool(self, tool_name: str):
426
+ """Get CLI tool instance."""
427
+ # Import here to avoid circular imports
428
+ if tool_name == "claude_cli":
429
+ from hanzo_mcp.tools.agent.claude_cli_tool import ClaudeCLITool
430
+
431
+ return ClaudeCLITool(self.permission_manager)
432
+ elif tool_name == "codex_cli":
433
+ from hanzo_mcp.tools.agent.codex_cli_tool import CodexCLITool
434
+
435
+ return CodexCLITool(self.permission_manager)
436
+ elif tool_name == "gemini_cli":
437
+ from hanzo_mcp.tools.agent.gemini_cli_tool import GeminiCLITool
438
+
439
+ return GeminiCLITool(self.permission_manager)
440
+ elif tool_name == "grok_cli":
441
+ from hanzo_mcp.tools.agent.grok_cli_tool import GrokCLITool
442
+
443
+ return GrokCLITool(self.permission_manager)
444
+ return None
445
+
446
+ def _format_network_results(
447
+ self,
448
+ agents_config: Dict[str, Any],
449
+ results: Dict[str, str],
450
+ entry_point: Optional[str],
451
+ ) -> str:
452
+ """Format results from agent network execution."""
453
+ output = ["Agent Network Execution Results"]
454
+ output.append("=" * 80)
455
+ output.append(f"Total agents: {len(agents_config)}")
456
+ output.append(f"Completed: {len(results)}")
457
+ output.append(f"Failed: {len([r for r in results.values() if r.startswith('Error:')])}")
458
+
459
+ if entry_point:
460
+ output.append(f"Entry point: {entry_point}")
461
+
462
+ output.append("\nExecution Flow:")
463
+ output.append("-" * 40)
464
+
465
+ # Show results in execution order
466
+ def format_agent_tree(agent_id: str, level: int = 0) -> List[str]:
467
+ lines = []
468
+ indent = " " * level
469
+
470
+ if agent_id in agents_config:
471
+ config = agents_config[agent_id]
472
+ role = config.get("role", "Agent")
473
+ model = config.get("model", "default")
474
+
475
+ status = "✅" if agent_id in results and not results[agent_id].startswith("Error:") else "❌"
476
+ lines.append(f"{indent}{status} {agent_id} ({role}) [{model}]")
477
+
478
+ # Show connections
479
+ connections = config.get("connections", [])
480
+ for conn in connections:
481
+ if conn in agents_config:
482
+ lines.extend(format_agent_tree(conn, level + 1))
483
+
484
+ return lines
485
+
486
+ # Start from entry point or roots
487
+ if entry_point:
488
+ output.extend(format_agent_tree(entry_point))
489
+ else:
490
+ # Find roots
491
+ roots = []
492
+ for agent_id in agents_config:
493
+ has_incoming = False
494
+ for config in agents_config.values():
495
+ if config.get("connections") and agent_id in config["connections"]:
496
+ has_incoming = True
497
+ break
498
+ if not has_incoming:
499
+ roots.append(agent_id)
500
+
501
+ for root in roots:
502
+ output.extend(format_agent_tree(root))
503
+
504
+ # Detailed results
505
+ output.append("\n\nDetailed Results:")
506
+ output.append("=" * 80)
507
+
508
+ for agent_id, result in results.items():
509
+ config = agents_config.get(agent_id, {})
510
+ role = config.get("role", "Agent")
511
+
512
+ output.append(f"\n### {agent_id} ({role})")
513
+ output.append("-" * 40)
514
+
515
+ if result.startswith("Error:"):
516
+ output.append(result)
517
+ else:
518
+ # Show first part of result
519
+ lines = result.split("\n")
520
+ preview_lines = lines[:10]
521
+ output.extend(preview_lines)
522
+
523
+ if len(lines) > 10:
524
+ output.append(f"... ({len(lines) - 10} more lines)")
525
+
526
+ return "\n".join(output)
527
+
528
+ @override
529
+ def register(self, mcp_server: FastMCP) -> None:
530
+ """Register this swarm tool with the MCP server."""
531
+ tool_self = self
532
+
533
+ @mcp_server.tool(name=self.name, description=self.description)
534
+ async def swarm(
535
+ ctx: MCPContext,
536
+ config: dict[str, Any],
537
+ query: str,
538
+ context: Optional[str] = None,
539
+ max_concurrent: int = 10,
540
+ ) -> str:
541
+ # Convert to typed format
542
+ typed_config = SwarmConfig(
543
+ agents=config.get("agents", {}),
544
+ entry_point=config.get("entry_point"),
545
+ topology=config.get("topology"),
546
+ )
547
+
548
+ return await tool_self.call(
549
+ ctx,
550
+ config=typed_config,
551
+ query=query,
552
+ context=context,
553
+ max_concurrent=max_concurrent,
554
+ )
@@ -11,6 +11,8 @@ from typing import Any, Dict, List, Optional
11
11
  from pathlib import Path
12
12
 
13
13
  from mcp.server import FastMCP
14
+
15
+ from hanzo_mcp.tools.common.auto_timeout import auto_timeout
14
16
  from mcp.server.fastmcp import Context
15
17
 
16
18
  from ..common.base import BaseTool
@@ -142,6 +144,9 @@ class UnifiedCLITool(BaseTool, CLIAgent):
142
144
  command.append(prompt)
143
145
  return command
144
146
 
147
+ @auto_timeout("unified_cli_tools")
148
+
149
+
145
150
  async def call(self, ctx: Context[Any, Any, Any], **params: Any) -> str:
146
151
  """Execute the CLI tool via MCP interface.
147
152