Skip to main content
Glama

MaverickMCP

by wshobson
MIT License
165
  • Apple
test_mcp_tools.py34.2 kB
""" MCP Tools Integration Tests for Claude Desktop Interaction. This test suite covers: - All MCP tool registrations and functionality - Tool parameter validation and error handling - Tool response formats and data integrity - Claude Desktop simulation and interaction patterns - Real-world usage scenarios - Performance and timeout handling """ import asyncio import logging from unittest.mock import Mock, patch import numpy as np import pytest from fastmcp import Context from maverick_mcp.api.routers.backtesting import setup_backtesting_tools logger = logging.getLogger(__name__) class MockFastMCP: """Mock FastMCP instance for testing tool registration.""" def __init__(self): self.tools = {} self.tool_functions = {} def tool(self, name: str = None): """Mock tool decorator.""" def decorator(func): tool_name = name or func.__name__ self.tools[tool_name] = { "function": func, "name": tool_name, "signature": self._get_function_signature(func), } self.tool_functions[tool_name] = func return func return decorator def _get_function_signature(self, func): """Extract function signature for validation.""" import inspect sig = inspect.signature(func) return { "parameters": list(sig.parameters.keys()), "annotations": {k: str(v.annotation) for k, v in sig.parameters.items()}, } class TestMCPToolsIntegration: """Integration tests for MCP tools and Claude Desktop interaction.""" @pytest.fixture def mock_mcp(self): """Create mock FastMCP instance.""" return MockFastMCP() @pytest.fixture def mock_context(self): """Create mock MCP context.""" context = Mock(spec=Context) context.session = {} return context @pytest.fixture async def setup_tools(self, mock_mcp): """Set up all backtesting tools for testing.""" setup_backtesting_tools(mock_mcp) return mock_mcp async def test_all_mcp_tools_registration(self, setup_tools): """Test that all MCP tools are properly registered.""" mcp = setup_tools # Expected tools from backtesting router expected_tools = [ "run_backtest", "optimize_strategy", "walk_forward_analysis", "monte_carlo_simulation", "compare_strategies", "list_strategies", "parse_strategy", "backtest_portfolio", "generate_backtest_charts", "generate_optimization_charts", "run_ml_strategy_backtest", "train_ml_predictor", "analyze_market_regimes", "create_strategy_ensemble", ] # Check all tools are registered registered_tools = set(mcp.tools.keys()) expected_set = set(expected_tools) missing_tools = expected_set - registered_tools extra_tools = registered_tools - expected_set assert len(missing_tools) == 0, f"Missing tools: {missing_tools}" logger.info(f"✓ All {len(registered_tools)} MCP tools registered successfully") if extra_tools: logger.info(f"Additional tools found: {extra_tools}") # Validate each tool has proper signature for tool_name, tool_info in mcp.tools.items(): assert callable(tool_info["function"]), f"Tool {tool_name} is not callable" assert "signature" in tool_info, f"Tool {tool_name} missing signature" return { "registered_tools": list(registered_tools), "tool_count": len(registered_tools), } async def test_run_backtest_tool_comprehensive(self, setup_tools, mock_context): """Test run_backtest tool with comprehensive parameter validation.""" mcp = setup_tools tool_func = mcp.tool_functions["run_backtest"] # Test cases with different parameter combinations test_cases = [ { "name": "basic_sma_cross", "params": { "symbol": "AAPL", "strategy": "sma_cross", "fast_period": "10", "slow_period": "20", }, "should_succeed": True, }, { "name": "rsi_strategy", "params": { "symbol": "GOOGL", "strategy": "rsi", "period": "14", "oversold": "30", "overbought": "70", }, "should_succeed": True, }, { "name": "invalid_symbol", "params": { "symbol": "", # Empty symbol "strategy": "sma_cross", }, "should_succeed": False, }, { "name": "invalid_strategy", "params": { "symbol": "AAPL", "strategy": "nonexistent_strategy", }, "should_succeed": False, }, { "name": "invalid_numeric_params", "params": { "symbol": "AAPL", "strategy": "sma_cross", "fast_period": "invalid_number", }, "should_succeed": False, }, ] results = {} for test_case in test_cases: try: # Mock the VectorBT engine to avoid actual data fetching with patch("maverick_mcp.backtesting.VectorBTEngine") as mock_engine: mock_instance = Mock() mock_engine.return_value = mock_instance # Mock successful backtest result mock_result = { "symbol": test_case["params"]["symbol"], "strategy_type": test_case["params"]["strategy"], "metrics": { "total_return": 0.15, "sharpe_ratio": 1.2, "max_drawdown": -0.12, "total_trades": 25, }, "trades": [], "equity_curve": [10000, 10100, 10200, 10300], "drawdown_series": [0, -0.01, -0.02, 0], } mock_instance.run_backtest.return_value = mock_result # Execute tool result = await tool_func(mock_context, **test_case["params"]) if test_case["should_succeed"]: assert isinstance(result, dict), ( f"Result should be dict for {test_case['name']}" ) assert "symbol" in result, ( f"Missing symbol in result for {test_case['name']}" ) assert "metrics" in result, ( f"Missing metrics in result for {test_case['name']}" ) results[test_case["name"]] = {"success": True, "result": result} logger.info(f"✓ {test_case['name']} succeeded as expected") else: # If we got here, it didn't fail as expected results[test_case["name"]] = { "success": False, "unexpected_success": True, } logger.warning( f"⚠ {test_case['name']} succeeded but was expected to fail" ) except Exception as e: if test_case["should_succeed"]: results[test_case["name"]] = {"success": False, "error": str(e)} logger.error(f"✗ {test_case['name']} failed unexpectedly: {e}") else: results[test_case["name"]] = { "success": True, "expected_error": str(e), } logger.info(f"✓ {test_case['name']} failed as expected: {e}") # Calculate success rate total_tests = len(test_cases) successful_tests = sum(1 for r in results.values() if r.get("success", False)) success_rate = successful_tests / total_tests assert success_rate >= 0.8, f"Success rate too low: {success_rate:.1%}" return {"test_results": results, "success_rate": success_rate} async def test_strategy_tools_integration(self, setup_tools, mock_context): """Test strategy-related tools integration.""" mcp = setup_tools # Test list_strategies tool list_func = mcp.tool_functions["list_strategies"] strategies_result = await list_func(mock_context) assert isinstance(strategies_result, dict), "list_strategies should return dict" assert "available_strategies" in strategies_result, ( "Missing available_strategies" ) assert "total_count" in strategies_result, "Missing total_count" assert strategies_result["total_count"] > 0, "Should have strategies available" logger.info(f"✓ Found {strategies_result['total_count']} available strategies") # Test parse_strategy tool parse_func = mcp.tool_functions["parse_strategy"] parse_test_cases = [ "Buy when RSI is below 30 and sell when above 70", "Use 10-day and 20-day moving average crossover", "MACD strategy with standard parameters", "Invalid strategy description that makes no sense", ] parse_results = {} for description in parse_test_cases: try: result = await parse_func(mock_context, description=description) assert isinstance(result, dict), "parse_strategy should return dict" assert "success" in result, "Missing success field" assert "strategy" in result, "Missing strategy field" parse_results[description] = result status = "✓" if result["success"] else "⚠" logger.info( f"{status} Parsed: '{description}' -> {result['strategy'].get('strategy_type', 'unknown')}" ) except Exception as e: parse_results[description] = {"error": str(e)} logger.error(f"✗ Parse failed for: '{description}' - {e}") return { "strategies_list": strategies_result, "parse_results": parse_results, } async def test_optimization_tools_integration(self, setup_tools, mock_context): """Test optimization-related tools integration.""" mcp = setup_tools # Mock VectorBT engine for optimization tests with patch("maverick_mcp.backtesting.VectorBTEngine") as mock_engine_class: mock_engine = Mock() mock_engine_class.return_value = mock_engine # Mock optimization results mock_optimization_result = { "best_parameters": {"fast_period": 12, "slow_period": 26}, "best_performance": { "total_return": 0.25, "sharpe_ratio": 1.8, "max_drawdown": -0.08, }, "optimization_results": [ { "parameters": {"fast_period": 10, "slow_period": 20}, "metrics": {"sharpe_ratio": 1.2}, }, { "parameters": {"fast_period": 12, "slow_period": 26}, "metrics": {"sharpe_ratio": 1.8}, }, ], } mock_engine.optimize_parameters.return_value = mock_optimization_result # Test optimize_strategy tool optimize_func = mcp.tool_functions["optimize_strategy"] result = await optimize_func( mock_context, symbol="AAPL", strategy="sma_cross", optimization_level="medium", top_n=5, ) assert isinstance(result, dict), "optimize_strategy should return dict" logger.info("✓ optimize_strategy tool executed successfully") # Test walk_forward_analysis tool walk_forward_func = mcp.tool_functions["walk_forward_analysis"] # Mock walk-forward analysis with patch( "maverick_mcp.backtesting.StrategyOptimizer" ) as mock_optimizer_class: mock_optimizer = Mock() mock_optimizer_class.return_value = mock_optimizer mock_walk_forward_result = { "out_of_sample_performance": { "total_return": 0.18, "sharpe_ratio": 1.5, "win_rate": 0.65, }, "windows_tested": 4, "average_window_performance": 0.15, } mock_optimizer.walk_forward_analysis.return_value = ( mock_walk_forward_result ) result = await walk_forward_func( mock_context, symbol="AAPL", strategy="sma_cross", window_size=252, step_size=63, ) assert isinstance(result, dict), ( "walk_forward_analysis should return dict" ) logger.info("✓ walk_forward_analysis tool executed successfully") return {"optimization_tests": "completed"} async def test_ml_tools_integration(self, setup_tools, mock_context): """Test ML-enhanced tools integration.""" mcp = setup_tools # Test ML strategy tools ml_tools = [ "run_ml_strategy_backtest", "train_ml_predictor", "analyze_market_regimes", "create_strategy_ensemble", ] ml_results = {} for tool_name in ml_tools: if tool_name in mcp.tool_functions: try: tool_func = mcp.tool_functions[tool_name] # Mock ML dependencies with patch( "maverick_mcp.backtesting.VectorBTEngine" ) as mock_engine: mock_instance = Mock() mock_engine.return_value = mock_instance # Mock historical data import numpy as np import pandas as pd dates = pd.date_range( start="2022-01-01", end="2023-12-31", freq="D" ) mock_data = pd.DataFrame( { "open": np.random.uniform(100, 200, len(dates)), "high": np.random.uniform(100, 200, len(dates)), "low": np.random.uniform(100, 200, len(dates)), "close": np.random.uniform(100, 200, len(dates)), "volume": np.random.randint( 1000000, 10000000, len(dates) ), }, index=dates, ) mock_instance.get_historical_data.return_value = mock_data # Test specific ML tools if tool_name == "run_ml_strategy_backtest": result = await tool_func( mock_context, symbol="AAPL", strategy_type="ml_predictor", model_type="random_forest", ) elif tool_name == "train_ml_predictor": result = await tool_func( mock_context, symbol="AAPL", model_type="random_forest", n_estimators=100, ) elif tool_name == "analyze_market_regimes": result = await tool_func( mock_context, symbol="AAPL", method="hmm", n_regimes=3, ) elif tool_name == "create_strategy_ensemble": result = await tool_func( mock_context, symbols=["AAPL", "GOOGL"], base_strategies=["sma_cross", "rsi"], ) ml_results[tool_name] = { "success": True, "type": type(result).__name__, } logger.info(f"✓ {tool_name} executed successfully") except Exception as e: ml_results[tool_name] = {"success": False, "error": str(e)} logger.error(f"✗ {tool_name} failed: {e}") else: ml_results[tool_name] = {"success": False, "error": "Tool not found"} return ml_results async def test_visualization_tools_integration(self, setup_tools, mock_context): """Test visualization tools integration.""" mcp = setup_tools visualization_tools = [ "generate_backtest_charts", "generate_optimization_charts", ] viz_results = {} for tool_name in visualization_tools: if tool_name in mcp.tool_functions: try: tool_func = mcp.tool_functions[tool_name] # Mock VectorBT engine and visualization dependencies with patch( "maverick_mcp.backtesting.VectorBTEngine" ) as mock_engine: mock_instance = Mock() mock_engine.return_value = mock_instance # Mock backtest result for charts mock_result = { "symbol": "AAPL", "equity_curve": [10000, 10100, 10200, 10300, 10250], "drawdown_series": [0, -0.01, -0.02, 0, -0.005], "trades": [ { "entry_time": "2023-01-01", "exit_time": "2023-02-01", "pnl": 100, }, { "entry_time": "2023-03-01", "exit_time": "2023-04-01", "pnl": -50, }, ], "metrics": { "total_return": 0.15, "sharpe_ratio": 1.2, "max_drawdown": -0.08, "total_trades": 10, }, } mock_instance.run_backtest.return_value = mock_result # Mock visualization functions with patch( "maverick_mcp.backtesting.visualization.generate_equity_curve" ) as mock_equity: with patch( "maverick_mcp.backtesting.visualization.generate_performance_dashboard" ) as mock_dashboard: with patch( "maverick_mcp.backtesting.visualization.generate_trade_scatter" ) as mock_scatter: with patch( "maverick_mcp.backtesting.visualization.generate_optimization_heatmap" ) as mock_heatmap: # Mock chart returns (base64 strings) mock_chart_data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" mock_equity.return_value = mock_chart_data mock_dashboard.return_value = mock_chart_data mock_scatter.return_value = mock_chart_data mock_heatmap.return_value = mock_chart_data # Execute visualization tool result = await tool_func( mock_context, symbol="AAPL", strategy="sma_cross", theme="light", ) assert isinstance(result, dict), ( f"{tool_name} should return dict" ) # Validate chart data for chart_name, chart_data in result.items(): assert isinstance(chart_data, str), ( f"Chart {chart_name} should be string" ) assert len(chart_data) > 0, ( f"Chart {chart_name} should have data" ) viz_results[tool_name] = { "success": True, "charts_generated": list(result.keys()), "chart_count": len(result), } logger.info( f"✓ {tool_name} generated {len(result)} charts successfully" ) except Exception as e: viz_results[tool_name] = {"success": False, "error": str(e)} logger.error(f"✗ {tool_name} failed: {e}") else: viz_results[tool_name] = {"success": False, "error": "Tool not found"} return viz_results async def test_claude_desktop_simulation(self, setup_tools, mock_context): """Simulate realistic Claude Desktop usage patterns.""" mcp = setup_tools # Simulate a typical Claude Desktop session session_commands = [ { "command": "List available strategies", "tool": "list_strategies", "params": {}, }, { "command": "Run backtest for AAPL with SMA crossover", "tool": "run_backtest", "params": { "symbol": "AAPL", "strategy": "sma_cross", "fast_period": "10", "slow_period": "20", }, }, { "command": "Compare multiple strategies", "tool": "compare_strategies", "params": { "symbol": "AAPL", "strategies": ["sma_cross", "rsi", "macd"], }, }, { "command": "Generate charts for backtest", "tool": "generate_backtest_charts", "params": { "symbol": "AAPL", "strategy": "sma_cross", }, }, ] session_results = [] # Mock all necessary dependencies for simulation with patch("maverick_mcp.backtesting.VectorBTEngine") as mock_engine_class: mock_engine = Mock() mock_engine_class.return_value = mock_engine # Mock results for different tools mock_backtest_result = { "symbol": "AAPL", "strategy_type": "sma_cross", "metrics": {"total_return": 0.15, "sharpe_ratio": 1.2}, "trades": [], "equity_curve": [10000, 10150], "drawdown_series": [0, -0.02], } mock_engine.run_backtest.return_value = mock_backtest_result # Mock comparison results with patch( "maverick_mcp.backtesting.BacktestAnalyzer" ) as mock_analyzer_class: mock_analyzer = Mock() mock_analyzer_class.return_value = mock_analyzer mock_comparison = { "strategy_rankings": [ {"strategy": "sma_cross", "rank": 1, "sharpe_ratio": 1.2}, {"strategy": "rsi", "rank": 2, "sharpe_ratio": 1.1}, {"strategy": "macd", "rank": 3, "sharpe_ratio": 0.9}, ], "best_strategy": "sma_cross", } mock_analyzer.compare_strategies.return_value = mock_comparison # Mock visualization with patch( "maverick_mcp.backtesting.visualization.generate_equity_curve" ) as mock_viz: mock_viz.return_value = "mock_chart_data" # Execute session commands for command_info in session_commands: try: start_time = asyncio.get_event_loop().time() tool_func = mcp.tool_functions[command_info["tool"]] result = await tool_func( mock_context, **command_info["params"] ) execution_time = ( asyncio.get_event_loop().time() - start_time ) session_results.append( { "command": command_info["command"], "tool": command_info["tool"], "success": True, "execution_time": execution_time, "result_type": type(result).__name__, } ) logger.info( f"✓ '{command_info['command']}' completed in {execution_time:.3f}s" ) except Exception as e: session_results.append( { "command": command_info["command"], "tool": command_info["tool"], "success": False, "error": str(e), } ) logger.error(f"✗ '{command_info['command']}' failed: {e}") # Analyze session results total_commands = len(session_commands) successful_commands = sum(1 for r in session_results if r.get("success", False)) success_rate = successful_commands / total_commands avg_execution_time = np.mean( [r.get("execution_time", 0) for r in session_results if r.get("success")] ) assert success_rate >= 0.75, f"Session success rate too low: {success_rate:.1%}" assert avg_execution_time < 5.0, ( f"Average execution time too high: {avg_execution_time:.3f}s" ) logger.info( f"Claude Desktop Simulation Results:\n" f" • Commands Executed: {total_commands}\n" f" • Successful: {successful_commands}\n" f" • Success Rate: {success_rate:.1%}\n" f" • Avg Execution Time: {avg_execution_time:.3f}s" ) return { "session_results": session_results, "success_rate": success_rate, "avg_execution_time": avg_execution_time, } async def test_tool_parameter_validation_comprehensive( self, setup_tools, mock_context ): """Test comprehensive parameter validation across all tools.""" mcp = setup_tools validation_tests = [] # Test parameter validation for key tools test_cases = [ { "tool": "run_backtest", "valid_params": {"symbol": "AAPL", "strategy": "sma_cross"}, "invalid_params": [ {"symbol": "", "strategy": "sma_cross"}, # Empty symbol {"symbol": "AAPL", "strategy": ""}, # Empty strategy { "symbol": "AAPL", "strategy": "sma_cross", "fast_period": "not_a_number", }, # Invalid number ], }, { "tool": "optimize_strategy", "valid_params": {"symbol": "AAPL", "strategy": "sma_cross"}, "invalid_params": [ { "symbol": "AAPL", "strategy": "invalid_strategy", }, # Invalid strategy { "symbol": "AAPL", "strategy": "sma_cross", "top_n": -1, }, # Negative top_n ], }, ] for test_case in test_cases: tool_name = test_case["tool"] if tool_name in mcp.tool_functions: tool_func = mcp.tool_functions[tool_name] # Test valid parameters try: with patch("maverick_mcp.backtesting.VectorBTEngine"): await tool_func(mock_context, **test_case["valid_params"]) validation_tests.append( { "tool": tool_name, "test": "valid_params", "success": True, } ) except Exception as e: validation_tests.append( { "tool": tool_name, "test": "valid_params", "success": False, "error": str(e), } ) # Test invalid parameters for invalid_params in test_case["invalid_params"]: try: with patch("maverick_mcp.backtesting.VectorBTEngine"): await tool_func(mock_context, **invalid_params) # If we got here, validation didn't catch the error validation_tests.append( { "tool": tool_name, "test": f"invalid_params_{invalid_params}", "success": False, "error": "Validation should have failed but didn't", } ) except Exception as e: # Expected to fail validation_tests.append( { "tool": tool_name, "test": f"invalid_params_{invalid_params}", "success": True, "expected_error": str(e), } ) # Calculate validation success rate total_validation_tests = len(validation_tests) successful_validations = sum( 1 for t in validation_tests if t.get("success", False) ) validation_success_rate = ( successful_validations / total_validation_tests if total_validation_tests > 0 else 0 ) logger.info( f"Parameter Validation Results:\n" f" • Total Validation Tests: {total_validation_tests}\n" f" • Successful Validations: {successful_validations}\n" f" • Validation Success Rate: {validation_success_rate:.1%}" ) return { "validation_tests": validation_tests, "validation_success_rate": validation_success_rate, } if __name__ == "__main__": # Run MCP tools integration tests pytest.main( [ __file__, "-v", "--tb=short", "--asyncio-mode=auto", "--timeout=300", # 5 minute timeout "--durations=10", ] )

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/wshobson/maverick-mcp'

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