hanzo-mcp 0.7.2__py3-none-any.whl → 0.7.6__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.
- hanzo_mcp/cli.py +10 -0
- hanzo_mcp/prompts/__init__.py +43 -0
- hanzo_mcp/prompts/example_custom_prompt.py +40 -0
- hanzo_mcp/prompts/tool_explorer.py +603 -0
- hanzo_mcp/tools/__init__.py +52 -51
- hanzo_mcp/tools/agent/__init__.py +3 -16
- hanzo_mcp/tools/agent/agent_tool.py +365 -525
- hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +641 -0
- hanzo_mcp/tools/agent/network_tool.py +3 -5
- hanzo_mcp/tools/agent/swarm_tool.py +447 -349
- hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +535 -0
- hanzo_mcp/tools/agent/tool_adapter.py +21 -2
- hanzo_mcp/tools/common/forgiving_edit.py +24 -14
- hanzo_mcp/tools/common/permissions.py +8 -0
- hanzo_mcp/tools/filesystem/__init__.py +5 -5
- hanzo_mcp/tools/filesystem/{symbols.py → ast_tool.py} +8 -8
- hanzo_mcp/tools/filesystem/batch_search.py +2 -2
- hanzo_mcp/tools/filesystem/directory_tree.py +8 -1
- hanzo_mcp/tools/filesystem/find.py +1 -0
- hanzo_mcp/tools/filesystem/grep.py +11 -2
- hanzo_mcp/tools/filesystem/read.py +8 -1
- hanzo_mcp/tools/filesystem/search_tool.py +1 -1
- hanzo_mcp/tools/jupyter/__init__.py +5 -1
- hanzo_mcp/tools/jupyter/base.py +2 -2
- hanzo_mcp/tools/jupyter/jupyter.py +89 -18
- hanzo_mcp/tools/search/find_tool.py +49 -8
- hanzo_mcp/tools/shell/base_process.py +7 -1
- hanzo_mcp/tools/shell/streaming_command.py +34 -1
- {hanzo_mcp-0.7.2.dist-info → hanzo_mcp-0.7.6.dist-info}/METADATA +8 -1
- {hanzo_mcp-0.7.2.dist-info → hanzo_mcp-0.7.6.dist-info}/RECORD +33 -31
- hanzo_mcp/tools/agent/agent_tool_v2.py +0 -492
- hanzo_mcp/tools/agent/swarm_tool_v2.py +0 -654
- {hanzo_mcp-0.7.2.dist-info → hanzo_mcp-0.7.6.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.7.2.dist-info → hanzo_mcp-0.7.6.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.7.2.dist-info → hanzo_mcp-0.7.6.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,535 @@
|
|
|
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 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
|
+
from hanzo_mcp.tools.agent.agent_tool import AgentTool
|
|
18
|
+
from hanzo_mcp.tools.common.base import BaseTool
|
|
19
|
+
from hanzo_mcp.tools.common.context import create_tool_context
|
|
20
|
+
from hanzo_mcp.tools.common.permissions import PermissionManager
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AgentNode(TypedDict):
|
|
24
|
+
"""Node in the agent network.
|
|
25
|
+
|
|
26
|
+
Attributes:
|
|
27
|
+
id: Unique identifier for this agent
|
|
28
|
+
query: The specific query/task for this agent
|
|
29
|
+
model: Optional model override (e.g., 'claude-3-5-sonnet', 'gpt-4o')
|
|
30
|
+
role: Optional role description (e.g., 'architect', 'frontend', 'reviewer')
|
|
31
|
+
connections: List of agent IDs this agent connects to (sends results to)
|
|
32
|
+
receives_from: Optional list of agent IDs this agent receives input from
|
|
33
|
+
file_path: Optional specific file for the agent to work on
|
|
34
|
+
"""
|
|
35
|
+
id: str
|
|
36
|
+
query: str
|
|
37
|
+
model: Optional[str]
|
|
38
|
+
role: Optional[str]
|
|
39
|
+
connections: Optional[List[str]]
|
|
40
|
+
receives_from: Optional[List[str]]
|
|
41
|
+
file_path: Optional[str]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SwarmConfig(TypedDict):
|
|
45
|
+
"""Configuration for an agent network.
|
|
46
|
+
|
|
47
|
+
Attributes:
|
|
48
|
+
agents: Dictionary of agent configurations keyed by ID
|
|
49
|
+
entry_point: ID of the first agent to execute (optional, defaults to finding roots)
|
|
50
|
+
topology: Optional topology type (tree, dag, pipeline, star, mesh)
|
|
51
|
+
"""
|
|
52
|
+
agents: Dict[str, AgentNode]
|
|
53
|
+
entry_point: Optional[str]
|
|
54
|
+
topology: Optional[str]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class SwarmToolParams(TypedDict):
|
|
58
|
+
"""Parameters for the SwarmTool.
|
|
59
|
+
|
|
60
|
+
Attributes:
|
|
61
|
+
config: Agent network configuration
|
|
62
|
+
query: Initial query to send to entry point agent(s)
|
|
63
|
+
context: Optional context shared by all agents
|
|
64
|
+
max_concurrent: Maximum number of concurrent agents (default: 10)
|
|
65
|
+
"""
|
|
66
|
+
config: SwarmConfig
|
|
67
|
+
query: str
|
|
68
|
+
context: Optional[str]
|
|
69
|
+
max_concurrent: Optional[int]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@final
|
|
73
|
+
class SwarmTool(BaseTool):
|
|
74
|
+
"""Tool for executing multiple agent tasks in parallel.
|
|
75
|
+
|
|
76
|
+
The SwarmTool enables efficient parallel processing of multiple files or tasks
|
|
77
|
+
by spawning independent agent instances for each task.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
@override
|
|
82
|
+
def name(self) -> str:
|
|
83
|
+
"""Get the tool name."""
|
|
84
|
+
return "swarm"
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
@override
|
|
88
|
+
def description(self) -> str:
|
|
89
|
+
"""Get the tool description."""
|
|
90
|
+
return """Execute a network of AI agents with flexible connection topologies.
|
|
91
|
+
|
|
92
|
+
This tool enables sophisticated agent orchestration where agents can be connected
|
|
93
|
+
in various network patterns. Each agent can pass results to connected agents,
|
|
94
|
+
enabling complex workflows.
|
|
95
|
+
|
|
96
|
+
Features:
|
|
97
|
+
- Flexible agent networks (tree, DAG, pipeline, star, mesh)
|
|
98
|
+
- Each agent can use different models (Claude, GPT-4, Gemini, etc.)
|
|
99
|
+
- Agents automatically pass results to connected agents
|
|
100
|
+
- Parallel execution with dependency management
|
|
101
|
+
- Full editing capabilities for each agent
|
|
102
|
+
|
|
103
|
+
Common Topologies:
|
|
104
|
+
|
|
105
|
+
1. Tree (Architect pattern):
|
|
106
|
+
architect → [frontend, backend, database] → reviewer
|
|
107
|
+
|
|
108
|
+
2. Pipeline (Sequential processing):
|
|
109
|
+
analyzer → planner → implementer → tester → reviewer
|
|
110
|
+
|
|
111
|
+
3. Star (Central coordinator):
|
|
112
|
+
coordinator ← → [agent1, agent2, agent3, agent4]
|
|
113
|
+
|
|
114
|
+
4. DAG (Complex dependencies):
|
|
115
|
+
Multiple agents with custom connections
|
|
116
|
+
|
|
117
|
+
Usage Example:
|
|
118
|
+
|
|
119
|
+
swarm(
|
|
120
|
+
config={
|
|
121
|
+
"agents": {
|
|
122
|
+
"architect": {
|
|
123
|
+
"id": "architect",
|
|
124
|
+
"query": "Analyze codebase and create refactoring plan",
|
|
125
|
+
"model": "claude-3-5-sonnet",
|
|
126
|
+
"connections": ["frontend", "backend", "database"]
|
|
127
|
+
},
|
|
128
|
+
"frontend": {
|
|
129
|
+
"id": "frontend",
|
|
130
|
+
"query": "Refactor UI components based on architect's plan",
|
|
131
|
+
"role": "Frontend Developer",
|
|
132
|
+
"connections": ["reviewer"]
|
|
133
|
+
},
|
|
134
|
+
"backend": {
|
|
135
|
+
"id": "backend",
|
|
136
|
+
"query": "Refactor API endpoints based on architect's plan",
|
|
137
|
+
"role": "Backend Developer",
|
|
138
|
+
"connections": ["reviewer"]
|
|
139
|
+
},
|
|
140
|
+
"database": {
|
|
141
|
+
"id": "database",
|
|
142
|
+
"query": "Optimize database schema based on architect's plan",
|
|
143
|
+
"role": "Database Expert",
|
|
144
|
+
"connections": ["reviewer"]
|
|
145
|
+
},
|
|
146
|
+
"reviewer": {
|
|
147
|
+
"id": "reviewer",
|
|
148
|
+
"query": "Review all changes and ensure consistency",
|
|
149
|
+
"model": "gpt-4o",
|
|
150
|
+
"receives_from": ["frontend", "backend", "database"]
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
"entry_point": "architect"
|
|
154
|
+
},
|
|
155
|
+
query="Refactor the authentication system for better security and performance"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
Models can be specified as:
|
|
159
|
+
- Full: 'anthropic/claude-3-5-sonnet-20241022'
|
|
160
|
+
- Short: 'claude-3-5-sonnet', 'gpt-4o', 'gemini-1.5-pro'
|
|
161
|
+
- CLI tools: 'claude_cli', 'codex_cli', 'gemini_cli', 'grok_cli'
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
def __init__(
|
|
165
|
+
self,
|
|
166
|
+
permission_manager: PermissionManager,
|
|
167
|
+
model: str | None = None,
|
|
168
|
+
api_key: str | None = None,
|
|
169
|
+
base_url: str | None = None,
|
|
170
|
+
max_tokens: int | None = None,
|
|
171
|
+
agent_max_iterations: int = 10,
|
|
172
|
+
agent_max_tool_uses: int = 30,
|
|
173
|
+
):
|
|
174
|
+
"""Initialize the swarm tool.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
permission_manager: Permission manager for access control
|
|
178
|
+
model: Optional model name override (defaults to Claude Sonnet)
|
|
179
|
+
api_key: Optional API key for the model provider
|
|
180
|
+
base_url: Optional base URL for the model provider
|
|
181
|
+
max_tokens: Optional maximum tokens for model responses
|
|
182
|
+
agent_max_iterations: Max iterations per agent (default: 10)
|
|
183
|
+
agent_max_tool_uses: Max tool uses per agent (default: 30)
|
|
184
|
+
"""
|
|
185
|
+
self.permission_manager = permission_manager
|
|
186
|
+
# Default to latest Claude Sonnet if no model specified
|
|
187
|
+
from hanzo_mcp.tools.agent.code_auth import get_latest_claude_model
|
|
188
|
+
self.model = model or f"anthropic/{get_latest_claude_model()}"
|
|
189
|
+
self.api_key = api_key or os.environ.get("ANTHROPIC_API_KEY") or os.environ.get("CLAUDE_API_KEY")
|
|
190
|
+
self.base_url = base_url
|
|
191
|
+
self.max_tokens = max_tokens
|
|
192
|
+
self.agent_max_iterations = agent_max_iterations
|
|
193
|
+
self.agent_max_tool_uses = agent_max_tool_uses
|
|
194
|
+
|
|
195
|
+
@override
|
|
196
|
+
async def call(
|
|
197
|
+
self,
|
|
198
|
+
ctx: MCPContext,
|
|
199
|
+
**params: Unpack[SwarmToolParams],
|
|
200
|
+
) -> str:
|
|
201
|
+
"""Execute the swarm tool.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
ctx: MCP context
|
|
205
|
+
**params: Tool parameters
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Combined results from all agents
|
|
209
|
+
"""
|
|
210
|
+
tool_ctx = create_tool_context(ctx)
|
|
211
|
+
await tool_ctx.set_tool_info(self.name)
|
|
212
|
+
|
|
213
|
+
# Extract parameters
|
|
214
|
+
agents = params.get("agents", [])
|
|
215
|
+
manager_query = params.get("manager_query")
|
|
216
|
+
reviewer_query = params.get("reviewer_query")
|
|
217
|
+
common_context = params.get("common_context", "")
|
|
218
|
+
max_concurrent = params.get("max_concurrent", 10)
|
|
219
|
+
|
|
220
|
+
if not agents:
|
|
221
|
+
await tool_ctx.error("No agents provided")
|
|
222
|
+
return "Error: At least one agent must be provided."
|
|
223
|
+
|
|
224
|
+
# Extract parameters
|
|
225
|
+
config = params.get("config", {})
|
|
226
|
+
initial_query = params.get("query", "")
|
|
227
|
+
context = params.get("context", "")
|
|
228
|
+
|
|
229
|
+
agents_config = config.get("agents", {})
|
|
230
|
+
entry_point = config.get("entry_point")
|
|
231
|
+
|
|
232
|
+
await tool_ctx.info(f"Starting swarm execution with {len(agents_config)} agents")
|
|
233
|
+
|
|
234
|
+
# Build agent network
|
|
235
|
+
agent_instances = {}
|
|
236
|
+
agent_results = {}
|
|
237
|
+
execution_queue = asyncio.Queue()
|
|
238
|
+
completed_agents = set()
|
|
239
|
+
|
|
240
|
+
# Create agent instances
|
|
241
|
+
for agent_id, agent_config in agents_config.items():
|
|
242
|
+
model = agent_config.get("model", self.model)
|
|
243
|
+
|
|
244
|
+
# Support CLI tools
|
|
245
|
+
cli_tools = {
|
|
246
|
+
"claude_cli": self._get_cli_tool("claude_cli"),
|
|
247
|
+
"codex_cli": self._get_cli_tool("codex_cli"),
|
|
248
|
+
"gemini_cli": self._get_cli_tool("gemini_cli"),
|
|
249
|
+
"grok_cli": self._get_cli_tool("grok_cli"),
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if model in cli_tools:
|
|
253
|
+
agent = cli_tools[model]
|
|
254
|
+
else:
|
|
255
|
+
# Regular agent with model
|
|
256
|
+
agent = AgentTool(
|
|
257
|
+
permission_manager=self.permission_manager,
|
|
258
|
+
model=self._normalize_model(model),
|
|
259
|
+
api_key=self.api_key,
|
|
260
|
+
base_url=self.base_url,
|
|
261
|
+
max_tokens=self.max_tokens,
|
|
262
|
+
max_iterations=self.agent_max_iterations,
|
|
263
|
+
max_tool_uses=self.agent_max_tool_uses,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
agent_instances[agent_id] = agent
|
|
267
|
+
|
|
268
|
+
# Find entry points (agents with no incoming connections)
|
|
269
|
+
if entry_point:
|
|
270
|
+
await execution_queue.put((entry_point, initial_query, {}))
|
|
271
|
+
else:
|
|
272
|
+
# Find root agents (no receives_from)
|
|
273
|
+
roots = []
|
|
274
|
+
for agent_id, agent_config in agents_config.items():
|
|
275
|
+
if not agent_config.get("receives_from"):
|
|
276
|
+
# Check if any other agent connects to this one
|
|
277
|
+
has_incoming = False
|
|
278
|
+
for other_config in agents_config.values():
|
|
279
|
+
if other_config.get("connections") and agent_id in other_config["connections"]:
|
|
280
|
+
has_incoming = True
|
|
281
|
+
break
|
|
282
|
+
if not has_incoming:
|
|
283
|
+
roots.append(agent_id)
|
|
284
|
+
|
|
285
|
+
if not roots:
|
|
286
|
+
await tool_ctx.error("No entry point found in agent network")
|
|
287
|
+
return "Error: Could not determine entry point for agent network"
|
|
288
|
+
|
|
289
|
+
for root in roots:
|
|
290
|
+
await execution_queue.put((root, initial_query, {}))
|
|
291
|
+
|
|
292
|
+
# Execute agents in network order
|
|
293
|
+
async def execute_agent(agent_id: str, query: str, inputs: Dict[str, str]) -> str:
|
|
294
|
+
"""Execute a single agent in the network."""
|
|
295
|
+
async with semaphore:
|
|
296
|
+
try:
|
|
297
|
+
agent_config = agents_config[agent_id]
|
|
298
|
+
agent = agent_instances[agent_id]
|
|
299
|
+
|
|
300
|
+
await tool_ctx.info(f"Executing agent: {agent_id} ({agent_config.get('role', 'Agent')})")
|
|
301
|
+
|
|
302
|
+
# Build prompt with context and inputs
|
|
303
|
+
prompt_parts = []
|
|
304
|
+
|
|
305
|
+
# Add role context
|
|
306
|
+
if agent_config.get("role"):
|
|
307
|
+
prompt_parts.append(f"Your role: {agent_config['role']}")
|
|
308
|
+
|
|
309
|
+
# Add shared context
|
|
310
|
+
if context:
|
|
311
|
+
prompt_parts.append(f"Context:\n{context}")
|
|
312
|
+
|
|
313
|
+
# Add inputs from connected agents
|
|
314
|
+
if inputs:
|
|
315
|
+
prompt_parts.append("Input from previous agents:")
|
|
316
|
+
for input_agent, input_result in inputs.items():
|
|
317
|
+
prompt_parts.append(f"\n--- From {input_agent} ---\n{input_result}")
|
|
318
|
+
|
|
319
|
+
# Add file context if specified
|
|
320
|
+
if agent_config.get("file_path"):
|
|
321
|
+
prompt_parts.append(f"\nFile to work on: {agent_config['file_path']}")
|
|
322
|
+
|
|
323
|
+
# Add the main query
|
|
324
|
+
prompt_parts.append(f"\nTask: {agent_config['query']}")
|
|
325
|
+
|
|
326
|
+
# Combine query with initial query if this is entry point
|
|
327
|
+
if query and query != agent_config['query']:
|
|
328
|
+
prompt_parts.append(f"\nMain objective: {query}")
|
|
329
|
+
|
|
330
|
+
full_prompt = "\n\n".join(prompt_parts)
|
|
331
|
+
|
|
332
|
+
# Execute the agent
|
|
333
|
+
result = await agent.call(ctx, prompts=full_prompt)
|
|
334
|
+
|
|
335
|
+
await tool_ctx.info(f"Agent {agent_id} completed")
|
|
336
|
+
return result
|
|
337
|
+
|
|
338
|
+
except Exception as e:
|
|
339
|
+
error_msg = f"Agent {agent_id} failed: {str(e)}"
|
|
340
|
+
await tool_ctx.error(error_msg)
|
|
341
|
+
return f"Error: {error_msg}"
|
|
342
|
+
|
|
343
|
+
# Process agent network
|
|
344
|
+
running_tasks = set()
|
|
345
|
+
|
|
346
|
+
while not execution_queue.empty() or running_tasks:
|
|
347
|
+
# Start new tasks up to concurrency limit
|
|
348
|
+
while not execution_queue.empty() and len(running_tasks) < max_concurrent:
|
|
349
|
+
agent_id, query, inputs = await execution_queue.get()
|
|
350
|
+
|
|
351
|
+
if agent_id not in completed_agents:
|
|
352
|
+
# Check if all dependencies are met
|
|
353
|
+
agent_config = agents_config[agent_id]
|
|
354
|
+
receives_from = agent_config.get("receives_from", [])
|
|
355
|
+
|
|
356
|
+
# Collect inputs from dependencies
|
|
357
|
+
ready = True
|
|
358
|
+
for dep in receives_from:
|
|
359
|
+
if dep not in agent_results:
|
|
360
|
+
ready = False
|
|
361
|
+
# Re-queue for later
|
|
362
|
+
await execution_queue.put((agent_id, query, inputs))
|
|
363
|
+
break
|
|
364
|
+
else:
|
|
365
|
+
inputs[dep] = agent_results[dep]
|
|
366
|
+
|
|
367
|
+
if ready:
|
|
368
|
+
# Execute agent
|
|
369
|
+
task = asyncio.create_task(execute_agent(agent_id, query, inputs))
|
|
370
|
+
running_tasks.add(task)
|
|
371
|
+
|
|
372
|
+
async def handle_completion(task, agent_id=agent_id):
|
|
373
|
+
result = await task
|
|
374
|
+
agent_results[agent_id] = result
|
|
375
|
+
completed_agents.add(agent_id)
|
|
376
|
+
running_tasks.discard(task)
|
|
377
|
+
|
|
378
|
+
# Queue connected agents
|
|
379
|
+
agent_config = agents_config[agent_id]
|
|
380
|
+
connections = agent_config.get("connections", [])
|
|
381
|
+
for next_agent in connections:
|
|
382
|
+
if next_agent in agents_config:
|
|
383
|
+
await execution_queue.put((next_agent, "", {agent_id: result}))
|
|
384
|
+
|
|
385
|
+
asyncio.create_task(handle_completion(task))
|
|
386
|
+
|
|
387
|
+
# Wait a bit if we're at capacity
|
|
388
|
+
if running_tasks:
|
|
389
|
+
await asyncio.sleep(0.1)
|
|
390
|
+
|
|
391
|
+
# Wait for all tasks to complete
|
|
392
|
+
if running_tasks:
|
|
393
|
+
await asyncio.gather(*running_tasks, return_exceptions=True)
|
|
394
|
+
|
|
395
|
+
# Format results
|
|
396
|
+
return self._format_network_results(agents_config, agent_results, entry_point)
|
|
397
|
+
|
|
398
|
+
def _normalize_model(self, model: str) -> str:
|
|
399
|
+
"""Normalize model names to full format."""
|
|
400
|
+
model_map = {
|
|
401
|
+
"claude-3-5-sonnet": "anthropic/claude-3-5-sonnet-20241022",
|
|
402
|
+
"claude-3-opus": "anthropic/claude-3-opus-20240229",
|
|
403
|
+
"gpt-4o": "openai/gpt-4o",
|
|
404
|
+
"gpt-4": "openai/gpt-4",
|
|
405
|
+
"gemini-1.5-pro": "google/gemini-1.5-pro",
|
|
406
|
+
"gemini-1.5-flash": "google/gemini-1.5-flash",
|
|
407
|
+
}
|
|
408
|
+
return model_map.get(model, model)
|
|
409
|
+
|
|
410
|
+
def _get_cli_tool(self, tool_name: str):
|
|
411
|
+
"""Get CLI tool instance."""
|
|
412
|
+
# Import here to avoid circular imports
|
|
413
|
+
if tool_name == "claude_cli":
|
|
414
|
+
from hanzo_mcp.tools.agent.claude_cli_tool import ClaudeCLITool
|
|
415
|
+
return ClaudeCLITool(self.permission_manager)
|
|
416
|
+
elif tool_name == "codex_cli":
|
|
417
|
+
from hanzo_mcp.tools.agent.codex_cli_tool import CodexCLITool
|
|
418
|
+
return CodexCLITool(self.permission_manager)
|
|
419
|
+
elif tool_name == "gemini_cli":
|
|
420
|
+
from hanzo_mcp.tools.agent.gemini_cli_tool import GeminiCLITool
|
|
421
|
+
return GeminiCLITool(self.permission_manager)
|
|
422
|
+
elif tool_name == "grok_cli":
|
|
423
|
+
from hanzo_mcp.tools.agent.grok_cli_tool import GrokCLITool
|
|
424
|
+
return GrokCLITool(self.permission_manager)
|
|
425
|
+
return None
|
|
426
|
+
|
|
427
|
+
def _format_network_results(
|
|
428
|
+
self,
|
|
429
|
+
agents_config: Dict[str, Any],
|
|
430
|
+
results: Dict[str, str],
|
|
431
|
+
entry_point: Optional[str]
|
|
432
|
+
) -> str:
|
|
433
|
+
"""Format results from agent network execution."""
|
|
434
|
+
output = ["Agent Network Execution Results"]
|
|
435
|
+
output.append("=" * 80)
|
|
436
|
+
output.append(f"Total agents: {len(agents_config)}")
|
|
437
|
+
output.append(f"Completed: {len(results)}")
|
|
438
|
+
output.append(f"Failed: {len([r for r in results.values() if r.startswith('Error:')])}")
|
|
439
|
+
|
|
440
|
+
if entry_point:
|
|
441
|
+
output.append(f"Entry point: {entry_point}")
|
|
442
|
+
|
|
443
|
+
output.append("\nExecution Flow:")
|
|
444
|
+
output.append("-" * 40)
|
|
445
|
+
|
|
446
|
+
# Show results in execution order
|
|
447
|
+
def format_agent_tree(agent_id: str, level: int = 0) -> List[str]:
|
|
448
|
+
lines = []
|
|
449
|
+
indent = " " * level
|
|
450
|
+
|
|
451
|
+
if agent_id in agents_config:
|
|
452
|
+
config = agents_config[agent_id]
|
|
453
|
+
role = config.get("role", "Agent")
|
|
454
|
+
model = config.get("model", "default")
|
|
455
|
+
|
|
456
|
+
status = "✅" if agent_id in results and not results[agent_id].startswith("Error:") else "❌"
|
|
457
|
+
lines.append(f"{indent}{status} {agent_id} ({role}) [{model}]")
|
|
458
|
+
|
|
459
|
+
# Show connections
|
|
460
|
+
connections = config.get("connections", [])
|
|
461
|
+
for conn in connections:
|
|
462
|
+
if conn in agents_config:
|
|
463
|
+
lines.extend(format_agent_tree(conn, level + 1))
|
|
464
|
+
|
|
465
|
+
return lines
|
|
466
|
+
|
|
467
|
+
# Start from entry point or roots
|
|
468
|
+
if entry_point:
|
|
469
|
+
output.extend(format_agent_tree(entry_point))
|
|
470
|
+
else:
|
|
471
|
+
# Find roots
|
|
472
|
+
roots = []
|
|
473
|
+
for agent_id in agents_config:
|
|
474
|
+
has_incoming = False
|
|
475
|
+
for config in agents_config.values():
|
|
476
|
+
if config.get("connections") and agent_id in config["connections"]:
|
|
477
|
+
has_incoming = True
|
|
478
|
+
break
|
|
479
|
+
if not has_incoming:
|
|
480
|
+
roots.append(agent_id)
|
|
481
|
+
|
|
482
|
+
for root in roots:
|
|
483
|
+
output.extend(format_agent_tree(root))
|
|
484
|
+
|
|
485
|
+
# Detailed results
|
|
486
|
+
output.append("\n\nDetailed Results:")
|
|
487
|
+
output.append("=" * 80)
|
|
488
|
+
|
|
489
|
+
for agent_id, result in results.items():
|
|
490
|
+
config = agents_config.get(agent_id, {})
|
|
491
|
+
role = config.get("role", "Agent")
|
|
492
|
+
|
|
493
|
+
output.append(f"\n### {agent_id} ({role})")
|
|
494
|
+
output.append("-" * 40)
|
|
495
|
+
|
|
496
|
+
if result.startswith("Error:"):
|
|
497
|
+
output.append(result)
|
|
498
|
+
else:
|
|
499
|
+
# Show first part of result
|
|
500
|
+
lines = result.split('\n')
|
|
501
|
+
preview_lines = lines[:10]
|
|
502
|
+
output.extend(preview_lines)
|
|
503
|
+
|
|
504
|
+
if len(lines) > 10:
|
|
505
|
+
output.append(f"... ({len(lines) - 10} more lines)")
|
|
506
|
+
|
|
507
|
+
return "\n".join(output)
|
|
508
|
+
|
|
509
|
+
@override
|
|
510
|
+
def register(self, mcp_server: FastMCP) -> None:
|
|
511
|
+
"""Register this swarm tool with the MCP server."""
|
|
512
|
+
tool_self = self
|
|
513
|
+
|
|
514
|
+
@mcp_server.tool(name=self.name, description=self.description)
|
|
515
|
+
async def swarm(
|
|
516
|
+
ctx: MCPContext,
|
|
517
|
+
config: dict[str, Any],
|
|
518
|
+
query: str,
|
|
519
|
+
context: Optional[str] = None,
|
|
520
|
+
max_concurrent: int = 10,
|
|
521
|
+
) -> str:
|
|
522
|
+
# Convert to typed format
|
|
523
|
+
typed_config = SwarmConfig(
|
|
524
|
+
agents=config.get("agents", {}),
|
|
525
|
+
entry_point=config.get("entry_point"),
|
|
526
|
+
topology=config.get("topology")
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
return await tool_self.call(
|
|
530
|
+
ctx,
|
|
531
|
+
config=typed_config,
|
|
532
|
+
query=query,
|
|
533
|
+
context=context,
|
|
534
|
+
max_concurrent=max_concurrent
|
|
535
|
+
)
|
|
@@ -76,5 +76,24 @@ def supports_parallel_function_calling(model: str) -> bool:
|
|
|
76
76
|
Returns:
|
|
77
77
|
True if the model supports parallel function calling, False otherwise
|
|
78
78
|
"""
|
|
79
|
-
#
|
|
80
|
-
|
|
79
|
+
# Since litellm doesn't have this function, we'll implement a simple check
|
|
80
|
+
# based on known models that support parallel function calling
|
|
81
|
+
parallel_capable_models = {
|
|
82
|
+
# OpenAI models that support parallel function calling
|
|
83
|
+
"gpt-4-turbo", "gpt-4-turbo-preview", "gpt-4-turbo-2024-04-09",
|
|
84
|
+
"gpt-4o", "gpt-4o-mini", "gpt-4o-2024-05-13", "gpt-4o-2024-08-06",
|
|
85
|
+
"gpt-3.5-turbo", "gpt-3.5-turbo-0125", "gpt-3.5-turbo-1106",
|
|
86
|
+
# Anthropic models with tool support
|
|
87
|
+
"claude-3-opus", "claude-3-sonnet", "claude-3-haiku",
|
|
88
|
+
"claude-3-5-sonnet", "claude-3-5-sonnet-20241022",
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Extract model name without provider prefix
|
|
92
|
+
model_name = model.split("/")[-1] if "/" in model else model
|
|
93
|
+
|
|
94
|
+
# Check if the base model name matches any known parallel-capable models
|
|
95
|
+
for capable_model in parallel_capable_models:
|
|
96
|
+
if model_name.startswith(capable_model):
|
|
97
|
+
return True
|
|
98
|
+
|
|
99
|
+
return False
|
|
@@ -22,19 +22,22 @@ class ForgivingEditHelper:
|
|
|
22
22
|
Returns:
|
|
23
23
|
Text with normalized whitespace
|
|
24
24
|
"""
|
|
25
|
-
#
|
|
26
|
-
text = text.replace('\t', ' ')
|
|
27
|
-
|
|
28
|
-
# Normalize multiple spaces to single space (except at line start)
|
|
25
|
+
# Handle the input line by line
|
|
29
26
|
lines = []
|
|
30
27
|
for line in text.split('\n'):
|
|
31
|
-
#
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
28
|
+
# Replace tabs with 4 spaces everywhere in the line
|
|
29
|
+
line = line.replace('\t', ' ')
|
|
30
|
+
|
|
31
|
+
# Split into indentation and content
|
|
32
|
+
stripped = line.lstrip()
|
|
33
|
+
indent = line[:len(line) - len(stripped)]
|
|
34
|
+
|
|
35
|
+
if stripped:
|
|
36
|
+
# For content, normalize multiple spaces to single space
|
|
37
|
+
content = re.sub(r' {2,}', ' ', stripped)
|
|
38
|
+
lines.append(indent + content)
|
|
39
|
+
else:
|
|
40
|
+
lines.append(indent)
|
|
38
41
|
|
|
39
42
|
return '\n'.join(lines)
|
|
40
43
|
|
|
@@ -236,8 +239,15 @@ class ForgivingEditHelper:
|
|
|
236
239
|
# Remove any line number prefixes (common in AI copy-paste)
|
|
237
240
|
lines = []
|
|
238
241
|
for line in text.split('\n'):
|
|
239
|
-
# Remove common line number patterns
|
|
240
|
-
|
|
241
|
-
|
|
242
|
+
# Remove common line number patterns while preserving indentation
|
|
243
|
+
# Match patterns like "1: ", "123: ", "1| ", "1- ", etc.
|
|
244
|
+
# But preserve the original indentation after the line number
|
|
245
|
+
match = re.match(r'^(\d+[:\|\-])\s(.*)', line)
|
|
246
|
+
if match:
|
|
247
|
+
# Keep only the content part (group 2) which includes any indentation
|
|
248
|
+
lines.append(match.group(2))
|
|
249
|
+
else:
|
|
250
|
+
# No line number pattern found, keep the line as-is
|
|
251
|
+
lines.append(line)
|
|
242
252
|
|
|
243
253
|
return '\n'.join(lines)
|
|
@@ -28,6 +28,14 @@ class PermissionManager:
|
|
|
28
28
|
else: # Unix/Linux/Mac
|
|
29
29
|
self.allowed_paths.add(Path("/tmp").resolve())
|
|
30
30
|
self.allowed_paths.add(Path("/var").resolve())
|
|
31
|
+
|
|
32
|
+
# Also allow user's home directory work folders
|
|
33
|
+
home = Path.home()
|
|
34
|
+
if home.exists():
|
|
35
|
+
# Add common development directories
|
|
36
|
+
work_dir = home / "work"
|
|
37
|
+
if work_dir.exists():
|
|
38
|
+
self.allowed_paths.add(work_dir.resolve())
|
|
31
39
|
|
|
32
40
|
# Excluded paths
|
|
33
41
|
self.excluded_paths: set[Path] = set()
|
|
@@ -13,7 +13,7 @@ from hanzo_mcp.tools.filesystem.content_replace import ContentReplaceTool
|
|
|
13
13
|
from hanzo_mcp.tools.filesystem.directory_tree import DirectoryTreeTool
|
|
14
14
|
from hanzo_mcp.tools.filesystem.edit import Edit
|
|
15
15
|
from hanzo_mcp.tools.filesystem.grep import Grep
|
|
16
|
-
from hanzo_mcp.tools.filesystem.
|
|
16
|
+
from hanzo_mcp.tools.filesystem.ast_tool import ASTTool
|
|
17
17
|
from hanzo_mcp.tools.filesystem.git_search import GitSearchTool
|
|
18
18
|
from hanzo_mcp.tools.filesystem.multi_edit import MultiEdit
|
|
19
19
|
from hanzo_mcp.tools.filesystem.read import ReadTool
|
|
@@ -41,7 +41,7 @@ __all__ = [
|
|
|
41
41
|
"DirectoryTreeTool",
|
|
42
42
|
"Grep",
|
|
43
43
|
"ContentReplaceTool",
|
|
44
|
-
"
|
|
44
|
+
"ASTTool",
|
|
45
45
|
"GitSearchTool",
|
|
46
46
|
"BatchSearchTool",
|
|
47
47
|
"FindFilesTool",
|
|
@@ -69,7 +69,7 @@ def get_read_only_filesystem_tools(
|
|
|
69
69
|
ReadTool(permission_manager),
|
|
70
70
|
DirectoryTreeTool(permission_manager),
|
|
71
71
|
Grep(permission_manager),
|
|
72
|
-
|
|
72
|
+
ASTTool(permission_manager),
|
|
73
73
|
GitSearchTool(permission_manager),
|
|
74
74
|
FindFilesTool(permission_manager),
|
|
75
75
|
RulesTool(permission_manager),
|
|
@@ -109,7 +109,7 @@ def get_filesystem_tools(permission_manager: PermissionManager, project_manager=
|
|
|
109
109
|
DirectoryTreeTool(permission_manager),
|
|
110
110
|
Grep(permission_manager),
|
|
111
111
|
ContentReplaceTool(permission_manager),
|
|
112
|
-
|
|
112
|
+
ASTTool(permission_manager),
|
|
113
113
|
GitSearchTool(permission_manager),
|
|
114
114
|
FindFilesTool(permission_manager),
|
|
115
115
|
RulesTool(permission_manager),
|
|
@@ -160,7 +160,7 @@ def register_filesystem_tools(
|
|
|
160
160
|
"multi_edit": MultiEdit,
|
|
161
161
|
"directory_tree": DirectoryTreeTool,
|
|
162
162
|
"grep": Grep,
|
|
163
|
-
"
|
|
163
|
+
"ast": ASTTool, # AST-based code structure search with tree-sitter
|
|
164
164
|
"git_search": GitSearchTool,
|
|
165
165
|
"content_replace": ContentReplaceTool,
|
|
166
166
|
"batch_search": BatchSearchTool,
|