""" MCP protocol tests for the MCP server. Tests for server setup, tool registration, and logging configuration. """ import sys import io from unittest.mock import patch import pytest # Skip all tests if MCP is not available try: import mcp # noqa: F401 MCP_AVAILABLE = True except ImportError: MCP_AVAILABLE = False pytestmark = pytest.mark.skipif( not MCP_AVAILABLE, reason="MCP package not installed" ) class TestMCPServerSetup: """Tests for MCP server initialization and configuration.""" def test_mcp_server_instance_created(self): """Verify FastMCP server instance is created correctly.""" from local_deep_research.mcp.server import mcp assert mcp is not None assert mcp.name == "local-deep-research" def test_mcp_server_instructions_set(self): """Verify server instructions/description is set.""" from local_deep_research.mcp.server import mcp # FastMCP stores instructions assert mcp.instructions is not None assert "research" in mcp.instructions.lower() def test_run_server_function_exists(self): """Verify run_server function is exported.""" from local_deep_research.mcp import run_server assert callable(run_server) class TestMCPToolRegistration: """Tests for MCP tool decorators and registration.""" def test_all_expected_tools_are_defined(self): """Verify all 7 expected tools are defined as functions.""" from local_deep_research.mcp.server import ( quick_research, detailed_research, generate_report, analyze_documents, list_search_engines, list_strategies, get_configuration, ) # All tools should be callable assert callable(quick_research) assert callable(detailed_research) assert callable(generate_report) assert callable(analyze_documents) assert callable(list_search_engines) assert callable(list_strategies) assert callable(get_configuration) def test_quick_research_has_docstring(self): """Verify quick_research has documentation.""" from local_deep_research.mcp.server import quick_research assert quick_research.__doc__ is not None assert len(quick_research.__doc__) > 50 # Substantial docstring assert "query" in quick_research.__doc__.lower() def test_detailed_research_has_docstring(self): """Verify detailed_research has documentation.""" from local_deep_research.mcp.server import detailed_research assert detailed_research.__doc__ is not None assert "detailed" in detailed_research.__doc__.lower() def test_generate_report_has_docstring(self): """Verify generate_report has documentation.""" from local_deep_research.mcp.server import generate_report assert generate_report.__doc__ is not None assert "report" in generate_report.__doc__.lower() def test_analyze_documents_has_docstring(self): """Verify analyze_documents has documentation.""" from local_deep_research.mcp.server import analyze_documents assert analyze_documents.__doc__ is not None assert "document" in analyze_documents.__doc__.lower() def test_list_search_engines_has_docstring(self): """Verify list_search_engines has documentation.""" from local_deep_research.mcp.server import list_search_engines assert list_search_engines.__doc__ is not None assert "search" in list_search_engines.__doc__.lower() def test_list_strategies_has_docstring(self): """Verify list_strategies has documentation.""" from local_deep_research.mcp.server import list_strategies assert list_strategies.__doc__ is not None assert "strateg" in list_strategies.__doc__.lower() def test_get_configuration_has_docstring(self): """Verify get_configuration has documentation.""" from local_deep_research.mcp.server import get_configuration assert get_configuration.__doc__ is not None assert "config" in get_configuration.__doc__.lower() class TestMCPLogging: """Tests for MCP logging configuration (critical for STDIO).""" def test_no_stdout_pollution_from_list_strategies(self): """Verify list_strategies doesn't write to stdout.""" from local_deep_research.mcp.server import list_strategies # Capture stdout old_stdout = sys.stdout sys.stdout = captured_stdout = io.StringIO() try: result = list_strategies() stdout_output = captured_stdout.getvalue() finally: sys.stdout = old_stdout # Should have no stdout output (MCP uses stdout for JSON-RPC) assert stdout_output == "", f"Unexpected stdout output: {stdout_output}" assert result["status"] == "success" def test_no_stdout_pollution_from_get_configuration(self): """Verify get_configuration doesn't write to stdout.""" from local_deep_research.mcp.server import get_configuration # Capture stdout old_stdout = sys.stdout sys.stdout = captured_stdout = io.StringIO() try: result = get_configuration() stdout_output = captured_stdout.getvalue() finally: sys.stdout = old_stdout # Should have no stdout output assert stdout_output == "", f"Unexpected stdout output: {stdout_output}" assert result["status"] == "success" def test_no_stdout_pollution_during_error(self): """Verify error handling doesn't write to stdout.""" from local_deep_research.mcp.server import quick_research # Capture stdout old_stdout = sys.stdout sys.stdout = captured_stdout = io.StringIO() try: with patch( "local_deep_research.mcp.server.ldr_quick_summary", side_effect=Exception("Test error"), ): result = quick_research(query="test") stdout_output = captured_stdout.getvalue() finally: sys.stdout = old_stdout # Should have no stdout output even during errors assert stdout_output == "", f"Unexpected stdout output: {stdout_output}" assert result["status"] == "error" class TestAvailableStrategies: """Tests for the get_available_strategies function. MCP server exposes all strategies (show_all=True), so these tests validate the full list. """ def test_available_strategies_is_list(self): """Verify get_available_strategies returns a list.""" from local_deep_research.search_system_factory import ( get_available_strategies, ) strategies = get_available_strategies() assert isinstance(strategies, list) def test_available_strategies_has_entries(self): """Verify get_available_strategies has multiple entries.""" from local_deep_research.search_system_factory import ( get_available_strategies, ) strategies = get_available_strategies() assert len(strategies) >= 5 def test_available_strategies_have_required_fields(self): """Verify each strategy has name and description.""" from local_deep_research.search_system_factory import ( get_available_strategies, ) strategies = get_available_strategies() for strategy in strategies: assert "name" in strategy, f"Strategy missing 'name': {strategy}" assert "description" in strategy, ( f"Strategy missing 'description': {strategy}" ) assert isinstance(strategy["name"], str) assert isinstance(strategy["description"], str) assert len(strategy["name"]) > 0 assert len(strategy["description"]) > 0 def test_source_based_strategy_exists(self): """Verify source-based strategy is in the list.""" from local_deep_research.search_system_factory import ( get_available_strategies, ) strategies = get_available_strategies() strategy_names = [s["name"] for s in strategies] assert "source-based" in strategy_names def test_focused_iteration_strategy_exists(self): """Verify focused-iteration strategy is in the list.""" from local_deep_research.search_system_factory import ( get_available_strategies, ) strategies = get_available_strategies() strategy_names = [s["name"] for s in strategies] assert "focused-iteration" in strategy_names class TestHelperFunctions: """Tests for helper functions in server.py.""" def test_classify_error_is_callable(self): """Verify _classify_error function exists and is callable.""" from local_deep_research.mcp.server import _classify_error assert callable(_classify_error) def test_build_settings_overrides_is_callable(self): """Verify _build_settings_overrides function exists and is callable.""" from local_deep_research.mcp.server import _build_settings_overrides assert callable(_build_settings_overrides) def test_classify_error_returns_string(self): """Verify _classify_error returns a string.""" from local_deep_research.mcp.server import _classify_error result = _classify_error("Some error") assert isinstance(result, str) def test_build_settings_overrides_returns_dict(self): """Verify _build_settings_overrides returns a dict.""" from local_deep_research.mcp.server import _build_settings_overrides result = _build_settings_overrides() assert isinstance(result, dict)