hanzo-mcp 0.8.3__py3-none-any.whl → 0.8.5__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/__init__.py CHANGED
@@ -28,4 +28,4 @@ if os.environ.get("HANZO_MCP_TRANSPORT") == "stdio":
28
28
  except ImportError:
29
29
  pass
30
30
 
31
- __version__ = "0.7.7"
31
+ __version__ = "0.8.4"
hanzo_mcp/cli.py CHANGED
@@ -1,4 +1,10 @@
1
- """Command-line interface for the Hanzo AI server."""
1
+ """Command-line interface for the Hanzo AI server.
2
+
3
+ This module intentionally defers heavy imports (like the server and its
4
+ dependencies) until after we determine the transport and configure logging.
5
+ This prevents any stdout/stderr noise from imports that would corrupt the
6
+ MCP stdio transport used by Claude Desktop and other MCP clients.
7
+ """
2
8
 
3
9
  import os
4
10
  import sys
@@ -9,52 +15,62 @@ import argparse
9
15
  from typing import Any, cast
10
16
  from pathlib import Path
11
17
 
12
- from hanzo_mcp.server import HanzoMCPServer
13
-
14
18
 
15
19
  def main() -> None:
16
20
  """Run the CLI for the Hanzo AI server."""
17
-
18
- # Pre-parse arguments to check transport type early
19
- import sys
20
-
21
+ # Pre-parse arguments to check transport type early, BEFORE importing server
21
22
  early_parser = argparse.ArgumentParser(add_help=False)
22
23
  early_parser.add_argument("--transport", choices=["stdio", "sse"], default="stdio")
23
24
  early_args, _ = early_parser.parse_known_args()
24
25
 
25
26
  # Configure logging VERY early based on transport
27
+ suppress_stdout = False
28
+ original_stdout = sys.stdout
26
29
  if early_args.transport == "stdio":
27
- # Set environment variable for server to detect stdio mode
28
- import os
29
-
30
+ # Set environment variable for server to detect stdio mode as early as possible
30
31
  os.environ["HANZO_MCP_TRANSPORT"] = "stdio"
32
+ # Aggressively quiet common dependency loggers/warnings in stdio mode
33
+ os.environ.setdefault("PYTHONWARNINGS", "ignore")
34
+ os.environ.setdefault("LITELLM_LOG", "ERROR")
35
+ os.environ.setdefault("LITELLM_LOGGING_LEVEL", "ERROR")
36
+ os.environ.setdefault("FASTMCP_LOG_LEVEL", "ERROR")
31
37
 
32
- # For stdio transport, disable ALL logging immediately
33
- from fastmcp.utilities.logging import configure_logging
38
+ # Suppress FastMCP logging (if available) and all standard logging
39
+ try:
40
+ from fastmcp.utilities.logging import configure_logging # type: ignore
34
41
 
35
- # Set to ERROR to suppress INFO/WARNING messages from FastMCP
36
- configure_logging(level="ERROR")
42
+ configure_logging(level="ERROR")
43
+ except Exception:
44
+ pass
37
45
 
38
- # Also configure standard logging to ERROR level
39
46
  logging.basicConfig(
40
47
  level=logging.ERROR, # Only show errors
41
48
  handlers=[], # No handlers for stdio to prevent protocol corruption
42
49
  )
43
50
 
44
51
  # Redirect stderr to devnull for stdio transport to prevent any output
45
- import sys
46
-
47
52
  sys.stderr = open(os.devnull, "w")
48
53
 
49
- from hanzo_mcp import __version__
54
+ # Suppress stdout during potentially noisy imports unless user requested help/version
55
+ if not any(flag in sys.argv for flag in ("--version", "-h", "--help")):
56
+ sys.stdout = open(os.devnull, "w")
57
+ suppress_stdout = True
58
+
59
+ # Import the server only AFTER transport/logging have been configured to avoid import-time noise
60
+ from hanzo_mcp.server import HanzoMCPServer
61
+
62
+ # Avoid importing hanzo_mcp package just to get version (it can have side-effects).
63
+ try:
64
+ from importlib.metadata import version as _pkg_version # py3.8+
65
+ _version = _pkg_version("hanzo-mcp")
66
+ except Exception:
67
+ _version = "unknown"
50
68
 
