We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/ankitaa186/host-terminal-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
"""Command execution with safety controls."""
import asyncio
import os
from dataclasses import dataclass
from pathlib import Path
from .config import Config
@dataclass
class ExecutionResult:
"""Result of command execution."""
command: str
stdout: str
stderr: str
return_code: int
timed_out: bool = False
truncated: bool = False
working_directory: str = ""
class CommandExecutor:
"""Execute commands with safety controls."""
def __init__(self, config: Config):
self.config = config
self._current_directory = str(Path.home())
@property
def current_directory(self) -> str:
"""Get current working directory."""
return self._current_directory
def change_directory(self, path: str) -> tuple[bool, str]:
"""
Change the current working directory.
Returns:
Tuple of (success, message)
"""
# Expand user home directory
expanded_path = os.path.expanduser(path)
# Make absolute if relative
if not os.path.isabs(expanded_path):
expanded_path = os.path.join(self._current_directory, expanded_path)
# Normalize the path
normalized_path = os.path.normpath(expanded_path)
# Check if path exists and is a directory
if not os.path.exists(normalized_path):
return False, f"Directory does not exist: {normalized_path}"
if not os.path.isdir(normalized_path):
return False, f"Not a directory: {normalized_path}"
# Resolve symlinks to prevent traversal outside allowed directories
normalized_path = os.path.realpath(normalized_path)
# Check if path is in allowed directories
is_allowed = False
for allowed_dir in self.config.allowed_directories:
allowed_expanded = os.path.realpath(os.path.expanduser(allowed_dir))
if normalized_path.startswith(allowed_expanded):
is_allowed = True
break
if not is_allowed:
return False, f"Directory not in allowed paths: {normalized_path}"
self._current_directory = normalized_path
return True, f"Changed directory to: {normalized_path}"
def _build_environment(self) -> dict[str, str]:
"""Build environment variables for command execution."""
env = {}
for var in self.config.environment_passthrough:
if var in os.environ:
env[var] = os.environ[var]
return env
async def execute(
self,
command: str,
working_directory: str | None = None,
) -> ExecutionResult:
"""
Execute a command.
Args:
command: The command to execute
working_directory: Optional working directory (uses current if not specified)
Returns:
ExecutionResult with stdout, stderr, return code, etc.
"""
# Use provided directory or current directory
cwd = working_directory or self._current_directory
# Expand, normalize, and resolve symlinks to prevent traversal
cwd = os.path.realpath(os.path.expanduser(cwd))
# Verify working directory is allowed
is_allowed = False
for allowed_dir in self.config.allowed_directories:
allowed_expanded = os.path.realpath(os.path.expanduser(allowed_dir))
if cwd.startswith(allowed_expanded):
is_allowed = True
break
if not is_allowed:
return ExecutionResult(
command=command,
stdout="",
stderr=f"Working directory not allowed: {cwd}",
return_code=1,
working_directory=cwd,
)
# Build environment
env = self._build_environment()
# Execute the command
try:
process = await asyncio.create_subprocess_shell(
command,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
cwd=cwd,
env=env,
shell=True,
executable=self.config.shell,
)
try:
stdout_bytes, stderr_bytes = await asyncio.wait_for(
process.communicate(),
timeout=self.config.timeout_seconds,
)
except asyncio.TimeoutError:
process.kill()
await process.wait()
return ExecutionResult(
command=command,
stdout="",
stderr=f"Command timed out after {self.config.timeout_seconds} seconds",
return_code=-1,
timed_out=True,
working_directory=cwd,
)
# Decode output
stdout = stdout_bytes.decode("utf-8", errors="replace")
stderr = stderr_bytes.decode("utf-8", errors="replace")
# Truncate if too large
truncated = False
if len(stdout) > self.config.max_output_size:
stdout = stdout[: self.config.max_output_size]
stdout += f"\n\n[Output truncated at {self.config.max_output_size} characters]"
truncated = True
if len(stderr) > self.config.max_output_size:
stderr = stderr[: self.config.max_output_size]
stderr += f"\n\n[Output truncated at {self.config.max_output_size} characters]"
truncated = True
return ExecutionResult(
command=command,
stdout=stdout,
stderr=stderr,
return_code=process.returncode or 0,
truncated=truncated,
working_directory=cwd,
)
except Exception as e:
return ExecutionResult(
command=command,
stdout="",
stderr=f"Execution error: {str(e)}",
return_code=1,
working_directory=cwd,
)