Skip to main content
Glama
wehnsdaefflae

Interactive Automation MCP Server

test_mcp_integration.py49.3 kB
#!/usr/bin/env python3 """ Integration tests for the MCP server with security validation Tests the complete workflow including security checks """ import asyncio import os import sys from types import SimpleNamespace from unittest.mock import patch import pytest # Add project root to path sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) from src.terminal_control_mcp.main import ( await_output, exit_terminal, get_screen_content, list_terminal_sessions, open_terminal, send_input, ) from src.terminal_control_mcp.models import ( AwaitOutputRequest, DestroySessionRequest, GetScreenContentRequest, OpenTerminalRequest, SendInputRequest, ) from src.terminal_control_mcp.security import SecurityManager from src.terminal_control_mcp.session_manager import SessionManager # Test timing constants AWAIT_OUTPUT_MAX_TIME = 5.0 AWAIT_OUTPUT_TIMEOUT_BUFFER = 1.5 AWAIT_OUTPUT_QUICK_TIMEOUT = 3.0 AWAIT_OUTPUT_LONG_TIMEOUT_MIN = 9.5 AWAIT_OUTPUT_LONG_TIMEOUT_MAX = 11.0 class TestMCPIntegration: """Integration tests for MCP server functionality with security""" @pytest.mark.asyncio async def test_basic_commands(self, mock_context): """Test basic non-interactive commands with security validation""" test_commands = [ ("echo 'Hello World'\n", "Hello World"), ("python3 --version\n", "Python"), ("whoami\n", ""), ("pwd\n", "/"), ("date\n", "2025"), ] for command, expected_content in test_commands: request = OpenTerminalRequest(shell="bash") result = await open_terminal(request, mock_context) assert result.success, "Failed to open terminal" # Send command to shell if result.session_id: input_request = SendInputRequest( session_id=result.session_id, input_text=command ) await send_input(input_request, mock_context) # Get screen content to check output if expected_content: await asyncio.sleep(0.5) # Give command time to execute screen_request = GetScreenContentRequest( session_id=result.session_id ) screen_result = await get_screen_content( screen_request, mock_context ) if screen_result.success and screen_result.screen_content: assert expected_content in screen_result.screen_content # Cleanup destroy_request = DestroySessionRequest(session_id=result.session_id) await exit_terminal(destroy_request, mock_context) @pytest.mark.asyncio async def test_dangerous_command_blocking(self, mock_context, dangerous_commands): """Test that dangerous commands are blocked by security""" # Only test commands that are actually blocked by send_input validation dangerous_input_commands = ["sudo rm -rf /", "su - root", "passwd"] for command in dangerous_input_commands: # Create terminal first request = OpenTerminalRequest(shell="bash") result = await open_terminal(request, mock_context) if result.success and result.session_id: # Now try to send dangerous command - should be blocked input_request = SendInputRequest( session_id=result.session_id, input_text=command ) with pytest.raises(ValueError, match="Security violation"): await send_input(input_request, mock_context) # Cleanup destroy_request = DestroySessionRequest(session_id=result.session_id) await exit_terminal(destroy_request, mock_context) @pytest.mark.asyncio async def test_path_validation_in_working_directory(self, mock_context): """Test path validation for working directory""" # Safe working directory should work safe_request = OpenTerminalRequest(shell="bash", working_directory="/tmp") result = await open_terminal(safe_request, mock_context) assert result.success # Cleanup if result.session_id: destroy_request = DestroySessionRequest(session_id=result.session_id) await exit_terminal(destroy_request, mock_context) # Dangerous working directory should be blocked dangerous_request = OpenTerminalRequest(shell="bash", working_directory="/etc") with pytest.raises(ValueError, match="Security violation"): await open_terminal(dangerous_request, mock_context) @pytest.mark.asyncio async def test_environment_variable_protection(self, mock_context): """Test protection of critical environment variables""" # Safe environment variables should work safe_request = OpenTerminalRequest( shell="bash", environment={"TEST_VAR": "safe_value"} ) result = await open_terminal(safe_request, mock_context) assert result.success # Cleanup if result.session_id: destroy_request = DestroySessionRequest(session_id=result.session_id) await exit_terminal(destroy_request, mock_context) # Protected environment variables should be blocked dangerous_request = OpenTerminalRequest( shell="bash", environment={"PATH": "/malicious/path"} ) with pytest.raises(ValueError, match="Security violation"): await open_terminal(dangerous_request, mock_context) @pytest.mark.asyncio async def test_session_management(self, mock_context): """Test session management with security validation""" # Test initial session list (should be empty) sessions = await list_terminal_sessions(mock_context) assert sessions.success initial_count = len(sessions.sessions) # Start a session with a safe command request = OpenTerminalRequest(shell="python3") result = await open_terminal(request, mock_context) assert result.success session_id = result.session_id try: # Test session list with active sessions sessions = await list_terminal_sessions(mock_context) assert sessions.success assert len(sessions.sessions) == initial_count + 1 # Test getting screen content screen_request = GetScreenContentRequest(session_id=session_id) screen_result = await get_screen_content(screen_request, mock_context) assert screen_result.success assert screen_result.process_running finally: # Always cleanup destroy_request = DestroySessionRequest(session_id=session_id) destroy_result = await exit_terminal(destroy_request, mock_context) assert destroy_result.success @pytest.mark.asyncio async def test_interactive_workflow_with_security(self, mock_context): """Test interactive workflow with input validation""" # Start interactive Python session request = OpenTerminalRequest(shell="python3") result = await open_terminal(request, mock_context) assert result.success session_id = result.session_id try: # Wait for process to be ready await asyncio.sleep(0.5) # Get screen content screen_request = GetScreenContentRequest(session_id=session_id) screen_result = await get_screen_content(screen_request, mock_context) assert screen_result.success # Send safe input input_request = SendInputRequest( session_id=session_id, input_text="Alice\n" ) input_result = await send_input(input_request, mock_context) assert input_result.success finally: # Cleanup destroy_request = DestroySessionRequest(session_id=session_id) await exit_terminal(destroy_request, mock_context) @pytest.mark.asyncio async def test_dangerous_input_blocking(self, mock_context): """Test that dangerous input is blocked""" # Start a simple interactive session request = OpenTerminalRequest(shell="python3") result = await open_terminal(request, mock_context) assert result.success session_id = result.session_id try: await asyncio.sleep(0.5) # Try to send dangerous input - should be blocked dangerous_inputs = ["sudo rm -rf /", "su - root", "passwd"] for dangerous_input in dangerous_inputs: input_request = SendInputRequest( session_id=session_id, input_text=dangerous_input ) with pytest.raises(ValueError, match="Security violation"): await send_input(input_request, mock_context) finally: # Cleanup destroy_request = DestroySessionRequest(session_id=session_id) await exit_terminal(destroy_request, mock_context) @pytest.mark.asyncio async def test_rate_limiting_integration(self, mock_context): """Test rate limiting in the context of tool calls""" # This test would need to make many rapid calls to trigger rate limiting # For now, we'll just verify the security manager is being called request = OpenTerminalRequest(shell="bash") # Mock the security manager to simulate rate limit exceeded with patch.object( mock_context.request_context.lifespan_context.security_manager, "validate_tool_call", return_value=False, ): with pytest.raises(ValueError, match="Security violation"): await open_terminal(request, mock_context) @pytest.mark.asyncio async def test_session_limits_integration(self, mock_context): """Test session limits in practice""" # Test that the security manager validates session limits # This would be called by the session manager when creating sessions security_manager = ( mock_context.request_context.lifespan_context.security_manager ) # Under limit should pass assert security_manager.validate_session_limits(25) is True # Over limit should fail assert security_manager.validate_session_limits(51) is False @pytest.mark.asyncio async def test_python_repl_security(self, mock_context): """Test Python REPL with security considerations""" request = OpenTerminalRequest(shell="python3") result = await open_terminal(request, mock_context) assert result.success session_id = result.session_id try: await asyncio.sleep(1) # Wait for Python prompt # Test safe Python commands safe_commands = ["import math", "print(math.pi)", "x = 2 + 3", "print(x)"] for cmd in safe_commands: input_request = SendInputRequest( session_id=session_id, input_text=cmd + "\n" ) result = await send_input(input_request, mock_context) assert result.success await asyncio.sleep(0.2) # Exit Python exit_request = SendInputRequest( session_id=session_id, input_text="exit()\n" ) await send_input(exit_request, mock_context) finally: # Cleanup try: destroy_request = DestroySessionRequest(session_id=session_id) await exit_terminal(destroy_request, mock_context) except Exception: pass # Session may have already ended @pytest.mark.asyncio async def test_error_handling_and_cleanup(self, mock_context): """Test proper error handling and session cleanup""" # Start a session request = OpenTerminalRequest(shell="bash") result = await open_terminal(request, mock_context) assert result.success session_id = result.session_id # Verify session exists sessions = await list_terminal_sessions(mock_context) session_ids = [s.session_id for s in sessions.sessions] assert session_id in session_ids # Destroy session destroy_request = DestroySessionRequest(session_id=session_id) destroy_result = await exit_terminal(destroy_request, mock_context) assert destroy_result.success # Verify session is gone sessions = await list_terminal_sessions(mock_context) session_ids = [s.session_id for s in sessions.sessions] assert session_id not in session_ids # Try to destroy non-existent session destroy_request = DestroySessionRequest(session_id="non-existent") destroy_result = await exit_terminal(destroy_request, mock_context) assert not destroy_result.success class TestSecurityIntegrationScenarios: """Test complex security scenarios in integrated workflows""" @pytest.fixture def mock_context(self): """Create mock context for tool calls""" session_manager = SessionManager() security_manager = SecurityManager() app_ctx = SimpleNamespace( session_manager=session_manager, security_manager=security_manager ) return SimpleNamespace( request_context=SimpleNamespace(lifespan_context=app_ctx) ) @pytest.mark.asyncio async def test_multi_step_attack_prevention(self, mock_context): """Test prevention of multi-step attack scenarios""" # Try to create session with safe command first request = OpenTerminalRequest(shell="bash") result = await open_terminal(request, mock_context) assert result.success session_id = result.session_id try: # Then try to send malicious input - should be blocked malicious_request = SendInputRequest( session_id=session_id, input_text="'; rm -rf / #" ) with pytest.raises(ValueError, match="Security violation"): await send_input(malicious_request, mock_context) finally: # Cleanup destroy_request = DestroySessionRequest(session_id=session_id) await exit_terminal(destroy_request, mock_context) @pytest.mark.asyncio async def test_resource_exhaustion_protection(self, mock_context): """Test protection against resource exhaustion""" # Test that we can't exceed session limits by trying to create too many session_manager = mock_context.request_context.lifespan_context.session_manager security_manager = ( mock_context.request_context.lifespan_context.security_manager ) # Mock having too many sessions with patch.object( session_manager, "sessions", {f"session_{i}": None for i in range(51)} ): pass # Test is about validating session limits directly # Should be blocked due to session limit # Note: This test depends on session_manager implementing the check # For now, we test the security manager validation directly assert security_manager.validate_session_limits(51) is False @pytest.mark.asyncio async def test_privilege_escalation_prevention(self, mock_context): """Test prevention of privilege escalation attempts""" escalation_attempts = [ # Environment manipulation attempts OpenTerminalRequest( shell="bash", environment={"LD_PRELOAD": "/tmp/malicious.so"}, ), # Path manipulation attempts OpenTerminalRequest( shell="bash", environment={"PATH": "/tmp/malicious:/usr/bin"}, ), ] for request in escalation_attempts: with pytest.raises(ValueError, match="Security violation"): await open_terminal(request, mock_context) @pytest.mark.asyncio async def test_data_exfiltration_prevention(self, mock_context): """Test prevention of data exfiltration attempts""" exfiltration_attempts = [ # Working directory manipulation OpenTerminalRequest(shell="bash", working_directory="/etc"), OpenTerminalRequest(shell="bash", working_directory="/.ssh"), ] for request in exfiltration_attempts: with pytest.raises(ValueError, match="Security violation"): await open_terminal(request, mock_context) class TestContentModeIntegration: """Integration tests for the new content mode functionality""" @pytest.mark.asyncio async def test_content_mode_parameter_validation(self, mock_context): """Test that content mode parameters are properly validated""" # Test valid content modes valid_modes = ["screen", "since_input", "history", "tail"] for mode in valid_modes: line_count = 10 if mode == "tail" else None request = GetScreenContentRequest( session_id="test_session", content_mode=mode, line_count=line_count ) # Request should be valid (will fail on session not found, but parameter validation passes) response = await get_screen_content(request, mock_context) assert response.success is False assert "Session not found" in response.error @pytest.mark.asyncio async def test_content_mode_with_real_session(self, mock_context): """Test content modes with a real terminal session""" # Create a terminal session first create_request = OpenTerminalRequest(shell="bash") create_response = await open_terminal(create_request, mock_context) assert create_response.success is True session_id = create_response.session_id try: # Test each content mode test_cases = [ ("screen", None, "should get current screen"), ("history", None, "should get full history"), ("tail", 5, "should get last 5 lines"), ("since_input", None, "should get output since last input"), ] for mode, line_count, description in test_cases: request = GetScreenContentRequest( session_id=session_id, content_mode=mode, line_count=line_count ) response = await get_screen_content(request, mock_context) assert ( response.success is True ), f"Failed for mode {mode}: {description}" assert response.session_id == session_id assert response.screen_content is not None assert response.timestamp is not None # For tail mode, verify line_count parameter is handled if mode == "tail" and line_count: # Content should be limited (though exact validation depends on tmux output) assert isinstance(response.screen_content, str) finally: # Clean up session destroy_request = DestroySessionRequest(session_id=session_id) destroy_response = await exit_terminal(destroy_request, mock_context) assert destroy_response.success is True @pytest.mark.asyncio async def test_content_mode_defaults(self, mock_context): """Test that content mode defaults to 'screen' when not specified""" # Create a terminal session create_request = OpenTerminalRequest(shell="bash") create_response = await open_terminal(create_request, mock_context) assert create_response.success is True session_id = create_response.session_id try: # Test request without content_mode (should default to "screen") request = GetScreenContentRequest(session_id=session_id) response = await get_screen_content(request, mock_context) assert response.success is True assert response.screen_content is not None # Compare with explicit screen mode explicit_request = GetScreenContentRequest( session_id=session_id, content_mode="screen" ) explicit_response = await get_screen_content(explicit_request, mock_context) assert explicit_response.success is True # Both should return similar content (may vary slightly due to timing) assert isinstance(response.screen_content, str) assert isinstance(explicit_response.screen_content, str) finally: # Clean up session destroy_request = DestroySessionRequest(session_id=session_id) await exit_terminal(destroy_request, mock_context) @pytest.mark.asyncio async def test_since_input_mode_workflow(self, mock_context): """Test the 'since_input' mode with actual input workflow""" # Create a terminal session create_request = OpenTerminalRequest(shell="bash") create_response = await open_terminal(create_request, mock_context) assert create_response.success is True session_id = create_response.session_id try: # Send some input input_request = SendInputRequest( session_id=session_id, input_text="echo 'Test output for since_input mode'\n", ) input_response = await send_input(input_request, mock_context) assert input_response.success is True # Wait a moment for command to process await asyncio.sleep(0.2) # Get content since last input content_request = GetScreenContentRequest( session_id=session_id, content_mode="since_input" ) content_response = await get_screen_content(content_request, mock_context) assert content_response.success is True assert content_response.screen_content is not None # Should contain recent output assert isinstance(content_response.screen_content, str) finally: # Clean up session destroy_request = DestroySessionRequest(session_id=session_id) await exit_terminal(destroy_request, mock_context) @pytest.mark.asyncio async def test_tail_mode_line_count_validation(self, mock_context): """Test that tail mode properly handles line_count parameter""" # Create a terminal session create_request = OpenTerminalRequest(shell="bash") create_response = await open_terminal(create_request, mock_context) assert create_response.success is True session_id = create_response.session_id try: # Test tail mode with different line counts line_counts = [1, 5, 10, 20] for line_count in line_counts: request = GetScreenContentRequest( session_id=session_id, content_mode="tail", line_count=line_count ) response = await get_screen_content(request, mock_context) assert response.success is True, f"Failed for line_count={line_count}" assert response.screen_content is not None # Content should be a string (exact line validation depends on tmux behavior) assert isinstance(response.screen_content, str) finally: # Clean up session destroy_request = DestroySessionRequest(session_id=session_id) await exit_terminal(destroy_request, mock_context) @pytest.mark.asyncio async def test_content_mode_backwards_compatibility(self, mock_context): """Test that existing code without content_mode still works""" # Create a terminal session create_request = OpenTerminalRequest(shell="bash") create_response = await open_terminal(create_request, mock_context) assert create_response.success is True session_id = create_response.session_id try: # Test old-style request (just session_id) old_style_request = GetScreenContentRequest(session_id=session_id) old_response = await get_screen_content(old_style_request, mock_context) # Test new-style request with explicit screen mode new_style_request = GetScreenContentRequest( session_id=session_id, content_mode="screen" ) new_response = await get_screen_content(new_style_request, mock_context) # Both should succeed and return similar results assert old_response.success is True assert new_response.success is True assert old_response.screen_content is not None assert new_response.screen_content is not None finally: # Clean up session destroy_request = DestroySessionRequest(session_id=session_id) await exit_terminal(destroy_request, mock_context) class TestAwaitOutputIntegration: """Integration tests for the new await_output tool""" @pytest.mark.asyncio async def test_await_output_pattern_match(self, mock_context): """Test await_output tool with pattern that matches""" # Create a terminal session create_request = OpenTerminalRequest(shell="bash") create_response = await open_terminal(create_request, mock_context) assert create_response.success is True session_id = create_response.session_id try: # Send a command that produces predictable output input_request = SendInputRequest( session_id=session_id, input_text="echo 'MATCH_TEST_123'\n" ) input_response = await send_input(input_request, mock_context) assert input_response.success is True # Wait for the pattern to appear await_request = AwaitOutputRequest( session_id=session_id, pattern=r"MATCH_TEST_123", timeout=5.0 ) await_response = await await_output(await_request, mock_context) assert await_response.success is True assert await_response.session_id == session_id assert await_response.match_text == "MATCH_TEST_123" assert await_response.screen_content is not None assert await_response.elapsed_time >= 0.0 assert await_response.elapsed_time < AWAIT_OUTPUT_MAX_TIME assert await_response.timestamp is not None assert await_response.error is None finally: # Clean up session destroy_request = DestroySessionRequest(session_id=session_id) await exit_terminal(destroy_request, mock_context) @pytest.mark.asyncio async def test_await_output_timeout(self, mock_context): """Test await_output tool with pattern that times out""" # Create a terminal session create_request = OpenTerminalRequest(shell="bash") create_response = await open_terminal(create_request, mock_context) assert create_response.success is True session_id = create_response.session_id try: # Wait for a pattern that will never appear await_request = AwaitOutputRequest( session_id=session_id, pattern=r"NEVER_APPEARS_PATTERN_xyz", timeout=1.0, # Short timeout for faster test ) await_response = await await_output(await_request, mock_context) assert await_response.success is True assert await_response.session_id == session_id assert await_response.match_text is None # No match due to timeout assert await_response.screen_content is not None assert await_response.elapsed_time >= 1.0 # Should be close to timeout assert ( await_response.elapsed_time < AWAIT_OUTPUT_TIMEOUT_BUFFER ) # But not too much over assert await_response.timestamp is not None assert await_response.error is None finally: # Clean up session destroy_request = DestroySessionRequest(session_id=session_id) await exit_terminal(destroy_request, mock_context) @pytest.mark.asyncio async def test_await_output_regex_patterns(self, mock_context): """Test await_output tool with various regex patterns""" # Create a terminal session create_request = OpenTerminalRequest(shell="bash") create_response = await open_terminal(create_request, mock_context) assert create_response.success is True session_id = create_response.session_id try: # Test different regex patterns test_cases = [ ("echo 'test123'\n", r"\d+", "123"), ("echo 'SUCCESS'\n", r"SUCCESS|FAIL", "SUCCESS"), ("echo '$USER@hostname:~$'\n", r"\$\s*$", "$"), ] for command, pattern, expected_match in test_cases: # Send command input_request = SendInputRequest( session_id=session_id, input_text=command ) await send_input(input_request, mock_context) # Wait for pattern await_request = AwaitOutputRequest( session_id=session_id, pattern=pattern, timeout=3.0 ) await_response = await await_output(await_request, mock_context) assert await_response.success is True, f"Failed for pattern: {pattern}" assert ( await_response.match_text is not None ), f"No match for pattern: {pattern}" assert ( expected_match in await_response.match_text or await_response.match_text == expected_match ) finally: # Clean up session destroy_request = DestroySessionRequest(session_id=session_id) await exit_terminal(destroy_request, mock_context) @pytest.mark.asyncio async def test_await_output_case_sensitivity(self, mock_context): """Test await_output tool case sensitivity with regex flags""" # Create a terminal session create_request = OpenTerminalRequest(shell="bash") create_response = await open_terminal(create_request, mock_context) assert create_response.success is True session_id = create_response.session_id try: # Send command with mixed case input_request = SendInputRequest( session_id=session_id, input_text="echo 'CaseSensitive'\n" ) await send_input(input_request, mock_context) # Test case-sensitive pattern (should match) await_request = AwaitOutputRequest( session_id=session_id, pattern=r"CaseSensitive", timeout=3.0 ) await_response = await await_output(await_request, mock_context) assert await_response.success is True assert await_response.match_text == "CaseSensitive" # Test case-insensitive pattern using regex flags input_request = SendInputRequest( session_id=session_id, input_text="echo 'AnotherTest'\n" ) await send_input(input_request, mock_context) await_request = AwaitOutputRequest( session_id=session_id, pattern=r"(?i)anothertest", # Case-insensitive flag timeout=3.0, ) await_response = await await_output(await_request, mock_context) assert await_response.success is True assert await_response.match_text == "AnotherTest" finally: # Clean up session destroy_request = DestroySessionRequest(session_id=session_id) await exit_terminal(destroy_request, mock_context) @pytest.mark.asyncio async def test_await_output_invalid_session(self, mock_context): """Test await_output tool with invalid session ID""" await_request = AwaitOutputRequest( session_id="invalid_session_id", pattern=r"test", timeout=1.0 ) await_response = await await_output(await_request, mock_context) assert await_response.success is False assert await_response.session_id == "invalid_session_id" assert await_response.match_text is None assert await_response.screen_content == "" assert await_response.elapsed_time == 0.0 assert await_response.error == "Session not found" @pytest.mark.asyncio async def test_await_output_invalid_regex(self, mock_context): """Test await_output tool with invalid regex pattern""" # Create a terminal session create_request = OpenTerminalRequest(shell="bash") create_response = await open_terminal(create_request, mock_context) assert create_response.success is True session_id = create_response.session_id try: # Test with invalid regex pattern await_request = AwaitOutputRequest( session_id=session_id, pattern=r"[unclosed_bracket", # Invalid regex timeout=1.0, ) await_response = await await_output(await_request, mock_context) assert await_response.success is False assert await_response.session_id == session_id assert await_response.match_text is None assert await_response.screen_content == "" assert await_response.elapsed_time == 0.0 assert await_response.error is not None assert ( "unterminated character set" in await_response.error.lower() or "error" in await_response.error.lower() ) finally: # Clean up session destroy_request = DestroySessionRequest(session_id=session_id) await exit_terminal(destroy_request, mock_context) @pytest.mark.asyncio async def test_await_output_different_timeouts(self, mock_context): """Test await_output tool with different timeout values""" # Create a terminal session create_request = OpenTerminalRequest(shell="bash") create_response = await open_terminal(create_request, mock_context) assert create_response.success is True session_id = create_response.session_id try: # Test various timeout values timeout_tests = [0.5, 1.0, 2.0, 5.0] for timeout_val in timeout_tests: # Wait for a pattern that won't appear await_request = AwaitOutputRequest( session_id=session_id, pattern=r"TIMEOUT_TEST_" + str(int(timeout_val * 1000)), timeout=timeout_val, ) await_response = await await_output(await_request, mock_context) assert await_response.success is True assert await_response.match_text is None # Should timeout assert ( await_response.elapsed_time >= timeout_val - 0.1 ) # Allow small variance assert ( await_response.elapsed_time <= timeout_val + 0.5 ) # Allow some overhead finally: # Clean up session destroy_request = DestroySessionRequest(session_id=session_id) await exit_terminal(destroy_request, mock_context) @pytest.mark.asyncio async def test_await_output_build_deployment_workflow(self, mock_context): """Test await_output in a realistic build/deployment workflow""" # Create a terminal session create_request = OpenTerminalRequest(shell="bash") create_response = await open_terminal(create_request, mock_context) assert create_response.success is True session_id = create_response.session_id try: # Simulate a build process build_commands = [ ("echo 'Starting build process...'\n", r"Starting build"), ( "sleep 0.5 && echo 'Build completed successfully'\n", r"Build completed|Build failed", ), ("echo 'Ready for deployment'\n", r"Ready for deployment"), ] for command, expected_pattern in build_commands: # Send command input_request = SendInputRequest( session_id=session_id, input_text=command ) await send_input(input_request, mock_context) # Wait for expected output await_request = AwaitOutputRequest( session_id=session_id, pattern=expected_pattern, timeout=3.0 ) await_response = await await_output(await_request, mock_context) assert ( await_response.success is True ), f"Failed waiting for: {expected_pattern}" assert await_response.match_text is not None assert await_response.elapsed_time < AWAIT_OUTPUT_QUICK_TIMEOUT finally: # Clean up session destroy_request = DestroySessionRequest(session_id=session_id) await exit_terminal(destroy_request, mock_context) @pytest.mark.asyncio async def test_await_output_python_repl_interaction(self, mock_context): """Test await_output with Python REPL interactions""" # Create a Python terminal session create_request = OpenTerminalRequest(shell="python3") create_response = await open_terminal(create_request, mock_context) assert create_response.success is True session_id = create_response.session_id try: # Wait for Python prompt prompt_request = AwaitOutputRequest( session_id=session_id, pattern=r">>>", timeout=5.0 ) prompt_response = await await_output(prompt_request, mock_context) assert prompt_response.success is True assert prompt_response.match_text == ">>>" # Send Python command input_request = SendInputRequest( session_id=session_id, input_text="print('Hello from Python')\n" ) await send_input(input_request, mock_context) # Wait for output output_request = AwaitOutputRequest( session_id=session_id, pattern=r"Hello from Python", timeout=3.0 ) output_response = await await_output(output_request, mock_context) assert output_response.success is True assert output_response.match_text == "Hello from Python" # Exit Python exit_request = SendInputRequest( session_id=session_id, input_text="exit()\n" ) await send_input(exit_request, mock_context) finally: # Clean up session try: destroy_request = DestroySessionRequest(session_id=session_id) await exit_terminal(destroy_request, mock_context) except Exception: pass # Python session may have already exited @pytest.mark.asyncio async def test_await_output_default_timeout(self, mock_context): """Test await_output tool uses default timeout when not specified""" # Create a terminal session create_request = OpenTerminalRequest(shell="bash") create_response = await open_terminal(create_request, mock_context) assert create_response.success is True session_id = create_response.session_id try: # Test that default timeout is used (should be 10.0 seconds) await_request = AwaitOutputRequest( session_id=session_id, pattern=r"NEVER_APPEARS_DEFAULT_TIMEOUT", # timeout not specified, should use default of 10.0 ) import time start_time = time.time() await_response = await await_output(await_request, mock_context) actual_elapsed = time.time() - start_time assert await_response.success is True assert await_response.match_text is None # Should timeout # Elapsed time should be close to 10 seconds (allowing some variance) assert actual_elapsed >= AWAIT_OUTPUT_LONG_TIMEOUT_MIN assert actual_elapsed <= AWAIT_OUTPUT_LONG_TIMEOUT_MAX # Also check the elapsed_time in the response is close to 10 assert await_response.elapsed_time >= AWAIT_OUTPUT_LONG_TIMEOUT_MIN assert await_response.elapsed_time <= AWAIT_OUTPUT_LONG_TIMEOUT_MAX finally: # Clean up session destroy_request = DestroySessionRequest(session_id=session_id) await exit_terminal(destroy_request, mock_context) class TestWebInterfaceIntegration: """Test web interface functionality and URL generation""" @pytest.mark.asyncio async def test_list_sessions_with_web_urls_enabled(self, mock_context): """Test that list_terminal_sessions returns web URLs when web interface is enabled""" from unittest.mock import patch # Mock web interface as enabled by creating a simple mock config mock_config = SimpleNamespace() mock_config.web_enabled = True mock_config.web_host = "localhost" mock_config.web_port = 8080 mock_config.external_web_host = None mock_config.web_auto_port = False mock_config.terminal_screen_content_delay = 1.0 mock_config.session_timeout = 30 with ( patch("src.terminal_control_mcp.main.WEB_INTERFACE_AVAILABLE", True), patch("src.terminal_control_mcp.main.config", mock_config), ): # Create a session request = OpenTerminalRequest(shell="bash") result = await open_terminal(request, mock_context) assert result.success session_id = result.session_id try: # List sessions and check for web URLs sessions_response = await list_terminal_sessions(mock_context) assert sessions_response.success assert len(sessions_response.sessions) >= 1 # Find our session our_session = None for session in sessions_response.sessions: if session.session_id == session_id: our_session = session break assert our_session is not None assert our_session.web_url is not None assert ( f"http://localhost:8080/session/{session_id}" in our_session.web_url ) finally: # Cleanup destroy_request = DestroySessionRequest(session_id=session_id) await exit_terminal(destroy_request, mock_context) @pytest.mark.asyncio async def test_list_sessions_without_web_urls_disabled(self, mock_context): """Test that list_terminal_sessions returns None web URLs when web interface is disabled""" from unittest.mock import patch # Mock web interface as disabled by creating a simple mock config mock_config = SimpleNamespace() mock_config.web_enabled = False mock_config.web_host = "localhost" mock_config.web_port = 8080 mock_config.external_web_host = None mock_config.web_auto_port = False mock_config.terminal_screen_content_delay = 1.0 mock_config.session_timeout = 30 with ( patch("src.terminal_control_mcp.main.WEB_INTERFACE_AVAILABLE", False), patch("src.terminal_control_mcp.main.config", mock_config), ): # Create a session request = OpenTerminalRequest(shell="bash") result = await open_terminal(request, mock_context) assert result.success session_id = result.session_id try: # List sessions and check that web URLs are None sessions_response = await list_terminal_sessions(mock_context) assert sessions_response.success assert len(sessions_response.sessions) >= 1 # Find our session our_session = None for session in sessions_response.sessions: if session.session_id == session_id: our_session = session break assert our_session is not None assert our_session.web_url is None finally: # Cleanup destroy_request = DestroySessionRequest(session_id=session_id) await exit_terminal(destroy_request, mock_context) @pytest.mark.asyncio async def test_web_url_with_external_host(self, mock_context): """Test web URL generation with external host configuration""" from unittest.mock import patch # Mock web interface with external host mock_config = SimpleNamespace() mock_config.web_enabled = True mock_config.web_host = "0.0.0.0" mock_config.web_port = 9000 mock_config.external_web_host = "server.example.com" mock_config.web_auto_port = False mock_config.terminal_screen_content_delay = 1.0 mock_config.session_timeout = 30 with ( patch("src.terminal_control_mcp.main.WEB_INTERFACE_AVAILABLE", True), patch("src.terminal_control_mcp.main.config", mock_config), ): # Create a session request = OpenTerminalRequest(shell="bash") result = await open_terminal(request, mock_context) assert result.success session_id = result.session_id try: # List sessions and check for external host in web URLs sessions_response = await list_terminal_sessions(mock_context) assert sessions_response.success assert len(sessions_response.sessions) >= 1 # Find our session our_session = None for session in sessions_response.sessions: if session.session_id == session_id: our_session = session break assert our_session is not None assert our_session.web_url is not None assert ( f"http://server.example.com:9000/session/{session_id}" in our_session.web_url ) finally: # Cleanup destroy_request = DestroySessionRequest(session_id=session_id) await exit_terminal(destroy_request, mock_context) @pytest.mark.asyncio async def test_open_terminal_returns_web_url(self, mock_context): """Test that open_terminal returns web URL when web interface is enabled""" from unittest.mock import patch # Mock web interface as enabled mock_config = SimpleNamespace() mock_config.web_enabled = True mock_config.web_host = "localhost" mock_config.web_port = 8080 mock_config.external_web_host = None mock_config.web_auto_port = False mock_config.terminal_screen_content_delay = 1.0 mock_config.session_timeout = 30 with ( patch("src.terminal_control_mcp.main.WEB_INTERFACE_AVAILABLE", True), patch("src.terminal_control_mcp.main.config", mock_config), ): # Create a session request = OpenTerminalRequest(shell="bash") result = await open_terminal(request, mock_context) try: assert result.success assert result.web_url is not None assert ( f"http://localhost:8080/session/{result.session_id}" in result.web_url ) finally: # Cleanup if result.success: destroy_request = DestroySessionRequest( session_id=result.session_id ) await exit_terminal(destroy_request, mock_context) if __name__ == "__main__": # Allow running tests directly pytest.main([__file__, "-v"])

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