hanzo-mcp 0.6.13__py3-none-any.whl → 0.7.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.
- hanzo_mcp/analytics/__init__.py +5 -0
- hanzo_mcp/analytics/posthog_analytics.py +364 -0
- hanzo_mcp/cli.py +3 -3
- hanzo_mcp/cli_enhanced.py +3 -3
- hanzo_mcp/config/settings.py +1 -1
- hanzo_mcp/config/tool_config.py +18 -4
- hanzo_mcp/server.py +34 -1
- hanzo_mcp/tools/__init__.py +65 -2
- hanzo_mcp/tools/agent/__init__.py +84 -3
- hanzo_mcp/tools/agent/agent_tool.py +102 -4
- hanzo_mcp/tools/agent/agent_tool_v2.py +492 -0
- hanzo_mcp/tools/agent/clarification_protocol.py +220 -0
- hanzo_mcp/tools/agent/clarification_tool.py +68 -0
- hanzo_mcp/tools/agent/claude_cli_tool.py +125 -0
- hanzo_mcp/tools/agent/claude_desktop_auth.py +508 -0
- hanzo_mcp/tools/agent/cli_agent_base.py +191 -0
- hanzo_mcp/tools/agent/code_auth.py +436 -0
- hanzo_mcp/tools/agent/code_auth_tool.py +194 -0
- hanzo_mcp/tools/agent/codex_cli_tool.py +123 -0
- hanzo_mcp/tools/agent/critic_tool.py +376 -0
- hanzo_mcp/tools/agent/gemini_cli_tool.py +128 -0
- hanzo_mcp/tools/agent/grok_cli_tool.py +128 -0
- hanzo_mcp/tools/agent/iching_tool.py +380 -0
- hanzo_mcp/tools/agent/network_tool.py +273 -0
- hanzo_mcp/tools/agent/prompt.py +62 -20
- hanzo_mcp/tools/agent/review_tool.py +433 -0
- hanzo_mcp/tools/agent/swarm_tool.py +535 -0
- hanzo_mcp/tools/agent/swarm_tool_v2.py +654 -0
- hanzo_mcp/tools/common/base.py +1 -0
- hanzo_mcp/tools/common/batch_tool.py +102 -10
- hanzo_mcp/tools/common/fastmcp_pagination.py +369 -0
- hanzo_mcp/tools/common/forgiving_edit.py +243 -0
- hanzo_mcp/tools/common/paginated_base.py +230 -0
- hanzo_mcp/tools/common/paginated_response.py +307 -0
- hanzo_mcp/tools/common/pagination.py +226 -0
- hanzo_mcp/tools/common/tool_list.py +3 -0
- hanzo_mcp/tools/common/truncate.py +101 -0
- hanzo_mcp/tools/filesystem/__init__.py +29 -0
- hanzo_mcp/tools/filesystem/ast_multi_edit.py +562 -0
- hanzo_mcp/tools/filesystem/directory_tree_paginated.py +338 -0
- hanzo_mcp/tools/lsp/__init__.py +5 -0
- hanzo_mcp/tools/lsp/lsp_tool.py +512 -0
- hanzo_mcp/tools/memory/__init__.py +76 -0
- hanzo_mcp/tools/memory/knowledge_tools.py +518 -0
- hanzo_mcp/tools/memory/memory_tools.py +456 -0
- hanzo_mcp/tools/search/__init__.py +6 -0
- hanzo_mcp/tools/search/find_tool.py +581 -0
- hanzo_mcp/tools/search/unified_search.py +953 -0
- hanzo_mcp/tools/shell/__init__.py +5 -0
- hanzo_mcp/tools/shell/auto_background.py +203 -0
- hanzo_mcp/tools/shell/base_process.py +53 -27
- hanzo_mcp/tools/shell/bash_tool.py +17 -33
- hanzo_mcp/tools/shell/npx_tool.py +15 -32
- hanzo_mcp/tools/shell/streaming_command.py +594 -0
- hanzo_mcp/tools/shell/uvx_tool.py +15 -32
- hanzo_mcp/types.py +23 -0
- {hanzo_mcp-0.6.13.dist-info → hanzo_mcp-0.7.1.dist-info}/METADATA +229 -71
- {hanzo_mcp-0.6.13.dist-info → hanzo_mcp-0.7.1.dist-info}/RECORD +61 -24
- hanzo_mcp-0.6.13.dist-info/licenses/LICENSE +0 -21
- {hanzo_mcp-0.6.13.dist-info → hanzo_mcp-0.7.1.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.6.13.dist-info → hanzo_mcp-0.7.1.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.6.13.dist-info → hanzo_mcp-0.7.1.dist-info}/top_level.txt +0 -0
|
@@ -14,6 +14,7 @@ from hanzo_mcp.tools.shell.npx_tool import npx_tool
|
|
|
14
14
|
from hanzo_mcp.tools.shell.uvx_tool import uvx_tool
|
|
15
15
|
from hanzo_mcp.tools.shell.process_tool import process_tool
|
|
16
16
|
from hanzo_mcp.tools.shell.open import open_tool
|
|
17
|
+
# from hanzo_mcp.tools.shell.streaming_command import StreamingCommandTool
|
|
17
18
|
|
|
18
19
|
# Export all tool classes
|
|
19
20
|
__all__ = [
|
|
@@ -38,12 +39,16 @@ def get_shell_tools(
|
|
|
38
39
|
npx_tool.permission_manager = permission_manager
|
|
39
40
|
uvx_tool.permission_manager = permission_manager
|
|
40
41
|
|
|
42
|
+
# Note: StreamingCommandTool is abstract and shouldn't be instantiated directly
|
|
43
|
+
# It's used as a base class for other streaming tools
|
|
44
|
+
|
|
41
45
|
return [
|
|
42
46
|
bash_tool,
|
|
43
47
|
npx_tool,
|
|
44
48
|
uvx_tool,
|
|
45
49
|
process_tool,
|
|
46
50
|
open_tool,
|
|
51
|
+
# streaming_command_tool, # Removed as it's abstract
|
|
47
52
|
]
|
|
48
53
|
|
|
49
54
|
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""Auto-backgrounding shell execution.
|
|
2
|
+
|
|
3
|
+
This module provides automatic backgrounding of long-running processes.
|
|
4
|
+
Commands that take more than 2 minutes automatically continue in background.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import os
|
|
9
|
+
import time
|
|
10
|
+
import uuid
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Optional, Tuple
|
|
13
|
+
|
|
14
|
+
from hanzo_mcp.tools.shell.base_process import ProcessManager
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AutoBackgroundExecutor:
|
|
18
|
+
"""Executor that automatically backgrounds long-running processes."""
|
|
19
|
+
|
|
20
|
+
# Default timeout before auto-backgrounding (2 minutes)
|
|
21
|
+
DEFAULT_TIMEOUT = 120.0
|
|
22
|
+
|
|
23
|
+
def __init__(self, process_manager: ProcessManager, timeout: float = DEFAULT_TIMEOUT):
|
|
24
|
+
"""Initialize the auto-background executor.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
process_manager: Process manager for tracking background processes
|
|
28
|
+
timeout: Timeout in seconds before auto-backgrounding (default: 120s)
|
|
29
|
+
"""
|
|
30
|
+
self.process_manager = process_manager
|
|
31
|
+
self.timeout = timeout
|
|
32
|
+
|
|
33
|
+
async def execute_with_auto_background(
|
|
34
|
+
self,
|
|
35
|
+
cmd_args: list[str],
|
|
36
|
+
tool_name: str,
|
|
37
|
+
cwd: Optional[Path] = None,
|
|
38
|
+
env: Optional[dict[str, str]] = None,
|
|
39
|
+
) -> Tuple[str, bool, Optional[str]]:
|
|
40
|
+
"""Execute a command with automatic backgrounding if it takes too long.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
cmd_args: Command arguments list
|
|
44
|
+
tool_name: Name of the tool (for process ID generation)
|
|
45
|
+
cwd: Working directory
|
|
46
|
+
env: Environment variables
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Tuple of (output/status, was_backgrounded, process_id)
|
|
50
|
+
"""
|
|
51
|
+
# Generate process ID
|
|
52
|
+
process_id = f"{tool_name}_{uuid.uuid4().hex[:8]}"
|
|
53
|
+
|
|
54
|
+
# Create log file
|
|
55
|
+
log_file = self.process_manager.create_log_file(process_id)
|
|
56
|
+
|
|
57
|
+
# Start the process
|
|
58
|
+
process = await asyncio.create_subprocess_exec(
|
|
59
|
+
*cmd_args,
|
|
60
|
+
stdout=asyncio.subprocess.PIPE,
|
|
61
|
+
stderr=asyncio.subprocess.STDOUT,
|
|
62
|
+
cwd=cwd,
|
|
63
|
+
env=env,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Track in process manager
|
|
67
|
+
self.process_manager.add_process(process_id, process, str(log_file))
|
|
68
|
+
|
|
69
|
+
# Try to wait for completion with timeout
|
|
70
|
+
start_time = time.time()
|
|
71
|
+
output_lines = []
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
# Create tasks for reading output and waiting for process
|
|
75
|
+
async def read_output():
|
|
76
|
+
"""Read output from process."""
|
|
77
|
+
if process.stdout:
|
|
78
|
+
async for line in process.stdout:
|
|
79
|
+
line_str = line.decode('utf-8', errors='replace')
|
|
80
|
+
output_lines.append(line_str)
|
|
81
|
+
# Also write to log file
|
|
82
|
+
with open(log_file, 'a') as f:
|
|
83
|
+
f.write(line_str)
|
|
84
|
+
|
|
85
|
+
async def wait_for_process():
|
|
86
|
+
"""Wait for process to complete."""
|
|
87
|
+
return await process.wait()
|
|
88
|
+
|
|
89
|
+
# Run both tasks with timeout
|
|
90
|
+
read_task = asyncio.create_task(read_output())
|
|
91
|
+
wait_task = asyncio.create_task(wait_for_process())
|
|
92
|
+
|
|
93
|
+
# Wait for either timeout or completion
|
|
94
|
+
done, pending = await asyncio.wait(
|
|
95
|
+
[read_task, wait_task],
|
|
96
|
+
timeout=self.timeout,
|
|
97
|
+
return_when=asyncio.FIRST_COMPLETED
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Check if process completed
|
|
101
|
+
if wait_task in done:
|
|
102
|
+
# Process completed within timeout
|
|
103
|
+
return_code = await wait_task
|
|
104
|
+
await read_task # Ensure all output is read
|
|
105
|
+
|
|
106
|
+
# Mark process as completed
|
|
107
|
+
self.process_manager.mark_completed(process_id, return_code)
|
|
108
|
+
|
|
109
|
+
output = ''.join(output_lines)
|
|
110
|
+
if return_code != 0:
|
|
111
|
+
return f"Command failed with exit code {return_code}:\n{output}", False, None
|
|
112
|
+
else:
|
|
113
|
+
return output, False, None
|
|
114
|
+
|
|
115
|
+
else:
|
|
116
|
+
# Timeout reached - background the process
|
|
117
|
+
# Cancel the tasks we were waiting on
|
|
118
|
+
for task in pending:
|
|
119
|
+
task.cancel()
|
|
120
|
+
|
|
121
|
+
# Continue reading output in background
|
|
122
|
+
asyncio.create_task(self._background_reader(process, process_id, log_file))
|
|
123
|
+
|
|
124
|
+
# Return status message
|
|
125
|
+
elapsed = time.time() - start_time
|
|
126
|
+
partial_output = ''.join(output_lines[-50:]) # Last 50 lines
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
f"Process automatically backgrounded after {elapsed:.1f}s\n"
|
|
130
|
+
f"Process ID: {process_id}\n"
|
|
131
|
+
f"Log file: {log_file}\n\n"
|
|
132
|
+
f"Use 'process --action logs --id {process_id}' to view full output\n"
|
|
133
|
+
f"Use 'process --action kill --id {process_id}' to stop the process\n\n"
|
|
134
|
+
f"=== Last output ===\n{partial_output}",
|
|
135
|
+
True,
|
|
136
|
+
process_id
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
except Exception as e:
|
|
140
|
+
# Handle errors
|
|
141
|
+
self.process_manager.mark_completed(process_id, -1)
|
|
142
|
+
return f"Error executing command: {str(e)}", False, None
|
|
143
|
+
|
|
144
|
+
async def _background_reader(self, process, process_id: str, log_file: Path):
|
|
145
|
+
"""Continue reading output from a backgrounded process.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
process: The subprocess
|
|
149
|
+
process_id: Process identifier
|
|
150
|
+
log_file: Log file path
|
|
151
|
+
"""
|
|
152
|
+
try:
|
|
153
|
+
# Continue reading output
|
|
154
|
+
if process.stdout:
|
|
155
|
+
async for line in process.stdout:
|
|
156
|
+
with open(log_file, 'a') as f:
|
|
157
|
+
f.write(line.decode('utf-8', errors='replace'))
|
|
158
|
+
|
|
159
|
+
# Wait for process to complete
|
|
160
|
+
return_code = await process.wait()
|
|
161
|
+
|
|
162
|
+
# Mark as completed
|
|
163
|
+
self.process_manager.mark_completed(process_id, return_code)
|
|
164
|
+
|
|
165
|
+
# Add completion marker to log
|
|
166
|
+
with open(log_file, 'a') as f:
|
|
167
|
+
f.write(f"\n\n=== Process completed with exit code {return_code} ===\n")
|
|
168
|
+
|
|
169
|
+
except Exception as e:
|
|
170
|
+
# Log error
|
|
171
|
+
with open(log_file, 'a') as f:
|
|
172
|
+
f.write(f"\n\n=== Background reader error: {str(e)} ===\n")
|
|
173
|
+
|
|
174
|
+
self.process_manager.mark_completed(process_id, -1)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def format_auto_background_message(
|
|
178
|
+
process_id: str,
|
|
179
|
+
elapsed_time: float,
|
|
180
|
+
log_file: str,
|
|
181
|
+
partial_output: str = "",
|
|
182
|
+
) -> str:
|
|
183
|
+
"""Format a user-friendly message for auto-backgrounded processes.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
process_id: Process identifier
|
|
187
|
+
elapsed_time: Time elapsed before backgrounding
|
|
188
|
+
log_file: Path to log file
|
|
189
|
+
partial_output: Partial output to show
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
Formatted message
|
|
193
|
+
"""
|
|
194
|
+
return (
|
|
195
|
+
f"🔄 Process automatically backgrounded after {elapsed_time:.1f}s\n\n"
|
|
196
|
+
f"📋 Process ID: {process_id}\n"
|
|
197
|
+
f"📄 Log file: {log_file}\n\n"
|
|
198
|
+
f"Commands:\n"
|
|
199
|
+
f" • View logs: process --action logs --id {process_id}\n"
|
|
200
|
+
f" • Check status: process\n"
|
|
201
|
+
f" • Stop process: process --action kill --id {process_id}\n"
|
|
202
|
+
f"{f'\\n=== Recent output ===\\n{partial_output}' if partial_output else ''}"
|
|
203
|
+
)
|
|
@@ -1,25 +1,28 @@
|
|
|
1
1
|
"""Base classes for process execution tools."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import os
|
|
4
5
|
import subprocess
|
|
5
6
|
import tempfile
|
|
7
|
+
import time
|
|
6
8
|
import uuid
|
|
7
9
|
from abc import abstractmethod
|
|
8
10
|
from pathlib import Path
|
|
9
|
-
from typing import Any, Dict, List, Optional, override
|
|
11
|
+
from typing import Any, Dict, List, Optional, Tuple, override
|
|
10
12
|
|
|
11
13
|
from mcp.server.fastmcp import Context as MCPContext
|
|
12
14
|
|
|
13
15
|
from hanzo_mcp.tools.common.base import BaseTool
|
|
14
16
|
from hanzo_mcp.tools.common.permissions import PermissionManager
|
|
17
|
+
# Import moved to __init__ to avoid circular import
|
|
15
18
|
|
|
16
19
|
|
|
17
20
|
class ProcessManager:
|
|
18
21
|
"""Singleton manager for background processes."""
|
|
19
22
|
|
|
20
23
|
_instance = None
|
|
21
|
-
_processes: Dict[str,
|
|
22
|
-
_logs: Dict[str,
|
|
24
|
+
_processes: Dict[str, Any] = {}
|
|
25
|
+
_logs: Dict[str, str] = {}
|
|
23
26
|
_log_dir = Path(tempfile.gettempdir()) / "hanzo_mcp_logs"
|
|
24
27
|
|
|
25
28
|
def __new__(cls):
|
|
@@ -28,12 +31,12 @@ class ProcessManager:
|
|
|
28
31
|
cls._instance._log_dir.mkdir(exist_ok=True)
|
|
29
32
|
return cls._instance
|
|
30
33
|
|
|
31
|
-
def add_process(self, process_id: str, process:
|
|
34
|
+
def add_process(self, process_id: str, process: Any, log_file: str) -> None:
|
|
32
35
|
"""Add a process to track."""
|
|
33
36
|
self._processes[process_id] = process
|
|
34
|
-
self._logs[process_id] =
|
|
37
|
+
self._logs[process_id] = log_file
|
|
35
38
|
|
|
36
|
-
def get_process(self, process_id: str) -> Optional[
|
|
39
|
+
def get_process(self, process_id: str) -> Optional[Any]:
|
|
37
40
|
"""Get a tracked process."""
|
|
38
41
|
return self._processes.get(process_id)
|
|
39
42
|
|
|
@@ -72,6 +75,30 @@ class ProcessManager:
|
|
|
72
75
|
def log_dir(self) -> Path:
|
|
73
76
|
"""Get the log directory."""
|
|
74
77
|
return self._log_dir
|
|
78
|
+
|
|
79
|
+
def create_log_file(self, process_id: str) -> Path:
|
|
80
|
+
"""Create a log file for a process.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
process_id: Process identifier
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Path to the created log file
|
|
87
|
+
"""
|
|
88
|
+
log_file = self._log_dir / f"{process_id}.log"
|
|
89
|
+
log_file.touch()
|
|
90
|
+
return log_file
|
|
91
|
+
|
|
92
|
+
def mark_completed(self, process_id: str, return_code: int) -> None:
|
|
93
|
+
"""Mark a process as completed with the given return code.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
process_id: Process identifier
|
|
97
|
+
return_code: Process exit code
|
|
98
|
+
"""
|
|
99
|
+
# For now, just remove from tracking
|
|
100
|
+
# In the future, we might want to keep a history
|
|
101
|
+
self.remove_process(process_id)
|
|
75
102
|
|
|
76
103
|
|
|
77
104
|
class BaseProcessTool(BaseTool):
|
|
@@ -86,6 +113,9 @@ class BaseProcessTool(BaseTool):
|
|
|
86
113
|
super().__init__()
|
|
87
114
|
self.permission_manager = permission_manager
|
|
88
115
|
self.process_manager = ProcessManager()
|
|
116
|
+
# Import here to avoid circular import
|
|
117
|
+
from hanzo_mcp.tools.shell.auto_background import AutoBackgroundExecutor
|
|
118
|
+
self.auto_background_executor = AutoBackgroundExecutor(self.process_manager)
|
|
89
119
|
|
|
90
120
|
@abstractmethod
|
|
91
121
|
def get_command_args(self, command: str, **kwargs) -> List[str]:
|
|
@@ -113,17 +143,17 @@ class BaseProcessTool(BaseTool):
|
|
|
113
143
|
timeout: Optional[int] = None,
|
|
114
144
|
**kwargs
|
|
115
145
|
) -> str:
|
|
116
|
-
"""Execute a command
|
|
146
|
+
"""Execute a command with auto-backgrounding after 2 minutes.
|
|
117
147
|
|
|
118
148
|
Args:
|
|
119
149
|
command: Command to execute
|
|
120
150
|
cwd: Working directory
|
|
121
151
|
env: Environment variables
|
|
122
|
-
timeout: Timeout in seconds
|
|
152
|
+
timeout: Timeout in seconds (ignored - auto-backgrounds after 2 minutes)
|
|
123
153
|
**kwargs: Additional tool-specific arguments
|
|
124
154
|
|
|
125
155
|
Returns:
|
|
126
|
-
Command output
|
|
156
|
+
Command output or background status
|
|
127
157
|
|
|
128
158
|
Raises:
|
|
129
159
|
RuntimeError: If command fails
|
|
@@ -141,24 +171,20 @@ class BaseProcessTool(BaseTool):
|
|
|
141
171
|
if env:
|
|
142
172
|
process_env.update(env)
|
|
143
173
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
error_msg = f"{self.get_tool_name()} command failed with exit code {e.returncode}"
|
|
159
|
-
if e.stderr:
|
|
160
|
-
error_msg += f"\nError: {e.stderr}"
|
|
161
|
-
raise RuntimeError(error_msg)
|
|
174
|
+
# Execute with auto-backgrounding
|
|
175
|
+
output, was_backgrounded, process_id = await self.auto_background_executor.execute_with_auto_background(
|
|
176
|
+
cmd_args=cmd_args,
|
|
177
|
+
tool_name=self.get_tool_name(),
|
|
178
|
+
cwd=cwd,
|
|
179
|
+
env=process_env
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
if was_backgrounded:
|
|
183
|
+
return output
|
|
184
|
+
else:
|
|
185
|
+
if output.startswith("Command failed"):
|
|
186
|
+
raise RuntimeError(output)
|
|
187
|
+
return output
|
|
162
188
|
|
|
163
189
|
async def execute_background(
|
|
164
190
|
self,
|
|
@@ -24,7 +24,6 @@ class BashTool(BaseScriptTool):
|
|
|
24
24
|
async def bash(
|
|
25
25
|
ctx: MCPContext,
|
|
26
26
|
command: str,
|
|
27
|
-
action: str = "run",
|
|
28
27
|
cwd: Optional[str] = None,
|
|
29
28
|
env: Optional[dict[str, str]] = None,
|
|
30
29
|
timeout: Optional[int] = None
|
|
@@ -32,7 +31,6 @@ class BashTool(BaseScriptTool):
|
|
|
32
31
|
return await tool_self.run(
|
|
33
32
|
ctx,
|
|
34
33
|
command=command,
|
|
35
|
-
action=action,
|
|
36
34
|
cwd=cwd,
|
|
37
35
|
env=env,
|
|
38
36
|
timeout=timeout
|
|
@@ -43,7 +41,6 @@ class BashTool(BaseScriptTool):
|
|
|
43
41
|
return await self.run(
|
|
44
42
|
ctx,
|
|
45
43
|
command=params["command"],
|
|
46
|
-
action=params.get("action", "run"),
|
|
47
44
|
cwd=params.get("cwd"),
|
|
48
45
|
env=params.get("env"),
|
|
49
46
|
timeout=params.get("timeout")
|
|
@@ -53,13 +50,16 @@ class BashTool(BaseScriptTool):
|
|
|
53
50
|
@override
|
|
54
51
|
def description(self) -> str:
|
|
55
52
|
"""Get the tool description."""
|
|
56
|
-
return """Run shell commands
|
|
53
|
+
return """Run shell commands with automatic backgrounding for long-running processes.
|
|
54
|
+
|
|
55
|
+
Commands that run for more than 2 minutes will automatically continue in the background.
|
|
56
|
+
You can check their status and logs using the 'process' tool.
|
|
57
57
|
|
|
58
58
|
Usage:
|
|
59
59
|
bash "ls -la"
|
|
60
|
-
bash
|
|
60
|
+
bash "python server.py" # Auto-backgrounds after 2 minutes
|
|
61
61
|
bash "git status && git diff"
|
|
62
|
-
bash
|
|
62
|
+
bash "npm run dev" --cwd ./frontend # Auto-backgrounds if needed"""
|
|
63
63
|
|
|
64
64
|
@override
|
|
65
65
|
def get_interpreter(self) -> str:
|
|
@@ -110,49 +110,33 @@ bash --action background "npm run dev" --cwd ./frontend"""
|
|
|
110
110
|
self,
|
|
111
111
|
ctx: MCPContext,
|
|
112
112
|
command: str,
|
|
113
|
-
action: str = "run",
|
|
114
113
|
cwd: Optional[str] = None,
|
|
115
114
|
env: Optional[dict[str, str]] = None,
|
|
116
115
|
timeout: Optional[int] = None,
|
|
117
116
|
) -> str:
|
|
118
|
-
"""Run a shell command.
|
|
117
|
+
"""Run a shell command with auto-backgrounding.
|
|
119
118
|
|
|
120
119
|
Args:
|
|
121
120
|
ctx: MCP context
|
|
122
121
|
command: Shell command to execute
|
|
123
|
-
action: Action to perform (run, background)
|
|
124
122
|
cwd: Working directory
|
|
125
123
|
env: Environment variables
|
|
126
|
-
timeout: Command timeout in seconds
|
|
124
|
+
timeout: Command timeout in seconds (ignored - auto-backgrounds after 2 minutes)
|
|
127
125
|
|
|
128
126
|
Returns:
|
|
129
|
-
Command output or
|
|
127
|
+
Command output or background status
|
|
130
128
|
"""
|
|
131
129
|
# Prepare working directory
|
|
132
130
|
work_dir = Path(cwd).resolve() if cwd else Path.cwd()
|
|
133
131
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
f"Process ID: {result['process_id']}\n"
|
|
143
|
-
f"PID: {result['pid']}\n"
|
|
144
|
-
f"Log file: {result['log_file']}\n"
|
|
145
|
-
f"Command: {command}"
|
|
146
|
-
)
|
|
147
|
-
else:
|
|
148
|
-
# Default to sync execution
|
|
149
|
-
output = await self.execute_sync(
|
|
150
|
-
command,
|
|
151
|
-
cwd=work_dir,
|
|
152
|
-
env=env,
|
|
153
|
-
timeout=timeout or 120 # Default 2 minute timeout
|
|
154
|
-
)
|
|
155
|
-
return output if output else "Command completed successfully (no output)"
|
|
132
|
+
# Always use execute_sync which now has auto-backgrounding
|
|
133
|
+
output = await self.execute_sync(
|
|
134
|
+
command,
|
|
135
|
+
cwd=work_dir,
|
|
136
|
+
env=env,
|
|
137
|
+
timeout=timeout
|
|
138
|
+
)
|
|
139
|
+
return output if output else "Command completed successfully (no output)"
|
|
156
140
|
|
|
157
141
|
|
|
158
142
|
# Create tool instance
|
|
@@ -18,13 +18,15 @@ class NpxTool(BaseBinaryTool):
|
|
|
18
18
|
@override
|
|
19
19
|
def description(self) -> str:
|
|
20
20
|
"""Get the tool description."""
|
|
21
|
-
return """Run npx packages
|
|
21
|
+
return """Run npx packages with automatic backgrounding for long-running processes.
|
|
22
|
+
|
|
23
|
+
Commands that run for more than 2 minutes will automatically continue in the background.
|
|
22
24
|
|
|
23
25
|
Usage:
|
|
24
26
|
npx create-react-app my-app
|
|
25
|
-
npx
|
|
27
|
+
npx http-server -p 8080 # Auto-backgrounds after 2 minutes
|
|
26
28
|
npx prettier --write "**/*.js"
|
|
27
|
-
npx
|
|
29
|
+
npx json-server db.json # Auto-backgrounds if needed"""
|
|
28
30
|
|
|
29
31
|
@override
|
|
30
32
|
def get_binary_name(self) -> str:
|
|
@@ -37,22 +39,20 @@ npx --action background json-server db.json"""
|
|
|
37
39
|
ctx: MCPContext,
|
|
38
40
|
package: str,
|
|
39
41
|
args: str = "",
|
|
40
|
-
action: str = "run",
|
|
41
42
|
cwd: Optional[str] = None,
|
|
42
43
|
yes: bool = True,
|
|
43
44
|
) -> str:
|
|
44
|
-
"""Run an npx command.
|
|
45
|
+
"""Run an npx command with auto-backgrounding.
|
|
45
46
|
|
|
46
47
|
Args:
|
|
47
48
|
ctx: MCP context
|
|
48
49
|
package: NPX package to run
|
|
49
50
|
args: Additional arguments
|
|
50
|
-
action: Action to perform (run, background)
|
|
51
51
|
cwd: Working directory
|
|
52
52
|
yes: Auto-confirm package installation
|
|
53
53
|
|
|
54
54
|
Returns:
|
|
55
|
-
Command output or
|
|
55
|
+
Command output or background status
|
|
56
56
|
"""
|
|
57
57
|
# Prepare working directory
|
|
58
58
|
work_dir = Path(cwd).resolve() if cwd else Path.cwd()
|
|
@@ -65,28 +65,14 @@ npx --action background json-server db.json"""
|
|
|
65
65
|
# Build full command
|
|
66
66
|
full_args = args.split() if args else []
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
f"Started npx process in background\n"
|
|
77
|
-
f"Process ID: {result['process_id']}\n"
|
|
78
|
-
f"PID: {result['pid']}\n"
|
|
79
|
-
f"Log file: {result['log_file']}"
|
|
80
|
-
)
|
|
81
|
-
else:
|
|
82
|
-
# Default to sync execution
|
|
83
|
-
return await self.execute_sync(
|
|
84
|
-
package,
|
|
85
|
-
cwd=work_dir,
|
|
86
|
-
flags=flags,
|
|
87
|
-
args=full_args,
|
|
88
|
-
timeout=300 # 5 minute timeout for npx
|
|
89
|
-
)
|
|
68
|
+
# Always use execute_sync which now has auto-backgrounding
|
|
69
|
+
return await self.execute_sync(
|
|
70
|
+
package,
|
|
71
|
+
cwd=work_dir,
|
|
72
|
+
flags=flags,
|
|
73
|
+
args=full_args,
|
|
74
|
+
timeout=None # Let auto-backgrounding handle timeout
|
|
75
|
+
)
|
|
90
76
|
|
|
91
77
|
def register(self, server: FastMCP) -> None:
|
|
92
78
|
"""Register the tool with the MCP server."""
|
|
@@ -97,7 +83,6 @@ npx --action background json-server db.json"""
|
|
|
97
83
|
ctx: MCPContext,
|
|
98
84
|
package: str,
|
|
99
85
|
args: str = "",
|
|
100
|
-
action: str = "run",
|
|
101
86
|
cwd: Optional[str] = None,
|
|
102
87
|
yes: bool = True
|
|
103
88
|
) -> str:
|
|
@@ -105,7 +90,6 @@ npx --action background json-server db.json"""
|
|
|
105
90
|
ctx,
|
|
106
91
|
package=package,
|
|
107
92
|
args=args,
|
|
108
|
-
action=action,
|
|
109
93
|
cwd=cwd,
|
|
110
94
|
yes=yes
|
|
111
95
|
)
|
|
@@ -116,7 +100,6 @@ npx --action background json-server db.json"""
|
|
|
116
100
|
ctx,
|
|
117
101
|
package=params["package"],
|
|
118
102
|
args=params.get("args", ""),
|
|
119
|
-
action=params.get("action", "run"),
|
|
120
103
|
cwd=params.get("cwd"),
|
|
121
104
|
yes=params.get("yes", True)
|
|
122
105
|
)
|