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
"""Configuration management for Host Terminal MCP."""
import os
import re
from enum import Enum
from pathlib import Path
import yaml
from pydantic import BaseModel, Field
class PermissionMode(str, Enum):
"""Permission modes for command execution."""
ALLOWLIST = "allowlist" # Only allow commands in the allow list
ASK = "ask" # Ask for permission if command not in allow list
ALLOW_ALL = "allow_all" # Allow all commands (dangerous!)
class CommandPattern(BaseModel):
"""A pattern for matching commands."""
pattern: str = Field(description="Regex pattern or exact command prefix")
description: str = Field(default="", description="Human-readable description")
is_regex: bool = Field(default=False, description="Whether pattern is a regex")
def matches(self, command: str) -> bool:
"""Check if a command matches this pattern."""
if self.is_regex:
try:
return bool(re.match(self.pattern, command))
except re.error:
return False
else:
if command == self.pattern:
return True
# Patterns ending with whitespace (e.g. "echo ", "sudo ") already
# encode the word boundary, so plain startswith is correct.
if self.pattern.endswith((" ", "\t")):
return command.startswith(self.pattern)
# Otherwise require a separator after the match so that
# "ls" matches "ls -la" but not "lsof".
return command.startswith(self.pattern + " ") or command.startswith(self.pattern + "\t")
class Config(BaseModel):
"""Configuration for Host Terminal MCP."""
permission_mode: PermissionMode = Field(
default=PermissionMode.ALLOWLIST,
description="How to handle commands not in the allow list"
)
allowed_commands: list[CommandPattern] = Field(
default_factory=list,
description="List of allowed command patterns"
)
blocked_commands: list[CommandPattern] = Field(
default_factory=list,
description="List of blocked command patterns (takes precedence over allowed)"
)
allowed_directories: list[str] = Field(
default_factory=lambda: [str(Path.home())],
description="Directories where commands can be executed"
)
timeout_seconds: int = Field(
default=300,
description="Maximum execution time for commands (seconds)"
)
max_output_size: int = Field(
default=100000,
description="Maximum output size in characters"
)
shell: str = Field(
default="/bin/bash",
description="Shell to use for command execution"
)
environment_passthrough: list[str] = Field(
default_factory=lambda: ["PATH", "HOME", "USER", "LANG", "LC_ALL"],
description="Environment variables to pass through to commands"
)
# Commands that have been dynamically approved during the session
session_approved_commands: list[str] = Field(
default_factory=list,
description="Commands approved during this session (not persisted)"
)
def is_command_allowed(self, command: str) -> tuple[bool, str]:
"""
Check if a command is allowed.
Returns:
Tuple of (is_allowed, reason)
"""
# First check blocked commands - they always take precedence
for blocked in self.blocked_commands:
if blocked.matches(command):
return False, f"Command matches blocked pattern: {blocked.description or blocked.pattern}"
# Check if in allowed commands
for allowed in self.allowed_commands:
if allowed.matches(command):
return True, f"Command matches allowed pattern: {allowed.description or allowed.pattern}"
# Check session-approved commands
if command in self.session_approved_commands:
return True, "Command was approved during this session"
# Handle based on permission mode
if self.permission_mode == PermissionMode.ALLOW_ALL:
return True, "allow_all mode is enabled"
elif self.permission_mode == PermissionMode.ASK:
return False, "NEEDS_APPROVAL"
else: # ALLOWLIST
return False, "Command not in allow list"
def approve_command_for_session(self, command: str) -> None:
"""Approve a command for the current session."""
if command not in self.session_approved_commands:
self.session_approved_commands.append(command)
def get_default_config_path() -> Path:
"""Get the default configuration file path."""
# Check XDG config directory first
xdg_config = os.environ.get("XDG_CONFIG_HOME")
if xdg_config:
config_dir = Path(xdg_config) / "host-terminal-mcp"
else:
config_dir = Path.home() / ".config" / "host-terminal-mcp"
return config_dir / "config.yaml"
def get_default_allowed_commands() -> list[CommandPattern]:
"""Get the default list of allowed developer commands."""
return [
# File listing and navigation
CommandPattern(pattern="ls", description="List directory contents"),
CommandPattern(pattern="ll", description="List directory contents (long format alias)"),
CommandPattern(pattern="la", description="List all files including hidden"),
CommandPattern(pattern="pwd", description="Print working directory"),
CommandPattern(pattern="tree", description="Display directory tree"),
CommandPattern(pattern="find ", description="Find files", is_regex=False),
CommandPattern(pattern="locate ", description="Locate files"),
CommandPattern(pattern="which ", description="Locate a command"),
CommandPattern(pattern="whereis ", description="Locate binary, source, and manual"),
CommandPattern(pattern="file ", description="Determine file type"),
# File content viewing
CommandPattern(pattern="cat ", description="Display file contents"),
CommandPattern(pattern="head ", description="Display first lines of file"),
CommandPattern(pattern="tail ", description="Display last lines of file"),
CommandPattern(pattern="less ", description="View file with pagination"),
CommandPattern(pattern="more ", description="View file with pagination"),
CommandPattern(pattern="bat ", description="Cat with syntax highlighting"),
CommandPattern(pattern="wc ", description="Word, line, character count"),
# Search and grep
CommandPattern(pattern="grep ", description="Search text patterns"),
CommandPattern(pattern="rg ", description="Ripgrep - fast search"),
CommandPattern(pattern="ag ", description="Silver searcher"),
CommandPattern(pattern="ack ", description="Ack search tool"),
CommandPattern(pattern="fzf", description="Fuzzy finder"),
# Git read operations
CommandPattern(pattern="git status", description="Git status"),
CommandPattern(pattern="git log", description="Git log"),
CommandPattern(pattern="git diff", description="Git diff"),
CommandPattern(pattern="git show", description="Git show"),
CommandPattern(pattern="git branch", description="Git branches"),
CommandPattern(pattern="git remote", description="Git remotes"),
CommandPattern(pattern="git tag", description="Git tags"),
CommandPattern(pattern="git stash list", description="Git stash list"),
CommandPattern(pattern="git rev-parse", description="Git rev-parse"),
CommandPattern(pattern="git config --get", description="Git config read"),
CommandPattern(pattern="git config --list", description="Git config list"),
CommandPattern(pattern="git blame", description="Git blame"),
CommandPattern(pattern="git shortlog", description="Git shortlog"),
CommandPattern(pattern="git describe", description="Git describe"),
# System info
CommandPattern(pattern="uname", description="System info"),
CommandPattern(pattern="hostname", description="System hostname"),
CommandPattern(pattern="whoami", description="Current user"),
CommandPattern(pattern="id", description="User/group IDs"),
CommandPattern(pattern="date", description="Current date/time"),
CommandPattern(pattern="uptime", description="System uptime"),
CommandPattern(pattern="df", description="Disk space usage"),
CommandPattern(pattern="du ", description="Directory space usage"),
CommandPattern(pattern="free", description="Memory usage"),
CommandPattern(pattern="top -l 1", description="Process info (macOS)"),
CommandPattern(pattern="ps", description="Process status"),
# Network info (read-only)
CommandPattern(pattern="ping -c", description="Ping with count"),
CommandPattern(pattern="curl -I", description="HTTP headers only"),
CommandPattern(pattern="curl --head", description="HTTP headers only"),
CommandPattern(pattern="dig ", description="DNS lookup"),
CommandPattern(pattern="nslookup ", description="DNS lookup"),
CommandPattern(pattern="host ", description="DNS lookup"),
CommandPattern(pattern="ifconfig", description="Network interfaces"),
CommandPattern(pattern="ip addr", description="IP addresses"),
CommandPattern(pattern="netstat", description="Network stats"),
CommandPattern(pattern="ss ", description="Socket stats"),
# Package managers (info only)
CommandPattern(pattern="npm list", description="NPM list packages"),
CommandPattern(pattern="npm ls", description="NPM list packages"),
CommandPattern(pattern="npm view", description="NPM view package"),
CommandPattern(pattern="npm show", description="NPM show package"),
CommandPattern(pattern="npm outdated", description="NPM outdated packages"),
CommandPattern(pattern="pip list", description="Pip list packages"),
CommandPattern(pattern="pip show", description="Pip show package"),
CommandPattern(pattern="pip freeze", description="Pip freeze"),
CommandPattern(pattern="brew list", description="Homebrew list"),
CommandPattern(pattern="brew info", description="Homebrew info"),
CommandPattern(pattern="apt list", description="APT list packages"),
CommandPattern(pattern="dpkg -l", description="DPKG list packages"),
# Development tools (read operations)
CommandPattern(pattern="python --version", description="Python version"),
CommandPattern(pattern="python3 --version", description="Python3 version"),
CommandPattern(pattern="node --version", description="Node version"),
CommandPattern(pattern="npm --version", description="NPM version"),
CommandPattern(pattern="cargo --version", description="Cargo version"),
CommandPattern(pattern="rustc --version", description="Rust version"),
CommandPattern(pattern="go version", description="Go version"),
CommandPattern(pattern="java --version", description="Java version"),
CommandPattern(pattern="javac --version", description="Javac version"),
CommandPattern(pattern="ruby --version", description="Ruby version"),
CommandPattern(pattern="docker --version", description="Docker version"),
CommandPattern(pattern="docker ps", description="Docker containers"),
CommandPattern(pattern="docker images", description="Docker images"),
CommandPattern(pattern="docker logs", description="Docker logs"),
# JSON/YAML processing
CommandPattern(pattern="jq ", description="JSON processor"),
CommandPattern(pattern="yq ", description="YAML processor"),
# Misc read operations
CommandPattern(pattern="man ", description="Manual pages"),
CommandPattern(pattern="help ", description="Help for commands"),
CommandPattern(pattern="type ", description="Command type"),
CommandPattern(pattern="stat ", description="File statistics"),
CommandPattern(pattern="md5sum ", description="MD5 checksum"),
CommandPattern(pattern="sha256sum ", description="SHA256 checksum"),
CommandPattern(pattern="shasum ", description="SHA checksum"),
]
def get_default_blocked_commands() -> list[CommandPattern]:
"""Get the default list of blocked commands."""
return [
# --- Destructive file operations ---
CommandPattern(
pattern=r"^rm\s+-rf\s+/",
description="Recursive force-delete from root. Blocked regex: ^rm\\s+-rf\\s+/",
is_regex=True,
),
CommandPattern(
pattern=r"^rm\s+-rf\s+~",
description="Recursive force-delete home directory. Blocked regex: ^rm\\s+-rf\\s+~",
is_regex=True,
),
CommandPattern(
pattern=r"^rm\s+-rf\s+\*",
description="Recursive force-delete wildcard. Blocked regex: ^rm\\s+-rf\\s+\\*",
is_regex=True,
),
CommandPattern(
pattern=r"^rm\s+-rf\s+\.",
description="Recursive force-delete current/parent directory. Blocked regex: ^rm\\s+-rf\\s+\\.",
is_regex=True,
),
CommandPattern(
pattern=r"^rm\s+-fr\s+/",
description="Recursive force-delete root (reversed flags). Blocked regex: ^rm\\s+-fr\\s+/",
is_regex=True,
),
CommandPattern(
pattern=r"^rm\s+--recursive",
description="Recursive delete using long flag. Blocked regex: ^rm\\s+--recursive",
is_regex=True,
),
CommandPattern(pattern="mkfs", description="Format filesystem"),
CommandPattern(pattern="dd", description="Disk duplicator — can overwrite drives and partitions"),
# --- Arbitrary command execution via find ---
CommandPattern(
pattern=r"^find\s+.*-exec",
description=(
"find with -exec/-execdir can run arbitrary commands "
"(e.g. find / -exec rm -rf {} \\;). "
"Use find without -exec, or pipe to xargs. "
"Blocked regex: ^find\\s+.*-exec"
),
is_regex=True,
),
# --- Fork bomb / device overwrite ---
CommandPattern(pattern=":(){", description="Fork bomb"),
CommandPattern(
pattern=r".*>\s*/dev/(sd|nvme|disk|vd|hd)",
description=(
"Redirect output to raw disk device. "
"Blocked regex: .*>\\s*/dev/(sd|nvme|disk|vd|hd)"
),
is_regex=True,
),
# --- Dangerous permission changes ---
CommandPattern(pattern="chmod -R 777 /", description="Recursive world-writable permissions on root"),
CommandPattern(pattern="chmod 777 /", description="World-writable permissions on root"),
CommandPattern(pattern="chmod -R 777 ~", description="Recursive world-writable permissions on home"),
CommandPattern(pattern="chown -R ", description="Recursive ownership change"),
# --- Privilege escalation ---
CommandPattern(pattern="sudo ", description="Superuser commands"),
CommandPattern(pattern="su ", description="Switch user"),
CommandPattern(pattern="doas ", description="OpenBSD sudo alternative"),
# --- System shutdown / process control ---
CommandPattern(pattern="reboot", description="System reboot"),
CommandPattern(pattern="shutdown", description="System shutdown"),
CommandPattern(pattern="halt", description="System halt"),
CommandPattern(pattern="poweroff", description="System power off"),
CommandPattern(pattern="kill", description="Kill process"),
CommandPattern(pattern="killall", description="Kill processes by name"),
CommandPattern(pattern="pkill", description="Kill processes by pattern"),
# --- Network attacks ---
CommandPattern(pattern="nc -l", description="Netcat listener"),
CommandPattern(pattern="nmap ", description="Port scanner"),
# --- Sensitive file access (blocked regardless of reader command) ---
CommandPattern(
pattern=r".*/etc/shadow",
description="Access to shadow password file. Blocked regex: .*/etc/shadow",
is_regex=True,
),
CommandPattern(
pattern=r".*/etc/passwd",
description="Access to system user file. Blocked regex: .*/etc/passwd",
is_regex=True,
),
CommandPattern(
pattern=r".*\.ssh/",
description="Access to SSH keys and configuration (~/.ssh/). Blocked regex: .*\\.ssh/",
is_regex=True,
),
CommandPattern(
pattern=r".*\.aws/",
description="Access to AWS credentials (~/.aws/). Blocked regex: .*\\.aws/",
is_regex=True,
),
CommandPattern(
pattern=r".*\.gnupg/",
description="Access to GPG keys (~/.gnupg/). Blocked regex: .*\\.gnupg/",
is_regex=True,
),
# --- History/credential wiping ---
CommandPattern(pattern="history -c", description="Clear history"),
CommandPattern(pattern="shred ", description="Secure delete"),
]
def load_config(config_path: Path | None = None) -> Config:
"""Load configuration from file or create default."""
if config_path is None:
config_path = get_default_config_path()
if config_path.exists():
with open(config_path) as f:
data = yaml.safe_load(f) or {}
# Convert command patterns
if "allowed_commands" in data:
data["allowed_commands"] = [
CommandPattern(**cmd) if isinstance(cmd, dict) else CommandPattern(pattern=cmd)
for cmd in data["allowed_commands"]
]
if "blocked_commands" in data:
data["blocked_commands"] = [
CommandPattern(**cmd) if isinstance(cmd, dict) else CommandPattern(pattern=cmd)
for cmd in data["blocked_commands"]
]
return Config(**data)
# Return default config
return Config(
allowed_commands=get_default_allowed_commands(),
blocked_commands=get_default_blocked_commands(),
)
def save_config(config: Config, config_path: Path | None = None) -> None:
"""Save configuration to file."""
if config_path is None:
config_path = get_default_config_path()
# Ensure directory exists
config_path.parent.mkdir(parents=True, exist_ok=True)
# Convert to dict, excluding session-only fields
data = config.model_dump(exclude={"session_approved_commands"})
# Convert permission_mode enum to string value
data["permission_mode"] = config.permission_mode.value
# Convert CommandPattern objects to dicts
data["allowed_commands"] = [
{"pattern": cmd["pattern"], "description": cmd["description"], "is_regex": cmd["is_regex"]}
for cmd in data["allowed_commands"]
]
data["blocked_commands"] = [
{"pattern": cmd["pattern"], "description": cmd["description"], "is_regex": cmd["is_regex"]}
for cmd in data["blocked_commands"]
]
with open(config_path, "w") as f:
yaml.safe_dump(data, f, default_flow_style=False, sort_keys=False)
def create_default_config_file(config_path: Path | None = None) -> Path:
"""Create a default configuration file."""
if config_path is None:
config_path = get_default_config_path()
config = Config(
allowed_commands=get_default_allowed_commands(),
blocked_commands=get_default_blocked_commands(),
)
save_config(config, config_path)
return config_path