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,701 @@
1
+ """Swarm tool implementation using hanzo-agents SDK.
2
+
3
+ This module implements the SwarmTool that leverages the hanzo-agents SDK
4
+ for sophisticated multi-agent orchestration with flexible network topologies.
5
+ """
6
+
7
+ import os
8
+ from typing import (
9
+ Any,
10
+ Dict,
11
+ List,
12
+ Unpack,
13
+ Optional,
14
+ TypedDict,
15
+ final,
16
+ override,
17
+ )
18
+
19
+ from mcp.server import FastMCP
20
+ from mcp.server.fastmcp import Context as MCPContext
21
+
22
+ from hanzo_mcp.tools.common.auto_timeout import auto_timeout
23
+
24
+ # Import hanzo-agents SDK with fallback
25
+ try:
26
+ from hanzo_agents import (
27
+ Tool,
28
+ Agent,
29
+ State,
30
+ Router,
31
+ History,
32
+ Network,
33
+ ToolCall,
34
+ ModelRegistry,
35
+ InferenceResult,
36
+ )
37
+
38
+ HANZO_AGENTS_AVAILABLE = True
39
+ except ImportError:
40
+ # Define minimal stubs if hanzo-agents is not available
41
+ HANZO_AGENTS_AVAILABLE = False
42
+
43
+ class Agent:
44
+ pass
45
+
46
+ class State:
47
+ pass
48
+
49
+ class Network:
50
+ pass
51
+
52
+ class Tool:
53
+ pass
54
+
55
+ class History:
56
+ pass
57
+
58
+ class ModelRegistry:
59
+ pass
60
+
61
+ class InferenceResult:
62
+ pass
63
+
64
+ class ToolCall:
65
+ pass
66
+
67
+ class Router:
68
+ pass
69
+
70
+
71
+ # Import optional components with fallbacks
72
+ try:
73
+ from hanzo_agents import LLMRouter, HybridRouter, DeterministicRouter
74
+ except ImportError:
75
+ try:
76
+ # Try core module import
77
+ from hanzo_agents.core.router import (
78
+ LLMRouter,
79
+ HybridRouter,
80
+ DeterministicRouter,
81
+ )
82
+ except ImportError:
83
+ # Define stubs if not available
84
+ class DeterministicRouter:
85
+ pass
86
+
87
+ class LLMRouter:
88
+ pass
89
+
90
+ class HybridRouter:
91
+ pass
92
+
93
+
94
+ try:
95
+ from hanzo_agents import create_memory_kv, create_memory_vector
96
+ except ImportError:
97
+ try:
98
+ # Try core module import
99
+ from hanzo_agents.core.memory import create_memory_kv, create_memory_vector
100
+ except ImportError:
101
+ # Define stubs if not available
102
+ def create_memory_kv(*args, **kwargs):
103
+ pass
104
+
105
+ def create_memory_vector(*args, **kwargs):
106
+ pass
107
+
108
+
109
+ try:
110
+ from hanzo_agents import sequential_router, conditional_router, state_based_router
111
+ except ImportError:
112
+ try:
113
+ # Try core module import
114
+ from hanzo_agents.core.router import (
115
+ sequential_router,
116
+ conditional_router,
117
+ state_based_router,
118
+ )
119
+ except ImportError:
120
+ # Define stubs if not available
121
+ def sequential_router(*args, **kwargs):
122
+ pass
123
+
124
+ def conditional_router(*args, **kwargs):
125
+ pass
126
+
127
+ def state_based_router(*args, **kwargs):
128
+ pass
129
+
130
+
131
+ try:
132
+ from hanzo_agents.core.cli_agent import (
133
+ GrokAgent,
134
+ GeminiAgent,
135
+ ClaudeCodeAgent,
136
+ OpenAICodexAgent,
137
+ )
138
+ except ImportError:
139
+ # Define stub classes if not available
140
+ class ClaudeCodeAgent(Agent):
141
+ pass
142
+
143
+ class OpenAICodexAgent(Agent):
144
+ pass
145
+
146
+ class GeminiAgent(Agent):
147
+ pass
148
+
149
+ class GrokAgent(Agent):
150
+ pass
151
+
152
+
153
+ from hanzo_mcp.tools.jupyter import get_read_only_jupyter_tools
154
+ from hanzo_mcp.tools.filesystem import Edit, MultiEdit, get_read_only_filesystem_tools
155
+ from hanzo_mcp.tools.common.base import BaseTool
156
+ from hanzo_mcp.tools.common.context import create_tool_context
157
+ from hanzo_mcp.tools.agent.agent_tool import MCPAgent
158
+ from hanzo_mcp.tools.common.batch_tool import BatchTool
159
+ from hanzo_mcp.tools.common.permissions import PermissionManager
160
+
161
+
162
+ class AgentNode(TypedDict):
163
+ """Node in the agent network."""
164
+
165
+ id: str
166
+ query: str
167
+ model: Optional[str]
168
+ role: Optional[str]
169
+ connections: Optional[List[str]]
170
+ receives_from: Optional[List[str]]
171
+ file_path: Optional[str]
172
+
173
+
174
+ class SwarmConfig(TypedDict):
175
+ """Configuration for an agent network."""
176
+
177
+ agents: Dict[str, AgentNode]
178
+ entry_point: Optional[str]
179
+ topology: Optional[str]
180
+
181
+
182
+ class SwarmToolParams(TypedDict):
183
+ """Parameters for the SwarmTool."""
184
+
185
+ config: SwarmConfig
186
+ query: str
187
+ context: Optional[str]
188
+ max_concurrent: Optional[int]
189
+ use_memory: Optional[bool]
190
+ memory_backend: Optional[str]
191
+
192
+
193
+ class SwarmState(State):
194
+ """State for swarm execution."""
195
+
196
+ def __init__(self, config: SwarmConfig, initial_query: str, context: Optional[str] = None):
197
+ """Initialize swarm state."""
198
+ super().__init__()
199
+ self.config = config
200
+ self.initial_query = initial_query
201
+ self.context = context
202
+ self.agent_results = {}
203
+ self.completed_agents = set()
204
+ self.current_agent = None
205
+ self.execution_order = []
206
+
207
+ def to_dict(self) -> Dict[str, Any]:
208
+ """Convert to dictionary."""
209
+ base_dict = super().to_dict()
210
+ base_dict.update(
211
+ {
212
+ "config": self.config,
213
+ "initial_query": self.initial_query,
214
+ "context": self.context,
215
+ "agent_results": self.agent_results,
216
+ "completed_agents": list(self.completed_agents),
217
+ "current_agent": self.current_agent,
218
+ "execution_order": self.execution_order,
219
+ }
220
+ )
221
+ return base_dict
222
+
223
+ @classmethod
224
+ def from_dict(cls, data: Dict[str, Any]) -> "SwarmState":
225
+ """Create from dictionary."""
226
+ state = cls(
227
+ config=data.get("config", {}),
228
+ initial_query=data.get("initial_query", ""),
229
+ context=data.get("context"),
230
+ )
231
+ state.agent_results = data.get("agent_results", {})
232
+ state.completed_agents = set(data.get("completed_agents", []))
233
+ state.current_agent = data.get("current_agent")
234
+ state.execution_order = data.get("execution_order", [])
235
+ return state
236
+
237
+
238
+ class SwarmAgent(MCPAgent):
239
+ """Agent that executes within a swarm network."""
240
+
241
+ def __init__(
242
+ self,
243
+ agent_id: str,
244
+ agent_config: AgentNode,
245
+ available_tools: List[BaseTool],
246
+ permission_manager: PermissionManager,
247
+ ctx: MCPContext,
248
+ **kwargs,
249
+ ):
250
+ """Initialize swarm agent."""
251
+ # Set name and description from config
252
+ self.name = agent_id
253
+ self.description = agent_config.get("role", f"Agent {agent_id}")
254
+ self.agent_config = agent_config
255
+
256
+ # Initialize with specified model
257
+ model = agent_config.get("model")
258
+ if model:
259
+ model = self._normalize_model(model)
260
+ else:
261
+ model = "model://anthropic/claude-3-5-sonnet-20241022"
262
+
263
+ super().__init__(
264
+ available_tools=available_tools,
265
+ permission_manager=permission_manager,
266
+ ctx=ctx,
267
+ model=model,
268
+ **kwargs,
269
+ )
270
+
271
+ def _normalize_model(self, model: str) -> str:
272
+ """Normalize model names to full format."""
273
+ model_map = {
274
+ "claude-3-5-sonnet": "model://anthropic/claude-3-5-sonnet-20241022",
275
+ "claude-3-opus": "model://anthropic/claude-3-opus-20240229",
276
+ "gpt-4o": "model://openai/gpt-4o",
277
+ "gpt-4": "model://openai/gpt-4",
278
+ "gemini-1.5-pro": "model://google/gemini-1.5-pro",
279
+ "gemini-1.5-flash": "model://google/gemini-1.5-flash",
280
+ }
281
+
282
+ # Check if it's already a model:// URI
283
+ if model.startswith("model://"):
284
+ return model
285
+
286
+ # Check mapping
287
+ if model in model_map:
288
+ return model_map[model]
289
+
290
+ # Assume it's a provider/model format
291
+ if "/" in model:
292
+ return f"model://{model}"
293
+
294
+ # Default to anthropic
295
+ return f"model://anthropic/{model}"
296
+
297
+ async def run(self, state: SwarmState, history: History, network: Network) -> InferenceResult:
298
+ """Execute the swarm agent."""
299
+ # Build prompt with context
300
+ prompt_parts = []
301
+
302
+ # Add role context
303
+ if self.agent_config.get("role"):
304
+ prompt_parts.append(f"Your role: {self.agent_config['role']}")
305
+
306
+ # Add shared context
307
+ if state.context:
308
+ prompt_parts.append(f"Context:\n{state.context}")
309
+
310
+ # Add inputs from connected agents
311
+ receives_from = self.agent_config.get("receives_from", [])
312
+ if receives_from:
313
+ inputs = {}
314
+ for agent_id in receives_from:
315
+ if agent_id in state.agent_results:
316
+ inputs[agent_id] = state.agent_results[agent_id]
317
+
318
+ if inputs:
319
+ prompt_parts.append("Input from previous agents:")
320
+ for input_agent, input_result in inputs.items():
321
+ prompt_parts.append(f"\n--- From {input_agent} ---\n{input_result}")
322
+
323
+ # Add file context if specified
324
+ if self.agent_config.get("file_path"):
325
+ prompt_parts.append(f"\nFile to work on: {self.agent_config['file_path']}")
326
+
327
+ # Add the main query
328
+ prompt_parts.append(f"\nTask: {self.agent_config['query']}")
329
+
330
+ # Add initial query if this is entry point
331
+ if state.current_agent == state.config.get("entry_point"):
332
+ prompt_parts.append(f"\nMain objective: {state.initial_query}")
333
+
334
+ full_prompt = "\n\n".join(prompt_parts)
335
+
336
+ # Execute using base class
337
+ messages = [
338
+ {"role": "system", "content": self._get_system_prompt()},
339
+ {"role": "user", "content": full_prompt},
340
+ ]
341
+
342
+ # Call model
343
+ from hanzo_agents import ModelRegistry
344
+
345
+ adapter = ModelRegistry.get_adapter(self.model)
346
+ response = await adapter.chat(messages)
347
+
348
+ # Store result in state
349
+ state.agent_results[self.name] = response
350
+ state.completed_agents.add(self.name)
351
+ state.execution_order.append(self.name)
352
+
353
+ # Return result
354
+ return InferenceResult(
355
+ agent=self.name,
356
+ content=response,
357
+ metadata={
358
+ "agent_id": self.name,
359
+ "role": self.agent_config.get("role"),
360
+ "connections": self.agent_config.get("connections", []),
361
+ },
362
+ )
363
+
364
+
365
+ class SwarmRouter(DeterministicRouter):
366
+ """Router for swarm agent orchestration."""
367
+
368
+ def __init__(self, swarm_config: SwarmConfig):
369
+ """Initialize swarm router."""
370
+ self.swarm_config = swarm_config
371
+ self.agents_config = swarm_config["agents"]
372
+ self.entry_point = swarm_config.get("entry_point")
373
+
374
+ # Build dependency graph
375
+ self.dependencies = {}
376
+ self.dependents = {}
377
+
378
+ for agent_id, config in self.agents_config.items():
379
+ # Dependencies (agents this one waits for)
380
+ self.dependencies[agent_id] = set(config.get("receives_from", []))
381
+
382
+ # Dependents (agents that wait for this one)
383
+ connections = config.get("connections", [])
384
+ for conn in connections:
385
+ if conn not in self.dependents:
386
+ self.dependents[conn] = set()
387
+ self.dependents[conn].add(agent_id)
388
+
389
+ def route(self, network, call_count, last_result, agent_stack):
390
+ """Determine next agent to execute."""
391
+ state = network.state
392
+
393
+ # First call - start with entry point or roots
394
+ if call_count == 0:
395
+ if self.entry_point:
396
+ state.current_agent = self.entry_point
397
+ return self._get_agent_class(self.entry_point, agent_stack)
398
+ else:
399
+ # Find roots (no dependencies)
400
+ roots = [aid for aid, deps in self.dependencies.items() if not deps]
401
+ if roots:
402
+ state.current_agent = roots[0]
403
+ return self._get_agent_class(roots[0], agent_stack)
404
+
405
+ # Find next agent to execute
406
+ for agent_id in self.agents_config:
407
+ if agent_id in state.completed_agents:
408
+ continue
409
+
410
+ # Check if all dependencies are met
411
+ deps = self.dependencies.get(agent_id, set())
412
+ if deps.issubset(state.completed_agents):
413
+ state.current_agent = agent_id
414
+ return self._get_agent_class(agent_id, agent_stack)
415
+
416
+ # No more agents to execute
417
+ return None
418
+
419
+ def _get_agent_class(self, agent_id: str, agent_stack: List[type[Agent]]) -> type[Agent]:
420
+ """Get agent class for given agent ID."""
421
+ # Find matching agent by name
422
+ for agent_class in agent_stack:
423
+ if hasattr(agent_class, "name") and agent_class.name == agent_id:
424
+ return agent_class
425
+
426
+ # Not found - this shouldn't happen
427
+ return None
428
+
429
+
430
+ @final
431
+ class SwarmTool(BaseTool):
432
+ """Tool for executing agent networks using hanzo-agents SDK."""
433
+
434
+ @property
435
+ @override
436
+ def name(self) -> str:
437
+ """Get the tool name."""
438
+ return "swarm"
439
+
440
+ @property
441
+ @override
442
+ def description(self) -> str:
443
+ """Get the tool description."""
444
+ return """Execute a network of AI agents with flexible connection topologies.
445
+
446
+ This tool enables sophisticated agent orchestration where agents can be connected
447
+ in various network patterns. Each agent can pass results to connected agents,
448
+ enabling complex workflows.
449
+
450
+ Features:
451
+ - Flexible agent networks (tree, DAG, pipeline, star, mesh)
452
+ - Each agent can use different models (Claude, GPT-4, Gemini, etc.)
453
+ - Agents automatically pass results to connected agents
454
+ - Parallel execution with dependency management
455
+ - Full editing capabilities for each agent
456
+ - Memory and state management via hanzo-agents SDK
457
+
458
+ Common Topologies:
459
+ 1. Tree (Architect pattern):
460
+ architect → [frontend, backend, database] → reviewer
461
+
462
+ 2. Pipeline (Sequential processing):
463
+ analyzer → planner → implementer → tester → reviewer
464
+
465
+ 3. Star (Central coordinator):
466
+ coordinator ← → [agent1, agent2, agent3, agent4]
467
+
468
+ 4. DAG (Complex dependencies):
469
+ Multiple agents with custom connections
470
+
471
+ Models can be specified as:
472
+ - Full: 'anthropic/claude-3-5-sonnet-20241022'
473
+ - Short: 'claude-3-5-sonnet', 'gpt-4o', 'gemini-1.5-pro'
474
+ - CLI tools: 'claude_cli', 'codex_cli', 'gemini_cli', 'grok_cli'
475
+ - Model URIs: 'model://anthropic/claude-3-opus'
476
+ """
477
+
478
+ def __init__(
479
+ self,
480
+ permission_manager: PermissionManager,
481
+ model: str | None = None,
482
+ api_key: str | None = None,
483
+ base_url: str | None = None,
484
+ max_tokens: int | None = None,
485
+ agent_max_iterations: int = 10,
486
+ agent_max_tool_uses: int = 30,
487
+ ):
488
+ """Initialize the swarm tool."""
489
+ self.permission_manager = permission_manager
490
+ # Default to latest Claude Sonnet if no model specified
491
+ from hanzo_mcp.tools.agent.code_auth import get_latest_claude_model
492
+
493
+ self.model = model or f"anthropic/{get_latest_claude_model()}"
494
+ self.api_key = api_key or os.environ.get("ANTHROPIC_API_KEY") or os.environ.get("CLAUDE_API_KEY")
495
+ self.base_url = base_url
496
+ self.max_tokens = max_tokens
497
+ self.agent_max_iterations = agent_max_iterations
498
+ self.agent_max_tool_uses = agent_max_tool_uses
499
+
500
+ # Set up available tools for agents
501
+ self.available_tools: list[BaseTool] = []
502
+ self.available_tools.extend(get_read_only_filesystem_tools(self.permission_manager))
503
+ self.available_tools.extend(get_read_only_jupyter_tools(self.permission_manager))
504
+
505
+ # Add edit tools
506
+ self.available_tools.append(Edit(self.permission_manager))
507
+ self.available_tools.append(MultiEdit(self.permission_manager))
508
+
509
+ # Add batch tool
510
+ self.available_tools.append(BatchTool({t.name: t for t in self.available_tools}))
511
+
512
+ @override
513
+ @auto_timeout("swarm")
514
+ async def call(
515
+ self,
516
+ ctx: MCPContext,
517
+ **params: Unpack[SwarmToolParams],
518
+ ) -> str:
519
+ """Execute the swarm tool."""
520
+ tool_ctx = create_tool_context(ctx)
521
+ await tool_ctx.set_tool_info(self.name)
522
+
523
+ # Extract parameters
524
+ config = params.get("config", {})
525
+ initial_query = params.get("query", "")
526
+ context = params.get("context", "")
527
+ max_concurrent = params.get("max_concurrent", 10)
528
+ use_memory = params.get("use_memory", False)
529
+ memory_backend = params.get("memory_backend", "sqlite")
530
+
531
+ agents_config = config.get("agents", {})
532
+
533
+ if not agents_config:
534
+ await tool_ctx.error("No agents provided")
535
+ return "Error: At least one agent must be provided."
536
+
537
+ # hanzo-agents SDK is required (already imported above)
538
+
539
+ await tool_ctx.info(f"Starting swarm execution with {len(agents_config)} agents using hanzo-agents SDK")
540
+
541
+ # Create state
542
+ state = SwarmState(config=config, initial_query=initial_query, context=context)
543
+
544
+ # Create agent classes dynamically
545
+ agent_classes = []
546
+ for agent_id, agent_config in agents_config.items():
547
+ # Check for CLI agents
548
+ model = agent_config.get("model", self.model)
549
+
550
+ cli_agents = {
551
+ "claude_cli": ClaudeCodeAgent,
552
+ "codex_cli": OpenAICodexAgent,
553
+ "gemini_cli": GeminiAgent,
554
+ "grok_cli": GrokAgent,
555
+ }
556
+
557
+ if model in cli_agents:
558
+ # Use CLI agent
559
+ agent_class = type(
560
+ f"Swarm{agent_id}",
561
+ (cli_agents[model],),
562
+ {
563
+ "name": agent_id,
564
+ "description": agent_config.get("role", f"Agent {agent_id}"),
565
+ "agent_config": agent_config,
566
+ },
567
+ )
568
+ else:
569
+ # Create dynamic SwarmAgent class
570
+ agent_class = type(
571
+ f"Swarm{agent_id}",
572
+ (SwarmAgent,),
573
+ {
574
+ "name": agent_id,
575
+ "__init__": lambda self, aid=agent_id, acfg=agent_config: SwarmAgent.__init__(
576
+ self,
577
+ agent_id=aid,
578
+ agent_config=acfg,
579
+ available_tools=self.available_tools,
580
+ permission_manager=self.permission_manager,
581
+ ctx=ctx,
582
+ ),
583
+ },
584
+ )
585
+
586
+ agent_classes.append(agent_class)
587
+
588
+ # Create memory if requested
589
+ memory_kv = None
590
+ memory_vector = None
591
+ if use_memory:
592
+ memory_kv = create_memory_kv(memory_backend)
593
+ memory_vector = create_memory_vector("simple")
594
+
595
+ # Create router
596
+ router = SwarmRouter(config)
597
+
598
+ # Create network
599
+ network = Network(
600
+ state=state,
601
+ agents=agent_classes,
602
+ router=router,
603
+ memory_kv=memory_kv,
604
+ memory_vector=memory_vector,
605
+ max_steps=self.agent_max_iterations * len(agents_config),
606
+ )
607
+
608
+ # Execute
609
+ try:
610
+ final_state = await network.run()
611
+
612
+ # Format results
613
+ return self._format_network_results(
614
+ agents_config,
615
+ final_state.agent_results,
616
+ final_state.execution_order,
617
+ config.get("entry_point"),
618
+ )
619
+
620
+ except Exception as e:
621
+ await tool_ctx.error(f"Swarm execution failed: {str(e)}")
622
+ return f"Error: {str(e)}"
623
+
624
+ def _format_network_results(
625
+ self,
626
+ agents_config: Dict[str, Any],
627
+ results: Dict[str, str],
628
+ execution_order: List[str],
629
+ entry_point: Optional[str],
630
+ ) -> str:
631
+ """Format results from agent network execution."""
632
+ output = ["Agent Network Execution Results (hanzo-agents SDK)"]
633
+ output.append("=" * 80)
634
+ output.append(f"Total agents: {len(agents_config)}")
635
+ output.append(f"Completed: {len(results)}")
636
+ output.append(f"Failed: {len([r for r in results.values() if r.startswith('Error:')])}")
637
+
638
+ if entry_point:
639
+ output.append(f"Entry point: {entry_point}")
640
+
641
+ output.append(f"\nExecution Order: {' → '.join(execution_order)}")
642
+ output.append("-" * 40)
643
+
644
+ # Detailed results
645
+ output.append("\n\nDetailed Results:")
646
+ output.append("=" * 80)
647
+
648
+ for agent_id in execution_order:
649
+ if agent_id in results:
650
+ config = agents_config.get(agent_id, {})
651
+ role = config.get("role", "Agent")
652
+ model = config.get("model", "default")
653
+
654
+ output.append(f"\n### {agent_id} ({role}) [{model}]")
655
+ output.append("-" * 40)
656
+
657
+ result = results[agent_id]
658
+ if result.startswith("Error:"):
659
+ output.append(result)
660
+ else:
661
+ # Show first part of result
662
+ lines = result.split("\n")
663
+ preview_lines = lines[:10]
664
+ output.extend(preview_lines)
665
+
666
+ if len(lines) > 10:
667
+ output.append(f"... ({len(lines) - 10} more lines)")
668
+
669
+ return "\n".join(output)
670
+
671
+ @override
672
+ def register(self, mcp_server: FastMCP) -> None:
673
+ """Register this swarm tool with the MCP server."""
674
+ tool_self = self
675
+
676
+ @mcp_server.tool(name=self.name, description=self.description)
677
+ async def swarm(
678
+ ctx: MCPContext,
679
+ config: dict[str, Any],
680
+ query: str,
681
+ context: Optional[str] = None,
682
+ max_concurrent: int = 10,
683
+ use_memory: bool = False,
684
+ memory_backend: str = "sqlite",
685
+ ) -> str:
686
+ # Convert to typed format
687
+ typed_config = SwarmConfig(
688
+ agents=config.get("agents", {}),
689
+ entry_point=config.get("entry_point"),
690
+ topology=config.get("topology"),
691
+ )
692
+
693
+ return await tool_self.call(
694
+ ctx,
695
+ config=typed_config,
696
+ query=query,
697
+ context=context,
698
+ max_concurrent=max_concurrent,
699
+ use_memory=use_memory,
700
+ memory_backend=memory_backend,
701
+ )