hanzo-mcp 0.7.7__py3-none-any.whl → 0.8.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 +6 -0
- hanzo_mcp/__main__.py +1 -1
- hanzo_mcp/analytics/__init__.py +2 -2
- hanzo_mcp/analytics/posthog_analytics.py +76 -82
- hanzo_mcp/cli.py +31 -36
- hanzo_mcp/cli_enhanced.py +94 -72
- hanzo_mcp/cli_plugin.py +27 -17
- hanzo_mcp/config/__init__.py +2 -2
- hanzo_mcp/config/settings.py +112 -88
- hanzo_mcp/config/tool_config.py +32 -34
- hanzo_mcp/dev_server.py +66 -67
- hanzo_mcp/prompts/__init__.py +94 -12
- hanzo_mcp/prompts/enhanced_prompts.py +809 -0
- hanzo_mcp/prompts/example_custom_prompt.py +6 -5
- hanzo_mcp/prompts/project_todo_reminder.py +0 -1
- hanzo_mcp/prompts/tool_explorer.py +10 -7
- hanzo_mcp/server.py +17 -21
- hanzo_mcp/server_enhanced.py +15 -22
- hanzo_mcp/tools/__init__.py +56 -28
- hanzo_mcp/tools/agent/__init__.py +16 -19
- hanzo_mcp/tools/agent/agent.py +82 -65
- hanzo_mcp/tools/agent/agent_tool.py +152 -122
- hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +66 -62
- hanzo_mcp/tools/agent/clarification_protocol.py +55 -50
- hanzo_mcp/tools/agent/clarification_tool.py +11 -10
- hanzo_mcp/tools/agent/claude_cli_tool.py +21 -20
- hanzo_mcp/tools/agent/claude_desktop_auth.py +130 -144
- hanzo_mcp/tools/agent/cli_agent_base.py +59 -53
- hanzo_mcp/tools/agent/code_auth.py +102 -107
- hanzo_mcp/tools/agent/code_auth_tool.py +28 -27
- hanzo_mcp/tools/agent/codex_cli_tool.py +20 -19
- hanzo_mcp/tools/agent/critic_tool.py +86 -73
- hanzo_mcp/tools/agent/gemini_cli_tool.py +21 -20
- hanzo_mcp/tools/agent/grok_cli_tool.py +21 -20
- hanzo_mcp/tools/agent/iching_tool.py +404 -139
- hanzo_mcp/tools/agent/network_tool.py +89 -73
- hanzo_mcp/tools/agent/prompt.py +2 -1
- hanzo_mcp/tools/agent/review_tool.py +101 -98
- hanzo_mcp/tools/agent/swarm_alias.py +87 -0
- hanzo_mcp/tools/agent/swarm_tool.py +246 -161
- hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +134 -92
- hanzo_mcp/tools/agent/tool_adapter.py +21 -11
- hanzo_mcp/tools/common/__init__.py +1 -1
- hanzo_mcp/tools/common/base.py +3 -5
- hanzo_mcp/tools/common/batch_tool.py +46 -39
- hanzo_mcp/tools/common/config_tool.py +120 -84
- hanzo_mcp/tools/common/context.py +1 -5
- hanzo_mcp/tools/common/context_fix.py +5 -3
- hanzo_mcp/tools/common/critic_tool.py +4 -8
- hanzo_mcp/tools/common/decorators.py +58 -56
- hanzo_mcp/tools/common/enhanced_base.py +29 -32
- hanzo_mcp/tools/common/fastmcp_pagination.py +91 -94
- hanzo_mcp/tools/common/forgiving_edit.py +91 -87
- hanzo_mcp/tools/common/mode.py +15 -17
- hanzo_mcp/tools/common/mode_loader.py +27 -24
- hanzo_mcp/tools/common/paginated_base.py +61 -53
- hanzo_mcp/tools/common/paginated_response.py +72 -79
- hanzo_mcp/tools/common/pagination.py +50 -53
- hanzo_mcp/tools/common/permissions.py +4 -4
- hanzo_mcp/tools/common/personality.py +186 -138
- hanzo_mcp/tools/common/plugin_loader.py +54 -54
- hanzo_mcp/tools/common/stats.py +65 -47
- hanzo_mcp/tools/common/test_helpers.py +31 -0
- hanzo_mcp/tools/common/thinking_tool.py +4 -8
- hanzo_mcp/tools/common/tool_disable.py +17 -12
- hanzo_mcp/tools/common/tool_enable.py +13 -14
- hanzo_mcp/tools/common/tool_list.py +36 -28
- hanzo_mcp/tools/common/truncate.py +23 -23
- hanzo_mcp/tools/config/__init__.py +4 -4
- hanzo_mcp/tools/config/config_tool.py +42 -29
- hanzo_mcp/tools/config/index_config.py +37 -34
- hanzo_mcp/tools/config/mode_tool.py +175 -55
- hanzo_mcp/tools/database/__init__.py +15 -12
- hanzo_mcp/tools/database/database_manager.py +77 -75
- hanzo_mcp/tools/database/graph.py +137 -91
- hanzo_mcp/tools/database/graph_add.py +30 -18
- hanzo_mcp/tools/database/graph_query.py +178 -102
- hanzo_mcp/tools/database/graph_remove.py +33 -28
- hanzo_mcp/tools/database/graph_search.py +97 -75
- hanzo_mcp/tools/database/graph_stats.py +91 -59
- hanzo_mcp/tools/database/sql.py +107 -79
- hanzo_mcp/tools/database/sql_query.py +30 -24
- hanzo_mcp/tools/database/sql_search.py +29 -25
- hanzo_mcp/tools/database/sql_stats.py +47 -35
- hanzo_mcp/tools/editor/neovim_command.py +25 -28
- hanzo_mcp/tools/editor/neovim_edit.py +21 -23
- hanzo_mcp/tools/editor/neovim_session.py +60 -54
- hanzo_mcp/tools/filesystem/__init__.py +31 -30
- hanzo_mcp/tools/filesystem/ast_multi_edit.py +329 -249
- hanzo_mcp/tools/filesystem/ast_tool.py +4 -4
- hanzo_mcp/tools/filesystem/base.py +1 -1
- hanzo_mcp/tools/filesystem/batch_search.py +316 -224
- hanzo_mcp/tools/filesystem/content_replace.py +4 -4
- hanzo_mcp/tools/filesystem/diff.py +71 -59
- hanzo_mcp/tools/filesystem/directory_tree.py +7 -7
- hanzo_mcp/tools/filesystem/directory_tree_paginated.py +49 -37
- hanzo_mcp/tools/filesystem/edit.py +4 -4
- hanzo_mcp/tools/filesystem/find.py +173 -80
- hanzo_mcp/tools/filesystem/find_files.py +73 -52
- hanzo_mcp/tools/filesystem/git_search.py +157 -104
- hanzo_mcp/tools/filesystem/grep.py +8 -8
- hanzo_mcp/tools/filesystem/multi_edit.py +4 -8
- hanzo_mcp/tools/filesystem/read.py +12 -10
- hanzo_mcp/tools/filesystem/rules_tool.py +59 -43
- hanzo_mcp/tools/filesystem/search_tool.py +263 -207
- hanzo_mcp/tools/filesystem/symbols_tool.py +94 -54
- hanzo_mcp/tools/filesystem/tree.py +35 -33
- hanzo_mcp/tools/filesystem/unix_aliases.py +13 -18
- hanzo_mcp/tools/filesystem/watch.py +37 -36
- hanzo_mcp/tools/filesystem/write.py +4 -8
- hanzo_mcp/tools/jupyter/__init__.py +4 -4
- hanzo_mcp/tools/jupyter/base.py +4 -5
- hanzo_mcp/tools/jupyter/jupyter.py +67 -47
- hanzo_mcp/tools/jupyter/notebook_edit.py +4 -4
- hanzo_mcp/tools/jupyter/notebook_read.py +4 -7
- hanzo_mcp/tools/llm/__init__.py +5 -7
- hanzo_mcp/tools/llm/consensus_tool.py +72 -52
- hanzo_mcp/tools/llm/llm_manage.py +101 -60
- hanzo_mcp/tools/llm/llm_tool.py +226 -166
- hanzo_mcp/tools/llm/provider_tools.py +25 -26
- hanzo_mcp/tools/lsp/__init__.py +1 -1
- hanzo_mcp/tools/lsp/lsp_tool.py +228 -143
- hanzo_mcp/tools/mcp/__init__.py +2 -3
- hanzo_mcp/tools/mcp/mcp_add.py +27 -25
- hanzo_mcp/tools/mcp/mcp_remove.py +7 -8
- hanzo_mcp/tools/mcp/mcp_stats.py +23 -22
- hanzo_mcp/tools/mcp/mcp_tool.py +129 -98
- hanzo_mcp/tools/memory/__init__.py +39 -21
- hanzo_mcp/tools/memory/knowledge_tools.py +124 -99
- hanzo_mcp/tools/memory/memory_tools.py +90 -108
- hanzo_mcp/tools/search/__init__.py +7 -2
- hanzo_mcp/tools/search/find_tool.py +297 -212
- hanzo_mcp/tools/search/unified_search.py +366 -314
- hanzo_mcp/tools/shell/__init__.py +8 -7
- hanzo_mcp/tools/shell/auto_background.py +56 -49
- hanzo_mcp/tools/shell/base.py +1 -1
- hanzo_mcp/tools/shell/base_process.py +75 -75
- hanzo_mcp/tools/shell/bash_session.py +2 -2
- hanzo_mcp/tools/shell/bash_session_executor.py +4 -4
- hanzo_mcp/tools/shell/bash_tool.py +24 -31
- hanzo_mcp/tools/shell/command_executor.py +12 -12
- hanzo_mcp/tools/shell/logs.py +43 -33
- hanzo_mcp/tools/shell/npx.py +13 -13
- hanzo_mcp/tools/shell/npx_background.py +24 -21
- hanzo_mcp/tools/shell/npx_tool.py +18 -22
- hanzo_mcp/tools/shell/open.py +19 -21
- hanzo_mcp/tools/shell/pkill.py +31 -26
- hanzo_mcp/tools/shell/process_tool.py +32 -32
- hanzo_mcp/tools/shell/processes.py +57 -58
- hanzo_mcp/tools/shell/run_background.py +24 -25
- hanzo_mcp/tools/shell/run_command.py +5 -5
- hanzo_mcp/tools/shell/run_command_windows.py +5 -5
- hanzo_mcp/tools/shell/session_storage.py +3 -3
- hanzo_mcp/tools/shell/streaming_command.py +141 -126
- hanzo_mcp/tools/shell/uvx.py +24 -25
- hanzo_mcp/tools/shell/uvx_background.py +35 -33
- hanzo_mcp/tools/shell/uvx_tool.py +18 -22
- hanzo_mcp/tools/todo/__init__.py +6 -2
- hanzo_mcp/tools/todo/todo.py +50 -37
- hanzo_mcp/tools/todo/todo_read.py +5 -8
- hanzo_mcp/tools/todo/todo_write.py +5 -7
- hanzo_mcp/tools/vector/__init__.py +40 -28
- hanzo_mcp/tools/vector/ast_analyzer.py +176 -143
- hanzo_mcp/tools/vector/git_ingester.py +170 -179
- hanzo_mcp/tools/vector/index_tool.py +96 -44
- hanzo_mcp/tools/vector/infinity_store.py +283 -228
- hanzo_mcp/tools/vector/mock_infinity.py +39 -40
- hanzo_mcp/tools/vector/project_manager.py +88 -78
- hanzo_mcp/tools/vector/vector.py +59 -42
- hanzo_mcp/tools/vector/vector_index.py +30 -27
- hanzo_mcp/tools/vector/vector_search.py +64 -45
- hanzo_mcp/types.py +6 -4
- {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.0.dist-info}/METADATA +1 -1
- hanzo_mcp-0.8.0.dist-info/RECORD +185 -0
- hanzo_mcp-0.7.7.dist-info/RECORD +0 -182
- {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.0.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.0.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.0.dist-info}/top_level.txt +0 -0
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
"""Plugin loader for custom user tools."""
|
|
2
2
|
|
|
3
|
-
import importlib.util
|
|
4
|
-
import inspect
|
|
5
|
-
import json
|
|
6
3
|
import os
|
|
7
4
|
import sys
|
|
5
|
+
import json
|
|
6
|
+
import inspect
|
|
7
|
+
import importlib.util
|
|
8
|
+
from typing import Any, Dict, List, Type, Optional
|
|
8
9
|
from pathlib import Path
|
|
9
|
-
from typing import Dict, List, Optional, Type, Any
|
|
10
10
|
from dataclasses import dataclass
|
|
11
11
|
|
|
12
12
|
from .base import BaseTool
|
|
13
|
-
from .context import ToolContext
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
@dataclass
|
|
17
16
|
class ToolPlugin:
|
|
18
17
|
"""Represents a loaded tool plugin."""
|
|
18
|
+
|
|
19
19
|
name: str
|
|
20
20
|
tool_class: Type[BaseTool]
|
|
21
21
|
source_path: Path
|
|
@@ -24,47 +24,47 @@ class ToolPlugin:
|
|
|
24
24
|
|
|
25
25
|
class PluginLoader:
|
|
26
26
|
"""Loads custom tool plugins from user directories."""
|
|
27
|
-
|
|
27
|
+
|
|
28
28
|
def __init__(self):
|
|
29
29
|
self.plugins: Dict[str, ToolPlugin] = {}
|
|
30
30
|
self.plugin_dirs: List[Path] = []
|
|
31
31
|
self._setup_plugin_directories()
|
|
32
|
-
|
|
32
|
+
|
|
33
33
|
def _setup_plugin_directories(self):
|
|
34
34
|
"""Set up standard plugin directories."""
|
|
35
35
|
# User's home directory plugins
|
|
36
36
|
home_plugins = Path.home() / ".hanzo" / "plugins"
|
|
37
37
|
home_plugins.mkdir(parents=True, exist_ok=True)
|
|
38
38
|
self.plugin_dirs.append(home_plugins)
|
|
39
|
-
|
|
39
|
+
|
|
40
40
|
# Project-local plugins
|
|
41
41
|
project_plugins = Path.cwd() / ".hanzo" / "plugins"
|
|
42
42
|
if project_plugins.exists():
|
|
43
43
|
self.plugin_dirs.append(project_plugins)
|
|
44
|
-
|
|
44
|
+
|
|
45
45
|
# Environment variable for additional paths
|
|
46
46
|
if custom_paths := os.environ.get("HANZO_PLUGIN_PATH"):
|
|
47
47
|
for path in custom_paths.split(":"):
|
|
48
48
|
plugin_dir = Path(path)
|
|
49
49
|
if plugin_dir.exists():
|
|
50
50
|
self.plugin_dirs.append(plugin_dir)
|
|
51
|
-
|
|
51
|
+
|
|
52
52
|
def load_plugins(self) -> Dict[str, ToolPlugin]:
|
|
53
53
|
"""Load all plugins from configured directories."""
|
|
54
54
|
for plugin_dir in self.plugin_dirs:
|
|
55
55
|
if not plugin_dir.exists():
|
|
56
56
|
continue
|
|
57
|
-
|
|
57
|
+
|
|
58
58
|
# Look for Python files
|
|
59
59
|
for py_file in plugin_dir.glob("*.py"):
|
|
60
60
|
if py_file.name.startswith("_"):
|
|
61
61
|
continue
|
|
62
|
-
|
|
62
|
+
|
|
63
63
|
try:
|
|
64
64
|
self._load_plugin_file(py_file)
|
|
65
65
|
except Exception as e:
|
|
66
66
|
print(f"Failed to load plugin {py_file}: {e}")
|
|
67
|
-
|
|
67
|
+
|
|
68
68
|
# Look for plugin packages
|
|
69
69
|
for package_dir in plugin_dir.iterdir():
|
|
70
70
|
if package_dir.is_dir() and (package_dir / "__init__.py").exists():
|
|
@@ -72,88 +72,88 @@ class PluginLoader:
|
|
|
72
72
|
self._load_plugin_package(package_dir)
|
|
73
73
|
except Exception as e:
|
|
74
74
|
print(f"Failed to load plugin package {package_dir}: {e}")
|
|
75
|
-
|
|
75
|
+
|
|
76
76
|
return self.plugins
|
|
77
|
-
|
|
77
|
+
|
|
78
78
|
def _load_plugin_file(self, file_path: Path):
|
|
79
79
|
"""Load a single plugin file."""
|
|
80
80
|
# Load the module
|
|
81
81
|
spec = importlib.util.spec_from_file_location(file_path.stem, file_path)
|
|
82
82
|
if not spec or not spec.loader:
|
|
83
83
|
return
|
|
84
|
-
|
|
84
|
+
|
|
85
85
|
module = importlib.util.module_from_spec(spec)
|
|
86
86
|
sys.modules[file_path.stem] = module
|
|
87
87
|
spec.loader.exec_module(module)
|
|
88
|
-
|
|
88
|
+
|
|
89
89
|
# Find tool classes
|
|
90
|
-
for
|
|
91
|
-
if (
|
|
92
|
-
|
|
93
|
-
obj
|
|
94
|
-
|
|
95
|
-
|
|
90
|
+
for _name, obj in inspect.getmembers(module):
|
|
91
|
+
if (
|
|
92
|
+
inspect.isclass(obj)
|
|
93
|
+
and issubclass(obj, BaseTool)
|
|
94
|
+
and obj != BaseTool
|
|
95
|
+
and hasattr(obj, "name")
|
|
96
|
+
):
|
|
96
97
|
# Load metadata if available
|
|
97
98
|
metadata = None
|
|
98
|
-
metadata_file = file_path.with_suffix(
|
|
99
|
+
metadata_file = file_path.with_suffix(".json")
|
|
99
100
|
if metadata_file.exists():
|
|
100
101
|
with open(metadata_file) as f:
|
|
101
102
|
metadata = json.load(f)
|
|
102
|
-
|
|
103
|
+
|
|
103
104
|
plugin = ToolPlugin(
|
|
104
105
|
name=obj.name,
|
|
105
106
|
tool_class=obj,
|
|
106
107
|
source_path=file_path,
|
|
107
|
-
metadata=metadata
|
|
108
|
+
metadata=metadata,
|
|
108
109
|
)
|
|
109
110
|
self.plugins[obj.name] = plugin
|
|
110
|
-
|
|
111
|
+
|
|
111
112
|
def _load_plugin_package(self, package_dir: Path):
|
|
112
113
|
"""Load a plugin package."""
|
|
113
114
|
# Add parent to path temporarily
|
|
114
115
|
parent = str(package_dir.parent)
|
|
115
116
|
if parent not in sys.path:
|
|
116
117
|
sys.path.insert(0, parent)
|
|
117
|
-
|
|
118
|
+
|
|
118
119
|
try:
|
|
119
120
|
# Import the package
|
|
120
121
|
module = importlib.import_module(package_dir.name)
|
|
121
|
-
|
|
122
|
+
|
|
122
123
|
# Look for tools
|
|
123
|
-
if hasattr(module,
|
|
124
|
+
if hasattr(module, "TOOLS"):
|
|
124
125
|
# Package exports TOOLS list
|
|
125
126
|
for tool_class in module.TOOLS:
|
|
126
127
|
if issubclass(tool_class, BaseTool):
|
|
127
128
|
plugin = ToolPlugin(
|
|
128
129
|
name=tool_class.name,
|
|
129
130
|
tool_class=tool_class,
|
|
130
|
-
source_path=package_dir
|
|
131
|
+
source_path=package_dir,
|
|
131
132
|
)
|
|
132
133
|
self.plugins[tool_class.name] = plugin
|
|
133
134
|
else:
|
|
134
135
|
# Search for tool classes
|
|
135
|
-
for
|
|
136
|
-
if (
|
|
137
|
-
|
|
138
|
-
obj
|
|
139
|
-
|
|
140
|
-
|
|
136
|
+
for _name, obj in inspect.getmembers(module):
|
|
137
|
+
if (
|
|
138
|
+
inspect.isclass(obj)
|
|
139
|
+
and issubclass(obj, BaseTool)
|
|
140
|
+
and obj != BaseTool
|
|
141
|
+
and hasattr(obj, "name")
|
|
142
|
+
):
|
|
141
143
|
plugin = ToolPlugin(
|
|
142
|
-
name=obj.name,
|
|
143
|
-
tool_class=obj,
|
|
144
|
-
source_path=package_dir
|
|
144
|
+
name=obj.name, tool_class=obj, source_path=package_dir
|
|
145
145
|
)
|
|
146
146
|
self.plugins[obj.name] = plugin
|
|
147
147
|
finally:
|
|
148
148
|
# Remove from path
|
|
149
149
|
if parent in sys.path:
|
|
150
150
|
sys.path.remove(parent)
|
|
151
|
-
|
|
151
|
+
|
|
152
152
|
def get_tool_class(self, name: str) -> Optional[Type[BaseTool]]:
|
|
153
153
|
"""Get a tool class by name."""
|
|
154
154
|
plugin = self.plugins.get(name)
|
|
155
155
|
return plugin.tool_class if plugin else None
|
|
156
|
-
|
|
156
|
+
|
|
157
157
|
def list_plugins(self) -> List[str]:
|
|
158
158
|
"""List all loaded plugin names."""
|
|
159
159
|
return list(self.plugins.keys())
|
|
@@ -181,7 +181,7 @@ def list_plugin_tools() -> List[str]:
|
|
|
181
181
|
def create_plugin_template(output_dir: Path, tool_name: str):
|
|
182
182
|
"""Create a template for a new plugin tool."""
|
|
183
183
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
184
|
-
|
|
184
|
+
|
|
185
185
|
# Create tool file
|
|
186
186
|
tool_file = output_dir / f"{tool_name}_tool.py"
|
|
187
187
|
tool_content = f'''"""Custom {tool_name} tool plugin."""
|
|
@@ -225,10 +225,10 @@ class {tool_name.title()}Tool(BaseTool):
|
|
|
225
225
|
# Optional: Export tools explicitly
|
|
226
226
|
TOOLS = [{tool_name.title()}Tool]
|
|
227
227
|
'''
|
|
228
|
-
|
|
229
|
-
with open(tool_file,
|
|
228
|
+
|
|
229
|
+
with open(tool_file, "w") as f:
|
|
230
230
|
f.write(tool_content)
|
|
231
|
-
|
|
231
|
+
|
|
232
232
|
# Create metadata file
|
|
233
233
|
metadata_file = output_dir / f"{tool_name}_tool.json"
|
|
234
234
|
metadata_content = {
|
|
@@ -240,12 +240,12 @@ TOOLS = [{tool_name.title()}Tool]
|
|
|
240
240
|
"dependencies": [],
|
|
241
241
|
"config": {
|
|
242
242
|
# Tool-specific configuration
|
|
243
|
-
}
|
|
243
|
+
},
|
|
244
244
|
}
|
|
245
|
-
|
|
246
|
-
with open(metadata_file,
|
|
245
|
+
|
|
246
|
+
with open(metadata_file, "w") as f:
|
|
247
247
|
json.dump(metadata_content, f, indent=2)
|
|
248
|
-
|
|
248
|
+
|
|
249
249
|
# Create README
|
|
250
250
|
readme_file = output_dir / "README.md"
|
|
251
251
|
readme_content = f"""# {tool_name.title()} Tool Plugin
|
|
@@ -276,12 +276,12 @@ Edit the `{tool_name}_tool.json` file to:
|
|
|
276
276
|
|
|
277
277
|
Modify `{tool_name}_tool.py` to implement your custom functionality.
|
|
278
278
|
"""
|
|
279
|
-
|
|
280
|
-
with open(readme_file,
|
|
279
|
+
|
|
280
|
+
with open(readme_file, "w") as f:
|
|
281
281
|
f.write(readme_content)
|
|
282
|
-
|
|
282
|
+
|
|
283
283
|
print(f"Created plugin template in {output_dir}")
|
|
284
284
|
print(f"Files created:")
|
|
285
285
|
print(f" - {tool_file}")
|
|
286
286
|
print(f" - {metadata_file}")
|
|
287
|
-
print(f" - {readme_file}")
|
|
287
|
+
print(f" - {readme_file}")
|
hanzo_mcp/tools/common/stats.py
CHANGED
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
"""Comprehensive system and MCP statistics."""
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import psutil
|
|
5
|
-
import shutil
|
|
6
|
-
from typing import TypedDict, Unpack, final, override
|
|
7
|
-
from datetime import datetime
|
|
3
|
+
from typing import Unpack, TypedDict, final, override
|
|
8
4
|
from pathlib import Path
|
|
5
|
+
from datetime import datetime
|
|
9
6
|
|
|
7
|
+
import psutil
|
|
10
8
|
from mcp.server.fastmcp import Context as MCPContext
|
|
11
9
|
|
|
12
10
|
from hanzo_mcp.tools.common.base import BaseTool
|
|
11
|
+
from hanzo_mcp.tools.mcp.mcp_add import McpAddTool
|
|
13
12
|
from hanzo_mcp.tools.common.context import create_tool_context
|
|
14
13
|
from hanzo_mcp.tools.shell.run_background import RunBackgroundTool
|
|
15
|
-
from hanzo_mcp.tools.mcp.mcp_add import McpAddTool
|
|
16
14
|
from hanzo_mcp.tools.database.database_manager import DatabaseManager
|
|
17
15
|
|
|
18
16
|
|
|
19
17
|
class StatsParams(TypedDict, total=False):
|
|
20
18
|
"""Parameters for stats tool."""
|
|
19
|
+
|
|
21
20
|
pass
|
|
22
21
|
|
|
23
22
|
|
|
@@ -27,7 +26,7 @@ class StatsTool(BaseTool):
|
|
|
27
26
|
|
|
28
27
|
def __init__(self, db_manager: DatabaseManager = None):
|
|
29
28
|
"""Initialize the stats tool.
|
|
30
|
-
|
|
29
|
+
|
|
31
30
|
Args:
|
|
32
31
|
db_manager: Optional database manager for DB stats
|
|
33
32
|
"""
|
|
@@ -77,50 +76,56 @@ Example:
|
|
|
77
76
|
|
|
78
77
|
output = []
|
|
79
78
|
warnings = []
|
|
80
|
-
|
|
79
|
+
|
|
81
80
|
# Header
|
|
82
81
|
output.append("=== Hanzo AI System Statistics ===")
|
|
83
82
|
output.append(f"Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
|
84
83
|
output.append("")
|
|
85
|
-
|
|
84
|
+
|
|
86
85
|
# System Resources
|
|
87
86
|
output.append("=== System Resources ===")
|
|
88
|
-
|
|
87
|
+
|
|
89
88
|
# CPU
|
|
90
89
|
cpu_percent = psutil.cpu_percent(interval=1)
|
|
91
90
|
cpu_count = psutil.cpu_count()
|
|
92
91
|
output.append(f"CPU Usage: {cpu_percent}% ({cpu_count} cores)")
|
|
93
92
|
if cpu_percent > 90:
|
|
94
93
|
warnings.append(f"⚠️ HIGH CPU USAGE: {cpu_percent}%")
|
|
95
|
-
|
|
94
|
+
|
|
96
95
|
# Memory
|
|
97
96
|
memory = psutil.virtual_memory()
|
|
98
97
|
memory_used_gb = memory.used / (1024**3)
|
|
99
98
|
memory_total_gb = memory.total / (1024**3)
|
|
100
99
|
memory_percent = memory.percent
|
|
101
|
-
output.append(
|
|
100
|
+
output.append(
|
|
101
|
+
f"Memory: {memory_used_gb:.1f}/{memory_total_gb:.1f} GB ({memory_percent}%)"
|
|
102
|
+
)
|
|
102
103
|
if memory_percent > 90:
|
|
103
104
|
warnings.append(f"⚠️ HIGH MEMORY USAGE: {memory_percent}%")
|
|
104
|
-
|
|
105
|
+
|
|
105
106
|
# Disk
|
|
106
|
-
disk = psutil.disk_usage(
|
|
107
|
+
disk = psutil.disk_usage("/")
|
|
107
108
|
disk_used_gb = disk.used / (1024**3)
|
|
108
109
|
disk_total_gb = disk.total / (1024**3)
|
|
109
110
|
disk_percent = disk.percent
|
|
110
111
|
disk_free_gb = disk.free / (1024**3)
|
|
111
|
-
output.append(
|
|
112
|
+
output.append(
|
|
113
|
+
f"Disk: {disk_used_gb:.1f}/{disk_total_gb:.1f} GB ({disk_percent}%)"
|
|
114
|
+
)
|
|
112
115
|
output.append(f"Free Space: {disk_free_gb:.1f} GB")
|
|
113
116
|
if disk_percent > 90:
|
|
114
|
-
warnings.append(
|
|
115
|
-
|
|
117
|
+
warnings.append(
|
|
118
|
+
f"⚠️ LOW DISK SPACE: Only {disk_free_gb:.1f} GB free ({100 - disk_percent:.1f}% remaining)"
|
|
119
|
+
)
|
|
120
|
+
|
|
116
121
|
output.append("")
|
|
117
|
-
|
|
122
|
+
|
|
118
123
|
# Background Processes
|
|
119
124
|
output.append("=== Background Processes ===")
|
|
120
125
|
processes = RunBackgroundTool.get_processes()
|
|
121
126
|
running_count = 0
|
|
122
127
|
total_memory_mb = 0
|
|
123
|
-
|
|
128
|
+
|
|
124
129
|
if processes:
|
|
125
130
|
for proc in processes.values():
|
|
126
131
|
if proc.is_running():
|
|
@@ -129,12 +134,12 @@ Example:
|
|
|
129
134
|
ps_proc = psutil.Process(proc.process.pid)
|
|
130
135
|
memory_mb = ps_proc.memory_info().rss / (1024**2)
|
|
131
136
|
total_memory_mb += memory_mb
|
|
132
|
-
except:
|
|
137
|
+
except Exception:
|
|
133
138
|
pass
|
|
134
|
-
|
|
139
|
+
|
|
135
140
|
output.append(f"Running Processes: {running_count}")
|
|
136
141
|
output.append(f"Total Memory Usage: {total_memory_mb:.1f} MB")
|
|
137
|
-
|
|
142
|
+
|
|
138
143
|
# List top processes by memory
|
|
139
144
|
if running_count > 0:
|
|
140
145
|
output.append("\nTop Processes:")
|
|
@@ -146,31 +151,33 @@ Example:
|
|
|
146
151
|
memory_mb = ps_proc.memory_info().rss / (1024**2)
|
|
147
152
|
cpu = ps_proc.cpu_percent(interval=0.1)
|
|
148
153
|
proc_list.append((proc.name, memory_mb, cpu, proc_id))
|
|
149
|
-
except:
|
|
154
|
+
except Exception:
|
|
150
155
|
proc_list.append((proc.name, 0, 0, proc_id))
|
|
151
|
-
|
|
156
|
+
|
|
152
157
|
proc_list.sort(key=lambda x: x[1], reverse=True)
|
|
153
158
|
for name, mem, cpu, pid in proc_list[:5]:
|
|
154
159
|
output.append(f" - {name} ({pid}): {mem:.1f} MB, {cpu:.1f}% CPU")
|
|
155
160
|
else:
|
|
156
161
|
output.append("No background processes running")
|
|
157
|
-
|
|
162
|
+
|
|
158
163
|
output.append("")
|
|
159
|
-
|
|
164
|
+
|
|
160
165
|
# Database Usage
|
|
161
166
|
if self.db_manager:
|
|
162
167
|
output.append("=== Database Usage ===")
|
|
163
168
|
db_dir = Path.home() / ".hanzo" / "db"
|
|
164
169
|
total_db_size = 0
|
|
165
|
-
|
|
170
|
+
|
|
166
171
|
if db_dir.exists():
|
|
167
172
|
for db_file in db_dir.rglob("*.db"):
|
|
168
173
|
size = db_file.stat().st_size
|
|
169
174
|
total_db_size += size
|
|
170
|
-
|
|
171
|
-
output.append(
|
|
175
|
+
|
|
176
|
+
output.append(
|
|
177
|
+
f"Total Database Size: {total_db_size / (1024**2):.1f} MB"
|
|
178
|
+
)
|
|
172
179
|
output.append(f"Active Projects: {len(self.db_manager.projects)}")
|
|
173
|
-
|
|
180
|
+
|
|
174
181
|
# List largest databases
|
|
175
182
|
db_sizes = []
|
|
176
183
|
for db_file in db_dir.rglob("*.db"):
|
|
@@ -179,7 +186,7 @@ Example:
|
|
|
179
186
|
project = db_file.parent.parent.name
|
|
180
187
|
db_type = db_file.stem
|
|
181
188
|
db_sizes.append((project, db_type, size))
|
|
182
|
-
|
|
189
|
+
|
|
183
190
|
if db_sizes:
|
|
184
191
|
db_sizes.sort(key=lambda x: x[2], reverse=True)
|
|
185
192
|
output.append("\nLargest Databases:")
|
|
@@ -187,43 +194,47 @@ Example:
|
|
|
187
194
|
output.append(f" - {project}/{db_type}: {size:.1f} MB")
|
|
188
195
|
else:
|
|
189
196
|
output.append("No databases found")
|
|
190
|
-
|
|
197
|
+
|
|
191
198
|
output.append("")
|
|
192
|
-
|
|
199
|
+
|
|
193
200
|
# MCP Servers
|
|
194
201
|
output.append("=== MCP Servers ===")
|
|
195
202
|
mcp_servers = McpAddTool.get_servers()
|
|
196
203
|
if mcp_servers:
|
|
197
|
-
running_mcp = sum(
|
|
204
|
+
running_mcp = sum(
|
|
205
|
+
1 for s in mcp_servers.values() if s.get("status") == "running"
|
|
206
|
+
)
|
|
198
207
|
total_mcp_tools = sum(len(s.get("tools", [])) for s in mcp_servers.values())
|
|
199
|
-
|
|
208
|
+
|
|
200
209
|
output.append(f"Total Servers: {len(mcp_servers)}")
|
|
201
210
|
output.append(f"Running: {running_mcp}")
|
|
202
211
|
output.append(f"Total Tools Available: {total_mcp_tools}")
|
|
203
212
|
else:
|
|
204
213
|
output.append("No MCP servers configured")
|
|
205
|
-
|
|
214
|
+
|
|
206
215
|
output.append("")
|
|
207
|
-
|
|
216
|
+
|
|
208
217
|
# Hanzo AI Specifics
|
|
209
218
|
output.append("=== Hanzo AI ===")
|
|
210
|
-
|
|
219
|
+
|
|
211
220
|
# Log directory size
|
|
212
221
|
log_dir = Path.home() / ".hanzo" / "logs"
|
|
213
222
|
if log_dir.exists():
|
|
214
223
|
log_size = sum(f.stat().st_size for f in log_dir.rglob("*") if f.is_file())
|
|
215
224
|
log_count = len(list(log_dir.rglob("*.log")))
|
|
216
225
|
output.append(f"Log Files: {log_count} ({log_size / (1024**2):.1f} MB)")
|
|
217
|
-
|
|
226
|
+
|
|
218
227
|
if log_size > 100 * 1024**2: # > 100MB
|
|
219
|
-
warnings.append(
|
|
220
|
-
|
|
228
|
+
warnings.append(
|
|
229
|
+
f"⚠️ Large log directory: {log_size / (1024**2):.1f} MB"
|
|
230
|
+
)
|
|
231
|
+
|
|
221
232
|
# Config directory
|
|
222
233
|
config_dir = Path.home() / ".hanzo" / "mcp"
|
|
223
234
|
if config_dir.exists():
|
|
224
235
|
config_count = len(list(config_dir.rglob("*.json")))
|
|
225
236
|
output.append(f"Config Files: {config_count}")
|
|
226
|
-
|
|
237
|
+
|
|
227
238
|
# Tool status (if available)
|
|
228
239
|
# TODO: Track tool usage statistics
|
|
229
240
|
output.append("\nTool Categories:")
|
|
@@ -232,14 +243,14 @@ Example:
|
|
|
232
243
|
output.append(" - Database: sql_query, graph_query, vector_search")
|
|
233
244
|
output.append(" - Package Runners: uvx, npx, uvx_background, npx_background")
|
|
234
245
|
output.append(" - MCP Management: mcp_add, mcp_remove, mcp_stats")
|
|
235
|
-
|
|
246
|
+
|
|
236
247
|
# Warnings Section
|
|
237
248
|
if warnings:
|
|
238
249
|
output.append("\n=== ⚠️ WARNINGS ===")
|
|
239
250
|
for warning in warnings:
|
|
240
251
|
output.append(warning)
|
|
241
252
|
output.append("")
|
|
242
|
-
|
|
253
|
+
|
|
243
254
|
# Recommendations
|
|
244
255
|
output.append("=== Recommendations ===")
|
|
245
256
|
if disk_free_gb < 5:
|
|
@@ -250,10 +261,17 @@ Example:
|
|
|
250
261
|
output.append("- Consider stopping unused background processes")
|
|
251
262
|
if log_size > 50 * 1024**2:
|
|
252
263
|
output.append("- Clean up old log files in ~/.hanzo/logs")
|
|
253
|
-
|
|
254
|
-
if not any(
|
|
264
|
+
|
|
265
|
+
if not any(
|
|
266
|
+
[
|
|
267
|
+
disk_free_gb < 5,
|
|
268
|
+
memory_percent > 80,
|
|
269
|
+
running_count > 10,
|
|
270
|
+
log_size > 50 * 1024**2,
|
|
271
|
+
]
|
|
272
|
+
):
|
|
255
273
|
output.append("✅ System resources are healthy")
|
|
256
|
-
|
|
274
|
+
|
|
257
275
|
return "\n".join(output)
|
|
258
276
|
|
|
259
277
|
def register(self, mcp_server) -> None:
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Test helper classes for MCP tools testing."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PaginatedResponseWrapper:
|
|
7
|
+
"""Wrapper class for paginated responses to support tests."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, items=None, next_cursor=None, has_more=False, total_items=None):
|
|
10
|
+
"""Initialize paginated response."""
|
|
11
|
+
self.items = items or []
|
|
12
|
+
self.next_cursor = next_cursor
|
|
13
|
+
self.has_more = has_more
|
|
14
|
+
self.total_items = total_items or len(self.items)
|
|
15
|
+
|
|
16
|
+
def to_json(self) -> Dict[str, Any]:
|
|
17
|
+
"""Convert to JSON-serializable dict."""
|
|
18
|
+
return {
|
|
19
|
+
"items": self.items,
|
|
20
|
+
"_meta": {
|
|
21
|
+
"next_cursor": self.next_cursor,
|
|
22
|
+
"has_more": self.has_more,
|
|
23
|
+
"total_items": self.total_items,
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Export a convenience constructor
|
|
29
|
+
def PaginatedResponse(items=None, next_cursor=None, has_more=False, total_items=None):
|
|
30
|
+
"""Create a paginated response for testing."""
|
|
31
|
+
return PaginatedResponseWrapper(items, next_cursor, has_more, total_items)
|
|
@@ -3,16 +3,15 @@
|
|
|
3
3
|
This module provides the ThinkingTool for Claude to engage in structured thinking.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
from typing import Annotated, TypedDict,
|
|
6
|
+
from typing import Unpack, Annotated, TypedDict, final, override
|
|
7
7
|
|
|
8
|
-
from mcp.server.fastmcp import Context as MCPContext
|
|
9
|
-
from mcp.server import FastMCP
|
|
10
8
|
from pydantic import Field
|
|
9
|
+
from mcp.server import FastMCP
|
|
10
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
11
11
|
|
|
12
12
|
from hanzo_mcp.tools.common.base import BaseTool
|
|
13
13
|
from hanzo_mcp.tools.common.context import create_tool_context
|
|
14
14
|
|
|
15
|
-
|
|
16
15
|
Thought = Annotated[
|
|
17
16
|
str,
|
|
18
17
|
Field(
|
|
@@ -144,8 +143,5 @@ Feature Implementation Planning
|
|
|
144
143
|
tool_self = self # Create a reference to self for use in the closure
|
|
145
144
|
|
|
146
145
|
@mcp_server.tool(name=self.name, description=self.description)
|
|
147
|
-
async def think(
|
|
148
|
-
thought: Thought,
|
|
149
|
-
ctx: MCPContext
|
|
150
|
-
) -> str:
|
|
146
|
+
async def think(thought: Thought, ctx: MCPContext) -> str:
|
|
151
147
|
return await tool_self.call(ctx, thought=thought)
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
"""Disable tools dynamically."""
|
|
2
2
|
|
|
3
|
-
from typing import Annotated, TypedDict,
|
|
3
|
+
from typing import Unpack, Annotated, TypedDict, final, override
|
|
4
4
|
|
|
5
|
-
from mcp.server.fastmcp import Context as MCPContext
|
|
6
5
|
from pydantic import Field
|
|
6
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
7
7
|
|
|
8
8
|
from hanzo_mcp.tools.common.base import BaseTool
|
|
9
9
|
from hanzo_mcp.tools.common.context import create_tool_context
|
|
10
10
|
from hanzo_mcp.tools.common.tool_enable import ToolEnableTool
|
|
11
11
|
|
|
12
|
-
|
|
13
12
|
ToolName = Annotated[
|
|
14
13
|
str,
|
|
15
14
|
Field(
|
|
@@ -104,13 +103,13 @@ Use 'tool_enable' to re-enable disabled tools.
|
|
|
104
103
|
|
|
105
104
|
# Check current state
|
|
106
105
|
was_enabled = ToolEnableTool.is_tool_enabled(tool_name)
|
|
107
|
-
|
|
106
|
+
|
|
108
107
|
if not was_enabled:
|
|
109
108
|
return f"Tool '{tool_name}' is already disabled."
|
|
110
109
|
|
|
111
110
|
# Disable the tool
|
|
112
111
|
ToolEnableTool._tool_states[tool_name] = False
|
|
113
|
-
|
|
112
|
+
|
|
114
113
|
# Persist if requested
|
|
115
114
|
if persist:
|
|
116
115
|
ToolEnableTool._save_states()
|
|
@@ -124,19 +123,25 @@ Use 'tool_enable' to re-enable disabled tools.
|
|
|
124
123
|
"The tool is now unavailable for use.",
|
|
125
124
|
f"Use 'tool_enable --tool {tool_name}' to re-enable it.",
|
|
126
125
|
]
|
|
127
|
-
|
|
126
|
+
|
|
128
127
|
if not persist:
|
|
129
|
-
output.append(
|
|
130
|
-
|
|
128
|
+
output.append(
|
|
129
|
+
"\nNote: This change is temporary and will be lost on restart."
|
|
130
|
+
)
|
|
131
|
+
|
|
131
132
|
# Warn about commonly used tools
|
|
132
133
|
common_tools = {"grep", "read", "write", "bash", "edit"}
|
|
133
134
|
if tool_name in common_tools:
|
|
134
|
-
output.append(
|
|
135
|
-
|
|
135
|
+
output.append(
|
|
136
|
+
f"\n⚠️ Warning: '{tool_name}' is a commonly used tool. Disabling it may affect normal operations."
|
|
137
|
+
)
|
|
138
|
+
|
|
136
139
|
# Count disabled tools
|
|
137
|
-
disabled_count = sum(
|
|
140
|
+
disabled_count = sum(
|
|
141
|
+
1 for enabled in ToolEnableTool._tool_states.values() if not enabled
|
|
142
|
+
)
|
|
138
143
|
output.append(f"\nTotal disabled tools: {disabled_count}")
|
|
139
|
-
|
|
144
|
+
|
|
140
145
|
return "\n".join(output)
|
|
141
146
|
|
|
142
147
|
def register(self, mcp_server) -> None:
|