mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-23 20:23:00 +00:00
Make uvloop fully opt-in (performance extra only, not a hard dep). Add Windows native support: shell resolution (pwsh/cmd), signal handling (SIGKILL guard), platform-appropriate paths, chmod guards. Changes: - hanzo-mcp: move uvloop from hard dep to [performance] extra - hanzo-tools-shell: resolve pwsh/powershell/cmd on Windows, use correct invocation flags (cmd /c, pwsh -Command) - hanzo-tools-shell: guard signal.SIGKILL (doesn't exist on Windows) - hanzo-tools-computer: replace HOME=/tmp fallback with Path.home() - hanzo-mcp cli: platform-appropriate socket path default - hanzoai auth: skip os.chmod on Windows - hanzo install: skip os.chmod on Windows - Add .github/workflows/test-windows.yml (native + WSL2 matrix) Versions bumped: hanzo-mcp 0.12.7, hanzo-async 0.1.2, hanzo-tools-shell 0.6.2, hanzo-tools-computer 0.5.4
807 lines
26 KiB
Python
807 lines
26 KiB
Python
"""Shell tool shims - zsh, bash, fish, dash, ksh, tcsh, csh, shell wrappers over cmd.
|
|
|
|
These are thin wrappers that set the shell and delegate to CmdTool.
|
|
Use cmd directly for full control.
|
|
|
|
Supported shells:
|
|
- zsh: Z shell (default on macOS, popular on Linux)
|
|
- bash: Bourne-Again shell (most common, default on most Linux)
|
|
- fish: Friendly Interactive Shell (modern, user-friendly)
|
|
- dash: Debian Almquist Shell (fast POSIX shell, Ubuntu's /bin/sh)
|
|
- ksh: KornShell (enterprise Unix, efficient scripting)
|
|
- tcsh: TENEX C Shell (enhanced csh with command completion)
|
|
- csh: C Shell (older, C-like syntax)
|
|
- shell: Auto-selects best available (zsh > bash > fish > dash > ksh > sh)
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import shutil
|
|
from typing import Any, Dict, List, Optional, Annotated, override
|
|
|
|
from pydantic import Field
|
|
from mcp.server import FastMCP
|
|
from mcp.server.fastmcp import Context as MCPContext
|
|
|
|
from hanzo_tools.core import BaseTool, auto_timeout, create_tool_context
|
|
from hanzo_tools.shell.cmd_tool import CmdTool, Command
|
|
|
|
|
|
class ZshTool(CmdTool):
|
|
"""Zsh shell - thin wrapper over cmd with shell=zsh.
|
|
|
|
Usage:
|
|
zsh("ls -la") # Single command
|
|
zsh(["ls", "pwd"]) # Sequential
|
|
zsh(["a", "b"], parallel=True) # Parallel
|
|
"""
|
|
|
|
name = "zsh"
|
|
|
|
def __init__(self, tools: Optional[Dict[str, BaseTool]] = None):
|
|
"""Initialize with zsh as default shell."""
|
|
super().__init__(tools=tools, default_shell="zsh")
|
|
|
|
def _resolve_shell(self, preferred: str) -> str:
|
|
"""Resolve zsh shell path. Falls back to bash, then platform default."""
|
|
force_shell = os.environ.get("HANZO_MCP_FORCE_SHELL")
|
|
if force_shell:
|
|
return force_shell
|
|
|
|
# On Windows, prefer powershell/cmd over Unix shells
|
|
if sys.platform == "win32":
|
|
for shell in ("pwsh", "powershell", "cmd"):
|
|
found = shutil.which(shell)
|
|
if found:
|
|
return found
|
|
return "cmd.exe"
|
|
|
|
search_paths = [
|
|
"/opt/homebrew/bin/zsh",
|
|
"/usr/local/bin/zsh",
|
|
"/bin/zsh",
|
|
"/usr/bin/zsh",
|
|
]
|
|
|
|
for path in search_paths:
|
|
if os.path.isfile(path) and os.access(path, os.X_OK):
|
|
return path
|
|
|
|
found = shutil.which("zsh")
|
|
if found:
|
|
return found
|
|
|
|
# Fallback to bash if zsh not found
|
|
return shutil.which("bash") or "sh"
|
|
|
|
@property
|
|
@override
|
|
def description(self) -> str:
|
|
shell_name = os.path.basename(self.default_shell)
|
|
return f"""Zsh shell (using: {shell_name}).
|
|
|
|
SIMPLE:
|
|
zsh("ls -la") # Single command
|
|
zsh("A ; B ; C") # Sequential via shell
|
|
|
|
ARRAYS:
|
|
zsh(["a", "b", "c"]) # Sequential
|
|
zsh(["a", "b"], parallel=True) # All parallel
|
|
zsh(["a", ["b", "c"], "d"]) # Nested = parallel
|
|
|
|
OPTIONS:
|
|
parallel: Run ALL commands concurrently
|
|
strict: Stop on first error
|
|
quiet: Suppress stdout
|
|
timeout: Per-command timeout (default: 45s)
|
|
cwd: Working directory
|
|
env: Environment variables
|
|
|
|
AUTO-BACKGROUNDING: Commands exceeding 45s auto-background.
|
|
Use ps --logs <id> to view, ps --kill <id> to stop."""
|
|
|
|
@override
|
|
def register(self, mcp_server: FastMCP) -> None:
|
|
"""Register zsh tool with MCP server."""
|
|
tool_self = self
|
|
|
|
@mcp_server.tool(name=self.name, description=self.description)
|
|
async def zsh_handler(
|
|
command: Annotated[
|
|
Optional[str],
|
|
Field(description="Single command to execute", default=None),
|
|
] = None,
|
|
commands: Annotated[
|
|
Optional[List[Any]],
|
|
Field(description="List of commands for DAG execution", default=None),
|
|
] = None,
|
|
parallel: Annotated[
|
|
bool, Field(description="Run all commands in parallel", default=False)
|
|
] = False,
|
|
cwd: Annotated[
|
|
Optional[str], Field(description="Working directory", default=None)
|
|
] = None,
|
|
env: Annotated[
|
|
Optional[Dict[str, str]],
|
|
Field(description="Environment variables", default=None),
|
|
] = None,
|
|
timeout: Annotated[
|
|
int, Field(description="Timeout per command (seconds)", default=30)
|
|
] = 30,
|
|
strict: Annotated[
|
|
bool, Field(description="Stop on first error", default=False)
|
|
] = False,
|
|
quiet: Annotated[
|
|
bool, Field(description="Suppress stdout", default=False)
|
|
] = False,
|
|
ctx: MCPContext = None,
|
|
) -> str:
|
|
return await tool_self.call(
|
|
ctx,
|
|
command=command,
|
|
commands=commands,
|
|
parallel=parallel,
|
|
shell=None, # Use default (zsh)
|
|
cwd=cwd,
|
|
env=env,
|
|
timeout=timeout,
|
|
strict=strict,
|
|
quiet=quiet,
|
|
)
|
|
|
|
|
|
class BashTool(CmdTool):
|
|
"""Bash shell - thin wrapper over cmd with shell=bash."""
|
|
|
|
name = "bash"
|
|
|
|
def __init__(self, tools: Optional[Dict[str, BaseTool]] = None):
|
|
"""Initialize with bash as default shell."""
|
|
super().__init__(tools=tools, default_shell="bash")
|
|
|
|
def _resolve_shell(self, preferred: str) -> str:
|
|
"""Resolve bash shell path."""
|
|
force_shell = os.environ.get("HANZO_MCP_FORCE_SHELL")
|
|
if force_shell:
|
|
return force_shell
|
|
|
|
found = shutil.which("bash")
|
|
if found:
|
|
return found
|
|
|
|
return "sh"
|
|
|
|
@property
|
|
@override
|
|
def description(self) -> str:
|
|
return """Bash shell - thin wrapper over cmd.
|
|
|
|
USAGE:
|
|
bash("ls -la") # Single command
|
|
bash(["a", "b", "c"]) # Sequential
|
|
bash(["a", "b"], parallel=True) # Parallel
|
|
|
|
AUTO-BACKGROUNDING: Commands exceeding 45s auto-background."""
|
|
|
|
@override
|
|
def register(self, mcp_server: FastMCP) -> None:
|
|
"""Register bash tool with MCP server."""
|
|
tool_self = self
|
|
|
|
@mcp_server.tool(name=self.name, description=self.description)
|
|
async def bash_handler(
|
|
command: Annotated[
|
|
Optional[str],
|
|
Field(description="Single command to execute", default=None),
|
|
] = None,
|
|
commands: Annotated[
|
|
Optional[List[Any]], Field(description="List of commands", default=None)
|
|
] = None,
|
|
parallel: Annotated[
|
|
bool, Field(description="Run in parallel", default=False)
|
|
] = False,
|
|
cwd: Annotated[
|
|
Optional[str], Field(description="Working directory", default=None)
|
|
] = None,
|
|
env: Annotated[
|
|
Optional[Dict[str, str]],
|
|
Field(description="Environment variables", default=None),
|
|
] = None,
|
|
timeout: Annotated[
|
|
int, Field(description="Timeout (seconds)", default=30)
|
|
] = 30,
|
|
strict: Annotated[
|
|
bool, Field(description="Stop on first error", default=False)
|
|
] = False,
|
|
quiet: Annotated[
|
|
bool, Field(description="Suppress stdout", default=False)
|
|
] = False,
|
|
ctx: MCPContext = None,
|
|
) -> str:
|
|
return await tool_self.call(
|
|
ctx,
|
|
command=command,
|
|
commands=commands,
|
|
parallel=parallel,
|
|
shell=None, # Use default (bash)
|
|
cwd=cwd,
|
|
env=env,
|
|
timeout=timeout,
|
|
strict=strict,
|
|
quiet=quiet,
|
|
)
|
|
|
|
|
|
class FishTool(CmdTool):
|
|
"""Fish shell - thin wrapper over cmd with shell=fish."""
|
|
|
|
name = "fish"
|
|
|
|
def __init__(self, tools: Optional[Dict[str, BaseTool]] = None):
|
|
"""Initialize with fish as default shell."""
|
|
super().__init__(tools=tools, default_shell="fish")
|
|
|
|
def _resolve_shell(self, preferred: str) -> str:
|
|
"""Resolve fish shell path."""
|
|
force_shell = os.environ.get("HANZO_MCP_FORCE_SHELL")
|
|
if force_shell:
|
|
return force_shell
|
|
|
|
search_paths = [
|
|
"/opt/homebrew/bin/fish",
|
|
"/usr/local/bin/fish",
|
|
"/usr/bin/fish",
|
|
]
|
|
|
|
for path in search_paths:
|
|
if os.path.isfile(path) and os.access(path, os.X_OK):
|
|
return path
|
|
|
|
found = shutil.which("fish")
|
|
if found:
|
|
return found
|
|
|
|
# Fallback to zsh/bash if fish not found
|
|
return shutil.which("zsh") or shutil.which("bash") or "sh"
|
|
|
|
@property
|
|
@override
|
|
def description(self) -> str:
|
|
shell_name = os.path.basename(self.default_shell)
|
|
return f"""Fish shell (using: {shell_name}).
|
|
|
|
USAGE:
|
|
fish("ls -la") # Single command
|
|
fish(["a", "b", "c"]) # Sequential
|
|
fish(["a", "b"], parallel=True) # Parallel
|
|
|
|
AUTO-BACKGROUNDING: Commands exceeding 45s auto-background."""
|
|
|
|
@override
|
|
def register(self, mcp_server: FastMCP) -> None:
|
|
"""Register fish tool with MCP server."""
|
|
tool_self = self
|
|
|
|
@mcp_server.tool(name=self.name, description=self.description)
|
|
async def fish_handler(
|
|
command: Annotated[
|
|
Optional[str],
|
|
Field(description="Single command to execute", default=None),
|
|
] = None,
|
|
commands: Annotated[
|
|
Optional[List[Any]], Field(description="List of commands", default=None)
|
|
] = None,
|
|
parallel: Annotated[
|
|
bool, Field(description="Run in parallel", default=False)
|
|
] = False,
|
|
cwd: Annotated[
|
|
Optional[str], Field(description="Working directory", default=None)
|
|
] = None,
|
|
env: Annotated[
|
|
Optional[Dict[str, str]],
|
|
Field(description="Environment variables", default=None),
|
|
] = None,
|
|
timeout: Annotated[
|
|
int, Field(description="Timeout (seconds)", default=30)
|
|
] = 30,
|
|
strict: Annotated[
|
|
bool, Field(description="Stop on first error", default=False)
|
|
] = False,
|
|
quiet: Annotated[
|
|
bool, Field(description="Suppress stdout", default=False)
|
|
] = False,
|
|
ctx: MCPContext = None,
|
|
) -> str:
|
|
return await tool_self.call(
|
|
ctx,
|
|
command=command,
|
|
commands=commands,
|
|
parallel=parallel,
|
|
shell=None, # Use default (fish)
|
|
cwd=cwd,
|
|
env=env,
|
|
timeout=timeout,
|
|
strict=strict,
|
|
quiet=quiet,
|
|
)
|
|
|
|
|
|
class DashTool(CmdTool):
|
|
"""Dash shell - fast POSIX-compliant shell (Ubuntu's /bin/sh)."""
|
|
|
|
name = "dash"
|
|
|
|
def __init__(self, tools: Optional[Dict[str, BaseTool]] = None):
|
|
"""Initialize with dash as default shell."""
|
|
super().__init__(tools=tools, default_shell="dash")
|
|
|
|
def _resolve_shell(self, preferred: str) -> str:
|
|
"""Resolve dash shell path."""
|
|
force_shell = os.environ.get("HANZO_MCP_FORCE_SHELL")
|
|
if force_shell:
|
|
return force_shell
|
|
|
|
# dash is often /bin/sh on Debian/Ubuntu
|
|
search_paths = [
|
|
"/bin/dash",
|
|
"/usr/bin/dash",
|
|
]
|
|
|
|
for path in search_paths:
|
|
if os.path.isfile(path) and os.access(path, os.X_OK):
|
|
return path
|
|
|
|
found = shutil.which("dash")
|
|
if found:
|
|
return found
|
|
|
|
# Fallback to sh (which may be dash on Ubuntu)
|
|
return "sh"
|
|
|
|
@property
|
|
@override
|
|
def description(self) -> str:
|
|
return """Dash shell - fast POSIX-compliant shell.
|
|
|
|
Dash is the Debian Almquist Shell, a POSIX-compliant shell that's
|
|
faster than bash for scripts. It's the default /bin/sh on Ubuntu/Debian.
|
|
|
|
USAGE:
|
|
dash("ls -la") # Single command
|
|
dash(["a", "b", "c"]) # Sequential
|
|
dash(["a", "b"], parallel=True) # Parallel
|
|
|
|
AUTO-BACKGROUNDING: Commands exceeding 45s auto-background."""
|
|
|
|
@override
|
|
def register(self, mcp_server: FastMCP) -> None:
|
|
"""Register dash tool with MCP server."""
|
|
tool_self = self
|
|
|
|
@mcp_server.tool(name=self.name, description=self.description)
|
|
async def dash_handler(
|
|
command: Annotated[
|
|
Optional[str],
|
|
Field(description="Single command to execute", default=None),
|
|
] = None,
|
|
commands: Annotated[
|
|
Optional[List[Any]], Field(description="List of commands", default=None)
|
|
] = None,
|
|
parallel: Annotated[
|
|
bool, Field(description="Run in parallel", default=False)
|
|
] = False,
|
|
cwd: Annotated[
|
|
Optional[str], Field(description="Working directory", default=None)
|
|
] = None,
|
|
env: Annotated[
|
|
Optional[Dict[str, str]],
|
|
Field(description="Environment variables", default=None),
|
|
] = None,
|
|
timeout: Annotated[
|
|
int, Field(description="Timeout (seconds)", default=30)
|
|
] = 30,
|
|
strict: Annotated[
|
|
bool, Field(description="Stop on first error", default=False)
|
|
] = False,
|
|
quiet: Annotated[
|
|
bool, Field(description="Suppress stdout", default=False)
|
|
] = False,
|
|
ctx: MCPContext = None,
|
|
) -> str:
|
|
return await tool_self.call(
|
|
ctx,
|
|
command=command,
|
|
commands=commands,
|
|
parallel=parallel,
|
|
shell=None, # Use default (dash)
|
|
cwd=cwd,
|
|
env=env,
|
|
timeout=timeout,
|
|
strict=strict,
|
|
quiet=quiet,
|
|
)
|
|
|
|
|
|
class KshTool(CmdTool):
|
|
"""KornShell - enterprise Unix shell with efficient scripting."""
|
|
|
|
name = "ksh"
|
|
|
|
def __init__(self, tools: Optional[Dict[str, BaseTool]] = None):
|
|
"""Initialize with ksh as default shell."""
|
|
super().__init__(tools=tools, default_shell="ksh")
|
|
|
|
def _resolve_shell(self, preferred: str) -> str:
|
|
"""Resolve ksh shell path."""
|
|
force_shell = os.environ.get("HANZO_MCP_FORCE_SHELL")
|
|
if force_shell:
|
|
return force_shell
|
|
|
|
search_paths = [
|
|
"/bin/ksh",
|
|
"/usr/bin/ksh",
|
|
"/usr/local/bin/ksh",
|
|
"/opt/homebrew/bin/ksh",
|
|
]
|
|
|
|
for path in search_paths:
|
|
if os.path.isfile(path) and os.access(path, os.X_OK):
|
|
return path
|
|
|
|
found = shutil.which("ksh")
|
|
if found:
|
|
return found
|
|
|
|
# Also try ksh93 and pdksh
|
|
for variant in ["ksh93", "pdksh", "mksh"]:
|
|
found = shutil.which(variant)
|
|
if found:
|
|
return found
|
|
|
|
return "sh"
|
|
|
|
@property
|
|
@override
|
|
def description(self) -> str:
|
|
return """KornShell (ksh) - enterprise Unix shell.
|
|
|
|
KornShell is historically important in enterprise Unix environments.
|
|
Efficient for scripting, still used on some commercial Unix systems.
|
|
|
|
USAGE:
|
|
ksh("ls -la") # Single command
|
|
ksh(["a", "b", "c"]) # Sequential
|
|
ksh(["a", "b"], parallel=True) # Parallel
|
|
|
|
AUTO-BACKGROUNDING: Commands exceeding 45s auto-background."""
|
|
|
|
@override
|
|
def register(self, mcp_server: FastMCP) -> None:
|
|
"""Register ksh tool with MCP server."""
|
|
tool_self = self
|
|
|
|
@mcp_server.tool(name=self.name, description=self.description)
|
|
async def ksh_handler(
|
|
command: Annotated[
|
|
Optional[str],
|
|
Field(description="Single command to execute", default=None),
|
|
] = None,
|
|
commands: Annotated[
|
|
Optional[List[Any]], Field(description="List of commands", default=None)
|
|
] = None,
|
|
parallel: Annotated[
|
|
bool, Field(description="Run in parallel", default=False)
|
|
] = False,
|
|
cwd: Annotated[
|
|
Optional[str], Field(description="Working directory", default=None)
|
|
] = None,
|
|
env: Annotated[
|
|
Optional[Dict[str, str]],
|
|
Field(description="Environment variables", default=None),
|
|
] = None,
|
|
timeout: Annotated[
|
|
int, Field(description="Timeout (seconds)", default=30)
|
|
] = 30,
|
|
strict: Annotated[
|
|
bool, Field(description="Stop on first error", default=False)
|
|
] = False,
|
|
quiet: Annotated[
|
|
bool, Field(description="Suppress stdout", default=False)
|
|
] = False,
|
|
ctx: MCPContext = None,
|
|
) -> str:
|
|
return await tool_self.call(
|
|
ctx,
|
|
command=command,
|
|
commands=commands,
|
|
parallel=parallel,
|
|
shell=None,
|
|
cwd=cwd,
|
|
env=env,
|
|
timeout=timeout,
|
|
strict=strict,
|
|
quiet=quiet,
|
|
)
|
|
|
|
|
|
class TcshTool(CmdTool):
|
|
"""TENEX C Shell - enhanced csh with command completion."""
|
|
|
|
name = "tcsh"
|
|
|
|
def __init__(self, tools: Optional[Dict[str, BaseTool]] = None):
|
|
"""Initialize with tcsh as default shell."""
|
|
super().__init__(tools=tools, default_shell="tcsh")
|
|
|
|
def _resolve_shell(self, preferred: str) -> str:
|
|
"""Resolve tcsh shell path."""
|
|
force_shell = os.environ.get("HANZO_MCP_FORCE_SHELL")
|
|
if force_shell:
|
|
return force_shell
|
|
|
|
search_paths = [
|
|
"/bin/tcsh",
|
|
"/usr/bin/tcsh",
|
|
"/usr/local/bin/tcsh",
|
|
"/opt/homebrew/bin/tcsh",
|
|
]
|
|
|
|
for path in search_paths:
|
|
if os.path.isfile(path) and os.access(path, os.X_OK):
|
|
return path
|
|
|
|
found = shutil.which("tcsh")
|
|
if found:
|
|
return found
|
|
|
|
# Fallback to csh
|
|
return shutil.which("csh") or "sh"
|
|
|
|
@property
|
|
@override
|
|
def description(self) -> str:
|
|
return """TENEX C Shell (tcsh) - enhanced C shell.
|
|
|
|
Tcsh is an enhanced version of csh with command completion,
|
|
command-line editing, and other improvements. Mostly legacy now
|
|
but still encountered in some environments.
|
|
|
|
USAGE:
|
|
tcsh("ls -la") # Single command
|
|
tcsh(["a", "b", "c"]) # Sequential
|
|
tcsh(["a", "b"], parallel=True) # Parallel
|
|
|
|
AUTO-BACKGROUNDING: Commands exceeding 45s auto-background."""
|
|
|
|
@override
|
|
def register(self, mcp_server: FastMCP) -> None:
|
|
"""Register tcsh tool with MCP server."""
|
|
tool_self = self
|
|
|
|
@mcp_server.tool(name=self.name, description=self.description)
|
|
async def tcsh_handler(
|
|
command: Annotated[
|
|
Optional[str],
|
|
Field(description="Single command to execute", default=None),
|
|
] = None,
|
|
commands: Annotated[
|
|
Optional[List[Any]], Field(description="List of commands", default=None)
|
|
] = None,
|
|
parallel: Annotated[
|
|
bool, Field(description="Run in parallel", default=False)
|
|
] = False,
|
|
cwd: Annotated[
|
|
Optional[str], Field(description="Working directory", default=None)
|
|
] = None,
|
|
env: Annotated[
|
|
Optional[Dict[str, str]],
|
|
Field(description="Environment variables", default=None),
|
|
] = None,
|
|
timeout: Annotated[
|
|
int, Field(description="Timeout (seconds)", default=30)
|
|
] = 30,
|
|
strict: Annotated[
|
|
bool, Field(description="Stop on first error", default=False)
|
|
] = False,
|
|
quiet: Annotated[
|
|
bool, Field(description="Suppress stdout", default=False)
|
|
] = False,
|
|
ctx: MCPContext = None,
|
|
) -> str:
|
|
return await tool_self.call(
|
|
ctx,
|
|
command=command,
|
|
commands=commands,
|
|
parallel=parallel,
|
|
shell=None,
|
|
cwd=cwd,
|
|
env=env,
|
|
timeout=timeout,
|
|
strict=strict,
|
|
quiet=quiet,
|
|
)
|
|
|
|
|
|
class CshTool(CmdTool):
|
|
"""C Shell - older shell with C-like syntax."""
|
|
|
|
name = "csh"
|
|
|
|
def __init__(self, tools: Optional[Dict[str, BaseTool]] = None):
|
|
"""Initialize with csh as default shell."""
|
|
super().__init__(tools=tools, default_shell="csh")
|
|
|
|
def _resolve_shell(self, preferred: str) -> str:
|
|
"""Resolve csh shell path."""
|
|
force_shell = os.environ.get("HANZO_MCP_FORCE_SHELL")
|
|
if force_shell:
|
|
return force_shell
|
|
|
|
search_paths = [
|
|
"/bin/csh",
|
|
"/usr/bin/csh",
|
|
"/usr/local/bin/csh",
|
|
]
|
|
|
|
for path in search_paths:
|
|
if os.path.isfile(path) and os.access(path, os.X_OK):
|
|
return path
|
|
|
|
found = shutil.which("csh")
|
|
if found:
|
|
return found
|
|
|
|
# Fallback to tcsh (often linked to csh)
|
|
return shutil.which("tcsh") or "sh"
|
|
|
|
@property
|
|
@override
|
|
def description(self) -> str:
|
|
return """C Shell (csh) - shell with C-like syntax.
|
|
|
|
The C shell is an older shell with syntax reminiscent of C.
|
|
Mostly legacy now but still encountered in some environments.
|
|
Consider using tcsh (enhanced csh) or bash instead.
|
|
|
|
USAGE:
|
|
csh("ls -la") # Single command
|
|
csh(["a", "b", "c"]) # Sequential
|
|
csh(["a", "b"], parallel=True) # Parallel
|
|
|
|
AUTO-BACKGROUNDING: Commands exceeding 45s auto-background."""
|
|
|
|
@override
|
|
def register(self, mcp_server: FastMCP) -> None:
|
|
"""Register csh tool with MCP server."""
|
|
tool_self = self
|
|
|
|
@mcp_server.tool(name=self.name, description=self.description)
|
|
async def csh_handler(
|
|
command: Annotated[
|
|
Optional[str],
|
|
Field(description="Single command to execute", default=None),
|
|
] = None,
|
|
commands: Annotated[
|
|
Optional[List[Any]], Field(description="List of commands", default=None)
|
|
] = None,
|
|
parallel: Annotated[
|
|
bool, Field(description="Run in parallel", default=False)
|
|
] = False,
|
|
cwd: Annotated[
|
|
Optional[str], Field(description="Working directory", default=None)
|
|
] = None,
|
|
env: Annotated[
|
|
Optional[Dict[str, str]],
|
|
Field(description="Environment variables", default=None),
|
|
] = None,
|
|
timeout: Annotated[
|
|
int, Field(description="Timeout (seconds)", default=30)
|
|
] = 30,
|
|
strict: Annotated[
|
|
bool, Field(description="Stop on first error", default=False)
|
|
] = False,
|
|
quiet: Annotated[
|
|
bool, Field(description="Suppress stdout", default=False)
|
|
] = False,
|
|
ctx: MCPContext = None,
|
|
) -> str:
|
|
return await tool_self.call(
|
|
ctx,
|
|
command=command,
|
|
commands=commands,
|
|
parallel=parallel,
|
|
shell=None,
|
|
cwd=cwd,
|
|
env=env,
|
|
timeout=timeout,
|
|
strict=strict,
|
|
quiet=quiet,
|
|
)
|
|
|
|
|
|
class ShellTool(CmdTool):
|
|
"""Smart shell - auto-selects best available shell (zsh > bash > fish > dash > ksh > sh)."""
|
|
|
|
name = "shell"
|
|
|
|
def __init__(self, tools: Optional[Dict[str, BaseTool]] = None):
|
|
"""Initialize with best available shell."""
|
|
super().__init__(
|
|
tools=tools, default_shell="zsh"
|
|
) # Will resolve to best available
|
|
|
|
@property
|
|
@override
|
|
def description(self) -> str:
|
|
shell_name = os.path.basename(self.default_shell)
|
|
return f"""Smart shell (auto-selected: {shell_name}).
|
|
|
|
Automatically uses the best available shell:
|
|
zsh > bash > fish > dash > sh
|
|
|
|
USAGE:
|
|
shell("ls -la") # Single command
|
|
shell(["a", "b", "c"]) # Sequential
|
|
shell(["a", "b"], parallel=True) # Parallel
|
|
|
|
AUTO-BACKGROUNDING: Commands exceeding 45s auto-background."""
|
|
|
|
@override
|
|
def register(self, mcp_server: FastMCP) -> None:
|
|
"""Register shell tool with MCP server."""
|
|
tool_self = self
|
|
|
|
@mcp_server.tool(name=self.name, description=self.description)
|
|
async def shell_handler(
|
|
command: Annotated[
|
|
Optional[str],
|
|
Field(description="Single command to execute", default=None),
|
|
] = None,
|
|
commands: Annotated[
|
|
Optional[List[Any]], Field(description="List of commands", default=None)
|
|
] = None,
|
|
parallel: Annotated[
|
|
bool, Field(description="Run in parallel", default=False)
|
|
] = False,
|
|
cwd: Annotated[
|
|
Optional[str], Field(description="Working directory", default=None)
|
|
] = None,
|
|
env: Annotated[
|
|
Optional[Dict[str, str]],
|
|
Field(description="Environment variables", default=None),
|
|
] = None,
|
|
timeout: Annotated[
|
|
int, Field(description="Timeout (seconds)", default=30)
|
|
] = 30,
|
|
strict: Annotated[
|
|
bool, Field(description="Stop on first error", default=False)
|
|
] = False,
|
|
quiet: Annotated[
|
|
bool, Field(description="Suppress stdout", default=False)
|
|
] = False,
|
|
ctx: MCPContext = None,
|
|
) -> str:
|
|
return await tool_self.call(
|
|
ctx,
|
|
command=command,
|
|
commands=commands,
|
|
parallel=parallel,
|
|
shell=None, # Use default
|
|
cwd=cwd,
|
|
env=env,
|
|
timeout=timeout,
|
|
strict=strict,
|
|
quiet=quiet,
|
|
)
|
|
|
|
|
|
# Singleton instances
|
|
zsh_tool = ZshTool()
|
|
bash_tool = BashTool()
|
|
fish_tool = FishTool()
|
|
dash_tool = DashTool()
|
|
ksh_tool = KshTool()
|
|
tcsh_tool = TcshTool()
|
|
csh_tool = CshTool()
|
|
shell_tool = ShellTool()
|