hanzo-mcp 0.1.25__py3-none-any.whl → 0.1.30__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 (48) hide show
  1. hanzo_mcp/__init__.py +2 -2
  2. hanzo_mcp/cli.py +80 -9
  3. hanzo_mcp/server.py +41 -10
  4. hanzo_mcp/tools/__init__.py +51 -32
  5. hanzo_mcp/tools/agent/__init__.py +59 -0
  6. hanzo_mcp/tools/agent/agent_tool.py +474 -0
  7. hanzo_mcp/tools/agent/prompt.py +137 -0
  8. hanzo_mcp/tools/agent/tool_adapter.py +75 -0
  9. hanzo_mcp/tools/common/__init__.py +17 -0
  10. hanzo_mcp/tools/common/base.py +216 -0
  11. hanzo_mcp/tools/common/context.py +7 -3
  12. hanzo_mcp/tools/common/permissions.py +63 -119
  13. hanzo_mcp/tools/common/session.py +91 -0
  14. hanzo_mcp/tools/common/thinking_tool.py +123 -0
  15. hanzo_mcp/tools/filesystem/__init__.py +85 -5
  16. hanzo_mcp/tools/filesystem/base.py +113 -0
  17. hanzo_mcp/tools/filesystem/content_replace.py +287 -0
  18. hanzo_mcp/tools/filesystem/directory_tree.py +286 -0
  19. hanzo_mcp/tools/filesystem/edit_file.py +287 -0
  20. hanzo_mcp/tools/filesystem/get_file_info.py +170 -0
  21. hanzo_mcp/tools/filesystem/read_files.py +198 -0
  22. hanzo_mcp/tools/filesystem/search_content.py +275 -0
  23. hanzo_mcp/tools/filesystem/write_file.py +162 -0
  24. hanzo_mcp/tools/jupyter/__init__.py +67 -4
  25. hanzo_mcp/tools/jupyter/base.py +284 -0
  26. hanzo_mcp/tools/jupyter/edit_notebook.py +295 -0
  27. hanzo_mcp/tools/jupyter/notebook_operations.py +72 -112
  28. hanzo_mcp/tools/jupyter/read_notebook.py +165 -0
  29. hanzo_mcp/tools/project/__init__.py +64 -1
  30. hanzo_mcp/tools/project/analysis.py +9 -6
  31. hanzo_mcp/tools/project/base.py +66 -0
  32. hanzo_mcp/tools/project/project_analyze.py +173 -0
  33. hanzo_mcp/tools/shell/__init__.py +58 -1
  34. hanzo_mcp/tools/shell/base.py +148 -0
  35. hanzo_mcp/tools/shell/command_executor.py +203 -322
  36. hanzo_mcp/tools/shell/run_command.py +204 -0
  37. hanzo_mcp/tools/shell/run_script.py +215 -0
  38. hanzo_mcp/tools/shell/script_tool.py +244 -0
  39. {hanzo_mcp-0.1.25.dist-info → hanzo_mcp-0.1.30.dist-info}/METADATA +72 -77
  40. hanzo_mcp-0.1.30.dist-info/RECORD +45 -0
  41. {hanzo_mcp-0.1.25.dist-info → hanzo_mcp-0.1.30.dist-info}/licenses/LICENSE +2 -2
  42. hanzo_mcp/tools/common/thinking.py +0 -65
  43. hanzo_mcp/tools/filesystem/file_operations.py +0 -1050
  44. hanzo_mcp-0.1.25.dist-info/RECORD +0 -24
  45. hanzo_mcp-0.1.25.dist-info/zip-safe +0 -1
  46. {hanzo_mcp-0.1.25.dist-info → hanzo_mcp-0.1.30.dist-info}/WHEEL +0 -0
  47. {hanzo_mcp-0.1.25.dist-info → hanzo_mcp-0.1.30.dist-info}/entry_points.txt +0 -0
  48. {hanzo_mcp-0.1.25.dist-info → hanzo_mcp-0.1.30.dist-info}/top_level.txt +0 -0
@@ -11,77 +11,16 @@ import shlex
11
11
  import sys
12
12
  import tempfile
13
13
  from collections.abc import Awaitable, Callable
14
- from typing import final
14
+ from pathlib import Path
15
+ from typing import Dict, Optional, final
15
16
 
16
17
  from mcp.server.fastmcp import Context as MCPContext
17
18
  from mcp.server.fastmcp import FastMCP
18
19
 
