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,492 @@
1
+ """Agent tool implementation using hanzo-agents SDK.
2
+
3
+ This module implements the AgentTool that leverages the hanzo-agents SDK
4
+ for sophisticated agent orchestration and execution.
5
+ """
6
+
7
+ import asyncio
8
+ import json
9
+ import os
10
+ import re
11
+ import time
12
+ from typing import Annotated, TypedDict, Unpack, final, override, Optional, Dict, Any, List
13
+
14
+ from mcp.server.fastmcp import Context as MCPContext
15
+ from mcp.server import FastMCP
16
+ from pydantic import Field
17
+
18
+ # Import hanzo-agents SDK
19
+ try:
20
+ from hanzo_agents import (
21
+ Agent, State, Network, Tool, History,
22
+ ModelRegistry, InferenceResult, ToolCall,
23
+ create_memory_kv, create_memory_vector,
24
+ sequential_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
+ from hanzo_mcp.tools.common.base import BaseTool
68
+ from hanzo_mcp.tools.common.context import ToolContext, create_tool_context
69
+ from hanzo_mcp.tools.common.permissions import PermissionManager
70
+ from hanzo_mcp.tools.filesystem import get_read_only_filesystem_tools, Edit, MultiEdit
71
+ from hanzo_mcp.tools.jupyter import get_read_only_jupyter_tools
72
+ from hanzo_mcp.tools.common.batch_tool import BatchTool
73
+ from hanzo_mcp.tools.agent.clarification_protocol import AgentClarificationMixin, ClarificationType
74
+ from hanzo_mcp.tools.agent.clarification_tool import ClarificationTool
75
+ from hanzo_mcp.tools.agent.critic_tool import CriticTool
76
+ from hanzo_mcp.tools.agent.review_tool import ReviewTool
77
+ from hanzo_mcp.tools.agent.iching_tool import IChingTool
78
+
79
+
80
+ class AgentToolParams(TypedDict, total=False):
81
+ """Parameters for the AgentTool."""
82
+ prompts: str | list[str]
83
+ model: Optional[str]
84
+ use_memory: Optional[bool]
85
+ memory_backend: Optional[str]
86
+
87
+
88
+ class MCPAgentState(State):
89
+ """State for MCP agents."""
90
+
91
+ def __init__(self, prompts: List[str], context: Dict[str, Any]):
92
+ """Initialize agent state."""
93
+ super().__init__()
94
+ self.prompts = prompts
95
+ self.context = context
96
+ self.current_prompt_index = 0
97
+ self.results = []
98
+
99
+ def to_dict(self) -> Dict[str, Any]:
100
+ """Convert to dictionary."""
101
+ base_dict = super().to_dict()
102
+ base_dict.update({
103
+ "prompts": self.prompts,
104
+ "context": self.context,
105
+ "current_prompt_index": self.current_prompt_index,
106
+ "results": self.results
107
+ })
108
+ return base_dict
109
+
110
+ @classmethod
111
+ def from_dict(cls, data: Dict[str, Any]) -> "MCPAgentState":
112
+ """Create from dictionary."""
113
+ state = cls(
114
+ prompts=data.get("prompts", []),
115
+ context=data.get("context", {})
116
+ )
117
+ state.current_prompt_index = data.get("current_prompt_index", 0)
118
+ state.results = data.get("results", [])
119
+ for k, v in data.items():
120
+ if k not in ["prompts", "context", "current_prompt_index", "results"]:
121
+ state[k] = v
122
+ return state
123
+
124
+
125
+ class MCPToolAdapter(Tool):
126
+ """Adapter to wrap MCP tools for hanzo-agents."""
127
+
128
+ def __init__(self, mcp_tool: BaseTool, ctx: MCPContext):
129
+ """Initialize adapter."""
130
+ self.mcp_tool = mcp_tool
131
+ self.ctx = ctx
132
+
133
+ @property
134
+ def name(self) -> str:
135
+ """Get tool name."""
136
+ return self.mcp_tool.name
137
+
138
+ @property
139
+ def description(self) -> str:
140
+ """Get tool description."""
141
+ return self.mcp_tool.description
142
+
143
+ async def execute(self, **kwargs) -> str:
144
+ """Execute the MCP tool."""
145
+ return await self.mcp_tool.call(self.ctx, **kwargs)
146
+
147
+
148
+ class MCPAgent(Agent):
149
+ """Agent that executes MCP tasks."""
150
+
151
+ name = "mcp_agent"
152
+ description = "Agent for executing MCP tasks"
153
+
154
+ def __init__(self,
155
+ available_tools: List[BaseTool],
156
+ permission_manager: PermissionManager,
157
+ ctx: MCPContext,
158
+ model: str = "model://anthropic/claude-3-5-sonnet-20241022",
159
+ **kwargs):
160
+ """Initialize MCP agent."""
161
+ super().__init__(model=model, **kwargs)
162
+
163
+ self.available_tools = available_tools
164
+ self.permission_manager = permission_manager
165
+ self.ctx = ctx
166
+
167
+ # Register MCP tools as agent tools
168
+ for mcp_tool in available_tools:
169
+ adapter = MCPToolAdapter(mcp_tool, ctx)
170
+ self.register_tool(adapter)
171
+
172
+ async def run(self, state: MCPAgentState, history: History, network: Network) -> InferenceResult:
173
+ """Execute the agent."""
174
+ # Get current prompt
175
+ if state.current_prompt_index >= len(state.prompts):
176
+ return InferenceResult(
177
+ agent=self.name,
178
+ content="All prompts completed",
179
+ metadata={"completed": True}
180
+ )
181
+
182
+ prompt = state.prompts[state.current_prompt_index]
183
+
184
+ # Execute with tools
185
+ messages = [
186
+ {"role": "system", "content": self._get_system_prompt()},
187
+ {"role": "user", "content": prompt}
188
+ ]
189
+
190
+ # Add history context
191
+ for entry in history[-10:]:
192
+ if entry.role == "assistant":
193
+ messages.append({
194
+ "role": "assistant",
195
+ "content": entry.content
196
+ })
197
+ elif entry.role == "user":
198
+ messages.append({
199
+ "role": "user",
200
+ "content": entry.content
201
+ })
202
+
203
+ # Call model
204
+ from hanzo_agents import ModelRegistry
205
+ adapter = ModelRegistry.get_adapter(self.model)
206
+ response = await adapter.chat(messages)
207
+
208
+ # Update state
209
+ state.current_prompt_index += 1
210
+ state.results.append(response)
211
+
212
+ # Return result
213
+ return InferenceResult(
214
+ agent=self.name,
215
+ content=response,
216
+ metadata={
217
+ "prompt_index": state.current_prompt_index - 1,
218
+ "total_prompts": len(state.prompts)
219
+ }
220
+ )
221
+
222
+ def _get_system_prompt(self) -> str:
223
+ """Get system prompt for the agent."""
224
+ tool_descriptions = []
225
+ for tool in self.tools.values():
226
+ tool_descriptions.append(f"- {tool.name}: {tool.description}")
227
+
228
+ return f"""You are an AI assistant with access to the following tools:
229
+
230
+ {chr(10).join(tool_descriptions)}
231
+
232
+ When you need to use a tool, respond with:
233
+ TOOL: tool_name(arg1="value1", arg2="value2")
234
+
235
+ Important guidelines:
236
+ - Always include absolute paths starting with / when working with files
237
+ - Be thorough in your searches and analysis
238
+ - Provide clear, actionable results
239
+ - Edit files when requested to make changes
240
+ """
241
+
242
+
243
+ @final
244
+ class AgentTool(AgentClarificationMixin, BaseTool):
245
+ """Tool for delegating tasks to sub-agents using hanzo-agents SDK."""
246
+
247
+ @property
248
+ @override
249
+ def name(self) -> str:
250
+ """Get the tool name."""
251
+ return "agent"
252
+
253
+ @property
254
+ @override
255
+ def description(self) -> str:
256
+ """Get the tool description."""
257
+ if not HANZO_AGENTS_AVAILABLE:
258
+ return "Agent tool (hanzo-agents SDK not available - using fallback)"
259
+
260
+ at = [t.name for t in self.available_tools]
261
+ return f"""Launch a new agent that has access to the following tools: {at}.
262
+
263
+ When to use the Agent tool:
264
+ - If you are searching for a keyword like "config" or "logger"
265
+ - When you need to perform edits across multiple files
266
+ - When you need to delegate complex file modification tasks
267
+
268
+ When NOT to use the Agent tool:
269
+ - If you want to read a specific file path
270
+ - If you are searching for a specific class definition
271
+ - Writing code and running bash commands
272
+ - Other tasks that are not related to searching
273
+
274
+ Usage notes:
275
+ 1. Launch multiple agents concurrently whenever possible
276
+ 2. Agent results are not visible to the user - summarize them
277
+ 3. Each agent invocation is stateless
278
+ 4. The agent's outputs should generally be trusted
279
+ 5. Clearly tell the agent whether you expect it to write code or just do research"""
280
+
281
+ def __init__(
282
+ self,
283
+ permission_manager: PermissionManager,
284
+ model: str | None = None,
285
+ api_key: str | None = None,
286
+ base_url: str | None = None,
287
+ max_tokens: int | None = None,
288
+ max_iterations: int = 10,
289
+ max_tool_uses: int = 30,
290
+ ) -> None:
291
+ """Initialize the agent tool."""
292
+ self.permission_manager = permission_manager
293
+ self.model_override = model
294
+ self.api_key_override = api_key
295
+ self.base_url_override = base_url
296
+ self.max_tokens_override = max_tokens
297
+ self.max_iterations = max_iterations
298
+ self.max_tool_uses = max_tool_uses
299
+
300
+ # Set up available tools
301
+ self.available_tools: list[BaseTool] = []
302
+ self.available_tools.extend(
303
+ get_read_only_filesystem_tools(self.permission_manager)
304
+ )
305
+ self.available_tools.extend(
306
+ get_read_only_jupyter_tools(self.permission_manager)
307
+ )
308
+
309
+ # Add edit tools
310
+ self.available_tools.append(Edit(self.permission_manager))
311
+ self.available_tools.append(MultiEdit(self.permission_manager))
312
+
313
+ # Add special tools
314
+ self.available_tools.append(ClarificationTool())
315
+ self.available_tools.append(CriticTool())
316
+ self.available_tools.append(ReviewTool())
317
+ self.available_tools.append(IChingTool())
318
+
319
+ self.available_tools.append(
320
+ BatchTool({t.name: t for t in self.available_tools})
321
+ )
322
+
323
+ @override
324
+ async def call(
325
+ self,
326
+ ctx: MCPContext,
327
+ **params: Unpack[AgentToolParams],
328
+ ) -> str:
329
+ """Execute the tool with the given parameters."""
330
+ start_time = time.time()
331
+
332
+ # Create tool context
333
+ tool_ctx = create_tool_context(ctx)
334
+ await tool_ctx.set_tool_info(self.name)
335
+
336
+ # Extract parameters
337
+ prompts = params.get("prompts")
338
+ if prompts is None:
339
+ await tool_ctx.error("No prompts provided")
340
+ return "Error: At least one prompt must be provided."
341
+
342
+ # Handle both string and list inputs
343
+ if isinstance(prompts, str):
344
+ prompt_list = [prompts]
345
+ elif isinstance(prompts, list):
346
+ if not prompts:
347
+ await tool_ctx.error("Empty prompts list provided")
348
+ return "Error: At least one prompt must be provided."
349
+ prompt_list = prompts
350
+ else:
351
+ await tool_ctx.error("Invalid prompts parameter type")
352
+ return "Error: Parameter 'prompts' must be a string or list of strings."
353
+
354
+ # Validate absolute paths
355
+ absolute_path_pattern = r"/(?:[^/\s]+/)*[^/\s]+"
356
+ for prompt in prompt_list:
357
+ if not re.search(absolute_path_pattern, prompt):
358
+ await tool_ctx.error(f"Prompt missing absolute path: {prompt[:50]}...")
359
+ return "Error: All prompts must contain at least one absolute path."
360
+
361
+ # Check if hanzo-agents is available
362
+ if not HANZO_AGENTS_AVAILABLE:
363
+ # Fall back to original implementation
364
+ await tool_ctx.warning("hanzo-agents SDK not available, using fallback")
365
+ from hanzo_mcp.tools.agent.agent_tool import AgentTool as OriginalAgentTool
366
+ original_tool = OriginalAgentTool(
367
+ permission_manager=self.permission_manager,
368
+ model=self.model_override,
369
+ api_key=self.api_key_override,
370
+ base_url=self.base_url_override,
371
+ max_tokens=self.max_tokens_override,
372
+ max_iterations=self.max_iterations,
373
+ max_tool_uses=self.max_tool_uses,
374
+ )
375
+ return await original_tool.call(ctx, prompts=prompts)
376
+
377
+ # Use hanzo-agents SDK
378
+ await tool_ctx.info(f"Launching {len(prompt_list)} agent(s) using hanzo-agents SDK")
379
+
380
+ # Determine model and agent type
381
+ model = params.get("model", self.model_override)
382
+ use_memory = params.get("use_memory", False)
383
+ memory_backend = params.get("memory_backend", "sqlite")
384
+
385
+ # Get appropriate agent class
386
+ agent_class = self._get_agent_class(model)
387
+
388
+ # Create state
389
+ state = MCPAgentState(
390
+ prompts=prompt_list,
391
+ context={
392
+ "permission_manager": self.permission_manager,
393
+ "api_key": self.api_key_override,
394
+ "base_url": self.base_url_override,
395
+ "max_tokens": self.max_tokens_override,
396
+ }
397
+ )
398
+
399
+ # Create memory if requested
400
+ memory_kv = None
401
+ memory_vector = None
402
+ if use_memory:
403
+ memory_kv = create_memory_kv(memory_backend)
404
+ memory_vector = create_memory_vector("simple")
405
+
406
+ # Create network
407
+ network = Network(
408
+ state=state,
409
+ agents=[agent_class],
410
+ router=sequential_router([agent_class] * len(prompt_list)),
411
+ memory_kv=memory_kv,
412
+ memory_vector=memory_vector,
413
+ max_steps=self.max_iterations * len(prompt_list),
414
+ )
415
+
416
+ # Execute
417
+ try:
418
+ final_state = await network.run()
419
+ execution_time = time.time() - start_time
420
+
421
+ # Format results
422
+ results = final_state.results
423
+ if len(results) == 1:
424
+ formatted_result = f"""Agent execution completed in {execution_time:.2f} seconds.
425
+
426
+ AGENT RESPONSE:
427
+ {results[0]}"""
428
+ else:
429
+ formatted_results = []
430
+ for i, result in enumerate(results):
431
+ formatted_results.append(f"Agent {i+1} Result:\n{result}")
432
+
433
+ formatted_result = f"""Multi-agent execution completed in {execution_time:.2f} seconds ({len(results)} agents).
434
+
435
+ AGENT RESPONSES:
436
+ {chr(10).join(formatted_results)}"""
437
+
438
+ await tool_ctx.info(f"Execution completed in {execution_time:.2f}s")
439
+ return formatted_result
440
+
441
+ except Exception as e:
442
+ await tool_ctx.error(f"Agent execution failed: {str(e)}")
443
+ return f"Error: {str(e)}"
444
+
445
+ def _get_agent_class(self, model: Optional[str]) -> type[Agent]:
446
+ """Get appropriate agent class based on model."""
447
+ if not model:
448
+ model = "model://anthropic/claude-3-5-sonnet-20241022"
449
+
450
+ # Check for CLI agents
451
+ cli_agents = {
452
+ "claude_cli": ClaudeCodeAgent,
453
+ "codex_cli": OpenAICodexAgent,
454
+ "gemini_cli": GeminiAgent,
455
+ "grok_cli": GrokAgent,
456
+ }
457
+
458
+ if model in cli_agents:
459
+ return cli_agents[model]
460
+
461
+ # Return generic MCP agent
462
+ return type("DynamicMCPAgent", (MCPAgent,), {
463
+ "model": model,
464
+ "__init__": lambda self: MCPAgent.__init__(
465
+ self,
466
+ available_tools=self.available_tools,
467
+ permission_manager=self.permission_manager,
468
+ ctx=self.ctx,
469
+ model=model
470
+ )
471
+ })
472
+
473
+ @override
474
+ def register(self, mcp_server: FastMCP) -> None:
475
+ """Register this agent tool with the MCP server."""
476
+ tool_self = self
477
+
478
+ @mcp_server.tool(name=self.name, description=self.description)
479
+ async def dispatch_agent(
480
+ prompts: str | list[str],
481
+ ctx: MCPContext,
482
+ model: Optional[str] = None,
483
+ use_memory: bool = False,
484
+ memory_backend: str = "sqlite"
485
+ ) -> str:
486
+ return await tool_self.call(
487
+ ctx,
488
+ prompts=prompts,
489
+ model=model,
490
+ use_memory=use_memory,
491
+ memory_backend=memory_backend
492
+ )