hanzo-mcp 0.1.36__tar.gz → 0.2.0__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.36 → hanzo_mcp-0.2.0}/PKG-INFO +1 -1
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/__init__.py +1 -1
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/cli.py +32 -12
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/server.py +8 -9
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/__init__.py +4 -2
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/common/__init__.py +0 -1
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/common/context.py +6 -8
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/common/permissions.py +6 -8
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/filesystem/__init__.py +6 -1
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/filesystem/directory_tree.py +7 -46
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/jupyter/__init__.py +6 -1
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/shell/command_executor.py +7 -6
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp.egg-info/PKG-INFO +1 -1
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp.egg-info/SOURCES.txt +1 -1
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/pyproject.toml +1 -1
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/tests/test_cli.py +94 -3
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/tests/test_server.py +20 -0
- hanzo_mcp-0.2.0/tests/test_tools_registration.py +180 -0
- hanzo_mcp-0.1.36/hanzo_mcp/tools/common/path_utils.py +0 -51
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/LICENSE +0 -0
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/README.md +0 -0
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/agent/__init__.py +0 -0
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/agent/agent_tool.py +0 -0
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/agent/prompt.py +0 -0
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/agent/tool_adapter.py +0 -0
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/common/base.py +0 -0
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/common/session.py +0 -0
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/common/think_tool.py +0 -0
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/common/validation.py +0 -0
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/common/version_tool.py +0 -0
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/filesystem/base.py +0 -0
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/filesystem/content_replace.py +0 -0
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/filesystem/edit_file.py +0 -0
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/filesystem/get_file_info.py +0 -0
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/filesystem/read_files.py +0 -0
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/filesystem/search_content.py +0 -0
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/filesystem/write_file.py +0 -0
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/jupyter/base.py +0 -0
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/jupyter/edit_notebook.py +0 -0
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/jupyter/notebook_operations.py +0 -0
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/jupyter/read_notebook.py +0 -0
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/project/__init__.py +0 -0
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/project/analysis.py +0 -0
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/project/base.py +0 -0
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/project/project_analyze.py +0 -0
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/shell/__init__.py +0 -0
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/shell/base.py +0 -0
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/shell/run_command.py +0 -0
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/shell/run_script.py +0 -0
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/shell/script_tool.py +0 -0
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp.egg-info/dependency_links.txt +0 -0
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp.egg-info/entry_points.txt +0 -0
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp.egg-info/requires.txt +0 -0
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp.egg-info/top_level.txt +0 -0
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/setup.cfg +0 -0
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/setup.py +0 -0
- {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/tests/test_validation.py +0 -0
|
@@ -8,7 +8,6 @@ from pathlib import Path
|
|
|
8
8
|
from typing import Any, cast
|
|
9
9
|
|
|
10
10
|
from hanzo_mcp.server import HanzoServer
|
|
11
|
-
from hanzo_mcp.tools.common.path_utils import PathUtils
|
|
12
11
|
|
|
13
12
|
|
|
14
13
|
def main() -> None:
|
|
@@ -83,6 +82,14 @@ def main() -> None:
|
|
|
83
82
|
default=False,
|
|
84
83
|
help="Enable the agent tool (disabled by default)"
|
|
85
84
|
)
|
|
85
|
+
|
|
86
|
+
_ = parser.add_argument(
|
|
87
|
+
"--disable-write-tools",
|
|
88
|
+
dest="disable_write_tools",
|
|
89
|
+
action="store_true",
|
|
90
|
+
default=False,
|
|
91
|
+
help="Disable write/edit tools (file writing, editing, notebook editing) to use IDE tools instead. Note: Shell commands can still modify files."
|
|
92
|
+
)
|
|
86
93
|
|
|
87
94
|
_ = parser.add_argument(
|
|
88
95
|
"--install",
|
|
@@ -103,29 +110,33 @@ def main() -> None:
|
|
|
103
110
|
agent_max_iterations: int = cast(int, args.agent_max_iterations)
|
|
104
111
|
agent_max_tool_uses: int = cast(int, args.agent_max_tool_uses)
|
|
105
112
|
enable_agent_tool: bool = cast(bool, args.enable_agent_tool)
|
|
113
|
+
disable_write_tools: bool = cast(bool, args.disable_write_tools)
|
|
106
114
|
allowed_paths: list[str] = (
|
|
107
115
|
cast(list[str], args.allowed_paths) if args.allowed_paths else []
|
|
108
116
|
)
|
|
109
117
|
|
|
110
118
|
if install:
|
|
111
|
-
install_claude_desktop_config(name, allowed_paths)
|
|
119
|
+
install_claude_desktop_config(name, allowed_paths, disable_write_tools)
|
|
112
120
|
return
|
|
113
121
|
|
|
114
122
|
# If no allowed paths are specified, use the user's home directory
|
|
115
123
|
if not allowed_paths:
|
|
116
124
|
allowed_paths = [str(Path.home())]
|
|
117
|
-
|
|
118
|
-
# Normalize all allowed paths
|
|
119
|
-
allowed_paths = [PathUtils.normalize_path(path) for path in allowed_paths]
|
|
120
125
|
|
|
121
|
-
# If project directory is specified,
|
|
126
|
+
# If project directory is specified, add it to allowed paths
|
|
127
|
+
if project_dir and project_dir not in allowed_paths:
|
|
128
|
+
allowed_paths.append(project_dir)
|
|
129
|
+
|
|
130
|
+
# Set project directory as initial working directory if provided
|
|
122
131
|
if project_dir:
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
132
|
+
# Expand user paths
|
|
133
|
+
project_dir = os.path.expanduser(project_dir)
|
|
134
|
+
# Make absolute
|
|
135
|
+
if not os.path.isabs(project_dir):
|
|
136
|
+
project_dir = os.path.abspath(project_dir)
|
|
126
137
|
|
|
127
138
|
# If no specific project directory, use the first allowed path
|
|
128
|
-
|
|
139
|
+
elif allowed_paths:
|
|
129
140
|
project_dir = allowed_paths[0]
|
|
130
141
|
|
|
131
142
|
# Run the server
|
|
@@ -138,20 +149,25 @@ def main() -> None:
|
|
|
138
149
|
agent_api_key=agent_api_key,
|
|
139
150
|
agent_max_iterations=agent_max_iterations,
|
|
140
151
|
agent_max_tool_uses=agent_max_tool_uses,
|
|
141
|
-
enable_agent_tool=enable_agent_tool
|
|
152
|
+
enable_agent_tool=enable_agent_tool,
|
|
153
|
+
disable_write_tools=disable_write_tools
|
|
142
154
|
)
|
|
143
155
|
# Transport will be automatically cast to Literal['stdio', 'sse'] by the server
|
|
144
156
|
server.run(transport=transport)
|
|
145
157
|
|
|
146
158
|
|
|
147
159
|
def install_claude_desktop_config(
|
|
148
|
-
name: str = "claude-code", allowed_paths: list[str] | None = None
|
|
160
|
+
name: str = "claude-code", allowed_paths: list[str] | None = None,
|
|
161
|
+
disable_write_tools: bool = False
|
|
149
162
|
) -> None:
|
|
150
163
|
"""Install the server configuration in Claude Desktop.
|
|
151
164
|
|
|
152
165
|
Args:
|
|
153
166
|
name: The name to use for the server in the config
|
|
154
167
|
allowed_paths: Optional list of paths to allow
|
|
168
|
+
disable_write_tools: Whether to disable write/edit tools (file writing, editing, notebook editing)
|
|
169
|
+
to use IDE tools instead. Note: Shell commands can still modify files.
|
|
170
|
+
(default: False)
|
|
155
171
|
"""
|
|
156
172
|
# Find the Claude Desktop config directory
|
|
157
173
|
home: Path = Path.home()
|
|
@@ -181,6 +197,10 @@ def install_claude_desktop_config(
|
|
|
181
197
|
else:
|
|
182
198
|
# Allow home directory by default
|
|
183
199
|
args.extend(["--allow-path", str(home)])
|
|
200
|
+
|
|
201
|
+
# Add disable_write_tools flag if specified
|
|
202
|
+
if disable_write_tools:
|
|
203
|
+
args.append("--disable-write-tools")
|
|
184
204
|
|
|
185
205
|
# Create config object
|
|
186
206
|
config: dict[str, Any] = {
|
|
@@ -6,7 +6,6 @@ from mcp.server.fastmcp import FastMCP
|
|
|
6
6
|
|
|
7
7
|
from hanzo_mcp.tools import register_all_tools
|
|
8
8
|
from hanzo_mcp.tools.common.context import DocumentContext
|
|
9
|
-
from hanzo_mcp.tools.common.path_utils import PathUtils
|
|
10
9
|
from hanzo_mcp.tools.common.permissions import PermissionManager
|
|
11
10
|
from hanzo_mcp.tools.project.analysis import ProjectAnalyzer, ProjectManager
|
|
12
11
|
from hanzo_mcp.tools.shell.command_executor import CommandExecutor
|
|
@@ -28,6 +27,7 @@ class HanzoServer:
|
|
|
28
27
|
agent_max_iterations: int = 10,
|
|
29
28
|
agent_max_tool_uses: int = 30,
|
|
30
29
|
enable_agent_tool: bool = False,
|
|
30
|
+
disable_write_tools: bool = False,
|
|
31
31
|
):
|
|
32
32
|
"""Initialize the Hanzo server.
|
|
33
33
|
|
|
@@ -42,6 +42,7 @@ class HanzoServer:
|
|
|
42
42
|
agent_max_iterations: Maximum number of iterations for agent (default: 10)
|
|
43
43
|
agent_max_tool_uses: Maximum number of total tool uses for agent (default: 30)
|
|
44
44
|
enable_agent_tool: Whether to enable the agent tool (default: False)
|
|
45
|
+
disable_write_tools: Whether to disable write/edit tools (default: False)
|
|
45
46
|
"""
|
|
46
47
|
self.mcp = mcp_instance if mcp_instance is not None else FastMCP(name)
|
|
47
48
|
|
|
@@ -71,10 +72,8 @@ class HanzoServer:
|
|
|
71
72
|
# Add allowed paths
|
|
72
73
|
if allowed_paths:
|
|
73
74
|
for path in allowed_paths:
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
self.permission_manager.add_allowed_path(normalized_path)
|
|
77
|
-
self.document_context.add_allowed_path(normalized_path)
|
|
75
|
+
self.permission_manager.add_allowed_path(path)
|
|
76
|
+
self.document_context.add_allowed_path(path)
|
|
78
77
|
|
|
79
78
|
# Store agent options
|
|
80
79
|
self.agent_model = agent_model
|
|
@@ -83,6 +82,7 @@ class HanzoServer:
|
|
|
83
82
|
self.agent_max_iterations = agent_max_iterations
|
|
84
83
|
self.agent_max_tool_uses = agent_max_tool_uses
|
|
85
84
|
self.enable_agent_tool = enable_agent_tool
|
|
85
|
+
self.disable_write_tools = disable_write_tools
|
|
86
86
|
|
|
87
87
|
# Register all tools
|
|
88
88
|
register_all_tools(
|
|
@@ -95,6 +95,7 @@ class HanzoServer:
|
|
|
95
95
|
agent_max_iterations=self.agent_max_iterations,
|
|
96
96
|
agent_max_tool_uses=self.agent_max_tool_uses,
|
|
97
97
|
enable_agent_tool=self.enable_agent_tool,
|
|
98
|
+
disable_write_tools=self.disable_write_tools,
|
|
98
99
|
)
|
|
99
100
|
|
|
100
101
|
def run(self, transport: str = "stdio", allowed_paths: list[str] | None = None):
|
|
@@ -107,10 +108,8 @@ class HanzoServer:
|
|
|
107
108
|
# Add allowed paths if provided
|
|
108
109
|
allowed_paths_list = allowed_paths or []
|
|
109
110
|
for path in allowed_paths_list:
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
self.permission_manager.add_allowed_path(normalized_path)
|
|
113
|
-
self.document_context.add_allowed_path(normalized_path)
|
|
111
|
+
self.permission_manager.add_allowed_path(path)
|
|
112
|
+
self.document_context.add_allowed_path(path)
|
|
114
113
|
|
|
115
114
|
# Run the server
|
|
116
115
|
transport_type = cast(Literal["stdio", "sse"], transport)
|
|
@@ -32,6 +32,7 @@ def register_all_tools(
|
|
|
32
32
|
agent_max_iterations: int = 10,
|
|
33
33
|
agent_max_tool_uses: int = 30,
|
|
34
34
|
enable_agent_tool: bool = False,
|
|
35
|
+
disable_write_tools: bool = False,
|
|
35
36
|
) -> None:
|
|
36
37
|
"""Register all Hanzo tools with the MCP server.
|
|
37
38
|
|
|
@@ -45,12 +46,13 @@ def register_all_tools(
|
|
|
45
46
|
agent_max_iterations: Maximum number of iterations for agent (default: 10)
|
|
46
47
|
agent_max_tool_uses: Maximum number of total tool uses for agent (default: 30)
|
|
47
48
|
enable_agent_tool: Whether to enable the agent tool (default: False)
|
|
49
|
+
disable_write_tools: Whether to disable write/edit tools (default: False)
|
|
48
50
|
"""
|
|
49
51
|
# Register all filesystem tools
|
|
50
|
-
register_filesystem_tools(mcp_server, document_context, permission_manager)
|
|
52
|
+
register_filesystem_tools(mcp_server, document_context, permission_manager, disable_write_tools)
|
|
51
53
|
|
|
52
54
|
# Register all jupyter tools
|
|
53
|
-
register_jupyter_tools(mcp_server, document_context, permission_manager)
|
|
55
|
+
register_jupyter_tools(mcp_server, document_context, permission_manager, disable_write_tools)
|
|
54
56
|
|
|
55
57
|
# Register shell tools
|
|
56
58
|
register_shell_tools(mcp_server, permission_manager)
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
from mcp.server.fastmcp import FastMCP
|
|
4
4
|
|
|
5
5
|
from hanzo_mcp.tools.common.base import ToolRegistry
|
|
6
|
-
from hanzo_mcp.tools.common.path_utils import PathUtils
|
|
7
6
|
from hanzo_mcp.tools.common.think_tool import ThinkingTool
|
|
8
7
|
from hanzo_mcp.tools.common.version_tool import VersionTool
|
|
9
8
|
|
|
@@ -14,8 +14,6 @@ from typing import Any, ClassVar, final
|
|
|
14
14
|
from mcp.server.fastmcp import Context as MCPContext
|
|
15
15
|
from mcp.server.lowlevel.helper_types import ReadResourceContents
|
|
16
16
|
|
|
17
|
-
from hanzo_mcp.tools.common.path_utils import PathUtils
|
|
18
|
-
|
|
19
17
|
|
|
20
18
|
@final
|
|
21
19
|
class ToolContext:
|
|
@@ -181,9 +179,9 @@ class DocumentContext:
|
|
|
181
179
|
Args:
|
|
182
180
|
path: The path to allow
|
|
183
181
|
"""
|
|
184
|
-
#
|
|
185
|
-
|
|
186
|
-
resolved_path: Path = Path(
|
|
182
|
+
# Expand user path (e.g., ~/ or $HOME)
|
|
183
|
+
expanded_path = os.path.expanduser(path)
|
|
184
|
+
resolved_path: Path = Path(expanded_path).resolve()
|
|
187
185
|
self.allowed_paths.add(resolved_path)
|
|
188
186
|
|
|
189
187
|
def is_path_allowed(self, path: str) -> bool:
|
|
@@ -195,9 +193,9 @@ class DocumentContext:
|
|
|
195
193
|
Returns:
|
|
196
194
|
True if the path is allowed, False otherwise
|
|
197
195
|
"""
|
|
198
|
-
#
|
|
199
|
-
|
|
200
|
-
resolved_path: Path = Path(
|
|
196
|
+
# Expand user path (e.g., ~/ or $HOME)
|
|
197
|
+
expanded_path = os.path.expanduser(path)
|
|
198
|
+
resolved_path: Path = Path(expanded_path).resolve()
|
|
201
199
|
|
|
202
200
|
# Check if the path is within any allowed path
|
|
203
201
|
for allowed_path in self.allowed_paths:
|
|
@@ -6,8 +6,6 @@ from collections.abc import Awaitable, Callable
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import Any, TypeVar, final
|
|
8
8
|
|
|
9
|
-
from hanzo_mcp.tools.common.path_utils import PathUtils
|
|
10
|
-
|
|
11
9
|
# Define type variables for better type annotations
|
|
12
10
|
T = TypeVar("T")
|
|
13
11
|
P = TypeVar("P")
|
|
@@ -71,9 +69,9 @@ class PermissionManager:
|
|
|
71
69
|
Args:
|
|
72
70
|
path: The path to allow
|
|
73
71
|
"""
|
|
74
|
-
#
|
|
75
|
-
|
|
76
|
-
resolved_path: Path = Path(
|
|
72
|
+
# Expand user path (e.g., ~/ or $HOME)
|
|
73
|
+
expanded_path = os.path.expanduser(path)
|
|
74
|
+
resolved_path: Path = Path(expanded_path).resolve()
|
|
77
75
|
self.allowed_paths.add(resolved_path)
|
|
78
76
|
|
|
79
77
|
def remove_allowed_path(self, path: str) -> None:
|
|
@@ -112,9 +110,9 @@ class PermissionManager:
|
|
|
112
110
|
Returns:
|
|
113
111
|
True if the path is allowed, False otherwise
|
|
114
112
|
"""
|
|
115
|
-
#
|
|
116
|
-
|
|
117
|
-
resolved_path: Path = Path(
|
|
113
|
+
# Expand user path (e.g., ~/ or $HOME)
|
|
114
|
+
expanded_path = os.path.expanduser(path)
|
|
115
|
+
resolved_path: Path = Path(expanded_path).resolve()
|
|
118
116
|
|
|
119
117
|
# Check exclusions first
|
|
120
118
|
if self._is_path_excluded(resolved_path):
|
|
@@ -77,6 +77,7 @@ def register_filesystem_tools(
|
|
|
77
77
|
mcp_server: FastMCP,
|
|
78
78
|
document_context: DocumentContext,
|
|
79
79
|
permission_manager: PermissionManager,
|
|
80
|
+
disable_write_tools: bool = False,
|
|
80
81
|
) -> None:
|
|
81
82
|
"""Register all filesystem tools with the MCP server.
|
|
82
83
|
|
|
@@ -84,6 +85,10 @@ def register_filesystem_tools(
|
|
|
84
85
|
mcp_server: The FastMCP server instance
|
|
85
86
|
document_context: Document context for tracking file contents
|
|
86
87
|
permission_manager: Permission manager for access control
|
|
88
|
+
disable_write_tools: Whether to disable write/edit tools (default: False)
|
|
87
89
|
"""
|
|
88
|
-
|
|
90
|
+
if disable_write_tools:
|
|
91
|
+
tools = get_read_only_filesystem_tools(document_context, permission_manager)
|
|
92
|
+
else:
|
|
93
|
+
tools = get_filesystem_tools(document_context, permission_manager)
|
|
89
94
|
ToolRegistry.register_tools(mcp_server, tools)
|
|
@@ -3,14 +3,12 @@
|
|
|
3
3
|
This module provides the DirectoryTreeTool for viewing file and directory structures.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
import os
|
|
7
6
|
from pathlib import Path
|
|
8
7
|
from typing import Any, final, override
|
|
9
8
|
|
|
10
9
|
from mcp.server.fastmcp import Context as MCPContext
|
|
11
10
|
from mcp.server.fastmcp import FastMCP
|
|
12
11
|
|
|
13
|
-
from hanzo_mcp.tools.common.path_utils import PathUtils
|
|
14
12
|
from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
|
|
15
13
|
|
|
16
14
|
|
|
@@ -139,47 +137,17 @@ requested. Only works within allowed directories."""
|
|
|
139
137
|
if not is_dir:
|
|
140
138
|
return error_msg
|
|
141
139
|
|
|
142
|
-
# Define filtered directories
|
|
140
|
+
# Define filtered directories
|
|
143
141
|
FILTERED_DIRECTORIES = {
|
|
144
|
-
|
|
145
|
-
"
|
|
146
|
-
".
|
|
147
|
-
".
|
|
148
|
-
".coverage", ".tox", ".nox", ".circleci", ".llm-context",
|
|
149
|
-
# Cache directories
|
|
150
|
-
"__pycache__", ".ipynb_checkpoints", "htmlcov", ".eggs",
|
|
151
|
-
# Build artifacts
|
|
152
|
-
"dist", "build", "target", "out", "site", "coverage",
|
|
153
|
-
# Dependency directories
|
|
154
|
-
"node_modules", "venv", "env", "ENV", "lib", "libs", "vendor",
|
|
155
|
-
"eggs", "sdist", "wheels", "share"
|
|
142
|
+
".git", "node_modules", ".venv", "venv",
|
|
143
|
+
"__pycache__", ".pytest_cache", ".idea",
|
|
144
|
+
".vs", ".vscode", "dist", "build", "target",
|
|
145
|
+
".ruff_cache",".llm-context"
|
|
156
146
|
}
|
|
157
147
|
|
|
158
148
|
# Log filtering settings
|
|
159
149
|
await tool_ctx.info(f"Directory tree filtering: include_filtered={include_filtered}")
|
|
160
150
|
|
|
161
|
-
# Try to get additional patterns from .gitignore if it exists
|
|
162
|
-
gitignore_patterns = set()
|
|
163
|
-
gitignore_path = dir_path / ".gitignore"
|
|
164
|
-
if gitignore_path.exists() and gitignore_path.is_file():
|
|
165
|
-
try:
|
|
166
|
-
with open(gitignore_path, "r") as f:
|
|
167
|
-
for line in f:
|
|
168
|
-
line = line.strip()
|
|
169
|
-
if line and not line.startswith("#"):
|
|
170
|
-
# Strip trailing slashes for directory patterns
|
|
171
|
-
if line.endswith("/"):
|
|
172
|
-
line = line[:-1]
|
|
173
|
-
# Extract the actual pattern without path
|
|
174
|
-
pattern = line.split("/")[-1]
|
|
175
|
-
if pattern and "*" not in pattern and "?" not in pattern:
|
|
176
|
-
gitignore_patterns.add(pattern)
|
|
177
|
-
except Exception as e:
|
|
178
|
-
await tool_ctx.warning(f"Error reading .gitignore: {str(e)}")
|
|
179
|
-
|
|
180
|
-
# Add gitignore patterns to filtered directories
|
|
181
|
-
FILTERED_DIRECTORIES.update(gitignore_patterns)
|
|
182
|
-
|
|
183
151
|
# Check if a directory should be filtered
|
|
184
152
|
def should_filter(current_path: Path) -> bool:
|
|
185
153
|
# Don't filter if it's the explicitly requested path
|
|
@@ -187,15 +155,8 @@ requested. Only works within allowed directories."""
|
|
|
187
155
|
# Don't filter explicitly requested paths
|
|
188
156
|
return False
|
|
189
157
|
|
|
190
|
-
#
|
|
191
|
-
|
|
192
|
-
return True
|
|
193
|
-
|
|
194
|
-
# Also filter hidden directories (dot directories) unless explicitly included
|
|
195
|
-
if PathUtils.is_dot_directory(current_path) and not include_filtered:
|
|
196
|
-
return True
|
|
197
|
-
|
|
198
|
-
return False
|
|
158
|
+
# Filter based on directory name if filtering is enabled
|
|
159
|
+
return current_path.name in FILTERED_DIRECTORIES and not include_filtered
|
|
199
160
|
|
|
200
161
|
# Track stats for summary
|
|
201
162
|
stats = {
|
|
@@ -59,6 +59,7 @@ def register_jupyter_tools(
|
|
|
59
59
|
mcp_server: FastMCP,
|
|
60
60
|
document_context: DocumentContext,
|
|
61
61
|
permission_manager: PermissionManager,
|
|
62
|
+
disable_write_tools: bool = False,
|
|
62
63
|
) -> None:
|
|
63
64
|
"""Register all Jupyter notebook tools with the MCP server.
|
|
64
65
|
|
|
@@ -66,6 +67,10 @@ def register_jupyter_tools(
|
|
|
66
67
|
mcp_server: The FastMCP server instance
|
|
67
68
|
document_context: Document context for tracking file contents
|
|
68
69
|
permission_manager: Permission manager for access control
|
|
70
|
+
disable_write_tools: Whether to disable write/edit tools (default: False)
|
|
69
71
|
"""
|
|
70
|
-
|
|
72
|
+
if disable_write_tools:
|
|
73
|
+
tools = get_read_only_jupyter_tools(document_context, permission_manager)
|
|
74
|
+
else:
|
|
75
|
+
tools = get_jupyter_tools(document_context, permission_manager)
|
|
71
76
|
ToolRegistry.register_tools(mcp_server, tools)
|
|
@@ -14,8 +14,6 @@ from collections.abc import Awaitable, Callable
|
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
from typing import Dict, Optional, final
|
|
16
16
|
|
|
17
|
-
from hanzo_mcp.tools.common.path_utils import PathUtils
|
|
18
|
-
|
|
19
17
|
from mcp.server.fastmcp import Context as MCPContext
|
|
20
18
|
from mcp.server.fastmcp import FastMCP
|
|
21
19
|
|
|
@@ -136,8 +134,8 @@ class CommandExecutor:
|
|
|
136
134
|
path: The path to set as the current working directory
|
|
137
135
|
"""
|
|
138
136
|
session = self.get_session_manager(session_id)
|
|
139
|
-
|
|
140
|
-
session.set_working_dir(Path(
|
|
137
|
+
expanded_path = os.path.expanduser(path)
|
|
138
|
+
session.set_working_dir(Path(expanded_path))
|
|
141
139
|
|
|
142
140
|
def get_working_dir(self, session_id: str) -> str:
|
|
143
141
|
"""Get the current working directory for the session.
|
|
@@ -251,8 +249,11 @@ class CommandExecutor:
|
|
|
251
249
|
session_cwd = self.get_working_dir(session_id)
|
|
252
250
|
target_dir = os.path.join(session_cwd, target_dir)
|
|
253
251
|
|
|
254
|
-
#
|
|
255
|
-
target_dir =
|
|
252
|
+
# Expand user paths
|
|
253
|
+
target_dir = os.path.expanduser(target_dir)
|
|
254
|
+
|
|
255
|
+
# Normalize path
|
|
256
|
+
target_dir = os.path.normpath(target_dir)
|
|
256
257
|
|
|
257
258
|
if os.path.isdir(target_dir):
|
|
258
259
|
self.set_working_dir(session_id, target_dir)
|
|
@@ -19,7 +19,6 @@ hanzo_mcp/tools/agent/tool_adapter.py
|
|
|
19
19
|
hanzo_mcp/tools/common/__init__.py
|
|
20
20
|
hanzo_mcp/tools/common/base.py
|
|
21
21
|
hanzo_mcp/tools/common/context.py
|
|
22
|
-
hanzo_mcp/tools/common/path_utils.py
|
|
23
22
|
hanzo_mcp/tools/common/permissions.py
|
|
24
23
|
hanzo_mcp/tools/common/session.py
|
|
25
24
|
hanzo_mcp/tools/common/think_tool.py
|
|
@@ -51,4 +50,5 @@ hanzo_mcp/tools/shell/run_script.py
|
|
|
51
50
|
hanzo_mcp/tools/shell/script_tool.py
|
|
52
51
|
tests/test_cli.py
|
|
53
52
|
tests/test_server.py
|
|
53
|
+
tests/test_tools_registration.py
|
|
54
54
|
tests/test_validation.py
|
|
@@ -33,6 +33,7 @@ class TestCLI:
|
|
|
33
33
|
mock_args.agent_max_iterations = 10
|
|
34
34
|
mock_args.agent_max_tool_uses = 30
|
|
35
35
|
mock_args.enable_agent_tool = False
|
|
36
|
+
mock_args.disable_write_tools = False
|
|
36
37
|
mock_parse_args.return_value = mock_args
|
|
37
38
|
|
|
38
39
|
# Mock server instance
|
|
@@ -53,7 +54,8 @@ class TestCLI:
|
|
|
53
54
|
agent_api_key="test_api_key",
|
|
54
55
|
agent_max_iterations=10,
|
|
55
56
|
agent_max_tool_uses=30,
|
|
56
|
-
enable_agent_tool=False
|
|
57
|
+
enable_agent_tool=False,
|
|
58
|
+
disable_write_tools=False
|
|
57
59
|
)
|
|
58
60
|
mock_server.run.assert_called_once_with(transport="stdio")
|
|
59
61
|
|
|
@@ -96,6 +98,7 @@ class TestCLI:
|
|
|
96
98
|
mock_args.agent_max_iterations = 10
|
|
97
99
|
mock_args.agent_max_tool_uses = 30
|
|
98
100
|
mock_args.enable_agent_tool = False
|
|
101
|
+
mock_args.disable_write_tools = False
|
|
99
102
|
mock_parse_args.return_value = mock_args
|
|
100
103
|
|
|
101
104
|
# Mock server instance
|
|
@@ -114,7 +117,52 @@ class TestCLI:
|
|
|
114
117
|
agent_api_key=None,
|
|
115
118
|
agent_max_iterations=10,
|
|
116
119
|
agent_max_tool_uses=30,
|
|
117
|
-
enable_agent_tool=False
|
|
120
|
+
enable_agent_tool=False,
|
|
121
|
+
disable_write_tools=False
|
|
122
|
+
)
|
|
123
|
+
mock_server.run.assert_called_once_with(transport="stdio")
|
|
124
|
+
|
|
125
|
+
def test_main_with_disable_write_tools(self) -> None:
|
|
126
|
+
"""Test the main function with disable_write_tools=True."""
|
|
127
|
+
with (
|
|
128
|
+
patch("argparse.ArgumentParser.parse_args") as mock_parse_args,
|
|
129
|
+
patch("hanzo_mcp.cli.HanzoServer") as mock_server_class,
|
|
130
|
+
):
|
|
131
|
+
# Mock parsed arguments
|
|
132
|
+
mock_args = MagicMock()
|
|
133
|
+
mock_args.name = "test-server"
|
|
134
|
+
mock_args.transport = "stdio"
|
|
135
|
+
mock_args.allowed_paths = ["/test/path"]
|
|
136
|
+
mock_args.project_dir = "/test/project"
|
|
137
|
+
mock_args.install = False
|
|
138
|
+
mock_args.agent_model = None
|
|
139
|
+
mock_args.agent_max_tokens = None
|
|
140
|
+
mock_args.agent_api_key = None
|
|
141
|
+
mock_args.agent_max_iterations = 10
|
|
142
|
+
mock_args.agent_max_tool_uses = 30
|
|
143
|
+
mock_args.enable_agent_tool = False
|
|
144
|
+
mock_args.disable_write_tools = True
|
|
145
|
+
mock_parse_args.return_value = mock_args
|
|
146
|
+
|
|
147
|
+
# Mock server instance
|
|
148
|
+
mock_server = MagicMock()
|
|
149
|
+
mock_server_class.return_value = mock_server
|
|
150
|
+
|
|
151
|
+
# Call main
|
|
152
|
+
main()
|
|
153
|
+
|
|
154
|
+
# Verify server was created with disable_write_tools=True
|
|
155
|
+
expected_paths = ["/test/path", "/test/project"]
|
|
156
|
+
mock_server_class.assert_called_once_with(
|
|
157
|
+
name="test-server",
|
|
158
|
+
allowed_paths=expected_paths,
|
|
159
|
+
agent_model=None,
|
|
160
|
+
agent_max_tokens=None,
|
|
161
|
+
agent_api_key=None,
|
|
162
|
+
agent_max_iterations=10,
|
|
163
|
+
agent_max_tool_uses=30,
|
|
164
|
+
enable_agent_tool=False,
|
|
165
|
+
disable_write_tools=True
|
|
118
166
|
)
|
|
119
167
|
mock_server.run.assert_called_once_with(transport="stdio")
|
|
120
168
|
|
|
@@ -313,4 +361,47 @@ class TestInstallClaudeDesktopConfig:
|
|
|
313
361
|
# Verify home directory was added as an allowed path
|
|
314
362
|
assert "--allow-path" in server_args
|
|
315
363
|
home_path_index = server_args.index("--allow-path") + 1
|
|
316
|
-
assert str(tmp_path) in server_args[home_path_index]
|
|
364
|
+
assert str(tmp_path) in server_args[home_path_index]
|
|
365
|
+
|
|
366
|
+
# Verify --disable-write-tools flag is not present
|
|
367
|
+
assert "--disable-write-tools" not in server_args
|
|
368
|
+
|
|
369
|
+
def test_install_config_with_disable_write_tools(
|
|
370
|
+
self, mock_platform: Callable[[str], str], tmp_path: Path
|
|
371
|
+
) -> None:
|
|
372
|
+
"""Test installing config with disable_write_tools=True."""
|
|
373
|
+
# Set platform to macOS
|
|
374
|
+
mock_platform("darwin")
|
|
375
|
+
|
|
376
|
+
# Mock home directory and config path
|
|
377
|
+
with (
|
|
378
|
+
patch("pathlib.Path.home", return_value=Path(tmp_path)),
|
|
379
|
+
patch("sys.executable", "/usr/bin/python3"),
|
|
380
|
+
patch("json.dump") as mock_json_dump,
|
|
381
|
+
patch("builtins.open", create=True) as mock_open,
|
|
382
|
+
patch("pathlib.Path.exists", return_value=False),
|
|
383
|
+
patch("pathlib.Path.mkdir"),
|
|
384
|
+
):
|
|
385
|
+
# Mock file opening
|
|
386
|
+
mock_file = MagicMock()
|
|
387
|
+
mock_open.return_value.__enter__.return_value = mock_file
|
|
388
|
+
|
|
389
|
+
# Call the install function with disable_write_tools=True
|
|
390
|
+
install_claude_desktop_config(
|
|
391
|
+
"test-server",
|
|
392
|
+
allowed_paths=["/test/path"],
|
|
393
|
+
disable_write_tools=True
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
# Verify correct config was written
|
|
397
|
+
mock_json_dump.assert_called_once()
|
|
398
|
+
config_data = mock_json_dump.call_args[0][0]
|
|
399
|
+
server_args = config_data["mcpServers"]["test-server"]["args"]
|
|
400
|
+
|
|
401
|
+
# Verify allowed path was added
|
|
402
|
+
assert "--allow-path" in server_args
|
|
403
|
+
path_index = server_args.index("--allow-path") + 1
|
|
404
|
+
assert "/test/path" in server_args[path_index]
|
|
405
|
+
|
|
406
|
+
# Verify --disable-write-tools flag is present
|
|
407
|
+
assert "--disable-write-tools" in server_args
|
|
@@ -35,6 +35,26 @@ class TestHanzoServer:
|
|
|
35
35
|
assert server_instance.command_executor is not None
|
|
36
36
|
assert server_instance.project_analyzer is not None
|
|
37
37
|
assert server_instance.project_manager is not None
|
|
38
|
+
|
|
39
|
+
def test_initialization_with_disable_write_tools(self) -> None:
|
|
40
|
+
"""Test initializing HanzoServer with disable_write_tools=True."""
|
|
41
|
+
with patch("mcp.server.fastmcp.FastMCP") as mock_fastmcp, \
|
|
42
|
+
patch("hanzo_mcp.tools.register_all_tools") as mock_register_all_tools:
|
|
43
|
+
# Create a mock FastMCP instance
|
|
44
|
+
mock_mcp = MagicMock()
|
|
45
|
+
mock_fastmcp.return_value = mock_mcp
|
|
46
|
+
|
|
47
|
+
# Create the server with disable_write_tools=True
|
|
48
|
+
server = HanzoServer(
|
|
49
|
+
name="test-server",
|
|
50
|
+
mcp_instance=mock_mcp,
|
|
51
|
+
disable_write_tools=True
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Verify that the disable_write_tools flag was passed to register_all_tools
|
|
55
|
+
mock_register_all_tools.assert_called_once()
|
|
56
|
+
args, kwargs = mock_register_all_tools.call_args
|
|
57
|
+
assert kwargs.get("disable_write_tools") is True
|
|
38
58
|
|
|
39
59
|
def test_initialization_with_allowed_paths(self) -> None:
|
|
40
60
|
"""Test initializing with allowed paths."""
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Tests for the tools registration process."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import MagicMock, patch
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from hanzo_mcp.tools import register_all_tools
|
|
8
|
+
from hanzo_mcp.tools.common.context import DocumentContext
|
|
9
|
+
from hanzo_mcp.tools.common.permissions import PermissionManager
|
|
10
|
+
from hanzo_mcp.tools.filesystem import (
|
|
11
|
+
ReadFilesTool,
|
|
12
|
+
WriteFileTool,
|
|
13
|
+
EditFileTool,
|
|
14
|
+
DirectoryTreeTool,
|
|
15
|
+
GetFileInfoTool,
|
|
16
|
+
SearchContentTool,
|
|
17
|
+
ContentReplaceTool
|
|
18
|
+
)
|
|
19
|
+
from hanzo_mcp.tools.jupyter import (
|
|
20
|
+
ReadNotebookTool,
|
|
21
|
+
EditNotebookTool
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TestToolsRegistration:
|
|
26
|
+
"""Test the tools registration process."""
|
|
27
|
+
|
|
28
|
+
@pytest.fixture
|
|
29
|
+
def mcp_server(self):
|
|
30
|
+
"""Create a mock MCP server."""
|
|
31
|
+
server = MagicMock()
|
|
32
|
+
return server
|
|
33
|
+
|
|
34
|
+
@pytest.fixture
|
|
35
|
+
def document_context(self):
|
|
36
|
+
"""Create a document context."""
|
|
37
|
+
return DocumentContext()
|
|
38
|
+
|
|
39
|
+
@pytest.fixture
|
|
40
|
+
def permission_manager(self):
|
|
41
|
+
"""Create a permission manager."""
|
|
42
|
+
return PermissionManager()
|
|
43
|
+
|
|
44
|
+
def test_register_all_tools_default(
|
|
45
|
+
self, mcp_server, document_context, permission_manager
|
|
46
|
+
):
|
|
47
|
+
"""Test registering all tools with default settings."""
|
|
48
|
+
# Mock the tool registry to capture registered tools
|
|
49
|
+
registered_tools = []
|
|
50
|
+
|
|
51
|
+
with patch("hanzo_mcp.tools.ToolRegistry.register_tools") as mock_register:
|
|
52
|
+
mock_register.side_effect = lambda _, tools: registered_tools.extend(tools)
|
|
53
|
+
|
|
54
|
+
# Register all tools with default settings
|
|
55
|
+
register_all_tools(
|
|
56
|
+
mcp_server=mcp_server,
|
|
57
|
+
document_context=document_context,
|
|
58
|
+
permission_manager=permission_manager
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Check that all filesystem tools are registered
|
|
62
|
+
fs_tool_types = [type(tool) for tool in registered_tools
|
|
63
|
+
if isinstance(tool, (ReadFilesTool, WriteFileTool, EditFileTool,
|
|
64
|
+
DirectoryTreeTool, GetFileInfoTool,
|
|
65
|
+
SearchContentTool, ContentReplaceTool))]
|
|
66
|
+
|
|
67
|
+
assert ReadFilesTool in fs_tool_types
|
|
68
|
+
assert WriteFileTool in fs_tool_types
|
|
69
|
+
assert EditFileTool in fs_tool_types
|
|
70
|
+
|
|
71
|
+
# Check that all Jupyter tools are registered
|
|
72
|
+
jupyter_tool_types = [type(tool) for tool in registered_tools
|
|
73
|
+
if isinstance(tool, (ReadNotebookTool, EditNotebookTool))]
|
|
74
|
+
|
|
75
|
+
assert ReadNotebookTool in jupyter_tool_types
|
|
76
|
+
assert EditNotebookTool in jupyter_tool_types
|
|
77
|
+
|
|
78
|
+
def test_register_all_tools_disable_write_tools(
|
|
79
|
+
self, mcp_server, document_context, permission_manager
|
|
80
|
+
):
|
|
81
|
+
"""Test registering all tools with disable_write_tools=True."""
|
|
82
|
+
# Mock the tool registry to capture registered tools
|
|
83
|
+
registered_tools = []
|
|
84
|
+
|
|
85
|
+
with patch("hanzo_mcp.tools.ToolRegistry.register_tools") as mock_register:
|
|
86
|
+
mock_register.side_effect = lambda _, tools: registered_tools.extend(tools)
|
|
87
|
+
|
|
88
|
+
# Register all tools with disable_write_tools=True
|
|
89
|
+
register_all_tools(
|
|
90
|
+
mcp_server=mcp_server,
|
|
91
|
+
document_context=document_context,
|
|
92
|
+
permission_manager=permission_manager,
|
|
93
|
+
disable_write_tools=True
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Check that only read-only filesystem tools are registered
|
|
97
|
+
fs_tool_types = [type(tool) for tool in registered_tools]
|
|
98
|
+
|
|
99
|
+
# Read-only tools should be present
|
|
100
|
+
assert ReadFilesTool in fs_tool_types
|
|
101
|
+
assert DirectoryTreeTool in fs_tool_types
|
|
102
|
+
assert GetFileInfoTool in fs_tool_types
|
|
103
|
+
assert SearchContentTool in fs_tool_types
|
|
104
|
+
|
|
105
|
+
# Write tools should not be present
|
|
106
|
+
assert WriteFileTool not in fs_tool_types
|
|
107
|
+
assert EditFileTool not in fs_tool_types
|
|
108
|
+
assert ContentReplaceTool not in fs_tool_types
|
|
109
|
+
|
|
110
|
+
# Check that only read-only Jupyter tools are registered
|
|
111
|
+
jupyter_tool_types = [type(tool) for tool in registered_tools]
|
|
112
|
+
|
|
113
|
+
# Read-only tools should be present
|
|
114
|
+
assert ReadNotebookTool in jupyter_tool_types
|
|
115
|
+
|
|
116
|
+
# Write tools should not be present
|
|
117
|
+
assert EditNotebookTool not in jupyter_tool_types
|
|
118
|
+
|
|
119
|
+
def test_register_filesystem_tools_with_disabled_write(
|
|
120
|
+
self, mcp_server, document_context, permission_manager
|
|
121
|
+
):
|
|
122
|
+
"""Test registering filesystem tools with disable_write_tools=True."""
|
|
123
|
+
from hanzo_mcp.tools.filesystem import register_filesystem_tools
|
|
124
|
+
|
|
125
|
+
# Mock the tool registry to capture registered tools
|
|
126
|
+
registered_tools = []
|
|
127
|
+
|
|
128
|
+
with patch("hanzo_mcp.tools.filesystem.ToolRegistry.register_tools") as mock_register:
|
|
129
|
+
mock_register.side_effect = lambda _, tools: registered_tools.extend(tools)
|
|
130
|
+
|
|
131
|
+
# Register filesystem tools with disable_write_tools=True
|
|
132
|
+
register_filesystem_tools(
|
|
133
|
+
mcp_server=mcp_server,
|
|
134
|
+
document_context=document_context,
|
|
135
|
+
permission_manager=permission_manager,
|
|
136
|
+
disable_write_tools=True
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Check that only read-only tools are registered
|
|
140
|
+
tool_types = [type(tool) for tool in registered_tools]
|
|
141
|
+
|
|
142
|
+
# Read-only tools should be present
|
|
143
|
+
assert ReadFilesTool in tool_types
|
|
144
|
+
assert DirectoryTreeTool in tool_types
|
|
145
|
+
assert GetFileInfoTool in tool_types
|
|
146
|
+
assert SearchContentTool in tool_types
|
|
147
|
+
|
|
148
|
+
# Write tools should not be present
|
|
149
|
+
assert WriteFileTool not in tool_types
|
|
150
|
+
assert EditFileTool not in tool_types
|
|
151
|
+
assert ContentReplaceTool not in tool_types
|
|
152
|
+
|
|
153
|
+
def test_register_jupyter_tools_with_disabled_write(
|
|
154
|
+
self, mcp_server, document_context, permission_manager
|
|
155
|
+
):
|
|
156
|
+
"""Test registering Jupyter tools with disable_write_tools=True."""
|
|
157
|
+
from hanzo_mcp.tools.jupyter import register_jupyter_tools
|
|
158
|
+
|
|
159
|
+
# Mock the tool registry to capture registered tools
|
|
160
|
+
registered_tools = []
|
|
161
|
+
|
|
162
|
+
with patch("hanzo_mcp.tools.jupyter.ToolRegistry.register_tools") as mock_register:
|
|
163
|
+
mock_register.side_effect = lambda _, tools: registered_tools.extend(tools)
|
|
164
|
+
|
|
165
|
+
# Register Jupyter tools with disable_write_tools=True
|
|
166
|
+
register_jupyter_tools(
|
|
167
|
+
mcp_server=mcp_server,
|
|
168
|
+
document_context=document_context,
|
|
169
|
+
permission_manager=permission_manager,
|
|
170
|
+
disable_write_tools=True
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Check that only read-only tools are registered
|
|
174
|
+
tool_types = [type(tool) for tool in registered_tools]
|
|
175
|
+
|
|
176
|
+
# Read-only tools should be present
|
|
177
|
+
assert ReadNotebookTool in tool_types
|
|
178
|
+
|
|
179
|
+
# Write tools should not be present
|
|
180
|
+
assert EditNotebookTool not in tool_types
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
"""Path utilities for Hanzo MCP.
|
|
2
|
-
|
|
3
|
-
This module provides path normalization and validation utilities.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
import os
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
from typing import final
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@final
|
|
12
|
-
class PathUtils:
|
|
13
|
-
"""Utilities for path handling."""
|
|
14
|
-
|
|
15
|
-
@staticmethod
|
|
16
|
-
def normalize_path(path: str) -> str:
|
|
17
|
-
"""Normalize a path by expanding user paths and making it absolute.
|
|
18
|
-
|
|
19
|
-
Args:
|
|
20
|
-
path: The path to normalize
|
|
21
|
-
|
|
22
|
-
Returns:
|
|
23
|
-
The normalized path
|
|
24
|
-
"""
|
|
25
|
-
# Expand user paths (e.g., ~/ or $HOME)
|
|
26
|
-
expanded_path = os.path.expanduser(path)
|
|
27
|
-
|
|
28
|
-
# Make the path absolute if it isn't already
|
|
29
|
-
if not os.path.isabs(expanded_path):
|
|
30
|
-
expanded_path = os.path.abspath(expanded_path)
|
|
31
|
-
|
|
32
|
-
# Normalize the path (resolve symlinks, etc.)
|
|
33
|
-
try:
|
|
34
|
-
normalized_path = os.path.normpath(expanded_path)
|
|
35
|
-
return normalized_path
|
|
36
|
-
except Exception:
|
|
37
|
-
# Return the expanded path if normalization fails
|
|
38
|
-
return expanded_path
|
|
39
|
-
|
|
40
|
-
@staticmethod
|
|
41
|
-
def is_dot_directory(path: Path) -> bool:
|
|
42
|
-
"""Check if a path is a dot directory (e.g., .git, .vscode).
|
|
43
|
-
|
|
44
|
-
Args:
|
|
45
|
-
path: The path to check
|
|
46
|
-
|
|
47
|
-
Returns:
|
|
48
|
-
True if the path is a dot directory, False otherwise
|
|
49
|
-
"""
|
|
50
|
-
# Consider any directory starting with "." to be a dot directory
|
|
51
|
-
return path.is_dir() and path.name.startswith(".")
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|