hanzo-mcp 0.2.0__py3-none-any.whl → 0.3.3__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
@@ -1,3 +1,3 @@
1
1
  """Hanzo MCP - Implementation of Hanzo capabilities using MCP."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.3.3"
hanzo_mcp/cli.py CHANGED
@@ -1,20 +1,42 @@
1
- """Command-line interface for the Hanzo MCP server."""
1
+ """Command-line interface for the Hanzo MCP server.
2
+
3
+ Includes logging configuration and enhanced error handling.
4
+ """
2
5
 
3
6
  import argparse
4
7
  import json
8
+ import logging
5
9
  import os
6
10
  import sys
7
11
  from pathlib import Path
8
12
  from typing import Any, cast
9
13
 
14
+ from hanzo_mcp import __version__
15
+ from hanzo_mcp.tools.common.logging_config import setup_logging
16
+
10
17
  from hanzo_mcp.server import HanzoServer
11
18
 
12
19
 
13
20
  def main() -> None:
14
21
  """Run the CLI for the Hanzo MCP server."""
22
+ # Initialize logger
23
+ logger = logging.getLogger(__name__)
24
+
25
+ # Check if 'version' is the first argument
26
+ if len(sys.argv) > 1 and sys.argv[1] == 'version':
27
+ print(f"hanzo-mcp {__version__}")
28
+ return
29
+
15
30
  parser = argparse.ArgumentParser(
16
31
  description="MCP server implementing Hanzo capabilities"
17
32
  )
33
+
34
+ parser.add_argument(
35
+ "--version",
36
+ action="version",
37
+ version=f"%(prog)s {__version__}",
38
+ help="Show the current version and exit"
39
+ )
18
40
 
19
41
  _ = parser.add_argument(
20
42
  "--transport",
@@ -23,6 +45,19 @@ def main() -> None:
23
45
  help="Transport protocol to use (default: stdio)",
24
46
  )
25
47
 
48
+ _ = parser.add_argument(
49
+ "--port",
50
+ type=int,
51
+ default=3001,
52
+ help="Port to use for SSE transport (default: 3001)",
53
+ )
54
+
55
+ _ = parser.add_argument(
56
+ "--host",
57
+ default="0.0.0.0",
58
+ help="Host to bind to for SSE transport (default: 0.0.0.0)",
59
+ )
60
+
26
61
  _ = parser.add_argument(
27
62
  "--name",
28
63
  default="claude-code",
@@ -39,26 +74,26 @@ def main() -> None:
39
74
  _ = parser.add_argument(
40
75
  "--project-dir", dest="project_dir", help="Set the project directory to analyze"
41
76
  )
42
-
77
+
43
78
  _ = parser.add_argument(
44
79
  "--agent-model",
45
80
  dest="agent_model",
46
81
  help="Specify the model name in LiteLLM format (e.g., 'openai/gpt-4o', 'anthropic/claude-3-sonnet')"
47
82
  )
48
-
83
+
49
84
  _ = parser.add_argument(
50
85
  "--agent-max-tokens",
51
86
  dest="agent_max_tokens",
52
87
  type=int,
53
88
  help="Specify the maximum tokens for agent responses"
54
89
  )
55
-
90
+
56
91
  _ = parser.add_argument(
57
92
  "--agent-api-key",
58
93
  dest="agent_api_key",
59
94
  help="Specify the API key for the LLM provider (for development/testing only)"
60
95
  )
61
-
96
+
62
97
  _ = parser.add_argument(
63
98
  "--agent-max-iterations",
64
99
  dest="agent_max_iterations",
@@ -66,7 +101,7 @@ def main() -> None:
66
101
  default=10,
67
102
  help="Maximum number of iterations for agent (default: 10)"
68
103
  )
69
-
104
+
70
105
  _ = parser.add_argument(
71
106
  "--agent-max-tool-uses",
72
107
  dest="agent_max_tool_uses",
@@ -74,7 +109,7 @@ def main() -> None:
74
109
  default=30,
75
110
  help="Maximum number of total tool uses for agent (default: 30)"
76
111
  )
77
-
112
+
78
113
  _ = parser.add_argument(
79
114
  "--enable-agent-tool",
80
115
  dest="enable_agent_tool",
@@ -82,7 +117,31 @@ def main() -> None:
82
117
  default=False,
83
118
  help="Enable the agent tool (disabled by default)"
84
119
  )
120
+
121
+ _ = parser.add_argument(
122
+ "--log-level",
123
+ dest="log_level",
124
+ choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
125
+ default="INFO",
126
+ help="Set the logging level (default: INFO)"
127
+ )
128
+
129
+ _ = parser.add_argument(
130
+ "--disable-file-logging",
131
+ dest="disable_file_logging",
132
+ action="store_true",
133
+ default=False,
134
+ help="Disable logging to file (logs to console only)"
135
+ )
85
136
 
137
+ _ = parser.add_argument(
138
+ "--enable-console-logging",
139
+ dest="enable_console_logging",
140
+ action="store_true",
141
+ default=False,
142
+ help="Enable logging to console (stdout/stderr). By default, logs to file only."
143
+ )
144
+
86
145
  _ = parser.add_argument(
87
146
  "--disable-write-tools",
88
147
  dest="disable_write_tools",
@@ -103,6 +162,8 @@ def main() -> None:
103
162
  name: str = cast(str, args.name)
104
163
  install: bool = cast(bool, args.install)
105
164
  transport: str = cast(str, args.transport)
165
+ port: int = cast(int, args.port)
166
+ host: str = cast(str, args.host)
106
167
  project_dir: str | None = cast(str | None, args.project_dir)
107
168
  agent_model: str | None = cast(str | None, args.agent_model)
108
169
  agent_max_tokens: int | None = cast(int | None, args.agent_max_tokens)
@@ -111,17 +172,37 @@ def main() -> None:
111
172
  agent_max_tool_uses: int = cast(int, args.agent_max_tool_uses)
112
173
  enable_agent_tool: bool = cast(bool, args.enable_agent_tool)
113
174
  disable_write_tools: bool = cast(bool, args.disable_write_tools)
175
+ log_level: str = cast(str, args.log_level)
176
+ disable_file_logging: bool = cast(bool, args.disable_file_logging)
177
+ enable_console_logging: bool = cast(bool, args.enable_console_logging)
114
178
  allowed_paths: list[str] = (
115
179
  cast(list[str], args.allowed_paths) if args.allowed_paths else []
116
180
  )
117
181
 
182
+ # Setup logging
183
+ # Ensure absolutely NO logging when using stdio transport to avoid protocol corruption
184
+ # For sse transport, use file logging by default and console logging only if explicitly requested
185
+ # Only set up logging if not using stdio transport or explicitly requested
186
+ if transport != "stdio" or (enable_console_logging or not disable_file_logging):
187
+ setup_logging(
188
+ log_level=log_level,
189
+ log_to_file=not disable_file_logging and transport != "stdio", # Disable file logging for stdio transport
190
+ log_to_console=enable_console_logging and transport != "stdio", # Only enable console logging if requested AND not using stdio
191
+ transport=transport, # Pass the transport to ensure it's properly handled
192
+ testing="pytest" in sys.modules
193
+ )
194
+ logger.debug(f"Hanzo MCP CLI started with arguments: {args}")
195
+ # No logging setup at all for stdio transport unless explicitly requested
196
+
197
+
118
198
  if install:
119
- install_claude_desktop_config(name, allowed_paths, disable_write_tools)
199
+ install_claude_desktop_config(name, allowed_paths, host, port)
120
200
  return
121
201
 
122
202
  # If no allowed paths are specified, use the user's home directory
123
203
  if not allowed_paths:
124
204
  allowed_paths = [str(Path.home())]
205
+ logger.info(f"No allowed paths specified, using home directory: {allowed_paths[0]}")
125
206
 
126
207
  # If project directory is specified, add it to allowed paths
127
208
  if project_dir and project_dir not in allowed_paths:
@@ -139,26 +220,48 @@ def main() -> None:
139
220
  elif allowed_paths:
140
221
  project_dir = allowed_paths[0]
141
222
 
142
- # Run the server
143
- server = HanzoServer(
144
- name=name,
145
- allowed_paths=allowed_paths,
146
- project_dir=project_dir, # Pass project_dir for initial working directory
147
- agent_model=agent_model,
148
- agent_max_tokens=agent_max_tokens,
149
- agent_api_key=agent_api_key,
150
- agent_max_iterations=agent_max_iterations,
151
- agent_max_tool_uses=agent_max_tool_uses,
152
- enable_agent_tool=enable_agent_tool,
153
- disable_write_tools=disable_write_tools
154
- )
155
- # Transport will be automatically cast to Literal['stdio', 'sse'] by the server
156
- server.run(transport=transport)
223
+ # Run the server - only log if not using stdio transport or logging is explicitly enabled
224
+ if transport != "stdio" or (enable_console_logging or not disable_file_logging):
225
+ logger.info(f"Starting Hanzo MCP server with name: {name}")
226
+ logger.debug(f"Allowed paths: {allowed_paths}")
227
+ logger.debug(f"Project directory: {project_dir}")
228
+
229
+ try:
230
+ server = HanzoServer(
231
+ name=name,
232
+ allowed_paths=allowed_paths,
233
+ project_dir=project_dir, # Pass project_dir for initial working directory
234
+ agent_model=agent_model,
235
+ agent_max_tokens=agent_max_tokens,
236
+ agent_api_key=agent_api_key,
237
+ agent_max_iterations=agent_max_iterations,
238
+ agent_max_tool_uses=agent_max_tool_uses,
239
+ enable_agent_tool=enable_agent_tool,
240
+ disable_write_tools=disable_write_tools,
241
+ host=host,
242
+ port=port
243
+ )
244
+
245
+ # Only log if not using stdio transport or logging is explicitly enabled
246
+ if transport != "stdio" or (enable_console_logging or not disable_file_logging):
247
+ logger.info(f"Server initialized successfully, running with transport: {transport}")
248
+
249
+ # Transport will be automatically cast to Literal['stdio', 'sse'] by the server
250
+ server.run(transport=transport)
251
+ except Exception as e:
252
+ # Only log if not using stdio transport or logging is explicitly enabled
253
+ if transport != "stdio" or (enable_console_logging or not disable_file_logging):
254
+ logger.error(f"Error starting server: {str(e)}")
255
+ logger.exception("Server startup failed with exception:")
256
+ # For stdio transport, we want a clean exception without any logging
257
+ # Re-raise the exception for proper error handling
258
+ raise
157
259
 
158
260
 
159
261
  def install_claude_desktop_config(
160
262
  name: str = "claude-code", allowed_paths: list[str] | None = None,
161
- disable_write_tools: bool = False
263
+ disable_write_tools: bool = False,
264
+ host: str = "0.0.0.0", port: int = 3001
162
265
  ) -> None:
163
266
  """Install the server configuration in Claude Desktop.
