hanzo-mcp 0.6.13__py3-none-any.whl → 0.7.1__py3-none-any.whl

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

Potentially problematic release.


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

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