""" Integration test for the MCP (Model Context Protocol) server. Tests MCP endpoints by starting a FastAPI server with MCP enabled and using the MCP client. Note: MCP server is integrated with the web server. These tests require HINDSIGHT_API_MCP_ENABLED=true. """ import asyncio import pytest import pytest_asyncio import httpx from mcp import ClientSession from mcp.client.sse import sse_client from hindsight_api.api import create_app @pytest_asyncio.fixture async def mcp_server(memory): """Start the FastAPI app with MCP enabled and return the SSE URL.""" # Memory is already initialized by the conftest fixture (with migrations) app = create_app( memory, initialize_memory=False, mcp_api_enabled=True ) # Use httpx to create a test server transport = httpx.ASGITransport(app=app) async with httpx.AsyncClient(transport=transport, base_url="http://test") as client: # The MCP SSE endpoint is at /mcp/sse # We need to yield the base URL for sse_client to connect # However, sse_client expects a real URL, not a test client # So we'll start a real server on a random port pass # For now, skip these tests as they require a real server # The sse_client doesn't work with ASGI test transport pytest.skip("MCP tests require a real running server. Run: HINDSIGHT_API_MCP_ENABLED=true uvicorn hindsight_api.api:app") @pytest.mark.asyncio async def test_mcp_server_tools_via_sse(mcp_server): """Test MCP server tools via SSE transport using proper MCP client.""" sse_url = mcp_server async with sse_client(sse_url) as (read, write): async with ClientSession(read, write) as session: await session.initialize() # Test 1: List tools tools_list = await session.list_tools() print(f"Tools: {tools_list}") tool_names = [t.name for t in tools_list.tools] assert "hindsight_search" in tool_names assert "hindsight_put" in tool_names # Test 2: Call hindsight_put put_result = await session.call_tool( "hindsight_put", arguments={ "content": "User loves Python programming", "context": "programming_preferences", "explanation": "Storing user's programming language preference" } ) print(f"Put result: {put_result}") assert put_result is not None # Wait a bit for indexing await asyncio.sleep(1) # Test 3: Call hindsight_search search_result = await session.call_tool( "hindsight_search", arguments={ "query": "What programming languages does the user like?", "max_tokens": 4096, "explanation": "Searching for programming preferences" } ) print(f"Search result: {search_result}") assert search_result is not None @pytest.mark.asyncio async def test_multiple_concurrent_requests(mcp_server): """Test multiple concurrent requests from a single session.""" sse_url = mcp_server async with sse_client(sse_url) as (read, write): async with ClientSession(read, write) as session: await session.initialize() # Fire off 10 concurrent search requests from same session async def make_search(idx): try: result = await session.call_tool( "hindsight_search", arguments={ "query": f"test query {idx}", "explanation": f"Concurrent test {idx}" } ) return idx, "success", result except Exception as e: return idx, "error", str(e) tasks = [make_search(i) for i in range(10)] results = await asyncio.gather(*tasks, return_exceptions=True) # Check results successes = 0 failures = 0 for result in results: if isinstance(result, Exception): print(f"Request failed with exception: {result}") failures += 1 else: idx, status, data = result if status == "success": successes += 1 else: print(f"Request {idx} failed: {data}") failures += 1 print(f"Successes: {successes}, Failures: {failures}") # We expect all requests to succeed assert successes >= 8, f"Too many failures: {failures}/10" @pytest.mark.asyncio async def test_race_condition_with_rapid_requests(mcp_server): """Test rapid-fire requests with multiple sessions to trigger race condition.""" sse_url = mcp_server async def rapid_session_search(idx): """Create a new session and immediately make a request.""" try: async with sse_client(sse_url) as (read, write): async with ClientSession(read, write) as session: await session.initialize() # Make request immediately after initialization result = await session.call_tool( "hindsight_search", arguments={ "query": f"rapid query {idx}", "max_tokens": 2048 } ) return idx, "success", result except Exception as e: return idx, "error", str(e) # Fire 20 requests with minimal delay, each with its own session tasks = [rapid_session_search(i) for i in range(20)] results = await asyncio.gather(*tasks) # Analyze results errors = [] for idx, status, data in results: if status == "error": errors.append((idx, data)) if errors: print(f"Found {len(errors)} errors:") for idx, error_msg in errors: print(f" Request {idx}: {error_msg}") # Most requests should succeed assert len(errors) < 5, f"Too many errors: {len(errors)}/20" if __name__ == "__main__": pytest.main([__file__, "-v", "-s"])