hanzo-mcp 0.1.20__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of hanzo-mcp might be problematic. Click here for more details.

Files changed (44) hide show
  1. hanzo_mcp/__init__.py +3 -0
  2. hanzo_mcp/cli.py +213 -0
  3. hanzo_mcp/server.py +149 -0
  4. hanzo_mcp/tools/__init__.py +81 -0
  5. hanzo_mcp/tools/agent/__init__.py +59 -0
  6. hanzo_mcp/tools/agent/agent_tool.py +474 -0
  7. hanzo_mcp/tools/agent/prompt.py +137 -0
  8. hanzo_mcp/tools/agent/tool_adapter.py +75 -0
  9. hanzo_mcp/tools/common/__init__.py +18 -0
  10. hanzo_mcp/tools/common/base.py +216 -0
  11. hanzo_mcp/tools/common/context.py +444 -0
  12. hanzo_mcp/tools/common/permissions.py +253 -0
  13. hanzo_mcp/tools/common/thinking_tool.py +123 -0
  14. hanzo_mcp/tools/common/validation.py +124 -0
  15. hanzo_mcp/tools/filesystem/__init__.py +89 -0
  16. hanzo_mcp/tools/filesystem/base.py +113 -0
  17. hanzo_mcp/tools/filesystem/content_replace.py +287 -0
  18. hanzo_mcp/tools/filesystem/directory_tree.py +286 -0
  19. hanzo_mcp/tools/filesystem/edit_file.py +287 -0
  20. hanzo_mcp/tools/filesystem/get_file_info.py +170 -0
  21. hanzo_mcp/tools/filesystem/read_files.py +198 -0
  22. hanzo_mcp/tools/filesystem/search_content.py +275 -0
  23. hanzo_mcp/tools/filesystem/write_file.py +162 -0
  24. hanzo_mcp/tools/jupyter/__init__.py +71 -0
  25. hanzo_mcp/tools/jupyter/base.py +284 -0
  26. hanzo_mcp/tools/jupyter/edit_notebook.py +295 -0
  27. hanzo_mcp/tools/jupyter/notebook_operations.py +514 -0
  28. hanzo_mcp/tools/jupyter/read_notebook.py +165 -0
  29. hanzo_mcp/tools/project/__init__.py +64 -0
  30. hanzo_mcp/tools/project/analysis.py +882 -0
  31. hanzo_mcp/tools/project/base.py +66 -0
  32. hanzo_mcp/tools/project/project_analyze.py +173 -0
  33. hanzo_mcp/tools/shell/__init__.py +58 -0
  34. hanzo_mcp/tools/shell/base.py +148 -0
  35. hanzo_mcp/tools/shell/command_executor.py +740 -0
  36. hanzo_mcp/tools/shell/run_command.py +204 -0
  37. hanzo_mcp/tools/shell/run_script.py +215 -0
  38. hanzo_mcp/tools/shell/script_tool.py +244 -0
  39. hanzo_mcp-0.1.20.dist-info/METADATA +111 -0
  40. hanzo_mcp-0.1.20.dist-info/RECORD +44 -0
  41. hanzo_mcp-0.1.20.dist-info/WHEEL +5 -0
  42. hanzo_mcp-0.1.20.dist-info/entry_points.txt +2 -0
  43. hanzo_mcp-0.1.20.dist-info/licenses/LICENSE +21 -0
  44. hanzo_mcp-0.1.20.dist-info/top_level.txt +1 -0
