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.

Files changed (117) hide show
  1. hanzo_mcp/__init__.py +2 -2
  2. hanzo_mcp/analytics/__init__.py +5 -0
  3. hanzo_mcp/analytics/posthog_analytics.py +364 -0
  4. hanzo_mcp/cli.py +5 -5
  5. hanzo_mcp/cli_enhanced.py +7 -7
  6. hanzo_mcp/cli_plugin.py +91 -0
  7. hanzo_mcp/config/__init__.py +1 -1
  8. hanzo_mcp/config/settings.py +70 -7
  9. hanzo_mcp/config/tool_config.py +20 -6
  10. hanzo_mcp/dev_server.py +3 -3
  11. hanzo_mcp/prompts/project_system.py +1 -1
  12. hanzo_mcp/server.py +40 -3
  13. hanzo_mcp/server_enhanced.py +69 -0
  14. hanzo_mcp/tools/__init__.py +140 -31
  15. hanzo_mcp/tools/agent/__init__.py +85 -4
  16. hanzo_mcp/tools/agent/agent_tool.py +104 -6
  17. hanzo_mcp/tools/agent/agent_tool_v2.py +459 -0
  18. hanzo_mcp/tools/agent/clarification_protocol.py +220 -0
  19. hanzo_mcp/tools/agent/clarification_tool.py +68 -0
  20. hanzo_mcp/tools/agent/claude_cli_tool.py +125 -0
  21. hanzo_mcp/tools/agent/claude_desktop_auth.py +508 -0
  22. hanzo_mcp/tools/agent/cli_agent_base.py +191 -0
  23. hanzo_mcp/tools/agent/code_auth.py +436 -0
  24. hanzo_mcp/tools/agent/code_auth_tool.py +194 -0
  25. hanzo_mcp/tools/agent/codex_cli_tool.py +123 -0
  26. hanzo_mcp/tools/agent/critic_tool.py +376 -0
  27. hanzo_mcp/tools/agent/gemini_cli_tool.py +128 -0
  28. hanzo_mcp/tools/agent/grok_cli_tool.py +128 -0
  29. hanzo_mcp/tools/agent/iching_tool.py +380 -0
  30. hanzo_mcp/tools/agent/network_tool.py +273 -0
  31. hanzo_mcp/tools/agent/prompt.py +62 -20
  32. hanzo_mcp/tools/agent/review_tool.py +433 -0
  33. hanzo_mcp/tools/agent/swarm_tool.py +535 -0
  34. hanzo_mcp/tools/agent/swarm_tool_v2.py +594 -0
  35. hanzo_mcp/tools/common/__init__.py +15 -1
  36. hanzo_mcp/tools/common/base.py +5 -4
  37. hanzo_mcp/tools/common/batch_tool.py +103 -11
  38. hanzo_mcp/tools/common/config_tool.py +2 -2
  39. hanzo_mcp/tools/common/context.py +2 -2
  40. hanzo_mcp/tools/common/context_fix.py +26 -0
  41. hanzo_mcp/tools/common/critic_tool.py +196 -0
  42. hanzo_mcp/tools/common/decorators.py +208 -0
  43. hanzo_mcp/tools/common/enhanced_base.py +106 -0
  44. hanzo_mcp/tools/common/fastmcp_pagination.py +369 -0
  45. hanzo_mcp/tools/common/forgiving_edit.py +243 -0
  46. hanzo_mcp/tools/common/mode.py +116 -0
  47. hanzo_mcp/tools/common/mode_loader.py +105 -0
  48. hanzo_mcp/tools/common/paginated_base.py +230 -0
  49. hanzo_mcp/tools/common/paginated_response.py +307 -0
  50. hanzo_mcp/tools/common/pagination.py +226 -0
  51. hanzo_mcp/tools/common/permissions.py +1 -1
  52. hanzo_mcp/tools/common/personality.py +936 -0
  53. hanzo_mcp/tools/common/plugin_loader.py +287 -0
  54. hanzo_mcp/tools/common/stats.py +4 -4
  55. hanzo_mcp/tools/common/tool_list.py +4 -1
  56. hanzo_mcp/tools/common/truncate.py +101 -0
  57. hanzo_mcp/tools/common/validation.py +1 -1
  58. hanzo_mcp/tools/config/__init__.py +3 -1
  59. hanzo_mcp/tools/config/config_tool.py +1 -1
  60. hanzo_mcp/tools/config/mode_tool.py +209 -0
  61. hanzo_mcp/tools/database/__init__.py +1 -1
  62. hanzo_mcp/tools/editor/__init__.py +1 -1
  63. hanzo_mcp/tools/filesystem/__init__.py +48 -14
  64. hanzo_mcp/tools/filesystem/ast_multi_edit.py +562 -0
  65. hanzo_mcp/tools/filesystem/batch_search.py +3 -3
  66. hanzo_mcp/tools/filesystem/diff.py +2 -2
  67. hanzo_mcp/tools/filesystem/directory_tree_paginated.py +338 -0
  68. hanzo_mcp/tools/filesystem/rules_tool.py +235 -0
  69. hanzo_mcp/tools/filesystem/{unified_search.py → search_tool.py} +12 -12
  70. hanzo_mcp/tools/filesystem/{symbols_unified.py → symbols_tool.py} +104 -5
  71. hanzo_mcp/tools/filesystem/watch.py +3 -2
  72. hanzo_mcp/tools/jupyter/__init__.py +2 -2
  73. hanzo_mcp/tools/jupyter/jupyter.py +1 -1
  74. hanzo_mcp/tools/llm/__init__.py +3 -3
  75. hanzo_mcp/tools/llm/llm_tool.py +648 -143
  76. hanzo_mcp/tools/lsp/__init__.py +5 -0
  77. hanzo_mcp/tools/lsp/lsp_tool.py +512 -0
  78. hanzo_mcp/tools/mcp/__init__.py +2 -2
  79. hanzo_mcp/tools/mcp/{mcp_unified.py → mcp_tool.py} +3 -3
  80. hanzo_mcp/tools/memory/__init__.py +76 -0
  81. hanzo_mcp/tools/memory/knowledge_tools.py +518 -0
  82. hanzo_mcp/tools/memory/memory_tools.py +456 -0
  83. hanzo_mcp/tools/search/__init__.py +6 -0
  84. hanzo_mcp/tools/search/find_tool.py +581 -0
  85. hanzo_mcp/tools/search/unified_search.py +953 -0
  86. hanzo_mcp/tools/shell/__init__.py +11 -6
  87. hanzo_mcp/tools/shell/auto_background.py +203 -0
  88. hanzo_mcp/tools/shell/base_process.py +57 -29
  89. hanzo_mcp/tools/shell/bash_session_executor.py +1 -1
  90. hanzo_mcp/tools/shell/{bash_unified.py → bash_tool.py} +18 -34
  91. hanzo_mcp/tools/shell/command_executor.py +2 -2
  92. hanzo_mcp/tools/shell/{npx_unified.py → npx_tool.py} +16 -33
  93. hanzo_mcp/tools/shell/open.py +2 -2
  94. hanzo_mcp/tools/shell/{process_unified.py → process_tool.py} +1 -1
  95. hanzo_mcp/tools/shell/run_command_windows.py +1 -1
  96. hanzo_mcp/tools/shell/streaming_command.py +594 -0
  97. hanzo_mcp/tools/shell/uvx.py +47 -2
  98. hanzo_mcp/tools/shell/uvx_background.py +47 -2
  99. hanzo_mcp/tools/shell/{uvx_unified.py → uvx_tool.py} +16 -33
  100. hanzo_mcp/tools/todo/__init__.py +14 -19
  101. hanzo_mcp/tools/todo/todo.py +22 -1
  102. hanzo_mcp/tools/vector/__init__.py +1 -1
  103. hanzo_mcp/tools/vector/infinity_store.py +2 -2
  104. hanzo_mcp/tools/vector/project_manager.py +1 -1
  105. hanzo_mcp/types.py +23 -0
  106. hanzo_mcp-0.7.0.dist-info/METADATA +516 -0
  107. hanzo_mcp-0.7.0.dist-info/RECORD +180 -0
  108. {hanzo_mcp-0.6.12.dist-info → hanzo_mcp-0.7.0.dist-info}/entry_points.txt +1 -0
  109. hanzo_mcp/tools/common/palette.py +0 -344
  110. hanzo_mcp/tools/common/palette_loader.py +0 -108
  111. hanzo_mcp/tools/config/palette_tool.py +0 -179
  112. hanzo_mcp/tools/llm/llm_unified.py +0 -851
  113. hanzo_mcp-0.6.12.dist-info/METADATA +0 -339
  114. hanzo_mcp-0.6.12.dist-info/RECORD +0 -135
  115. hanzo_mcp-0.6.12.dist-info/licenses/LICENSE +0 -21
  116. {hanzo_mcp-0.6.12.dist-info → hanzo_mcp-0.7.0.dist-info}/WHEEL +0 -0
  117. {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}")
@@ -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 MCP statistics.
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 MCP System Statistics ===")
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 MCP Specifics
209
- output.append("=== Hanzo MCP ===")
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", "Unified search (parallel grep/symbols/vector/git)"),
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,4 +1,4 @@
1
- """Parameter validation utilities for Hanzo MCP tools.
1
+ """Parameter validation utilities for Hanzo AI tools.
2
2
 
3
3
  This module provides utilities for validating parameters in tool functions.
4
4
  """
@@ -1,10 +1,12 @@
1
- """Configuration tools for Hanzo MCP."""
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
  ]
@@ -1,4 +1,4 @@
1
- """Configuration tool for Hanzo MCP.
1
+ """Configuration tool for Hanzo AI.
2
2
 
3
3
  Git-style config tool for managing settings.
4
4
  """
@@ -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()
@@ -1,4 +1,4 @@
1
- """Database tools for Hanzo MCP.
1
+ """Database tools for Hanzo AI.
2
2
 
3
3
  This package provides tools for working with embedded SQLite databases
4
4
  and graph databases in projects.