""" Integration tests for MCP server connectivity. These tests require the full stack to be running: - docker compose -f docker-compose.dev.yml up Run with: pytest tests/integration/ -v --integration Or skip with: pytest tests/ -v --ignore=tests/integration/ """ import os from typing import Any import httpx import pytest # Skip all tests in this module if not running integration tests pytestmark = pytest.mark.skipif( os.getenv("RUN_INTEGRATION_TESTS", "false").lower() != "true", reason="Integration tests require RUN_INTEGRATION_TESTS=true and running stack", ) # Configuration from environment BACKEND_URL = os.getenv("BACKEND_URL", "http://localhost:8000") LLM_GATEWAY_URL = os.getenv("LLM_GATEWAY_URL", "http://localhost:8001") KNOWLEDGE_BASE_URL = os.getenv("KNOWLEDGE_BASE_URL", "http://localhost:8002") class TestMCPServerHealth: """Test that MCP servers are healthy and reachable.""" @pytest.mark.asyncio async def test_llm_gateway_health(self) -> None: """Test LLM Gateway health endpoint.""" async with httpx.AsyncClient() as client: response = await client.get(f"{LLM_GATEWAY_URL}/health", timeout=10.0) assert response.status_code == 200 data = response.json() assert data.get("status") == "healthy" or data.get("healthy") is True @pytest.mark.asyncio async def test_knowledge_base_health(self) -> None: """Test Knowledge Base health endpoint.""" async with httpx.AsyncClient() as client: response = await client.get(f"{KNOWLEDGE_BASE_URL}/health", timeout=10.0) assert response.status_code == 200 data = response.json() assert data.get("status") == "healthy" or data.get("healthy") is True @pytest.mark.asyncio async def test_backend_health(self) -> None: """Test Backend health endpoint.""" async with httpx.AsyncClient() as client: response = await client.get(f"{BACKEND_URL}/health", timeout=10.0) assert response.status_code == 200 class TestMCPClientManagerIntegration: """Test MCPClientManager can connect to real MCP servers.""" @pytest.mark.asyncio async def test_mcp_servers_list(self) -> None: """Test that backend can list MCP servers via API.""" async with httpx.AsyncClient() as client: # This endpoint lists configured MCP servers response = await client.get( f"{BACKEND_URL}/api/v1/mcp/servers", timeout=10.0, ) # Should return 200 or 401 (if auth required) assert response.status_code in [200, 401, 403] @pytest.mark.asyncio async def test_mcp_health_check_endpoint(self) -> None: """Test backend's MCP health check endpoint.""" async with httpx.AsyncClient() as client: response = await client.get( f"{BACKEND_URL}/api/v1/mcp/health", timeout=30.0, # MCP health checks can take time ) # Should return 200 or 401 (if auth required) if response.status_code == 200: data = response.json() # Check structure assert "servers" in data or "healthy" in data class TestLLMGatewayIntegration: """Test LLM Gateway MCP server functionality.""" @pytest.mark.asyncio async def test_list_models(self) -> None: """Test that LLM Gateway can list available models.""" async with httpx.AsyncClient() as client: # MCP servers use JSON-RPC 2.0 protocol at /mcp endpoint response = await client.post( f"{LLM_GATEWAY_URL}/mcp", json={ "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}, }, timeout=10.0, ) assert response.status_code == 200 data = response.json() # Should have tools listed assert "result" in data or "error" in data @pytest.mark.asyncio async def test_count_tokens(self) -> None: """Test token counting functionality.""" async with httpx.AsyncClient() as client: response = await client.post( f"{LLM_GATEWAY_URL}/mcp", json={ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "count_tokens", "arguments": { "project_id": "test-project", "agent_id": "test-agent", "text": "Hello, world!", }, }, }, timeout=10.0, ) assert response.status_code == 200 data = response.json() # Check for result or error if "result" in data: assert "content" in data["result"] or "token_count" in str( data["result"] ) class TestKnowledgeBaseIntegration: """Test Knowledge Base MCP server functionality.""" @pytest.mark.asyncio async def test_list_tools(self) -> None: """Test that Knowledge Base can list available tools.""" async with httpx.AsyncClient() as client: # Knowledge Base uses GET /mcp/tools for listing response = await client.get( f"{KNOWLEDGE_BASE_URL}/mcp/tools", timeout=10.0, ) assert response.status_code == 200 data = response.json() assert "tools" in data @pytest.mark.asyncio async def test_search_knowledge_empty(self) -> None: """Test search on empty knowledge base.""" async with httpx.AsyncClient() as client: # Knowledge Base uses direct tool name as method response = await client.post( f"{KNOWLEDGE_BASE_URL}/mcp", json={ "jsonrpc": "2.0", "id": 1, "method": "search_knowledge", "params": { "project_id": "test-project", "agent_id": "test-agent", "query": "test query", "limit": 5, }, }, timeout=10.0, ) assert response.status_code == 200 data = response.json() # Should return empty results or error for no collection assert "result" in data or "error" in data class TestEndToEndMCPFlow: """End-to-end tests for MCP integration flow.""" @pytest.mark.asyncio async def test_full_mcp_discovery_flow(self) -> None: """Test the full flow of discovering and listing MCP tools.""" async with httpx.AsyncClient() as client: # 1. Check backend health health = await client.get(f"{BACKEND_URL}/health", timeout=10.0) assert health.status_code == 200 # 2. Check LLM Gateway health llm_health = await client.get(f"{LLM_GATEWAY_URL}/health", timeout=10.0) assert llm_health.status_code == 200 # 3. Check Knowledge Base health kb_health = await client.get(f"{KNOWLEDGE_BASE_URL}/health", timeout=10.0) assert kb_health.status_code == 200 # 4. List tools from LLM Gateway (uses JSON-RPC at /mcp) llm_tools = await client.post( f"{LLM_GATEWAY_URL}/mcp", json={"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}}, timeout=10.0, ) assert llm_tools.status_code == 200 # 5. List tools from Knowledge Base (uses GET /mcp/tools) kb_tools = await client.get( f"{KNOWLEDGE_BASE_URL}/mcp/tools", timeout=10.0, ) assert kb_tools.status_code == 200 class TestContextEngineIntegration: """Test Context Engine integration with MCP servers.""" @pytest.mark.asyncio async def test_context_health_endpoint(self) -> None: """Test context engine health endpoint.""" async with httpx.AsyncClient() as client: response = await client.get( f"{BACKEND_URL}/api/v1/context/health", timeout=10.0, ) assert response.status_code == 200 data = response.json() assert data.get("status") == "healthy" assert "mcp_connected" in data assert "cache_enabled" in data @pytest.mark.asyncio async def test_context_budget_endpoint(self) -> None: """Test token budget endpoint.""" async with httpx.AsyncClient() as client: response = await client.get( f"{BACKEND_URL}/api/v1/context/budget/claude-3-sonnet", timeout=10.0, ) assert response.status_code == 200 data = response.json() assert "total_tokens" in data assert "system_tokens" in data assert data.get("model") == "claude-3-sonnet" @pytest.mark.asyncio async def test_context_assembly_requires_auth(self) -> None: """Test that context assembly requires authentication.""" async with httpx.AsyncClient() as client: response = await client.post( f"{BACKEND_URL}/api/v1/context/assemble", json={ "project_id": "test-project", "agent_id": "test-agent", "query": "test query", "model": "claude-3-sonnet", }, timeout=10.0, ) # Should require auth assert response.status_code in [401, 403] def run_quick_health_check() -> dict[str, Any]: """ Quick synchronous health check for all services. Can be run standalone to verify the stack is up. """ import httpx results: dict[str, Any] = { "backend": False, "llm_gateway": False, "knowledge_base": False, } try: with httpx.Client(timeout=5.0) as client: try: r = client.get(f"{BACKEND_URL}/health") results["backend"] = r.status_code == 200 except Exception: pass try: r = client.get(f"{LLM_GATEWAY_URL}/health") results["llm_gateway"] = r.status_code == 200 except Exception: pass try: r = client.get(f"{KNOWLEDGE_BASE_URL}/health") results["knowledge_base"] = r.status_code == 200 except Exception: pass except Exception: pass return results if __name__ == "__main__": print("Checking service health...") results = run_quick_health_check() for service, healthy in results.items(): status = "OK" if healthy else "FAILED" print(f" {service}: {status}") all_healthy = all(results.values()) if all_healthy: print("\nAll services healthy! Run integration tests with:") print(" RUN_INTEGRATION_TESTS=true pytest tests/integration/ -v") else: print("\nSome services are not healthy. Start the stack with:") print(" make dev")