19
20
  from hanzo_mcp.tools.common.context import create_tool_context
20
21
  from hanzo_mcp.tools.common.permissions import PermissionManager
21
-
22
-
23
- @final
24
- class CommandResult:
25
- """Represents the result of a command execution."""
26
-
27
- def __init__(
28
- self,
29
- return_code: int = 0,
30
- stdout: str = "",
31
- stderr: str = "",
32
- error_message: str | None = None,
33
- ):
34
- """Initialize a command result.
35
-
36
- Args:
37
- return_code: The command's return code (0 for success)
38
- stdout: Standard output from the command
39
- stderr: Standard error from the command
40
- error_message: Optional error message for failure cases
41
- """
42
- self.return_code: int = return_code
43
- self.stdout: str = stdout
44
- self.stderr: str = stderr
45
- self.error_message: str | None = error_message
46
-
47
- @property
48
- def is_success(self) -> bool:
49
- """Check if the command executed successfully.
50
-
51
- Returns:
52
- True if the command succeeded, False otherwise
53
- """
54
- return self.return_code == 0
55
-
56
- def format_output(self, include_exit_code: bool = True) -> str:
57
- """Format the command output as a string.
58
-
59
- Args:
60
- include_exit_code: Whether to include the exit code in the output
61
-
62
- Returns:
63
- Formatted output string
64
- """
65
- result_parts: list[str] = []
66
-
67
- # Add error message if present
68
- if self.error_message:
69
- result_parts.append(f"Error: {self.error_message}")
70
-
71
- # Add exit code if requested and not zero (for non-errors)
72
- if include_exit_code and (self.return_code != 0 or not self.error_message):
73
- result_parts.append(f"Exit code: {self.return_code}")
74
-
75
- # Add stdout if present
76
- if self.stdout:
77
- result_parts.append(f"STDOUT:\n{self.stdout}")
78
-
79
- # Add stderr if present
80
- if self.stderr:
81
- result_parts.append(f"STDERR:\n{self.stderr}")
82
-
83
- # Join with newlines
84
- return "\n\n".join(result_parts)
22
+ from hanzo_mcp.tools.common.session import SessionManager
23
+ from hanzo_mcp.tools.shell.base import CommandResult
85
24
 
86
25
 
87
26
  @final
@@ -104,14 +43,17 @@ class CommandExecutor:
104
43
  self.permission_manager: PermissionManager = permission_manager
105
44
  self.verbose: bool = verbose
106
45
 
46
+ # Session management (initialized on first use with session ID)
47
+ self.session_manager: Optional[SessionManager] = None
48
+
107
49
  # Excluded commands or patterns
108
50
  self.excluded_commands: list[str] = ["rm"]
109
51
 
110
52
  # Map of supported interpreters with special handling
