hanzo-mcp 0.8.8__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.

Files changed (167) hide show
  1. hanzo_mcp/__init__.py +1 -3
  2. hanzo_mcp/analytics/posthog_analytics.py +4 -17
  3. hanzo_mcp/bridge.py +9 -25
  4. hanzo_mcp/cli.py +8 -17
  5. hanzo_mcp/cli_enhanced.py +5 -14
  6. hanzo_mcp/cli_plugin.py +3 -9
  7. hanzo_mcp/config/settings.py +6 -20
  8. hanzo_mcp/config/tool_config.py +2 -4
  9. hanzo_mcp/core/base_agent.py +88 -88
  10. hanzo_mcp/core/model_registry.py +238 -210
  11. hanzo_mcp/dev_server.py +5 -15
  12. hanzo_mcp/prompts/__init__.py +2 -6
  13. hanzo_mcp/prompts/project_todo_reminder.py +3 -9
  14. hanzo_mcp/prompts/tool_explorer.py +1 -3
  15. hanzo_mcp/prompts/utils.py +7 -21
  16. hanzo_mcp/server.py +6 -7
  17. hanzo_mcp/tools/__init__.py +29 -32
  18. hanzo_mcp/tools/agent/__init__.py +2 -1
  19. hanzo_mcp/tools/agent/agent.py +10 -30
  20. hanzo_mcp/tools/agent/agent_tool.py +23 -17
  21. hanzo_mcp/tools/agent/claude_desktop_auth.py +3 -9
  22. hanzo_mcp/tools/agent/cli_agent_base.py +7 -24
  23. hanzo_mcp/tools/agent/cli_tools.py +76 -75
  24. hanzo_mcp/tools/agent/code_auth.py +1 -3
  25. hanzo_mcp/tools/agent/code_auth_tool.py +2 -6
  26. hanzo_mcp/tools/agent/critic_tool.py +8 -24
  27. hanzo_mcp/tools/agent/iching_tool.py +12 -36
  28. hanzo_mcp/tools/agent/network_tool.py +7 -18
  29. hanzo_mcp/tools/agent/prompt.py +1 -5
  30. hanzo_mcp/tools/agent/review_tool.py +10 -25
  31. hanzo_mcp/tools/agent/swarm_alias.py +1 -3
  32. hanzo_mcp/tools/agent/unified_cli_tools.py +38 -38
  33. hanzo_mcp/tools/common/batch_tool.py +15 -45
  34. hanzo_mcp/tools/common/config_tool.py +9 -28
  35. hanzo_mcp/tools/common/context.py +1 -3
  36. hanzo_mcp/tools/common/critic_tool.py +1 -3
  37. hanzo_mcp/tools/common/decorators.py +2 -6
  38. hanzo_mcp/tools/common/enhanced_base.py +2 -6
  39. hanzo_mcp/tools/common/fastmcp_pagination.py +4 -12
  40. hanzo_mcp/tools/common/forgiving_edit.py +9 -28
  41. hanzo_mcp/tools/common/mode.py +1 -5
  42. hanzo_mcp/tools/common/paginated_base.py +3 -11
  43. hanzo_mcp/tools/common/paginated_response.py +10 -30
  44. hanzo_mcp/tools/common/pagination.py +3 -9
  45. hanzo_mcp/tools/common/path_utils.py +34 -0
  46. hanzo_mcp/tools/common/permissions.py +14 -13
  47. hanzo_mcp/tools/common/personality.py +983 -701
  48. hanzo_mcp/tools/common/plugin_loader.py +3 -15
  49. hanzo_mcp/tools/common/stats.py +7 -19
  50. hanzo_mcp/tools/common/thinking_tool.py +1 -3
  51. hanzo_mcp/tools/common/tool_disable.py +2 -6
  52. hanzo_mcp/tools/common/tool_list.py +2 -6
  53. hanzo_mcp/tools/common/validation.py +1 -3
  54. hanzo_mcp/tools/compiler/__init__.py +8 -0
  55. hanzo_mcp/tools/compiler/sandboxed_compiler.py +681 -0
  56. hanzo_mcp/tools/config/config_tool.py +7 -13
  57. hanzo_mcp/tools/config/index_config.py +1 -3
  58. hanzo_mcp/tools/config/mode_tool.py +5 -15
  59. hanzo_mcp/tools/database/database_manager.py +3 -9
  60. hanzo_mcp/tools/database/graph.py +1 -3
  61. hanzo_mcp/tools/database/graph_add.py +3 -9
  62. hanzo_mcp/tools/database/graph_query.py +11 -34
  63. hanzo_mcp/tools/database/graph_remove.py +3 -9
  64. hanzo_mcp/tools/database/graph_search.py +6 -20
  65. hanzo_mcp/tools/database/graph_stats.py +11 -33
  66. hanzo_mcp/tools/database/sql.py +4 -12
  67. hanzo_mcp/tools/database/sql_query.py +6 -10
  68. hanzo_mcp/tools/database/sql_search.py +2 -6
  69. hanzo_mcp/tools/database/sql_stats.py +5 -15
  70. hanzo_mcp/tools/editor/neovim_command.py +1 -3
  71. hanzo_mcp/tools/editor/neovim_session.py +7 -13
  72. hanzo_mcp/tools/environment/__init__.py +8 -0
  73. hanzo_mcp/tools/environment/environment_detector.py +594 -0
  74. hanzo_mcp/tools/filesystem/__init__.py +28 -26
  75. hanzo_mcp/tools/filesystem/ast_multi_edit.py +14 -43
  76. hanzo_mcp/tools/filesystem/ast_tool.py +3 -0
  77. hanzo_mcp/tools/filesystem/base.py +20 -12
  78. hanzo_mcp/tools/filesystem/content_replace.py +7 -12
  79. hanzo_mcp/tools/filesystem/diff.py +2 -10
  80. hanzo_mcp/tools/filesystem/directory_tree.py +285 -51
  81. hanzo_mcp/tools/filesystem/edit.py +10 -18
  82. hanzo_mcp/tools/filesystem/find.py +312 -179
  83. hanzo_mcp/tools/filesystem/git_search.py +12 -24
  84. hanzo_mcp/tools/filesystem/multi_edit.py +10 -18
  85. hanzo_mcp/tools/filesystem/read.py +14 -30
  86. hanzo_mcp/tools/filesystem/rules_tool.py +9 -17
  87. hanzo_mcp/tools/filesystem/search.py +1160 -0
  88. hanzo_mcp/tools/filesystem/watch.py +2 -4
  89. hanzo_mcp/tools/filesystem/write.py +7 -10
  90. hanzo_mcp/tools/framework/__init__.py +8 -0
  91. hanzo_mcp/tools/framework/framework_modes.py +714 -0
  92. hanzo_mcp/tools/jupyter/base.py +6 -20
  93. hanzo_mcp/tools/jupyter/jupyter.py +4 -12
  94. hanzo_mcp/tools/llm/consensus_tool.py +8 -24
  95. hanzo_mcp/tools/llm/llm_manage.py +2 -6
  96. hanzo_mcp/tools/llm/llm_tool.py +17 -58
  97. hanzo_mcp/tools/llm/llm_unified.py +18 -59
  98. hanzo_mcp/tools/llm/provider_tools.py +1 -3
  99. hanzo_mcp/tools/lsp/lsp_tool.py +621 -481
  100. hanzo_mcp/tools/mcp/mcp_add.py +3 -5
  101. hanzo_mcp/tools/mcp/mcp_remove.py +1 -1
  102. hanzo_mcp/tools/mcp/mcp_stats.py +1 -3
  103. hanzo_mcp/tools/mcp/mcp_tool.py +9 -23
  104. hanzo_mcp/tools/memory/__init__.py +33 -40
  105. hanzo_mcp/tools/memory/conversation_memory.py +636 -0
  106. hanzo_mcp/tools/memory/knowledge_tools.py +7 -25
  107. hanzo_mcp/tools/memory/memory_tools.py +7 -19
  108. hanzo_mcp/tools/search/find_tool.py +12 -34
  109. hanzo_mcp/tools/search/unified_search.py +27 -81
  110. hanzo_mcp/tools/shell/__init__.py +16 -4
  111. hanzo_mcp/tools/shell/auto_background.py +2 -6
  112. hanzo_mcp/tools/shell/base.py +1 -5
  113. hanzo_mcp/tools/shell/base_process.py +5 -7
  114. hanzo_mcp/tools/shell/bash_session.py +7 -24
  115. hanzo_mcp/tools/shell/bash_session_executor.py +5 -15
  116. hanzo_mcp/tools/shell/bash_tool.py +3 -7
  117. hanzo_mcp/tools/shell/command_executor.py +26 -79
  118. hanzo_mcp/tools/shell/logs.py +4 -16
  119. hanzo_mcp/tools/shell/npx.py +2 -8
  120. hanzo_mcp/tools/shell/npx_tool.py +1 -3
  121. hanzo_mcp/tools/shell/pkill.py +4 -12
  122. hanzo_mcp/tools/shell/process_tool.py +2 -8
  123. hanzo_mcp/tools/shell/processes.py +5 -17
  124. hanzo_mcp/tools/shell/run_background.py +1 -3
  125. hanzo_mcp/tools/shell/run_command.py +1 -3
  126. hanzo_mcp/tools/shell/run_command_windows.py +1 -3
  127. hanzo_mcp/tools/shell/run_tool.py +56 -0
  128. hanzo_mcp/tools/shell/session_manager.py +2 -6
  129. hanzo_mcp/tools/shell/session_storage.py +2 -6
  130. hanzo_mcp/tools/shell/streaming_command.py +7 -23
  131. hanzo_mcp/tools/shell/uvx.py +4 -14
  132. hanzo_mcp/tools/shell/uvx_background.py +2 -6
  133. hanzo_mcp/tools/shell/uvx_tool.py +1 -3
  134. hanzo_mcp/tools/shell/zsh_tool.py +12 -20
  135. hanzo_mcp/tools/todo/todo.py +1 -3
  136. hanzo_mcp/tools/vector/__init__.py +97 -50
  137. hanzo_mcp/tools/vector/ast_analyzer.py +6 -20
  138. hanzo_mcp/tools/vector/git_ingester.py +10 -30
  139. hanzo_mcp/tools/vector/index_tool.py +3 -9
  140. hanzo_mcp/tools/vector/infinity_store.py +11 -30
  141. hanzo_mcp/tools/vector/mock_infinity.py +159 -0
  142. hanzo_mcp/tools/vector/node_tool.py +538 -0
  143. hanzo_mcp/tools/vector/project_manager.py +4 -12
  144. hanzo_mcp/tools/vector/unified_vector.py +384 -0
  145. hanzo_mcp/tools/vector/vector.py +2 -6
  146. hanzo_mcp/tools/vector/vector_index.py +8 -8
  147. hanzo_mcp/tools/vector/vector_search.py +7 -21
  148. {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/METADATA +2 -2
  149. hanzo_mcp-0.9.0.dist-info/RECORD +191 -0
  150. hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +0 -645
  151. hanzo_mcp/tools/agent/swarm_tool.py +0 -723
  152. hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +0 -577
  153. hanzo_mcp/tools/filesystem/batch_search.py +0 -900
  154. hanzo_mcp/tools/filesystem/directory_tree_paginated.py +0 -350
  155. hanzo_mcp/tools/filesystem/find_files.py +0 -369
  156. hanzo_mcp/tools/filesystem/grep.py +0 -467
  157. hanzo_mcp/tools/filesystem/search_tool.py +0 -767
  158. hanzo_mcp/tools/filesystem/symbols_tool.py +0 -515
  159. hanzo_mcp/tools/filesystem/tree.py +0 -270
  160. hanzo_mcp/tools/jupyter/notebook_edit.py +0 -317
  161. hanzo_mcp/tools/jupyter/notebook_read.py +0 -147
  162. hanzo_mcp/tools/todo/todo_read.py +0 -143
  163. hanzo_mcp/tools/todo/todo_write.py +0 -374
  164. hanzo_mcp-0.8.8.dist-info/RECORD +0 -192
  165. {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/WHEEL +0 -0
  166. {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/entry_points.txt +0 -0
  167. {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/top_level.txt +0 -0
@@ -84,11 +84,7 @@ def escape_bash_special_chars(command: str) -> str:
84
84
 
85
85
  def visit_node(node: Any) -> None:
86
86
  nonlocal last_pos
87
- if (
88
- node.kind == "redirect"
89
- and hasattr(node, "heredoc")
90
- and node.heredoc is not None
91
- ):
87
+ if node.kind == "redirect" and hasattr(node, "heredoc") and node.heredoc is not None:
92
88
  # We're entering a heredoc - preserve everything as-is until we see EOF
93
89
  between = command[last_pos : node.pos[0]]
94
90
  parts.append(between)
@@ -414,15 +410,11 @@ class BashSession:
414
410
  "> ", # generic
415
411
  ">", # generic without space
416
412
  ]
417
- output_ends_with_prompt = any(
418
- cur_pane_output.rstrip().endswith(pattern)
419
- for pattern in prompt_patterns
420
- )
413
+ output_ends_with_prompt = any(cur_pane_output.rstrip().endswith(pattern) for pattern in prompt_patterns)
421
414
 
422
415
  # Also check for username@hostname pattern (common in many shells)
423
416
  has_user_host_pattern = "@" in cur_pane_output and any(
424
- cur_pane_output.rstrip().endswith(indicator)
425
- for indicator in prompt_patterns
417
+ cur_pane_output.rstrip().endswith(indicator) for indicator in prompt_patterns
426
418
  )
427
419
 
428
420
  if output_ends_with_prompt or has_user_host_pattern:
@@ -430,10 +422,7 @@ class BashSession:
430
422
 
431
423
  # 2) No-change timeout (only if not blocking)
432
424
  time_since_last_change = time.time() - last_change_time
433
- if (
434
- not blocking
435
- and time_since_last_change >= self.NO_CHANGE_TIMEOUT_SECONDS
436
- ):
425
+ if not blocking and time_since_last_change >= self.NO_CHANGE_TIMEOUT_SECONDS:
437
426
  # Extract current output
438
427
  lines = cur_pane_output.strip().split("\n")
439
428
  output = "\n".join(lines)
@@ -497,9 +486,7 @@ class BashSession:
497
486
  session_id=self.id,
498
487
  )
499
488
 
500
- def _handle_nochange_timeout_command(
501
- self, command: str, pane_content: str
502
- ) -> CommandResult:
489
+ def _handle_nochange_timeout_command(self, command: str, pane_content: str) -> CommandResult:
503
490
  """Handle a command that timed out due to no output changes."""
504
491
  self.prev_status = BashCommandStatus.NO_CHANGE_TIMEOUT
505
492
 
@@ -530,9 +517,7 @@ class BashSession:
530
517
  session_id=self.id,
531
518
  )
532
519
 
533
- def _handle_hard_timeout_command(
534
- self, command: str, pane_content: str, timeout: float
535
- ) -> CommandResult:
520
+ def _handle_hard_timeout_command(self, command: str, pane_content: str, timeout: float) -> CommandResult:
536
521
  """Handle a command that hit the hard timeout."""
537
522
  self.prev_status = BashCommandStatus.HARD_TIMEOUT
538
523
 
@@ -563,9 +548,7 @@ class BashSession:
563
548
  session_id=self.id,
564
549
  )
565
550
 
566
- def _fallback_completion_detection(
567
- self, command: str, pane_content: str
568
- ) -> CommandResult:
551
+ def _fallback_completion_detection(self, command: str, pane_content: str) -> CommandResult:
569
552
  """Fallback completion detection when PS1 metadata is not available."""
570
553
  # Use the old logic as fallback
571
554
  self.pane.send_keys("echo EXIT_CODE:$?", enter=True)
@@ -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)}")
@@ -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
 
@@ -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."""
@@ -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: