/** * @file MCPClientTests.cpp * @brief Integration tests for MCPClient (15 TI) */ #include #include "shared/mcp/MCPClient.hpp" #include "mocks/MockTransport.hpp" #include #include using namespace aissia::mcp; using namespace aissia::tests; using json = nlohmann::json; // ============================================================================ // Helper: Create test config file // ============================================================================ std::string createTestConfigFile(const json& config) { std::string path = "test_mcp_config.json"; std::ofstream file(path); file << config.dump(2); file.close(); return path; } void cleanupTestConfigFile(const std::string& path) { std::filesystem::remove(path); } // ============================================================================ // TI_CLIENT_001: Load config valid // ============================================================================ TEST_CASE("TI_CLIENT_001_LoadConfigValid", "[mcp][client]") { json config = { {"servers", { {"test_server", { {"command", "python3"}, {"args", json::array({"server.py"})}, {"enabled", true} }} }} }; auto path = createTestConfigFile(config); MCPClient client; bool loaded = client.loadConfig(path); REQUIRE(loaded == true); cleanupTestConfigFile(path); } // ============================================================================ // TI_CLIENT_002: Load config invalid // ============================================================================ TEST_CASE("TI_CLIENT_002_LoadConfigInvalid", "[mcp][client]") { // Create file with invalid JSON std::string path = "invalid_config.json"; std::ofstream file(path); file << "{ invalid json }"; file.close(); MCPClient client; bool loaded = client.loadConfig(path); REQUIRE(loaded == false); cleanupTestConfigFile(path); } // ============================================================================ // TI_CLIENT_003: Load config missing file // ============================================================================ TEST_CASE("TI_CLIENT_003_LoadConfigMissingFile", "[mcp][client]") { MCPClient client; bool loaded = client.loadConfig("nonexistent_file.json"); REQUIRE(loaded == false); } // ============================================================================ // TI_CLIENT_004: ConnectAll starts servers // ============================================================================ TEST_CASE("TI_CLIENT_004_ConnectAllStartsServers", "[mcp][client]") { // Use the real mock MCP server fixture MCPClient client; bool loaded = client.loadConfig("tests/fixtures/mock_mcp.json"); if (loaded) { int connected = client.connectAll(); // Should connect to enabled servers REQUIRE(connected >= 0); client.disconnectAll(); } else { // Skip if fixture not available SUCCEED(); } } // ============================================================================ // TI_CLIENT_005: ConnectAll skips disabled // ============================================================================ TEST_CASE("TI_CLIENT_005_ConnectAllSkipsDisabled", "[mcp][client]") { json config = { {"servers", { {"enabled_server", { {"command", "python3"}, {"args", json::array({"tests/fixtures/echo_server.py"})}, {"enabled", true} }}, {"disabled_server", { {"command", "nonexistent"}, {"enabled", false} }} }} }; auto path = createTestConfigFile(config); MCPClient client; client.loadConfig(path); int connected = client.connectAll(); // disabled_server should not be connected REQUIRE(client.isConnected("disabled_server") == false); client.disconnectAll(); cleanupTestConfigFile(path); } // ============================================================================ // TI_CLIENT_006: Connect single server // ============================================================================ TEST_CASE("TI_CLIENT_006_ConnectSingleServer", "[mcp][client]") { json config = { {"servers", { {"server1", { {"command", "python3"}, {"args", json::array({"tests/fixtures/echo_server.py"})}, {"enabled", true} }}, {"server2", { {"command", "python3"}, {"args", json::array({"tests/fixtures/echo_server.py"})}, {"enabled", true} }} }} }; auto path = createTestConfigFile(config); MCPClient client; client.loadConfig(path); // Connect only server1 bool connected = client.connect("server1"); REQUIRE(connected == true); REQUIRE(client.isConnected("server1") == true); REQUIRE(client.isConnected("server2") == false); client.disconnectAll(); cleanupTestConfigFile(path); } // ============================================================================ // TI_CLIENT_007: Disconnect single server // ============================================================================ TEST_CASE("TI_CLIENT_007_DisconnectSingleServer", "[mcp][client]") { json config = { {"servers", { {"server1", { {"command", "python3"}, {"args", json::array({"tests/fixtures/echo_server.py"})}, {"enabled", true} }} }} }; auto path = createTestConfigFile(config); MCPClient client; client.loadConfig(path); client.connect("server1"); REQUIRE(client.isConnected("server1") == true); client.disconnect("server1"); REQUIRE(client.isConnected("server1") == false); cleanupTestConfigFile(path); } // ============================================================================ // TI_CLIENT_008: DisconnectAll cleans up // ============================================================================ TEST_CASE("TI_CLIENT_008_DisconnectAllCleansUp", "[mcp][client]") { json config = { {"servers", { {"server1", { {"command", "python3"}, {"args", json::array({"tests/fixtures/echo_server.py"})}, {"enabled", true} }}, {"server2", { {"command", "python3"}, {"args", json::array({"tests/fixtures/echo_server.py"})}, {"enabled", true} }} }} }; auto path = createTestConfigFile(config); MCPClient client; client.loadConfig(path); client.connectAll(); client.disconnectAll(); REQUIRE(client.isConnected("server1") == false); REQUIRE(client.isConnected("server2") == false); REQUIRE(client.getConnectedServers().empty() == true); cleanupTestConfigFile(path); } // ============================================================================ // TI_CLIENT_009: ListAllTools aggregates // ============================================================================ TEST_CASE("TI_CLIENT_009_ListAllToolsAggregates", "[mcp][client]") { MCPClient client; bool loaded = client.loadConfig("tests/fixtures/mock_mcp.json"); if (loaded) { client.connectAll(); auto tools = client.listAllTools(); // Should have tools from mock server REQUIRE(tools.size() >= 0); client.disconnectAll(); } else { SUCCEED(); } } // ============================================================================ // TI_CLIENT_010: Tool name prefixed // ============================================================================ TEST_CASE("TI_CLIENT_010_ToolNamePrefixed", "[mcp][client]") { MCPClient client; bool loaded = client.loadConfig("tests/fixtures/mock_mcp.json"); if (loaded) { client.connectAll(); auto tools = client.listAllTools(); bool hasPrefix = false; for (const auto& tool : tools) { if (tool.name.find(":") != std::string::npos) { hasPrefix = true; break; } } if (!tools.empty()) { REQUIRE(hasPrefix == true); } client.disconnectAll(); } else { SUCCEED(); } } // ============================================================================ // TI_CLIENT_011: CallTool routes to server // ============================================================================ TEST_CASE("TI_CLIENT_011_CallToolRoutesToServer", "[mcp][client]") { MCPClient client; bool loaded = client.loadConfig("tests/fixtures/mock_mcp.json"); if (loaded) { client.connectAll(); auto tools = client.listAllTools(); if (!tools.empty()) { // Call the first available tool auto result = client.callTool(tools[0].name, json::object()); // Should get some result (success or error) REQUIRE(result.content.size() >= 0); } client.disconnectAll(); } else { SUCCEED(); } } // ============================================================================ // TI_CLIENT_012: CallTool invalid name // ============================================================================ TEST_CASE("TI_CLIENT_012_CallToolInvalidName", "[mcp][client]") { MCPClient client; client.loadConfig("tests/fixtures/mock_mcp.json"); client.connectAll(); auto result = client.callTool("nonexistent:tool", json::object()); REQUIRE(result.isError == true); client.disconnectAll(); } // ============================================================================ // TI_CLIENT_013: CallTool disconnected server // ============================================================================ TEST_CASE("TI_CLIENT_013_CallToolDisconnectedServer", "[mcp][client]") { MCPClient client; // Don't connect any servers auto result = client.callTool("server:tool", json::object()); REQUIRE(result.isError == true); } // ============================================================================ // TI_CLIENT_014: ToolCount accurate // ============================================================================ TEST_CASE("TI_CLIENT_014_ToolCountAccurate", "[mcp][client]") { MCPClient client; bool loaded = client.loadConfig("tests/fixtures/mock_mcp.json"); if (loaded) { client.connectAll(); size_t count = client.toolCount(); auto tools = client.listAllTools(); REQUIRE(count == tools.size()); client.disconnectAll(); } else { SUCCEED(); } } // ============================================================================ // TI_CLIENT_015: IsConnected accurate // ============================================================================ TEST_CASE("TI_CLIENT_015_IsConnectedAccurate", "[mcp][client]") { json config = { {"servers", { {"test_server", { {"command", "python3"}, {"args", json::array({"tests/fixtures/echo_server.py"})}, {"enabled", true} }} }} }; auto path = createTestConfigFile(config); MCPClient client; client.loadConfig(path); // Not connected yet REQUIRE(client.isConnected("test_server") == false); // Connect client.connect("test_server"); REQUIRE(client.isConnected("test_server") == true); // Disconnect client.disconnect("test_server"); REQUIRE(client.isConnected("test_server") == false); cleanupTestConfigFile(path); }