111
- self.special_interpreters: dict[
53
+ self.special_interpreters: Dict[
112
54
  str,
113
55
  Callable[
114
- [str, str, str | None, dict[str, str] | None, float | None],
56
+ [str, str, str], dict[str, str]], Optional[float | None | None,
115
57
  Awaitable[CommandResult],
116
58
  ],
117
59
  ] = {
@@ -136,7 +78,78 @@ class CommandExecutor:
136
78
  if command not in self.excluded_commands:
137
79
  self.excluded_commands.append(command)
138
80
 
139
- def _log(self, message: str, data: object = None) -> None:
81
+ def _get_preferred_shell(self) -> str:
82
+ """Get the best available shell for the current environment.
83
+
84
+ Tries to find the best shell in this order:
85
+ 1. User's SHELL environment variable
86
+ 2. zsh
87
+ 3. bash
88
+ 4. fish
89
+ 5. sh (fallback)
90
+
91
+ Returns:
92
+ Path to the preferred shell
93
+ """
94
+ # First check the SHELL environment variable
95
+ user_shell = os.environ.get("SHELL")
96
+ if user_shell and os.path.exists(user_shell):
97
+ return user_shell
98
+
99
+ # Try common shells in order of preference
100
+ shell_paths = [
101
+ "/bin/zsh",
102
+ "/usr/bin/zsh",
103
+ "/bin/bash",
104
+ "/usr/bin/bash",
105
+ "/bin/fish",
106
+ "/usr/bin/fish",
107
+ "/bin/sh",
108
+ "/usr/bin/sh",
109
+ ]
110
+
111
+ for shell_path in shell_paths:
112
+ if os.path.exists(shell_path) and os.access(shell_path, os.X_OK):
113
+ return shell_path
114
+
115
+ # Fallback to /bin/sh which should exist on most systems
116
+ return "/bin/sh"
117
+
118
+ def get_session_manager(self, session_id: str) -> SessionManager:
119
+ """Get the session manager for the given session ID.
120
+
121
+ Args:
122
+ session_id: The session ID
123
+
124
+ Returns:
125
+ The session manager instance
126
+ """
127
+ return SessionManager.get_instance(session_id)
128
+
129
+ def set_working_dir(self, session_id: str, path: str) -> None:
130
+ """Set the current working directory for the session.
131
+
132
+ Args:
133
+ session_id: The session ID
134
+ path: The path to set as the current working directory
135
+ """
136
+ session = self.get_session_manager(session_id)
137
+ expanded_path = os.path.expanduser(path)
138
+ session.set_working_dir(Path(expanded_path))
139
+
140
+ def get_working_dir(self, session_id: str) -> str:
141
+ """Get the current working directory for the session.
142
+
143
+ Args:
144
+ session_id: The session ID
145
+
146
+ Returns:
147
+ The current working directory
148
+ """
149
+ session = self.get_session_manager(session_id)
150
+ return str(session.current_working_dir)
151
+
152
+ def _log(self, message: str, data: object | None = None) -> None:
140
153
  """Log a message if verbose logging is enabled.
141
154
 
142
155
  Args:
@@ -193,17 +206,19 @@ class CommandExecutor:
193
206
  command: str,
194
207
  cwd: str | None = None,
195
208
  env: dict[str, str] | None = None,
196
- timeout: float | None = 600.0,
209
+ timeout: float | None = 60.0,
197
210
  use_login_shell: bool = True,
211
+ session_id: str | None = None,
198
212
  ) -> CommandResult:
199
213
  """Execute a shell command with safety checks.
200
214
 
201
215
  Args:
202
216
  command: The command to execute
203
- cwd: Optional working directory
217
+ cwd: Optional working directory (if None, uses session's current working directory)
204
218
  env: Optional environment variables
205
219
  timeout: Optional timeout in seconds
206
220
  use_login_shell: Whether to use login shell. default true (loads ~/.zshrc, ~/.bashrc, etc.)
221
+ session_id: Optional session ID for persistent working directory
207
222
 
208
223
  Returns:
209
224
  CommandResult containing execution results
@@ -216,17 +231,50 @@ class CommandExecutor:
216
231
  return_code=1, error_message=f"Command not allowed: {command}"
217
232
  )
218
233
 
234
+ # Use session working directory if no cwd specified and session_id provided
235
+ effective_cwd = cwd
236
+ if session_id and not cwd:
237
+ effective_cwd = self.get_working_dir(session_id)
238
+ self._log(f"Using session working directory: {effective_cwd}")
239
+
240
+ # Check if it's a cd command and update session working directory
241
+ is_cd_command = False
242
+ if session_id and command.strip().startswith("cd "):
243
+ is_cd_command = True
244
+ args = shlex.split(command)
245
+ if len(args) > 1:
246
+ target_dir = args[1]
247
+ # Handle relative paths
248
+ if not os.path.isabs(target_dir):
249
+ session_cwd = self.get_working_dir(session_id)
250
+ target_dir = os.path.join(session_cwd, target_dir)
251
+
252
+ # Expand user paths
253
+ target_dir = os.path.expanduser(target_dir)
254
+
255
+ # Normalize path
256
+ target_dir = os.path.normpath(target_dir)
257
+
258
+ if os.path.isdir(target_dir):
259
+ self.set_working_dir(session_id, target_dir)
260
+ self._log(f"Updated session working directory: {target_dir}")
261
+ return CommandResult(return_code=0, stdout="")
262
+ else:
263
+ return CommandResult(
264
+ return_code=1, error_message=f"Directory does not exist: {target_dir}"
265
+ )
266
+
219
267
  # Check working directory permissions if specified
220
- if cwd:
221
- if not os.path.isdir(cwd):
268
+ if effective_cwd:
269
+ if not os.path.isdir(effective_cwd):
222
270
  return CommandResult(
223
271
  return_code=1,
224
- error_message=f"Working directory does not exist: {cwd}",
272
+ error_message=f"Working directory does not exist: {effective_cwd}",
225
273
  )
226
274
 
227
- if not self.permission_manager.is_path_allowed(cwd):
275
+ if not self.permission_manager.is_path_allowed(effective_cwd):
228
276
  return CommandResult(
229
- return_code=1, error_message=f"Working directory not allowed: {cwd}"
277
+ return_code=1, error_message=f"Working directory not allowed: {effective_cwd}"
230
278
  )
231
279
 
232
280
  # Set up environment
@@ -244,8 +292,8 @@ class CommandExecutor:
244
292
  shell_cmd = command
245
293
 
246
294
  if use_login_shell:
247
- # Get the user's login shell
248
- user_shell = os.environ.get("SHELL", "/bin/bash")
295
+ # Try to find the best available shell
296
+ user_shell = self._get_preferred_shell()
249
297
  shell_basename = os.path.basename(user_shell)
250
298
 
251
299
  self._log(f"Using login shell: {user_shell}")
@@ -270,7 +318,7 @@ class CommandExecutor:
270
318
  shell_cmd,
271
319
  stdout=asyncio.subprocess.PIPE,
272
320
  stderr=asyncio.subprocess.PIPE,
273
- cwd=cwd,
321
+ cwd=effective_cwd,
274
322
  env=command_env,
275
323
  )
276
324
  else:
@@ -282,7 +330,7 @@ class CommandExecutor:
282
330
  *args,
283
331
  stdout=asyncio.subprocess.PIPE,
284
332
  stderr=asyncio.subprocess.PIPE,
285
- cwd=cwd,
333
+ cwd=effective_cwd,
286
334
  env=command_env,
287
335
  )
