"""Tests for CLI Runtime MCP functionality.""" from unittest.mock import MagicMock, patch import pytest from openhands.core.config import OpenHandsConfig from openhands.core.config.mcp_config import ( MCPConfig, MCPSSEServerConfig, MCPStdioServerConfig, ) from openhands.events.action.mcp import MCPAction from openhands.events.observation import ErrorObservation from openhands.events.observation.mcp import MCPObservation from openhands.llm.llm_registry import LLMRegistry from openhands.runtime.impl.cli.cli_runtime import CLIRuntime class TestCLIRuntimeMCP: """Test MCP functionality in CLI Runtime.""" def setup_method(self): """Set up test fixtures.""" self.config = OpenHandsConfig() self.event_stream = MagicMock() llm_registry = LLMRegistry(config=OpenHandsConfig()) self.runtime = CLIRuntime( config=self.config, event_stream=self.event_stream, sid='test-session', llm_registry=llm_registry, ) @pytest.mark.asyncio async def test_call_tool_mcp_no_servers_configured(self): """Test MCP call with no servers configured.""" # Set up empty MCP config self.runtime.config.mcp = MCPConfig() action = MCPAction(name='test_tool', arguments={'arg1': 'value1'}) with patch('sys.platform', 'linux'): result = await self.runtime.call_tool_mcp(action) assert isinstance(result, ErrorObservation) assert 'No MCP servers configured' in result.content @pytest.mark.asyncio @patch('openhands.mcp.utils.create_mcp_clients') async def test_call_tool_mcp_no_clients_created(self, mock_create_clients): """Test MCP call when no clients can be created.""" # Set up MCP config with servers self.runtime.config.mcp = MCPConfig( sse_servers=[MCPSSEServerConfig(url='http://test.com')] ) # Mock create_mcp_clients to return empty list mock_create_clients.return_value = [] action = MCPAction(name='test_tool', arguments={'arg1': 'value1'}) with patch('sys.platform', 'linux'): result = await self.runtime.call_tool_mcp(action) assert isinstance(result, ErrorObservation) assert 'No MCP clients could be created' in result.content mock_create_clients.assert_called_once() @pytest.mark.asyncio @patch('openhands.mcp.utils.create_mcp_clients') @patch('openhands.mcp.utils.call_tool_mcp') async def test_call_tool_mcp_success(self, mock_call_tool, mock_create_clients): """Test successful MCP tool call.""" # Set up MCP config with servers self.runtime.config.mcp = MCPConfig( sse_servers=[MCPSSEServerConfig(url='http://test.com')], stdio_servers=[MCPStdioServerConfig(name='test-stdio', command='python')], ) # Mock successful client creation mock_client = MagicMock() mock_create_clients.return_value = [mock_client] # Mock successful tool call expected_observation = MCPObservation( content='{"result": "success"}', name='test_tool', arguments={'arg1': 'value1'}, ) mock_call_tool.return_value = expected_observation action = MCPAction(name='test_tool', arguments={'arg1': 'value1'}) with patch('sys.platform', 'linux'): result = await self.runtime.call_tool_mcp(action) assert result == expected_observation mock_create_clients.assert_called_once_with( self.runtime.config.mcp.sse_servers, self.runtime.config.mcp.shttp_servers, self.runtime.sid, self.runtime.config.mcp.stdio_servers, ) mock_call_tool.assert_called_once_with([mock_client], action) @pytest.mark.asyncio @patch('openhands.mcp.utils.create_mcp_clients') async def test_call_tool_mcp_exception_handling(self, mock_create_clients): """Test exception handling in MCP tool call.""" # Set up MCP config with servers self.runtime.config.mcp = MCPConfig( sse_servers=[MCPSSEServerConfig(url='http://test.com')] ) # Mock create_mcp_clients to raise an exception mock_create_clients.side_effect = Exception('Connection error') action = MCPAction(name='test_tool', arguments={'arg1': 'value1'}) with patch('sys.platform', 'linux'): result = await self.runtime.call_tool_mcp(action) assert isinstance(result, ErrorObservation) assert 'Error executing MCP tool test_tool' in result.content assert 'Connection error' in result.content def test_get_mcp_config_basic(self): """Test basic MCP config retrieval.""" # Set up MCP config expected_config = MCPConfig( sse_servers=[MCPSSEServerConfig(url='http://test.com')], stdio_servers=[MCPStdioServerConfig(name='test-stdio', command='python')], ) self.runtime.config.mcp = expected_config with patch('sys.platform', 'linux'): result = self.runtime.get_mcp_config() assert result == expected_config def test_get_mcp_config_with_extra_stdio_servers(self): """Test MCP config with extra stdio servers.""" # Set up initial MCP config initial_stdio_server = MCPStdioServerConfig(name='initial', command='python') self.runtime.config.mcp = MCPConfig(stdio_servers=[initial_stdio_server]) # Add extra stdio servers extra_servers = [ MCPStdioServerConfig(name='extra1', command='node'), MCPStdioServerConfig(name='extra2', command='java'), ] with patch('sys.platform', 'linux'): result = self.runtime.get_mcp_config(extra_stdio_servers=extra_servers) # Should have all three servers assert len(result.stdio_servers) == 3 assert initial_stdio_server in result.stdio_servers assert extra_servers[0] in result.stdio_servers assert extra_servers[1] in result.stdio_servers