hanzo-mcp 0.5.1__py3-none-any.whl → 0.6.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of hanzo-mcp might be problematic. Click here for more details.

Files changed (118) hide show
  1. hanzo_mcp/__init__.py +1 -1
  2. hanzo_mcp/cli.py +32 -0
  3. hanzo_mcp/dev_server.py +246 -0
  4. hanzo_mcp/prompts/__init__.py +1 -1
  5. hanzo_mcp/prompts/project_system.py +43 -7
  6. hanzo_mcp/server.py +5 -1
  7. hanzo_mcp/tools/__init__.py +168 -6
  8. hanzo_mcp/tools/agent/__init__.py +1 -1
  9. hanzo_mcp/tools/agent/agent.py +401 -0
  10. hanzo_mcp/tools/agent/agent_tool.py +3 -4
  11. hanzo_mcp/tools/common/__init__.py +1 -1
  12. hanzo_mcp/tools/common/base.py +9 -4
  13. hanzo_mcp/tools/common/batch_tool.py +3 -5
  14. hanzo_mcp/tools/common/config_tool.py +1 -1
  15. hanzo_mcp/tools/common/context.py +1 -1
  16. hanzo_mcp/tools/common/palette.py +344 -0
  17. hanzo_mcp/tools/common/palette_loader.py +108 -0
  18. hanzo_mcp/tools/common/stats.py +261 -0
  19. hanzo_mcp/tools/common/thinking_tool.py +3 -5
  20. hanzo_mcp/tools/common/tool_disable.py +144 -0
  21. hanzo_mcp/tools/common/tool_enable.py +182 -0
  22. hanzo_mcp/tools/common/tool_list.py +260 -0
  23. hanzo_mcp/tools/config/__init__.py +10 -0
  24. hanzo_mcp/tools/config/config_tool.py +212 -0
  25. hanzo_mcp/tools/config/index_config.py +176 -0
  26. hanzo_mcp/tools/config/palette_tool.py +166 -0
  27. hanzo_mcp/tools/database/__init__.py +71 -0
  28. hanzo_mcp/tools/database/database_manager.py +246 -0
  29. hanzo_mcp/tools/database/graph.py +482 -0
  30. hanzo_mcp/tools/database/graph_add.py +257 -0
  31. hanzo_mcp/tools/database/graph_query.py +536 -0
  32. hanzo_mcp/tools/database/graph_remove.py +267 -0
  33. hanzo_mcp/tools/database/graph_search.py +348 -0
  34. hanzo_mcp/tools/database/graph_stats.py +345 -0
  35. hanzo_mcp/tools/database/sql.py +411 -0
  36. hanzo_mcp/tools/database/sql_query.py +229 -0
  37. hanzo_mcp/tools/database/sql_search.py +296 -0
  38. hanzo_mcp/tools/database/sql_stats.py +254 -0
  39. hanzo_mcp/tools/editor/__init__.py +11 -0
  40. hanzo_mcp/tools/editor/neovim_command.py +272 -0
  41. hanzo_mcp/tools/editor/neovim_edit.py +290 -0
  42. hanzo_mcp/tools/editor/neovim_session.py +356 -0
  43. hanzo_mcp/tools/filesystem/__init__.py +52 -13
  44. hanzo_mcp/tools/filesystem/base.py +1 -1
  45. hanzo_mcp/tools/filesystem/batch_search.py +812 -0
  46. hanzo_mcp/tools/filesystem/content_replace.py +3 -5
  47. hanzo_mcp/tools/filesystem/diff.py +193 -0
  48. hanzo_mcp/tools/filesystem/directory_tree.py +3 -5
  49. hanzo_mcp/tools/filesystem/edit.py +3 -5
  50. hanzo_mcp/tools/filesystem/find.py +443 -0
  51. hanzo_mcp/tools/filesystem/find_files.py +348 -0
  52. hanzo_mcp/tools/filesystem/git_search.py +505 -0
  53. hanzo_mcp/tools/filesystem/grep.py +2 -2
  54. hanzo_mcp/tools/filesystem/multi_edit.py +3 -5
  55. hanzo_mcp/tools/filesystem/read.py +17 -5
  56. hanzo_mcp/tools/filesystem/{grep_ast_tool.py → symbols.py} +17 -27
  57. hanzo_mcp/tools/filesystem/symbols_unified.py +376 -0
  58. hanzo_mcp/tools/filesystem/tree.py +268 -0
  59. hanzo_mcp/tools/filesystem/unified_search.py +465 -443
  60. hanzo_mcp/tools/filesystem/unix_aliases.py +99 -0
  61. hanzo_mcp/tools/filesystem/watch.py +174 -0
  62. hanzo_mcp/tools/filesystem/write.py +3 -5
  63. hanzo_mcp/tools/jupyter/__init__.py +9 -12
  64. hanzo_mcp/tools/jupyter/base.py +1 -1
  65. hanzo_mcp/tools/jupyter/jupyter.py +326 -0
  66. hanzo_mcp/tools/jupyter/notebook_edit.py +3 -4
  67. hanzo_mcp/tools/jupyter/notebook_read.py +3 -5
  68. hanzo_mcp/tools/llm/__init__.py +31 -0
  69. hanzo_mcp/tools/llm/consensus_tool.py +351 -0
  70. hanzo_mcp/tools/llm/llm_manage.py +413 -0
  71. hanzo_mcp/tools/llm/llm_tool.py +346 -0
  72. hanzo_mcp/tools/llm/llm_unified.py +851 -0
  73. hanzo_mcp/tools/llm/provider_tools.py +412 -0
  74. hanzo_mcp/tools/mcp/__init__.py +15 -0
  75. hanzo_mcp/tools/mcp/mcp_add.py +263 -0
  76. hanzo_mcp/tools/mcp/mcp_remove.py +127 -0
  77. hanzo_mcp/tools/mcp/mcp_stats.py +165 -0
  78. hanzo_mcp/tools/mcp/mcp_unified.py +503 -0
  79. hanzo_mcp/tools/shell/__init__.py +21 -23
  80. hanzo_mcp/tools/shell/base.py +1 -1
  81. hanzo_mcp/tools/shell/base_process.py +303 -0
  82. hanzo_mcp/tools/shell/bash_unified.py +134 -0
  83. hanzo_mcp/tools/shell/logs.py +265 -0
  84. hanzo_mcp/tools/shell/npx.py +194 -0
  85. hanzo_mcp/tools/shell/npx_background.py +254 -0
  86. hanzo_mcp/tools/shell/npx_unified.py +101 -0
  87. hanzo_mcp/tools/shell/open.py +107 -0
  88. hanzo_mcp/tools/shell/pkill.py +262 -0
  89. hanzo_mcp/tools/shell/process_unified.py +131 -0
  90. hanzo_mcp/tools/shell/processes.py +279 -0
  91. hanzo_mcp/tools/shell/run_background.py +326 -0
  92. hanzo_mcp/tools/shell/run_command.py +3 -4
  93. hanzo_mcp/tools/shell/run_command_windows.py +3 -4
  94. hanzo_mcp/tools/shell/uvx.py +187 -0
  95. hanzo_mcp/tools/shell/uvx_background.py +249 -0
  96. hanzo_mcp/tools/shell/uvx_unified.py +101 -0
  97. hanzo_mcp/tools/todo/__init__.py +1 -1
  98. hanzo_mcp/tools/todo/base.py +1 -1
  99. hanzo_mcp/tools/todo/todo.py +265 -0
  100. hanzo_mcp/tools/todo/todo_read.py +3 -5
  101. hanzo_mcp/tools/todo/todo_write.py +3 -5
  102. hanzo_mcp/tools/vector/__init__.py +6 -1
  103. hanzo_mcp/tools/vector/git_ingester.py +3 -0
  104. hanzo_mcp/tools/vector/index_tool.py +358 -0
  105. hanzo_mcp/tools/vector/infinity_store.py +98 -0
  106. hanzo_mcp/tools/vector/project_manager.py +27 -5
  107. hanzo_mcp/tools/vector/vector.py +311 -0
  108. hanzo_mcp/tools/vector/vector_index.py +1 -1
  109. hanzo_mcp/tools/vector/vector_search.py +12 -7
  110. hanzo_mcp-0.6.1.dist-info/METADATA +336 -0
  111. hanzo_mcp-0.6.1.dist-info/RECORD +134 -0
  112. hanzo_mcp-0.6.1.dist-info/entry_points.txt +3 -0
  113. hanzo_mcp-0.5.1.dist-info/METADATA +0 -276
  114. hanzo_mcp-0.5.1.dist-info/RECORD +0 -68
  115. hanzo_mcp-0.5.1.dist-info/entry_points.txt +0 -2
  116. {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.6.1.dist-info}/WHEEL +0 -0
  117. {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.6.1.dist-info}/licenses/LICENSE +0 -0
  118. {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.6.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,261 @@
1
+ """Comprehensive system and MCP statistics."""
2
+
3
+ import os
4
+ import psutil
5
+ import shutil
6
+ from typing import TypedDict, Unpack, final, override
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+
10
+ from mcp.server.fastmcp import Context as MCPContext
11
+
12
+ from hanzo_mcp.tools.common.base import BaseTool
13
+ from hanzo_mcp.tools.common.context import create_tool_context
14
+ from hanzo_mcp.tools.shell.run_background import RunBackgroundTool
15
+ from hanzo_mcp.tools.mcp.mcp_add import McpAddTool
16
+ from hanzo_mcp.tools.database.database_manager import DatabaseManager
17
+
18
+
19
+ class StatsParams(TypedDict, total=False):
20
+ """Parameters for stats tool."""
21
+ pass
22
+
23
+
24
+ @final
25
+ class StatsTool(BaseTool):
26
+ """Tool for showing comprehensive system and MCP statistics."""
27
+
28
+ def __init__(self, db_manager: DatabaseManager = None):
29
+ """Initialize the stats tool.
30
+
31
+ Args:
32
+ db_manager: Optional database manager for DB stats
33
+ """
34
+ self.db_manager = db_manager
35
+
36
+ @property
37
+ @override
38
+ def name(self) -> str:
39
+ """Get the tool name."""
40
+ return "stats"
41
+
42
+ @property
43
+ @override
44
+ def description(self) -> str:
45
+ """Get the tool description."""
46
+ return """Show comprehensive system and Hanzo MCP statistics.
47
+
48
+ Displays:
49
+ - System resources (CPU, memory, disk)
50
+ - Running processes
51
+ - Database usage
52
+ - MCP server status
53
+ - Tool usage statistics
54
+ - Warnings for high resource usage
55
+
56
+ Example:
57
+ - stats
58
+ """
59
+
60
+ @override
61
+ async def call(
62
+ self,
63
+ ctx: MCPContext,
64
+ **params: Unpack[StatsParams],
65
+ ) -> str:
66
+ """Get comprehensive statistics.
67
+
68
+ Args:
69
+ ctx: MCP context
70
+ **params: Tool parameters
71
+
72
+ Returns:
73
+ Comprehensive statistics
74
+ """
75
+ tool_ctx = create_tool_context(ctx)
76
+ await tool_ctx.set_tool_info(self.name)
77
+
78
+ output = []
79
+ warnings = []
80
+
81
+ # Header
82
+ output.append("=== Hanzo MCP System Statistics ===")
83
+ output.append(f"Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
84
+ output.append("")
85
+
86
+ # System Resources
87
+ output.append("=== System Resources ===")
88
+
89
+ # CPU
90
+ cpu_percent = psutil.cpu_percent(interval=1)
91
+ cpu_count = psutil.cpu_count()
92
+ output.append(f"CPU Usage: {cpu_percent}% ({cpu_count} cores)")
93
+ if cpu_percent > 90:
94
+ warnings.append(f"⚠️ HIGH CPU USAGE: {cpu_percent}%")
95
+
96
+ # Memory
97
+ memory = psutil.virtual_memory()
98
+ memory_used_gb = memory.used / (1024**3)
99
+ memory_total_gb = memory.total / (1024**3)
100
+ memory_percent = memory.percent
101
+ output.append(f"Memory: {memory_used_gb:.1f}/{memory_total_gb:.1f} GB ({memory_percent}%)")
102
+ if memory_percent > 90:
103
+ warnings.append(f"⚠️ HIGH MEMORY USAGE: {memory_percent}%")
104
+
105
+ # Disk
106
+ disk = psutil.disk_usage('/')
107
+ disk_used_gb = disk.used / (1024**3)
108
+ disk_total_gb = disk.total / (1024**3)
109
+ disk_percent = disk.percent
110
+ disk_free_gb = disk.free / (1024**3)
111
+ output.append(f"Disk: {disk_used_gb:.1f}/{disk_total_gb:.1f} GB ({disk_percent}%)")
112
+ output.append(f"Free Space: {disk_free_gb:.1f} GB")
113
+ if disk_percent > 90:
114
+ warnings.append(f"⚠️ LOW DISK SPACE: Only {disk_free_gb:.1f} GB free ({100-disk_percent:.1f}% remaining)")
115
+
116
+ output.append("")
117
+
118
+ # Background Processes
119
+ output.append("=== Background Processes ===")
120
+ processes = RunBackgroundTool.get_processes()
121
+ running_count = 0
122
+ total_memory_mb = 0
123
+
124
+ if processes:
125
+ for proc in processes.values():
126
+ if proc.is_running():
127
+ running_count += 1
128
+ try:
129
+ ps_proc = psutil.Process(proc.process.pid)
130
+ memory_mb = ps_proc.memory_info().rss / (1024**2)
131
+ total_memory_mb += memory_mb
132
+ except:
133
+ pass
134
+
135
+ output.append(f"Running Processes: {running_count}")
136
+ output.append(f"Total Memory Usage: {total_memory_mb:.1f} MB")
137
+
138
+ # List top processes by memory
139
+ if running_count > 0:
140
+ output.append("\nTop Processes:")
141
+ proc_list = []
142
+ for proc_id, proc in processes.items():
143
+ if proc.is_running():
144
+ try:
145
+ ps_proc = psutil.Process(proc.process.pid)
146
+ memory_mb = ps_proc.memory_info().rss / (1024**2)
147
+ cpu = ps_proc.cpu_percent(interval=0.1)
148
+ proc_list.append((proc.name, memory_mb, cpu, proc_id))
149
+ except:
150
+ proc_list.append((proc.name, 0, 0, proc_id))
151
+
152
+ proc_list.sort(key=lambda x: x[1], reverse=True)
153
+ for name, mem, cpu, pid in proc_list[:5]:
154
+ output.append(f" - {name} ({pid}): {mem:.1f} MB, {cpu:.1f}% CPU")
155
+ else:
156
+ output.append("No background processes running")
157
+
158
+ output.append("")
159
+
160
+ # Database Usage
161
+ if self.db_manager:
162
+ output.append("=== Database Usage ===")
163
+ db_dir = Path.home() / ".hanzo" / "db"
164
+ total_db_size = 0
165
+
166
+ if db_dir.exists():
167
+ for db_file in db_dir.rglob("*.db"):
168
+ size = db_file.stat().st_size
169
+ total_db_size += size
170
+
171
+ output.append(f"Total Database Size: {total_db_size / (1024**2):.1f} MB")
172
+ output.append(f"Active Projects: {len(self.db_manager.projects)}")
173
+
174
+ # List largest databases
175
+ db_sizes = []
176
+ for db_file in db_dir.rglob("*.db"):
177
+ size = db_file.stat().st_size / (1024**2)
178
+ if size > 0.1: # Only show DBs > 100KB
179
+ project = db_file.parent.parent.name
180
+ db_type = db_file.stem
181
+ db_sizes.append((project, db_type, size))
182
+
183
+ if db_sizes:
184
+ db_sizes.sort(key=lambda x: x[2], reverse=True)
185
+ output.append("\nLargest Databases:")
186
+ for project, db_type, size in db_sizes[:5]:
187
+ output.append(f" - {project}/{db_type}: {size:.1f} MB")
188
+ else:
189
+ output.append("No databases found")
190
+
191
+ output.append("")
192
+
193
+ # MCP Servers
194
+ output.append("=== MCP Servers ===")
195
+ mcp_servers = McpAddTool.get_servers()
196
+ if mcp_servers:
197
+ running_mcp = sum(1 for s in mcp_servers.values() if s.get("status") == "running")
198
+ total_mcp_tools = sum(len(s.get("tools", [])) for s in mcp_servers.values())
199
+
200
+ output.append(f"Total Servers: {len(mcp_servers)}")
201
+ output.append(f"Running: {running_mcp}")
202
+ output.append(f"Total Tools Available: {total_mcp_tools}")
203
+ else:
204
+ output.append("No MCP servers configured")
205
+
206
+ output.append("")
207
+
208
+ # Hanzo MCP Specifics
209
+ output.append("=== Hanzo MCP ===")
210
+
211
+ # Log directory size
212
+ log_dir = Path.home() / ".hanzo" / "logs"
213
+ if log_dir.exists():
214
+ log_size = sum(f.stat().st_size for f in log_dir.rglob("*") if f.is_file())
215
+ log_count = len(list(log_dir.rglob("*.log")))
216
+ output.append(f"Log Files: {log_count} ({log_size / (1024**2):.1f} MB)")
217
+
218
+ if log_size > 100 * 1024**2: # > 100MB
219
+ warnings.append(f"⚠️ Large log directory: {log_size / (1024**2):.1f} MB")
220
+
221
+ # Config directory
222
+ config_dir = Path.home() / ".hanzo" / "mcp"
223
+ if config_dir.exists():
224
+ config_count = len(list(config_dir.rglob("*.json")))
225
+ output.append(f"Config Files: {config_count}")
226
+
227
+ # Tool status (if available)
228
+ # TODO: Track tool usage statistics
229
+ output.append("\nTool Categories:")
230
+ output.append(" - File Operations: grep, find_files, read, write, edit")
231
+ output.append(" - Shell: bash, run_background, processes, pkill")
232
+ output.append(" - Database: sql_query, graph_query, vector_search")
233
+ output.append(" - Package Runners: uvx, npx, uvx_background, npx_background")
234
+ output.append(" - MCP Management: mcp_add, mcp_remove, mcp_stats")
235
+
236
+ # Warnings Section
237
+ if warnings:
238
+ output.append("\n=== ⚠️ WARNINGS ===")
239
+ for warning in warnings:
240
+ output.append(warning)
241
+ output.append("")
242
+
243
+ # Recommendations
244
+ output.append("=== Recommendations ===")
245
+ if disk_free_gb < 5:
246
+ output.append("- Free up disk space (< 5GB remaining)")
247
+ if memory_percent > 80:
248
+ output.append("- Close unused applications to free memory")
249
+ if running_count > 10:
250
+ output.append("- Consider stopping unused background processes")
251
+ if log_size > 50 * 1024**2:
252
+ output.append("- Clean up old log files in ~/.hanzo/logs")
253
+
254
+ if not any([disk_free_gb < 5, memory_percent > 80, running_count > 10, log_size > 50 * 1024**2]):
255
+ output.append("✅ System resources are healthy")
256
+
257
+ return "\n".join(output)
258
+
259
+ def register(self, mcp_server) -> None:
260
+ """Register this tool with the MCP server."""
261
+ pass
@@ -5,9 +5,8 @@ This module provides the ThinkingTool for Claude to engage in structured thinkin
5
5
 
6
6
  from typing import Annotated, TypedDict, Unpack, final, override
7
7
 
8
- from fastmcp import Context as MCPContext
9
- from fastmcp import FastMCP
10
- from fastmcp.server.dependencies import get_context
8
+ from mcp.server.fastmcp import Context as MCPContext
9
+ from mcp.server import FastMCP
11
10
  from pydantic import Field
12
11
 
13
12
  from hanzo_mcp.tools.common.base import BaseTool
@@ -146,8 +145,7 @@ Feature Implementation Planning
146
145
 
147
146
  @mcp_server.tool(name=self.name, description=self.description)
148
147
  async def think(
149
- ctx: MCPContext,
150
148
  thought: Thought,
149
+ ctx: MCPContext
151
150
  ) -> str:
152
- ctx = get_context()
153
151
  return await tool_self.call(ctx, thought=thought)
@@ -0,0 +1,144 @@
1
+ """Disable tools dynamically."""
2
+
3
+ from typing import Annotated, TypedDict, Unpack, final, override
4
+
5
+ from mcp.server.fastmcp import Context as MCPContext
6
+ from pydantic import Field
7
+
8
+ from hanzo_mcp.tools.common.base import BaseTool
9
+ from hanzo_mcp.tools.common.context import create_tool_context
10
+ from hanzo_mcp.tools.common.tool_enable import ToolEnableTool
11
+
12
+
13
+ ToolName = Annotated[
14
+ str,
15
+ Field(
16
+ description="Name of the tool to disable (e.g., 'grep', 'vector_search')",
17
+ min_length=1,
18
+ ),
19
+ ]
20
+
21
+ Persist = Annotated[
22
+ bool,
23
+ Field(
24
+ description="Persist the change to config file",
25
+ default=True,
26
+ ),
27
+ ]
28
+
29
+
30
+ class ToolDisableParams(TypedDict, total=False):
31
+ """Parameters for tool disable."""
32
+
33
+ tool: str
34
+ persist: bool
35
+
36
+
37
+ @final
38
+ class ToolDisableTool(BaseTool):
39
+ """Tool for disabling other tools dynamically."""
40
+
41
+ def __init__(self):
42
+ """Initialize the tool disable tool."""
43
+ # Ensure states are loaded
44
+ if not ToolEnableTool._initialized:
45
+ ToolEnableTool._load_states()
46
+ ToolEnableTool._initialized = True
47
+
48
+ @property
49
+ @override
50
+ def name(self) -> str:
51
+ """Get the tool name."""
52
+ return "tool_disable"
53
+
54
+ @property
55
+ @override
56
+ def description(self) -> str:
57
+ """Get the tool description."""
58
+ return """Disable tools to prevent their use.
59
+
60
+ This allows you to temporarily or permanently disable tools.
61
+ Useful for testing or when a tool is misbehaving.
62
+ Changes are persisted by default.
63
+
64
+ Critical tools (tool_enable, tool_disable, tool_list) cannot be disabled.
65
+
66
+ Examples:
67
+ - tool_disable --tool vector_search
68
+ - tool_disable --tool uvx_background
69
+ - tool_disable --tool grep --no-persist
70
+
71
+ Use 'tool_list' to see all available tools and their status.
72
+ Use 'tool_enable' to re-enable disabled tools.
73
+ """
74
+
75
+ @override
76
+ async def call(
77
+ self,
78
+ ctx: MCPContext,
79
+ **params: Unpack[ToolDisableParams],
80
+ ) -> str:
81
+ """Disable a tool.
82
+
83
+ Args:
84
+ ctx: MCP context
85
+ **params: Tool parameters
86
+
87
+ Returns:
88
+ Result of disabling the tool
89
+ """
90
+ tool_ctx = create_tool_context(ctx)
91
+ await tool_ctx.set_tool_info(self.name)
92
+
93
+ # Extract parameters
94
+ tool_name = params.get("tool")
95
+ if not tool_name:
96
+ return "Error: tool name is required"
97
+
98
+ persist = params.get("persist", True)
99
+
100
+ # Prevent disabling critical tools
101
+ critical_tools = {"tool_enable", "tool_disable", "tool_list", "stats"}
102
+ if tool_name in critical_tools:
103
+ return f"Error: Cannot disable critical tool '{tool_name}'. These tools are required for system management."
104
+
105
+ # Check current state
106
+ was_enabled = ToolEnableTool.is_tool_enabled(tool_name)
107
+
108
+ if not was_enabled:
109
+ return f"Tool '{tool_name}' is already disabled."
110
+
111
+ # Disable the tool
112
+ ToolEnableTool._tool_states[tool_name] = False
113
+
114
+ # Persist if requested
115
+ if persist:
116
+ ToolEnableTool._save_states()
117
+ await tool_ctx.info(f"Disabled tool '{tool_name}' (persisted)")
118
+ else:
119
+ await tool_ctx.info(f"Disabled tool '{tool_name}' (temporary)")
120
+
121
+ output = [
122
+ f"Successfully disabled tool '{tool_name}'",
123
+ "",
124
+ "The tool is now unavailable for use.",
125
+ f"Use 'tool_enable --tool {tool_name}' to re-enable it.",
126
+ ]
127
+
128
+ if not persist:
129
+ output.append("\nNote: This change is temporary and will be lost on restart.")
130
+
131
+ # Warn about commonly used tools
132
+ common_tools = {"grep", "read", "write", "bash", "edit"}
133
+ if tool_name in common_tools:
134
+ output.append(f"\n⚠️ Warning: '{tool_name}' is a commonly used tool. Disabling it may affect normal operations.")
135
+
136
+ # Count disabled tools
137
+ disabled_count = sum(1 for enabled in ToolEnableTool._tool_states.values() if not enabled)
138
+ output.append(f"\nTotal disabled tools: {disabled_count}")
139
+
140
+ return "\n".join(output)
141
+
142
+ def register(self, mcp_server) -> None:
143
+ """Register this tool with the MCP server."""
144
+ pass
@@ -0,0 +1,182 @@
1
+ """Enable tools dynamically."""
2
+
3
+ import json
4
+ from typing import Annotated, TypedDict, Unpack, final, override
5
+ from pathlib import Path
6
+
7
+ from mcp.server.fastmcp import Context as MCPContext
8
+ from pydantic import Field
9
+
10
+ from hanzo_mcp.tools.common.base import BaseTool
11
+ from hanzo_mcp.tools.common.context import create_tool_context
12
+
13
+
14
+ ToolName = Annotated[
15
+ str,
16
+ Field(
17
+ description="Name of the tool to enable (e.g., 'grep', 'vector_search')",
18
+ min_length=1,
19
+ ),
20
+ ]
21
+
22
+ Persist = Annotated[
23
+ bool,
24
+ Field(
25
+ description="Persist the change to config file",
26
+ default=True,
27
+ ),
28
+ ]
29
+
30
+
31
+ class ToolEnableParams(TypedDict, total=False):
32
+ """Parameters for tool enable."""
33
+
34
+ tool: str
35
+ persist: bool
36
+
37
+
38
+ @final
39
+ class ToolEnableTool(BaseTool):
40
+ """Tool for enabling other tools dynamically."""
41
+
42
+ # Class variable to track enabled/disabled tools
43
+ _tool_states = {}
44
+ _config_file = Path.home() / ".hanzo" / "mcp" / "tool_states.json"
45
+ _initialized = False
46
+
47
+ def __init__(self):
48
+ """Initialize the tool enable tool."""
49
+ if not ToolEnableTool._initialized:
50
+ self._load_states()
51
+ ToolEnableTool._initialized = True
52
+
53
+ @classmethod
54
+ def _load_states(cls):
55
+ """Load tool states from config file."""
56
+ if cls._config_file.exists():
57
+ try:
58
+ with open(cls._config_file, 'r') as f:
59
+ cls._tool_states = json.load(f)
60
+ except Exception:
61
+ cls._tool_states = {}
62
+ else:
63
+ # Default all tools to enabled
64
+ cls._tool_states = {}
65
+
66
+ @classmethod
67
+ def _save_states(cls):
68
+ """Save tool states to config file."""
69
+ cls._config_file.parent.mkdir(parents=True, exist_ok=True)
70
+ with open(cls._config_file, 'w') as f:
71
+ json.dump(cls._tool_states, f, indent=2)
72
+
73
+ @classmethod
74
+ def is_tool_enabled(cls, tool_name: str) -> bool:
75
+ """Check if a tool is enabled.
76
+
77
+ Args:
78
+ tool_name: Name of the tool
79
+
80
+ Returns:
81
+ True if enabled (default), False if explicitly disabled
82
+ """
83
+ # Load states if not initialized
84
+ if not cls._initialized:
85
+ cls._load_states()
86
+ cls._initialized = True
87
+
88
+ # Default to enabled if not in states
89
+ return cls._tool_states.get(tool_name, True)
90
+
91
+ @classmethod
92
+ def get_all_states(cls) -> dict:
93
+ """Get all tool states."""
94
+ if not cls._initialized:
95
+ cls._load_states()
96
+ cls._initialized = True
97
+ return cls._tool_states.copy()
98
+
99
+ @property
100
+ @override
101
+ def name(self) -> str:
102
+ """Get the tool name."""
103
+ return "tool_enable"
104
+
105
+ @property
106
+ @override
107
+ def description(self) -> str:
108
+ """Get the tool description."""
109
+ return """Enable tools that have been disabled.
110
+
111
+ This allows you to re-enable tools that were previously disabled.
112
+ Changes are persisted by default.
113
+
114
+ Examples:
115
+ - tool_enable --tool grep
116
+ - tool_enable --tool vector_search
117
+ - tool_enable --tool uvx_background --no-persist
118
+
119
+ Use 'tool_list' to see all available tools and their status.
120
+ """
121
+
122
+ @override
123
+ async def call(
124
+ self,
125
+ ctx: MCPContext,
126
+ **params: Unpack[ToolEnableParams],
127
+ ) -> str:
128
+ """Enable a tool.
129
+
130
+ Args:
131
+ ctx: MCP context
132
+ **params: Tool parameters
133
+
134
+ Returns:
135
+ Result of enabling the tool
136
+ """
137
+ tool_ctx = create_tool_context(ctx)
138
+ await tool_ctx.set_tool_info(self.name)
139
+
140
+ # Extract parameters
141
+ tool_name = params.get("tool")
142
+ if not tool_name:
143
+ return "Error: tool name is required"
144
+
145
+ persist = params.get("persist", True)
146
+
147
+ # Check current state
148
+ was_enabled = self.is_tool_enabled(tool_name)
149
+
150
+ if was_enabled:
151
+ return f"Tool '{tool_name}' is already enabled."
152
+
153
+ # Enable the tool
154
+ self._tool_states[tool_name] = True
155
+
156
+ # Persist if requested
157
+ if persist:
158
+ self._save_states()
159
+ await tool_ctx.info(f"Enabled tool '{tool_name}' (persisted)")
160
+ else:
161
+ await tool_ctx.info(f"Enabled tool '{tool_name}' (temporary)")
162
+
163
+ output = [
164
+ f"Successfully enabled tool '{tool_name}'",
165
+ "",
166
+ "The tool is now available for use.",
167
+ ]
168
+
169
+ if not persist:
170
+ output.append("Note: This change is temporary and will be lost on restart.")
171
+
172
+ # Count enabled/disabled tools
173
+ disabled_count = sum(1 for enabled in self._tool_states.values() if not enabled)
174
+ if disabled_count > 0:
175
+ output.append(f"\nCurrently disabled tools: {disabled_count}")
176
+ output.append("Use 'tool_list --disabled' to see them.")
177
+
178
+ return "\n".join(output)
179
+
180
+ def register(self, mcp_server) -> None:
181
+ """Register this tool with the MCP server."""
182
+ pass