288
336
 
@@ -320,34 +368,43 @@ class CommandExecutor:
320
368
  interpreter: str = "bash",
321
369
  cwd: str | None = None,
322
370
  env: dict[str, str] | None = None,
323
- timeout: float | None = 600.0,
371
+ timeout: float | None = 60.0,
324
372
  use_login_shell: bool = True,
373
+ session_id: str | None = None,
325
374
  ) -> CommandResult:
326
375
  """Execute a script with the specified interpreter.
327
376
 
328
377
  Args:
329
378
  script: The script content to execute
330
379
  interpreter: The interpreter to use (bash, python, etc.)
331
- cwd: Optional working directory
380
+ cwd: Optional working directory (if None, uses session's current working directory)
332
381
  env: Optional environment variables
333
382
  timeout: Optional timeout in seconds
383
+ use_login_shell: Whether to use login shell (loads ~/.zshrc, ~/.bashrc, etc.)
384
+ session_id: Optional session ID for persistent working directory
334
385
 
335
386
  Returns:
336
387
  CommandResult containing execution results
337
388
  """
338
389
  self._log(f"Executing script with interpreter: {interpreter}")
339
390
 
391
+ # Use session working directory if no cwd specified and session_id provided
392
+ effective_cwd = cwd
393
+ if session_id and not cwd:
394
+ effective_cwd = self.get_working_dir(session_id)
395
+ self._log(f"Using session working directory: {effective_cwd}")
396
+
340
397
  # Check working directory permissions if specified
341
- if cwd:
342
- if not os.path.isdir(cwd):
398
+ if effective_cwd:
399
+ if not os.path.isdir(effective_cwd):
343
400
  return CommandResult(
344
401
  return_code=1,
345
- error_message=f"Working directory does not exist: {cwd}",
402
+ error_message=f"Working directory does not exist: {effective_cwd}",
346
403
  )
347
404
 
348
- if not self.permission_manager.is_path_allowed(cwd):
405
+ if not self.permission_manager.is_path_allowed(effective_cwd):
349
406
  return CommandResult(
350
- return_code=1, error_message=f"Working directory not allowed: {cwd}"
407
+ return_code=1, error_message=f"Working directory not allowed: {effective_cwd}"
351
408
  )
352
409
 
353
410
  # Check if we need special handling for this interpreter
@@ -355,11 +412,11 @@ class CommandExecutor:
355
412
  if interpreter_name in self.special_interpreters:
356
413
  self._log(f"Using special handler for interpreter: {interpreter_name}")
357
414
  special_handler = self.special_interpreters[interpreter_name]
358
- return await special_handler(interpreter, script, cwd, env, timeout)
415
+ return await special_handler(interpreter, script, effective_cwd, env, timeout)
359
416
 
360
417
  # Regular execution
361
418
  return await self._execute_script_with_stdin(
362
- interpreter, script, cwd, env, timeout, use_login_shell
419
+ interpreter, script, effective_cwd, env, timeout, use_login_shell
363
420
  )
364
421
 
