Skip to main content
Glama
test_server.py28.3 kB
""" Unit tests for MCP server functionality. """ import pytest import json from unittest.mock import AsyncMock, patch, MagicMock from mcp.types import CallToolRequest, CallToolResult, ListToolsRequest from daniel_lightrag_mcp.server import ( handle_list_tools, handle_call_tool, _validate_tool_arguments, _create_success_response, _create_error_response ) from daniel_lightrag_mcp.client import ( LightRAGError, LightRAGConnectionError, LightRAGValidationError, LightRAGAPIError ) class TestServerToolListing: """Test MCP server tool listing functionality.""" @pytest.mark.asyncio async def test_handle_list_tools(self): """Test that all 22 tools are listed correctly.""" result = await handle_list_tools() assert len(result.tools) == 22 # Check that all expected tools are present tool_names = [tool.name for tool in result.tools] # Document Management Tools (8 tools) document_tools = [ "insert_text", "insert_texts", "upload_document", "scan_documents", "get_documents", "get_documents_paginated", "delete_document", "clear_documents" ] # Query Tools (2 tools) query_tools = ["query_text", "query_text_stream"] # Knowledge Graph Tools (7 tools) graph_tools = [ "get_knowledge_graph", "get_graph_labels", "check_entity_exists", "update_entity", "update_relation", "delete_entity", "delete_relation" ] # System Management Tools (5 tools) system_tools = [ "get_pipeline_status", "get_track_status", "get_document_status_counts", "clear_cache", "get_health" ] all_expected_tools = document_tools + query_tools + graph_tools + system_tools for tool_name in all_expected_tools: assert tool_name in tool_names, f"Tool {tool_name} not found in listed tools" # Verify tool schemas have required properties for tool in result.tools: assert hasattr(tool, 'name') assert hasattr(tool, 'description') assert hasattr(tool, 'inputSchema') assert tool.description is not None assert tool.inputSchema is not None class TestToolArgumentValidation: """Test tool argument validation functionality.""" def test_validate_required_arguments_success(self): """Test successful validation of required arguments.""" # Test tools with required arguments _validate_tool_arguments("insert_text", {"text": "test content"}) _validate_tool_arguments("query_text", {"query": "test query"}) _validate_tool_arguments("delete_document", {"document_id": "doc_123"}) _validate_tool_arguments("update_entity", {"entity_id": "ent_123", "properties": {}}) def test_validate_missing_required_arguments(self): """Test validation failure for missing required arguments.""" with pytest.raises(LightRAGValidationError, match="Missing required arguments"): _validate_tool_arguments("insert_text", {}) with pytest.raises(LightRAGValidationError, match="Missing required arguments"): _validate_tool_arguments("query_text", {}) with pytest.raises(LightRAGValidationError, match="Missing required arguments"): _validate_tool_arguments("update_entity", {"entity_id": "ent_123"}) def test_validate_pagination_arguments(self): """Test validation of pagination arguments.""" # Valid pagination _validate_tool_arguments("get_documents_paginated", {"page": 1, "page_size": 10}) # Invalid page number with pytest.raises(LightRAGValidationError, match="Page must be a positive integer"): _validate_tool_arguments("get_documents_paginated", {"page": 0, "page_size": 10}) # Invalid page size with pytest.raises(LightRAGValidationError, match="Page size must be an integer"): _validate_tool_arguments("get_documents_paginated", {"page": 1, "page_size": 101}) def test_validate_query_mode(self): """Test validation of query mode arguments.""" # Valid modes for mode in ["naive", "local", "global", "hybrid"]: _validate_tool_arguments("query_text", {"query": "test", "mode": mode}) # Invalid mode with pytest.raises(LightRAGValidationError, match="Invalid query mode"): _validate_tool_arguments("query_text", {"query": "test", "mode": "invalid"}) class TestResponseCreation: """Test response creation utilities.""" def test_create_success_response(self): """Test creation of success responses.""" test_result = {"status": "success", "data": "test"} response = _create_success_response(test_result, "test_tool") assert isinstance(response, CallToolResult) assert not response.isError assert len(response.content) == 1 # Parse the JSON content content_text = response.content[0].text parsed_content = json.loads(content_text) assert parsed_content == test_result def test_create_error_response_lightrag_error(self): """Test creation of error responses for LightRAG errors.""" error = LightRAGValidationError("Test validation error", status_code=400) response = _create_error_response(error, "test_tool") assert isinstance(response, CallToolResult) assert response.isError assert len(response.content) == 1 # Parse the JSON content content_text = response.content[0].text parsed_content = json.loads(content_text) assert parsed_content["tool"] == "test_tool" assert parsed_content["error_type"] == "LightRAGValidationError" assert parsed_content["message"] == "Test validation error" def test_create_error_response_generic_error(self): """Test creation of error responses for generic errors.""" error = ValueError("Generic error") response = _create_error_response(error, "test_tool") assert isinstance(response, CallToolResult) assert response.isError assert len(response.content) == 1 # Parse the JSON content content_text = response.content[0].text parsed_content = json.loads(content_text) assert parsed_content["tool"] == "test_tool" assert parsed_content["error_type"] == "ValueError" assert parsed_content["message"] == "Generic error" @pytest.mark.asyncio class TestDocumentManagementTools: """Test document management MCP tools.""" @patch('daniel_lightrag_mcp.server.lightrag_client') async def test_insert_text_success(self, mock_client): """Test successful text insertion.""" # Setup mock mock_result = {"id": "doc_123", "status": "success"} mock_client.insert_text = AsyncMock(return_value=mock_result) # Create request request = CallToolRequest( method="tools/call", params={"name": "insert_text", "arguments": {"text": "test content"}} ) # Execute result = await handle_call_tool(request) # Verify assert not result.isError mock_client.insert_text.assert_called_once_with("test content") # Parse response content = json.loads(result.content[0].text) assert content == mock_result @patch('daniel_lightrag_mcp.server.lightrag_client') async def test_insert_texts_success(self, mock_client): """Test successful multiple text insertion.""" # Setup mock mock_result = {"id": "batch_123", "status": "success"} mock_client.insert_texts = AsyncMock(return_value=mock_result) # Create request texts = [{"content": "text 1"}, {"content": "text 2"}] request = CallToolRequest( method="tools/call", params={"name": "insert_texts", "arguments": {"texts": texts}} ) # Execute result = await handle_call_tool(request) # Verify assert not result.isError mock_client.insert_texts.assert_called_once_with(texts) @patch('daniel_lightrag_mcp.server.lightrag_client') async def test_upload_document_success(self, mock_client): """Test successful document upload.""" # Setup mock mock_result = {"filename": "test.txt", "status": "uploaded"} mock_client.upload_document = AsyncMock(return_value=mock_result) # Create request request = CallToolRequest( method="tools/call", params={"name": "upload_document", "arguments": {"file_path": "/path/to/file.txt"}} ) # Execute result = await handle_call_tool(request) # Verify assert not result.isError mock_client.upload_document.assert_called_once_with("/path/to/file.txt") @patch('daniel_lightrag_mcp.server.lightrag_client') async def test_scan_documents_success(self, mock_client): """Test successful document scanning.""" # Setup mock mock_result = {"scanned": 5, "new_documents": ["doc1.txt", "doc2.txt"]} mock_client.scan_documents = AsyncMock(return_value=mock_result) # Create request request = CallToolRequest( method="tools/call", params={"name": "scan_documents", "arguments": {}} ) # Execute result = await handle_call_tool(request) # Verify assert not result.isError mock_client.scan_documents.assert_called_once() @patch('daniel_lightrag_mcp.server.lightrag_client') async def test_get_documents_success(self, mock_client): """Test successful document retrieval.""" # Setup mock mock_result = {"documents": [{"id": "doc_123", "title": "Test"}], "total": 1} mock_client.get_documents = AsyncMock(return_value=mock_result) # Create request request = CallToolRequest( method="tools/call", params={"name": "get_documents", "arguments": {}} ) # Execute result = await handle_call_tool(request) # Verify assert not result.isError mock_client.get_documents.assert_called_once() @patch('daniel_lightrag_mcp.server.lightrag_client') async def test_get_documents_paginated_success(self, mock_client): """Test successful paginated document retrieval.""" # Setup mock mock_result = { "documents": [{"id": "doc_123", "title": "Test"}], "pagination": {"page": 1, "page_size": 10, "total_pages": 1} } mock_client.get_documents_paginated = AsyncMock(return_value=mock_result) # Create request request = CallToolRequest( method="tools/call", params={"name": "get_documents_paginated", "arguments": {"page": 1, "page_size": 10}} ) # Execute result = await handle_call_tool(request) # Verify assert not result.isError mock_client.get_documents_paginated.assert_called_once_with(1, 10) @patch('daniel_lightrag_mcp.server.lightrag_client') async def test_delete_document_success(self, mock_client): """Test successful document deletion.""" # Setup mock mock_result = {"deleted": True, "document_id": "doc_123"} mock_client.delete_document = AsyncMock(return_value=mock_result) # Create request request = CallToolRequest( method="tools/call", params={"name": "delete_document", "arguments": {"document_id": "doc_123"}} ) # Execute result = await handle_call_tool(request) # Verify assert not result.isError mock_client.delete_document.assert_called_once_with("doc_123") @patch('daniel_lightrag_mcp.server.lightrag_client') async def test_clear_documents_success(self, mock_client): """Test successful document clearing.""" # Setup mock mock_result = {"cleared": True, "count": 10} mock_client.clear_documents = AsyncMock(return_value=mock_result) # Create request request = CallToolRequest( method="tools/call", params={"name": "clear_documents", "arguments": {}} ) # Execute result = await handle_call_tool(request) # Verify assert not result.isError mock_client.clear_documents.assert_called_once() @pytest.mark.asyncio class TestQueryTools: """Test query MCP tools.""" @patch('daniel_lightrag_mcp.server.lightrag_client') async def test_query_text_success(self, mock_client): """Test successful text query.""" # Setup mock mock_result = { "query": "test query", "results": [{"document_id": "doc_123", "snippet": "test snippet", "score": 0.95}], "total_results": 1 } mock_client.query_text = AsyncMock(return_value=mock_result) # Create request request = CallToolRequest( method="tools/call", params={ "name": "query_text", "arguments": {"query": "test query", "mode": "hybrid", "only_need_context": False} } ) # Execute result = await handle_call_tool(request) # Verify assert not result.isError mock_client.query_text.assert_called_once_with( "test query", mode="hybrid", only_need_context=False ) @patch('daniel_lightrag_mcp.server.lightrag_client') async def test_query_text_stream_success(self, mock_client): """Test successful streaming text query.""" # Setup mock - async generator with proper signature async def mock_stream(query, mode="hybrid", only_need_context=False): yield "chunk 1" yield "chunk 2" yield "chunk 3" # Setup mock client mock_client.query_text_stream = mock_stream # Create request request = CallToolRequest( method="tools/call", params={ "name": "query_text_stream", "arguments": {"query": "test query", "mode": "hybrid"} } ) # Execute result = await handle_call_tool(request) # Verify assert not result.isError # Parse response to check streaming result content = json.loads(result.content[0].text) assert "streaming_response" in content assert content["streaming_response"] == "chunk 1chunk 2chunk 3" @pytest.mark.asyncio class TestKnowledgeGraphTools: """Test knowledge graph MCP tools.""" @patch('daniel_lightrag_mcp.server.lightrag_client') async def test_get_knowledge_graph_success(self, mock_client): """Test successful knowledge graph retrieval.""" # Setup mock mock_result = { "entities": [{"id": "ent_123", "name": "Test Entity"}], "relations": [{"id": "rel_123", "source_entity": "ent_123", "target_entity": "ent_456"}], "total_entities": 1, "total_relations": 1 } mock_client.get_knowledge_graph = AsyncMock(return_value=mock_result) # Create request request = CallToolRequest( method="tools/call", params={"name": "get_knowledge_graph", "arguments": {}} ) # Execute result = await handle_call_tool(request) # Verify assert not result.isError mock_client.get_knowledge_graph.assert_called_once() @patch('daniel_lightrag_mcp.server.lightrag_client') async def test_get_graph_labels_success(self, mock_client): """Test successful graph labels retrieval.""" # Setup mock mock_result = { "entity_labels": ["Person", "Organization", "Location"], "relation_labels": ["works_for", "located_in", "related_to"] } mock_client.get_graph_labels = AsyncMock(return_value=mock_result) # Create request request = CallToolRequest( method="tools/call", params={"name": "get_graph_labels", "arguments": {}} ) # Execute result = await handle_call_tool(request) # Verify assert not result.isError mock_client.get_graph_labels.assert_called_once() @patch('daniel_lightrag_mcp.server.lightrag_client') async def test_check_entity_exists_success(self, mock_client): """Test successful entity existence check.""" # Setup mock mock_result = {"exists": True, "entity_name": "Test Entity", "entity_id": "ent_123"} mock_client.check_entity_exists = AsyncMock(return_value=mock_result) # Create request request = CallToolRequest( method="tools/call", params={"name": "check_entity_exists", "arguments": {"entity_name": "Test Entity"}} ) # Execute result = await handle_call_tool(request) # Verify assert not result.isError mock_client.check_entity_exists.assert_called_once_with("Test Entity") @patch('daniel_lightrag_mcp.server.lightrag_client') async def test_update_entity_success(self, mock_client): """Test successful entity update.""" # Setup mock mock_result = {"updated": True, "entity_id": "ent_123"} mock_client.update_entity = AsyncMock(return_value=mock_result) # Create request properties = {"name": "Updated Entity", "type": "concept"} request = CallToolRequest( method="tools/call", params={ "name": "update_entity", "arguments": {"entity_id": "ent_123", "properties": properties} } ) # Execute result = await handle_call_tool(request) # Verify assert not result.isError mock_client.update_entity.assert_called_once_with("ent_123", properties) @patch('daniel_lightrag_mcp.server.lightrag_client') async def test_update_relation_success(self, mock_client): """Test successful relation update.""" # Setup mock mock_result = {"updated": True, "relation_id": "rel_123"} mock_client.update_relation = AsyncMock(return_value=mock_result) # Create request properties = {"type": "strongly_related", "weight": 0.9} request = CallToolRequest( method="tools/call", params={ "name": "update_relation", "arguments": {"relation_id": "rel_123", "properties": properties} } ) # Execute result = await handle_call_tool(request) # Verify assert not result.isError mock_client.update_relation.assert_called_once_with("rel_123", properties) @patch('daniel_lightrag_mcp.server.lightrag_client') async def test_delete_entity_success(self, mock_client): """Test successful entity deletion.""" # Setup mock mock_result = {"deleted": True, "id": "ent_123", "type": "entity"} mock_client.delete_entity = AsyncMock(return_value=mock_result) # Create request request = CallToolRequest( method="tools/call", params={"name": "delete_entity", "arguments": {"entity_id": "ent_123"}} ) # Execute result = await handle_call_tool(request) # Verify assert not result.isError mock_client.delete_entity.assert_called_once_with("ent_123") @patch('daniel_lightrag_mcp.server.lightrag_client') async def test_delete_relation_success(self, mock_client): """Test successful relation deletion.""" # Setup mock mock_result = {"deleted": True, "id": "rel_123", "type": "relation"} mock_client.delete_relation = AsyncMock(return_value=mock_result) # Create request request = CallToolRequest( method="tools/call", params={"name": "delete_relation", "arguments": {"relation_id": "rel_123"}} ) # Execute result = await handle_call_tool(request) # Verify assert not result.isError mock_client.delete_relation.assert_called_once_with("rel_123") @pytest.mark.asyncio class TestSystemManagementTools: """Test system management MCP tools.""" @patch('daniel_lightrag_mcp.server.lightrag_client') async def test_get_pipeline_status_success(self, mock_client): """Test successful pipeline status retrieval.""" # Setup mock mock_result = { "status": "running", "progress": 75.5, "current_task": "processing documents" } mock_client.get_pipeline_status = AsyncMock(return_value=mock_result) # Create request request = CallToolRequest( method="tools/call", params={"name": "get_pipeline_status", "arguments": {}} ) # Execute result = await handle_call_tool(request) # Verify assert not result.isError mock_client.get_pipeline_status.assert_called_once() @patch('daniel_lightrag_mcp.server.lightrag_client') async def test_get_track_status_success(self, mock_client): """Test successful track status retrieval.""" # Setup mock mock_result = { "track_id": "track_123", "status": "completed", "progress": 100.0 } mock_client.get_track_status = AsyncMock(return_value=mock_result) # Create request request = CallToolRequest( method="tools/call", params={"name": "get_track_status", "arguments": {"track_id": "track_123"}} ) # Execute result = await handle_call_tool(request) # Verify assert not result.isError mock_client.get_track_status.assert_called_once_with("track_123") @patch('daniel_lightrag_mcp.server.lightrag_client') async def test_get_document_status_counts_success(self, mock_client): """Test successful document status counts retrieval.""" # Setup mock mock_result = { "pending": 5, "processing": 2, "processed": 100, "failed": 1, "total": 108 } mock_client.get_document_status_counts = AsyncMock(return_value=mock_result) # Create request request = CallToolRequest( method="tools/call", params={"name": "get_document_status_counts", "arguments": {}} ) # Execute result = await handle_call_tool(request) # Verify assert not result.isError mock_client.get_document_status_counts.assert_called_once() @patch('daniel_lightrag_mcp.server.lightrag_client') async def test_clear_cache_success(self, mock_client): """Test successful cache clearing.""" # Setup mock mock_result = {"cleared": True, "cache_type": "all"} mock_client.clear_cache = AsyncMock(return_value=mock_result) # Create request request = CallToolRequest( method="tools/call", params={"name": "clear_cache", "arguments": {}} ) # Execute result = await handle_call_tool(request) # Verify assert not result.isError mock_client.clear_cache.assert_called_once() @patch('daniel_lightrag_mcp.server.lightrag_client') async def test_get_health_success(self, mock_client): """Test successful health check.""" # Setup mock mock_result = { "status": "healthy", "version": "1.0.0", "uptime": 3600.0, "database_status": "connected" } mock_client.get_health = AsyncMock(return_value=mock_result) # Create request request = CallToolRequest( method="tools/call", params={"name": "get_health", "arguments": {}} ) # Execute result = await handle_call_tool(request) # Verify assert not result.isError mock_client.get_health.assert_called_once() @pytest.mark.asyncio class TestErrorHandling: """Test error handling in MCP tools.""" @patch('daniel_lightrag_mcp.server.lightrag_client') async def test_tool_validation_error(self, mock_client): """Test handling of validation errors.""" # Create request with missing required argument request = CallToolRequest( method="tools/call", params={"name": "insert_text", "arguments": {}} # Missing 'text' argument ) # Execute result = await handle_call_tool(request) # Verify error response assert result.isError content = json.loads(result.content[0].text) assert content["error_type"] == "LightRAGValidationError" assert "Missing required arguments" in content["message"] @patch('daniel_lightrag_mcp.server.lightrag_client') async def test_client_connection_error(self, mock_client): """Test handling of connection errors.""" # Setup mock to raise connection error mock_client.insert_text = AsyncMock( side_effect=LightRAGConnectionError("Connection failed") ) # Create request request = CallToolRequest( method="tools/call", params={"name": "insert_text", "arguments": {"text": "test"}} ) # Execute result = await handle_call_tool(request) # Verify error response assert result.isError content = json.loads(result.content[0].text) assert content["error_type"] == "LightRAGConnectionError" assert "Connection failed" in content["message"] @patch('daniel_lightrag_mcp.server.lightrag_client') async def test_unknown_tool_error(self, mock_client): """Test handling of unknown tool calls.""" # Create request for non-existent tool request = CallToolRequest( method="tools/call", params={"name": "unknown_tool", "arguments": {}} ) # Execute result = await handle_call_tool(request) # Verify error response assert result.isError assert "Unknown tool: unknown_tool" in result.content[0].text @patch('daniel_lightrag_mcp.server.lightrag_client', None) async def test_client_initialization_error(self): """Test handling of client initialization errors.""" with patch('daniel_lightrag_mcp.server.LightRAGClient', side_effect=Exception("Init failed")): # Create request request = CallToolRequest( method="tools/call", params={"name": "get_health", "arguments": {}} ) # Execute result = await handle_call_tool(request) # Verify error response assert result.isError content = json.loads(result.content[0].text) assert content["error_type"] == "LightRAGConnectionError" assert "Failed to initialize LightRAG client" in content["message"]

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/desimpkins/daniel-lightrag-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server