hanzo-mcp 0.1.21__tar.gz → 0.1.25__tar.gz

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 (36) hide show
  1. {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/PKG-INFO +2 -2
  2. {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/hanzo_mcp/__init__.py +1 -1
  3. {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/hanzo_mcp/cli.py +3 -3
  4. {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/hanzo_mcp/server.py +3 -3
  5. hanzo_mcp-0.1.25/hanzo_mcp/tools/common/__init__.py +1 -0
  6. {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/hanzo_mcp/tools/common/context.py +2 -2
  7. hanzo_mcp-0.1.25/hanzo_mcp/tools/common/permissions.py +313 -0
  8. {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/hanzo_mcp/tools/common/validation.py +1 -1
  9. {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/hanzo_mcp/tools/filesystem/__init__.py +1 -1
  10. {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/hanzo_mcp/tools/filesystem/file_operations.py +2 -2
  11. {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/hanzo_mcp/tools/jupyter/__init__.py +1 -1
  12. {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/hanzo_mcp/tools/jupyter/notebook_operations.py +1 -1
  13. hanzo_mcp-0.1.25/hanzo_mcp/tools/project/__init__.py +1 -0
  14. {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/hanzo_mcp/tools/project/analysis.py +5 -5
  15. hanzo_mcp-0.1.25/hanzo_mcp/tools/shell/__init__.py +1 -0
  16. {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/hanzo_mcp/tools/shell/command_executor.py +10 -10
  17. {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/hanzo_mcp.egg-info/PKG-INFO +2 -2
  18. {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/hanzo_mcp.egg-info/SOURCES.txt +1 -0
  19. hanzo_mcp-0.1.25/hanzo_mcp.egg-info/zip-safe +1 -0
  20. {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/pyproject.toml +8 -2
  21. {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/tests/test_cli.py +2 -2
  22. {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/tests/test_server.py +12 -12
  23. {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/tests/test_validation.py +1 -1
  24. hanzo_mcp-0.1.21/hanzo_mcp/tools/common/__init__.py +0 -1
  25. hanzo_mcp-0.1.21/hanzo_mcp/tools/common/permissions.py +0 -253
  26. hanzo_mcp-0.1.21/hanzo_mcp/tools/project/__init__.py +0 -1
  27. hanzo_mcp-0.1.21/hanzo_mcp/tools/shell/__init__.py +0 -1
  28. {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/LICENSE +0 -0
  29. {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/README.md +0 -0
  30. {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/hanzo_mcp/tools/__init__.py +0 -0
  31. {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/hanzo_mcp/tools/common/thinking.py +0 -0
  32. {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/hanzo_mcp.egg-info/dependency_links.txt +0 -0
  33. {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/hanzo_mcp.egg-info/entry_points.txt +0 -0
  34. {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/hanzo_mcp.egg-info/requires.txt +0 -0
  35. {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/hanzo_mcp.egg-info/top_level.txt +0 -0
  36. {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/setup.cfg +0 -0
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hanzo-mcp
3
- Version: 0.1.21
3
+ Version: 0.1.25
4
4
  Summary: MCP server for accessing Hanzo APIs and Platform capabilities
5
- Author-email: Hanzo Dev <dev@hanzo.ai>
5
+ Author-email: Hanzo <dev@hanzo.ai>
6
6
  License: MIT
7
7
  Classifier: Programming Language :: Python :: 3
8
8
  Classifier: License :: OSI Approved :: MIT License
@@ -1,3 +1,3 @@
1
1
  """Hanzo MCP - Implementation of Hanzo Platform capabilities using MCP."""
2
2
 
3
- __version__ = "0.1.8"
3
+ __version__ = "0.1.22"
@@ -7,11 +7,11 @@ import sys
7
7
  from pathlib import Path
8
8
  from typing import Any, cast
9
9
 
10
- from hanzo_mcp.server import HanzoDevServer
10
+ from hanzo_mcp.server import HanzoMCPServer
11
11
 
12
12
 
13
13
  def main() -> None:
14
- """Run the CLI for the Hanzo Dev MCP server."""
14
+ """Run the CLI for the Hanzo MCP server."""
15
15
  parser = argparse.ArgumentParser(
16
16
  description="MCP server for accessing Hanzo APIs and Platform capabilities"
17
17
  )
@@ -70,7 +70,7 @@ def main() -> None:
70
70
  allowed_paths.append(project_dir)
71
71
 
72
72
  # Run the server
73
- server = HanzoDevServer(name=name, allowed_paths=allowed_paths)
73
+ server = HanzoMCPServer(name=name, allowed_paths=allowed_paths)
74
74
  # Transport will be automatically cast to Literal['stdio', 'sse'] by the server
75
75
  server.run(transport=transport)
76
76
 
@@ -12,7 +12,7 @@ from hanzo_mcp.tools.shell.command_executor import CommandExecutor
12
12
 
13
13
 
14
14
  @final
15
- class HanzoDevServer:
15
+ class HanzoMCPServer:
16
16
  """MCP server for accessing Hanzo APIs and Platform capabilities."""
17
17
 
18
18
  def __init__(
@@ -21,7 +21,7 @@ class HanzoDevServer:
21
21
  allowed_paths: list[str] | None = None,
22
22
  mcp_instance: FastMCP | None = None,
23
23
  ):
24
- """Initialize the Hanzo Dev server.
24
+ """Initialize the Hanzo server.
25
25
 
26
26
  Args:
27
27
  name: The name of the server
@@ -117,7 +117,7 @@ def main():
117
117
  allowed_paths: list[str] | None = args.allowed_paths
118
118
 
119
119
  # Create and run the server
120
- server = HanzoDevServer(name=name, allowed_paths=allowed_paths)
120
+ server = HanzoMCPServer(name=name, allowed_paths=allowed_paths)
121
121
  server.run(transport=transport, allowed_paths=allowed_paths or [])
122
122
 
123
123
 
@@ -0,0 +1 @@
1
+ """Common utilities for Hanzo MCP tools."""
@@ -1,4 +1,4 @@
1
- """Enhanced Context for Hanzo Dev MCP tools.
1
+ """Enhanced Context for Hanzo MCP tools.
2
2
 
3
3
  This module provides an enhanced Context class that wraps the MCP Context
4
4
  and adds additional functionality specific to Claude Code tools.
@@ -17,7 +17,7 @@ from mcp.server.lowlevel.helper_types import ReadResourceContents
17
17
 
18
18
  @final
19
19
  class ToolContext:
20
- """Enhanced context for Hanzo Dev MCP tools.
20
+ """Enhanced context for Hanzo MCP tools.
21
21
 
22
22
  This class wraps the MCP Context and adds additional functionality
23
23
  for tracking tool execution, progress reporting, and resource access.
@@ -0,0 +1,313 @@
1
+ """Permission system for the Hanzo MCP server."""
2
+
3
+ import json
4
+ import os
5
+ from collections.abc import Awaitable, Callable
6
+ from pathlib import Path
7
+ from typing import Any, TypeVar, final
8
+
9
+ # Define type variables for better type annotations
10
+ T = TypeVar("T")
11
+ P = TypeVar("P")
12
+
13
+
14
+ def normalize_path(path: str) -> Path:
15
+ """Normalize a path with proper user directory expansion.
16
+
17
+ This utility function handles path normalization with proper handling of
18
+ tilde (~) for home directory expansion and ensures consistent path handling
19
+ across the application.
20
+
21
+ Args:
22
+ path: The path to normalize (can include ~ for home directory)
23
+
24
+ Returns:
25
+ A normalized Path object with user directories expanded and resolved to
26
+ its absolute canonical form.
27
+ """
28
+ # Expand the user directory, handling the tilde (~) if present.
29
+ expanded_path = os.path.expanduser(path)
30
+ # Resolve the expanded path to its absolute form.
31
+ resolved_path = Path(expanded_path).resolve()
32
+ return resolved_path
33
+
34
+
35
+ @final
36
+ class PermissionManager:
37
+ """Manages permissions for file and command operations.
38
+
39
+ This class is responsible for tracking allowed file system paths as well as
40
+ paths and patterns that should be excluded from permitted operations.
41
+ """
42
+
43
+ def __init__(self) -> None:
44
+ """Initialize the permission manager with default allowed and excluded paths.
45
+
46
+ Allowed paths are those where operations (read, write, execute, etc.) are permitted,
47
+ while excluded paths and patterns represent paths and file patterns that are sensitive
48
+ and should be disallowed.
49
+ """
50
+ # Allowed paths: operations are permitted on these paths.
51
+ self.allowed_paths: set[Path] = set(
52
+ [Path("/tmp").resolve(), Path("/var").resolve()]
53
+ )
54
+
55
+ # Excluded paths: specific paths that are explicitly disallowed.
56
+ self.excluded_paths: set[Path] = set()
57
+
58
+ # Excluded patterns: patterns for sensitive directories and file names.
59
+ self.excluded_patterns: list[str] = []
60
+
61
+ # Add default exclusions for sensitive files and directories.
62
+ self._add_default_exclusions()
63
+
64
+ def _add_default_exclusions(self) -> None:
65
+ """Add default exclusions for sensitive files and directories.
66
+
67
+ This method populates the excluded_patterns list with common sensitive
68
+ directories (e.g., .ssh, .gnupg) and file patterns (e.g., *.key, *.log)
69
+ that should be excluded from allowed operations.
70
+ """
71
+ # Sensitive directories (Note: .git is allowed by default)
72
+ sensitive_dirs: list[str] = [
73
+ ".ssh",
74
+ ".gnupg",
75
+ ".config",
76
+ "node_modules",
77
+ "__pycache__",
78
+ ".venv",
79
+ "venv",
80
+ "env",
81
+ ".idea",
82
+ ".vscode",
83
+ ".DS_Store",
84
+ ]
85
+ self.excluded_patterns.extend(sensitive_dirs)
86
+
87
+ # Sensitive file patterns
88
+ sensitive_patterns: list[str] = [
89
+ ".env",
90
+ "*.key",
91
+ "*.pem",
92
+ "*.crt",
93
+ "*password*",
94
+ "*secret*",
95
+ "*.sqlite",
96
+ "*.db",
97
+ "*.sqlite3",
98
+ "*.log",
99
+ ]
100
+ self.excluded_patterns.extend(sensitive_patterns)
101
+
102
+ def add_allowed_path(self, path: str) -> None:
103
+ """Add a new path to the allowed paths.
104
+
105
+ Args:
106
+ path: The file system path to add to the allowed list.
107
+ """
108
+ resolved_path: Path = normalize_path(path)
109
+ self.allowed_paths.add(resolved_path)
110
+
111
+ def remove_allowed_path(self, path: str) -> None:
112
+ """Remove a path from the allowed paths.
113
+
114
+ Args:
115
+ path: The file system path to remove from the allowed list.
116
+ """
117
+ resolved_path: Path = normalize_path(path)
118
+ if resolved_path in self.allowed_paths:
119
+ self.allowed_paths.remove(resolved_path)
120
+
121
+ def exclude_path(self, path: str) -> None:
122
+ """Add a path to the exclusion list.
123
+
124
+ Args:
125
+ path: The file system path to explicitly exclude from operations.
126
+ """
127
+ resolved_path: Path = normalize_path(path)
128
+ self.excluded_paths.add(resolved_path)
129
+
130
+ def add_exclusion_pattern(self, pattern: str) -> None:
131
+ """Add a new exclusion pattern.
132
+
133
+ Args:
134
+ pattern: A string pattern that matches file or directory names to exclude.
135
+ """
136
+ self.excluded_patterns.append(pattern)
137
+
138
+ def is_path_allowed(self, path: str) -> bool:
139
+ """Determine if a given path is allowed for operations.
140
+
141
+ The method normalizes the input path and then checks it against the list
142
+ of excluded paths and patterns. If the path is not excluded and is a
143
+ subpath of one of the allowed base paths, the method returns True.
144
+
145
+ Args:
146
+ path: The file system path to check.
147
+
148
+ Returns:
149
+ True if the path is allowed for operations, False otherwise.
150
+ """
151
+ resolved_path: Path = normalize_path(path)
152
+
153
+ # First, check if the path matches any excluded paths or patterns.
154
+ if self._is_path_excluded(resolved_path):
155
+ return False
156
+
157
+ # Check if the normalized path is within any allowed path.
158
+ for allowed_path in self.allowed_paths:
159
+ try:
160
+ # relative_to will succeed if resolved_path is a subpath of allowed_path.
161
+ resolved_path.relative_to(allowed_path)
162
+ return True
163
+ except ValueError:
164
+ continue
165
+
166
+ return False
167
+
168
+ def _is_path_excluded(self, path: Path) -> bool:
169
+ """Determine if a normalized path should be excluded.
170
+
171
+ The method checks two conditions:
172
+ 1. If the path exactly matches an entry in the excluded_paths set.
173
+ 2. If the path string contains any of the excluded patterns, either as a
174
+ suffix for wildcard patterns (e.g., "*.log") or as an exact match
175
+ within any of the path's components.
176
+
177
+ Args:
178
+ path: The normalized path to check.
179
+
180
+ Returns:
181
+ True if the path is excluded, False otherwise.
182
+ """
183
+ # Direct match: Check if the path is in the explicitly excluded paths set.
184
+ if path in self.excluded_paths:
185
+ return True
186
+
187
+ # Convert the path to a string for pattern matching.
188
+ path_str: str = str(path)
189
+
190
+ # Split the path into its individual components (directories and file name).
191
+ path_parts = path_str.split(os.sep)
192
+
193
+ # Iterate over each exclusion pattern to see if it matches.
194
+ for pattern in self.excluded_patterns:
195
+ # If the pattern starts with a wildcard, perform a suffix match.
196
+ if pattern.startswith("*"):
197
+ if path_str.endswith(pattern[1:]):
198
+ return True
199
+ else:
200
+ # For non-wildcard patterns, check if any path component exactly matches the pattern.
201
+ if pattern in path_parts:
202
+ return True
203
+
204
+ return False
205
+
206
+ def to_json(self) -> str:
207
+ """Serialize the permission manager's configuration to a JSON string.
208
+
209
+ The JSON representation includes the allowed paths, excluded paths, and
210
+ excluded patterns, which can be used to restore the configuration later.
211
+
212
+ Returns:
213
+ A JSON string representing the current state of the permission manager.
214
+ """
215
+ data: dict[str, Any] = {
216
+ "allowed_paths": [str(p) for p in self.allowed_paths],
217
+ "excluded_paths": [str(p) for p in self.excluded_paths],
218
+ "excluded_patterns": self.excluded_patterns,
219
+ }
220
+ return json.dumps(data)
221
+
222
+ @classmethod
223
+ def from_json(cls, json_str: str) -> "PermissionManager":
224
+ """Create a PermissionManager instance from a JSON string.
225
+
226
+ The JSON string should represent a configuration with allowed paths,
227
+ excluded paths, and exclusion patterns. This method rehydrates the state
228
+ accordingly.
229
+
230
+ Args:
231
+ json_str: The JSON string containing the permission manager configuration.
232
+
233
+ Returns:
234
+ A new PermissionManager instance with configuration loaded from the JSON string.
235
+ """
236
+ data: dict[str, Any] = json.loads(json_str)
237
+ manager = cls()
238
+
239
+ for path in data.get("allowed_paths", []):
240
+ manager.add_allowed_path(path)
241
+
242
+ for path in data.get("excluded_paths", []):
243
+ manager.exclude_path(path)
244
+
245
+ manager.excluded_patterns = data.get("excluded_patterns", [])
246
+ return manager
247
+
248
+
249
+ class PermissibleOperation:
250
+ """A decorator for operations that require permission checks.
251
+
252
+ This decorator uses a PermissionManager instance to enforce that a given
253
+ operation (e.g., read, write, execute) is permitted on a provided file system
254
+ path before allowing the decorated function to execute.
255
+ """
256
+
257
+ def __init__(
258
+ self,
259
+ permission_manager: PermissionManager,
260
+ operation: str,
261
+ get_path_fn: Callable[[list[Any], dict[str, Any]], str] | None = None,
262
+ ) -> None:
263
+ """Initialize the PermissibleOperation decorator.
264
+
265
+ Args:
266
+ permission_manager: The PermissionManager instance used for permission checks.
267
+ operation: A string representing the operation type (e.g., 'read', 'write').
268
+ get_path_fn: Optional function to extract the file system path from the function's
269
+ arguments. If not provided, defaults to using the first positional argument
270
+ or the first value from keyword arguments.
271
+ """
272
+ self.permission_manager: PermissionManager = permission_manager
273
+ self.operation: str = operation
274
+ self.get_path_fn: Callable[[list[Any], dict[str, Any]], str] | None = get_path_fn
275
+
276
+ def __call__(
277
+ self, func: Callable[..., Awaitable[T]]
278
+ ) -> Callable[..., Awaitable[T]]:
279
+ """Decorate the function to enforce permission checks before execution.
280
+
281
+ This method wraps the original asynchronous function, extracting a file system path
282
+ from its arguments and using the PermissionManager to verify if the specified operation
283
+ is allowed on that path. If permission is denied, a PermissionError is raised.
284
+
285
+ Args:
286
+ func: The asynchronous function to decorate.
287
+
288
+ Returns:
289
+ An asynchronous function that includes permission checks prior to calling the original function.
290
+ """
291
+ async def wrapper(*args: Any, **kwargs: Any) -> T:
292
+ # Extract the file system path using the provided get_path_fn if available.
293
+ if self.get_path_fn:
294
+ path = self.get_path_fn(list(args), kwargs)
295
+ else:
296
+ # Default extraction: use the first positional argument if available,
297
+ # otherwise use the first keyword argument value.
298
+ path = args[0] if args else next(iter(kwargs.values()), None)
299
+
300
+ # Ensure that the extracted path is a string.
301
+ if not isinstance(path, str):
302
+ raise ValueError(f"Invalid path type: {type(path)}. Expected a string.")
303
+
304
+ # Check if the operation is allowed on the specified path.
305
+ if not self.permission_manager.is_path_allowed(path):
306
+ raise PermissionError(
307
+ f"Operation '{self.operation}' not allowed for path: {path}"
308
+ )
309
+
310
+ # Execute the original function if the permission check passes.
311
+ return await func(*args, **kwargs)
312
+
313
+ return wrapper
@@ -1,4 +1,4 @@
1
- """Parameter validation utilities for Hanzo Dev MCP tools.
1
+ """Parameter validation utilities for Hanzo MCP tools.
2
2
 
3
3
  This module provides utilities for validating parameters in tool functions.
4
4
  """
@@ -1,4 +1,4 @@
1
- """Filesystem tools for Hanzo Dev MCP.
1
+ """Filesystem tools for Hanzo MCP.
2
2
 
3
3
  This module provides comprehensive tools for interacting with the filesystem,
4
4
  including reading, writing, editing files, directory operations, and searching.
@@ -1,4 +1,4 @@
1
- """Filesystem operations tools for Hanzo Dev MCP.
1
+ """Filesystem operations tools for Hanzo MCP.
2
2
 
3
3
  This module provides comprehensive tools for interacting with the filesystem,
4
4
  including reading, writing, editing files, directory operations, and searching.
@@ -20,7 +20,7 @@ from hanzo_mcp.tools.common.validation import validate_path_parameter
20
20
 
21
21
  @final
22
22
  class FileOperations:
23
- """File and filesystem operations tools for Hanzo Dev MCP."""
23
+ """File and filesystem operations tools for Hanzo MCP."""
24
24
 
25
25
  def __init__(
26
26
  self, document_context: DocumentContext, permission_manager: PermissionManager
@@ -1,4 +1,4 @@
1
- """Jupyter notebook tools for Hanzo Dev MCP.
1
+ """Jupyter notebook tools for Hanzo MCP.
2
2
 
3
3
  This module provides tools for reading and editing Jupyter notebooks (.ipynb files).
4
4
  """
@@ -1,4 +1,4 @@
1
- """Jupyter notebook operations tools for Hanzo Dev MCP.
1
+ """Jupyter notebook operations tools for Hanzo MCP.
2
2
 
3
3
  This module provides tools for reading and editing Jupyter notebook (.ipynb) files.
4
4
  It supports reading notebook cells with their outputs and modifying notebook contents.
@@ -0,0 +1 @@
1
+ """Project analysis and management tools for Hanzo MCP."""
@@ -1,4 +1,4 @@
1
- """Project analysis tools for Hanzo Dev MCP.
1
+ """Project analysis tools for Hanzo MCP.
2
2
 
3
3
  This module provides tools for analyzing project structure and dependencies.
4
4
  """
@@ -84,7 +84,7 @@ print(json.dumps(result))
84
84
 
85
85
  # Execute script
86
86
  result = await self.command_executor.execute_script_from_file(
87
- script=script, language="python", cwd=project_dir, timeout=30.0
87
+ script=script, language="python", cwd=project_dir, timeout=600.0
88
88
  )
89
89
  code, stdout, stderr = result.return_code, result.stdout, result.stderr
90
90
 
@@ -204,7 +204,7 @@ try {
204
204
 
205
205
  # Execute script
206
206
  result = await self.command_executor.execute_script_from_file(
207
- script=script, language="javascript", cwd=project_dir, timeout=30.0
207
+ script=script, language="javascript", cwd=project_dir, timeout=600.0
208
208
  )
209
209
  code, stdout, stderr = result.return_code, result.stdout, result.stderr
210
210
 
@@ -286,7 +286,7 @@ print(json.dumps(result))
286
286
 
287
287
  # Execute script
288
288
  result = await self.command_executor.execute_script_from_file(
289
- script=script, language="python", cwd=project_dir, timeout=30.0
289
+ script=script, language="python", cwd=project_dir, timeout=600.0
290
290
  )
291
291
  code, stdout, stderr = result.return_code, result.stdout, result.stderr
292
292
 
@@ -795,7 +795,7 @@ class ProjectManager:
795
795
 
796
796
  @final
797
797
  class ProjectAnalysis:
798
- """Project analysis tools for Hanzo Dev MCP."""
798
+ """Project analysis tools for Hanzo MCP."""
799
799
 
800
800
  def __init__(
801
801
  self,
@@ -0,0 +1 @@
1
+ """Shell and command execution tools for Hanzo MCP."""
@@ -1,4 +1,4 @@
1
- """Command executor tools for Hanzo Dev MCP.
1
+ """Command executor tools for Hanzo MCP.
2
2
 
3
3
  This module provides tools for executing shell commands and scripts with
4
4
  comprehensive error handling, permissions checking, and progress tracking.
@@ -86,7 +86,7 @@ class CommandResult:
86
86
 
87
87
  @final
88
88
  class CommandExecutor:
89
- """Command executor tools for Hanzo Dev MCP.
89
+ """Command executor tools for Hanzo MCP.
90
90
 
91
91
  This class provides tools for executing shell commands and scripts with
92
92
  comprehensive error handling, permissions checking, and progress tracking.
@@ -193,7 +193,7 @@ class CommandExecutor:
193
193
  command: str,
194
194
  cwd: str | None = None,
195
195
  env: dict[str, str] | None = None,
196
- timeout: float | None = 60.0,
196
+ timeout: float | None = 600.0,
197
197
  use_login_shell: bool = True,
198
198
  ) -> CommandResult:
199
199
  """Execute a shell command with safety checks.
@@ -320,7 +320,7 @@ class CommandExecutor:
320
320
  interpreter: str = "bash",
321
321
  cwd: str | None = None,
322
322
  env: dict[str, str] | None = None,
323
- timeout: float | None = 60.0,
323
+ timeout: float | None = 600.0,
324
324
  use_login_shell: bool = True,
325
325
  ) -> CommandResult:
326
326
  """Execute a script with the specified interpreter.
@@ -368,7 +368,7 @@ class CommandExecutor:
368
368
  script: str,
369
369
  cwd: str | None = None,
370
370
  env: dict[str, str] | None = None,
371
- timeout: float | None = 60.0,
371
+ timeout: float | None = 600.0,
372
372
  use_login_shell: bool = True,
373
373
  ) -> CommandResult:
374
374
  """Execute a script by passing it to stdin of the interpreter.
@@ -458,7 +458,7 @@ class CommandExecutor:
458
458
  script: str,
459
459
  cwd: str | None = None,
460
460
  env: dict[str, str] | None = None,
461
- timeout: float | None = 60.0,
461
+ timeout: float | None = 600.0,
462
462
  ) -> CommandResult:
463
463
  """Special handler for Fish shell scripts.
464
464
 
@@ -533,7 +533,7 @@ class CommandExecutor:
533
533
  language: str,
534
534
  cwd: str | None = None,
535
535
  env: dict[str, str] | None = None,
536
- timeout: float | None = 60.0,
536
+ timeout: float | None = 600.0,
537
537
  args: list[str] | None = None,
538
538
  use_login_shell: bool = True,
539
539
  ) -> CommandResult:
@@ -773,7 +773,7 @@ class CommandExecutor:
773
773
 
774
774
  # Execute the command
775
775
  result: CommandResult = await self.execute_command(
776
- command, cwd=cwd, timeout=30.0, use_login_shell=use_login_shell
776
+ command, cwd=cwd, timeout=600.0, use_login_shell=use_login_shell
777
777
  )
778
778
 
779
779
  # Report result
@@ -869,7 +869,7 @@ class CommandExecutor:
869
869
  script=script,
870
870
  interpreter=interpreter,
871
871
  cwd=cwd, # cwd is now a required parameter
872
- timeout=30.0,
872
+ timeout=600.0,
873
873
  use_login_shell=use_login_shell,
874
874
  )
875
875
 
@@ -974,7 +974,7 @@ class CommandExecutor:
974
974
  script=script,
975
975
  language=language,
976
976
  cwd=cwd, # cwd is now a required parameter
977
- timeout=30.0,
977
+ timeout=600.0,
978
978
  args=args,
979
979
  use_login_shell=use_login_shell,
980
980
  )
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hanzo-mcp
3
- Version: 0.1.21
3
+ Version: 0.1.25
4
4
  Summary: MCP server for accessing Hanzo APIs and Platform capabilities
5
- Author-email: Hanzo Dev <dev@hanzo.ai>
5
+ Author-email: Hanzo <dev@hanzo.ai>
6
6
  License: MIT
7
7
  Classifier: Programming Language :: Python :: 3
8
8
  Classifier: License :: OSI Approved :: MIT License
@@ -10,6 +10,7 @@ hanzo_mcp.egg-info/dependency_links.txt
10
10
  hanzo_mcp.egg-info/entry_points.txt
11
11
  hanzo_mcp.egg-info/requires.txt
12
12
  hanzo_mcp.egg-info/top_level.txt
13
+ hanzo_mcp.egg-info/zip-safe
13
14
  hanzo_mcp/tools/__init__.py
14
15
  hanzo_mcp/tools/common/__init__.py
15
16
  hanzo_mcp/tools/common/context.py
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "hanzo-mcp"
7
- version = "0.1.21"
7
+ version = "0.1.25"
8
8
  description = "MCP server for accessing Hanzo APIs and Platform capabilities"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"
11
11
  license = { text = "MIT" }
12
- authors = [{ name = "Hanzo Dev", email = "dev@hanzo.ai" }]
12
+ authors = [{ name = "Hanzo", email = "dev@hanzo.ai" }]
13
13
  classifiers = [
14
14
  "Programming Language :: Python :: 3",
15
15
  "License :: OSI Approved :: MIT License",
@@ -31,10 +31,16 @@ performance = ["ujson>=5.7.0", "orjson>=3.9.0"]
31
31
  [project.scripts]
32
32
  hanzo-mcp = "hanzo_mcp.cli:main"
33
33
 
34
+ [tool.setuptools]
35
+ zip-safe = true
36
+
34
37
  [tool.setuptools.packages.find]
35
38
  where = ["."]
36
39
  include = ["hanzo_mcp*"]
37
40
 
41
+ [tool.bdist_wheel]
42
+ universal = true
43
+
38
44
  [tool.basedpyright]
39
45
  include = ["hanzo_mcp"]
40
46
  exclude = [
@@ -18,7 +18,7 @@ class TestCLI:
18
18
  """Test the main function running the server."""
19
19
  with (
20
20
  patch("argparse.ArgumentParser.parse_args") as mock_parse_args,
21
- patch("hanzo_mcp.cli.HanzoDevServer") as mock_server_class,
21
+ patch("hanzo_mcp.cli.HanzoMCPServer") as mock_server_class,
22
22
  ):
23
23
  # Mock parsed arguments
24
24
  mock_args = MagicMock()
@@ -67,7 +67,7 @@ class TestCLI:
67
67
  """Test the main function without specified allowed paths."""
68
68
  with (
69
69
  patch("argparse.ArgumentParser.parse_args") as mock_parse_args,
70
- patch("hanzo_mcp.cli.HanzoDevServer") as mock_server_class,
70
+ patch("hanzo_mcp.cli.HanzoMCPServer") as mock_server_class,
71
71
  patch("os.getcwd", return_value="/current/dir"),
72
72
  ):
73
73
  # Mock parsed arguments
@@ -5,28 +5,28 @@ from typing import Tuple
5
5
 
6
6
  import pytest
7
7
 
8
- from hanzo_mcp.server import HanzoDevServer
8
+ from hanzo_mcp.server import HanzoMCPServer
9
9
 
10
10
 
11
- class TestHanzoDevServer:
12
- """Test the HanzoDevServer class."""
11
+ class TestHanzoMCPServer:
12
+ """Test the HanzoMCPServer class."""
13
13
 
14
14
  @pytest.fixture
15
- def server(self) -> Tuple[HanzoDevServer, MagicMock]:
16
- """Create a HanzoDevServer instance for testing."""
15
+ def server(self) -> Tuple[HanzoMCPServer, MagicMock]:
16
+ """Create a HanzoMCPServer instance for testing."""
17
17
  with patch("mcp.server.fastmcp.FastMCP") as mock_fastmcp:
18
18
  # Create a mock FastMCP instance
19
19
  mock_mcp = MagicMock()
20
20
  mock_fastmcp.return_value = mock_mcp
21
21
 
22
22
  # Create the server with the mock MCP
23
- server = HanzoDevServer(name="test-server", mcp_instance=mock_mcp)
23
+ server = HanzoMCPServer(name="test-server", mcp_instance=mock_mcp)
24
24
 
25
25
  # Return both the server and the mock MCP
26
26
  yield server, mock_mcp
27
27
 
28
- def test_initialization(self, server: Tuple[HanzoDevServer, MagicMock]) -> None:
29
- """Test initializing HanzoDevServer."""
28
+ def test_initialization(self, server: Tuple[HanzoMCPServer, MagicMock]) -> None:
29
+ """Test initializing HanzoMCPServer."""
30
30
  server_instance, mock_mcp = server
31
31
 
32
32
  # Verify components were initialized
@@ -54,7 +54,7 @@ class TestHanzoDevServer:
54
54
  doc_context = MagicMock()
55
55
 
56
56
  # Create the server
57
- server = HanzoDevServer(name="test-server", mcp_instance=mock_mcp)
57
+ server = HanzoMCPServer(name="test-server", mcp_instance=mock_mcp)
58
58
 
59
59
  # Inject our mocks
60
60
  server.permission_manager = perm_manager
@@ -89,7 +89,7 @@ class TestHanzoDevServer:
89
89
  mock_register.assert_called_once()
90
90
 
91
91
  @pytest.mark.skip(reason="Cannot run stdio server in a test environment")
92
- def test_run(self, server: Tuple[HanzoDevServer, MagicMock]) -> None:
92
+ def test_run(self, server: Tuple[HanzoMCPServer, MagicMock]) -> None:
93
93
  """Test running the server."""
94
94
  server_instance, mock_mcp = server
95
95
 
@@ -111,7 +111,7 @@ class TestHanzoDevServer:
111
111
 
112
112
  @pytest.mark.skip(reason="Cannot run stdio server in a test environment")
113
113
  def test_run_with_allowed_paths(
114
- self, server: Tuple[HanzoDevServer, MagicMock]
114
+ self, server: Tuple[HanzoMCPServer, MagicMock]
115
115
  ) -> None:
116
116
  """Test running the server with additional allowed paths."""
117
117
  server_instance, mock_mcp = server
@@ -145,7 +145,7 @@ def test_main() -> None:
145
145
  """Test the main function."""
146
146
  with (
147
147
  patch("argparse.ArgumentParser.parse_args") as mock_parse_args,
148
- patch("hanzo_mcp.server.HanzoDevServer") as mock_server_class,
148
+ patch("hanzo_mcp.server.HanzoMCPServer") as mock_server_class,
149
149
  ):
150
150
  # Mock parsed arguments
151
151
  mock_args = MagicMock()
@@ -1,4 +1,4 @@
1
- """Tests for parameter validation in Hanzo Dev MCP tools."""
1
+ """Tests for parameter validation in Hanzo MCP tools."""
2
2
 
3
3
  from hanzo_mcp.tools.common.validation import (
4
4
  validate_parameter,
@@ -1 +0,0 @@
1
- """Common utilities for Hanzo Dev MCP tools."""
@@ -1,253 +0,0 @@
1
- """Permission system for the Hanzo Dev MCP server."""
2
-
3
- import json
4
- import os
5
- from collections.abc import Awaitable, Callable
6
- from pathlib import Path
7
- from typing import Any, TypeVar, final
8
-
9
- # Define type variables for better type annotations
10
- T = TypeVar("T")
11
- P = TypeVar("P")
12
-
13
-
14
- @final
15
- class PermissionManager:
16
- """Manages permissions for file and command operations."""
17
-
18
- def __init__(self) -> None:
19
- """Initialize the permission manager."""
20
- # Allowed paths
21
- self.allowed_paths: set[Path] = set(
22
- [Path("/tmp").resolve(), Path("/var").resolve()]
23
- )
24
-
25
- # Excluded paths
26
- self.excluded_paths: set[Path] = set()
27
- self.excluded_patterns: list[str] = []
28
-
29
- # Default excluded patterns
30
- self._add_default_exclusions()
31
-
32
- def _add_default_exclusions(self) -> None:
33
- """Add default exclusions for sensitive files and directories."""
34
- # Sensitive directories
35
- sensitive_dirs: list[str] = [
36
- # ".git" is now allowed by default
37
- ".ssh",
38
- ".gnupg",
39
- ".config",
40
- "node_modules",
41
- "__pycache__",
42
- ".venv",
43
- "venv",
44
- "env",
45
- ".idea",
46
- ".vscode",
47
- ".DS_Store",
48
- ]
49
- self.excluded_patterns.extend(sensitive_dirs)
50
-
51
- # Sensitive file patterns
52
- sensitive_patterns: list[str] = [
53
- ".env",
54
- "*.key",
55
- "*.pem",
56
- "*.crt",
57
- "*password*",
58
- "*secret*",
59
- "*.sqlite",
60
- "*.db",
61
- "*.sqlite3",
62
- "*.log",
63
- ]
64
- self.excluded_patterns.extend(sensitive_patterns)
65
-
66
- def add_allowed_path(self, path: str) -> None:
67
- """Add a path to the allowed paths.
68
-
69
- Args:
70
- path: The path to allow
71
- """
72
- resolved_path: Path = Path(path).resolve()
73
- self.allowed_paths.add(resolved_path)
74
-
75
- def remove_allowed_path(self, path: str) -> None:
76
- """Remove a path from the allowed paths.
77
-
78
- Args:
79
- path: The path to remove
80
- """
81
- resolved_path: Path = Path(path).resolve()
82
- if resolved_path in self.allowed_paths:
83
- self.allowed_paths.remove(resolved_path)
84
-
85
- def exclude_path(self, path: str) -> None:
86
- """Exclude a path from allowed operations.
87
-
88
- Args:
89
- path: The path to exclude
90
- """
91
- resolved_path: Path = Path(path).resolve()
92
- self.excluded_paths.add(resolved_path)
93
-
94
- def add_exclusion_pattern(self, pattern: str) -> None:
95
- """Add an exclusion pattern.
96
-
97
- Args:
98
- pattern: The pattern to exclude
99
- """
100
- self.excluded_patterns.append(pattern)
101
-
102
- def is_path_allowed(self, path: str) -> bool:
103
- """Check if a path is allowed.
104
-
105
- Args:
106
- path: The path to check
107
-
108
- Returns:
109
- True if the path is allowed, False otherwise
110
- """
111
- resolved_path: Path = Path(path).resolve()
112
-
113
- # Check exclusions first
114
- if self._is_path_excluded(resolved_path):
115
- return False
116
-
117
- # Check if the path is within any allowed path
118
- for allowed_path in self.allowed_paths:
119
- try:
120
- resolved_path.relative_to(allowed_path)
121
- return True
122
- except ValueError:
123
- continue
124
-
125
- return False
126
-
127
- def _is_path_excluded(self, path: Path) -> bool:
128
- """Check if a path is excluded.
129
-
130
- Args:
131
- path: The path to check
132
-
133
- Returns:
134
- True if the path is excluded, False otherwise
135
- """
136
-
137
- # Check exact excluded paths
138
- if path in self.excluded_paths:
139
- return True
140
-
141
- # Check excluded patterns
142
- path_str: str = str(path)
143
-
144
- # Get path parts to check for exact directory/file name matches
145
- path_parts = path_str.split(os.sep)
146
-
147
- for pattern in self.excluded_patterns:
148
- # Handle wildcard patterns (e.g., "*.log")
149
- if pattern.startswith("*"):
150
- if path_str.endswith(pattern[1:]):
151
- return True
152
- else:
153
- # For non-wildcard patterns, check if any path component matches exactly
154
- if pattern in path_parts:
155
- return True
156
-
157
- return False
158
-
159
- def to_json(self) -> str:
160
- """Convert the permission manager to a JSON string.
161
-
162
- Returns:
163
- A JSON string representation of the permission manager
164
- """
165
- data: dict[str, Any] = {
166
- "allowed_paths": [str(p) for p in self.allowed_paths],
167
- "excluded_paths": [str(p) for p in self.excluded_paths],
168
- "excluded_patterns": self.excluded_patterns,
169
- }
170
-
171
- return json.dumps(data)
172
-
173
- @classmethod
174
- def from_json(cls, json_str: str) -> "PermissionManager":
175
- """Create a permission manager from a JSON string.
176
-
177
- Args:
178
- json_str: The JSON string
179
-
180
- Returns:
181
- A new PermissionManager instance
182
- """
183
- data: dict[str, Any] = json.loads(json_str)
184
-
185
- manager = cls()
186
-
187
- for path in data.get("allowed_paths", []):
188
- manager.add_allowed_path(path)
189
-
190
- for path in data.get("excluded_paths", []):
191
- manager.exclude_path(path)
192
-
193
- manager.excluded_patterns = data.get("excluded_patterns", [])
194
-
195
- return manager
196
-
197
-
198
- class PermissibleOperation:
199
- """A decorator for operations that require permission."""
200
-
201
- def __init__(
202
- self,
203
- permission_manager: PermissionManager,
204
- operation: str,
205
- get_path_fn: Callable[[list[Any], dict[str, Any]], str] | None = None,
206
- ) -> None:
207
- """Initialize the permissible operation.
208
-
209
- Args:
210
- permission_manager: The permission manager
211
- operation: The operation type (read, write, execute, etc.)
212
- get_path_fn: Optional function to extract the path from args and kwargs
213
- """
214
- self.permission_manager: PermissionManager = permission_manager
215
- self.operation: str = operation
216
- self.get_path_fn: Callable[[list[Any], dict[str, Any]], str] | None = (
217
- get_path_fn
218
- )
219
-
220
- def __call__(
221
- self, func: Callable[..., Awaitable[T]]
222
- ) -> Callable[..., Awaitable[T]]:
223
- """Decorate the function.
224
-
225
- Args:
226
- func: The function to decorate
227
-
228
- Returns:
229
- The decorated function
230
- """
231
-
232
- async def wrapper(*args: Any, **kwargs: Any) -> T:
233
- # Extract the path
234
- if self.get_path_fn:
235
- # Pass args as a list and kwargs as a dict to the path function
236
- path = self.get_path_fn(list(args), kwargs)
237
- else:
238
- # Default to first argument
239
- path = args[0] if args else next(iter(kwargs.values()), None)
240
-
241
- if not isinstance(path, str):
242
- raise ValueError(f"Invalid path type: {type(path)}")
243
-
244
- # Check permission
245
- if not self.permission_manager.is_path_allowed(path):
246
- raise PermissionError(
247
- f"Operation '{self.operation}' not allowed for path: {path}"
248
- )
249
-
250
- # Call the function
251
- return await func(*args, **kwargs)
252
-
253
- return wrapper
@@ -1 +0,0 @@
1
- """Project analysis and management tools for Hanzo Dev MCP."""
@@ -1 +0,0 @@
1
- """Shell and command execution tools for Hanzo Dev MCP."""
File without changes
File without changes
File without changes