hanzo-mcp 0.1.36__py3-none-any.whl → 0.3.1__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.
- hanzo_mcp/__init__.py +1 -1
- hanzo_mcp/cli.py +136 -31
- hanzo_mcp/server.py +34 -13
- hanzo_mcp/tools/__init__.py +4 -2
- hanzo_mcp/tools/agent/base_provider.py +73 -0
- hanzo_mcp/tools/agent/litellm_provider.py +45 -0
- hanzo_mcp/tools/agent/lmstudio_agent.py +385 -0
- hanzo_mcp/tools/agent/lmstudio_provider.py +219 -0
- hanzo_mcp/tools/agent/provider_registry.py +120 -0
- hanzo_mcp/tools/common/__init__.py +0 -1
- hanzo_mcp/tools/common/context.py +6 -8
- hanzo_mcp/tools/common/error_handling.py +86 -0
- hanzo_mcp/tools/common/logging_config.py +84 -0
- hanzo_mcp/tools/common/permissions.py +6 -8
- hanzo_mcp/tools/filesystem/__init__.py +6 -1
- hanzo_mcp/tools/filesystem/directory_tree.py +7 -46
- hanzo_mcp/tools/jupyter/__init__.py +6 -1
- hanzo_mcp/tools/shell/command_executor.py +7 -6
- {hanzo_mcp-0.1.36.dist-info → hanzo_mcp-0.3.1.dist-info}/METADATA +1 -1
- {hanzo_mcp-0.1.36.dist-info → hanzo_mcp-0.3.1.dist-info}/RECORD +24 -18
- hanzo_mcp/tools/common/path_utils.py +0 -51
- {hanzo_mcp-0.1.36.dist-info → hanzo_mcp-0.3.1.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.1.36.dist-info → hanzo_mcp-0.3.1.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.1.36.dist-info → hanzo_mcp-0.3.1.dist-info}/licenses/LICENSE +0 -0
- {hanzo_mcp-0.1.36.dist-info → hanzo_mcp-0.3.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Provider registry for agent delegation.
|
|
2
|
+
|
|
3
|
+
Manages different model providers for agent delegation.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Any, Dict, List, Optional, Tuple, Type
|
|
8
|
+
|
|
9
|
+
from hanzo_mcp.tools.agent.base_provider import BaseModelProvider
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ProviderRegistry:
|
|
15
|
+
"""Registry for model providers."""
|
|
16
|
+
|
|
17
|
+
_instance = None
|
|
18
|
+
|
|
19
|
+
def __new__(cls):
|
|
20
|
+
"""Singleton pattern to ensure only one registry instance exists."""
|
|
21
|
+
if cls._instance is None:
|
|
22
|
+
cls._instance = super(ProviderRegistry, cls).__new__(cls)
|
|
23
|
+
cls._instance._initialized = False
|
|
24
|
+
return cls._instance
|
|
25
|
+
|
|
26
|
+
def __init__(self):
|
|
27
|
+
"""Initialize the provider registry."""
|
|
28
|
+
if self._initialized:
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
self.providers = {}
|
|
32
|
+
self.provider_classes = {}
|
|
33
|
+
self._initialized = True
|
|
34
|
+
logger.info("Provider registry initialized")
|
|
35
|
+
|
|
36
|
+
def register_provider_class(self, provider_type: str, provider_class: Type[BaseModelProvider]) -> None:
|
|
37
|
+
"""Register a provider class with the registry.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
provider_type: The type identifier for the provider
|
|
41
|
+
provider_class: The provider class to register
|
|
42
|
+
"""
|
|
43
|
+
self.provider_classes[provider_type] = provider_class
|
|
44
|
+
logger.info(f"Registered provider class: {provider_type}")
|
|
45
|
+
|
|
46
|
+
async def get_provider(self, provider_type: str) -> BaseModelProvider:
|
|
47
|
+
"""Get or create a provider instance for the given type.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
provider_type: The type identifier for the provider
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
A provider instance
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
ValueError: If the provider type is not registered
|
|
57
|
+
"""
|
|
58
|
+
# Check if we already have an instance
|
|
59
|
+
if provider_type in self.providers:
|
|
60
|
+
return self.providers[provider_type]
|
|
61
|
+
|
|
62
|
+
# Check if we have a class for this type
|
|
63
|
+
if provider_type not in self.provider_classes:
|
|
64
|
+
raise ValueError(f"Unknown provider type: {provider_type}")
|
|
65
|
+
|
|
66
|
+
# Create a new instance
|
|
67
|
+
provider_class = self.provider_classes[provider_type]
|
|
68
|
+
provider = provider_class()
|
|
69
|
+
|
|
70
|
+
# Initialize the provider
|
|
71
|
+
await provider.initialize()
|
|
72
|
+
|
|
73
|
+
# Store and return the provider
|
|
74
|
+
self.providers[provider_type] = provider
|
|
75
|
+
logger.info(f"Created and initialized provider: {provider_type}")
|
|
76
|
+
return provider
|
|
77
|
+
|
|
78
|
+
async def shutdown_all(self) -> None:
|
|
79
|
+
"""Shutdown all providers."""
|
|
80
|
+
for provider_type, provider in self.providers.items():
|
|
81
|
+
try:
|
|
82
|
+
await provider.shutdown()
|
|
83
|
+
logger.info(f"Provider shut down: {provider_type}")
|
|
84
|
+
except Exception as e:
|
|
85
|
+
logger.error(f"Failed to shut down provider {provider_type}: {str(e)}")
|
|
86
|
+
|
|
87
|
+
self.providers = {}
|
|
88
|
+
logger.info("All providers shut down")
|
|
89
|
+
|
|
90
|
+
async def shutdown_provider(self, provider_type: str) -> None:
|
|
91
|
+
"""Shutdown a specific provider.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
provider_type: The type identifier for the provider
|
|
95
|
+
"""
|
|
96
|
+
if provider_type not in self.providers:
|
|
97
|
+
logger.warning(f"Provider not found: {provider_type}")
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
await self.providers[provider_type].shutdown()
|
|
102
|
+
del self.providers[provider_type]
|
|
103
|
+
logger.info(f"Provider shut down: {provider_type}")
|
|
104
|
+
except Exception as e:
|
|
105
|
+
logger.error(f"Failed to shut down provider {provider_type}: {str(e)}")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# Create a singleton instance
|
|
109
|
+
registry = ProviderRegistry()
|
|
110
|
+
|
|
111
|
+
# Register LiteLLM provider
|
|
112
|
+
from hanzo_mcp.tools.agent.litellm_provider import LiteLLMProvider
|
|
113
|
+
registry.register_provider_class("litellm", LiteLLMProvider)
|
|
114
|
+
|
|
115
|
+
# Try to register LM Studio provider if available
|
|
116
|
+
try:
|
|
117
|
+
from hanzo_mcp.tools.agent.lmstudio_provider import LMStudioProvider
|
|
118
|
+
registry.register_provider_class("lmstudio", LMStudioProvider)
|
|
119
|
+
except ImportError:
|
|
120
|
+
logger.warning("LM Studio provider not available. Install the package if needed.")
|
|
@@ -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:
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Error handling utilities for MCP tools.
|
|
2
|
+
|
|
3
|
+
This module provides utility functions for better error handling in MCP tools.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
import traceback
|
|
8
|
+
from functools import wraps
|
|
9
|
+
from typing import Any, Callable, TypeVar, Awaitable, cast
|
|
10
|
+
|
|
11
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
12
|
+
|
|
13
|
+
# Setup logger
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
# Type variables for generic function signatures
|
|
17
|
+
T = TypeVar('T')
|
|
18
|
+
F = TypeVar('F', bound=Callable[..., Awaitable[Any]])
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def log_error(ctx: MCPContext, error: Exception, message: str) -> None:
|
|
22
|
+
"""Log an error to both the logger and the MCP context.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
ctx: The MCP context
|
|
26
|
+
error: The exception that occurred
|
|
27
|
+
message: A descriptive message about the error
|
|
28
|
+
"""
|
|
29
|
+
error_message = f"{message}: {str(error)}"
|
|
30
|
+
stack_trace = "".join(traceback.format_exception(type(error), error, error.__traceback__))
|
|
31
|
+
|
|
32
|
+
# Log to system logger
|
|
33
|
+
logger.error(error_message)
|
|
34
|
+
logger.debug(stack_trace)
|
|
35
|
+
|
|
36
|
+
# Log to MCP context if available
|
|
37
|
+
try:
|
|
38
|
+
await ctx.error(error_message)
|
|
39
|
+
except Exception as e:
|
|
40
|
+
logger.error(f"Failed to log error to MCP context: {str(e)}")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def tool_error_handler(func: F) -> F:
|
|
44
|
+
"""Decorator for handling errors in tool execution.
|
|
45
|
+
|
|
46
|
+
This decorator wraps a tool function to catch and properly handle exceptions,
|
|
47
|
+
ensuring they are logged and proper error messages are returned.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
func: The async tool function to wrap
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Wrapped function with error handling
|
|
54
|
+
"""
|
|
55
|
+
@wraps(func)
|
|
56
|
+
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
57
|
+
try:
|
|
58
|
+
# Extract the MCP context from arguments
|
|
59
|
+
ctx = None
|
|
60
|
+
for arg in args:
|
|
61
|
+
if isinstance(arg, MCPContext):
|
|
62
|
+
ctx = arg
|
|
63
|
+
break
|
|
64
|
+
|
|
65
|
+
if not ctx and 'ctx' in kwargs:
|
|
66
|
+
ctx = kwargs['ctx']
|
|
67
|
+
|
|
68
|
+
if not ctx:
|
|
69
|
+
logger.warning("No MCP context found in tool arguments, error handling will be limited")
|
|
70
|
+
|
|
71
|
+
# Call the original function
|
|
72
|
+
return await func(*args, **kwargs)
|
|
73
|
+
except Exception as e:
|
|
74
|
+
# Log the error
|
|
75
|
+
error_message = f"Error in tool execution: {func.__name__}"
|
|
76
|
+
if ctx:
|
|
77
|
+
await log_error(ctx, e, error_message)
|
|
78
|
+
else:
|
|
79
|
+
stack_trace = "".join(traceback.format_exception(type(e), e, e.__traceback__))
|
|
80
|
+
logger.error(f"{error_message}: {str(e)}")
|
|
81
|
+
logger.debug(stack_trace)
|
|
82
|
+
|
|
83
|
+
# Return a friendly error message
|
|
84
|
+
return f"Error executing {func.__name__}: {str(e)}"
|
|
85
|
+
|
|
86
|
+
return cast(F, wrapper)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Logging configuration for Hanzo MCP.
|
|
2
|
+
|
|
3
|
+
This module sets up logging for the Hanzo MCP project.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def setup_logging(log_level: str = "INFO", log_to_file: bool = True, testing: bool = False) -> None:
|
|
14
|
+
"""Set up logging configuration.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
log_level: The logging level ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL")
|
|
18
|
+
log_to_file: Whether to log to a file in addition to the console
|
|
19
|
+
testing: Set to True to disable file operations for testing
|
|
20
|
+
"""
|
|
21
|
+
# Convert string log level to logging constant
|
|
22
|
+
numeric_level = getattr(logging, log_level.upper(), None)
|
|
23
|
+
if not isinstance(numeric_level, int):
|
|
24
|
+
raise ValueError(f"Invalid log level: {log_level}")
|
|
25
|
+
|
|
26
|
+
# Create logs directory if needed
|
|
27
|
+
log_dir = Path.home() / ".hanzo" / "logs"
|
|
28
|
+
if log_to_file and not testing:
|
|
29
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
|
|
31
|
+
# Generate log filename based on current date
|
|
32
|
+
current_time = datetime.now().strftime("%Y-%m-%d")
|
|
33
|
+
log_file = log_dir / f"hanzo-mcp-{current_time}.log"
|
|
34
|
+
|
|
35
|
+
# Base configuration
|
|
36
|
+
handlers = []
|
|
37
|
+
|
|
38
|
+
# Console handler
|
|
39
|
+
console = logging.StreamHandler(sys.stdout)
|
|
40
|
+
console.setLevel(numeric_level)
|
|
41
|
+
console_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
42
|
+
console.setFormatter(console_formatter)
|
|
43
|
+
handlers.append(console)
|
|
44
|
+
|
|
45
|
+
# File handler (if enabled)
|
|
46
|
+
if log_to_file and not testing:
|
|
47
|
+
file_handler = logging.FileHandler(log_file)
|
|
48
|
+
file_handler.setLevel(numeric_level)
|
|
49
|
+
file_formatter = logging.Formatter(
|
|
50
|
+
'%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s'
|
|
51
|
+
)
|
|
52
|
+
file_handler.setFormatter(file_formatter)
|
|
53
|
+
handlers.append(file_handler)
|
|
54
|
+
|
|
55
|
+
# Configure root logger
|
|
56
|
+
logging.basicConfig(
|
|
57
|
+
level=numeric_level,
|
|
58
|
+
handlers=handlers,
|
|
59
|
+
force=True # Overwrite any existing configuration
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Set specific log levels for third-party libraries
|
|
63
|
+
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
|
64
|
+
logging.getLogger("asyncio").setLevel(logging.WARNING)
|
|
65
|
+
|
|
66
|
+
# Log startup message
|
|
67
|
+
root_logger = logging.getLogger()
|
|
68
|
+
root_logger.info(f"Logging initialized at level {log_level}")
|
|
69
|
+
if log_to_file and not testing:
|
|
70
|
+
root_logger.info(f"Log file: {log_file}")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_log_files() -> list[str]:
|
|
74
|
+
"""Get a list of all log files.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
List of log file paths
|
|
78
|
+
"""
|
|
79
|
+
log_dir = Path.home() / ".hanzo" / "logs"
|
|
80
|
+
if not log_dir.exists():
|
|
81
|
+
return []
|
|
82
|
+
|
|
83
|
+
log_files = [str(f) for f in log_dir.glob("hanzo-mcp-*.log")]
|
|
84
|
+
return sorted(log_files, reverse=True)
|
|
@@ -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)
|
|
@@ -1,30 +1,36 @@
|
|
|
1
|
-
hanzo_mcp/__init__.py,sha256=
|
|
2
|
-
hanzo_mcp/cli.py,sha256=
|
|
3
|
-
hanzo_mcp/server.py,sha256=
|
|
4
|
-
hanzo_mcp/tools/__init__.py,sha256=
|
|
1
|
+
hanzo_mcp/__init__.py,sha256=roRFuV1ugS6Zf28PtFGKzOnpJMDFncOz6fR3TlG-LF0,89
|
|
2
|
+
hanzo_mcp/cli.py,sha256=ST428ys32fputErgrmLNJV9oYHbKGwkjrPBJfKh_K_Y,10820
|
|
3
|
+
hanzo_mcp/server.py,sha256=bV4ywWOgm7BwZrbZwv1qoRKbAsMYT08aCGQwuFS8raM,6678
|
|
4
|
+
hanzo_mcp/tools/__init__.py,sha256=9LbWAPfSntDwLaAex3dagsHO4BgZQHKj5E6UX-Gmyb4,3496
|
|
5
5
|
hanzo_mcp/tools/agent/__init__.py,sha256=0eyQqqdAy7WCZEqUfV6xh66bDpQI9neB6iDjWf0_pJI,2189
|
|
6
6
|
hanzo_mcp/tools/agent/agent_tool.py,sha256=qXu62ZRGM0o9mxOiRVFy-ABIZtEJ8z03DqAXshKAieI,19180
|
|
7
|
+
hanzo_mcp/tools/agent/base_provider.py,sha256=dd1J75Q0wl_t_Gcuwc8Ft3PWxeDsmf0B3ybhRsUEgmA,2080
|
|
8
|
+
hanzo_mcp/tools/agent/litellm_provider.py,sha256=6mVOLSNpfjAJKj6aBMflyYU6DRJpEqIxgu8wSzeRmxU,1334
|
|
9
|
+
hanzo_mcp/tools/agent/lmstudio_agent.py,sha256=c4-VIDRXksv39HYpXzqwyF1PisoNd73R3Txa8Mkzibg,14510
|
|
10
|
+
hanzo_mcp/tools/agent/lmstudio_provider.py,sha256=-gX8fxebyWaC7bqmZERR6QQc62Paj62IIDIu_rBu0wM,7876
|
|
7
11
|
hanzo_mcp/tools/agent/prompt.py,sha256=jmYRI4Sm2D3LwEdC2qWakpqupgfGg8CT6grmx4NEDaA,4431
|
|
12
|
+
hanzo_mcp/tools/agent/provider_registry.py,sha256=A-Wd5B5hLBznQV4wWbxE-yyvBPnbuUL2OtSJUz2K_H0,4236
|
|
8
13
|
hanzo_mcp/tools/agent/tool_adapter.py,sha256=g9NKfIET0WOsm0r28xEXsibsprpI1C6ripcM6QwU-rI,2172
|
|
9
|
-
hanzo_mcp/tools/common/__init__.py,sha256=
|
|
14
|
+
hanzo_mcp/tools/common/__init__.py,sha256=felrGAEqLWOa8fuHjTCS7khqtlA-izz8k865ky7R-f8,797
|
|
10
15
|
hanzo_mcp/tools/common/base.py,sha256=O7Lgft0XiC9Iyi3fYsmoWWrvKDK2Aa-FJLxPgnNJRJY,7341
|
|
11
|
-
hanzo_mcp/tools/common/context.py,sha256=
|
|
12
|
-
hanzo_mcp/tools/common/
|
|
13
|
-
hanzo_mcp/tools/common/
|
|
16
|
+
hanzo_mcp/tools/common/context.py,sha256=ReIfcm37j5qnLQ8G_-d88ad5uC1OKkjQZKG9HdJPybI,13145
|
|
17
|
+
hanzo_mcp/tools/common/error_handling.py,sha256=rluaHpXV89pjcf8JxgYB6EdqVkcxihb3pyEO2gN-u7w,2789
|
|
18
|
+
hanzo_mcp/tools/common/logging_config.py,sha256=b1swgWV3nuhpVT0TCcbxlOC0gmiRLmbey_t2LOWEMbU,2793
|
|
19
|
+
hanzo_mcp/tools/common/permissions.py,sha256=4YCfA2PJUOl-z_47n5uaRXO8gAZ_shMaPhpi1dilgRE,7594
|
|
14
20
|
hanzo_mcp/tools/common/session.py,sha256=csX5ZhgBjO2bdXXXPpsUPzOCc7Tib-apYe01AP8sh8k,2774
|
|
15
21
|
hanzo_mcp/tools/common/think_tool.py,sha256=I-O6ipM-PUofkNoMMzv37Y_2Yfx9mh7F1upTTsfRN4M,4046
|
|
16
22
|
hanzo_mcp/tools/common/validation.py,sha256=gB3uM_cbPZsH4Ez0hnTgIcdP-AUlHZU02aRmZEpk_6I,3648
|
|
17
23
|
hanzo_mcp/tools/common/version_tool.py,sha256=4bJZhqgtvwQMyVSSZ-xU-NQvr1xfnyDi_4FnOpZvuw0,3406
|
|
18
|
-
hanzo_mcp/tools/filesystem/__init__.py,sha256
|
|
24
|
+
hanzo_mcp/tools/filesystem/__init__.py,sha256=Jm5AmLIdRxzcZ4L8ajNJCQYObpmUeoBOzdYdFvDgQIA,3513
|
|
19
25
|
hanzo_mcp/tools/filesystem/base.py,sha256=HAzuMCrS0dKOBZNoLr7y74tdbYyKpi0FGhStuRgkFTU,3917
|
|
20
26
|
hanzo_mcp/tools/filesystem/content_replace.py,sha256=ZwzxyOTASUmzP-jtEnsSR-MKtNFC4R3kQunpV3KOCyg,11049
|
|
21
|
-
hanzo_mcp/tools/filesystem/directory_tree.py,sha256=
|
|
27
|
+
hanzo_mcp/tools/filesystem/directory_tree.py,sha256=cx-zpOeKP8DDuMt1ls3QhRk9h3RVmMhpPwpqn4wTfP4,11271
|
|
22
28
|
hanzo_mcp/tools/filesystem/edit_file.py,sha256=03ku1_8X_uAUPfqGlR6fv55VEz-0Pifp9DJtkcOTFHY,10980
|
|
23
29
|
hanzo_mcp/tools/filesystem/get_file_info.py,sha256=WR7uMqFcpKboS3FX3jF-MD-0-ROJJcppX7M_GtR0yLs,5392
|
|
24
30
|
hanzo_mcp/tools/filesystem/read_files.py,sha256=0JYJ2kM8FIoksbnnO8V0uY3D2R1uWvR7zb7_oXV0sMM,6968
|
|
25
31
|
hanzo_mcp/tools/filesystem/search_content.py,sha256=2zXke1YHYxg6GKQ_XHb0sXeaSkHI7Jx3P-YAqrpOTNM,10766
|
|
26
32
|
hanzo_mcp/tools/filesystem/write_file.py,sha256=7ZNR1ygECBjT7m62QNkeIEt0OGxNZL2zijX-bASWj0Y,5303
|
|
27
|
-
hanzo_mcp/tools/jupyter/__init__.py,sha256=
|
|
33
|
+
hanzo_mcp/tools/jupyter/__init__.py,sha256=9mZ55Mq0JMmjOxCxbeFHS8vOqvezzh_uCst02EPB4Ng,2527
|
|
28
34
|
hanzo_mcp/tools/jupyter/base.py,sha256=xtssHrkHx_u_nE12dqtZGcvuJe8kfsbSkMmq-6KQobQ,10412
|
|
29
35
|
hanzo_mcp/tools/jupyter/edit_notebook.py,sha256=_ZNlsCYaO9_SbZouvrLYElvssL6nlElCc2JxNCeMdQo,11986
|
|
30
36
|
hanzo_mcp/tools/jupyter/notebook_operations.py,sha256=PkZXk_PYPkBGxg2RWzqh-rN6VDHjFybImhdUm3xLLoY,23120
|
|
@@ -35,13 +41,13 @@ hanzo_mcp/tools/project/base.py,sha256=CniLAsjum5vC3cgvF9AqU-_ZY_0Nf9uaF2L_xV2ob
|
|
|
35
41
|
hanzo_mcp/tools/project/project_analyze.py,sha256=6GLE_JcSiCy6kKdee0sMI5T2229A-Vpp98s2j_JD6yI,5711
|
|
36
42
|
hanzo_mcp/tools/shell/__init__.py,sha256=lKgh0WXds4tZJ1tIL9MJbyMSzP6A9uZQALjGGBvyYc4,1679
|
|
37
43
|
hanzo_mcp/tools/shell/base.py,sha256=OxKNWMp-fB-vozzWOE_hHvr5M_pFKSMIYfOX0dEOWzA,4836
|
|
38
|
-
hanzo_mcp/tools/shell/command_executor.py,sha256=
|
|
44
|
+
hanzo_mcp/tools/shell/command_executor.py,sha256=5GcJvg54uty9fl_tkGdWTBcHyjxuynQZ_Tv6qOyKgdk,32616
|
|
39
45
|
hanzo_mcp/tools/shell/run_command.py,sha256=r7HBw0lqabgkGnVsDXmLnrTo0SU9g8gLvzpa-9n-cmM,6891
|
|
40
46
|
hanzo_mcp/tools/shell/run_script.py,sha256=CLYnDc0Ze8plkXU6d98RgE4UrBg-fwaMVdcn9Fc6Ixw,7432
|
|
41
47
|
hanzo_mcp/tools/shell/script_tool.py,sha256=s63tawIZBvwgm_kU9P7A3D4v2ulVw7j4l_rpsa_zGuc,8680
|
|
42
|
-
hanzo_mcp-0.1.
|
|
43
|
-
hanzo_mcp-0.1.
|
|
44
|
-
hanzo_mcp-0.1.
|
|
45
|
-
hanzo_mcp-0.1.
|
|
46
|
-
hanzo_mcp-0.1.
|
|
47
|
-
hanzo_mcp-0.1.
|
|
48
|
+
hanzo_mcp-0.3.1.dist-info/licenses/LICENSE,sha256=mf1qZGFsPGskoPgytp9B-RsahfKvXsBpmaAbTLGTt8Y,1063
|
|
49
|
+
hanzo_mcp-0.3.1.dist-info/METADATA,sha256=ay2jJEkPi-ivKlvoCRyCNj57-M0tT36tL4w5nuK_SQ0,7550
|
|
50
|
+
hanzo_mcp-0.3.1.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
|
51
|
+
hanzo_mcp-0.3.1.dist-info/entry_points.txt,sha256=aRKOKXtuQr-idSr-yH4efnRl2v8te94AcgN3ysqqSYs,49
|
|
52
|
+
hanzo_mcp-0.3.1.dist-info/top_level.txt,sha256=eGFANatA0MHWiVlpS56fTYRIShtibrSom1uXI6XU0GU,10
|
|
53
|
+
hanzo_mcp-0.3.1.dist-info/RECORD,,
|
|
@@ -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
|