hanzo-mcp 0.8.11__py3-none-any.whl → 0.9.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 +1 -3
- hanzo_mcp/analytics/posthog_analytics.py +3 -9
- hanzo_mcp/bridge.py +9 -25
- hanzo_mcp/cli.py +6 -15
- hanzo_mcp/cli_enhanced.py +5 -14
- hanzo_mcp/cli_plugin.py +3 -9
- hanzo_mcp/config/settings.py +6 -20
- hanzo_mcp/config/tool_config.py +1 -3
- hanzo_mcp/core/base_agent.py +88 -88
- hanzo_mcp/core/model_registry.py +238 -210
- hanzo_mcp/dev_server.py +5 -15
- hanzo_mcp/prompts/__init__.py +2 -6
- hanzo_mcp/prompts/project_todo_reminder.py +3 -9
- hanzo_mcp/prompts/tool_explorer.py +1 -3
- hanzo_mcp/prompts/utils.py +7 -21
- hanzo_mcp/server.py +2 -6
- hanzo_mcp/tools/__init__.py +26 -27
- hanzo_mcp/tools/agent/__init__.py +2 -1
- hanzo_mcp/tools/agent/agent.py +10 -30
- hanzo_mcp/tools/agent/agent_tool.py +22 -15
- hanzo_mcp/tools/agent/claude_desktop_auth.py +3 -9
- hanzo_mcp/tools/agent/cli_agent_base.py +7 -24
- hanzo_mcp/tools/agent/cli_tools.py +75 -74
- hanzo_mcp/tools/agent/code_auth.py +1 -3
- hanzo_mcp/tools/agent/code_auth_tool.py +2 -6
- hanzo_mcp/tools/agent/critic_tool.py +8 -24
- hanzo_mcp/tools/agent/iching_tool.py +12 -36
- hanzo_mcp/tools/agent/network_tool.py +7 -18
- hanzo_mcp/tools/agent/prompt.py +1 -5
- hanzo_mcp/tools/agent/review_tool.py +10 -25
- hanzo_mcp/tools/agent/swarm_alias.py +1 -3
- hanzo_mcp/tools/agent/unified_cli_tools.py +38 -38
- hanzo_mcp/tools/common/batch_tool.py +15 -45
- hanzo_mcp/tools/common/config_tool.py +9 -28
- hanzo_mcp/tools/common/context.py +1 -3
- hanzo_mcp/tools/common/critic_tool.py +1 -3
- hanzo_mcp/tools/common/decorators.py +2 -6
- hanzo_mcp/tools/common/enhanced_base.py +2 -6
- hanzo_mcp/tools/common/fastmcp_pagination.py +4 -12
- hanzo_mcp/tools/common/forgiving_edit.py +9 -28
- hanzo_mcp/tools/common/mode.py +1 -5
- hanzo_mcp/tools/common/paginated_base.py +3 -11
- hanzo_mcp/tools/common/paginated_response.py +10 -30
- hanzo_mcp/tools/common/pagination.py +3 -9
- hanzo_mcp/tools/common/path_utils.py +34 -0
- hanzo_mcp/tools/common/permissions.py +14 -13
- hanzo_mcp/tools/common/personality.py +983 -701
- hanzo_mcp/tools/common/plugin_loader.py +3 -15
- hanzo_mcp/tools/common/stats.py +6 -18
- hanzo_mcp/tools/common/thinking_tool.py +1 -3
- hanzo_mcp/tools/common/tool_disable.py +2 -6
- hanzo_mcp/tools/common/tool_list.py +2 -6
- hanzo_mcp/tools/common/validation.py +1 -3
- hanzo_mcp/tools/compiler/__init__.py +8 -0
- hanzo_mcp/tools/compiler/sandboxed_compiler.py +681 -0
- hanzo_mcp/tools/config/config_tool.py +7 -13
- hanzo_mcp/tools/config/index_config.py +1 -3
- hanzo_mcp/tools/config/mode_tool.py +5 -15
- hanzo_mcp/tools/database/database_manager.py +3 -9
- hanzo_mcp/tools/database/graph.py +1 -3
- hanzo_mcp/tools/database/graph_add.py +3 -9
- hanzo_mcp/tools/database/graph_query.py +11 -34
- hanzo_mcp/tools/database/graph_remove.py +3 -9
- hanzo_mcp/tools/database/graph_search.py +6 -20
- hanzo_mcp/tools/database/graph_stats.py +11 -33
- hanzo_mcp/tools/database/sql.py +4 -12
- hanzo_mcp/tools/database/sql_query.py +6 -10
- hanzo_mcp/tools/database/sql_search.py +2 -6
- hanzo_mcp/tools/database/sql_stats.py +5 -15
- hanzo_mcp/tools/editor/neovim_command.py +1 -3
- hanzo_mcp/tools/editor/neovim_session.py +7 -13
- hanzo_mcp/tools/environment/__init__.py +8 -0
- hanzo_mcp/tools/environment/environment_detector.py +594 -0
- hanzo_mcp/tools/filesystem/__init__.py +28 -26
- hanzo_mcp/tools/filesystem/ast_multi_edit.py +14 -43
- hanzo_mcp/tools/filesystem/ast_tool.py +3 -0
- hanzo_mcp/tools/filesystem/base.py +20 -12
- hanzo_mcp/tools/filesystem/content_replace.py +7 -12
- hanzo_mcp/tools/filesystem/diff.py +2 -10
- hanzo_mcp/tools/filesystem/directory_tree.py +285 -51
- hanzo_mcp/tools/filesystem/edit.py +10 -18
- hanzo_mcp/tools/filesystem/find.py +312 -179
- hanzo_mcp/tools/filesystem/git_search.py +12 -24
- hanzo_mcp/tools/filesystem/multi_edit.py +10 -18
- hanzo_mcp/tools/filesystem/read.py +14 -30
- hanzo_mcp/tools/filesystem/rules_tool.py +9 -17
- hanzo_mcp/tools/filesystem/search.py +1160 -0
- hanzo_mcp/tools/filesystem/watch.py +2 -4
- hanzo_mcp/tools/filesystem/write.py +7 -10
- hanzo_mcp/tools/framework/__init__.py +8 -0
- hanzo_mcp/tools/framework/framework_modes.py +714 -0
- hanzo_mcp/tools/jupyter/base.py +6 -20
- hanzo_mcp/tools/jupyter/jupyter.py +4 -12
- hanzo_mcp/tools/llm/consensus_tool.py +8 -24
- hanzo_mcp/tools/llm/llm_manage.py +2 -6
- hanzo_mcp/tools/llm/llm_tool.py +17 -58
- hanzo_mcp/tools/llm/llm_unified.py +18 -59
- hanzo_mcp/tools/llm/provider_tools.py +1 -3
- hanzo_mcp/tools/lsp/lsp_tool.py +621 -481
- hanzo_mcp/tools/mcp/mcp_add.py +1 -3
- hanzo_mcp/tools/mcp/mcp_stats.py +1 -3
- hanzo_mcp/tools/mcp/mcp_tool.py +9 -23
- hanzo_mcp/tools/memory/__init__.py +10 -27
- hanzo_mcp/tools/memory/conversation_memory.py +636 -0
- hanzo_mcp/tools/memory/knowledge_tools.py +7 -25
- hanzo_mcp/tools/memory/memory_tools.py +6 -18
- hanzo_mcp/tools/search/find_tool.py +12 -34
- hanzo_mcp/tools/search/unified_search.py +24 -78
- hanzo_mcp/tools/shell/__init__.py +16 -4
- hanzo_mcp/tools/shell/auto_background.py +2 -6
- hanzo_mcp/tools/shell/base.py +1 -5
- hanzo_mcp/tools/shell/base_process.py +5 -7
- hanzo_mcp/tools/shell/bash_session.py +7 -24
- hanzo_mcp/tools/shell/bash_session_executor.py +5 -15
- hanzo_mcp/tools/shell/bash_tool.py +3 -7
- hanzo_mcp/tools/shell/command_executor.py +26 -79
- hanzo_mcp/tools/shell/logs.py +4 -16
- hanzo_mcp/tools/shell/npx.py +2 -8
- hanzo_mcp/tools/shell/npx_tool.py +1 -3
- hanzo_mcp/tools/shell/pkill.py +4 -12
- hanzo_mcp/tools/shell/process_tool.py +2 -8
- hanzo_mcp/tools/shell/processes.py +5 -17
- hanzo_mcp/tools/shell/run_background.py +1 -3
- hanzo_mcp/tools/shell/run_command.py +1 -3
- hanzo_mcp/tools/shell/run_command_windows.py +1 -3
- hanzo_mcp/tools/shell/run_tool.py +56 -0
- hanzo_mcp/tools/shell/session_manager.py +2 -6
- hanzo_mcp/tools/shell/session_storage.py +2 -6
- hanzo_mcp/tools/shell/streaming_command.py +7 -23
- hanzo_mcp/tools/shell/uvx.py +4 -14
- hanzo_mcp/tools/shell/uvx_background.py +2 -6
- hanzo_mcp/tools/shell/uvx_tool.py +1 -3
- hanzo_mcp/tools/shell/zsh_tool.py +12 -20
- hanzo_mcp/tools/todo/todo.py +1 -3
- hanzo_mcp/tools/vector/__init__.py +97 -50
- hanzo_mcp/tools/vector/ast_analyzer.py +6 -20
- hanzo_mcp/tools/vector/git_ingester.py +10 -30
- hanzo_mcp/tools/vector/index_tool.py +3 -9
- hanzo_mcp/tools/vector/infinity_store.py +7 -27
- hanzo_mcp/tools/vector/mock_infinity.py +1 -3
- hanzo_mcp/tools/vector/node_tool.py +538 -0
- hanzo_mcp/tools/vector/project_manager.py +4 -12
- hanzo_mcp/tools/vector/unified_vector.py +384 -0
- hanzo_mcp/tools/vector/vector.py +2 -6
- hanzo_mcp/tools/vector/vector_index.py +8 -8
- hanzo_mcp/tools/vector/vector_search.py +7 -21
- {hanzo_mcp-0.8.11.dist-info → hanzo_mcp-0.9.0.dist-info}/METADATA +2 -2
- hanzo_mcp-0.9.0.dist-info/RECORD +191 -0
- hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +0 -645
- hanzo_mcp/tools/agent/swarm_tool.py +0 -718
- hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +0 -577
- hanzo_mcp/tools/filesystem/batch_search.py +0 -900
- hanzo_mcp/tools/filesystem/directory_tree_paginated.py +0 -350
- hanzo_mcp/tools/filesystem/find_files.py +0 -369
- hanzo_mcp/tools/filesystem/grep.py +0 -467
- hanzo_mcp/tools/filesystem/search_tool.py +0 -767
- hanzo_mcp/tools/filesystem/symbols_tool.py +0 -515
- hanzo_mcp/tools/filesystem/tree.py +0 -270
- hanzo_mcp/tools/jupyter/notebook_edit.py +0 -317
- hanzo_mcp/tools/jupyter/notebook_read.py +0 -147
- hanzo_mcp/tools/todo/todo_read.py +0 -143
- hanzo_mcp/tools/todo/todo_write.py +0 -374
- hanzo_mcp-0.8.11.dist-info/RECORD +0 -193
- {hanzo_mcp-0.8.11.dist-info → hanzo_mcp-0.9.0.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.8.11.dist-info → hanzo_mcp-0.9.0.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.8.11.dist-info → hanzo_mcp-0.9.0.dist-info}/top_level.txt +0 -0
|
@@ -44,9 +44,7 @@ class BashSessionExecutor:
|
|
|
44
44
|
self.fast_test_mode: bool = fast_test_mode
|
|
45
45
|
|
|
46
46
|
# If no session manager is provided, create a non-singleton instance to avoid shared state
|
|
47
|
-
self.session_manager: SessionManager = session_manager or SessionManager(
|
|
48
|
-
use_singleton=False
|
|
49
|
-
)
|
|
47
|
+
self.session_manager: SessionManager = session_manager or SessionManager(use_singleton=False)
|
|
50
48
|
|
|
51
49
|
# Excluded commands or patterns (for compatibility)
|
|
52
50
|
self.excluded_commands: list[str] = ["rm"]
|
|
@@ -146,9 +144,7 @@ class BashSessionExecutor:
|
|
|
146
144
|
Returns:
|
|
147
145
|
CommandResult containing execution results
|
|
148
146
|
"""
|
|
149
|
-
self._log(
|
|
150
|
-
f"Executing command: {command} (is_input={is_input}, blocking={blocking})"
|
|
151
|
-
)
|
|
147
|
+
self._log(f"Executing command: {command} (is_input={is_input}, blocking={blocking})")
|
|
152
148
|
|
|
153
149
|
# Check if the command is allowed (skip for input to running processes)
|
|
154
150
|
if not is_input and not self.is_command_allowed(command):
|
|
@@ -183,9 +179,7 @@ class BashSessionExecutor:
|
|
|
183
179
|
if session is None:
|
|
184
180
|
# Use faster timeouts and polling for tests
|
|
185
181
|
if self.fast_test_mode:
|
|
186
|
-
timeout_seconds =
|
|
187
|
-
10 # Faster timeout for tests but not too aggressive
|
|
188
|
-
)
|
|
182
|
+
timeout_seconds = 10 # Faster timeout for tests but not too aggressive
|
|
189
183
|
poll_interval = 0.2 # Faster polling for tests but still reasonable
|
|
190
184
|
else:
|
|
191
185
|
timeout_seconds = 30 # Default timeout
|
|
@@ -206,9 +200,7 @@ class BashSessionExecutor:
|
|
|
206
200
|
self._log(f"Failed to set environment variable {key}")
|
|
207
201
|
|
|
208
202
|
# Execute the command with enhanced parameters
|
|
209
|
-
result = session.execute(
|
|
210
|
-
command=command, is_input=is_input, blocking=blocking, timeout=timeout
|
|
211
|
-
)
|
|
203
|
+
result = session.execute(command=command, is_input=is_input, blocking=blocking, timeout=timeout)
|
|
212
204
|
|
|
213
205
|
# Add session_id to the result
|
|
214
206
|
result.session_id = effective_session_id
|
|
@@ -257,9 +249,7 @@ class BashSessionExecutor:
|
|
|
257
249
|
|
|
258
250
|
# Wait for completion with timeout
|
|
259
251
|
try:
|
|
260
|
-
stdout, stderr = await asyncio.wait_for(
|
|
261
|
-
process.communicate(), timeout=timeout
|
|
262
|
-
)
|
|
252
|
+
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=timeout)
|
|
263
253
|
except asyncio.TimeoutError:
|
|
264
254
|
# Kill the process if it times out
|
|
265
255
|
process.kill()
|
|
@@ -28,9 +28,7 @@ class BashTool(BaseScriptTool):
|
|
|
28
28
|
env: Optional[dict[str, str]] = None,
|
|
29
29
|
timeout: Optional[int] = None,
|
|
30
30
|
) -> str:
|
|
31
|
-
return await tool_self.run(
|
|
32
|
-
ctx, command=command, cwd=cwd, env=env, timeout=timeout
|
|
33
|
-
)
|
|
31
|
+
return await tool_self.run(ctx, command=command, cwd=cwd, env=env, timeout=timeout)
|
|
34
32
|
|
|
35
33
|
async def call(self, ctx: MCPContext, **params) -> str:
|
|
36
34
|
"""Call the tool with arguments."""
|
|
@@ -71,7 +69,7 @@ bash "npm run dev" --cwd ./frontend # Auto-backgrounds if needed"""
|
|
|
71
69
|
if Path(path).exists():
|
|
72
70
|
return path
|
|
73
71
|
return "cmd.exe" # Fall back to cmd if no bash found
|
|
74
|
-
|
|
72
|
+
|
|
75
73
|
# On Unix-like systems, always use bash
|
|
76
74
|
return "bash"
|
|
77
75
|
|
|
@@ -112,9 +110,7 @@ bash "npm run dev" --cwd ./frontend # Auto-backgrounds if needed"""
|
|
|
112
110
|
work_dir = Path(cwd).resolve() if cwd else Path.cwd()
|
|
113
111
|
|
|
114
112
|
# Always use execute_sync which now has auto-backgrounding
|
|
115
|
-
output = await self.execute_sync(
|
|
116
|
-
command, cwd=work_dir, env=env, timeout=timeout
|
|
117
|
-
)
|
|
113
|
+
output = await self.execute_sync(command, cwd=work_dir, env=env, timeout=timeout)
|
|
118
114
|
return output if output else "Command completed successfully (no output)"
|
|
119
115
|
|
|
120
116
|
|
|
@@ -27,9 +27,7 @@ class CommandExecutor:
|
|
|
27
27
|
comprehensive error handling, permissions checking, and progress tracking.
|
|
28
28
|
"""
|
|
29
29
|
|
|
30
|
-
def __init__(
|
|
31
|
-
self, permission_manager: PermissionManager, verbose: bool = False
|
|
32
|
-
) -> None:
|
|
30
|
+
def __init__(self, permission_manager: PermissionManager, verbose: bool = False) -> None:
|
|
33
31
|
"""Initialize command execution.
|
|
34
32
|
|
|
35
33
|
Args:
|
|
@@ -94,9 +92,7 @@ class CommandExecutor:
|
|
|
94
92
|
user_shell = os.environ.get("SHELL", "/bin/bash")
|
|
95
93
|
return os.path.basename(user_shell).lower(), user_shell
|
|
96
94
|
|
|
97
|
-
def _format_win32_shell_command(
|
|
98
|
-
self, shell_basename, user_shell, command, use_login_shell=True
|
|
99
|
-
):
|
|
95
|
+
def _format_win32_shell_command(self, shell_basename, user_shell, command, use_login_shell=True):
|
|
100
96
|
"""Format a command for execution with the appropriate Windows shell.
|
|
101
97
|
|
|
102
98
|
Args:
|
|
@@ -250,9 +246,7 @@ class CommandExecutor:
|
|
|
250
246
|
|
|
251
247
|
# Check if the command is allowed
|
|
252
248
|
if not self.is_command_allowed(command):
|
|
253
|
-
return CommandResult(
|
|
254
|
-
return_code=1, error_message=f"Command not allowed: {command}"
|
|
255
|
-
)
|
|
249
|
+
return CommandResult(return_code=1, error_message=f"Command not allowed: {command}")
|
|
256
250
|
|
|
257
251
|
# Check working directory permissions if specified
|
|
258
252
|
if cwd:
|
|
@@ -263,9 +257,7 @@ class CommandExecutor:
|
|
|
263
257
|
)
|
|
264
258
|
|
|
265
259
|
if not self.permission_manager.is_path_allowed(cwd):
|
|
266
|
-
return CommandResult(
|
|
267
|
-
return_code=1, error_message=f"Working directory not allowed: {cwd}"
|
|
268
|
-
)
|
|
260
|
+
return CommandResult(return_code=1, error_message=f"Working directory not allowed: {cwd}")
|
|
269
261
|
|
|
270
262
|
# Set up environment
|
|
271
263
|
command_env: dict[str, str] = os.environ.copy()
|
|
@@ -281,9 +273,7 @@ class CommandExecutor:
|
|
|
281
273
|
self._log(f"Using shell on Windows: {user_shell} ({shell_basename})")
|
|
282
274
|
|
|
283
275
|
# Format command using helper method
|
|
284
|
-
shell_cmd = self._format_win32_shell_command(
|
|
285
|
-
shell_basename, user_shell, command, use_login_shell
|
|
286
|
-
)
|
|
276
|
+
shell_cmd = self._format_win32_shell_command(shell_basename, user_shell, command, use_login_shell)
|
|
287
277
|
|
|
288
278
|
# Use shell for command execution
|
|
289
279
|
process = await asyncio.create_subprocess_shell(
|
|
@@ -312,19 +302,13 @@ class CommandExecutor:
|
|
|
312
302
|
self._log(f"Escaped command: {escaped_command}")
|
|
313
303
|
|
|
314
304
|
# Wrap command with appropriate shell invocation
|
|
315
|
-
if
|
|
316
|
-
shell_basename == "zsh"
|
|
317
|
-
or shell_basename == "bash"
|
|
318
|
-
or shell_basename == "fish"
|
|
319
|
-
):
|
|
305
|
+
if shell_basename == "zsh" or shell_basename == "bash" or shell_basename == "fish":
|
|
320
306
|
shell_cmd = f"{user_shell} -l -c '{escaped_command}'"
|
|
321
307
|
else:
|
|
322
308
|
# Default fallback
|
|
323
309
|
shell_cmd = f"{user_shell} -c '{escaped_command}'"
|
|
324
310
|
else:
|
|
325
|
-
self._log(
|
|
326
|
-
f"Using shell for command with shell operators: {command}"
|
|
327
|
-
)
|
|
311
|
+
self._log(f"Using shell for command with shell operators: {command}")
|
|
328
312
|
# Escape single quotes in command for shell execution
|
|
329
313
|
escaped_command = command.replace("'", "'\\''")
|
|
330
314
|
self._log(f"Original command: {command}")
|
|
@@ -354,9 +338,7 @@ class CommandExecutor:
|
|
|
354
338
|
|
|
355
339
|
# Wait for the process to complete with timeout
|
|
356
340
|
try:
|
|
357
|
-
stdout_bytes, stderr_bytes = await asyncio.wait_for(
|
|
358
|
-
process.communicate(), timeout=timeout
|
|
359
|
-
)
|
|
341
|
+
stdout_bytes, stderr_bytes = await asyncio.wait_for(process.communicate(), timeout=timeout)
|
|
360
342
|
|
|
361
343
|
return CommandResult(
|
|
362
344
|
return_code=process.returncode or 0,
|
|
@@ -376,9 +358,7 @@ class CommandExecutor:
|
|
|
376
358
|
)
|
|
377
359
|
except Exception as e:
|
|
378
360
|
self._log(f"Command execution error: {str(e)}")
|
|
379
|
-
return CommandResult(
|
|
380
|
-
return_code=1, error_message=f"Error executing command: {str(e)}"
|
|
381
|
-
)
|
|
361
|
+
return CommandResult(return_code=1, error_message=f"Error executing command: {str(e)}")
|
|
382
362
|
|
|
383
363
|
async def execute_script(
|
|
384
364
|
self,
|
|
@@ -415,9 +395,7 @@ class CommandExecutor:
|
|
|
415
395
|
)
|
|
416
396
|
|
|
417
397
|
if not self.permission_manager.is_path_allowed(cwd):
|
|
418
|
-
return CommandResult(
|
|
419
|
-
return_code=1, error_message=f"Working directory not allowed: {cwd}"
|
|
420
|
-
)
|
|
398
|
+
return CommandResult(return_code=1, error_message=f"Working directory not allowed: {cwd}")
|
|
421
399
|
|
|
422
400
|
# Check if we need special handling for this interpreter
|
|
423
401
|
interpreter_name = interpreter.split()[0].lower()
|
|
@@ -469,9 +447,7 @@ class CommandExecutor:
|
|
|
469
447
|
self._log(f"Using shell on Windows for interpreter: {user_shell}")
|
|
470
448
|
|
|
471
449
|
# Format command using helper method for the interpreter
|
|
472
|
-
shell_cmd = self._format_win32_shell_command(
|
|
473
|
-
shell_basename, user_shell, interpreter, use_login_shell
|
|
474
|
-
)
|
|
450
|
+
shell_cmd = self._format_win32_shell_command(shell_basename, user_shell, interpreter, use_login_shell)
|
|
475
451
|
|
|
476
452
|
# Create and run the process with shell
|
|
477
453
|
process = await asyncio.create_subprocess_shell(
|
|
@@ -516,9 +492,7 @@ class CommandExecutor:
|
|
|
516
492
|
# Wait for the process to complete with timeout
|
|
517
493
|
try:
|
|
518
494
|
script_bytes: bytes = script.encode("utf-8")
|
|
519
|
-
stdout_bytes, stderr_bytes = await asyncio.wait_for(
|
|
520
|
-
process.communicate(script_bytes), timeout=timeout
|
|
521
|
-
)
|
|
495
|
+
stdout_bytes, stderr_bytes = await asyncio.wait_for(process.communicate(script_bytes), timeout=timeout)
|
|
522
496
|
|
|
523
497
|
return CommandResult(
|
|
524
498
|
return_code=process.returncode or 0,
|
|
@@ -538,9 +512,7 @@ class CommandExecutor:
|
|
|
538
512
|
)
|
|
539
513
|
except Exception as e:
|
|
540
514
|
self._log(f"Script execution error: {str(e)}")
|
|
541
|
-
return CommandResult(
|
|
542
|
-
return_code=1, error_message=f"Error executing script: {str(e)}"
|
|
543
|
-
)
|
|
515
|
+
return CommandResult(return_code=1, error_message=f"Error executing script: {str(e)}")
|
|
544
516
|
|
|
545
517
|
async def _handle_fish_script(
|
|
546
518
|
self,
|
|
@@ -591,9 +563,7 @@ class CommandExecutor:
|
|
|
591
563
|
|
|
592
564
|
# Wait for the process to complete with timeout
|
|
593
565
|
try:
|
|
594
|
-
stdout_bytes, stderr_bytes = await asyncio.wait_for(
|
|
595
|
-
process.communicate(), timeout=timeout
|
|
596
|
-
)
|
|
566
|
+
stdout_bytes, stderr_bytes = await asyncio.wait_for(process.communicate(), timeout=timeout)
|
|
597
567
|
|
|
598
568
|
return CommandResult(
|
|
599
569
|
return_code=process.returncode or 0,
|
|
@@ -613,9 +583,7 @@ class CommandExecutor:
|
|
|
613
583
|
)
|
|
614
584
|
except Exception as e:
|
|
615
585
|
self._log(f"Fish script execution error: {str(e)}")
|
|
616
|
-
return CommandResult(
|
|
617
|
-
return_code=1, error_message=f"Error executing Fish script: {str(e)}"
|
|
618
|
-
)
|
|
586
|
+
return CommandResult(return_code=1, error_message=f"Error executing Fish script: {str(e)}")
|
|
619
587
|
|
|
620
588
|
async def execute_script_from_file(
|
|
621
589
|
self,
|
|
@@ -671,9 +639,7 @@ class CommandExecutor:
|
|
|
671
639
|
command_env.update(env)
|
|
672
640
|
|
|
673
641
|
# Create a temporary file for the script
|
|
674
|
-
with tempfile.NamedTemporaryFile(
|
|
675
|
-
suffix=extension, mode="w", delete=False
|
|
676
|
-
) as temp:
|
|
642
|
+
with tempfile.NamedTemporaryFile(suffix=extension, mode="w", delete=False) as temp:
|
|
677
643
|
temp_path = temp.name
|
|
678
644
|
_ = temp.write(script) # Explicitly ignore the return value
|
|
679
645
|
|
|
@@ -696,9 +662,7 @@ class CommandExecutor:
|
|
|
696
662
|
wsl_path = temp_path.replace("\\", "/")
|
|
697
663
|
self._log(f"WSL path conversion may be incomplete: {wsl_path}")
|
|
698
664
|
|
|
699
|
-
self._log(
|
|
700
|
-
f"Converted Windows path '{temp_path}' to WSL path '{wsl_path}'"
|
|
701
|
-
)
|
|
665
|
+
self._log(f"Converted Windows path '{temp_path}' to WSL path '{wsl_path}'")
|
|
702
666
|
temp_path = wsl_path
|
|
703
667
|
|
|
704
668
|
# Build the command including args
|
|
@@ -709,13 +673,9 @@ class CommandExecutor:
|
|
|
709
673
|
cmd += " " + " ".join(args)
|
|
710
674
|
|
|
711
675
|
# Format command using helper method
|
|
712
|
-
shell_cmd = self._format_win32_shell_command(
|
|
713
|
-
shell_basename, user_shell, cmd, use_login_shell
|
|
714
|
-
)
|
|
676
|
+
shell_cmd = self._format_win32_shell_command(shell_basename, user_shell, cmd, use_login_shell)
|
|
715
677
|
|
|
716
|
-
self._log(
|
|
717
|
-
f"Executing script from file on Windows with shell: {shell_cmd}"
|
|
718
|
-
)
|
|
678
|
+
self._log(f"Executing script from file on Windows with shell: {shell_cmd}")
|
|
719
679
|
|
|
720
680
|
# Create and run the process with shell
|
|
721
681
|
process = await asyncio.create_subprocess_shell(
|
|
@@ -741,9 +701,7 @@ class CommandExecutor:
|
|
|
741
701
|
# Create command that runs script through login shell
|
|
742
702
|
shell_cmd = f"{user_shell} -l -c '{cmd}'"
|
|
743
703
|
|
|
744
|
-
self._log(
|
|
745
|
-
f"Executing script from file with login shell: {shell_cmd}"
|
|
746
|
-
)
|
|
704
|
+
self._log(f"Executing script from file with login shell: {shell_cmd}")
|
|
747
705
|
|
|
748
706
|
# Create and run the process with shell
|
|
749
707
|
process = await asyncio.create_subprocess_shell(
|
|
@@ -772,9 +730,7 @@ class CommandExecutor:
|
|
|
772
730
|
|
|
773
731
|
# Wait for the process to complete with timeout
|
|
774
732
|
try:
|
|
775
|
-
stdout_bytes, stderr_bytes = await asyncio.wait_for(
|
|
776
|
-
process.communicate(), timeout=timeout
|
|
777
|
-
)
|
|
733
|
+
stdout_bytes, stderr_bytes = await asyncio.wait_for(process.communicate(), timeout=timeout)
|
|
778
734
|
|
|
779
735
|
return CommandResult(
|
|
780
736
|
return_code=process.returncode or 0,
|
|
@@ -794,9 +750,7 @@ class CommandExecutor:
|
|
|
794
750
|
)
|
|
795
751
|
except Exception as e:
|
|
796
752
|
self._log(f"Script file execution error: {str(e)}")
|
|
797
|
-
return CommandResult(
|
|
798
|
-
return_code=1, error_message=f"Error executing script: {str(e)}"
|
|
799
|
-
)
|
|
753
|
+
return CommandResult(return_code=1, error_message=f"Error executing script: {str(e)}")
|
|
800
754
|
finally:
|
|
801
755
|
# Clean up temporary file
|
|
802
756
|
try:
|
|
@@ -863,9 +817,7 @@ class CommandExecutor:
|
|
|
863
817
|
},
|
|
864
818
|
}
|
|
865
819
|
|
|
866
|
-
def _get_interpreter_path(
|
|
867
|
-
self, language: str, shell_type: str | None = None
|
|
868
|
-
) -> tuple[str, list[str]]:
|
|
820
|
+
def _get_interpreter_path(self, language: str, shell_type: str | None = None) -> tuple[str, list[str]]:
|
|
869
821
|
"""Get the full path to the interpreter for the given language.
|
|
870
822
|
|
|
871
823
|
Attempts to find the full path to the interpreter command, but only for
|
|
@@ -901,25 +853,20 @@ class CommandExecutor:
|
|
|
901
853
|
|
|
902
854
|
# For Windows shell types, try to find the full path
|
|
903
855
|
if sys.platform == "win32" and (
|
|
904
|
-
not shell_type
|
|
905
|
-
or shell_type.lower() in ["cmd", "powershell", "cmd.exe", "powershell.exe"]
|
|
856
|
+
not shell_type or shell_type.lower() in ["cmd", "powershell", "cmd.exe", "powershell.exe"]
|
|
906
857
|
):
|
|
907
858
|
try:
|
|
908
859
|
# Try to find the full path to the command
|
|
909
860
|
full_path = shutil.which(command)
|
|
910
861
|
if full_path:
|
|
911
|
-
self._log(
|
|
912
|
-
f"Found full path for {language} interpreter: {full_path}"
|
|
913
|
-
)
|
|
862
|
+
self._log(f"Found full path for {language} interpreter: {full_path}")
|
|
914
863
|
return full_path, args
|
|
915
864
|
|
|
916
865
|
# If primary command not found, try alternatives
|
|
917
866
|
for alt_command in alternatives:
|
|
918
867
|
alt_path = shutil.which(alt_command)
|
|
919
868
|
if alt_path:
|
|
920
|
-
self._log(
|
|
921
|
-
f"Found alternative path for {language} interpreter: {alt_path}"
|
|
922
|
-
)
|
|
869
|
+
self._log(f"Found alternative path for {language} interpreter: {alt_path}")
|
|
923
870
|
return alt_path, args
|
|
924
871
|
except Exception as e:
|
|
925
872
|
self._log(f"Error finding path for {language} interpreter: {str(e)}")
|
hanzo_mcp/tools/shell/logs.py
CHANGED
|
@@ -167,9 +167,7 @@ Use run_command with 'tail -f' for continuous monitoring.
|
|
|
167
167
|
|
|
168
168
|
# Note about follow mode
|
|
169
169
|
if follow:
|
|
170
|
-
await tool_ctx.warning(
|
|
171
|
-
"Follow mode not supported in MCP. Showing latest lines instead."
|
|
172
|
-
)
|
|
170
|
+
await tool_ctx.warning("Follow mode not supported in MCP. Showing latest lines instead.")
|
|
173
171
|
|
|
174
172
|
# Read log file
|
|
175
173
|
await tool_ctx.info(f"Reading log file: {log_path}")
|
|
@@ -197,11 +195,7 @@ Use run_command with 'tail -f' for continuous monitoring.
|
|
|
197
195
|
if process:
|
|
198
196
|
header += f"Process: {process.name} (ID: {process_id})\n"
|
|
199
197
|
header += f"Command: {process.command}\n"
|
|
200
|
-
status = (
|
|
201
|
-
"running"
|
|
202
|
-
if process.is_running
|
|
203
|
-
else f"finished (code: {process.return_code})"
|
|
204
|
-
)
|
|
198
|
+
status = "running" if process.is_running else f"finished (code: {process.return_code})"
|
|
205
199
|
header += f"Status: {status}\n"
|
|
206
200
|
header += f"{'=' * 50}\n"
|
|
207
201
|
|
|
@@ -232,11 +226,7 @@ Use run_command with 'tail -f' for continuous monitoring.
|
|
|
232
226
|
|
|
233
227
|
# Check which logs belong to active processes
|
|
234
228
|
active_processes = RunBackgroundTool.get_processes()
|
|
235
|
-
active_log_files = {
|
|
236
|
-
str(p.log_file): (pid, p)
|
|
237
|
-
for pid, p in active_processes.items()
|
|
238
|
-
if p.log_file
|
|
239
|
-
}
|
|
229
|
+
active_log_files = {str(p.log_file): (pid, p) for pid, p in active_processes.items() if p.log_file}
|
|
240
230
|
|
|
241
231
|
# Build output
|
|
242
232
|
output = []
|
|
@@ -250,9 +240,7 @@ Use run_command with 'tail -f' for continuous monitoring.
|
|
|
250
240
|
if str(log_file) in active_log_files:
|
|
251
241
|
pid, process = active_log_files[str(log_file)]
|
|
252
242
|
status = "active" if process.is_running else "finished"
|
|
253
|
-
output.append(
|
|
254
|
-
f"{log_file.name:<50} {size_str:>10} [{status}] (ID: {pid})"
|
|
255
|
-
)
|
|
243
|
+
output.append(f"{log_file.name:<50} {size_str:>10} [{status}] (ID: {pid})")
|
|
256
244
|
else:
|
|
257
245
|
output.append(f"{log_file.name:<50} {size_str:>10}")
|
|
258
246
|
|
hanzo_mcp/tools/shell/npx.py
CHANGED
|
@@ -160,9 +160,7 @@ Or download from: https://nodejs.org/"""
|
|
|
160
160
|
|
|
161
161
|
try:
|
|
162
162
|
# Execute command
|
|
163
|
-
result = subprocess.run(
|
|
164
|
-
cmd, capture_output=True, text=True, timeout=timeout, check=True
|
|
165
|
-
)
|
|
163
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, check=True)
|
|
166
164
|
|
|
167
165
|
output = []
|
|
168
166
|
if result.stdout:
|
|
@@ -170,11 +168,7 @@ Or download from: https://nodejs.org/"""
|
|
|
170
168
|
if result.stderr:
|
|
171
169
|
output.append(f"\nSTDERR:\n{result.stderr}")
|
|
172
170
|
|
|
173
|
-
return (
|
|
174
|
-
"\n".join(output)
|
|
175
|
-
if output
|
|
176
|
-
else "Command completed successfully with no output."
|
|
177
|
-
)
|
|
171
|
+
return "\n".join(output) if output else "Command completed successfully with no output."
|
|
178
172
|
|
|
179
173
|
except subprocess.TimeoutExpired:
|
|
180
174
|
return f"Error: Command timed out after {timeout} seconds. Use npx_background for long-running processes."
|
|
@@ -86,9 +86,7 @@ npx json-server db.json # Auto-backgrounds if needed"""
|
|
|
86
86
|
cwd: Optional[str] = None,
|
|
87
87
|
yes: bool = True,
|
|
88
88
|
) -> str:
|
|
89
|
-
return await tool_self.run(
|
|
90
|
-
ctx, package=package, args=args, cwd=cwd, yes=yes
|
|
91
|
-
)
|
|
89
|
+
return await tool_self.run(ctx, package=package, args=args, cwd=cwd, yes=yes)
|
|
92
90
|
|
|
93
91
|
async def call(self, ctx: MCPContext, **params) -> str:
|
|
94
92
|
"""Call the tool with arguments."""
|
hanzo_mcp/tools/shell/pkill.py
CHANGED
|
@@ -151,9 +151,7 @@ Examples:
|
|
|
151
151
|
else:
|
|
152
152
|
process.terminate()
|
|
153
153
|
killed_count += 1
|
|
154
|
-
await tool_ctx.info(
|
|
155
|
-
f"Killed process {proc_id} ({process.name})"
|
|
156
|
-
)
|
|
154
|
+
await tool_ctx.info(f"Killed process {proc_id} ({process.name})")
|
|
157
155
|
except Exception as e:
|
|
158
156
|
errors.append(f"Failed to kill {proc_id}: {str(e)}")
|
|
159
157
|
|
|
@@ -222,9 +220,7 @@ Examples:
|
|
|
222
220
|
else:
|
|
223
221
|
process.terminate()
|
|
224
222
|
killed_count += 1
|
|
225
|
-
await tool_ctx.info(
|
|
226
|
-
f"Killed background process {proc_id} ({process.name})"
|
|
227
|
-
)
|
|
223
|
+
await tool_ctx.info(f"Killed background process {proc_id} ({process.name})")
|
|
228
224
|
except Exception as e:
|
|
229
225
|
errors.append(f"Failed to kill {proc_id}: {str(e)}")
|
|
230
226
|
|
|
@@ -237,15 +233,11 @@ Examples:
|
|
|
237
233
|
else:
|
|
238
234
|
proc.terminate()
|
|
239
235
|
killed_count += 1
|
|
240
|
-
await tool_ctx.info(
|
|
241
|
-
f"Killed {proc.info['name']} (PID: {proc.info['pid']})"
|
|
242
|
-
)
|
|
236
|
+
await tool_ctx.info(f"Killed {proc.info['name']} (PID: {proc.info['pid']})")
|
|
243
237
|
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
244
238
|
continue
|
|
245
239
|
except Exception as e:
|
|
246
|
-
errors.append(
|
|
247
|
-
f"Failed to kill PID {proc.info['pid']}: {str(e)}"
|
|
248
|
-
)
|
|
240
|
+
errors.append(f"Failed to kill PID {proc.info['pid']}: {str(e)}")
|
|
249
241
|
|
|
250
242
|
# Build result message
|
|
251
243
|
if killed_count > 0:
|
|
@@ -61,11 +61,7 @@ process --action logs --id bash_ghi789 --lines 50"""
|
|
|
61
61
|
|
|
62
62
|
output = ["Background processes:"]
|
|
63
63
|
for proc_id, info in processes.items():
|
|
64
|
-
status = (
|
|
65
|
-
"running"
|
|
66
|
-
if info["running"]
|
|
67
|
-
else f"stopped (exit code: {info.get('return_code', 'unknown')})"
|
|
68
|
-
)
|
|
64
|
+
status = "running" if info["running"] else f"stopped (exit code: {info.get('return_code', 'unknown')})"
|
|
69
65
|
output.append(f"- {proc_id}: PID {info['pid']} - {status}")
|
|
70
66
|
if info.get("log_file"):
|
|
71
67
|
output.append(f" Log: {info['log_file']}")
|
|
@@ -134,9 +130,7 @@ process --action logs --id bash_ghi789 --lines 50"""
|
|
|
134
130
|
signal_type: str = "TERM",
|
|
135
131
|
lines: int = 100,
|
|
136
132
|
) -> str:
|
|
137
|
-
return await tool_self.run(
|
|
138
|
-
ctx, action=action, id=id, signal_type=signal_type, lines=lines
|
|
139
|
-
)
|
|
133
|
+
return await tool_self.run(ctx, action=action, id=id, signal_type=signal_type, lines=lines)
|
|
140
134
|
|
|
141
135
|
async def call(self, ctx: MCPContext, **params) -> str:
|
|
142
136
|
"""Call the tool with arguments."""
|
|
@@ -126,9 +126,7 @@ Examples:
|
|
|
126
126
|
await tool_ctx.error(f"Failed to list processes: {str(e)}")
|
|
127
127
|
return f"Error listing processes: {str(e)}"
|
|
128
128
|
|
|
129
|
-
def _list_background_processes(
|
|
130
|
-
self, filter_name: Optional[str], show_details: bool
|
|
131
|
-
) -> str:
|
|
129
|
+
def _list_background_processes(self, filter_name: Optional[str], show_details: bool) -> str:
|
|
132
130
|
"""List background processes started with run_background."""
|
|
133
131
|
processes = RunBackgroundTool.get_processes()
|
|
134
132
|
|
|
@@ -154,11 +152,7 @@ Examples:
|
|
|
154
152
|
filtered_processes.sort(key=lambda x: x[1].start_time, reverse=True)
|
|
155
153
|
|
|
156
154
|
for proc_id, process in filtered_processes:
|
|
157
|
-
status = (
|
|
158
|
-
"running"
|
|
159
|
-
if process.is_running
|
|
160
|
-
else f"finished (code: {process.return_code})"
|
|
161
|
-
)
|
|
155
|
+
status = "running" if process.is_running else f"finished (code: {process.return_code})"
|
|
162
156
|
runtime = datetime.now() - process.start_time
|
|
163
157
|
runtime_str = str(runtime).split(".")[0] # Remove microseconds
|
|
164
158
|
|
|
@@ -191,9 +185,7 @@ Examples:
|
|
|
191
185
|
|
|
192
186
|
return "\n".join(output)
|
|
193
187
|
|
|
194
|
-
def _list_system_processes(
|
|
195
|
-
self, filter_name: Optional[str], show_details: bool
|
|
196
|
-
) -> str:
|
|
188
|
+
def _list_system_processes(self, filter_name: Optional[str], show_details: bool) -> str:
|
|
197
189
|
"""List all system processes."""
|
|
198
190
|
try:
|
|
199
191
|
processes = []
|
|
@@ -229,9 +221,7 @@ Examples:
|
|
|
229
221
|
|
|
230
222
|
if show_details:
|
|
231
223
|
process_info["cpu"] = proc.cpu_percent(interval=0.1)
|
|
232
|
-
process_info["memory"] = (
|
|
233
|
-
proc.memory_info().rss / 1024 / 1024
|
|
234
|
-
) # MB
|
|
224
|
+
process_info["memory"] = proc.memory_info().rss / 1024 / 1024 # MB
|
|
235
225
|
|
|
236
226
|
processes.append(process_info)
|
|
237
227
|
|
|
@@ -250,9 +240,7 @@ Examples:
|
|
|
250
240
|
|
|
251
241
|
# Header
|
|
252
242
|
if show_details:
|
|
253
|
-
output.append(
|
|
254
|
-
f"{'PID':>7} {'CPU%':>5} {'MEM(MB)':>8} {'NAME':<20} COMMAND"
|
|
255
|
-
)
|
|
243
|
+
output.append(f"{'PID':>7} {'CPU%':>5} {'MEM(MB)':>8} {'NAME':<20} COMMAND")
|
|
256
244
|
output.append("-" * 80)
|
|
257
245
|
|
|
258
246
|
for proc in processes:
|
|
@@ -273,9 +273,7 @@ Examples:
|
|
|
273
273
|
# Clean up finished processes
|
|
274
274
|
self._cleanup_finished_processes()
|
|
275
275
|
|
|
276
|
-
await tool_ctx.info(
|
|
277
|
-
f"Process started with ID: {process_id}, PID: {process.pid}"
|
|
278
|
-
)
|
|
276
|
+
await tool_ctx.info(f"Process started with ID: {process_id}, PID: {process.pid}")
|
|
279
277
|
|
|
280
278
|
# Return process information
|
|
281
279
|
return f"""Background process started successfully!
|
|
@@ -77,9 +77,7 @@ class RunCommandToolParams(TypedDict):
|
|
|
77
77
|
class RunCommandTool(ShellBaseTool):
|
|
78
78
|
"""Tool for executing shell commands."""
|
|
79
79
|
|
|
80
|
-
def __init__(
|
|
81
|
-
self, permission_manager: Any, command_executor: BashSessionExecutor
|
|
82
|
-
) -> None:
|
|
80
|
+
def __init__(self, permission_manager: Any, command_executor: BashSessionExecutor) -> None:
|
|
83
81
|
"""Initialize the run command tool.
|
|
84
82
|
|
|
85
83
|
Args:
|
|
@@ -20,9 +20,7 @@ from hanzo_mcp.tools.shell.command_executor import CommandExecutor
|
|
|
20
20
|
class RunCommandTool(ShellBaseTool):
|
|
21
21
|
"""Tool for executing shell commands."""
|
|
22
22
|
|
|
23
|
-
def __init__(
|
|
24
|
-
self, permission_manager: Any, command_executor: CommandExecutor
|
|
25
|
-
) -> None:
|
|
23
|
+
def __init__(self, permission_manager: Any, command_executor: CommandExecutor) -> None:
|
|
26
24
|
"""Initialize the run command tool.
|
|
27
25
|
|
|
28
26
|
Args:
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Run tool for command execution with automatic backgrounding."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional, override
|
|
4
|
+
from mcp.server import FastMCP
|
|
5
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
6
|
+
|
|
7
|
+
from hanzo_mcp.tools.shell.zsh_tool import ShellTool
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RunTool(ShellTool):
|
|
11
|
+
"""Tool for running commands with automatic backgrounding (alias of shell tool)."""
|
|
12
|
+
|
|
13
|
+
name = "run"
|
|
14
|
+
|
|
15
|
+
def register(self, server: FastMCP) -> None:
|
|
16
|
+
"""Register the tool with the MCP server."""
|
|
17
|
+
tool_self = self
|
|
18
|
+
|
|
19
|
+
@server.tool(name=self.name, description=self.description)
|
|
20
|
+
async def run(
|
|
21
|
+
ctx: MCPContext,
|
|
22
|
+
command: str,
|
|
23
|
+
cwd: Optional[str] = None,
|
|
24
|
+
env: Optional[dict[str, str]] = None,
|
|
25
|
+
timeout: Optional[int] = None,
|
|
26
|
+
) -> str:
|
|
27
|
+
return await tool_self.run(ctx, command=command, cwd=cwd, env=env, timeout=timeout)
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
@override
|
|
31
|
+
def description(self) -> str:
|
|
32
|
+
"""Get the tool description."""
|
|
33
|
+
return """Execute shell commands with automatic backgrounding for long-running processes.
|
|
34
|
+
|
|
35
|
+
Automatically selects the best available shell:
|
|
36
|
+
- Zsh if available (with .zshrc)
|
|
37
|
+
- User's preferred shell ($SHELL)
|
|
38
|
+
- Bash as fallback
|
|
39
|
+
|
|
40
|
+
Commands that run for more than 2 minutes will automatically continue in the background.
|
|
41
|
+
You can check their status and logs using the 'process' tool.
|
|
42
|
+
|
|
43
|
+
Usage:
|
|
44
|
+
run "ls -la"
|
|
45
|
+
run "python server.py" # Auto-backgrounds after 2 minutes
|
|
46
|
+
run "git status && git diff"
|
|
47
|
+
run "npm run dev" --cwd ./frontend # Auto-backgrounds if needed"""
|
|
48
|
+
|
|
49
|
+
@override
|
|
50
|
+
def get_tool_name(self) -> str:
|
|
51
|
+
"""Get the tool name."""
|
|
52
|
+
return "run"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# Create instance
|
|
56
|
+
run_tool = RunTool()
|