Skip to main content
Glama

Weather MCP Server

Apache 2.0
23
  • Linux
  • Apple
test_performance.py17.4 kB
""" Performance and load tests for MCP Weather Server. """ import pytest import asyncio import time from unittest.mock import AsyncMock, Mock, patch from src.mcp_weather_server.server import ( register_all_tools, call_tool, tool_handlers ) class TestPerformance: """Performance tests for the weather server.""" def setup_method(self): """Setup for each test.""" global tool_handlers tool_handlers.clear() register_all_tools() @pytest.mark.asyncio async def test_weather_request_performance(self): """Test that weather requests complete within reasonable time.""" mock_geo_response = { "results": [{"latitude": 40.7128, "longitude": -74.0060}] } mock_weather_response = { "hourly": { "time": ["2024-01-01T12:00"], "temperature_2m": [20.0], "relative_humidity_2m": [60], "dew_point_2m": [12.0], "weather_code": [0], "wind_speed_10m": [15.0], "wind_direction_10m": [180], "wind_gusts_10m": [25.0], "precipitation": [0.0], "rain": [0.0], "snowfall": [0.0], "precipitation_probability": [10], "pressure_msl": [1013.25], "cloud_cover": [25], "uv_index": [5.0], "apparent_temperature": [19.0], "visibility": [10000] } } with patch('httpx.AsyncClient') as mock_client_class: mock_client = AsyncMock() def mock_get(url): response = Mock() response.status_code = 200 if "geocoding-api" in url: response.json.return_value = mock_geo_response else: response.json.return_value = mock_weather_response return response mock_client.get.side_effect = mock_get mock_client_class.return_value.__aenter__.return_value = mock_client with patch('src.mcp_weather_server.utils.get_closest_utc_index', return_value=0): start_time = time.time() result = await call_tool("get_current_weather", {"city": "New York"}) end_time = time.time() # Request should complete in less than 1 second (mocked) assert end_time - start_time < 1.0 assert len(result) == 1 assert not result[0].text.startswith("Error:") @pytest.mark.asyncio async def test_concurrent_request_performance(self): """Test performance with multiple concurrent requests.""" mock_geo_response = { "results": [{"latitude": 40.7128, "longitude": -74.0060}] } mock_weather_response = { "hourly": { "time": ["2024-01-01T12:00"], "temperature_2m": [20.0], "relative_humidity_2m": [60], "dew_point_2m": [12.0], "weather_code": [0], "wind_speed_10m": [15.0], "wind_direction_10m": [180], "wind_gusts_10m": [25.0], "precipitation": [0.0], "rain": [0.0], "snowfall": [0.0], "precipitation_probability": [10], "pressure_msl": [1013.25], "cloud_cover": [25], "uv_index": [5.0], "apparent_temperature": [19.0], "visibility": [10000] } } with patch('httpx.AsyncClient') as mock_client_class: mock_client = AsyncMock() def mock_get(url): response = Mock() response.status_code = 200 if "geocoding-api" in url: response.json.return_value = mock_geo_response else: response.json.return_value = mock_weather_response return response mock_client.get.side_effect = mock_get mock_client_class.return_value.__aenter__.return_value = mock_client with patch('src.mcp_weather_server.utils.get_closest_utc_index', return_value=0): # Create 10 concurrent requests tasks = [ call_tool("get_current_weather", {"city": f"City{i}"}) for i in range(10) ] start_time = time.time() results = await asyncio.gather(*tasks) end_time = time.time() # All requests should complete in reasonable time assert end_time - start_time < 2.0 assert len(results) == 10 # All requests should succeed for result in results: assert len(result) == 1 assert not result[0].text.startswith("Error:") @pytest.mark.asyncio async def test_time_tool_performance(self): """Test performance of time-related tools.""" from datetime import datetime from zoneinfo import ZoneInfo fixed_time = datetime(2024, 1, 1, 12, 0, 0, tzinfo=ZoneInfo("UTC")) with patch('src.mcp_weather_server.utils.get_zoneinfo') as mock_get_tz: mock_get_tz.return_value = ZoneInfo("UTC") with patch('src.mcp_weather_server.tools.tools_time.datetime') as mock_datetime: mock_datetime.now.return_value = fixed_time start_time = time.time() result = await call_tool("get_current_datetime", {"timezone_name": "UTC"}) end_time = time.time() # Time tools should be very fast assert end_time - start_time < 0.1 assert len(result) == 1 assert not result[0].text.startswith("Error:") @pytest.mark.asyncio async def test_memory_usage_with_many_requests(self): """Test that memory usage doesn't grow excessively with many requests.""" import gc import sys # Get initial memory usage (approximation) gc.collect() initial_objects = len(gc.get_objects()) mock_geo_response = { "results": [{"latitude": 40.7128, "longitude": -74.0060}] } mock_weather_response = { "hourly": { "time": ["2024-01-01T12:00"], "temperature_2m": [20.0], "relative_humidity_2m": [60], "dew_point_2m": [12.0], "weather_code": [0], "wind_speed_10m": [15.0], "wind_direction_10m": [180], "wind_gusts_10m": [25.0], "precipitation": [0.0], "rain": [0.0], "snowfall": [0.0], "precipitation_probability": [10], "pressure_msl": [1013.25], "cloud_cover": [25], "uv_index": [5.0], "apparent_temperature": [19.0], "visibility": [10000] } } with patch('httpx.AsyncClient') as mock_client_class: mock_client = AsyncMock() def mock_get(url): response = Mock() response.status_code = 200 if "geocoding-api" in url: response.json.return_value = mock_geo_response else: response.json.return_value = mock_weather_response return response mock_client.get.side_effect = mock_get mock_client_class.return_value.__aenter__.return_value = mock_client with patch('src.mcp_weather_server.utils.get_closest_utc_index', return_value=0): # Make many sequential requests for i in range(50): result = await call_tool("get_current_weather", {"city": f"City{i}"}) assert len(result) == 1 assert not result[0].text.startswith("Error:") # Force garbage collection and check memory gc.collect() final_objects = len(gc.get_objects()) # Object count shouldn't grow dramatically # Allow some growth but not excessive (factor of 2) assert final_objects < initial_objects * 2 class TestLoadTesting: """Load testing scenarios.""" def setup_method(self): """Setup for each test.""" global tool_handlers tool_handlers.clear() register_all_tools() @pytest.mark.asyncio async def test_burst_load_handling(self): """Test handling of burst load (many requests at once).""" mock_geo_response = { "results": [{"latitude": 40.7128, "longitude": -74.0060}] } mock_weather_response = { "hourly": { "time": ["2024-01-01T12:00"], "temperature_2m": [20.0], "relative_humidity_2m": [60], "dew_point_2m": [12.0], "weather_code": [0] } } with patch('httpx.AsyncClient') as mock_client_class: mock_client = AsyncMock() def mock_get(url): response = Mock() response.status_code = 200 if "geocoding-api" in url: response.json.return_value = mock_geo_response else: response.json.return_value = mock_weather_response return response mock_client.get.side_effect = mock_get mock_client_class.return_value.__aenter__.return_value = mock_client with patch('src.mcp_weather_server.utils.get_closest_utc_index', return_value=0): # Create a large burst of requests (simulate high load) burst_size = 25 tasks = [ call_tool("get_current_weather", {"city": f"BurstCity{i}"}) for i in range(burst_size) ] start_time = time.time() results = await asyncio.gather(*tasks, return_exceptions=True) end_time = time.time() # Check that most requests succeeded successful_results = [r for r in results if not isinstance(r, Exception)] error_results = [r for r in results if isinstance(r, Exception)] # At least 80% should succeed under load success_rate = len(successful_results) / len(results) assert success_rate >= 0.8, f"Success rate {success_rate} too low" # Total time should be reasonable even under load assert end_time - start_time < 5.0 @pytest.mark.asyncio async def test_mixed_tool_load(self): """Test load with mixed tool types.""" from datetime import datetime from zoneinfo import ZoneInfo # Setup weather mocks mock_geo_response = { "results": [{"latitude": 40.7128, "longitude": -74.0060}] } mock_weather_response = { "hourly": { "time": ["2024-01-01T12:00"], "temperature_2m": [20.0], "relative_humidity_2m": [60], "dew_point_2m": [12.0], "weather_code": [0] } } # Setup time mocks fixed_time = datetime(2024, 1, 1, 12, 0, 0, tzinfo=ZoneInfo("UTC")) with patch('httpx.AsyncClient') as mock_client_class: mock_client = AsyncMock() def mock_get(url): response = Mock() response.status_code = 200 if "geocoding-api" in url: response.json.return_value = mock_geo_response else: response.json.return_value = mock_weather_response return response mock_client.get.side_effect = mock_get mock_client_class.return_value.__aenter__.return_value = mock_client with patch('src.mcp_weather_server.utils.get_zoneinfo') as mock_get_tz: mock_get_tz.return_value = ZoneInfo("UTC") with patch('src.mcp_weather_server.tools.tools_time.datetime') as mock_datetime: mock_datetime.now.return_value = fixed_time with patch('src.mcp_weather_server.utils.get_closest_utc_index', return_value=0): # Create mixed requests tasks = [] for i in range(20): if i % 3 == 0: tasks.append(call_tool("get_current_weather", {"city": f"City{i}"})) elif i % 3 == 1: tasks.append(call_tool("get_current_datetime", {"timezone_name": "UTC"})) else: tasks.append(call_tool("get_timezone_info", {"timezone_name": "UTC"})) start_time = time.time() results = await asyncio.gather(*tasks, return_exceptions=True) end_time = time.time() # Check results successful_results = [r for r in results if not isinstance(r, Exception)] success_rate = len(successful_results) / len(results) assert success_rate >= 0.9, f"Mixed load success rate {success_rate} too low" assert end_time - start_time < 3.0 assert len(results) == 20 class TestStressConditions: """Stress testing under adverse conditions.""" def setup_method(self): """Setup for each test.""" global tool_handlers tool_handlers.clear() register_all_tools() @pytest.mark.asyncio async def test_high_error_rate_resilience(self): """Test resilience when external APIs have high error rates.""" error_count = 0 success_count = 0 def mock_get_with_errors(url): nonlocal error_count, success_count response = Mock() # Simulate 30% error rate if (error_count + success_count) % 10 < 3: error_count += 1 response.status_code = 500 else: success_count += 1 response.status_code = 200 if "geocoding-api" in url: response.json.return_value = { "results": [{"latitude": 40.7128, "longitude": -74.0060}] } else: response.json.return_value = { "hourly": { "time": ["2024-01-01T12:00"], "temperature_2m": [20.0], "relative_humidity_2m": [60], "dew_point_2m": [12.0], "weather_code": [0], "wind_speed_10m": [15.0], "wind_direction_10m": [180], "wind_gusts_10m": [25.0], "precipitation": [0.0], "rain": [0.0], "snowfall": [0.0], "precipitation_probability": [10], "pressure_msl": [1013.25], "cloud_cover": [25], "uv_index": [5.0], "apparent_temperature": [19.0], "visibility": [10000] } } return response with patch('httpx.AsyncClient') as mock_client_class: mock_client = AsyncMock() mock_client.get.side_effect = mock_get_with_errors mock_client_class.return_value.__aenter__.return_value = mock_client with patch('src.mcp_weather_server.utils.get_closest_utc_index', return_value=0): # Make requests despite high error rate tasks = [ call_tool("get_current_weather", {"city": f"ErrorTestCity{i}"}) for i in range(20) ] results = await asyncio.gather(*tasks, return_exceptions=True) # Count successes and errors successful_results = [] error_results = [] for result in results: if isinstance(result, Exception): error_results.append(result) else: if result[0].text.startswith("Error:"): error_results.append(result) else: successful_results.append(result) # Should handle errors gracefully without crashing assert len(results) == 20 # Some should succeed despite errors assert len(successful_results) > 0 # Error handling should be proper for error_result in error_results: if not isinstance(error_result, Exception): assert "Error:" in error_result[0].text

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/isdaniel/mcp_weather_server'

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