365
422
  async def _execute_script_with_stdin(
@@ -368,7 +425,7 @@ class CommandExecutor:
368
425
  script: str,
369
426
  cwd: str | None = None,
370
427
  env: dict[str, str] | None = None,
371
- timeout: float | None = 600.0,
428
+ timeout: float | None = 60.0,
372
429
  use_login_shell: bool = True,
373
430
  ) -> CommandResult:
374
431
  """Execute a script by passing it to stdin of the interpreter.
@@ -379,6 +436,7 @@ class CommandExecutor:
379
436
  cwd: Optional working directory
380
437
  env: Optional environment variables
381
438
  timeout: Optional timeout in seconds
439
+ use_login_shell: Whether to use login shell (loads ~/.zshrc, ~/.bashrc, etc.)
382
440
 
383
441
  Returns:
384
442
  CommandResult containing execution results
@@ -391,9 +449,9 @@ class CommandExecutor:
391
449
  try:
392
450
  # Determine if we should use a login shell
393
451
  if use_login_shell:
394
- # Get the user's login shell
395
- user_shell = os.environ.get("SHELL", "/bin/bash")
396
- os.path.basename(user_shell)
452
+ # Try to find the best available shell
453
+ user_shell = self._get_preferred_shell()
454
+ shell_basename = os.path.basename(user_shell)
397
455
 
398
456
  self._log(f"Using login shell for interpreter: {user_shell}")
399
457
 
@@ -403,10 +461,9 @@ class CommandExecutor:
403
461
  # Create and run the process with shell
404
462
  process = await asyncio.create_subprocess_shell(
405
463
  shell_cmd,
406
- stdin=asyncio.subprocess.PIPE,
407
464
  stdout=asyncio.subprocess.PIPE,
408
465
  stderr=asyncio.subprocess.PIPE,
409
- cwd=cwd,
466
+ cwd=effective_cwd,
410
467
  env=command_env,
411
468
  )
412
469
  else:
@@ -419,7 +476,7 @@ class CommandExecutor:
419
476
  stdin=asyncio.subprocess.PIPE,
420
477
  stdout=asyncio.subprocess.PIPE,
421
478
  stderr=asyncio.subprocess.PIPE,
422
- cwd=cwd,
479
+ cwd=effective_cwd,
423
480
  env=command_env,
424
481
  )
425
482
 
@@ -458,7 +515,7 @@ class CommandExecutor:
458
515
  script: str,
459
516
  cwd: str | None = None,
460
517
  env: dict[str, str] | None = None,
461
- timeout: float | None = 600.0,
518
+ timeout: float | None = 60.0,
462
519
  ) -> CommandResult:
463
520
  """Special handler for Fish shell scripts.
464
521
 
@@ -533,9 +590,10 @@ class CommandExecutor:
533
590
  language: str,
534
591
  cwd: str | None = None,
535
592
  env: dict[str, str] | None = None,
536
- timeout: float | None = 600.0,
593
+ timeout: float | None = 60.0,
537
594
  args: list[str] | None = None,
538
595
  use_login_shell: bool = True,
596
+ session_id: str | None = None,
539
597
  ) -> CommandResult:
540
598
  """Execute a script by writing it to a temporary file and executing it.
541
599
 
@@ -545,16 +603,21 @@ class CommandExecutor:
545
603
  Args:
546
604
  script: The script content
547
605
  language: The script language (determines file extension and interpreter)
548
- cwd: Optional working directory
606
+ cwd: Optional working directory (if None, uses session's current working directory)
549
607
  env: Optional environment variables
550
608
  timeout: Optional timeout in seconds
551
609
  args: Optional command-line arguments
552
610
  use_login_shell: Whether to use login shell. default true (loads ~/.zshrc, ~/.bashrc, etc.)
553
-
611
+ session_id: Optional session ID for persistent working directory
554
612
 
555
613
  Returns:
556
614
  CommandResult containing execution results
557
615
  """
616
+ # Use session working directory if no cwd specified and session_id provided
617
+ effective_cwd = cwd
618
+ if session_id and not cwd:
619
+ effective_cwd = self.get_working_dir(session_id)
620
+ self._log(f"Using session working directory: {effective_cwd}")
558
621
  # Language to interpreter mapping
