hanzo-mcp 0.6.13__py3-none-any.whl → 0.7.0__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (62) hide show
  1. hanzo_mcp/analytics/__init__.py +5 -0
  2. hanzo_mcp/analytics/posthog_analytics.py +364 -0
  3. hanzo_mcp/cli.py +3 -3
  4. hanzo_mcp/cli_enhanced.py +3 -3
  5. hanzo_mcp/config/settings.py +1 -1
  6. hanzo_mcp/config/tool_config.py +18 -4
  7. hanzo_mcp/server.py +34 -1
  8. hanzo_mcp/tools/__init__.py +65 -2
  9. hanzo_mcp/tools/agent/__init__.py +84 -3
  10. hanzo_mcp/tools/agent/agent_tool.py +102 -4
  11. hanzo_mcp/tools/agent/agent_tool_v2.py +459 -0
  12. hanzo_mcp/tools/agent/clarification_protocol.py +220 -0
  13. hanzo_mcp/tools/agent/clarification_tool.py +68 -0
  14. hanzo_mcp/tools/agent/claude_cli_tool.py +125 -0
  15. hanzo_mcp/tools/agent/claude_desktop_auth.py +508 -0
  16. hanzo_mcp/tools/agent/cli_agent_base.py +191 -0
  17. hanzo_mcp/tools/agent/code_auth.py +436 -0
  18. hanzo_mcp/tools/agent/code_auth_tool.py +194 -0
  19. hanzo_mcp/tools/agent/codex_cli_tool.py +123 -0
  20. hanzo_mcp/tools/agent/critic_tool.py +376 -0
  21. hanzo_mcp/tools/agent/gemini_cli_tool.py +128 -0
  22. hanzo_mcp/tools/agent/grok_cli_tool.py +128 -0
  23. hanzo_mcp/tools/agent/iching_tool.py +380 -0
  24. hanzo_mcp/tools/agent/network_tool.py +273 -0
  25. hanzo_mcp/tools/agent/prompt.py +62 -20
  26. hanzo_mcp/tools/agent/review_tool.py +433 -0
  27. hanzo_mcp/tools/agent/swarm_tool.py +535 -0
  28. hanzo_mcp/tools/agent/swarm_tool_v2.py +594 -0
  29. hanzo_mcp/tools/common/base.py +1 -0
  30. hanzo_mcp/tools/common/batch_tool.py +102 -10
  31. hanzo_mcp/tools/common/fastmcp_pagination.py +369 -0
  32. hanzo_mcp/tools/common/forgiving_edit.py +243 -0
  33. hanzo_mcp/tools/common/paginated_base.py +230 -0
  34. hanzo_mcp/tools/common/paginated_response.py +307 -0
  35. hanzo_mcp/tools/common/pagination.py +226 -0
  36. hanzo_mcp/tools/common/tool_list.py +3 -0
  37. hanzo_mcp/tools/common/truncate.py +101 -0
  38. hanzo_mcp/tools/filesystem/__init__.py +29 -0
  39. hanzo_mcp/tools/filesystem/ast_multi_edit.py +562 -0
  40. hanzo_mcp/tools/filesystem/directory_tree_paginated.py +338 -0
  41. hanzo_mcp/tools/lsp/__init__.py +5 -0
  42. hanzo_mcp/tools/lsp/lsp_tool.py +512 -0
  43. hanzo_mcp/tools/memory/__init__.py +76 -0
  44. hanzo_mcp/tools/memory/knowledge_tools.py +518 -0
  45. hanzo_mcp/tools/memory/memory_tools.py +456 -0
  46. hanzo_mcp/tools/search/__init__.py +6 -0
  47. hanzo_mcp/tools/search/find_tool.py +581 -0
  48. hanzo_mcp/tools/search/unified_search.py +953 -0
  49. hanzo_mcp/tools/shell/__init__.py +5 -0
  50. hanzo_mcp/tools/shell/auto_background.py +203 -0
  51. hanzo_mcp/tools/shell/base_process.py +53 -27
  52. hanzo_mcp/tools/shell/bash_tool.py +17 -33
  53. hanzo_mcp/tools/shell/npx_tool.py +15 -32
  54. hanzo_mcp/tools/shell/streaming_command.py +594 -0
  55. hanzo_mcp/tools/shell/uvx_tool.py +15 -32
  56. hanzo_mcp/types.py +23 -0
  57. {hanzo_mcp-0.6.13.dist-info → hanzo_mcp-0.7.0.dist-info}/METADATA +228 -71
  58. {hanzo_mcp-0.6.13.dist-info → hanzo_mcp-0.7.0.dist-info}/RECORD +61 -24
  59. hanzo_mcp-0.6.13.dist-info/licenses/LICENSE +0 -21
  60. {hanzo_mcp-0.6.13.dist-info → hanzo_mcp-0.7.0.dist-info}/WHEEL +0 -0
  61. {hanzo_mcp-0.6.13.dist-info → hanzo_mcp-0.7.0.dist-info}/entry_points.txt +0 -0
  62. {hanzo_mcp-0.6.13.dist-info → hanzo_mcp-0.7.0.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, subprocess.Popen] = {}
22
- _logs: Dict[str, List[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: subprocess.Popen, log_file: Path) -> None:
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] = str(log_file)
37
+ self._logs[process_id] = log_file
35
38
 
36
- def get_process(self, process_id: str) -> Optional[subprocess.Popen]:
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 synchronously and return output.
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
- try:
145
- result = subprocess.run(
146
- cmd_args,
147
- cwd=cwd,
148
- env=process_env,
149
- capture_output=True,
150
- text=True,
151
- timeout=timeout,
152
- check=True
153
- )
154
- return result.stdout
155
- except subprocess.TimeoutExpired:
156
- raise RuntimeError(f"{self.get_tool_name()} command timed out after {timeout} seconds")
157
- except subprocess.CalledProcessError as e:
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. Actions: run (default), background.
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 --action background "python server.py"
60
+ bash "python server.py" # Auto-backgrounds after 2 minutes
61
61
  bash "git status && git diff"
62
- bash --action background "npm run dev" --cwd ./frontend"""
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 process info
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
- if action == "background":
135
- result = await self.execute_background(
136
- command,
137
- cwd=work_dir,
138
- env=env
139
- )
140
- return (
141
- f"Started command in background\n"
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. Actions: run (default), background.
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 --action background http-server -p 8080
27
+ npx http-server -p 8080 # Auto-backgrounds after 2 minutes
26
28
  npx prettier --write "**/*.js"
27
- npx --action background json-server db.json"""
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 process info
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
- if action == "background":
69
- result = await self.execute_background(
70
- package,
71
- cwd=work_dir,
72
- flags=flags,
73
- args=full_args
74
- )
75
- return (
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
  )