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,503 @@
1
+ """Unified MCP tool for managing MCP servers."""
2
+
3
+ from typing import Annotated, TypedDict, Unpack, final, override, Optional, Dict, Any, List
4
+ from pathlib import Path
5
+ import json
6
+ import subprocess
7
+ import os
8
+ import signal
9
+
10
+ from mcp.server.fastmcp import Context as MCPContext
11
+ from pydantic import Field
12
+
13
+ from hanzo_mcp.tools.common.base import BaseTool
14
+ from hanzo_mcp.tools.common.context import create_tool_context
15
+
16
+
17
+ # Parameter types
18
+ Action = Annotated[
19
+ str,
20
+ Field(
21
+ description="Action to perform: list, add, remove, enable, disable, restart, config",
22
+ default="list",
23
+ ),
24
+ ]
25
+
26
+ Name = Annotated[
27
+ Optional[str],
28
+ Field(
29
+ description="MCP server name",
30
+ default=None,
31
+ ),
32
+ ]
33
+
34
+ Command = Annotated[
35
+ Optional[str],
36
+ Field(
37
+ description="Command to run the MCP server",
38
+ default=None,
39
+ ),
40
+ ]
41
+
42
+ Args = Annotated[
43
+ Optional[List[str]],
44
+ Field(
45
+ description="Arguments for the MCP server command",
46
+ default=None,
47
+ ),
48
+ ]
49
+
50
+ Env = Annotated[
51
+ Optional[Dict[str, str]],
52
+ Field(
53
+ description="Environment variables for the MCP server",
54
+ default=None,
55
+ ),
56
+ ]
57
+
58
+ ConfigKey = Annotated[
59
+ Optional[str],
60
+ Field(
61
+ description="Configuration key to get/set",
62
+ default=None,
63
+ ),
64
+ ]
65
+
66
+ ConfigValue = Annotated[
67
+ Optional[Any],
68
+ Field(
69
+ description="Configuration value to set",
70
+ default=None,
71
+ ),
72
+ ]
73
+
74
+ AutoStart = Annotated[
75
+ bool,
76
+ Field(
77
+ description="Auto-start server when Hanzo MCP starts",
78
+ default=True,
79
+ ),
80
+ ]
81
+
82
+
83
+ class MCPParams(TypedDict, total=False):
84
+ """Parameters for MCP tool."""
85
+ action: str
86
+ name: Optional[str]
87
+ command: Optional[str]
88
+ args: Optional[List[str]]
89
+ env: Optional[Dict[str, str]]
90
+ config_key: Optional[str]
91
+ config_value: Optional[Any]
92
+ auto_start: bool
93
+
94
+
95
+ @final
96
+ class UnifiedMCPTool(BaseTool):
97
+ """Unified tool for managing MCP servers."""
98
+
99
+ # Config file
100
+ CONFIG_FILE = Path.home() / ".hanzo" / "mcp" / "servers.json"
101
+
102
+ # Running servers tracking
103
+ _running_servers: Dict[str, subprocess.Popen] = {}
104
+
105
+ def __init__(self):
106
+ """Initialize the MCP management tool."""
107
+ self.config = self._load_config()
108
+
109
+ # Auto-start servers if configured
110
+ self._auto_start_servers()
111
+
112
+ def _load_config(self) -> Dict[str, Any]:
113
+ """Load MCP server configuration."""
114
+ if self.CONFIG_FILE.exists():
115
+ try:
116
+ with open(self.CONFIG_FILE, 'r') as f:
117
+ return json.load(f)
118
+ except:
119
+ pass
120
+
121
+ # Default configuration with some examples
122
+ return {
123
+ "servers": {
124
+ # Example configurations (disabled by default)
125
+ "filesystem": {
126
+ "command": "npx",
127
+ "args": ["@modelcontextprotocol/server-filesystem", "/tmp"],
128
+ "env": {},
129
+ "enabled": False,
130
+ "auto_start": False,
131
+ "description": "MCP filesystem server for /tmp access"
132
+ },
133
+ "github": {
134
+ "command": "npx",
135
+ "args": ["@modelcontextprotocol/server-github"],
136
+ "env": {"GITHUB_TOKEN": "${GITHUB_TOKEN}"},
137
+ "enabled": False,
138
+ "auto_start": False,
139
+ "description": "GitHub API access via MCP"
140
+ },
141
+ "postgres": {
142
+ "command": "npx",
143
+ "args": ["@modelcontextprotocol/server-postgres", "postgresql://localhost/db"],
144
+ "env": {},
145
+ "enabled": False,
146
+ "auto_start": False,
147
+ "description": "PostgreSQL database access"
148
+ }
149
+ },
150
+ "global_env": {},
151
+ "log_dir": str(Path.home() / ".hanzo" / "mcp" / "logs")
152
+ }
153
+
154
+ def _save_config(self):
155
+ """Save configuration."""
156
+ self.CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
157
+ with open(self.CONFIG_FILE, 'w') as f:
158
+ json.dump(self.config, f, indent=2)
159
+
160
+ def _auto_start_servers(self):
161
+ """Auto-start servers configured for auto-start."""
162
+ for name, server_config in self.config.get("servers", {}).items():
163
+ if server_config.get("enabled", False) and server_config.get("auto_start", False):
164
+ self._start_server(name, server_config)
165
+
166
+ def _start_server(self, name: str, config: Dict[str, Any]) -> bool:
167
+ """Start an MCP server."""
168
+ if name in self._running_servers:
169
+ return False # Already running
170
+
171
+ try:
172
+ # Prepare environment
173
+ env = os.environ.copy()
174
+ env.update(self.config.get("global_env", {}))
175
+
176
+ # Process server-specific env vars
177
+ server_env = config.get("env", {})
178
+ for key, value in server_env.items():
179
+ # Replace ${VAR} with actual environment variable
180
+ if value.startswith("${") and value.endswith("}"):
181
+ var_name = value[2:-1]
182
+ if var_name in os.environ:
183
+ value = os.environ[var_name]
184
+ env[key] = value
185
+
186
+ # Prepare command
187
+ cmd = [config["command"]] + config.get("args", [])
188
+
189
+ # Create log directory
190
+ log_dir = Path(self.config.get("log_dir", str(Path.home() / ".hanzo" / "mcp" / "logs")))
191
+ log_dir.mkdir(parents=True, exist_ok=True)
192
+
193
+ # Start process
194
+ log_file = log_dir / f"{name}.log"
195
+ with open(log_file, 'a') as log:
196
+ process = subprocess.Popen(
197
+ cmd,
198
+ env=env,
199
+ stdout=log,
200
+ stderr=subprocess.STDOUT,
201
+ preexec_fn=os.setsid if os.name != 'nt' else None
202
+ )
203
+
204
+ self._running_servers[name] = process
205
+ return True
206
+
207
+ except Exception as e:
208
+ return False
209
+
210
+ def _stop_server(self, name: str) -> bool:
211
+ """Stop an MCP server."""
212
+ if name not in self._running_servers:
213
+ return False
214
+
215
+ process = self._running_servers[name]
216
+ try:
217
+ if os.name == 'nt':
218
+ process.terminate()
219
+ else:
220
+ os.killpg(os.getpgid(process.pid), signal.SIGTERM)
221
+
222
+ process.wait(timeout=5)
223
+ except:
224
+ # Force kill if needed
225
+ try:
226
+ if os.name == 'nt':
227
+ process.kill()
228
+ else:
229
+ os.killpg(os.getpgid(process.pid), signal.SIGKILL)
230
+ except:
231
+ pass
232
+
233
+ del self._running_servers[name]
234
+ return True
235
+
236
+ @property
237
+ @override
238
+ def name(self) -> str:
239
+ """Get the tool name."""
240
+ return "mcp"
241
+
242
+ @property
243
+ @override
244
+ def description(self) -> str:
245
+ """Get the tool description."""
246
+ servers = self.config.get("servers", {})
247
+ enabled = sum(1 for s in servers.values() if s.get("enabled", False))
248
+ running = len(self._running_servers)
249
+
250
+ return f"""Manage MCP servers. Actions: list (default), add, remove, enable, disable, restart, config.
251
+
252
+ Usage:
253
+ mcp
254
+ mcp --action add --name github --command npx --args '["@modelcontextprotocol/server-github"]'
255
+ mcp --action enable --name github
256
+
257
+ Status: {enabled} enabled, {running} running"""
258
+
259
+ @override
260
+ async def call(
261
+ self,
262
+ ctx: MCPContext,
263
+ **params: Unpack[MCPParams],
264
+ ) -> str:
265
+ """Execute MCP management action."""
266
+ # Create tool context only if we have a proper MCP context
267
+ tool_ctx = None
268
+ try:
269
+ if hasattr(ctx, 'client') and ctx.client and hasattr(ctx.client, 'server'):
270
+ tool_ctx = create_tool_context(ctx)
271
+ if tool_ctx:
272
+ await tool_ctx.set_tool_info(self.name)
273
+ except:
274
+ pass
275
+
276
+ # Extract action
277
+ action = params.get("action", "list")
278
+
279
+ # Route to appropriate handler
280
+ if action == "list":
281
+ return self._handle_list()
282
+ elif action == "add":
283
+ return self._handle_add(params)
284
+ elif action == "remove":
285
+ return self._handle_remove(params.get("name"))
286
+ elif action == "enable":
287
+ return self._handle_enable(params.get("name"))
288
+ elif action == "disable":
289
+ return self._handle_disable(params.get("name"))
290
+ elif action == "restart":
291
+ return self._handle_restart(params.get("name"))
292
+ elif action == "config":
293
+ return self._handle_config(params.get("config_key"), params.get("config_value"))
294
+ else:
295
+ return f"Error: Unknown action '{action}'. Valid actions: list, add, remove, enable, disable, restart, config"
296
+
297
+ def _handle_list(self) -> str:
298
+ """List all MCP servers."""
299
+ servers = self.config.get("servers", {})
300
+
301
+ if not servers:
302
+ return "No MCP servers configured. Use 'mcp --action add' to add one."
303
+
304
+ output = ["=== MCP Servers ==="]
305
+ output.append(f"Total: {len(servers)} | Enabled: {sum(1 for s in servers.values() if s.get('enabled', False))} | Running: {len(self._running_servers)}")
306
+ output.append("")
307
+
308
+ for name, config in sorted(servers.items()):
309
+ status_parts = []
310
+
311
+ # Check if enabled
312
+ if config.get("enabled", False):
313
+ status_parts.append("✅ Enabled")
314
+ else:
315
+ status_parts.append("❌ Disabled")
316
+
317
+ # Check if running
318
+ if name in self._running_servers:
319
+ process = self._running_servers[name]
320
+ if process.poll() is None:
321
+ status_parts.append("🟢 Running")
322
+ else:
323
+ status_parts.append("🔴 Stopped")
324
+ del self._running_servers[name]
325
+ else:
326
+ status_parts.append("⚫ Not running")
327
+
328
+ # Auto-start status
329
+ if config.get("auto_start", False):
330
+ status_parts.append("🚀 Auto-start")
331
+
332
+ status = " | ".join(status_parts)
333
+
334
+ output.append(f"{name}: {status}")
335
+ if config.get("description"):
336
+ output.append(f" Description: {config['description']}")
337
+ output.append(f" Command: {config['command']} {' '.join(config.get('args', []))}")
338
+
339
+ if config.get("env"):
340
+ env_str = ", ".join([f"{k}={v}" for k, v in config['env'].items()])
341
+ output.append(f" Environment: {env_str}")
342
+
343
+ output.append("\nUse 'mcp --action enable --name <server>' to enable a server")
344
+ output.append("Use 'mcp --action add' to add a new server")
345
+
346
+ return "\n".join(output)
347
+
348
+ def _handle_add(self, params: Dict[str, Any]) -> str:
349
+ """Add a new MCP server."""
350
+ name = params.get("name")
351
+ command = params.get("command")
352
+
353
+ if not name:
354
+ return "Error: name is required for add action"
355
+ if not command:
356
+ return "Error: command is required for add action"
357
+
358
+ servers = self.config.get("servers", {})
359
+ if name in servers:
360
+ return f"Error: Server '{name}' already exists. Use a different name or remove it first."
361
+
362
+ # Create server config
363
+ server_config = {
364
+ "command": command,
365
+ "args": params.get("args", []),
366
+ "env": params.get("env", {}),
367
+ "enabled": False,
368
+ "auto_start": params.get("auto_start", True),
369
+ "description": params.get("description", "")
370
+ }
371
+
372
+ servers[name] = server_config
373
+ self.config["servers"] = servers
374
+ self._save_config()
375
+
376
+ return f"Successfully added MCP server '{name}'. Use 'mcp --action enable --name {name}' to enable it."
377
+
378
+ def _handle_remove(self, name: Optional[str]) -> str:
379
+ """Remove an MCP server."""
380
+ if not name:
381
+ return "Error: name is required for remove action"
382
+
383
+ servers = self.config.get("servers", {})
384
+ if name not in servers:
385
+ return f"Error: Server '{name}' not found"
386
+
387
+ # Stop if running
388
+ if name in self._running_servers:
389
+ self._stop_server(name)
390
+
391
+ del servers[name]
392
+ self.config["servers"] = servers
393
+ self._save_config()
394
+
395
+ return f"Successfully removed MCP server '{name}'"
396
+
397
+ def _handle_enable(self, name: Optional[str]) -> str:
398
+ """Enable an MCP server."""
399
+ if not name:
400
+ return "Error: name is required for enable action"
401
+
402
+ servers = self.config.get("servers", {})
403
+ if name not in servers:
404
+ return f"Error: Server '{name}' not found"
405
+
406
+ servers[name]["enabled"] = True
407
+ self.config["servers"] = servers
408
+ self._save_config()
409
+
410
+ # Start if auto-start is enabled
411
+ if servers[name].get("auto_start", False):
412
+ if self._start_server(name, servers[name]):
413
+ return f"Successfully enabled and started MCP server '{name}'"
414
+ else:
415
+ return f"Enabled MCP server '{name}' but failed to start it. Check the configuration."
416
+
417
+ return f"Successfully enabled MCP server '{name}'"
418
+
419
+ def _handle_disable(self, name: Optional[str]) -> str:
420
+ """Disable an MCP server."""
421
+ if not name:
422
+ return "Error: name is required for disable action"
423
+
424
+ servers = self.config.get("servers", {})
425
+ if name not in servers:
426
+ return f"Error: Server '{name}' not found"
427
+
428
+ # Stop if running
429
+ if name in self._running_servers:
430
+ self._stop_server(name)
431
+
432
+ servers[name]["enabled"] = False
433
+ self.config["servers"] = servers
434
+ self._save_config()
435
+
436
+ return f"Successfully disabled MCP server '{name}'"
437
+
438
+ def _handle_restart(self, name: Optional[str]) -> str:
439
+ """Restart an MCP server."""
440
+ if not name:
441
+ return "Error: name is required for restart action"
442
+
443
+ servers = self.config.get("servers", {})
444
+ if name not in servers:
445
+ return f"Error: Server '{name}' not found"
446
+
447
+ if not servers[name].get("enabled", False):
448
+ return f"Error: Server '{name}' is not enabled"
449
+
450
+ # Stop if running
451
+ if name in self._running_servers:
452
+ self._stop_server(name)
453
+
454
+ # Start again
455
+ if self._start_server(name, servers[name]):
456
+ return f"Successfully restarted MCP server '{name}'"
457
+ else:
458
+ return f"Failed to restart MCP server '{name}'. Check the configuration."
459
+
460
+ def _handle_config(self, key: Optional[str], value: Optional[Any]) -> str:
461
+ """Get or set configuration values."""
462
+ if not key:
463
+ # Show all config
464
+ return json.dumps(self.config, indent=2)
465
+
466
+ # Parse nested keys (e.g., "servers.github.auto_start")
467
+ keys = key.split('.')
468
+
469
+ if value is None:
470
+ # Get value
471
+ current = self.config
472
+ for k in keys:
473
+ if isinstance(current, dict) and k in current:
474
+ current = current[k]
475
+ else:
476
+ return f"Configuration key '{key}' not found"
477
+
478
+ return json.dumps(current, indent=2) if isinstance(current, (dict, list)) else str(current)
479
+ else:
480
+ # Set value
481
+ # Navigate to parent
482
+ current = self.config
483
+ for k in keys[:-1]:
484
+ if k not in current:
485
+ current[k] = {}
486
+ current = current[k]
487
+
488
+ # Parse value if it looks like JSON
489
+ if isinstance(value, str) and value.startswith('{') or value.startswith('['):
490
+ try:
491
+ value = json.loads(value)
492
+ except:
493
+ pass
494
+
495
+ # Set the value
496
+ current[keys[-1]] = value
497
+ self._save_config()
498
+
499
+ return f"Successfully set {key} = {json.dumps(value) if isinstance(value, (dict, list)) else value}"
500
+
501
+ def register(self, mcp_server) -> None:
502
+ """Register this tool with the MCP server."""
503
+ pass
@@ -3,14 +3,17 @@
3
3
  This package provides tools for executing shell commands and scripts.
