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.

Files changed (57) hide show
  1. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/PKG-INFO +1 -1
  2. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/__init__.py +1 -1
  3. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/cli.py +32 -12
  4. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/server.py +8 -9
  5. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/__init__.py +4 -2
  6. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/common/__init__.py +0 -1
  7. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/common/context.py +6 -8
  8. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/common/permissions.py +6 -8
  9. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/filesystem/__init__.py +6 -1
  10. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/filesystem/directory_tree.py +7 -46
  11. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/jupyter/__init__.py +6 -1
  12. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/shell/command_executor.py +7 -6
  13. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp.egg-info/PKG-INFO +1 -1
  14. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp.egg-info/SOURCES.txt +1 -1
  15. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/pyproject.toml +1 -1
  16. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/tests/test_cli.py +94 -3
  17. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/tests/test_server.py +20 -0
  18. hanzo_mcp-0.2.0/tests/test_tools_registration.py +180 -0
  19. hanzo_mcp-0.1.36/hanzo_mcp/tools/common/path_utils.py +0 -51
  20. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/LICENSE +0 -0
  21. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/README.md +0 -0
  22. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/agent/__init__.py +0 -0
  23. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/agent/agent_tool.py +0 -0
  24. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/agent/prompt.py +0 -0
  25. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/agent/tool_adapter.py +0 -0
  26. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/common/base.py +0 -0
  27. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/common/session.py +0 -0
  28. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/common/think_tool.py +0 -0
  29. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/common/validation.py +0 -0
  30. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/common/version_tool.py +0 -0
  31. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/filesystem/base.py +0 -0
  32. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/filesystem/content_replace.py +0 -0
  33. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/filesystem/edit_file.py +0 -0
  34. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/filesystem/get_file_info.py +0 -0
  35. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/filesystem/read_files.py +0 -0
  36. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/filesystem/search_content.py +0 -0
  37. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/filesystem/write_file.py +0 -0
  38. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/jupyter/base.py +0 -0
  39. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/jupyter/edit_notebook.py +0 -0
  40. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/jupyter/notebook_operations.py +0 -0
  41. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/jupyter/read_notebook.py +0 -0
  42. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/project/__init__.py +0 -0
  43. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/project/analysis.py +0 -0
  44. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/project/base.py +0 -0
  45. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/project/project_analyze.py +0 -0
  46. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/shell/__init__.py +0 -0
  47. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/shell/base.py +0 -0
  48. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/shell/run_command.py +0 -0
  49. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/shell/run_script.py +0 -0
  50. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp/tools/shell/script_tool.py +0 -0
  51. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp.egg-info/dependency_links.txt +0 -0
  52. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp.egg-info/entry_points.txt +0 -0
  53. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp.egg-info/requires.txt +0 -0
  54. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/hanzo_mcp.egg-info/top_level.txt +0 -0
  55. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/setup.cfg +0 -0
  56. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/setup.py +0 -0
  57. {hanzo_mcp-0.1.36 → hanzo_mcp-0.2.0}/tests/test_validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hanzo-mcp
3
- Version: 0.1.36
3
+ Version: 0.2.0
4
4
  Summary: MCP implementation of Hanzo capabilities
5
5
  Author-email: Hanzo Industries Inc <dev@hanzo.ai>
6
6
  License: MIT
@@ -1,3 +1,3 @@
1
1
  """Hanzo MCP - Implementation of Hanzo capabilities using MCP."""
2
2
 
3
- __version__ = "0.1.36"
3
+ __version__ = "0.2.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, normalize it and add to allowed paths
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
- project_dir = PathUtils.normalize_path(project_dir)
124
- if project_dir not in allowed_paths:
125
- allowed_paths.append(project_dir)
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
- if not project_dir and allowed_paths:
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
- # Path should already be normalized from CLI, but normalize here for safety
75
- normalized_path = PathUtils.normalize_path(path)
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
- # Normalize path before adding
111
- normalized_path = PathUtils.normalize_path(path)
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
- # Normalize path (expand user paths and make absolute)
185
- normalized_path = PathUtils.normalize_path(path)
186
- resolved_path: Path = Path(normalized_path).resolve()
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
- # Normalize path (expand user paths and make absolute)
199
- normalized_path = PathUtils.normalize_path(path)
200
- resolved_path: Path = Path(normalized_path).resolve()
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
- # Normalize path (expand user paths and make absolute)
75
- normalized_path = PathUtils.normalize_path(path)
76
- resolved_path: Path = Path(normalized_path).resolve()
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
- # Normalize path (expand user paths and make absolute)
116
- normalized_path = PathUtils.normalize_path(path)
117
- resolved_path: Path = Path(normalized_path).resolve()
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
- tools = get_filesystem_tools(document_context, permission_manager)
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 based on common patterns and .gitignore
140
+ # Define filtered directories
143
141
  FILTERED_DIRECTORIES = {
144
- # Hidden/dot directories
145
- ".git", ".github", ".gitignore", ".hg", ".svn", ".venv", ".env",
146
- ".idea", ".vscode", ".vs", ".cache", ".config", ".local",
147
- ".pytest_cache", ".ruff_cache", ".mypy_cache", ".pytype",
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
- # First check standard filtered directories
191
- if current_path.name in FILTERED_DIRECTORIES and not include_filtered:
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
- tools = get_jupyter_tools(document_context, permission_manager)
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
- normalized_path = PathUtils.normalize_path(path)
140
- session.set_working_dir(Path(normalized_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
- # Normalize path (expand user and make absolute)
255
- target_dir = PathUtils.normalize_path(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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hanzo-mcp
3
- Version: 0.1.36
3
+ Version: 0.2.0
4
4
  Summary: MCP implementation of Hanzo capabilities
5
5
  Author-email: Hanzo Industries Inc <dev@hanzo.ai>
6
6
  License: MIT
@@ -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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "hanzo-mcp"
7
- version = "0.1.36"
7
+ version = "0.2.0"
8
8
  description = "MCP implementation of Hanzo capabilities"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -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