Skip to main content
Glama
test_mcp.py27.1 kB
"""Tests for MCP server functionality.""" from unittest.mock import patch import pytest from mcp.types import TextContent from aerospace_mcp.server import ( TOOLS, _handle_calculate_distance, _handle_get_aircraft_performance, _handle_get_system_status, _handle_plan_flight, _handle_search_airports, handle_call_tool, handle_list_tools, server, ) class TestMCPServerInitialization: """Tests for MCP server initialization.""" @pytest.mark.unit def test_server_instance_created(self): """Test that server instance is created correctly.""" assert server is not None assert server.name == "aerospace-mcp" @pytest.mark.unit def test_tools_definition(self): """Test that all required tools are defined.""" tool_names = [tool.name for tool in TOOLS] expected_tools = [ "search_airports", "plan_flight", "calculate_distance", "get_aircraft_performance", "get_system_status", ] for expected_tool in expected_tools: assert expected_tool in tool_names # Updated for Phase 4 - now we have many more tools, just check required ones exist assert len(TOOLS) >= len(expected_tools) @pytest.mark.unit def test_tool_schemas_valid(self): """Test that all tool schemas are valid.""" for tool in TOOLS: assert isinstance(tool.name, str) assert len(tool.name) > 0 assert isinstance(tool.description, str) assert len(tool.description) > 0 assert isinstance(tool.inputSchema, dict) assert "type" in tool.inputSchema assert tool.inputSchema["type"] == "object" class TestListTools: """Tests for the list tools handler.""" @pytest.mark.unit @pytest.mark.asyncio async def test_handle_list_tools(self): """Test listing all available tools.""" tools = await handle_list_tools() assert isinstance(tools, list) assert len(tools) == len(TOOLS) tool_names = [tool.name for tool in tools] assert "search_airports" in tool_names assert "plan_flight" in tool_names assert "calculate_distance" in tool_names assert "get_aircraft_performance" in tool_names assert "get_system_status" in tool_names class TestSearchAirportsTool: """Tests for the search_airports tool.""" @pytest.mark.unit @pytest.mark.asyncio async def test_search_airports_by_iata(self, mock_airports_iata, sjc_airport): """Test searching airports by IATA code.""" with patch("aerospace_mcp.server._airport_from_iata") as mock_airport: mock_airport.return_value = sjc_airport result = await _handle_search_airports( {"query": "SJC", "query_type": "iata"} ) assert len(result) == 1 assert isinstance(result[0], TextContent) text_content = result[0].text assert "SJC" in text_content assert "San Jose" in text_content @pytest.mark.unit @pytest.mark.asyncio async def test_search_airports_by_city(self, mock_airports_iata, sjc_airport): """Test searching airports by city name.""" with patch("aerospace_mcp.server._find_city_airports") as mock_find: mock_find.return_value = [sjc_airport] result = await _handle_search_airports( {"query": "San Jose", "query_type": "city", "country": "US"} ) assert len(result) == 1 assert isinstance(result[0], TextContent) text_content = result[0].text assert "Found 1 airport(s)" in text_content assert "SJC" in text_content @pytest.mark.unit @pytest.mark.asyncio async def test_search_airports_auto_detect_iata( self, mock_airports_iata, sjc_airport ): """Test auto-detection of IATA codes.""" with patch("aerospace_mcp.server._airport_from_iata") as mock_airport: mock_airport.return_value = sjc_airport await _handle_search_airports({"query": "SJC", "query_type": "auto"}) mock_airport.assert_called_once_with("SJC") @pytest.mark.unit @pytest.mark.asyncio async def test_search_airports_auto_detect_city(self, sjc_airport): """Test auto-detection of city names.""" with patch("aerospace_mcp.server._find_city_airports") as mock_find: mock_find.return_value = [sjc_airport] await _handle_search_airports({"query": "San Jose", "query_type": "auto"}) mock_find.assert_called_once_with("San Jose", None) @pytest.mark.unit @pytest.mark.asyncio async def test_search_airports_not_found(self): """Test searching for non-existent airports.""" with patch("aerospace_mcp.server._airport_from_iata", return_value=None): result = await _handle_search_airports( {"query": "XYZ", "query_type": "iata"} ) assert len(result) == 1 assert "No airports found" in result[0].text @pytest.mark.unit @pytest.mark.asyncio async def test_search_airports_empty_query(self): """Test searching with empty query.""" result = await _handle_search_airports({"query": ""}) assert len(result) == 1 assert "Error: Query parameter is required" in result[0].text class TestPlanFlightTool: """Tests for the plan_flight tool.""" @pytest.mark.unit @pytest.mark.asyncio async def test_plan_flight_success( self, mock_airports_iata, sjc_airport, nrt_airport, mock_openap_flight_generator, mock_openap_fuel_flow, mock_openap_props, ): """Test successful flight planning.""" with patch("aerospace_mcp.server._resolve_endpoint") as mock_resolve: mock_resolve.side_effect = [sjc_airport, nrt_airport] with patch("aerospace_mcp.server.great_circle_points") as mock_gc: mock_gc.return_value = ([(37.36, -121.93), (35.76, 140.39)], 9000.0) with patch("aerospace_mcp.server.estimates_openap") as mock_estimates: mock_estimates.return_value = ( { "block": {"time_min": 600.0, "fuel_kg": 15000.0}, "climb": { "time_min": 20.0, "distance_km": 100.0, "fuel_kg": 1000.0, }, "cruise": { "time_min": 560.0, "distance_km": 8800.0, "fuel_kg": 13500.0, }, "descent": { "time_min": 20.0, "distance_km": 100.0, "fuel_kg": 500.0, }, "assumptions": {"mass_kg": 85000.0, "cruise_alt_ft": 35000}, }, "openap", ) result = await _handle_plan_flight( { "departure": {"city": "San Jose", "country": "US"}, "arrival": {"city": "Tokyo", "country": "JP"}, "aircraft": {"type": "A359"}, } ) assert len(result) == 1 text = result[0].text assert "Flight Plan: SJC → NRT" in text assert "Distance: 9000 km" in text assert "Block Time: 600 minutes" in text assert "Block Fuel: 15000 kg" in text @pytest.mark.unit @pytest.mark.asyncio async def test_plan_flight_same_city_error(self): """Test error when departure and arrival are the same city.""" result = await _handle_plan_flight( { "departure": {"city": "San Jose"}, "arrival": {"city": "San Jose"}, "aircraft": {"type": "A320"}, } ) assert len(result) == 1 assert "identical" in result[0].text.lower() @pytest.mark.unit @pytest.mark.asyncio async def test_plan_flight_airport_resolution_error(self): """Test error during airport resolution.""" with patch("aerospace_mcp.server._resolve_endpoint") as mock_resolve: from aerospace_mcp.core import AirportResolutionError mock_resolve.side_effect = AirportResolutionError("Airport not found") result = await _handle_plan_flight( { "departure": {"city": "Nonexistent"}, "arrival": {"city": "Tokyo"}, "aircraft": {"type": "A320"}, } ) assert len(result) == 1 assert "Airport resolution error" in result[0].text @pytest.mark.unit @pytest.mark.asyncio async def test_plan_flight_openap_error(self, sjc_airport, nrt_airport): """Test error during OpenAP estimation.""" with patch("aerospace_mcp.server._resolve_endpoint") as mock_resolve: mock_resolve.side_effect = [sjc_airport, nrt_airport] with patch("aerospace_mcp.server.great_circle_points") as mock_gc: mock_gc.return_value = ([(37.36, -121.93)], 9000.0) with patch("aerospace_mcp.server.estimates_openap") as mock_estimates: from aerospace_mcp.core import OpenAPError mock_estimates.side_effect = OpenAPError("OpenAP unavailable") result = await _handle_plan_flight( { "departure": {"city": "San Jose"}, "arrival": {"city": "Tokyo"}, "aircraft": {"type": "A320"}, } ) assert len(result) == 1 assert "Performance estimation error" in result[0].text @pytest.mark.unit @pytest.mark.asyncio async def test_plan_flight_with_preferred_iata(self, sjc_airport, nrt_airport): """Test flight planning with preferred IATA codes.""" with patch("aerospace_mcp.server._resolve_endpoint") as mock_resolve: mock_resolve.side_effect = [sjc_airport, nrt_airport] with patch("aerospace_mcp.server.great_circle_points") as mock_gc: mock_gc.return_value = ([(37.36, -121.93)], 1000.0) with patch("aerospace_mcp.server.estimates_openap") as mock_estimates: mock_estimates.return_value = ( { "block": {"time_min": 120.0, "fuel_kg": 3000.0}, "climb": { "time_min": 10.0, "distance_km": 50.0, "fuel_kg": 200.0, }, "cruise": { "time_min": 100.0, "distance_km": 900.0, "fuel_kg": 2600.0, }, "descent": { "time_min": 10.0, "distance_km": 50.0, "fuel_kg": 200.0, }, "assumptions": {"mass_kg": 70000.0, "cruise_alt_ft": 35000}, }, "openap", ) await _handle_plan_flight( { "departure": {"city": "Any City", "iata": "SJC"}, "arrival": {"city": "Any City", "iata": "NRT"}, "aircraft": {"type": "A320"}, } ) # Should call resolve_endpoint with the preferred IATA codes mock_resolve.assert_any_call("Any City", None, "SJC", "departure") mock_resolve.assert_any_call("Any City", None, "NRT", "arrival") class TestCalculateDistanceTool: """Tests for the calculate_distance tool.""" @pytest.mark.unit @pytest.mark.asyncio async def test_calculate_distance_success(self): """Test successful distance calculation.""" with patch("aerospace_mcp.server.great_circle_points") as mock_gc: mock_gc.return_value = ([(37.36, -121.93), (35.76, 140.39)], 9000.0) result = await _handle_calculate_distance( { "origin": {"latitude": 37.3626, "longitude": -121.929}, "destination": {"latitude": 35.7647, "longitude": 140.386}, "step_km": 100.0, } ) assert len(result) == 1 text = result[0].text assert "Great Circle Distance Calculation" in text assert "9000.00 km" in text assert "Origin: 37.3626, -121.9290" in text assert "Destination: 35.7647, 140.3860" in text @pytest.mark.unit @pytest.mark.asyncio async def test_calculate_distance_missing_coordinates(self): """Test error with missing coordinates.""" result = await _handle_calculate_distance( { "origin": {"latitude": 37.3626}, # Missing longitude "destination": {"latitude": 35.7647, "longitude": 140.386}, } ) assert len(result) == 1 assert "coordinates are required" in result[0].text @pytest.mark.unit @pytest.mark.asyncio async def test_calculate_distance_with_default_step(self): """Test distance calculation with default step size.""" with patch("aerospace_mcp.server.great_circle_points") as mock_gc: mock_gc.return_value = ([(37.36, -121.93)], 500.0) await _handle_calculate_distance( { "origin": {"latitude": 37.3626, "longitude": -121.929}, "destination": {"latitude": 35.7647, "longitude": 140.386}, } ) # Should use default step_km of 50.0 mock_gc.assert_called_once_with(37.3626, -121.929, 35.7647, 140.386, 50.0) class TestGetAircraftPerformanceTool: """Tests for the get_aircraft_performance tool.""" @pytest.mark.unit @pytest.mark.asyncio async def test_get_aircraft_performance_success(self): """Test successful aircraft performance estimation.""" with patch("aerospace_mcp.server.estimates_openap") as mock_estimates: mock_estimates.return_value = ( { "block": {"time_min": 180.0, "fuel_kg": 4500.0}, "climb": {"time_min": 15.0, "distance_km": 75.0, "fuel_kg": 500.0}, "cruise": { "time_min": 150.0, "distance_km": 1350.0, "fuel_kg": 3500.0, }, "descent": { "time_min": 15.0, "distance_km": 75.0, "fuel_kg": 500.0, }, "assumptions": {"mass_kg": 70000.0, "cruise_alt_ft": 35000}, }, "openap", ) result = await _handle_get_aircraft_performance( { "aircraft_type": "A320", "distance_km": 1500.0, "cruise_altitude": 35000, } ) assert len(result) == 1 text = result[0].text assert "Aircraft Performance Estimates (openap)" in text assert "Aircraft: A320" in text assert "Distance: 1500 km" in text assert "Time: 180 minutes" in text assert "Fuel: 4500 kg" in text @pytest.mark.unit @pytest.mark.asyncio async def test_get_aircraft_performance_missing_aircraft(self): """Test error with missing aircraft type.""" result = await _handle_get_aircraft_performance({"distance_km": 1500.0}) assert len(result) == 1 assert "Aircraft type is required" in result[0].text @pytest.mark.unit @pytest.mark.asyncio async def test_get_aircraft_performance_invalid_distance(self): """Test error with invalid distance.""" result = await _handle_get_aircraft_performance( {"aircraft_type": "A320", "distance_km": -100.0} ) assert len(result) == 1 assert "Distance must be positive" in result[0].text @pytest.mark.unit @pytest.mark.asyncio async def test_get_aircraft_performance_openap_error(self): """Test error during OpenAP estimation.""" with patch("aerospace_mcp.server.estimates_openap") as mock_estimates: from aerospace_mcp.core import OpenAPError mock_estimates.side_effect = OpenAPError("OpenAP not available") result = await _handle_get_aircraft_performance( {"aircraft_type": "A320", "distance_km": 1500.0} ) assert len(result) == 1 assert "Performance estimation error" in result[0].text class TestGetSystemStatusTool: """Tests for the get_system_status tool.""" @pytest.mark.unit @pytest.mark.asyncio async def test_get_system_status_with_openap(self): """Test system status when OpenAP is available.""" with patch("aerospace_mcp.server.OPENAP_AVAILABLE", True): with patch("aerospace_mcp.core._AIRPORTS_IATA", {"SJC": {}, "NRT": {}}): result = await _handle_get_system_status({}) assert len(result) == 1 text = result[0].text assert "Aerospace MCP Server Status" in text assert "OpenAP Available: Yes" in text assert "Airports Loaded: 2" in text assert "search_airports" in text assert "plan_flight" in text @pytest.mark.unit @pytest.mark.asyncio async def test_get_system_status_without_openap(self): """Test system status when OpenAP is not available.""" with patch("aerospace_mcp.server.OPENAP_AVAILABLE", False): with patch("aerospace_mcp.core._AIRPORTS_IATA", {"SJC": {}}): result = await _handle_get_system_status({}) assert len(result) == 1 text = result[0].text assert "OpenAP Available: No" in text assert "pip install openap" in text class TestHandleCallTool: """Tests for the main tool call handler.""" @pytest.mark.unit @pytest.mark.asyncio async def test_handle_call_tool_search_airports(self, sjc_airport): """Test calling search_airports tool through main handler.""" with patch("aerospace_mcp.server._handle_search_airports") as mock_handler: mock_handler.return_value = [TextContent(type="text", text="Test result")] result = await handle_call_tool("search_airports", {"query": "SJC"}) mock_handler.assert_called_once_with({"query": "SJC"}) assert len(result) == 1 assert result[0].text == "Test result" @pytest.mark.unit @pytest.mark.asyncio async def test_handle_call_tool_unknown_tool(self): """Test calling an unknown tool.""" result = await handle_call_tool("unknown_tool", {}) assert len(result) == 1 assert "Unknown tool: unknown_tool" in result[0].text @pytest.mark.unit @pytest.mark.asyncio async def test_handle_call_tool_exception_handling(self): """Test exception handling in tool calls.""" with patch("aerospace_mcp.server._handle_search_airports") as mock_handler: mock_handler.side_effect = Exception("Test exception") result = await handle_call_tool("search_airports", {"query": "SJC"}) assert len(result) == 1 assert "Error: Test exception" in result[0].text @pytest.mark.unit @pytest.mark.asyncio async def test_all_tools_callable_through_handler(self): """Test that all defined tools are callable through the main handler.""" tool_names = [tool.name for tool in TOOLS] for tool_name in tool_names: # Mock the specific handler for each tool handler_name = f"_handle_{tool_name}" with patch(f"aerospace_mcp.server.{handler_name}") as mock_handler: mock_handler.return_value = [ TextContent(type="text", text="Mock result") ] result = await handle_call_tool(tool_name, {}) assert len(result) == 1 assert result[0].text == "Mock result" mock_handler.assert_called_once() class TestMCPServerIntegration: """Integration tests for MCP server functionality.""" @pytest.mark.integration @pytest.mark.asyncio async def test_full_flight_planning_flow( self, mock_airports_iata, sjc_airport, nrt_airport ): """Test full flight planning workflow through MCP interface.""" # First, search for airports with patch("aerospace_mcp.server._find_city_airports") as mock_find: mock_find.return_value = [sjc_airport] search_result = await handle_call_tool( "search_airports", {"query": "San Jose", "country": "US"} ) assert len(search_result) == 1 assert "SJC" in search_result[0].text # Then, plan a flight with patch("aerospace_mcp.server._resolve_endpoint") as mock_resolve: mock_resolve.side_effect = [sjc_airport, nrt_airport] with patch("aerospace_mcp.server.great_circle_points") as mock_gc: mock_gc.return_value = ([(37.36, -121.93), (35.76, 140.39)], 9000.0) with patch("aerospace_mcp.server.estimates_openap") as mock_estimates: mock_estimates.return_value = ( { "block": {"time_min": 600.0, "fuel_kg": 15000.0}, "climb": { "time_min": 20.0, "distance_km": 100.0, "fuel_kg": 1000.0, }, "cruise": { "time_min": 560.0, "distance_km": 8800.0, "fuel_kg": 13500.0, }, "descent": { "time_min": 20.0, "distance_km": 100.0, "fuel_kg": 500.0, }, "assumptions": {"mass_kg": 85000.0, "cruise_alt_ft": 35000}, }, "openap", ) plan_result = await handle_call_tool( "plan_flight", { "departure": {"city": "San Jose", "country": "US"}, "arrival": {"city": "Tokyo", "country": "JP"}, "aircraft": {"type": "A359"}, }, ) assert len(plan_result) == 1 text = plan_result[0].text assert "Flight Plan: SJC → NRT" in text assert "Block Time: 600 minutes" in text @pytest.mark.integration @pytest.mark.asyncio async def test_error_propagation_through_mcp(self): """Test that errors propagate correctly through MCP interface.""" # Test airport resolution error with patch("aerospace_mcp.server._resolve_endpoint") as mock_resolve: from aerospace_mcp.core import AirportResolutionError mock_resolve.side_effect = AirportResolutionError("Airport not found") result = await handle_call_tool( "plan_flight", { "departure": {"city": "InvalidCity"}, "arrival": {"city": "Tokyo"}, "aircraft": {"type": "A320"}, }, ) assert len(result) == 1 assert "Airport resolution error" in result[0].text @pytest.mark.integration @pytest.mark.asyncio async def test_system_status_integration(self): """Test system status provides accurate information.""" result = await handle_call_tool("get_system_status", {}) assert len(result) == 1 text = result[0].text assert "Aerospace MCP Server Status" in text assert "Available Tools:" in text # Check that all tools are listed for tool in TOOLS: assert tool.name in text class TestMCPServerTransports: """Tests for MCP server transport mechanisms.""" @pytest.mark.unit def test_stdio_transport_function_exists(self): """Test that stdio transport function exists.""" from aerospace_mcp.server import run_stdio assert callable(run_stdio) @pytest.mark.unit def test_sse_transport_function_exists(self): """Test that SSE transport function exists.""" from aerospace_mcp.server import run_sse assert callable(run_sse) @pytest.mark.unit def test_main_run_function_exists(self): """Test that main run function exists.""" from aerospace_mcp.server import run assert callable(run) @pytest.mark.unit def test_run_function_argument_parsing(self): """Test run function argument parsing logic.""" import sys from unittest.mock import patch from aerospace_mcp.server import run # Test stdio mode (default) with patch("aerospace_mcp.server.run_stdio") as mock_stdio: with patch.object(sys, "argv", ["server.py"]): try: run() except Exception: pass # run_stdio will fail in test, but we just want to check it's called mock_stdio.assert_called_once() # Test SSE mode with patch("aerospace_mcp.server.run_sse") as mock_sse: with patch.object(sys, "argv", ["server.py", "sse"]): try: run() except Exception: pass # run_sse will fail in test, but we just want to check it's called mock_sse.assert_called_once_with("localhost", 8001)

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/cheesejaguar/aerospace-mcp'

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