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.
- {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/PKG-INFO +2 -2
- {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/hanzo_mcp/__init__.py +1 -1
- {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/hanzo_mcp/cli.py +3 -3
- {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/hanzo_mcp/server.py +3 -3
- hanzo_mcp-0.1.25/hanzo_mcp/tools/common/__init__.py +1 -0
- {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/hanzo_mcp/tools/common/context.py +2 -2
- hanzo_mcp-0.1.25/hanzo_mcp/tools/common/permissions.py +313 -0
- {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/hanzo_mcp/tools/common/validation.py +1 -1
- {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/hanzo_mcp/tools/filesystem/__init__.py +1 -1
- {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/hanzo_mcp/tools/filesystem/file_operations.py +2 -2
- {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/hanzo_mcp/tools/jupyter/__init__.py +1 -1
- {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/hanzo_mcp/tools/jupyter/notebook_operations.py +1 -1
- hanzo_mcp-0.1.25/hanzo_mcp/tools/project/__init__.py +1 -0
- {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/hanzo_mcp/tools/project/analysis.py +5 -5
- hanzo_mcp-0.1.25/hanzo_mcp/tools/shell/__init__.py +1 -0
- {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/hanzo_mcp/tools/shell/command_executor.py +10 -10
- {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/hanzo_mcp.egg-info/PKG-INFO +2 -2
- {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/hanzo_mcp.egg-info/SOURCES.txt +1 -0
- hanzo_mcp-0.1.25/hanzo_mcp.egg-info/zip-safe +1 -0
- {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/pyproject.toml +8 -2
- {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/tests/test_cli.py +2 -2
- {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/tests/test_server.py +12 -12
- {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/tests/test_validation.py +1 -1
- hanzo_mcp-0.1.21/hanzo_mcp/tools/common/__init__.py +0 -1
- hanzo_mcp-0.1.21/hanzo_mcp/tools/common/permissions.py +0 -253
- hanzo_mcp-0.1.21/hanzo_mcp/tools/project/__init__.py +0 -1
- hanzo_mcp-0.1.21/hanzo_mcp/tools/shell/__init__.py +0 -1
- {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/LICENSE +0 -0
- {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/README.md +0 -0
- {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/hanzo_mcp/tools/__init__.py +0 -0
- {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/hanzo_mcp/tools/common/thinking.py +0 -0
- {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/hanzo_mcp.egg-info/dependency_links.txt +0 -0
- {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/hanzo_mcp.egg-info/entry_points.txt +0 -0
- {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/hanzo_mcp.egg-info/requires.txt +0 -0
- {hanzo_mcp-0.1.21 → hanzo_mcp-0.1.25}/hanzo_mcp.egg-info/top_level.txt +0 -0
- {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.
|
|
3
|
+
Version: 0.1.25
|
|
4
4
|
Summary: MCP server for accessing Hanzo APIs and Platform capabilities
|
|
5
|
-
Author-email: Hanzo
|
|
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
|
|
@@ -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
|
|
10
|
+
from hanzo_mcp.server import HanzoMCPServer
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def main() -> None:
|
|
14
|
-
"""Run the CLI for the Hanzo
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
"""Filesystem operations tools for Hanzo
|
|
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
|
|
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 operations tools for Hanzo
|
|
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
|
|
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=
|
|
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=
|
|
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=
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
3
|
+
Version: 0.1.25
|
|
4
4
|
Summary: MCP server for accessing Hanzo APIs and Platform capabilities
|
|
5
|
-
Author-email: Hanzo
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "hanzo-mcp"
|
|
7
|
-
version = "0.1.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
8
|
+
from hanzo_mcp.server import HanzoMCPServer
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
class
|
|
12
|
-
"""Test the
|
|
11
|
+
class TestHanzoMCPServer:
|
|
12
|
+
"""Test the HanzoMCPServer class."""
|
|
13
13
|
|
|
14
14
|
@pytest.fixture
|
|
15
|
-
def server(self) -> Tuple[
|
|
16
|
-
"""Create a
|
|
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 =
|
|
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[
|
|
29
|
-
"""Test initializing
|
|
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 =
|
|
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[
|
|
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[
|
|
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.
|
|
148
|
+
patch("hanzo_mcp.server.HanzoMCPServer") as mock_server_class,
|
|
149
149
|
):
|
|
150
150
|
# Mock parsed arguments
|
|
151
151
|
mock_args = MagicMock()
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|