|
| 1 | +""" |
| 2 | +Manual validation test script for MCP Design Pattern tools. |
| 3 | +
|
| 4 | +This script tests all three design patterns through the actual MCP server: |
| 5 | +1. Progressive Tool Discovery |
| 6 | +2. Context Switching |
| 7 | +3. Tool Unloading (Workflow Stage Progression) |
| 8 | +
|
| 9 | +Run this script to validate that all pattern workflows execute correctly. |
| 10 | +""" |
| 11 | + |
| 12 | +import asyncio |
| 13 | +import json |
| 14 | +import os |
| 15 | +import sys |
| 16 | +from pathlib import Path |
| 17 | + |
| 18 | +# Add parent directory to path to import mcp_arangodb_async |
| 19 | +sys.path.insert(0, str(Path(__file__).parent)) |
| 20 | + |
| 21 | +from mcp_arangodb_async.entry import server |
| 22 | +from mcp_arangodb_async.config import load_config |
| 23 | +from mcp_arangodb_async.db import get_client_and_db |
| 24 | +from unittest.mock import Mock, patch |
| 25 | + |
| 26 | + |
| 27 | +async def setup_server(): |
| 28 | + """Initialize the MCP server with database connection.""" |
| 29 | + print("=" * 80) |
| 30 | + print("Setting up MCP server connection...") |
| 31 | + print("=" * 80) |
| 32 | + |
| 33 | + # Load configuration |
| 34 | + config = load_config() |
| 35 | + print(f"✓ Configuration loaded: {config.arango_url}, database: {config.database}") |
| 36 | + |
| 37 | + # Connect to database |
| 38 | + client, db = get_client_and_db(config) |
| 39 | + print(f"✓ Connected to ArangoDB at {config.arango_url}") |
| 40 | + |
| 41 | + # Create mock context with database |
| 42 | + mock_ctx = Mock() |
| 43 | + mock_ctx.lifespan_context = {"db": db, "client": client} |
| 44 | + |
| 45 | + return mock_ctx, db, client |
| 46 | + |
| 47 | + |
| 48 | +async def test_pattern_1_progressive_tool_discovery(mock_ctx): |
| 49 | + """Test Pattern 1: Progressive Tool Discovery.""" |
| 50 | + print("\n" + "=" * 80) |
| 51 | + print("PATTERN 1: PROGRESSIVE TOOL DISCOVERY") |
| 52 | + print("=" * 80) |
| 53 | + |
| 54 | + # Test 1.1: Search tools by keywords |
| 55 | + print("\n[Test 1.1] Search tools by keywords 'graph'...") |
| 56 | + with patch.object(server, 'request_context', mock_ctx): |
| 57 | + result = await server._handlers["call_tool"]( |
| 58 | + "arango_search_tools", |
| 59 | + {"keywords": ["graph"], "detail_level": "summary"} |
| 60 | + ) |
| 61 | + response = json.loads(result[0].text) |
| 62 | + print(f"✓ Found {len(response.get('matches', []))} tools matching 'graph'") |
| 63 | + print(f" Sample tools: {[t['name'] for t in response.get('matches', [])[:3]]}") |
| 64 | + |
| 65 | + # Test 1.2: Search with category filter |
| 66 | + print("\n[Test 1.2] Search tools with category filter 'graph_basic'...") |
| 67 | + with patch.object(server, 'request_context', mock_ctx): |
| 68 | + result = await server._handlers["call_tool"]( |
| 69 | + "arango_search_tools", |
| 70 | + {"keywords": ["traverse"], "categories": ["graph_basic"], "detail_level": "name"} |
| 71 | + ) |
| 72 | + response = json.loads(result[0].text) |
| 73 | + print(f"✓ Found {len(response.get('matches', []))} tools in 'graph_basic' category") |
| 74 | + |
| 75 | + # Test 1.3: List all tools by category |
| 76 | + print("\n[Test 1.3] List all tools in 'core_data' category...") |
| 77 | + with patch.object(server, 'request_context', mock_ctx): |
| 78 | + result = await server._handlers["call_tool"]( |
| 79 | + "arango_list_tools_by_category", |
| 80 | + {"category": "core_data"} |
| 81 | + ) |
| 82 | + response = json.loads(result[0].text) |
| 83 | + print(f"✓ Category 'core_data' contains {len(response.get('tools', []))} tools") |
| 84 | + print(f" Tools: {response.get('tools', [])[:5]}") |
| 85 | + |
| 86 | + # Test 1.4: List all categories |
| 87 | + print("\n[Test 1.4] List all tool categories...") |
| 88 | + with patch.object(server, 'request_context', mock_ctx): |
| 89 | + result = await server._handlers["call_tool"]( |
| 90 | + "arango_list_tools_by_category", |
| 91 | + {} |
| 92 | + ) |
| 93 | + response = json.loads(result[0].text) |
| 94 | + categories = response.get('categories', {}) |
| 95 | + print(f"✓ Found {len(categories)} categories") |
| 96 | + for cat_name, cat_info in list(categories.items())[:3]: |
| 97 | + print(f" - {cat_name}: {cat_info.get('description', 'N/A')[:50]}...") |
| 98 | + |
| 99 | + # Test 1.5: Test full detail level |
| 100 | + print("\n[Test 1.5] Search with 'full' detail level...") |
| 101 | + with patch.object(server, 'request_context', mock_ctx): |
| 102 | + result = await server._handlers["call_tool"]( |
| 103 | + "arango_search_tools", |
| 104 | + {"keywords": ["query"], "detail_level": "full"} |
| 105 | + ) |
| 106 | + response = json.loads(result[0].text) |
| 107 | + if response.get('matches'): |
| 108 | + first_tool = response['matches'][0] |
| 109 | + has_schema = 'inputSchema' in first_tool |
| 110 | + print(f"✓ Full detail includes schema: {has_schema}") |
| 111 | + |
| 112 | + print("\n✅ Pattern 1: Progressive Tool Discovery - ALL TESTS PASSED") |
| 113 | + |
| 114 | + |
| 115 | +async def test_pattern_2_context_switching(mock_ctx): |
| 116 | + """Test Pattern 2: Context Switching.""" |
| 117 | + print("\n" + "=" * 80) |
| 118 | + print("PATTERN 2: CONTEXT SWITCHING") |
| 119 | + print("=" * 80) |
| 120 | + |
| 121 | + # Test 2.1: List all contexts |
| 122 | + print("\n[Test 2.1] List all available contexts...") |
| 123 | + with patch.object(server, 'request_context', mock_ctx): |
| 124 | + result = await server._handlers["call_tool"]( |
| 125 | + "arango_list_contexts", |
| 126 | + {"include_tools": False} |
| 127 | + ) |
| 128 | + response = json.loads(result[0].text) |
| 129 | + contexts = response.get('contexts', {}) |
| 130 | + print(f"✓ Found {len(contexts)} contexts") |
| 131 | + for ctx_name in contexts.keys(): |
| 132 | + print(f" - {ctx_name}") |
| 133 | + |
| 134 | + # Test 2.2: Get initial active context |
| 135 | + print("\n[Test 2.2] Get initial active context...") |
| 136 | + with patch.object(server, 'request_context', mock_ctx): |
| 137 | + result = await server._handlers["call_tool"]( |
| 138 | + "arango_get_active_context", |
| 139 | + {} |
| 140 | + ) |
| 141 | + response = json.loads(result[0].text) |
| 142 | + initial_context = response.get('active_context') |
| 143 | + print(f"✓ Initial context: {initial_context}") |
| 144 | + print(f" Tool count: {response.get('tool_count', 0)}") |
| 145 | + |
| 146 | + # Test 2.3: Switch to graph_modeling context |
| 147 | + print("\n[Test 2.3] Switch to 'graph_modeling' context...") |
| 148 | + with patch.object(server, 'request_context', mock_ctx): |
| 149 | + result = await server._handlers["call_tool"]( |
| 150 | + "arango_switch_context", |
| 151 | + {"context": "graph_modeling"} |
| 152 | + ) |
| 153 | + response = json.loads(result[0].text) |
| 154 | + print(f"✓ Switched to: {response.get('to_context')}") |
| 155 | + print(f" Previous context: {response.get('from_context')}") |
| 156 | + print(f" Active tools: {response.get('total_tools', 0)}") |
| 157 | + |
| 158 | + # Test 2.4: Verify context changed |
| 159 | + print("\n[Test 2.4] Verify context changed...") |
| 160 | + with patch.object(server, 'request_context', mock_ctx): |
| 161 | + result = await server._handlers["call_tool"]( |
| 162 | + "arango_get_active_context", |
| 163 | + {} |
| 164 | + ) |
| 165 | + response = json.loads(result[0].text) |
| 166 | + current_context = response.get('active_context') |
| 167 | + assert current_context == "graph_modeling", f"Expected 'graph_modeling', got '{current_context}'" |
| 168 | + print(f"✓ Context verified: {current_context}") |
| 169 | + |
| 170 | + # Test 2.5: Switch to data_analysis context |
| 171 | + print("\n[Test 2.5] Switch to 'data_analysis' context...") |
| 172 | + with patch.object(server, 'request_context', mock_ctx): |
| 173 | + result = await server._handlers["call_tool"]( |
| 174 | + "arango_switch_context", |
| 175 | + {"context": "data_analysis"} |
| 176 | + ) |
| 177 | + response = json.loads(result[0].text) |
| 178 | + print(f"✓ Switched to: {response.get('to_context')}") |
| 179 | + |
| 180 | + # Test 2.6: Test invalid context name |
| 181 | + print("\n[Test 2.6] Test invalid context name (should fail gracefully)...") |
| 182 | + with patch.object(server, 'request_context', mock_ctx): |
| 183 | + result = await server._handlers["call_tool"]( |
| 184 | + "arango_switch_context", |
| 185 | + {"context": "invalid_context_name"} |
| 186 | + ) |
| 187 | + response = json.loads(result[0].text) |
| 188 | + has_error = 'error' in response or 'success' in response and not response['success'] |
| 189 | + print(f"✓ Invalid context handled correctly: {has_error}") |
| 190 | + |
| 191 | + # Test 2.7: List contexts with tools |
| 192 | + print("\n[Test 2.7] List contexts with tool details...") |
| 193 | + with patch.object(server, 'request_context', mock_ctx): |
| 194 | + result = await server._handlers["call_tool"]( |
| 195 | + "arango_list_contexts", |
| 196 | + {"include_tools": True} |
| 197 | + ) |
| 198 | + response = json.loads(result[0].text) |
| 199 | + contexts = response.get('contexts', {}) |
| 200 | + baseline_tools = contexts.get('baseline', {}).get('tools', []) |
| 201 | + print(f"✓ Baseline context has {len(baseline_tools)} tools") |
| 202 | + |
| 203 | + # Test 2.8: Switch back to baseline |
| 204 | + print("\n[Test 2.8] Switch back to 'baseline' context...") |
| 205 | + with patch.object(server, 'request_context', mock_ctx): |
| 206 | + result = await server._handlers["call_tool"]( |
| 207 | + "arango_switch_context", |
| 208 | + {"context": "baseline"} |
| 209 | + ) |
| 210 | + response = json.loads(result[0].text) |
| 211 | + print(f"✓ Switched back to: {response.get('to_context')}") |
| 212 | + |
| 213 | + print("\n✅ Pattern 2: Context Switching - ALL TESTS PASSED") |
| 214 | + |
| 215 | + |
| 216 | +async def test_pattern_3_tool_unloading(mock_ctx): |
| 217 | + """Test Pattern 3: Tool Unloading (Workflow Stage Progression).""" |
| 218 | + print("\n" + "=" * 80) |
| 219 | + print("PATTERN 3: TOOL UNLOADING (WORKFLOW STAGE PROGRESSION)") |
| 220 | + print("=" * 80) |
| 221 | + |
| 222 | + # Test 3.1: Advance to setup stage |
| 223 | + print("\n[Test 3.1] Advance to 'setup' stage...") |
| 224 | + with patch.object(server, 'request_context', mock_ctx): |
| 225 | + result = await server._handlers["call_tool"]( |
| 226 | + "arango_advance_workflow_stage", |
| 227 | + {"stage": "setup"} |
| 228 | + ) |
| 229 | + response = json.loads(result[0].text) |
| 230 | + print(f"✓ Advanced to stage: {response.get('to_stage')}") |
| 231 | + print(f" Active tools: {response.get('total_active_tools', 0)}") |
| 232 | + |
| 233 | + # Test 3.2: Verify stage changed via tool usage stats |
| 234 | + print("\n[Test 3.2] Verify stage changed...") |
| 235 | + with patch.object(server, 'request_context', mock_ctx): |
| 236 | + result = await server._handlers["call_tool"]( |
| 237 | + "arango_get_tool_usage_stats", |
| 238 | + {} |
| 239 | + ) |
| 240 | + response = json.loads(result[0].text) |
| 241 | + print(f"✓ Current stage: {response.get('current_stage')}") |
| 242 | + print(f" Active stage tools: {len(response.get('active_stage_tools', []))}") |
| 243 | + |
| 244 | + # Test 3.3: Advance to data_loading stage |
| 245 | + print("\n[Test 3.3] Advance to 'data_loading' stage...") |
| 246 | + with patch.object(server, 'request_context', mock_ctx): |
| 247 | + result = await server._handlers["call_tool"]( |
| 248 | + "arango_advance_workflow_stage", |
| 249 | + {"stage": "data_loading"} |
| 250 | + ) |
| 251 | + response = json.loads(result[0].text) |
| 252 | + print(f"✓ Advanced to stage: {response.get('to_stage')}") |
| 253 | + |
| 254 | + # Test 3.4: Get tool usage stats |
| 255 | + print("\n[Test 3.4] Get tool usage statistics...") |
| 256 | + with patch.object(server, 'request_context', mock_ctx): |
| 257 | + result = await server._handlers["call_tool"]( |
| 258 | + "arango_get_tool_usage_stats", |
| 259 | + {} |
| 260 | + ) |
| 261 | + response = json.loads(result[0].text) |
| 262 | + stats = response.get('tool_usage', {}) |
| 263 | + total_uses = sum(tool.get('use_count', 0) for tool in stats.values()) |
| 264 | + print(f"✓ Total tool uses tracked: {total_uses}") |
| 265 | + print(f" Tools with usage data: {len(stats)}") |
| 266 | + |
| 267 | + # Test 3.5: Manually unload specific tools |
| 268 | + print("\n[Test 3.5] Manually unload specific tools...") |
| 269 | + with patch.object(server, 'request_context', mock_ctx): |
| 270 | + result = await server._handlers["call_tool"]( |
| 271 | + "arango_unload_tools", |
| 272 | + {"tool_names": ["arango_create_collection", "arango_backup"]} |
| 273 | + ) |
| 274 | + response = json.loads(result[0].text) |
| 275 | + unloaded = response.get('unloaded_tools', []) |
| 276 | + print(f"✓ Unloaded {len(unloaded)} tools: {unloaded}") |
| 277 | + |
| 278 | + # Test 3.6: Advance to analysis stage |
| 279 | + print("\n[Test 3.6] Advance to 'analysis' stage...") |
| 280 | + with patch.object(server, 'request_context', mock_ctx): |
| 281 | + result = await server._handlers["call_tool"]( |
| 282 | + "arango_advance_workflow_stage", |
| 283 | + {"stage": "analysis"} |
| 284 | + ) |
| 285 | + response = json.loads(result[0].text) |
| 286 | + print(f"✓ Advanced to stage: {response.get('to_stage')}") |
| 287 | + |
| 288 | + # Test 3.7: Advance to cleanup stage |
| 289 | + print("\n[Test 3.7] Advance to 'cleanup' stage...") |
| 290 | + with patch.object(server, 'request_context', mock_ctx): |
| 291 | + result = await server._handlers["call_tool"]( |
| 292 | + "arango_advance_workflow_stage", |
| 293 | + {"stage": "cleanup"} |
| 294 | + ) |
| 295 | + response = json.loads(result[0].text) |
| 296 | + print(f"✓ Advanced to stage: {response.get('to_stage')}") |
| 297 | + |
| 298 | + # Test 3.8: Test invalid stage name |
| 299 | + print("\n[Test 3.8] Test invalid stage name (should fail gracefully)...") |
| 300 | + with patch.object(server, 'request_context', mock_ctx): |
| 301 | + result = await server._handlers["call_tool"]( |
| 302 | + "arango_advance_workflow_stage", |
| 303 | + {"stage": "invalid_stage"} |
| 304 | + ) |
| 305 | + response = json.loads(result[0].text) |
| 306 | + has_error = 'error' in response or 'success' in response and not response['success'] |
| 307 | + print(f"✓ Invalid stage handled correctly: {has_error}") |
| 308 | + |
| 309 | + # Test 3.9: Unload non-existent tools |
| 310 | + print("\n[Test 3.9] Unload non-existent tools (should handle gracefully)...") |
| 311 | + with patch.object(server, 'request_context', mock_ctx): |
| 312 | + result = await server._handlers["call_tool"]( |
| 313 | + "arango_unload_tools", |
| 314 | + {"tool_names": ["nonexistent_tool_1", "nonexistent_tool_2"]} |
| 315 | + ) |
| 316 | + response = json.loads(result[0].text) |
| 317 | + not_found = response.get('not_found', []) |
| 318 | + print(f"✓ Not found tools: {not_found}") |
| 319 | + |
| 320 | + print("\n✅ Pattern 3: Tool Unloading - ALL TESTS PASSED") |
| 321 | + |
| 322 | + |
| 323 | +async def main(): |
| 324 | + """Main test execution.""" |
| 325 | + print("\n" + "=" * 80) |
| 326 | + print("MCP DESIGN PATTERNS - MANUAL VALIDATION TEST SUITE") |
| 327 | + print("=" * 80) |
| 328 | + print("\nThis script validates all three MCP Design Pattern workflows:") |
| 329 | + print("1. Progressive Tool Discovery") |
| 330 | + print("2. Context Switching") |
| 331 | + print("3. Tool Unloading (Workflow Stage Progression)") |
| 332 | + print("\n" + "=" * 80) |
| 333 | + |
| 334 | + try: |
| 335 | + # Setup server connection |
| 336 | + mock_ctx, db, client = await setup_server() |
| 337 | + |
| 338 | + # Run all pattern tests |
| 339 | + await test_pattern_1_progressive_tool_discovery(mock_ctx) |
| 340 | + await test_pattern_2_context_switching(mock_ctx) |
| 341 | + await test_pattern_3_tool_unloading(mock_ctx) |
| 342 | + |
| 343 | + # Final summary |
| 344 | + print("\n" + "=" * 80) |
| 345 | + print("✅ ALL PATTERN VALIDATION TESTS PASSED SUCCESSFULLY!") |
| 346 | + print("=" * 80) |
| 347 | + print("\nSummary:") |
| 348 | + print(" ✓ Pattern 1: Progressive Tool Discovery - 5 tests passed") |
| 349 | + print(" ✓ Pattern 2: Context Switching - 8 tests passed") |
| 350 | + print(" ✓ Pattern 3: Tool Unloading - 9 tests passed") |
| 351 | + print("\nTotal: 22 validation tests executed successfully") |
| 352 | + print("=" * 80) |
| 353 | + |
| 354 | + return 0 |
| 355 | + |
| 356 | + except Exception as e: |
| 357 | + print(f"\n❌ ERROR: {e}") |
| 358 | + import traceback |
| 359 | + traceback.print_exc() |
| 360 | + return 1 |
| 361 | + |
| 362 | + |
| 363 | +if __name__ == "__main__": |
| 364 | + exit_code = asyncio.run(main()) |
| 365 | + sys.exit(exit_code) |
| 366 | + |
0 commit comments