hanzo-mcp 0.3.8__py3-none-any.whl → 0.5.0__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 +118 -170
- hanzo_mcp/cli_enhanced.py +438 -0
- hanzo_mcp/config/__init__.py +19 -0
- hanzo_mcp/config/settings.py +388 -0
- hanzo_mcp/config/tool_config.py +197 -0
- hanzo_mcp/prompts/__init__.py +117 -0
- hanzo_mcp/prompts/compact_conversation.py +77 -0
- hanzo_mcp/prompts/create_release.py +38 -0
- hanzo_mcp/prompts/project_system.py +120 -0
- hanzo_mcp/prompts/project_todo_reminder.py +111 -0
- hanzo_mcp/prompts/utils.py +286 -0
- hanzo_mcp/server.py +117 -99
- hanzo_mcp/tools/__init__.py +105 -32
- hanzo_mcp/tools/agent/__init__.py +8 -11
- hanzo_mcp/tools/agent/agent_tool.py +290 -224
- hanzo_mcp/tools/agent/prompt.py +16 -13
- hanzo_mcp/tools/agent/tool_adapter.py +9 -9
- hanzo_mcp/tools/common/__init__.py +17 -16
- hanzo_mcp/tools/common/base.py +79 -110
- hanzo_mcp/tools/common/batch_tool.py +330 -0
- hanzo_mcp/tools/common/context.py +26 -292
- hanzo_mcp/tools/common/permissions.py +12 -12
- hanzo_mcp/tools/common/thinking_tool.py +153 -0
- hanzo_mcp/tools/common/validation.py +1 -63
- hanzo_mcp/tools/filesystem/__init__.py +88 -57
- hanzo_mcp/tools/filesystem/base.py +32 -24
- hanzo_mcp/tools/filesystem/content_replace.py +114 -107
- hanzo_mcp/tools/filesystem/directory_tree.py +129 -105
- hanzo_mcp/tools/filesystem/edit.py +279 -0
- hanzo_mcp/tools/filesystem/grep.py +458 -0
- hanzo_mcp/tools/filesystem/grep_ast_tool.py +250 -0
- hanzo_mcp/tools/filesystem/multi_edit.py +362 -0
- hanzo_mcp/tools/filesystem/read.py +255 -0
- hanzo_mcp/tools/filesystem/write.py +156 -0
- hanzo_mcp/tools/jupyter/__init__.py +41 -29
- hanzo_mcp/tools/jupyter/base.py +66 -57
- hanzo_mcp/tools/jupyter/{edit_notebook.py → notebook_edit.py} +162 -139
- hanzo_mcp/tools/jupyter/notebook_read.py +152 -0
- hanzo_mcp/tools/shell/__init__.py +29 -20
- hanzo_mcp/tools/shell/base.py +87 -45
- hanzo_mcp/tools/shell/bash_session.py +731 -0
- hanzo_mcp/tools/shell/bash_session_executor.py +295 -0
- hanzo_mcp/tools/shell/command_executor.py +435 -384
- hanzo_mcp/tools/shell/run_command.py +284 -131
- hanzo_mcp/tools/shell/run_command_windows.py +328 -0
- hanzo_mcp/tools/shell/session_manager.py +196 -0
- hanzo_mcp/tools/shell/session_storage.py +325 -0
- hanzo_mcp/tools/todo/__init__.py +66 -0
- hanzo_mcp/tools/todo/base.py +319 -0
- hanzo_mcp/tools/todo/todo_read.py +148 -0
- hanzo_mcp/tools/todo/todo_write.py +378 -0
- hanzo_mcp/tools/vector/__init__.py +95 -0
- hanzo_mcp/tools/vector/infinity_store.py +365 -0
- hanzo_mcp/tools/vector/project_manager.py +361 -0
- hanzo_mcp/tools/vector/vector_index.py +115 -0
- hanzo_mcp/tools/vector/vector_search.py +215 -0
- {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.0.dist-info}/METADATA +33 -1
- hanzo_mcp-0.5.0.dist-info/RECORD +63 -0
- {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.0.dist-info}/WHEEL +1 -1
- hanzo_mcp/tools/agent/base_provider.py +0 -73
- hanzo_mcp/tools/agent/litellm_provider.py +0 -45
- hanzo_mcp/tools/agent/lmstudio_agent.py +0 -385
- hanzo_mcp/tools/agent/lmstudio_provider.py +0 -219
- hanzo_mcp/tools/agent/provider_registry.py +0 -120
- hanzo_mcp/tools/common/error_handling.py +0 -86
- hanzo_mcp/tools/common/logging_config.py +0 -115
- hanzo_mcp/tools/common/session.py +0 -91
- hanzo_mcp/tools/common/think_tool.py +0 -123
- hanzo_mcp/tools/common/version_tool.py +0 -120
- hanzo_mcp/tools/filesystem/edit_file.py +0 -287
- hanzo_mcp/tools/filesystem/get_file_info.py +0 -170
- hanzo_mcp/tools/filesystem/read_files.py +0 -199
- hanzo_mcp/tools/filesystem/search_content.py +0 -275
- hanzo_mcp/tools/filesystem/write_file.py +0 -162
- hanzo_mcp/tools/jupyter/notebook_operations.py +0 -514
- hanzo_mcp/tools/jupyter/read_notebook.py +0 -165
- hanzo_mcp/tools/project/__init__.py +0 -64
- hanzo_mcp/tools/project/analysis.py +0 -886
- hanzo_mcp/tools/project/base.py +0 -66
- hanzo_mcp/tools/project/project_analyze.py +0 -173
- hanzo_mcp/tools/shell/run_script.py +0 -215
- hanzo_mcp/tools/shell/script_tool.py +0 -244
- hanzo_mcp-0.3.8.dist-info/RECORD +0 -53
- {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.0.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.0.dist-info}/top_level.txt +0 -0
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
"""LM Studio provider for agent delegation.
|
|
2
|
-
|
|
3
|
-
Enables the use of local LLMs via LM Studio's Python SDK.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
import logging
|
|
7
|
-
import asyncio
|
|
8
|
-
import functools
|
|
9
|
-
from typing import Any, Dict, List, Optional, Tuple
|
|
10
|
-
|
|
11
|
-
from hanzo_mcp.tools.agent.base_provider import BaseModelProvider
|
|
12
|
-
|
|
13
|
-
logger = logging.getLogger(__name__)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class LMStudioProvider(BaseModelProvider):
|
|
17
|
-
"""Provider for local models via LM Studio Python SDK."""
|
|
18
|
-
|
|
19
|
-
def __init__(self):
|
|
20
|
-
"""Initialize the LM Studio provider."""
|
|
21
|
-
self.models = {}
|
|
22
|
-
self.initialized = False
|
|
23
|
-
|
|
24
|
-
async def initialize(self) -> None:
|
|
25
|
-
"""Initialize the LM Studio provider.
|
|
26
|
-
|
|
27
|
-
Import is done here to avoid dependency issues if LM Studio SDK is not installed.
|
|
28
|
-
"""
|
|
29
|
-
if self.initialized:
|
|
30
|
-
return
|
|
31
|
-
|
|
32
|
-
try:
|
|
33
|
-
# Dynamic import to avoid dependency issues if LM Studio is not installed
|
|
34
|
-
from importlib.util import find_spec
|
|
35
|
-
if find_spec("lmstudio") is None:
|
|
36
|
-
logger.warning("LM Studio Python SDK not installed. Install with 'pip install lmstudio'")
|
|
37
|
-
return
|
|
38
|
-
|
|
39
|
-
# Import the LM Studio module
|
|
40
|
-
import lmstudio as lms
|
|
41
|
-
self.lms = lms
|
|
42
|
-
self.initialized = True
|
|
43
|
-
logger.info("LM Studio provider initialized successfully")
|
|
44
|
-
except ImportError as e:
|
|
45
|
-
logger.error(f"Failed to import LM Studio Python SDK: {str(e)}")
|
|
46
|
-
logger.error("Install LM Studio Python SDK with 'pip install lmstudio'")
|
|
47
|
-
except Exception as e:
|
|
48
|
-
logger.error(f"Failed to initialize LM Studio provider: {str(e)}")
|
|
49
|
-
|
|
50
|
-
async def load_model(self, model_name: str, identifier: Optional[str] = None) -> str:
|
|
51
|
-
"""Load a model from LM Studio.
|
|
52
|
-
|
|
53
|
-
Args:
|
|
54
|
-
model_name: The name of the model to load
|
|
55
|
-
identifier: Optional identifier for the model instance
|
|
56
|
-
|
|
57
|
-
Returns:
|
|
58
|
-
The identifier for the loaded model
|
|
59
|
-
"""
|
|
60
|
-
if not self.initialized:
|
|
61
|
-
await self.initialize()
|
|
62
|
-
if not self.initialized:
|
|
63
|
-
raise RuntimeError("LM Studio provider failed to initialize")
|
|
64
|
-
|
|
65
|
-
model_id = identifier or model_name
|
|
66
|
-
|
|
67
|
-
try:
|
|
68
|
-
if model_id in self.models:
|
|
69
|
-
logger.info(f"Model {model_id} already loaded")
|
|
70
|
-
return model_id
|
|
71
|
-
|
|
72
|
-
logger.info(f"Loading model {model_name}")
|
|
73
|
-
|
|
74
|
-
# Use the thread pool to run the blocking operation
|
|
75
|
-
loop = asyncio.get_event_loop()
|
|
76
|
-
model = await loop.run_in_executor(
|
|
77
|
-
None,
|
|
78
|
-
functools.partial(self.lms.llm, model_name)
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
# Store the model with its identifier
|
|
82
|
-
self.models[model_id] = model
|
|
83
|
-
logger.info(f"Model {model_name} loaded successfully as {model_id}")
|
|
84
|
-
return model_id
|
|
85
|
-
|
|
86
|
-
except Exception as e:
|
|
87
|
-
logger.error(f"Failed to load model {model_name}: {str(e)}")
|
|
88
|
-
raise RuntimeError(f"Failed to load model {model_name}: {str(e)}")
|
|
89
|
-
|
|
90
|
-
async def generate(
|
|
91
|
-
self,
|
|
92
|
-
model_id: str,
|
|
93
|
-
prompt: str,
|
|
94
|
-
system_prompt: Optional[str] = None,
|
|
95
|
-
max_tokens: int = 4096,
|
|
96
|
-
temperature: float = 0.7,
|
|
97
|
-
top_p: float = 0.95,
|
|
98
|
-
stop_sequences: Optional[List[str]] = None,
|
|
99
|
-
) -> Tuple[str, Dict[str, Any]]:
|
|
100
|
-
"""Generate a response from the model.
|
|
101
|
-
|
|
102
|
-
Args:
|
|
103
|
-
model_id: The identifier of the model to use
|
|
104
|
-
prompt: The prompt to send to the model
|
|
105
|
-
system_prompt: Optional system prompt to send to the model
|
|
106
|
-
max_tokens: Maximum number of tokens to generate
|
|
107
|
-
temperature: Sampling temperature
|
|
108
|
-
top_p: Top-p sampling parameter
|
|
109
|
-
stop_sequences: Optional list of strings that will stop generation
|
|
110
|
-
|
|
111
|
-
Returns:
|
|
112
|
-
A tuple of (generated text, metadata)
|
|
113
|
-
"""
|
|
114
|
-
if not self.initialized:
|
|
115
|
-
await self.initialize()
|
|
116
|
-
if not self.initialized:
|
|
117
|
-
raise RuntimeError("LM Studio provider failed to initialize")
|
|
118
|
-
|
|
119
|
-
if model_id not in self.models:
|
|
120
|
-
raise ValueError(f"Model {model_id} not loaded")
|
|
121
|
-
|
|
122
|
-
model = self.models[model_id]
|
|
123
|
-
|
|
124
|
-
try:
|
|
125
|
-
logger.debug(f"Generating with model {model_id}")
|
|
126
|
-
|
|
127
|
-
# Prepare generation parameters
|
|
128
|
-
params = {
|
|
129
|
-
"max_tokens": max_tokens,
|
|
130
|
-
"temperature": temperature,
|
|
131
|
-
"top_p": top_p
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if stop_sequences:
|
|
135
|
-
params["stop"] = stop_sequences
|
|
136
|
-
|
|
137
|
-
# Generate response
|
|
138
|
-
loop = asyncio.get_event_loop()
|
|
139
|
-
|
|
140
|
-
if system_prompt:
|
|
141
|
-
# For chat models with system prompt
|
|
142
|
-
response_future = loop.run_in_executor(
|
|
143
|
-
None,
|
|
144
|
-
functools.partial(
|
|
145
|
-
model.chat,
|
|
146
|
-
system=system_prompt,
|
|
147
|
-
message=prompt,
|
|
148
|
-
**params
|
|
149
|
-
)
|
|
150
|
-
)
|
|
151
|
-
else:
|
|
152
|
-
# For completion models without system prompt
|
|
153
|
-
response_future = loop.run_in_executor(
|
|
154
|
-
None,
|
|
155
|
-
functools.partial(
|
|
156
|
-
model.respond,
|
|
157
|
-
prompt,
|
|
158
|
-
**params
|
|
159
|
-
)
|
|
160
|
-
)
|
|
161
|
-
|
|
162
|
-
response = await response_future
|
|
163
|
-
|
|
164
|
-
# Extract the generated text
|
|
165
|
-
if isinstance(response, dict) and "text" in response:
|
|
166
|
-
generated_text = response["text"]
|
|
167
|
-
elif isinstance(response, str):
|
|
168
|
-
generated_text = response
|
|
169
|
-
else:
|
|
170
|
-
generated_text = str(response)
|
|
171
|
-
|
|
172
|
-
# Metadata
|
|
173
|
-
metadata = {
|
|
174
|
-
"model": model_id,
|
|
175
|
-
"usage": {
|
|
176
|
-
"prompt_tokens": -1, # LM Studio Python SDK doesn't provide token counts
|
|
177
|
-
"completion_tokens": -1,
|
|
178
|
-
"total_tokens": -1
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
logger.debug(f"Generated {len(generated_text)} chars with model {model_id}")
|
|
183
|
-
return generated_text, metadata
|
|
184
|
-
except Exception as e:
|
|
185
|
-
logger.error(f"Failed to generate with model {model_id}: {str(e)}")
|
|
186
|
-
raise RuntimeError(f"Failed to generate with model {model_id}: {str(e)}")
|
|
187
|
-
|
|
188
|
-
async def unload_model(self, model_id: str) -> None:
|
|
189
|
-
"""Unload a model from LM Studio.
|
|
190
|
-
|
|
191
|
-
Args:
|
|
192
|
-
model_id: The identifier of the model to unload
|
|
193
|
-
"""
|
|
194
|
-
if not self.initialized:
|
|
195
|
-
return
|
|
196
|
-
|
|
197
|
-
if model_id not in self.models:
|
|
198
|
-
logger.warning(f"Model {model_id} not loaded")
|
|
199
|
-
return
|
|
200
|
-
|
|
201
|
-
try:
|
|
202
|
-
# Just remove the model reference, Python garbage collection will handle it
|
|
203
|
-
del self.models[model_id]
|
|
204
|
-
logger.info(f"Model {model_id} unloaded")
|
|
205
|
-
except Exception as e:
|
|
206
|
-
logger.error(f"Failed to unload model {model_id}: {str(e)}")
|
|
207
|
-
|
|
208
|
-
async def shutdown(self) -> None:
|
|
209
|
-
"""Shutdown the LM Studio provider."""
|
|
210
|
-
if not self.initialized:
|
|
211
|
-
return
|
|
212
|
-
|
|
213
|
-
try:
|
|
214
|
-
# Clear all model references
|
|
215
|
-
self.models = {}
|
|
216
|
-
self.initialized = False
|
|
217
|
-
logger.info("LM Studio provider shut down")
|
|
218
|
-
except Exception as e:
|
|
219
|
-
logger.error(f"Failed to shut down LM Studio provider: {str(e)}")
|
|
@@ -1,120 +0,0 @@
|
|
|
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.")
|
|
@@ -1,86 +0,0 @@
|
|
|
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)
|
|
@@ -1,115 +0,0 @@
|
|
|
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
|
-
from typing import Optional
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def setup_logging(
|
|
15
|
-
log_level: str = "INFO",
|
|
16
|
-
log_to_file: bool = True,
|
|
17
|
-
log_to_console: bool = False, # Changed default to False
|
|
18
|
-
transport: Optional[str] = None,
|
|
19
|
-
testing: bool = False
|
|
20
|
-
) -> None:
|
|
21
|
-
"""Set up logging configuration.
|
|
22
|
-
|
|
23
|
-
Args:
|
|
24
|
-
log_level: The logging level ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL")
|
|
25
|
-
log_to_file: Whether to log to a file in addition to the console (default: True)
|
|
26
|
-
log_to_console: Whether to log to the console (default: False to avoid stdio transport conflicts)
|
|
27
|
-
transport: The transport mechanism being used ("stdio" or "sse")
|
|
28
|
-
testing: Set to True to disable file operations for testing
|
|
29
|
-
"""
|
|
30
|
-
# Convert string log level to logging constant
|
|
31
|
-
numeric_level = getattr(logging, log_level.upper(), None)
|
|
32
|
-
if not isinstance(numeric_level, int):
|
|
33
|
-
raise ValueError(f"Invalid log level: {log_level}")
|
|
34
|
-
|
|
35
|
-
# Create logs directory if needed
|
|
36
|
-
log_dir = Path.home() / ".hanzo" / "logs"
|
|
37
|
-
if log_to_file and not testing:
|
|
38
|
-
log_dir.mkdir(parents=True, exist_ok=True)
|
|
39
|
-
|
|
40
|
-
# Generate log filename based on current date
|
|
41
|
-
current_time = datetime.now().strftime("%Y-%m-%d")
|
|
42
|
-
log_file = log_dir / f"hanzo-mcp-{current_time}.log"
|
|
43
|
-
|
|
44
|
-
# Base configuration
|
|
45
|
-
handlers = []
|
|
46
|
-
|
|
47
|
-
# Console handler - Always use stderr to avoid interfering with stdio transport
|
|
48
|
-
# Disable console logging when using stdio transport to avoid protocol corruption
|
|
49
|
-
if log_to_console and (transport != "stdio"):
|
|
50
|
-
console = logging.StreamHandler(sys.stderr)
|
|
51
|
-
console.setLevel(numeric_level)
|
|
52
|
-
console_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
53
|
-
console.setFormatter(console_formatter)
|
|
54
|
-
handlers.append(console)
|
|
55
|
-
|
|
56
|
-
# File handler (if enabled)
|
|
57
|
-
if log_to_file and not testing:
|
|
58
|
-
file_handler = logging.FileHandler(log_file)
|
|
59
|
-
file_handler.setLevel(numeric_level)
|
|
60
|
-
file_formatter = logging.Formatter(
|
|
61
|
-
'%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s'
|
|
62
|
-
)
|
|
63
|
-
file_handler.setFormatter(file_formatter)
|
|
64
|
-
handlers.append(file_handler)
|
|
65
|
-
|
|
66
|
-
# Configure root logger
|
|
67
|
-
logging.basicConfig(
|
|
68
|
-
level=numeric_level,
|
|
69
|
-
handlers=handlers,
|
|
70
|
-
force=True # Overwrite any existing configuration
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
# Set specific log levels for third-party libraries
|
|
74
|
-
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
|
75
|
-
logging.getLogger("asyncio").setLevel(logging.WARNING)
|
|
76
|
-
|
|
77
|
-
# Log startup message
|
|
78
|
-
root_logger = logging.getLogger()
|
|
79
|
-
root_logger.info(f"Logging initialized at level {log_level}")
|
|
80
|
-
if log_to_file and not testing:
|
|
81
|
-
root_logger.info(f"Log file: {log_file}")
|
|
82
|
-
if not log_to_console or transport == "stdio":
|
|
83
|
-
root_logger.info("Console logging disabled")
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def get_log_files() -> list[str]:
|
|
87
|
-
"""Get a list of all log files.
|
|
88
|
-
|
|
89
|
-
Returns:
|
|
90
|
-
List of log file paths
|
|
91
|
-
"""
|
|
92
|
-
log_dir = Path.home() / ".hanzo" / "logs"
|
|
93
|
-
if not log_dir.exists():
|
|
94
|
-
return []
|
|
95
|
-
|
|
96
|
-
log_files = [str(f) for f in log_dir.glob("hanzo-mcp-*.log")]
|
|
97
|
-
return sorted(log_files, reverse=True)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def get_current_log_file() -> Optional[str]:
|
|
101
|
-
"""Get the path to the current log file.
|
|
102
|
-
|
|
103
|
-
Returns:
|
|
104
|
-
The path to the current log file, or None if no log file exists
|
|
105
|
-
"""
|
|
106
|
-
log_dir = Path.home() / ".hanzo" / "logs"
|
|
107
|
-
if not log_dir.exists():
|
|
108
|
-
return None
|
|
109
|
-
|
|
110
|
-
current_time = datetime.now().strftime("%Y-%m-%d")
|
|
111
|
-
log_file = log_dir / f"hanzo-mcp-{current_time}.log"
|
|
112
|
-
|
|
113
|
-
if log_file.exists():
|
|
114
|
-
return str(log_file)
|
|
115
|
-
return None
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
"""Session management for maintaining state across tool executions."""
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from typing import Dict, Optional, final
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
@final
|
|
9
|
-
class SessionManager:
|
|
10
|
-
"""Manages session state across tool executions."""
|
|
11
|
-
|
|
12
|
-
_instances: Dict[str, "SessionManager"] = {}
|
|
13
|
-
|
|
14
|
-
@classmethod
|
|
15
|
-
def get_instance(cls, session_id: str) -> "SessionManager":
|
|
16
|
-
"""Get or create a session manager instance for the given session ID.
|
|
17
|
-
|
|
18
|
-
Args:
|
|
19
|
-
session_id: The session ID
|
|
20
|
-
|
|
21
|
-
Returns:
|
|
22
|
-
The session manager instance
|
|
23
|
-
"""
|
|
24
|
-
if session_id not in cls._instances:
|
|
25
|
-
cls._instances[session_id] = cls(session_id)
|
|
26
|
-
return cls._instances[session_id]
|
|
27
|
-
|
|
28
|
-
def __init__(self, session_id: str):
|
|
29
|
-
"""Initialize the session manager.
|
|
30
|
-
|
|
31
|
-
Args:
|
|
32
|
-
session_id: The session ID
|
|
33
|
-
"""
|
|
34
|
-
self.session_id = session_id
|
|
35
|
-
self._current_working_dir: Optional[Path] = None
|
|
36
|
-
self._initial_working_dir: Optional[Path] = None
|
|
37
|
-
self._environment_vars: Dict[str, str] = {}
|
|
38
|
-
|
|
39
|
-
@property
|
|
40
|
-
def current_working_dir(self) -> Path:
|
|
41
|
-
"""Get the current working directory.
|
|
42
|
-
|
|
43
|
-
Returns:
|
|
44
|
-
The current working directory
|
|
45
|
-
"""
|
|
46
|
-
if self._current_working_dir is None:
|
|
47
|
-
# Default to project directory if set, otherwise use current directory
|
|
48
|
-
self._current_working_dir = Path(os.getcwd())
|
|
49
|
-
self._initial_working_dir = self._current_working_dir
|
|
50
|
-
return self._current_working_dir
|
|
51
|
-
|
|
52
|
-
def set_working_dir(self, path: Path) -> None:
|
|
53
|
-
"""Set the current working directory.
|
|
54
|
-
|
|
55
|
-
Args:
|
|
56
|
-
path: The path to set as the current working directory
|
|
57
|
-
"""
|
|
58
|
-
self._current_working_dir = path
|
|
59
|
-
|
|
60
|
-
def reset_working_dir(self) -> None:
|
|
61
|
-
"""Reset the working directory to the initial directory."""
|
|
62
|
-
if self._initial_working_dir is not None:
|
|
63
|
-
self._current_working_dir = self._initial_working_dir
|
|
64
|
-
|
|
65
|
-
def set_env_var(self, key: str, value: str) -> None:
|
|
66
|
-
"""Set an environment variable.
|
|
67
|
-
|
|
68
|
-
Args:
|
|
69
|
-
key: The environment variable name
|
|
70
|
-
value: The environment variable value
|
|
71
|
-
"""
|
|
72
|
-
self._environment_vars[key] = value
|
|
73
|
-
|
|
74
|
-
def get_env_var(self, key: str) -> Optional[str]:
|
|
75
|
-
"""Get an environment variable.
|
|
76
|
-
|
|
77
|
-
Args:
|
|
78
|
-
key: The environment variable name
|
|
79
|
-
|
|
80
|
-
Returns:
|
|
81
|
-
The environment variable value, or None if not set
|
|
82
|
-
"""
|
|
83
|
-
return self._environment_vars.get(key)
|
|
84
|
-
|
|
85
|
-
def get_env_vars(self) -> Dict[str, str]:
|
|
86
|
-
"""Get all environment variables.
|
|
87
|
-
|
|
88
|
-
Returns:
|
|
89
|
-
A dictionary of environment variables
|
|
90
|
-
"""
|
|
91
|
-
return self._environment_vars.copy()
|