| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121 |
- import os
- import subprocess
- from pathlib import Path
- from mcp.server.fastmcp import FastMCP
- mcp = FastMCP("terminal", host="0.0.0.0", port=8011)
- # Allowed directories for file operations
- ALLOWED_DIRS = ["/projects", "/data", "/scripts", "/backups", "/tmp"]
- ALLOWED_PREFIXES = [
- "ls", "cat", "head", "tail", "wc", "find", "grep", "rg", "which",
- "pwd", "whoami", "id", "date", "uname", "df", "du", "ps",
- "free", "uptime", "env", "echo", "sort", "uniq", "cut", "tr",
- "diff", "cmp", "stat", "file", "tree",
- "mkdir", "touch", "npm", "npx", "node", "python3", "ng",
- "docker ps", "docker images", "docker compose ps",
- "docker inspect", "docker logs",
- "curl", "ping", "traceroute", "nslookup", "dig", "host",
- ]
- BLOCKED = [
- "rm ", "dd ", "mkfs.", "shutdown", "reboot", "halt",
- "sudo", "su", "chmod", "chown", "passwd", "kill ",
- "|", "&&", "||", "`", "$(",
- "docker rm", "docker rmi", "docker stop", "docker kill",
- "docker compose down", "docker compose rm",
- ]
- def _is_path_allowed(path: str) -> bool:
- resolved = Path(path).resolve()
- for allowed in ALLOWED_DIRS:
- allowed_path = Path(allowed).resolve()
- if allowed_path in resolved.parents or allowed_path == resolved:
- return True
- return False
- @mcp.tool()
- def run_command(command: str, timeout: int = 60) -> str:
- """Execute a shell command.
- Allowed: development commands (npm, npx, node, python3, ng, mkdir, touch),
- read-only commands (ls, cat, head, tail, grep, find, etc.), and docker inspect.
- File redirect '>' is allowed only when writing to /projects/ paths.
- """
- if not command or len(command) > 4000:
- return "Error: command is empty or too long"
- lowered = command.strip().lower()
- # Check for blocked patterns first
- for b in BLOCKED:
- if b in lowered:
- return f"Error: blocked pattern: {b}"
- # For redirects, check allowed path
- if ">" in lowered or ">>" in lowered:
- # Extract the redirect target path (last argument after >)
- parts = command.split()
- for i, p in enumerate(parts):
- if p in (">", ">>"):
- target = parts[i + 1] if i + 1 < len(parts) else ""
- if target and not _is_path_allowed(target):
- return f"Error: redirect target not in allowed directories: {target}"
- # Allow redirect commands without checking prefix
- else:
- ok = any(lowered.startswith(p) for p in ALLOWED_PREFIXES)
- if not ok:
- return "Error: command not in allowed list"
- try:
- r = subprocess.run(
- ["sh", "-c", command],
- capture_output=True, text=True, timeout=min(timeout, 120),
- )
- out = r.stdout or ""
- if r.stderr:
- out += "\n--- stderr ---\n" + r.stderr
- if r.returncode != 0:
- out += f"\n(exit code: {r.returncode})"
- # Truncate output
- if len(out) > 50000:
- out = out[:50000] + "\n... (output truncated)"
- return out or "(no output)"
- except subprocess.TimeoutExpired:
- return f"Error: timed out after {timeout}s"
- except Exception as e:
- return f"Error: {e}"
- @mcp.tool()
- def write_file(path: str, content: str) -> str:
- """Write or overwrite a file with the given content. Creates parent directories.
- Only allowed within: /projects, /data, /scripts, /backups, /tmp
- """
- if not _is_path_allowed(path):
- return f"Error: path not in allowed directories: {', '.join(ALLOWED_DIRS)}"
- try:
- p = Path(path).resolve()
- p.parent.mkdir(parents=True, exist_ok=True)
- p.write_text(content)
- return f"OK - wrote {len(content)} bytes to {path}"
- except Exception as e:
- return f"Error writing file: {e}"
- @mcp.tool()
- def create_directory(path: str) -> str:
- """Create a directory and all parent directories if they do not exist.
- Only allowed within: /projects, /data, /scripts, /backups, /tmp
- """
- if not _is_path_allowed(path):
- return f"Error: path not in allowed directories: {', '.join(ALLOWED_DIRS)}"
- try:
- p = Path(path).resolve()
- p.mkdir(parents=True, exist_ok=True)
- return f"OK - created directory {path}"
- except Exception as e:
- return f"Error creating directory: {e}"
- if __name__ == "__main__":
- mcp.run(transport="stdio")
|