@@ -0,0 +1,253 @@
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
+ @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
@@ -0,0 +1,123 @@
1
+ """Thinking tool implementation.
2
+
3
+ This module provides the ThinkingTool for Claude to engage in structured thinking.
4
+ """
5
+
6
+ from typing import Any, final, override
7
+
8
+ from mcp.server.fastmcp import Context as MCPContext
9
+ from mcp.server.fastmcp import FastMCP
10
+
11
+ from hanzo_mcp.tools.common.base import BaseTool
12
+ from hanzo_mcp.tools.common.context import create_tool_context
13
+
14
+
15
+ @final
16
+ class ThinkingTool(BaseTool):
17
+ """Tool for Claude to engage in structured thinking."""
18
+
19
+ @property
20
+ @override
21
+ def name(self) -> str:
22
+ """Get the tool name.
23
+
24
+ Returns:
25
+ Tool name
26
+ """
27
+ return "think"
28
+
29
+ @property
30
+ @override
31
+ def description(self) -> str:
32
+ """Get the tool description.
33
+
34
+ Returns:
35
+ Tool description
36
+ """
37
+ return """Use the tool to think about something.
38
+
39
+ It will not obtain new information or make any changes to the repository, but just log the thought. Use it when complex reasoning or brainstorming is needed. For example, if you explore the repo and discover the source of a bug, call this tool to brainstorm several unique ways of fixing the bug, and assess which change(s) are likely to be simplest and most effective. Alternatively, if you receive some test results, call this tool to brainstorm ways to fix the failing tests."""
40
+
41
+ @property
42
+ @override
43
+ def parameters(self) -> dict[str, Any]:
44
+ """Get the parameter specifications for the tool.
45
+
46
+ Returns:
47
+ Parameter specifications
48
+ """
49
+ return {
50
+ "properties": {
51
+ "thought": {
52
+ "title": "Thought",
53
+ "type": "string"
54
+ }
55
+ },
56
+ "required": ["thought"],
57
+ "title": "thinkArguments",
58
+ "type": "object"
59
+ }
60
+
61
+ @property
62
+ @override
63
+ def required(self) -> list[str]:
64
+ """Get the list of required parameter names.
65
+
66
+ Returns:
67
+ List of required parameter names
68
+ """
69
+ return ["thought"]
70
+
71
+ def __init__(self) -> None:
72
+ """Initialize the thinking tool."""
73
+ pass
74
+
75
+ @override
76
+ async def call(self, ctx: MCPContext, **params: Any) -> str:
77
+ """Execute the tool with the given parameters.
78
+
79
+ Args:
80
+ ctx: MCP context
81
+ **params: Tool parameters
82
+
83
+ Returns:
84
+ Tool result
85
+ """
86
+ tool_ctx = create_tool_context(ctx)
87
+ tool_ctx.set_tool_info(self.name)
88
+
89
+ # Extract parameters
90
+ thought = params.get("thought")
91
+
92
+ # Validate required thought parameter
93
+ if not thought:
94
+ await tool_ctx.error(
95
+ "Parameter 'thought' is required but was None or empty"
96
+ )
97
+ return "Error: Parameter 'thought' is required but was None or empty"
98
+
99
+ if thought.strip() == "":
100
+ await tool_ctx.error("Parameter 'thought' cannot be empty")
101
+ return "Error: Parameter 'thought' cannot be empty"
102
+
103
+ # Log the thought but don't take action
104
+ await tool_ctx.info("Thinking process recorded")
105
+
106
+ # Return confirmation
107
+ return "I've recorded your thinking process. You can continue with your next action based on this analysis."
108
+
109
+ @override
110
+ def register(self, mcp_server: FastMCP) -> None:
111
+ """Register this thinking tool with the MCP server.
112
+
113
+ Creates a wrapper function with explicitly defined parameters that match
114
+ the tool's parameter schema and registers it with the MCP server.
115
+
116
+ Args:
117
+ mcp_server: The FastMCP server instance
118
+ """
119
+ tool_self = self # Create a reference to self for use in the closure
120
+
121
+ @mcp_server.tool(name=self.name, description=self.mcp_description)
122
+ async def think(thought: str, ctx: MCPContext) -> str:
123
+ return await tool_self.call(ctx, thought=thought)
@@ -0,0 +1,124 @@
1
+ """Parameter validation utilities for Hanzo MCP tools.
2
+
3
+ This module provides utilities for validating parameters in tool functions.
4
+ """
5
+
6
+ from typing import Any, TypeVar, final
7
+
8
+ T = TypeVar("T")
9
+
10
+
11
+ @final
12
+ class ValidationResult:
13
+ """Result of a parameter validation."""
14
+
15
+ def __init__(self, is_valid: bool, error_message: str = ""):
16
+ """Initialize a validation result.
17
+
18
+ Args:
19
+ is_valid: Whether the parameter is valid
20
+ error_message: Optional error message for invalid parameters
21
+ """
22
+ self.is_valid: bool = is_valid
23
+ self.error_message: str = error_message
24
+
25
+ @property
26
+ def is_error(self) -> bool:
27
+ """Check if the validation resulted in an error.
28
+
29
+ Returns:
30
+ True if there was a validation error, False otherwise
31
+ """
32
+ return not self.is_valid
33
+
34
+
35
+ def validate_parameter(
36
+ parameter: Any, parameter_name: str, allow_empty: bool = False
37
+ ) -> ValidationResult:
38
+ """Validate a single parameter.
39
+
40
+ Args:
41
+ parameter: The parameter value to validate
42
+ parameter_name: The name of the parameter (for error messages)
43
+ allow_empty: Whether to allow empty strings, lists, etc.
44
+
45
+ Returns:
46
+ A ValidationResult indicating whether the parameter is valid
47
+ """
48
+ # Check for None
49
+ if parameter is None:
50
+ return ValidationResult(
51
+ is_valid=False,
52
+ error_message=f"Parameter '{parameter_name}' is required but was None",
53
+ )
54
+
55
+ # Check for empty strings
56
+ if isinstance(parameter, str) and not allow_empty and parameter.strip() == "":
57
+ return ValidationResult(
58
+ is_valid=False,
59
+ error_message=f"Parameter '{parameter_name}' is required but was empty string",
60
+ )
61
+
62
+ # Check for empty collections
63
+ if (
64
+ isinstance(parameter, (list, tuple, dict, set))
65
+ and not allow_empty
66
+ and len(parameter) == 0
67
+ ):
68
+ return ValidationResult(
69
+ is_valid=False,
70
+ error_message=f"Parameter '{parameter_name}' is required but was empty {type(parameter).__name__}",
71
+ )
72
+
73
+ # Parameter is valid
74
+ return ValidationResult(is_valid=True)
75
+
76
+
77
+ def validate_path_parameter(
78
+ path: str | None, parameter_name: str = "path"
79
+ ) -> ValidationResult:
80
+ """Validate a path parameter.
81
+
82
+ Args:
83
+ path: The path parameter to validate
84
+ parameter_name: The name of the parameter (for error messages)
85
+
86
+ Returns:
87
+ A ValidationResult indicating whether the parameter is valid
88
+ """
89
+ # Check for None
90
+ if path is None:
91
+ return ValidationResult(
92
+ is_valid=False,
93
+ error_message=f"Path parameter '{parameter_name}' is required but was None",
94
+ )
95
+
96
+ # Check for empty path
97
+ if path.strip() == "":
98
+ return ValidationResult(
99
+ is_valid=False,
100
+ error_message=f"Path parameter '{parameter_name}' is required but was empty string",
101
+ )
102
+
103
+ # Path is valid
104
+ return ValidationResult(is_valid=True)
105
+
106
+
107
+ def validate_parameters(**kwargs: Any) -> ValidationResult:
108
+ """Validate multiple parameters.
109
+
110
+ Accepts keyword arguments where the key is the parameter name and the value is the parameter value.
111
+
112
+ Args:
113
+ **kwargs: Parameters to validate as name=value pairs
114
+
115
+ Returns:
116
+ A ValidationResult for the first invalid parameter, or a valid result if all are valid
117
+ """
118
+ for name, value in kwargs.items():
119
+ result = validate_parameter(value, name)
120
+ if result.is_error:
121
+ return result
122
+
123
+ # All parameters are valid
124
+ return ValidationResult(is_valid=True)
@@ -0,0 +1,89 @@
1
+ """Filesystem tools package for Hanzo MCP.
2
+
3
+ This package provides tools for interacting with the filesystem, including reading, writing,
4
+ and editing files, directory navigation, and content searching.
5
+ """
6
+
7
+ from mcp.server.fastmcp import FastMCP
8
+
9
+ from hanzo_mcp.tools.common.base import BaseTool, ToolRegistry
10
+ from hanzo_mcp.tools.common.context import DocumentContext
11
+ from hanzo_mcp.tools.common.permissions import PermissionManager
12
+ from hanzo_mcp.tools.filesystem.content_replace import ContentReplaceTool
13
+ from hanzo_mcp.tools.filesystem.directory_tree import DirectoryTreeTool
14
+ from hanzo_mcp.tools.filesystem.edit_file import EditFileTool
15
+ from hanzo_mcp.tools.filesystem.get_file_info import GetFileInfoTool
16
+ from hanzo_mcp.tools.filesystem.read_files import ReadFilesTool
17
+ from hanzo_mcp.tools.filesystem.search_content import SearchContentTool
18
+ from hanzo_mcp.tools.filesystem.write_file import WriteFileTool
19
+
20
+ # Export all tool classes
21
+ __all__ = [
22
+ "ReadFilesTool",
23
+ "WriteFileTool",
24
+ "EditFileTool",
25
+ "DirectoryTreeTool",
26
+ "GetFileInfoTool",
27
+ "SearchContentTool",
28
+ "ContentReplaceTool",
29
+ "get_filesystem_tools",
30
+ "register_filesystem_tools",
31
+ ]
32
+
33
+ def get_read_only_filesystem_tools(
34
+ document_context: DocumentContext, permission_manager: PermissionManager
35
+ ) -> list[BaseTool]:
36
+ """Create instances of read-only filesystem tools.
37
+
38
+ Args:
39
+ document_context: Document context for tracking file contents
40
+ permission_manager: Permission manager for access control
41
+
42
+ Returns:
43
+ List of read-only filesystem tool instances
44
+ """
45
+ return [
46
+ ReadFilesTool(document_context, permission_manager),
47
+ DirectoryTreeTool(document_context, permission_manager),
48
+ GetFileInfoTool(document_context, permission_manager),
49
+ SearchContentTool(document_context, permission_manager),
50
+ ]
51
+
52
+
53
+ def get_filesystem_tools(
54
+ document_context: DocumentContext, permission_manager: PermissionManager
55
+ ) -> list[BaseTool]:
56
+ """Create instances of all filesystem tools.
57
+
58
+ Args:
59
+ document_context: Document context for tracking file contents
60
+ permission_manager: Permission manager for access control
61
+
62
+ Returns:
63
+ List of filesystem tool instances
64
+ """
65
+ return [
66
+ ReadFilesTool(document_context, permission_manager),
67
+ WriteFileTool(document_context, permission_manager),
68
+ EditFileTool(document_context, permission_manager),
69
+ DirectoryTreeTool(document_context, permission_manager),
70
+ GetFileInfoTool(document_context, permission_manager),
71
+ SearchContentTool(document_context, permission_manager),
72
+ ContentReplaceTool(document_context, permission_manager),
73
+ ]
74
+
75
+
76
+ def register_filesystem_tools(
77
+ mcp_server: FastMCP,
78
+ document_context: DocumentContext,
79
+ permission_manager: PermissionManager,
80
+ ) -> None:
81
+ """Register all filesystem tools with the MCP server.
82
+
83
+ Args:
84
+ mcp_server: The FastMCP server instance
85
+ document_context: Document context for tracking file contents
86
+ permission_manager: Permission manager for access control
87
+ """
88
+ tools = get_filesystem_tools(document_context, permission_manager)
89
+ ToolRegistry.register_tools(mcp_server, tools)
@@ -0,0 +1,113 @@
1
+ """Base functionality for filesystem tools.
2
+
3
+ This module provides common functionality for filesystem tools including path handling,
4
+ error formatting, and shared utilities for file operations.
5
+ """
6
+
7
+ from abc import ABC
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from mcp.server.fastmcp import Context as MCPContext
12
+
13
+ from hanzo_mcp.tools.common.base import FileSystemTool
14
+ from hanzo_mcp.tools.common.context import ToolContext, create_tool_context
15
+
16
+
17
+ class FilesystemBaseTool(FileSystemTool,ABC):
18
+ """Enhanced base class for all filesystem tools.
19
+
20
+ Provides additional utilities specific to filesystem operations beyond
21
+ the base functionality in FileSystemTool.
22
+ """
23
+
24
+ async def check_path_allowed(self, path: str, tool_ctx: Any, error_prefix: str = "Error") -> tuple[bool, str]:
25
+ """Check if a path is allowed and log an error if not.
26
+
27
+ Args:
28
+ path: Path to check
29
+ tool_ctx: Tool context for logging
30
+ error_prefix: Prefix for error messages
31
+
32
+ Returns:
33
+ tuple of (is_allowed, error_message)
34
+ """
35
+ if not self.is_path_allowed(path):
36
+ message = f"Access denied - path outside allowed directories: {path}"
37
+ await tool_ctx.error(message)
38
+ return False, f"{error_prefix}: {message}"
39
+ return True, ""
40
+
41
+ async def check_path_exists(self, path: str, tool_ctx: Any, error_prefix: str = "Error") -> tuple[bool, str]:
42
+ """Check if a path exists and log an error if not.
43
+
44
+ Args:
45
+ path: Path to check
46
+ tool_ctx: Tool context for logging
47
+ error_prefix: Prefix for error messages
48
+
49
+ Returns:
50
+ tuple of (exists, error_message)
51
+ """
52
+ file_path = Path(path)
53
+ if not file_path.exists():
54
+ message = f"Path does not exist: {path}"
55
+ await tool_ctx.error(message)
56
+ return False, f"{error_prefix}: {message}"
57
+ return True, ""
58
+
59
+ async def check_is_file(self, path: str, tool_ctx: Any, error_prefix: str = "Error") -> tuple[bool, str]:
60
+ """Check if a path is a file and log an error if not.
61
+
62
+ Args:
63
+ path: Path to check
64
+ tool_ctx: Tool context for logging
65
+ error_prefix: Prefix for error messages
66
+
67
+ Returns:
68
+ tuple of (is_file, error_message)
69
+ """
70
+ file_path = Path(path)
71
+ if not file_path.is_file():
72
+ message = f"Path is not a file: {path}"
73
+ await tool_ctx.error(message)
74
+ return False, f"{error_prefix}: {message}"
75
+ return True, ""
76
+
77
+ async def check_is_directory(self, path: str, tool_ctx: Any, error_prefix: str = "Error") -> tuple[bool, str]:
78
+ """Check if a path is a directory and log an error if not.
79
+
80
+ Args:
81
+ path: Path to check
82
+ tool_ctx: Tool context for logging
83
+ error_prefix: Prefix for error messages
84
+
85
+ Returns:
86
+ tuple of (is_directory, error_message)
87
+ """
88
+ dir_path = Path(path)
89
+ if not dir_path.is_dir():
90
+ message = f"Path is not a directory: {path}"
91
+ await tool_ctx.error(message)
92
+ return False, f"{error_prefix}: {message}"
93
+ return True, ""
94
+
95
+ def create_tool_context(self, ctx: MCPContext) -> ToolContext:
96
+ """Create a tool context with the tool name.
97
+
98
+ Args:
99
+ ctx: MCP context
100
+
101
+ Returns:
102
+ Tool context
103
+ """
104
+ tool_ctx = create_tool_context(ctx)
105
+ return tool_ctx
106
+
107
+ def set_tool_context_info(self, tool_ctx: ToolContext) -> None:
108
+ """Set the tool info on the context.
109
+
110
+ Args:
111
+ tool_ctx: Tool context
112
+ """
113
+ tool_ctx.set_tool_info(self.name)