4
4
  """
5
5
 
6
- import shutil
7
-
8
- from fastmcp import FastMCP
6
+ from mcp.server import FastMCP
9
7
 
10
8
  from hanzo_mcp.tools.common.base import BaseTool, ToolRegistry
11
9
  from hanzo_mcp.tools.common.permissions import PermissionManager
12
- from hanzo_mcp.tools.shell.bash_session_executor import BashSessionExecutor
13
- from hanzo_mcp.tools.shell.command_executor import CommandExecutor
10
+
11
+ # Import unified tools
12
+ from hanzo_mcp.tools.shell.bash_unified import bash_tool
13
+ from hanzo_mcp.tools.shell.npx_unified import npx_tool
14
+ from hanzo_mcp.tools.shell.uvx_unified import uvx_tool
15
+ from hanzo_mcp.tools.shell.process_unified import process_tool
16
+ from hanzo_mcp.tools.shell.open import open_tool
14
17
 
15
18
  # Export all tool classes
16
19
  __all__ = [
@@ -30,23 +33,18 @@ def get_shell_tools(
30
33
  Returns:
31
34
  List of shell tool instances
32
35
  """
33
- # Detect tmux availability and choose appropriate implementation
34
- if shutil.which("tmux") is not None:
35
- # Use tmux-based implementation for interactive sessions
36
- from hanzo_mcp.tools.shell.run_command import RunCommandTool
37
-
38
- command_executor = BashSessionExecutor(permission_manager)
39
- return [
40
- RunCommandTool(permission_manager, command_executor),
41
- ]
42
- else:
43
- # Use Windows-compatible implementation
44
- from hanzo_mcp.tools.shell.run_command_windows import RunCommandTool
45
-
46
- command_executor = CommandExecutor(permission_manager)
47
- return [
48
- RunCommandTool(permission_manager, command_executor),
49
- ]
36
+ # Set permission manager for tools that need it
37
+ bash_tool.permission_manager = permission_manager
38
+ npx_tool.permission_manager = permission_manager
39
+ uvx_tool.permission_manager = permission_manager
40
+
41
+ return [
42
+ bash_tool,
43
+ npx_tool,
44
+ uvx_tool,
45
+ process_tool,
46
+ open_tool,
47
+ ]
50
48
 
51
49
 
52
50
  def register_shell_tools(
@@ -64,4 +62,4 @@ def register_shell_tools(
64
62
  """
65
63
  tools = get_shell_tools(permission_manager)
66
64
  ToolRegistry.register_tools(mcp_server, tools)
67
- return tools
65
+ return tools
@@ -8,7 +8,7 @@ from abc import ABC, abstractmethod
8
8
  from enum import Enum
9
9
  from typing import Any, Self, final
10
10
 
11
- from fastmcp import Context as MCPContext
11
+ from mcp.server.fastmcp import Context as MCPContext
12
12
 
13
13
  from hanzo_mcp.tools.common.base import BaseTool
14
14
  from hanzo_mcp.tools.common.permissions import PermissionManager