559
622
  language_map: dict[str, dict[str, str]] = {
560
623
  "python": {
@@ -622,9 +685,9 @@ class CommandExecutor:
622
685
  try:
623
686
  # Determine if we should use a login shell
624
687
  if use_login_shell:
625
- # Get the user's login shell
626
- user_shell = os.environ.get("SHELL", "/bin/bash")
627
- os.path.basename(user_shell)
688
+ # Try to find the best available shell
689
+ user_shell = self._get_preferred_shell()
690
+ shell_basename = os.path.basename(user_shell)
628
691
 
629
692
  self._log(f"Using login shell for script execution: {user_shell}")
630
693
 
@@ -643,7 +706,7 @@ class CommandExecutor:
643
706
  shell_cmd,
644
707
  stdout=asyncio.subprocess.PIPE,
645
708
  stderr=asyncio.subprocess.PIPE,
646
- cwd=cwd,
709
+ cwd=effective_cwd,
647
710
  env=command_env,
648
711
  )
649
712
  else:
@@ -659,7 +722,7 @@ class CommandExecutor:
659
722
  *cmd_args,
660
723
  stdout=asyncio.subprocess.PIPE,
661
724
  stderr=asyncio.subprocess.PIPE,
662
- cwd=cwd,
725
+ cwd=effective_cwd,
663
726
  env=command_env,
664
727
  )
665
728
 
@@ -717,14 +780,17 @@ class CommandExecutor:
717
780
  }
718
781
  return list(language_map.keys())
719
782
 
783
+ # Legacy method to keep backwards compatibility with tests
720
784
  def register_tools(self, mcp_server: FastMCP) -> None:
721
785
  """Register command execution tools with the MCP server.
786
+
787
+ Legacy method for backwards compatibility with existing tests.
788
+ New code should use the modular tool classes instead.
722
789
 
723
790
  Args:
724
791
  mcp_server: The FastMCP server instance
725
792
  """
726
-
727
- # Run Command Tool
793
+ # Run Command Tool - keep original method names for test compatibility
728
794
  @mcp_server.tool()
729
795
  async def run_command(
730
796
  command: str,
@@ -732,68 +798,27 @@ class CommandExecutor:
732
798
  ctx: MCPContext,
733
799
  use_login_shell: bool = True,
734
800
  ) -> str:
735
- """Execute a shell command.
736
-
737
- Args:
738
- command: The shell command to execute
739
- cwd: Working directory for the command
740
-
741
- use_login_shell: Whether to use login shell (loads ~/.zshrc, ~/.bashrc, etc.)
742
-
743
- Returns:
744
- The output of the command
745
- """
746
801
  tool_ctx = create_tool_context(ctx)
747
802
  tool_ctx.set_tool_info("run_command")
748
803
  await tool_ctx.info(f"Executing command: {command}")
749
804
 
750
- # Check if command is allowed
751
- if not self.is_command_allowed(command):
752
- await tool_ctx.error(f"Command not allowed: {command}")
753
- return f"Error: Command not allowed: {command}"
754
-
755
- # Validate required cwd parameter
756
- if not cwd:
757
- await tool_ctx.error("Parameter 'cwd' is required but was None")
758
- return "Error: Parameter 'cwd' is required but was None"
759
-
760
- if cwd.strip() == "":
761
- await tool_ctx.error("Parameter 'cwd' cannot be empty")
762
- return "Error: Parameter 'cwd' cannot be empty"
763
-
764
- # Check if working directory is allowed
765
- if not self.permission_manager.is_path_allowed(cwd):
766
- await tool_ctx.error(f"Working directory not allowed: {cwd}")
767
- return f"Error: Working directory not allowed: {cwd}"
768
-
769
- # Check if working directory exists
770
- if not os.path.isdir(cwd):
771
- await tool_ctx.error(f"Working directory does not exist: {cwd}")
772
- return f"Error: Working directory does not exist: {cwd}"
773
-
774
- # Execute the command
775
- result: CommandResult = await self.execute_command(
776
- command, cwd=cwd, timeout=600.0, use_login_shell=use_login_shell
777
- )
778
-
779
- # Report result
780
- if result.is_success:
781
- await tool_ctx.info("Command executed successfully")
782
- else:
783
- await tool_ctx.error(
784
- f"Command failed with exit code {result.return_code}"
785
- )
805
+ # Use request_id as session_id for persistent working directory
806
+ session_id = ctx.request_id
786
807
 
787
- # Format the result
808
+ # Run validations and execute
809
+ result = await self.execute_command(
810
+ command,
811
+ cwd,
812
+ timeout=30.0,
813
+ use_login_shell=use_login_shell,
814
+ session_id=session_id
815
+ )
816
+
788
817
  if result.is_success:
