Skip to main content
Glama
wehnsdaefflae

Interactive Automation MCP Server

terminal_utils.py7.49 kB
#!/usr/bin/env python3 """ Terminal utility functions for session management Shared utilities for terminal window management and tmux operations """ import asyncio import logging import shutil from .settings import ServerConfig logger = logging.getLogger(__name__) def detect_terminal_emulator() -> str | None: """Detect available terminal emulator using configuration""" config = ServerConfig() terminal_emulators = config.terminal_emulators for emulator in terminal_emulators: name = emulator["name"] command = emulator["command"] if shutil.which(command[0]): logger.info(f"Detected terminal emulator: {name}") return str(command[0]) return None def _build_terminal_command(terminal_cmd: str, tmux_session_name: str) -> list[str]: """Build the appropriate command for different terminal emulators using configuration""" config = ServerConfig() # Find the matching emulator configuration for emulator in config.terminal_emulators: if emulator["command"][0] == terminal_cmd: # Get the base command from configuration base_command: list[str] = list(emulator["command"]) # Handle special cases for different terminal types if terminal_cmd == "open": # macOS Terminal return [ "open", "-a", "Terminal", "--args", "tmux", "attach-session", "-t", tmux_session_name, ] elif terminal_cmd == "kitty": # Kitty doesn't use -e return ["kitty", "tmux", "attach-session", "-t", tmux_session_name] else: # Most terminals use the pattern: terminal [args] tmux attach-session -t session return base_command + [ "tmux", "attach-session", "-t", tmux_session_name, ] # Fallback if not found in configuration return [terminal_cmd, "-e", "tmux", "attach-session", "-t", tmux_session_name] def _prepare_environment() -> dict[str, str]: """Prepare environment variables for terminal process""" import os env = os.environ.copy() env["DISPLAY"] = env.get("DISPLAY", ":0") # Remove Snap-related environment variables to fix VS Code/Snap conflicts # VS Code (Snap package) sets various environment variables that redirect # system commands to use Snap's bundled libraries, causing symbol lookup errors snap_vars_to_remove = [ "LD_LIBRARY_PATH", "GTK_PATH", "GTK_EXE_PREFIX", "GIO_MODULE_DIR", "GSETTINGS_SCHEMA_DIR", "GTK_IM_MODULE_FILE", "LOCPATH", "XDG_DATA_HOME", "XDG_DATA_DIRS", ] for var in snap_vars_to_remove: env.pop(var, None) # Restore original values if they exist (VS Code saves originals with _VSCODE_SNAP_ORIG suffix) snap_orig_vars = { "XDG_CONFIG_DIRS": "XDG_CONFIG_DIRS_VSCODE_SNAP_ORIG", "GDK_BACKEND": "GDK_BACKEND_VSCODE_SNAP_ORIG", "GIO_MODULE_DIR": "GIO_MODULE_DIR_VSCODE_SNAP_ORIG", "GSETTINGS_SCHEMA_DIR": "GSETTINGS_SCHEMA_DIR_VSCODE_SNAP_ORIG", "GTK_IM_MODULE_FILE": "GTK_IM_MODULE_FILE_VSCODE_SNAP_ORIG", "XDG_DATA_HOME": "XDG_DATA_HOME_VSCODE_SNAP_ORIG", "GTK_EXE_PREFIX": "GTK_EXE_PREFIX_VSCODE_SNAP_ORIG", "GTK_PATH": "GTK_PATH_VSCODE_SNAP_ORIG", "XDG_DATA_DIRS": "XDG_DATA_DIRS_VSCODE_SNAP_ORIG", "LOCPATH": "LOCPATH_VSCODE_SNAP_ORIG", } for env_var, orig_var in snap_orig_vars.items(): if orig_var in env and env[orig_var]: env[env_var] = env[orig_var] # Remove the _VSCODE_SNAP_ORIG variables as they're not needed env.pop(orig_var, None) return env async def _check_process_result( process: asyncio.subprocess.Process, session_id: str, cmd: list[str] ) -> bool: """Check if the terminal process started successfully""" try: config = ServerConfig() await asyncio.wait_for( process.wait(), timeout=config.terminal_process_check_timeout ) if process.returncode == 0: logger.info(f"Terminal window opened successfully for session {session_id}") return True else: stderr = ( await process.stderr.read() if process.stderr else b"No stderr available" ) logger.warning( f"Terminal window failed with return code {process.returncode}: {stderr.decode()}" ) return False except TimeoutError: # Process is still running, which is good - terminal is open logger.info(f"Terminal window opened for session {session_id}: {' '.join(cmd)}") return True async def open_terminal_window(session_id: str) -> bool: """Open a terminal window that attaches to the tmux session""" terminal_cmd = detect_terminal_emulator() if not terminal_cmd: logger.warning( f"No terminal emulator found - user will need to manually attach with: tmux attach -t {session_id}" ) return False try: tmux_session_name = f"mcp_{session_id}" cmd = _build_terminal_command(terminal_cmd, tmux_session_name) env = _prepare_environment() process = await asyncio.create_subprocess_exec( *cmd, stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.PIPE, env=env, ) return await _check_process_result(process, session_id, cmd) except Exception as e: logger.warning(f"Failed to open terminal window for session {session_id}: {e}") logger.info( f"User can manually attach with: tmux attach-session -t mcp_{session_id}" ) return False async def close_terminal_window(session_id: str) -> bool: """Close terminal windows that are attached to the tmux session""" try: # Build the tmux session name (sessions are prefixed with 'mcp_') tmux_session_name = f"mcp_{session_id}" # Use tmux to kill the session, which will close attached terminals cmd = ["tmux", "kill-session", "-t", tmux_session_name] process = await asyncio.create_subprocess_exec( *cmd, stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.PIPE ) # Wait for the command to complete config = ServerConfig() await asyncio.wait_for(process.wait(), timeout=config.terminal_close_timeout) if process.returncode == 0: logger.info(f"Terminal window closed successfully for session {session_id}") return True else: stderr = ( await process.stderr.read() if process.stderr else b"No stderr available" ) logger.warning( f"Failed to close terminal window for session {session_id}: {stderr.decode()}" ) return False except TimeoutError: logger.warning(f"Timeout closing terminal window for session {session_id}") return False except Exception as e: logger.error(f"Error closing terminal window for session {session_id}: {e}") return False

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/wehnsdaefflae/MCPAutomationServer'

If you have feedback or need assistance with the MCP directory API, please join our Discord server