hanzo-mcp 0.6.12__py3-none-any.whl → 0.7.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 +2 -2
- hanzo_mcp/analytics/__init__.py +5 -0
- hanzo_mcp/analytics/posthog_analytics.py +364 -0
- hanzo_mcp/cli.py +5 -5
- hanzo_mcp/cli_enhanced.py +7 -7
- hanzo_mcp/cli_plugin.py +91 -0
- hanzo_mcp/config/__init__.py +1 -1
- hanzo_mcp/config/settings.py +70 -7
- hanzo_mcp/config/tool_config.py +20 -6
- hanzo_mcp/dev_server.py +3 -3
- hanzo_mcp/prompts/project_system.py +1 -1
- hanzo_mcp/server.py +40 -3
- hanzo_mcp/server_enhanced.py +69 -0
- hanzo_mcp/tools/__init__.py +140 -31
- hanzo_mcp/tools/agent/__init__.py +85 -4
- hanzo_mcp/tools/agent/agent_tool.py +104 -6
- hanzo_mcp/tools/agent/agent_tool_v2.py +459 -0
- hanzo_mcp/tools/agent/clarification_protocol.py +220 -0
- hanzo_mcp/tools/agent/clarification_tool.py +68 -0
- hanzo_mcp/tools/agent/claude_cli_tool.py +125 -0
- hanzo_mcp/tools/agent/claude_desktop_auth.py +508 -0
- hanzo_mcp/tools/agent/cli_agent_base.py +191 -0
- hanzo_mcp/tools/agent/code_auth.py +436 -0
- hanzo_mcp/tools/agent/code_auth_tool.py +194 -0
- hanzo_mcp/tools/agent/codex_cli_tool.py +123 -0
- hanzo_mcp/tools/agent/critic_tool.py +376 -0
- hanzo_mcp/tools/agent/gemini_cli_tool.py +128 -0
- hanzo_mcp/tools/agent/grok_cli_tool.py +128 -0
- hanzo_mcp/tools/agent/iching_tool.py +380 -0
- hanzo_mcp/tools/agent/network_tool.py +273 -0
- hanzo_mcp/tools/agent/prompt.py +62 -20
- hanzo_mcp/tools/agent/review_tool.py +433 -0
- hanzo_mcp/tools/agent/swarm_tool.py +535 -0
- hanzo_mcp/tools/agent/swarm_tool_v2.py +594 -0
- hanzo_mcp/tools/common/__init__.py +15 -1
- hanzo_mcp/tools/common/base.py +5 -4
- hanzo_mcp/tools/common/batch_tool.py +103 -11
- hanzo_mcp/tools/common/config_tool.py +2 -2
- hanzo_mcp/tools/common/context.py +2 -2
- hanzo_mcp/tools/common/context_fix.py +26 -0
- hanzo_mcp/tools/common/critic_tool.py +196 -0
- hanzo_mcp/tools/common/decorators.py +208 -0
- hanzo_mcp/tools/common/enhanced_base.py +106 -0
- hanzo_mcp/tools/common/fastmcp_pagination.py +369 -0
- hanzo_mcp/tools/common/forgiving_edit.py +243 -0
- hanzo_mcp/tools/common/mode.py +116 -0
- hanzo_mcp/tools/common/mode_loader.py +105 -0
- hanzo_mcp/tools/common/paginated_base.py +230 -0
- hanzo_mcp/tools/common/paginated_response.py +307 -0
- hanzo_mcp/tools/common/pagination.py +226 -0
- hanzo_mcp/tools/common/permissions.py +1 -1
- hanzo_mcp/tools/common/personality.py +936 -0
- hanzo_mcp/tools/common/plugin_loader.py +287 -0
- hanzo_mcp/tools/common/stats.py +4 -4
- hanzo_mcp/tools/common/tool_list.py +4 -1
- hanzo_mcp/tools/common/truncate.py +101 -0
- hanzo_mcp/tools/common/validation.py +1 -1
- hanzo_mcp/tools/config/__init__.py +3 -1
- hanzo_mcp/tools/config/config_tool.py +1 -1
- hanzo_mcp/tools/config/mode_tool.py +209 -0
- hanzo_mcp/tools/database/__init__.py +1 -1
- hanzo_mcp/tools/editor/__init__.py +1 -1
- hanzo_mcp/tools/filesystem/__init__.py +48 -14
- hanzo_mcp/tools/filesystem/ast_multi_edit.py +562 -0
- hanzo_mcp/tools/filesystem/batch_search.py +3 -3
- hanzo_mcp/tools/filesystem/diff.py +2 -2
- hanzo_mcp/tools/filesystem/directory_tree_paginated.py +338 -0
- hanzo_mcp/tools/filesystem/rules_tool.py +235 -0
- hanzo_mcp/tools/filesystem/{unified_search.py → search_tool.py} +12 -12
- hanzo_mcp/tools/filesystem/{symbols_unified.py → symbols_tool.py} +104 -5
- hanzo_mcp/tools/filesystem/watch.py +3 -2
- hanzo_mcp/tools/jupyter/__init__.py +2 -2
- hanzo_mcp/tools/jupyter/jupyter.py +1 -1
- hanzo_mcp/tools/llm/__init__.py +3 -3
- hanzo_mcp/tools/llm/llm_tool.py +648 -143
- hanzo_mcp/tools/lsp/__init__.py +5 -0
- hanzo_mcp/tools/lsp/lsp_tool.py +512 -0
- hanzo_mcp/tools/mcp/__init__.py +2 -2
- hanzo_mcp/tools/mcp/{mcp_unified.py → mcp_tool.py} +3 -3
- hanzo_mcp/tools/memory/__init__.py +76 -0
- hanzo_mcp/tools/memory/knowledge_tools.py +518 -0
- hanzo_mcp/tools/memory/memory_tools.py +456 -0
- hanzo_mcp/tools/search/__init__.py +6 -0
- hanzo_mcp/tools/search/find_tool.py +581 -0
- hanzo_mcp/tools/search/unified_search.py +953 -0
- hanzo_mcp/tools/shell/__init__.py +11 -6
- hanzo_mcp/tools/shell/auto_background.py +203 -0
- hanzo_mcp/tools/shell/base_process.py +57 -29
- hanzo_mcp/tools/shell/bash_session_executor.py +1 -1
- hanzo_mcp/tools/shell/{bash_unified.py → bash_tool.py} +18 -34
- hanzo_mcp/tools/shell/command_executor.py +2 -2
- hanzo_mcp/tools/shell/{npx_unified.py → npx_tool.py} +16 -33
- hanzo_mcp/tools/shell/open.py +2 -2
- hanzo_mcp/tools/shell/{process_unified.py → process_tool.py} +1 -1
- hanzo_mcp/tools/shell/run_command_windows.py +1 -1
- hanzo_mcp/tools/shell/streaming_command.py +594 -0
- hanzo_mcp/tools/shell/uvx.py +47 -2
- hanzo_mcp/tools/shell/uvx_background.py +47 -2
- hanzo_mcp/tools/shell/{uvx_unified.py → uvx_tool.py} +16 -33
- hanzo_mcp/tools/todo/__init__.py +14 -19
- hanzo_mcp/tools/todo/todo.py +22 -1
- hanzo_mcp/tools/vector/__init__.py +1 -1
- hanzo_mcp/tools/vector/infinity_store.py +2 -2
- hanzo_mcp/tools/vector/project_manager.py +1 -1
- hanzo_mcp/types.py +23 -0
- hanzo_mcp-0.7.0.dist-info/METADATA +516 -0
- hanzo_mcp-0.7.0.dist-info/RECORD +180 -0
- {hanzo_mcp-0.6.12.dist-info → hanzo_mcp-0.7.0.dist-info}/entry_points.txt +1 -0
- hanzo_mcp/tools/common/palette.py +0 -344
- hanzo_mcp/tools/common/palette_loader.py +0 -108
- hanzo_mcp/tools/config/palette_tool.py +0 -179
- hanzo_mcp/tools/llm/llm_unified.py +0 -851
- hanzo_mcp-0.6.12.dist-info/METADATA +0 -339
- hanzo_mcp-0.6.12.dist-info/RECORD +0 -135
- hanzo_mcp-0.6.12.dist-info/licenses/LICENSE +0 -21
- {hanzo_mcp-0.6.12.dist-info → hanzo_mcp-0.7.0.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.6.12.dist-info → hanzo_mcp-0.7.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""Plugin loader for custom user tools."""
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import inspect
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Dict, List, Optional, Type, Any
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
|
|
12
|
+
from .base import BaseTool
|
|
13
|
+
from .context import ToolContext
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class ToolPlugin:
|
|
18
|
+
"""Represents a loaded tool plugin."""
|
|
19
|
+
name: str
|
|
20
|
+
tool_class: Type[BaseTool]
|
|
21
|
+
source_path: Path
|
|
22
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PluginLoader:
|
|
26
|
+
"""Loads custom tool plugins from user directories."""
|
|
27
|
+
|
|
28
|
+
def __init__(self):
|
|
29
|
+
self.plugins: Dict[str, ToolPlugin] = {}
|
|
30
|
+
self.plugin_dirs: List[Path] = []
|
|
31
|
+
self._setup_plugin_directories()
|
|
32
|
+
|
|
33
|
+
def _setup_plugin_directories(self):
|
|
34
|
+
"""Set up standard plugin directories."""
|
|
35
|
+
# User's home directory plugins
|
|
36
|
+
home_plugins = Path.home() / ".hanzo" / "plugins"
|
|
37
|
+
home_plugins.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
self.plugin_dirs.append(home_plugins)
|
|
39
|
+
|
|
40
|
+
# Project-local plugins
|
|
41
|
+
project_plugins = Path.cwd() / ".hanzo" / "plugins"
|
|
42
|
+
if project_plugins.exists():
|
|
43
|
+
self.plugin_dirs.append(project_plugins)
|
|
44
|
+
|
|
45
|
+
# Environment variable for additional paths
|
|
46
|
+
if custom_paths := os.environ.get("HANZO_PLUGIN_PATH"):
|
|
47
|
+
for path in custom_paths.split(":"):
|
|
48
|
+
plugin_dir = Path(path)
|
|
49
|
+
if plugin_dir.exists():
|
|
50
|
+
self.plugin_dirs.append(plugin_dir)
|
|
51
|
+
|
|
52
|
+
def load_plugins(self) -> Dict[str, ToolPlugin]:
|
|
53
|
+
"""Load all plugins from configured directories."""
|
|
54
|
+
for plugin_dir in self.plugin_dirs:
|
|
55
|
+
if not plugin_dir.exists():
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
# Look for Python files
|
|
59
|
+
for py_file in plugin_dir.glob("*.py"):
|
|
60
|
+
if py_file.name.startswith("_"):
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
self._load_plugin_file(py_file)
|
|
65
|
+
except Exception as e:
|
|
66
|
+
print(f"Failed to load plugin {py_file}: {e}")
|
|
67
|
+
|
|
68
|
+
# Look for plugin packages
|
|
69
|
+
for package_dir in plugin_dir.iterdir():
|
|
70
|
+
if package_dir.is_dir() and (package_dir / "__init__.py").exists():
|
|
71
|
+
try:
|
|
72
|
+
self._load_plugin_package(package_dir)
|
|
73
|
+
except Exception as e:
|
|
74
|
+
print(f"Failed to load plugin package {package_dir}: {e}")
|
|
75
|
+
|
|
76
|
+
return self.plugins
|
|
77
|
+
|
|
78
|
+
def _load_plugin_file(self, file_path: Path):
|
|
79
|
+
"""Load a single plugin file."""
|
|
80
|
+
# Load the module
|
|
81
|
+
spec = importlib.util.spec_from_file_location(file_path.stem, file_path)
|
|
82
|
+
if not spec or not spec.loader:
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
module = importlib.util.module_from_spec(spec)
|
|
86
|
+
sys.modules[file_path.stem] = module
|
|
87
|
+
spec.loader.exec_module(module)
|
|
88
|
+
|
|
89
|
+
# Find tool classes
|
|
90
|
+
for name, obj in inspect.getmembers(module):
|
|
91
|
+
if (inspect.isclass(obj) and
|
|
92
|
+
issubclass(obj, BaseTool) and
|
|
93
|
+
obj != BaseTool and
|
|
94
|
+
hasattr(obj, 'name')):
|
|
95
|
+
|
|
96
|
+
# Load metadata if available
|
|
97
|
+
metadata = None
|
|
98
|
+
metadata_file = file_path.with_suffix('.json')
|
|
99
|
+
if metadata_file.exists():
|
|
100
|
+
with open(metadata_file) as f:
|
|
101
|
+
metadata = json.load(f)
|
|
102
|
+
|
|
103
|
+
plugin = ToolPlugin(
|
|
104
|
+
name=obj.name,
|
|
105
|
+
tool_class=obj,
|
|
106
|
+
source_path=file_path,
|
|
107
|
+
metadata=metadata
|
|
108
|
+
)
|
|
109
|
+
self.plugins[obj.name] = plugin
|
|
110
|
+
|
|
111
|
+
def _load_plugin_package(self, package_dir: Path):
|
|
112
|
+
"""Load a plugin package."""
|
|
113
|
+
# Add parent to path temporarily
|
|
114
|
+
parent = str(package_dir.parent)
|
|
115
|
+
if parent not in sys.path:
|
|
116
|
+
sys.path.insert(0, parent)
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
# Import the package
|
|
120
|
+
module = importlib.import_module(package_dir.name)
|
|
121
|
+
|
|
122
|
+
# Look for tools
|
|
123
|
+
if hasattr(module, 'TOOLS'):
|
|
124
|
+
# Package exports TOOLS list
|
|
125
|
+
for tool_class in module.TOOLS:
|
|
126
|
+
if issubclass(tool_class, BaseTool):
|
|
127
|
+
plugin = ToolPlugin(
|
|
128
|
+
name=tool_class.name,
|
|
129
|
+
tool_class=tool_class,
|
|
130
|
+
source_path=package_dir
|
|
131
|
+
)
|
|
132
|
+
self.plugins[tool_class.name] = plugin
|
|
133
|
+
else:
|
|
134
|
+
# Search for tool classes
|
|
135
|
+
for name, obj in inspect.getmembers(module):
|
|
136
|
+
if (inspect.isclass(obj) and
|
|
137
|
+
issubclass(obj, BaseTool) and
|
|
138
|
+
obj != BaseTool and
|
|
139
|
+
hasattr(obj, 'name')):
|
|
140
|
+
|
|
141
|
+
plugin = ToolPlugin(
|
|
142
|
+
name=obj.name,
|
|
143
|
+
tool_class=obj,
|
|
144
|
+
source_path=package_dir
|
|
145
|
+
)
|
|
146
|
+
self.plugins[obj.name] = plugin
|
|
147
|
+
finally:
|
|
148
|
+
# Remove from path
|
|
149
|
+
if parent in sys.path:
|
|
150
|
+
sys.path.remove(parent)
|
|
151
|
+
|
|
152
|
+
def get_tool_class(self, name: str) -> Optional[Type[BaseTool]]:
|
|
153
|
+
"""Get a tool class by name."""
|
|
154
|
+
plugin = self.plugins.get(name)
|
|
155
|
+
return plugin.tool_class if plugin else None
|
|
156
|
+
|
|
157
|
+
def list_plugins(self) -> List[str]:
|
|
158
|
+
"""List all loaded plugin names."""
|
|
159
|
+
return list(self.plugins.keys())
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# Global plugin loader instance
|
|
163
|
+
_plugin_loader = PluginLoader()
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def load_user_plugins() -> Dict[str, ToolPlugin]:
|
|
167
|
+
"""Load all user plugins."""
|
|
168
|
+
return _plugin_loader.load_plugins()
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def get_plugin_tool(name: str) -> Optional[Type[BaseTool]]:
|
|
172
|
+
"""Get a plugin tool class by name."""
|
|
173
|
+
return _plugin_loader.get_tool_class(name)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def list_plugin_tools() -> List[str]:
|
|
177
|
+
"""List all available plugin tools."""
|
|
178
|
+
return _plugin_loader.list_plugins()
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def create_plugin_template(output_dir: Path, tool_name: str):
|
|
182
|
+
"""Create a template for a new plugin tool."""
|
|
183
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
184
|
+
|
|
185
|
+
# Create tool file
|
|
186
|
+
tool_file = output_dir / f"{tool_name}_tool.py"
|
|
187
|
+
tool_content = f'''"""Custom {tool_name} tool plugin."""
|
|
188
|
+
|
|
189
|
+
from hanzo_mcp.tools.common.base import BaseTool
|
|
190
|
+
from typing import Dict, Any
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class {tool_name.title()}Tool(BaseTool):
|
|
194
|
+
"""Custom {tool_name} tool implementation."""
|
|
195
|
+
|
|
196
|
+
name = "{tool_name}"
|
|
197
|
+
description = "Custom {tool_name} tool"
|
|
198
|
+
|
|
199
|
+
async def run(self, params: Dict[str, Any], ctx) -> Dict[str, Any]:
|
|
200
|
+
"""Execute the {tool_name} tool."""
|
|
201
|
+
# Get parameters
|
|
202
|
+
action = params.get("action", "default")
|
|
203
|
+
|
|
204
|
+
# Implement your tool logic here
|
|
205
|
+
if action == "default":
|
|
206
|
+
return {{
|
|
207
|
+
"status": "success",
|
|
208
|
+
"message": f"Running {tool_name} tool",
|
|
209
|
+
"data": {{
|
|
210
|
+
"params": params
|
|
211
|
+
}}
|
|
212
|
+
}}
|
|
213
|
+
|
|
214
|
+
# Add more actions as needed
|
|
215
|
+
elif action == "custom_action":
|
|
216
|
+
# Your custom logic here
|
|
217
|
+
pass
|
|
218
|
+
|
|
219
|
+
return {{
|
|
220
|
+
"status": "error",
|
|
221
|
+
"message": f"Unknown action: {{action}}"
|
|
222
|
+
}}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# Optional: Export tools explicitly
|
|
226
|
+
TOOLS = [{tool_name.title()}Tool]
|
|
227
|
+
'''
|
|
228
|
+
|
|
229
|
+
with open(tool_file, 'w') as f:
|
|
230
|
+
f.write(tool_content)
|
|
231
|
+
|
|
232
|
+
# Create metadata file
|
|
233
|
+
metadata_file = output_dir / f"{tool_name}_tool.json"
|
|
234
|
+
metadata_content = {
|
|
235
|
+
"name": tool_name,
|
|
236
|
+
"version": "1.0.0",
|
|
237
|
+
"author": "Your Name",
|
|
238
|
+
"description": f"Custom {tool_name} tool",
|
|
239
|
+
"modes": ["custom"], # Modes this tool should be added to
|
|
240
|
+
"dependencies": [],
|
|
241
|
+
"config": {
|
|
242
|
+
# Tool-specific configuration
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
with open(metadata_file, 'w') as f:
|
|
247
|
+
json.dump(metadata_content, f, indent=2)
|
|
248
|
+
|
|
249
|
+
# Create README
|
|
250
|
+
readme_file = output_dir / "README.md"
|
|
251
|
+
readme_content = f"""# {tool_name.title()} Tool Plugin
|
|
252
|
+
|
|
253
|
+
Custom tool plugin for Hanzo MCP.
|
|
254
|
+
|
|
255
|
+
## Installation
|
|
256
|
+
|
|
257
|
+
1. Place this directory in one of:
|
|
258
|
+
- `~/.hanzo/plugins/`
|
|
259
|
+
- `./.hanzo/plugins/` (project-specific)
|
|
260
|
+
- Any path in `HANZO_PLUGIN_PATH` environment variable
|
|
261
|
+
|
|
262
|
+
2. The tool will be automatically loaded when Hanzo MCP starts.
|
|
263
|
+
|
|
264
|
+
## Usage
|
|
265
|
+
|
|
266
|
+
The tool will be available as `{tool_name}` in any mode that includes it.
|
|
267
|
+
|
|
268
|
+
## Configuration
|
|
269
|
+
|
|
270
|
+
Edit the `{tool_name}_tool.json` file to:
|
|
271
|
+
- Add the tool to specific modes
|
|
272
|
+
- Configure tool-specific settings
|
|
273
|
+
- Specify dependencies
|
|
274
|
+
|
|
275
|
+
## Development
|
|
276
|
+
|
|
277
|
+
Modify `{tool_name}_tool.py` to implement your custom functionality.
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
with open(readme_file, 'w') as f:
|
|
281
|
+
f.write(readme_content)
|
|
282
|
+
|
|
283
|
+
print(f"Created plugin template in {output_dir}")
|
|
284
|
+
print(f"Files created:")
|
|
285
|
+
print(f" - {tool_file}")
|
|
286
|
+
print(f" - {metadata_file}")
|
|
287
|
+
print(f" - {readme_file}")
|
hanzo_mcp/tools/common/stats.py
CHANGED
|
@@ -43,7 +43,7 @@ class StatsTool(BaseTool):
|
|
|
43
43
|
@override
|
|
44
44
|
def description(self) -> str:
|
|
45
45
|
"""Get the tool description."""
|
|
46
|
-
return """Show comprehensive system and Hanzo
|
|
46
|
+
return """Show comprehensive system and Hanzo AI statistics.
|
|
47
47
|
|
|
48
48
|
Displays:
|
|
49
49
|
- System resources (CPU, memory, disk)
|
|
@@ -79,7 +79,7 @@ Example:
|
|
|
79
79
|
warnings = []
|
|
80
80
|
|
|
81
81
|
# Header
|
|
82
|
-
output.append("=== Hanzo
|
|
82
|
+
output.append("=== Hanzo AI System Statistics ===")
|
|
83
83
|
output.append(f"Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
|
84
84
|
output.append("")
|
|
85
85
|
|
|
@@ -205,8 +205,8 @@ Example:
|
|
|
205
205
|
|
|
206
206
|
output.append("")
|
|
207
207
|
|
|
208
|
-
# Hanzo
|
|
209
|
-
output.append("=== Hanzo
|
|
208
|
+
# Hanzo AI Specifics
|
|
209
|
+
output.append("=== Hanzo AI ===")
|
|
210
210
|
|
|
211
211
|
# Log directory size
|
|
212
212
|
log_dir = Path.home() / ".hanzo" / "logs"
|
|
@@ -57,13 +57,14 @@ class ToolListTool(BaseTool):
|
|
|
57
57
|
("tree", "Directory tree visualization (Unix-style)"),
|
|
58
58
|
("find", "Find text in files (rg/ag/ack/grep)"),
|
|
59
59
|
("symbols", "Code symbols search with tree-sitter"),
|
|
60
|
-
("search", "
|
|
60
|
+
("search", "Search (parallel grep/symbols/vector/git)"),
|
|
61
61
|
("git_search", "Search git history"),
|
|
62
62
|
("glob", "Find files by name pattern"),
|
|
63
63
|
("content_replace", "Replace content across files"),
|
|
64
64
|
],
|
|
65
65
|
"shell": [
|
|
66
66
|
("run_command", "Execute shell commands (--background option)"),
|
|
67
|
+
("streaming_command", "Run commands with disk-based output streaming"),
|
|
67
68
|
("processes", "List background processes"),
|
|
68
69
|
("pkill", "Kill background processes"),
|
|
69
70
|
("logs", "View process logs"),
|
|
@@ -78,6 +79,8 @@ class ToolListTool(BaseTool):
|
|
|
78
79
|
"ai": [
|
|
79
80
|
("llm", "LLM interface (query/consensus/list/models/enable/disable)"),
|
|
80
81
|
("agent", "AI agents (run/start/call/stop/list with A2A support)"),
|
|
82
|
+
("swarm", "Parallel agent execution across multiple files"),
|
|
83
|
+
("hierarchical_swarm", "Hierarchical agent teams with Claude Code integration"),
|
|
81
84
|
("mcp", "MCP servers (list/add/remove/enable/disable/restart)"),
|
|
82
85
|
],
|
|
83
86
|
"config": [
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Response truncation utilities for MCP tools.
|
|
2
|
+
|
|
3
|
+
This module provides utilities to ensure MCP tool responses don't exceed token limits.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import tiktoken
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def estimate_tokens(text: str, model: str = "gpt-4") -> int:
|
|
10
|
+
"""Estimate the number of tokens in a text string.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
text: The text to estimate tokens for
|
|
14
|
+
model: The model to use for token estimation (default: gpt-4)
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
Estimated number of tokens
|
|
18
|
+
"""
|
|
19
|
+
try:
|
|
20
|
+
# Try to get the encoding for the specific model
|
|
21
|
+
encoding = tiktoken.encoding_for_model(model)
|
|
22
|
+
except KeyError:
|
|
23
|
+
# Fall back to cl100k_base which is used by newer models
|
|
24
|
+
encoding = tiktoken.get_encoding("cl100k_base")
|
|
25
|
+
|
|
26
|
+
return len(encoding.encode(text))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def truncate_response(
|
|
30
|
+
response: str,
|
|
31
|
+
max_tokens: int = 20000,
|
|
32
|
+
truncation_message: str = "\n\n[Response truncated due to length. Please use pagination, filtering, or limit parameters to see more.]"
|
|
33
|
+
) -> str:
|
|
34
|
+
"""Truncate a response to fit within token limits.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
response: The response text to truncate
|
|
38
|
+
max_tokens: Maximum number of tokens allowed (default: 20000)
|
|
39
|
+
truncation_message: Message to append when truncating
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Truncated response if needed, original response otherwise
|
|
43
|
+
"""
|
|
44
|
+
# Quick check - if response is short, no need to count tokens
|
|
45
|
+
if len(response) < max_tokens * 2: # Rough estimate: 1 token ≈ 2-4 chars
|
|
46
|
+
return response
|
|
47
|
+
|
|
48
|
+
# Estimate tokens
|
|
49
|
+
token_count = estimate_tokens(response)
|
|
50
|
+
|
|
51
|
+
# If within limit, return as-is
|
|
52
|
+
if token_count <= max_tokens:
|
|
53
|
+
return response
|
|
54
|
+
|
|
55
|
+
# Need to truncate
|
|
56
|
+
# Binary search to find the right truncation point
|
|
57
|
+
left, right = 0, len(response)
|
|
58
|
+
truncation_msg_tokens = estimate_tokens(truncation_message)
|
|
59
|
+
target_tokens = max_tokens - truncation_msg_tokens
|
|
60
|
+
|
|
61
|
+
while left < right - 1:
|
|
62
|
+
mid = (left + right) // 2
|
|
63
|
+
mid_tokens = estimate_tokens(response[:mid])
|
|
64
|
+
|
|
65
|
+
if mid_tokens <= target_tokens:
|
|
66
|
+
left = mid
|
|
67
|
+
else:
|
|
68
|
+
right = mid
|
|
69
|
+
|
|
70
|
+
# Find a good break point (newline or space)
|
|
71
|
+
truncate_at = left
|
|
72
|
+
for i in range(min(100, left), -1, -1):
|
|
73
|
+
if response[left - i] in '\n ':
|
|
74
|
+
truncate_at = left - i
|
|
75
|
+
break
|
|
76
|
+
|
|
77
|
+
return response[:truncate_at] + truncation_message
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def truncate_lines(
|
|
81
|
+
response: str,
|
|
82
|
+
max_lines: int = 1000,
|
|
83
|
+
truncation_message: str = "\n\n[Response truncated to {max_lines} lines. Please use pagination or filtering to see more.]"
|
|
84
|
+
) -> str:
|
|
85
|
+
"""Truncate a response by number of lines.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
response: The response text to truncate
|
|
89
|
+
max_lines: Maximum number of lines allowed (default: 1000)
|
|
90
|
+
truncation_message: Message template to append when truncating
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Truncated response if needed, original response otherwise
|
|
94
|
+
"""
|
|
95
|
+
lines = response.split('\n')
|
|
96
|
+
|
|
97
|
+
if len(lines) <= max_lines:
|
|
98
|
+
return response
|
|
99
|
+
|
|
100
|
+
truncated = '\n'.join(lines[:max_lines])
|
|
101
|
+
return truncated + truncation_message.format(max_lines=max_lines)
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
"""Configuration tools for Hanzo
|
|
1
|
+
"""Configuration tools for Hanzo AI."""
|
|
2
2
|
|
|
3
3
|
from hanzo_mcp.tools.config.config_tool import ConfigTool
|
|
4
4
|
from hanzo_mcp.tools.config.index_config import IndexConfig, IndexScope
|
|
5
|
+
from hanzo_mcp.tools.config.mode_tool import mode_tool
|
|
5
6
|
|
|
6
7
|
__all__ = [
|
|
7
8
|
"ConfigTool",
|
|
8
9
|
"IndexConfig",
|
|
9
10
|
"IndexScope",
|
|
11
|
+
"mode_tool",
|
|
10
12
|
]
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""Tool for managing development modes with programmer personalities."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional, override
|
|
4
|
+
|
|
5
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
6
|
+
|
|
7
|
+
from hanzo_mcp.tools.common.base import BaseTool
|
|
8
|
+
from hanzo_mcp.tools.common.mode import ModeRegistry, register_default_modes
|
|
9
|
+
from mcp.server import FastMCP
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ModeTool(BaseTool):
|
|
13
|
+
"""Tool for managing development modes."""
|
|
14
|
+
|
|
15
|
+
name = "mode"
|
|
16
|
+
|
|
17
|
+
def __init__(self):
|
|
18
|
+
"""Initialize the mode tool."""
|
|
19
|
+
super().__init__()
|
|
20
|
+
# Register default modes on initialization
|
|
21
|
+
register_default_modes()
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
@override
|
|
25
|
+
def description(self) -> str:
|
|
26
|
+
"""Get the tool description."""
|
|
27
|
+
return """Manage development modes (programmer personalities). Actions: list (default), activate, show, current.
|
|
28
|
+
|
|
29
|
+
Usage:
|
|
30
|
+
mode
|
|
31
|
+
mode --action list
|
|
32
|
+
mode --action activate guido
|
|
33
|
+
mode --action show linus
|
|
34
|
+
mode --action current"""
|
|
35
|
+
|
|
36
|
+
@override
|
|
37
|
+
async def run(
|
|
38
|
+
self,
|
|
39
|
+
ctx: MCPContext,
|
|
40
|
+
action: str = "list",
|
|
41
|
+
name: Optional[str] = None,
|
|
42
|
+
) -> str:
|
|
43
|
+
"""Manage development modes.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
ctx: MCP context
|
|
47
|
+
action: Action to perform (list, activate, show, current)
|
|
48
|
+
name: Mode name (for activate/show actions)
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Action result
|
|
52
|
+
"""
|
|
53
|
+
if action == "list":
|
|
54
|
+
modes = ModeRegistry.list()
|
|
55
|
+
if not modes:
|
|
56
|
+
return "No modes registered"
|
|
57
|
+
|
|
58
|
+
output = ["Available development modes (100 programmer personalities):"]
|
|
59
|
+
active = ModeRegistry.get_active()
|
|
60
|
+
|
|
61
|
+
# Group modes by category
|
|
62
|
+
categories = {
|
|
63
|
+
"Language Creators": ["guido", "matz", "brendan", "dennis", "bjarne", "james", "anders", "larry", "rasmus", "rich"],
|
|
64
|
+
"Systems & Infrastructure": ["linus", "rob", "ken", "bill", "richard", "brian", "donald", "graydon", "ryan", "mitchell"],
|
|
65
|
+
"Web & Frontend": ["tim", "douglas", "john", "evan", "jordan", "jeremy", "david", "taylor", "adrian", "matt"],
|
|
66
|
+
"Database & Data": ["michael_s", "michael_w", "salvatore", "dwight", "edgar", "jim_gray", "jeff_dean", "sanjay", "mike", "matei"],
|
|
67
|
+
"AI & Machine Learning": ["yann", "geoffrey", "yoshua", "andrew", "demis", "ilya", "andrej", "chris", "francois", "jeremy_howard"],
|
|
68
|
+
"Security & Cryptography": ["bruce", "phil", "whitfield", "ralph", "daniel_b", "moxie", "theo", "dan_kaminsky", "katie", "matt_blaze"],
|
|
69
|
+
"Gaming & Graphics": ["john_carmack", "sid", "shigeru", "gabe", "markus", "jonathan", "casey", "tim_sweeney", "hideo", "will"],
|
|
70
|
+
"Open Source Leaders": ["miguel", "nat", "patrick", "ian", "mark_shuttleworth", "lennart", "bram", "daniel_r", "judd", "fabrice"],
|
|
71
|
+
"Modern Innovators": ["vitalik", "satoshi", "chris_lattner", "joe", "jose", "sebastian", "palmer", "dylan", "guillermo", "tom"],
|
|
72
|
+
"Special Configurations": ["fullstack", "minimal", "data_scientist", "devops", "security", "academic", "startup", "enterprise", "creative", "hanzo"],
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for category, mode_names in categories.items():
|
|
76
|
+
output.append(f"\n{category}:")
|
|
77
|
+
for mode_name in mode_names:
|
|
78
|
+
mode = next((m for m in modes if m.name == mode_name), None)
|
|
79
|
+
if mode:
|
|
80
|
+
marker = " (active)" if active and active.name == mode.name else ""
|
|
81
|
+
output.append(f" {mode.name}{marker}: {mode.programmer} - {mode.description}")
|
|
82
|
+
|
|
83
|
+
output.append("\nUse 'mode --action activate <name>' to activate a mode")
|
|
84
|
+
|
|
85
|
+
return "\n".join(output)
|
|
86
|
+
|
|
87
|
+
elif action == "activate":
|
|
88
|
+
if not name:
|
|
89
|
+
return "Error: Mode name required for activate action"
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
ModeRegistry.set_active(name)
|
|
93
|
+
mode = ModeRegistry.get(name)
|
|
94
|
+
|
|
95
|
+
output = [f"Activated mode: {mode.name}"]
|
|
96
|
+
output.append(f"Programmer: {mode.programmer}")
|
|
97
|
+
output.append(f"Description: {mode.description}")
|
|
98
|
+
if mode.philosophy:
|
|
99
|
+
output.append(f"Philosophy: {mode.philosophy}")
|
|
100
|
+
output.append(f"\nEnabled tools ({len(mode.tools)}):")
|
|
101
|
+
|
|
102
|
+
# Group tools by category
|
|
103
|
+
core_tools = []
|
|
104
|
+
package_tools = []
|
|
105
|
+
ai_tools = []
|
|
106
|
+
search_tools = []
|
|
107
|
+
other_tools = []
|
|
108
|
+
|
|
109
|
+
for tool in sorted(mode.tools):
|
|
110
|
+
if tool in ["read", "write", "edit", "multi_edit", "bash", "tree", "grep"]:
|
|
111
|
+
core_tools.append(tool)
|
|
112
|
+
elif tool in ["npx", "uvx", "pip", "cargo", "gem"]:
|
|
113
|
+
package_tools.append(tool)
|
|
114
|
+
elif tool in ["agent", "consensus", "critic", "think"]:
|
|
115
|
+
ai_tools.append(tool)
|
|
116
|
+
elif tool in ["search", "symbols", "git_search"]:
|
|
117
|
+
search_tools.append(tool)
|
|
118
|
+
else:
|
|
119
|
+
other_tools.append(tool)
|
|
120
|
+
|
|
121
|
+
if core_tools:
|
|
122
|
+
output.append(f" Core: {', '.join(core_tools)}")
|
|
123
|
+
if package_tools:
|
|
124
|
+
output.append(f" Package managers: {', '.join(package_tools)}")
|
|
125
|
+
if ai_tools:
|
|
126
|
+
output.append(f" AI tools: {', '.join(ai_tools)}")
|
|
127
|
+
if search_tools:
|
|
128
|
+
output.append(f" Search: {', '.join(search_tools)}")
|
|
129
|
+
if other_tools:
|
|
130
|
+
output.append(f" Specialized: {', '.join(other_tools)}")
|
|
131
|
+
|
|
132
|
+
if mode.environment:
|
|
133
|
+
output.append("\nEnvironment variables:")
|
|
134
|
+
for key, value in mode.environment.items():
|
|
135
|
+
output.append(f" {key}={value}")
|
|
136
|
+
|
|
137
|
+
output.append("\nNote: Restart MCP session for changes to take full effect")
|
|
138
|
+
|
|
139
|
+
return "\n".join(output)
|
|
140
|
+
|
|
141
|
+
except ValueError as e:
|
|
142
|
+
return str(e)
|
|
143
|
+
|
|
144
|
+
elif action == "show":
|
|
145
|
+
if not name:
|
|
146
|
+
return "Error: Mode name required for show action"
|
|
147
|
+
|
|
148
|
+
mode = ModeRegistry.get(name)
|
|
149
|
+
if not mode:
|
|
150
|
+
return f"Mode '{name}' not found"
|
|
151
|
+
|
|
152
|
+
output = [f"Mode: {mode.name}"]
|
|
153
|
+
output.append(f"Programmer: {mode.programmer}")
|
|
154
|
+
output.append(f"Description: {mode.description}")
|
|
155
|
+
if mode.philosophy:
|
|
156
|
+
output.append(f"Philosophy: {mode.philosophy}")
|
|
157
|
+
output.append(f"\nTools ({len(mode.tools)}):")
|
|
158
|
+
|
|
159
|
+
for tool in sorted(mode.tools):
|
|
160
|
+
output.append(f" - {tool}")
|
|
161
|
+
|
|
162
|
+
if mode.environment:
|
|
163
|
+
output.append("\nEnvironment:")
|
|
164
|
+
for key, value in mode.environment.items():
|
|
165
|
+
output.append(f" {key}={value}")
|
|
166
|
+
|
|
167
|
+
return "\n".join(output)
|
|
168
|
+
|
|
169
|
+
elif action == "current":
|
|
170
|
+
active = ModeRegistry.get_active()
|
|
171
|
+
if not active:
|
|
172
|
+
return "No mode currently active\nUse 'mode --action activate <name>' to activate one"
|
|
173
|
+
|
|
174
|
+
output = [f"Current mode: {active.name}"]
|
|
175
|
+
output.append(f"Programmer: {active.programmer}")
|
|
176
|
+
output.append(f"Description: {active.description}")
|
|
177
|
+
if active.philosophy:
|
|
178
|
+
output.append(f"Philosophy: {active.philosophy}")
|
|
179
|
+
output.append(f"Enabled tools: {len(active.tools)}")
|
|
180
|
+
|
|
181
|
+
return "\n".join(output)
|
|
182
|
+
|
|
183
|
+
else:
|
|
184
|
+
return f"Unknown action: {action}. Use 'list', 'activate', 'show', or 'current'"
|
|
185
|
+
|
|
186
|
+
def register(self, server: FastMCP) -> None:
|
|
187
|
+
"""Register the tool with the MCP server."""
|
|
188
|
+
tool_self = self
|
|
189
|
+
|
|
190
|
+
@server.tool(name=self.name, description=self.description)
|
|
191
|
+
async def mode_handler(
|
|
192
|
+
ctx: MCPContext,
|
|
193
|
+
action: str = "list",
|
|
194
|
+
name: Optional[str] = None
|
|
195
|
+
) -> str:
|
|
196
|
+
"""Handle mode tool calls."""
|
|
197
|
+
return await tool_self.run(ctx, action=action, name=name)
|
|
198
|
+
|
|
199
|
+
async def call(self, ctx: MCPContext, **params) -> str:
|
|
200
|
+
"""Call the tool with arguments."""
|
|
201
|
+
return await self.run(
|
|
202
|
+
ctx,
|
|
203
|
+
action=params.get("action", "list"),
|
|
204
|
+
name=params.get("name")
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# Create tool instance
|
|
209
|
+
mode_tool = ModeTool()
|