789
- # For successful commands, just return stdout unless stderr has content
790
- if result.stderr:
791
- return f"Command executed successfully.\n\nSTDOUT:\n{result.stdout}\n\nSTDERR:\n{result.stderr}"
792
818
  return result.stdout
793
819
  else:
794
- # For failed commands, include all available information
795
820
  return result.format_output()
796
-
821
+
797
822
  # Run Script Tool
798
823
  @mcp_server.tool()
799
824
  async def run_script(
@@ -803,94 +828,27 @@ class CommandExecutor:
803
828
  interpreter: str = "bash",
804
829
  use_login_shell: bool = True,
805
830
  ) -> str:
806
- """Execute a script with the specified interpreter.
807
-
808
- Args:
809
- script: The script content to execute
810
- cwd: Working directory for script execution
811
-
812
- interpreter: The interpreter to use (bash, python, etc.)
813
- use_login_shell: Whether to use login shell (loads ~/.zshrc, ~/.bashrc, etc.)
814
-
815
- Returns:
816
- The output of the script
817
- """
818
831
  tool_ctx = create_tool_context(ctx)
819
832
  tool_ctx.set_tool_info("run_script")
820
-
821
- # Validate script parameter
822
- if not script:
823
- await tool_ctx.error("Parameter 'script' is required but was None")
824
- return "Error: Parameter 'script' is required but was None"
825
-
826
- if script.strip() == "":
827
- await tool_ctx.error("Parameter 'script' cannot be empty")
828
- return "Error: Parameter 'script' cannot be empty"
829
-
830
- # interpreter can be None safely as it has a default value
831
- if not interpreter:
832
- interpreter = "bash" # Use default if None
833
- elif interpreter.strip() == "":
834
- await tool_ctx.error("Parameter 'interpreter' cannot be empty")
835
- return "Error: Parameter 'interpreter' cannot be empty"
836
-
837
- # Validate required cwd parameter
838
- if not cwd:
839
- await tool_ctx.error("Parameter 'cwd' is required but was None")
840
- return "Error: Parameter 'cwd' is required but was None"
841
-
842
- if cwd.strip() == "":
843
- await tool_ctx.error("Parameter 'cwd' cannot be empty")
844
- return "Error: Parameter 'cwd' cannot be empty"
845
-
846
- await tool_ctx.info(f"Executing script with interpreter: {interpreter}")
847
-
848
- # Validate required cwd parameter
849
- if not cwd:
850
- await tool_ctx.error("Parameter 'cwd' is required but was None")
851
- return "Error: Parameter 'cwd' is required but was None"
852
-
853
- if cwd.strip() == "":
854
- await tool_ctx.error("Parameter 'cwd' cannot be empty")
855
- return "Error: Parameter 'cwd' cannot be empty"
856
-
857
- # Check if working directory is allowed
858
- if not self.permission_manager.is_path_allowed(cwd):
859
- await tool_ctx.error(f"Working directory not allowed: {cwd}")
860
- return f"Error: Working directory not allowed: {cwd}"
861
-
862
- # Check if working directory exists
863
- if not os.path.isdir(cwd):
864
- await tool_ctx.error(f"Working directory does not exist: {cwd}")
865
- return f"Error: Working directory does not exist: {cwd}"
866
-
833
+
834
+ # Use request_id as session_id for persistent working directory
835
+ session_id = ctx.request_id
836
+
867
837
  # Execute the script