164
267
 
@@ -168,6 +271,8 @@ def install_claude_desktop_config(
168
271
  disable_write_tools: Whether to disable write/edit tools (file writing, editing, notebook editing)
169
272
  to use IDE tools instead. Note: Shell commands can still modify files.
170
273
  (default: False)
274
+ host: Host to bind to for SSE transport (default: '0.0.0.0')
275
+ port: Port to use for SSE transport (default: 3001)
171
276
  """
172
277
  # Find the Claude Desktop config directory
173
278
  home: Path = Path.home()
@@ -197,11 +302,19 @@ def install_claude_desktop_config(
197
302
  else:
198
303
  # Allow home directory by default
199
304
  args.extend(["--allow-path", str(home)])
200
-
305
+
306
+ # Add host and port
307
+ args.extend(["--host", host])
308
+ args.extend(["--port", str(port)])
309
+
201
310
  # Add disable_write_tools flag if specified
202
311
  if disable_write_tools:
203
312
  args.append("--disable-write-tools")
204
313
 
314
+ # Add host and port
315
+ args.extend(["--host", host])
316
+ args.extend(["--port", str(port)])
317
+
205
318
  # Create config object
206
319
  config: dict[str, Any] = {
207
320
  "mcpServers": {name: {"command": str(script_path), "args": args}}
hanzo_mcp/server.py CHANGED
@@ -1,4 +1,7 @@
1
- """MCP server implementing Hanzo capabilities."""
1
+ """MCP server implementing Hanzo capabilities.
2
+
3
+ Includes improved error handling and debugging for tool execution.
4
+ """
2
5
 
3
6
  from typing import Literal, cast, final
4
7
 
@@ -13,7 +16,10 @@ from hanzo_mcp.tools.shell.command_executor import CommandExecutor
13
16
 
14
17
  @final
15
18
  class HanzoServer:
16
- """MCP server implementing Hanzo capabilities."""
19
+ """MCP server implementing Hanzo capabilities.
20
+
21
+ Includes improved error handling and debugging for tool execution.
22
+ """
17
23
 
18
24
  def __init__(
19
25
  self,
@@ -28,6 +34,8 @@ class HanzoServer:
28
34
  agent_max_tool_uses: int = 30,
29
35
  enable_agent_tool: bool = False,
30
36
  disable_write_tools: bool = False,
37
+ host: str = "0.0.0.0",
38
+ port: int = 3001,
31
39
  ):
32
40
  """Initialize the Hanzo server.
33
41
 
@@ -43,6 +51,8 @@ class HanzoServer:
43
51
  agent_max_tool_uses: Maximum number of total tool uses for agent (default: 30)
44
52
  enable_agent_tool: Whether to enable the agent tool (default: False)
45
53
  disable_write_tools: Whether to disable write/edit tools (default: False)
54
+ host: Host to bind to for SSE transport (default: '0.0.0.0')
55
+ port: Port to use for SSE transport (default: 3001)
46
56
  """
47
57
  self.mcp = mcp_instance if mcp_instance is not None else FastMCP(name)
48
58
 
@@ -55,7 +65,7 @@ class HanzoServer:
55
65
  permission_manager=self.permission_manager,
56
66
  verbose=False, # Set to True for debugging
57
67
  )
58
-
68
+
59
69
  # If project_dir is specified, set it as initial working directory for all sessions
60
70
  if project_dir:
61
71
  initial_session_id = name # Use server name as default session ID
@@ -83,7 +93,11 @@ class HanzoServer:
83
93
  self.agent_max_tool_uses = agent_max_tool_uses
84
94
  self.enable_agent_tool = enable_agent_tool
85
95
  self.disable_write_tools = disable_write_tools
86
-
96
+
97
+ # Store network options
98
+ self.host = host
99
+ self.port = port
100
+
87
101
  # Register all tools
88
102
  register_all_tools(
89
103
  mcp_server=self.mcp,
@@ -111,6 +125,14 @@ class HanzoServer:
111
125
  self.permission_manager.add_allowed_path(path)
112
126
  self.document_context.add_allowed_path(path)
113
127
 
128
+ # If using SSE, set the port and host in the environment variables
129
+ if transport == "sse":
130
+ import os
131
+ # Set environment variables for FastMCP settings
132
+ os.environ["FASTMCP_PORT"] = str(self.port)
133
+ os.environ["FASTMCP_HOST"] = self.host
134
+ print(f"Starting SSE server on {self.host}:{self.port}")
135
+
114
136
  # Run the server
115
137
  transport_type = cast(Literal["stdio", "sse"], transport)
116
138
  self.mcp.run(transport=transport_type)
@@ -0,0 +1,73 @@
1
+ """Base model provider for agent delegation.
2
+
3
+ Defines the interface for model providers.
4
+ """
5
+
6
+ import logging
7
+ from abc import ABC, abstractmethod
8
+ from typing import Any, Dict, List, Optional, Tuple
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class BaseModelProvider(ABC):
14
+ """Base class for model providers."""
15
+
16
+ @abstractmethod
17
+ async def initialize(self) -> None:
18
+ """Initialize the provider."""
19
+ pass
20
+
21
+ @abstractmethod
22
+ async def load_model(self, model_name: str, identifier: Optional[str] = None) -> str:
23
+ """Load a model.
24
+
25
+ Args:
26
+ model_name: The name of the model to load
27
+ identifier: Optional identifier for the model instance
28
+
29
+ Returns:
30
+ The identifier for the loaded model
31
+ """
32
+ pass
33
+
34
+ @abstractmethod
35
+ async def generate(
36
+ self,
37
+ model_id: str,
38
+ prompt: str,
39
+ system_prompt: Optional[str] = None,
40
+ max_tokens: int = 4096,
41
+ temperature: float = 0.7,
42
+ top_p: float = 0.95,
43
+ stop_sequences: Optional[List[str]] = None,
44
+ ) -> Tuple[str, Dict[str, Any]]:
45
+ """Generate a response from the model.
46
+
47
+ Args:
48
+ model_id: The identifier of the model to use
49
+ prompt: The prompt to send to the model
50
+ system_prompt: Optional system prompt to send to the model
51
+ max_tokens: Maximum number of tokens to generate
52
+ temperature: Sampling temperature
53
+ top_p: Top-p sampling parameter
54
+ stop_sequences: Optional list of strings that will stop generation
55
+
56
+ Returns:
57
+ A tuple of (generated text, metadata)
58
+ """
59
+ pass
60
+
61
+ @abstractmethod
62
+ async def unload_model(self, model_id: str) -> None:
63
+ """Unload a model.
64
+
65
+ Args:
66
+ model_id: The identifier of the model to unload
67
+ """
68
+ pass
69
+
70
+ @abstractmethod
71
+ async def shutdown(self) -> None:
72
+ """Shutdown the provider."""
73
+ pass
@@ -0,0 +1,45 @@
1
+ """LiteLLM provider for agent delegation.
2
+
3
+ Enables the use of various cloud LLM providers via LiteLLM.
4
+ """
5
+
6
+ import asyncio
7
+ import logging
8
+ import json
9
+ from typing import Any, Dict, List, Optional, Tuple
10
+
11
+ from hanzo_mcp.tools.agent.base_provider import BaseModelProvider
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ # Define model capabilities
16
+ DEFAULT_MAX_TOKENS = 4096
17
+ DEFAULT_CONTEXT_WINDOW = 8192
18
+
19
+
20
+ class LiteLLMProvider(BaseModelProvider):
21
+ """Provider for cloud models via LiteLLM."""
22
+
23
+ def __init__(self):
24
+ """Initialize the LiteLLM provider."""
25
+ self.models = {}
26
+ self.initialized = False
27
+
28
+ async def initialize(self) -> None:
29
+ """Initialize the LiteLLM provider."""
30
+ if self.initialized:
31
+ return
32
+
33
+ try:
34
+ # Import LiteLLM
35
+ import litellm
36
+ self.litellm = litellm
37
+ self.initialized = True
38
+ logger.info("LiteLLM provider initialized successfully")
39
+ except ImportError:
40
+ logger.error("Failed to import LiteLLM")
41
+ logger.error("Install LiteLLM with 'pip install litellm'")
42
+ except Exception as e:
43
+ logger.error(f"Failed to initialize LiteLLM provider: {str(e)}")
44
+
45
+ async def load_model(self, model_name: str, identifier: Optional[str] = None