51
69
  parser = argparse.ArgumentParser(
52
70
  description="MCP server implementing Hanzo AI capabilities"
53
71
  )
54
72
 
55
- parser.add_argument(
56
- "--version", action="version", version=f"hanzo-mcp {__version__}"
57
- )
73
+ parser.add_argument("--version", action="version", version=f"hanzo-mcp {_version}")
58
74
 
59
75
  _ = parser.add_argument(
60
76
  "--transport",
@@ -199,6 +215,14 @@ def main() -> None:
199
215
 
200
216
  args = parser.parse_args()
201
217
 
218
+ # Restore stdout after parsing, before any explicit output or server start
219
+ if suppress_stdout:
220
+ try:
221
+ sys.stdout.close() # Close devnull handle
222
+ except Exception:
223
+ pass
224
+ sys.stdout = original_stdout
225
+
202
226
  # Cast args attributes to appropriate types to avoid 'Any' warnings
203
227
  name: str = cast(str, args.name)
204
228
  install: bool = cast(bool, args.install)
@@ -10,13 +10,12 @@ import os
10
10
  import asyncio
11
11
  import logging
12
12
  from abc import ABC, abstractmethod
13
- from typing import Any, Dict, List, Optional, Protocol, TypeVar, Generic, runtime_checkable
14
- from dataclasses import dataclass, field
15
- from datetime import datetime
13
+ from typing import Any, Dict, List, Generic, TypeVar, Optional, Protocol, runtime_checkable
16
14
  from pathlib import Path
15
+ from datetime import datetime
16
+ from dataclasses import field, dataclass
17
17
 
18
- from .model_registry import registry, ModelConfig
19
-
18
+ from .model_registry import ModelConfig, registry
20
19
 
21
20
  logger = logging.getLogger(__name__)
22
21
 
@@ -8,9 +8,9 @@ Thread-safe singleton implementation.
8
8
  from __future__ import annotations
9
9
 
10
10
  import threading
11
- from dataclasses import dataclass, field
12
- from typing import Dict, List, Optional, Set, Any
13
11
  from enum import Enum
12
+ from typing import Any, Set, Dict, List, Optional
13
+ from dataclasses import field, dataclass
14
14
 
15
15
 
16
16
  class ModelProvider(Enum):
@@ -8,25 +8,25 @@ from mcp.server import FastMCP
8
8
 
9
9
  from hanzo_mcp.tools.common.base import BaseTool, ToolRegistry
10
10
 
11
- # Import the main implementations (using hanzo-agents SDK)
12
- from hanzo_mcp.tools.agent.agent_tool import AgentTool
13
- from hanzo_mcp.tools.agent.swarm_tool import SwarmTool
14
- from hanzo_mcp.tools.agent.network_tool import NetworkTool
15
- from hanzo_mcp.tools.common.permissions import PermissionManager
16
11
  # Import unified CLI tools (single source of truth)
17
12
  from hanzo_mcp.tools.agent.cli_tools import (
18
- ClaudeCLITool,
19
- ClaudeCodeCLITool, # cc alias
13
+ GrokCLITool,
14
+ AiderCLITool,
15
+ ClineCLITool,
20
16
  CodexCLITool,
17
+ ClaudeCLITool,
21
18
  GeminiCLITool,
22
- GrokCLITool,
19
+ HanzoDevCLITool,
23
20
  OpenHandsCLITool,
21
+ ClaudeCodeCLITool, # cc alias
24
22
  OpenHandsShortCLITool, # oh alias
25
- HanzoDevCLITool,
26
- ClineCLITool,
27
- AiderCLITool,
28
23
  register_cli_tools,
29
24
  )
25
+
26
+ # Import the main implementations (using hanzo-agents SDK)
27
+ from hanzo_mcp.tools.agent.agent_tool import AgentTool
28
+ from hanzo_mcp.tools.agent.network_tool import NetworkTool
29
+ from hanzo_mcp.tools.common.permissions import PermissionManager
30
30
  from hanzo_mcp.tools.agent.code_auth_tool import CodeAuthTool
31
31
 
32
32
 
@@ -67,16 +67,48 @@ def register_agent_tools(
67
67
  max_tool_uses=agent_max_tool_uses,
68
68
  )
69
69
 
70
- # Create swarm tool
71
- swarm_tool = SwarmTool(
72
- permission_manager=permission_manager,
73
- model=agent_model,
74
- api_key=agent_api_key,
75
- base_url=agent_base_url,
76
- max_tokens=agent_max_tokens,
77
- agent_max_iterations=agent_max_iterations,
78
- agent_max_tool_uses=agent_max_tool_uses,
79
- )
70
+ # Register a swarm alias that forwards to AgentTool with default concurrency
71
+ class SwarmAliasTool(BaseTool):
72
+ name = "swarm"
73
+ description = (
74
+ "Alias for agent with concurrency. swarm == agent:5 by default.\n"
75
+ "Use 'swarm' for parallel multi-agent runs; 'swarm:N' for N agents."
76
+ )
77
+
78
+ def __init__(self, agent_tool: AgentTool):
79
+ self._agent = agent_tool
80
+
81
+ async def call(self, ctx, **params): # type: ignore[override]
82
+ # Default to 5 agents unless explicitly provided
83
+ params = dict(params)
84
+ params.setdefault("concurrency", 5)
85
+ return await self._agent.call(ctx, **params)
86
+
87
+ def register(self, mcp_server: FastMCP): # type: ignore[override]
88
+ tool_self = self
89
+
90
+ @mcp_server.tool(name=self.name, description=self.description)
91
+ async def swarm(
92
+ ctx,
93
+ prompts: str | list[str], # forwarded
94
+ concurrency: int | None = None,
95
+ model: str | None = None,
96
+ use_memory: bool | None = None,
97
+ memory_backend: str | None = None,
98
+ ) -> str:
99
+ p = {
100
+ "prompts": prompts,
101
+ }
102
+ if concurrency is not None:
103
+ p["concurrency"] = concurrency
104
+ if model is not None:
105
+ p["model"] = model
106
+ if use_memory is not None:
107
+ p["use_memory"] = use_memory
108
+ if memory_backend is not None:
109
+ p["memory_backend"] = memory_backend
110
+ return await tool_self.call(ctx, **p)
111
+ return tool_self
80
112
 
81
113
  # Create auth management tool
82
114
  code_auth_tool = CodeAuthTool()
@@ -89,7 +121,7 @@ def register_agent_tools(
89
121
 
90
122
  # Register core agent tools
91
123
  ToolRegistry.register_tool(mcp_server, agent_tool)
92
- ToolRegistry.register_tool(mcp_server, swarm_tool)
124
+ ToolRegistry.register_tool(mcp_server, SwarmAliasTool(agent_tool))
93
125
  ToolRegistry.register_tool(mcp_server, network_tool)
94
126
  ToolRegistry.register_tool(mcp_server, code_auth_tool)
95
127
 
@@ -97,9 +129,4 @@ def register_agent_tools(
97
129
  cli_tools = register_cli_tools(mcp_server, permission_manager)
98
130
 
99
131
  # Return list of registered tools
100
- return [
101
- agent_tool,
102
- swarm_tool,
103
- network_tool,
104
- code_auth_tool,
105
- ] + cli_tools
132
+ return [agent_tool, network_tool, code_auth_tool] + cli_tools
@@ -109,6 +109,7 @@ class AgentToolParams(TypedDict, total=False):
109
109
  model: Optional[str]
110
110
  use_memory: Optional[bool]
111
111
  memory_backend: Optional[str]
112
+ concurrency: Optional[int]
112
113
 
113
114
 
114
115
  class MCPAgentState(State):
@@ -387,7 +388,17 @@ Usage notes:
387
388
  await tool_ctx.error("hanzo-agents SDK is required but not available")
388
389
  return "Error: hanzo-agents SDK is required for agent tool functionality. Please install it with: pip install hanzo-agents"
389
390
 
390
- # Use hanzo-agents SDK
391
+ # Determine concurrency (parallel agents)
392
+ concurrency = params.get("concurrency")
393
+ if concurrency is not None and isinstance(concurrency, int) and concurrency > 0:
394
+ # Expand prompt list to match concurrency
395
+ if len(prompt_list) == 1:
396
+ prompt_list = prompt_list * concurrency
397
+ elif len(prompt_list) < concurrency:
398
+ # Repeat prompts to reach concurrency
399
+ times = (concurrency + len(prompt_list) - 1) // len(prompt_list)
400
+ prompt_list = (prompt_list * times)[:concurrency]
401
+
391
402
  await tool_ctx.info(
392
403
  f"Launching {len(prompt_list)} agent(s) using hanzo-agents SDK"
393
404
  )
@@ -8,7 +8,7 @@ from __future__ import annotations
8
8
 
9
9
  import os
10
10
  import asyncio
11
- from typing import Any, Optional, Dict, List, Unpack, Annotated, TypedDict, override, final
11
+ from typing import Any, Dict, List, Unpack, Optional, Annotated, TypedDict, final, override
12
12
  from pathlib import Path
13
13
 
14
14
  from pydantic import Field
@@ -19,7 +19,6 @@ from hanzo_mcp.tools.common.base import BaseTool
19
19
  from hanzo_mcp.tools.common.context import create_tool_context
20
20
  from hanzo_mcp.tools.common.permissions import PermissionManager
21
21
 
22
-
23
22
  # Parameter types for CLI tools
24
23
  Prompt = Annotated[
25
24
  str,
@@ -177,37 +177,17 @@ class NetworkTool(BaseTool):
177
177
  results["error"] = f"Local execution failed: {str(e)}"
178
178
  return json.dumps(results, indent=2)
179
179
 
180
- # Fallback to agent-based execution
181
- # This would use hanzo-agents or the existing swarm implementation
182
- if not results["success"] or mode in ["distributed", "hybrid"]:
183
- # Import swarm tool as fallback
184
- from hanzo_mcp.tools.agent.swarm_tool import SwarmTool
185
-
186
- # Create temporary swarm tool
187
- swarm = SwarmTool(
188
- permission_manager=self.permission_manager, model=model_pref
189
- )
190
-
191
- # Convert network params to swarm params
192
- swarm_params = {
193
- "prompts": [task] if not agents_list else agents_list,
194
- "consensus": routing == "consensus",
195
- "parallel": routing == "parallel",
196
- }
197
-
198
- # Execute via swarm
199
- swarm_result = await swarm.call(ctx, **swarm_params)
200
- swarm_data = json.loads(swarm_result)
201
-
202
- # Merge results
203
- if swarm_data.get("success"):
204
- results["agents_used"].extend(
205
- [r["agent"] for r in swarm_data.get("results", [])]
206
- )
207
- results["results"].extend(swarm_data.get("results", []))
180
+ # Agent-based execution with concurrency
181
+ if not results["success"] or mode in ["distributed", "hybrid"]:
182
+ from hanzo_mcp.tools.agent.agent_tool import AgentTool
183
+ agent = AgentTool(permission_manager=self.permission_manager, model=model_pref)
184
+ concurrency = max(1, len(agents_list)) if agents_list else 5 if routing == "parallel" else 1
185
+ agent_params = {"prompts": task, "concurrency": concurrency}
186
+ agent_result = await agent.call(ctx, **agent_params)
187
+ # Wrap agent_result as a simple result list
188
+ results["agents_used"].append("agent")
189
+ results["results"].append({"agent": "agent", "response": agent_result})
208
190
  results["success"] = True
209
- else:
210
- results["error"] = swarm_data.get("error", "Unknown error")
211
191
 
212
192
  except Exception as e:
213
193
  results["error"] = str(e)
@@ -260,28 +240,4 @@ class NetworkTool(BaseTool):
260
240
  return tool
261
241
 
262
242
 
263
- # Alias swarm to use network tool with local-only mode
264
- @final
265
- class LocalSwarmTool(NetworkTool):
266
- """Local-only version of the network tool (swarm compatibility).
267
-
268
- This provides backward compatibility with the swarm tool
269
- while using local compute resources only.
270
- """
271
-
272
- name = "swarm"
273
- description = "Run agent swarms locally using hanzo-miner compute"
274
-
275
- def __init__(self, permission_manager: PermissionManager, **kwargs):
276
- """Initialize as local-only network."""
277
- super().__init__(
278
- permission_manager=permission_manager, default_mode="local", **kwargs
279
- )
280
-
281
- @override
282
- async def call(self, ctx: MCPContext, **params: Unpack[NetworkToolParams]) -> str:
283
- """Execute with local-only mode."""
284
- # Force local mode
285
- params["mode"] = "local"
286
- params["require_local"] = True
287
- return await super().call(ctx, **params)
243
+ # Remove swarm compatibility tool; swarm is an alias of agent with concurrency
@@ -7,16 +7,16 @@ following Python best practices and eliminating all duplication.
7
7
  from __future__ import annotations
8
8
 
9
9
  import os
10
- from typing import Any, Optional, List, Dict
10
+ from typing import Any, Dict, List, Optional
11
11
  from pathlib import Path
12
12
 
13
13
  from mcp.server import FastMCP
14
14
  from mcp.server.fastmcp import Context
15
15
 
16
- from ...core.model_registry import registry
17
- from ...core.base_agent import CLIAgent, AgentConfig
18
16
  from ..common.base import BaseTool
17
+ from ...core.base_agent import CLIAgent, AgentConfig
19
18
  from ..common.permissions import PermissionManager
19
+ from ...core.model_registry import registry
20
20
 
21
21
 
22
22
  class UnifiedCLITool(BaseTool, CLIAgent):
@@ -9,10 +9,10 @@ from pathlib import Path
9
9
  from pydantic import Field
10
10
  from mcp.server.fastmcp import Context as MCPContext
11
11
 
12
+ from hanzo_mcp.config import load_settings, save_settings
12
13
  from hanzo_mcp.tools.common.base import BaseTool
13
14
  from hanzo_mcp.tools.common.permissions import PermissionManager
14
15
  from hanzo_mcp.tools.config.index_config import IndexScope, IndexConfig
15
- from hanzo_mcp.config import load_settings, save_settings
16
16
 
17
17
  # Parameter types
18
18
  Action = Annotated[
@@ -223,7 +223,7 @@ class UnifiedLLMTool(BaseTool):
223
223
  try:
224
224
  with open(self.CONFIG_FILE, "r") as f:
225
225
  return json.load(f)
226
- except:
226
+ except Exception:
227
227
  pass
228
228
 
229
229
  # Default config
@@ -276,7 +276,7 @@ Available: {', '.join(available) if available else 'None'}"""
276
276
  tool_ctx = create_tool_context(ctx)
277
277
  if tool_ctx:
278
278
  await tool_ctx.set_tool_info(self.name)
279
- except:
279
+ except Exception:
280
280
  # Running in test mode without MCP context
281
281
  pass
282
282
 
@@ -9,6 +9,7 @@ from hanzo_mcp.tools.shell.open import open_tool
9
9
  from hanzo_mcp.tools.common.base import BaseTool, ToolRegistry
10
10
  from hanzo_mcp.tools.shell.npx_tool import npx_tool
11
11
  from hanzo_mcp.tools.shell.uvx_tool import uvx_tool
12
+ from hanzo_mcp.tools.shell.zsh_tool import zsh_tool, shell_tool
12
13
 
13
14
  # Import tools
14
15
  from hanzo_mcp.tools.shell.bash_tool import bash_tool
@@ -37,14 +38,19 @@ def get_shell_tools(
37
38
  """
38
39
  # Set permission manager for tools that need it
39
40
  bash_tool.permission_manager = permission_manager
41
+ zsh_tool.permission_manager = permission_manager
42
+ shell_tool.permission_manager = permission_manager
40
43
  npx_tool.permission_manager = permission_manager
41
44
  uvx_tool.permission_manager = permission_manager
42
45
 
43
46
  # Note: StreamingCommandTool is abstract and shouldn't be instantiated directly
44
47
  # It's used as a base class for other streaming tools
45
48
 
49
+ # Return shell_tool first (smart default), then specific shells
46
50
  return [
47
- bash_tool,
51
+ shell_tool, # Smart shell (prefers zsh if available)
52
+ zsh_tool, # Explicit zsh
53
+ bash_tool, # Explicit bash
48
54
  npx_tool,
49
55
  uvx_tool,
50
56
  process_tool,
@@ -59,29 +59,20 @@ bash "npm run dev" --cwd ./frontend # Auto-backgrounds if needed"""
59
59
 
60
60
  @override
61
61
  def get_interpreter(self) -> str:
62
- """Get the shell interpreter."""
62
+ """Get the bash interpreter."""
63
63
  if platform.system() == "Windows":
64
- return "cmd.exe"
65
-
66
- # Check for user's preferred shell from environment
67
- shell = os.environ.get("SHELL", "/bin/bash")
68
-
69
- # Extract just the shell name from the path
70
- shell_name = os.path.basename(shell)
71
-
72
- # Check if it's a supported shell and the config file exists
73
- if shell_name == "zsh":
74
- # Check for .zshrc
75
- zshrc_path = Path.home() / ".zshrc"
76
- if zshrc_path.exists():
77
- return shell # Use full path to zsh
78
- elif shell_name == "fish":
79
- # Check for fish config
80
- fish_config = Path.home() / ".config" / "fish" / "config.fish"
81
- if fish_config.exists():
82
- return shell # Use full path to fish
83
-
84
- # Default to bash if no special shell config found
64
+ # Try to find bash on Windows (Git Bash, WSL, etc.)
65
+ bash_paths = [
66
+ "C:\\Program Files\\Git\\bin\\bash.exe",
67
+ "C:\\cygwin64\\bin\\bash.exe",
68
+ "C:\\msys64\\usr\\bin\\bash.exe",
69
+ ]
70
+ for path in bash_paths:
71
+ if Path(path).exists():
72
+ return path
73
+ return "cmd.exe" # Fall back to cmd if no bash found
74
+
75
+ # On Unix-like systems, always use bash
85
76
  return "bash"
86
77
 
87
78
  @override
@@ -94,12 +85,7 @@ bash "npm run dev" --cwd ./frontend # Auto-backgrounds if needed"""
94
85
  @override
95
86
  def get_tool_name(self) -> str:
96
87
  """Get the tool name."""
97
- if platform.system() == "Windows":
98
- return "shell"
99
-
100
- # Return the actual shell being used
101
- interpreter = self.get_interpreter()
102
- return os.path.basename(interpreter)
88
+ return "bash"
103
89
 
104
90
  @override
105
91
  async def run(