868
- result: CommandResult = await self.execute_script(
838
+ result = await self.execute_script(
869
839
  script=script,
870
840
  interpreter=interpreter,
871
- cwd=cwd, # cwd is now a required parameter
872
- timeout=600.0,
841
+ cwd=cwd,
842
+ timeout=30.0,
873
843
  use_login_shell=use_login_shell,
844
+ session_id=session_id,
874
845
  )
875
-
876
- # Report result
877
- if result.is_success:
878
- await tool_ctx.info("Script executed successfully")
879
- else:
880
- await tool_ctx.error(
881
- f"Script execution failed with exit code {result.return_code}"
882
- )
883
-
884
- # Format the result
846
+
885
847
  if result.is_success:
886
- # For successful scripts, just return stdout unless stderr has content
887
- if result.stderr:
888
- return f"Script executed successfully.\n\nSTDOUT:\n{result.stdout}\n\nSTDERR:\n{result.stderr}"
889
848
  return result.stdout
890
849
  else:
891
- # For failed scripts, include all available information
892
850
  return result.format_output()
893
-
851
+
894
852
  # Script tool for executing scripts in various languages
895
853
  @mcp_server.tool()
896
854
  async def script_tool(
@@ -901,101 +859,24 @@ class CommandExecutor:
901
859
  args: list[str] | None = None,
902
860
  use_login_shell: bool = True,
903
861
  ) -> str:
904
- """Execute a script in the specified language.
905
-
906
- Args:
907
- language: The programming language (python, javascript, etc.)
908
- script: The script code to execute
909
- cwd: Working directory for script execution
910
-
911
- args: Optional command-line arguments
912
- use_login_shell: Whether to use login shell (loads ~/.zshrc, ~/.bashrc, etc.)
913
-
914
- Returns:
915
- Script execution results
916
- """
917
862
  tool_ctx = create_tool_context(ctx)
918
863
  tool_ctx.set_tool_info("script_tool")
919
-
920
- # Validate required parameters
921
- if not language:
922
- await tool_ctx.error("Parameter 'language' is required but was None")
923
- return "Error: Parameter 'language' is required but was None"
924
-
925
- if language.strip() == "":
926
- await tool_ctx.error("Parameter 'language' cannot be empty")
927
- return "Error: Parameter 'language' cannot be empty"
928
-
929
- if not script:
930
- await tool_ctx.error("Parameter 'script' is required but was None")
931
- return "Error: Parameter 'script' is required but was None"
932
-
933
- if script.strip() == "":
934
- await tool_ctx.error("Parameter 'script' cannot be empty")
935
- return "Error: Parameter 'script' cannot be empty"
936
-
937
- # args can be None as it's optional
938
- # Check for empty list but still allow None
939
- if args is not None and len(args) == 0:
940
- await tool_ctx.warning("Parameter 'args' is an empty list")
941
- # We don't return error for this as empty args is acceptable
942
-
943
- # Validate required cwd parameter
944
- if not cwd:
945
- await tool_ctx.error("Parameter 'cwd' is required but was None")
946
- return "Error: Parameter 'cwd' is required but was None"
947
-
948
- if cwd.strip() == "":
949
- await tool_ctx.error("Parameter 'cwd' cannot be empty")
950
- return "Error: Parameter 'cwd' cannot be empty"
951
-
952
- await tool_ctx.info(f"Executing {language} script")
953
-
954
- # Check if the language is supported
955
- if language not in self.get_available_languages():
956
- await tool_ctx.error(f"Unsupported language: {language}")
957
- return f"Error: Unsupported language: {language}. Supported languages: {', '.join(self.get_available_languages())}"
958
-
959
- # Check if working directory is allowed
960
- if not self.permission_manager.is_path_allowed(cwd):
961
- await tool_ctx.error(f"Working directory not allowed: {cwd}")
962
- return f"Error: Working directory not allowed: {cwd}"
963
-
964
- # Check if working directory exists
965
- if not os.path.isdir(cwd):
966
- await tool_ctx.error(f"Working directory does not exist: {cwd}")
967
- return f"Error: Working directory does not exist: {cwd}"
968
-
969
- # Proceed with execution
970
- await tool_ctx.info(f"Executing {language} script in {cwd}")
971
-
864
+
865
+ # Use request_id as session_id for persistent working directory
866
+ session_id = ctx.request_id
867
+
972
868
  # Execute the script
973
869
  result = await self.execute_script_from_file(
974
870
  script=script,
975
871
  language=language,
976
- cwd=cwd, # cwd is now a required parameter
977
- timeout=600.0,
872
+ cwd=cwd,
873
+ timeout=30.0,
978
874
  args=args,
979
875
  use_login_shell=use_login_shell,
876
+ session_id=session_id
980
877
  )
981
-
982
- # Report result
983
- if result.is_success:
984
- await tool_ctx.info(f"{language} script executed successfully")
985
- else:
986
- await tool_ctx.error(
987
- f"{language} script execution failed with exit code {result.return_code}"
988
- )
989
-
990
- # Format the result
878
+
991
879
  if result.is_success:
992
- # Format the successful result
993
- output = f"{language} script executed successfully.\n\n"
994
- if result.stdout:
995
- output += f"STDOUT:\n{result.stdout}\n\n"
996
- if result.stderr:
997
- output += f"STDERR:\n{result.stderr}"
998
- return output.strip()
880
+ return result.stdout
999
881
  else:
1000
- # For failed scripts, include all available information
1001
882
  return result.format_output()