From 3ff3706cafeb37070e7e15d5df33fbf728ca6e91 Mon Sep 17 00:00:00 2001 From: BrunoV21 <120278082+BrunoV21@users.noreply.github.com> Date: Mon, 30 Jun 2025 11:25:30 +0100 Subject: [PATCH 1/2] Add MCP client tests and update pytest configuration Added comprehensive test suite for MCP client functionality including connection management, server operations, and error handling. Updated pytest.ini with MCP-specific test configuration including coverage settings, logging, and test markers. --- tests/pytest.ini | 32 +++++++- tests/test_mcp.py | 182 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 tests/test_mcp.py diff --git a/tests/pytest.ini b/tests/pytest.ini index 3d86c18..ba45e7c 100644 --- a/tests/pytest.ini +++ b/tests/pytest.ini @@ -1,4 +1,34 @@ # pytest.ini [pytest] env = - WORKSPACE="bv-test" \ No newline at end of file + WORKSPACE="bv-test" + MCP_TEST_ENDPOINT="http://localhost:8080" + CONFIG_PATH="./config/config_example_mcp.yml" + MCP_JSON_PATH="./mcp_config_example.json" + LOGS_PATH="tests/logs" + +asyncio_mode = auto +testpaths = tests +python_files = test_*.py +python_functions = test_* +norecursedirs = .* venv build dist + +markers = + asyncio: mark tests as async (deselect with '-m "not asyncio"') + integration: integration tests that require external services + unit: unit tests that don't require external services + slow: tests that take longer to run + +log_cli = true +log_cli_level = INFO +log_cli_format = %(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s) +log_cli_date_format = %Y-%m-%d %H:%M:%S + +addopts = + --cov=aicore + --cov-report=term-missing + --cov-report=xml + --cov-fail-under=95 + -ra + -W ignore::DeprecationWarning + -p no:warnings \ No newline at end of file diff --git a/tests/test_mcp.py b/tests/test_mcp.py new file mode 100644 index 0000000..7c278a1 --- /dev/null +++ b/tests/test_mcp.py @@ -0,0 +1,182 @@ +import pytest +from unittest.mock import AsyncMock, MagicMock, patch +from aicore.llm.mcp.client import MCPClient, ServerManager, ServerConnection +from aicore.llm.mcp.models import MCPServerConfig, ToolSchema, ToolCallSchema +from aicore.models import FastMcpError +from aicore.llm.mcp.utils import raise_fast_mcp_error +import json +import os +import asyncio + +@pytest.fixture +def mock_mcp_config(tmp_path): + """Fixture providing a mock MCP configuration file.""" + config = { + "mcpServers": { + "test-server": { + "command": "echo", + "args": ["hello"], + "transport_type": "stdio" + }, + "ws-server": { + "url": "ws://localhost:8080", + "transport_type": "ws" + } + } + } + config_path = tmp_path / "mcp_config.json" + with open(config_path, "w") as f: + json.dump(config, f) + return config_path + +@pytest.fixture +def mock_tool_schema(): + """Fixture providing a mock ToolSchema instance.""" + return ToolSchema( + name="test-tool", + description="Test tool", + input_schema={ + "type": "object", + "properties": {}, + "required": [] + } + ) + +@pytest.fixture +def mock_mcp_client(mock_mcp_config): + """Fixture providing a mock MCPClient instance.""" + client = MCPClient.from_config(mock_mcp_config) + with patch.object(client, "_create_transport", return_value=AsyncMock()): + yield client + +@pytest.mark.asyncio +async def test_mcp_client_from_config(mock_mcp_config): + """Test creating MCPClient from config file.""" + client = MCPClient.from_config(mock_mcp_config) + assert isinstance(client, MCPClient) + assert "test-server" in client.server_configs + assert "ws-server" in client.server_configs + +@pytest.mark.asyncio +async def test_mcp_client_connect(mock_mcp_client): + """Test connecting MCPClient to servers.""" + await mock_mcp_client.connect() + assert mock_mcp_client._is_connected + assert len(mock_mcp_client.transports) == 2 + +@pytest.mark.asyncio +async def test_mcp_client_context_manager(mock_mcp_config): + """Test MCPClient as async context manager.""" + with patch("aicore.llm.mcp.client.MCPClient.connect") as mock_connect, \ + patch("aicore.llm.mcp.client.MCPClient.__aexit__") as mock_exit: + async with MCPClient.from_config(mock_mcp_config) as client: + assert isinstance(client, MCPClient) + mock_connect.assert_awaited_once() + mock_exit.assert_awaited_once() + +@pytest.mark.asyncio +async def test_server_manager_get_tools(mock_mcp_client): + """Test ServerManager.get_tools() method.""" + mock_client = MagicMock() + mock_client.transports = {"server1": AsyncMock()} + manager = ServerManager(mock_client) + + with patch("aicore.llm.mcp.client.FastMCPClient") as mock_fastmcp: + mock_fastmcp.return_value.list_tools.return_value = [] + tools = await manager.get_tools() + assert isinstance(tools, dict) + assert "server1" in tools + +@pytest.mark.asyncio +async def test_server_manager_call_tool(mock_tool_schema, mock_mcp_client): + """Test ServerManager.call_tool() method.""" + manager = mock_mcp_client.servers + manager._servers_cache = {"test-tool": "test-server"} + + with patch("aicore.llm.mcp.client.FastMCPClient") as mock_fastmcp: + mock_fastmcp.return_value.call_tool.return_value = "result" + result = await manager.call_tool("test-tool", {}) + assert result == "result" + +@pytest.mark.asyncio +async def test_server_manager_call_tool_not_found(mock_mcp_client): + """Test ServerManager.call_tool() with non-existent tool.""" + manager = mock_mcp_client.servers + with pytest.raises(ValueError): + await manager.call_tool("nonexistent-tool", {}) + +@pytest.mark.asyncio +async def test_server_connection_context_manager(): + """Test ServerConnection as async context manager.""" + mock_transport = AsyncMock() + async with ServerConnection(mock_transport) as conn: + assert isinstance(conn, AsyncMock) + mock_transport.__aexit__.assert_awaited_once() + +def test_tool_schema_from_mcp_tool(): + """Test ToolSchema.from_mcp_tool() method.""" + mock_tool = MagicMock() + mock_tool.name = "test-tool" + mock_tool.description = "Test tool" + mock_tool.inputSchema = {"type": "object"} + + tool_schema = ToolSchema.from_mcp_tool(mock_tool) + assert isinstance(tool_schema, ToolSchema) + assert tool_schema.name == "test-tool" + +def test_tool_call_schema_validation(): + """Test ToolCallSchema validation.""" + tool_call = ToolCallSchema( + id="123", + name="test-tool", + arguments={"param": "value"} + ) + assert tool_call.name == "test-tool" + assert tool_call.arguments == {"param": "value"} + +@pytest.mark.asyncio +async def test_mcp_error_handling(mock_mcp_client): + """Test error handling in MCP operations.""" + manager = mock_mcp_client.servers + manager._servers_cache = {"test-tool": "test-server"} + + with patch("aicore.llm.mcp.client.FastMCPClient") as mock_fastmcp, \ + pytest.raises(FastMcpError): + mock_fastmcp.return_value.call_tool.side_effect = Exception("Test error") + await manager.call_tool("test-tool", {}) + +def test_mcp_server_config_validation(): + """Test MCPServerConfig validation.""" + config = MCPServerConfig( + name="test", + parameters={"url": "test"}, + transport_type="ws" + ) + assert config.name == "test" + assert config.transport_type == "ws" + +def test_raise_fast_mcp_error_decorator(): + """Test the raise_fast_mcp_error decorator.""" + @raise_fast_mcp_error(prefix="test") + def failing_function(): + raise ValueError("Test error") + + with pytest.raises(FastMcpError): + failing_function() + +@pytest.mark.asyncio +async def test_mcp_client_add_server(): + """Test adding a server configuration manually.""" + client = MCPClient() + with patch("aicore.llm.mcp.client.MCPClient._create_transport") as mock_transport: + mock_transport.return_value = AsyncMock() + client.add_server("test-server", {"command": "echo", "args": ["hello"]}) + assert "test-server" in client.server_configs + assert "test-server" in client.transports + +@pytest.mark.asyncio +async def test_mcp_client_connect_specific_server(mock_mcp_client): + """Test connecting to a specific server.""" + await mock_mcp_client.connect("test-server") + assert "test-server" in mock_mcp_client.transports + assert len(mock_mcp_client.transports) == 1 \ No newline at end of file From 03e21ef6a868dd1bcd7d45becea6546792007d50 Mon Sep 17 00:00:00 2001 From: bruno_v <120278082+BrunoV21@users.noreply.github.com> Date: Mon, 30 Jun 2025 12:59:25 +0100 Subject: [PATCH 2/2] Delete tests/pytest.ini --- tests/pytest.ini | 34 ---------------------------------- 1 file changed, 34 deletions(-) delete mode 100644 tests/pytest.ini diff --git a/tests/pytest.ini b/tests/pytest.ini deleted file mode 100644 index ba45e7c..0000000 --- a/tests/pytest.ini +++ /dev/null @@ -1,34 +0,0 @@ -# pytest.ini -[pytest] -env = - WORKSPACE="bv-test" - MCP_TEST_ENDPOINT="http://localhost:8080" - CONFIG_PATH="./config/config_example_mcp.yml" - MCP_JSON_PATH="./mcp_config_example.json" - LOGS_PATH="tests/logs" - -asyncio_mode = auto -testpaths = tests -python_files = test_*.py -python_functions = test_* -norecursedirs = .* venv build dist - -markers = - asyncio: mark tests as async (deselect with '-m "not asyncio"') - integration: integration tests that require external services - unit: unit tests that don't require external services - slow: tests that take longer to run - -log_cli = true -log_cli_level = INFO -log_cli_format = %(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s) -log_cli_date_format = %Y-%m-%d %H:%M:%S - -addopts = - --cov=aicore - --cov-report=term-missing - --cov-report=xml - --cov-fail-under=95 - -ra - -W ignore::DeprecationWarning - -p no:warnings \ No newline at end of file