Skip to main content
Glama
06-testing-verification.md18.5 kB
# Implementation Step 6: Testing and Verification ## Overview This final step provides comprehensive testing strategies and verification procedures to ensure the MCP hub architecture works correctly end-to-end. It includes unit tests, integration tests, stress tests, and real-world usage scenarios. ## Goals 1. Verify all components work together correctly 2. Test error handling and edge cases 3. Ensure performance meets requirements 4. Validate with real MCP clients 5. Create automated test suite ## Test Categories ### 1. Unit Tests - Individual component testing - Mock dependencies - Fast, isolated tests ### 2. Integration Tests - Component interaction testing - Real network connections - File system operations ### 3. End-to-End Tests - Complete workflows - Real MCP clients - Multi-instance scenarios ### 4. Stress Tests - Performance under load - Resource leak detection - Concurrent operations ## Test Implementation ### 1. Test Infrastructure ```go // test/helpers/test_helpers.go package helpers import ( "context" "fmt" "io/ioutil" "net" "os" "path/filepath" "testing" "time" "github.com/standardbeagle/brummer/internal/discovery" "github.com/standardbeagle/brummer/internal/mcp" ) // TestInstance represents a test brummer instance type TestInstance struct { ID string Port int Path string Server *mcp.Server Registry *discovery.Registry Cancel context.CancelFunc } // StartTestInstance starts a test instance on a random port func StartTestInstance(t *testing.T, name string) *TestInstance { // Get free port listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } port := listener.Addr().(*net.TCPAddr).Port listener.Close() // Create temp directory tempDir := t.TempDir() // Create test instance instance := &TestInstance{ ID: fmt.Sprintf("test-%s-%d", name, time.Now().Unix()), Port: port, Path: tempDir, } // Start MCP server ctx, cancel := context.WithCancel(context.Background()) instance.Cancel = cancel go func() { server := mcp.NewServer() instance.Server = server // Start listening listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) if err != nil { t.Error(err) return } // Register with discovery registry := discovery.NewRegistry() instance.Registry = registry if err := registry.Register(tempDir, port, name, true); err != nil { t.Error(err) } // Serve server.Serve(listener) }() // Wait for server to start WaitForPort(t, port, 5*time.Second) return instance } // StopTestInstance stops a test instance func (ti *TestInstance) Stop() { ti.Cancel() if ti.Registry != nil { ti.Registry.Unregister() } } // WaitForPort waits for a port to be listening func WaitForPort(t *testing.T, port int, timeout time.Duration) { deadline := time.Now().Add(timeout) for time.Now().Before(deadline) { conn, err := net.Dial("tcp", fmt.Sprintf("localhost:%d", port)) if err == nil { conn.Close() return } time.Sleep(100 * time.Millisecond) } t.Fatalf("Port %d not listening after %s", port, timeout) } // TestHub represents a test hub type TestHub struct { Server *mcp.HubServer Port int Cancel context.CancelFunc } // StartTestHub starts a test hub func StartTestHub(t *testing.T) *TestHub { hub, err := mcp.NewHubServer() if err != nil { t.Fatal(err) } // Start hub ctx, cancel := context.WithCancel(context.Background()) go func() { if err := hub.Start(ctx); err != nil { t.Error(err) } }() return &TestHub{ Server: hub, Cancel: cancel, } } // MCPClient is a test MCP client type MCPClient struct { t *testing.T conn net.Conn } // NewMCPClient creates a test MCP client func NewMCPClient(t *testing.T) *MCPClient { // For stdio, we'd use exec.Command // For testing, we'll use direct connection return &MCPClient{t: t} } // Initialize sends initialize request func (c *MCPClient) Initialize() error { // Send initialize request // Parse response return nil } // CallTool calls an MCP tool func (c *MCPClient) CallTool(name string, args map[string]interface{}) (interface{}, error) { // Send tool call // Parse response return nil, nil } ``` ### 2. Component Tests ```go // internal/mcp/hub_server_test.go package mcp import ( "context" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestHubServerLifecycle(t *testing.T) { // Create hub hub, err := NewHubServer() require.NoError(t, err) // Start hub ctx, cancel := context.WithCancel(context.Background()) defer cancel() err = hub.Start(ctx) require.NoError(t, err) // Initialize result, err := hub.Initialize(ctx) require.NoError(t, err) assert.Equal(t, "brummer-hub", result.ServerInfo.Name) // List tools - should have hub tools tools, err := hub.ListTools(ctx) require.NoError(t, err) assert.True(t, len(tools.Tools) >= 3) // At least 3 hub tools // Shutdown cancel() // Verify clean shutdown } func TestInstanceDiscovery(t *testing.T) { // Start hub hub := helpers.StartTestHub(t) defer hub.Cancel() // Start instance instance := helpers.StartTestInstance(t, "test-project") defer instance.Stop() // Wait for discovery time.Sleep(500 * time.Millisecond) // List instances ctx := context.Background() result, err := hub.Server.CallTool(ctx, &mcp.CallToolRequest{ Name: "instances/list", }) require.NoError(t, err) // Verify instance appears // Parse result and check } func TestConnectionManagement(t *testing.T) { cm := NewConnectionManager() defer cm.Stop() // Test registration instance := &discovery.Instance{ ID: "test-123", Name: "test", Port: 7778, } err := cm.RegisterInstance(instance) assert.NoError(t, err) // Test state transitions instances := cm.ListInstances() assert.Len(t, instances, 1) assert.Equal(t, StateDiscovered, instances[0].State) // Test session mapping err = cm.ConnectSession("session-1", "test-123") assert.Error(t, err) // Should fail - not active // Update to active err = cm.UpdateState("test-123", StateActive) assert.NoError(t, err) // Now connect should work err = cm.ConnectSession("session-1", "test-123") assert.NoError(t, err) } ``` ### 3. Integration Tests ```go // test/integration/hub_integration_test.go package integration import ( "context" "encoding/json" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestCompleteWorkflow(t *testing.T) { // Start hub hub := helpers.StartTestHub(t) defer hub.Cancel() // Start two instances instance1 := helpers.StartTestInstance(t, "project1") defer instance1.Stop() instance2 := helpers.StartTestInstance(t, "project2") defer instance2.Stop() // Create MCP client client := helpers.NewMCPClient(t) // Initialize connection err := client.Initialize() require.NoError(t, err) // List instances result, err := client.CallTool("instances/list", nil) require.NoError(t, err) instances := parseInstances(result) assert.Len(t, instances, 2) // Connect to first instance err = client.CallTool("instances/connect", map[string]interface{}{ "instance_id": instance1.ID, }) require.NoError(t, err) // Call instance tool scripts, err := client.CallTool("scripts/list", nil) require.NoError(t, err) assert.NotNil(t, scripts) // Disconnect err = client.CallTool("instances/disconnect", nil) require.NoError(t, err) // Connect to second instance err = client.CallTool("instances/connect", map[string]interface{}{ "instance_id": instance2.ID, }) require.NoError(t, err) } func TestHealthMonitoring(t *testing.T) { hub := helpers.StartTestHub(t) defer hub.Cancel() instance := helpers.StartTestInstance(t, "health-test") defer instance.Stop() // Wait for connection time.Sleep(2 * time.Second) // Check health health, err := hub.Server.CallTool(context.Background(), &mcp.CallToolRequest{ Name: "instances/health", }) require.NoError(t, err) // Verify healthy // Parse and check missed_pings = 0 // Stop instance instance.Stop() // Wait for detection (20+ seconds) time.Sleep(25 * time.Second) // Check health again health, err = hub.Server.CallTool(context.Background(), &mcp.CallToolRequest{ Name: "instances/health", }) require.NoError(t, err) // Should show as retrying or dead } ``` ### 4. End-to-End Test Script ```bash #!/bin/bash # test/e2e/test_complete_flow.sh set -e echo "=== End-to-End MCP Hub Test ===" # Clean up any existing instances rm -rf ~/.local/share/brummer/instances/* # Start test projects echo "Starting test instances..." cd test/fixtures/project1 brum --no-tui & INSTANCE1_PID=$! cd ../project2 brum --no-tui & INSTANCE2_PID=$! sleep 2 # Start hub echo "Starting hub..." cd ../../.. brum --mcp > hub.log 2>&1 & HUB_PID=$! sleep 1 # Test with MCP inspector echo "Testing with MCP inspector..." npm install -g @modelcontextprotocol/inspector # Create test script for inspector cat > test_inspector.js << 'EOF' const { spawn } = require('child_process'); async function runTest() { // Connect to hub const hub = spawn('brum', ['--mcp']); // Send initialize hub.stdin.write(JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "1.0" } }) + '\n'); // Wait for response // ... parse and verify ... // List instances hub.stdin.write(JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/call", params: { name: "instances/list" } }) + '\n'); // ... continue test ... } runTest().catch(console.error); EOF node test_inspector.js # Test with Claude Desktop config echo "Testing Claude Desktop configuration..." cat > claude_desktop_test.json << EOF { "mcpServers": { "brummer-hub": { "command": "brum", "args": ["--mcp"] } } } EOF # Verify configuration works # This would require actual Claude Desktop # Cleanup echo "Cleaning up..." kill $INSTANCE1_PID $INSTANCE2_PID $HUB_PID echo "=== Test Complete ===" ``` ### 5. Stress Tests ```go // test/stress/stress_test.go package stress import ( "context" "sync" "testing" "time" ) func TestManyInstances(t *testing.T) { if testing.Short() { t.Skip("Skipping stress test") } hub := helpers.StartTestHub(t) defer hub.Cancel() // Start 50 instances var instances []*helpers.TestInstance for i := 0; i < 50; i++ { instance := helpers.StartTestInstance(t, fmt.Sprintf("stress-%d", i)) instances = append(instances, instance) defer instance.Stop() } // Wait for discovery time.Sleep(5 * time.Second) // List all instances result, err := hub.Server.CallTool(context.Background(), &mcp.CallToolRequest{ Name: "instances/list", }) require.NoError(t, err) // Should see all 50 // Connect to random instances concurrently var wg sync.WaitGroup for i := 0; i < 20; i++ { wg.Add(1) go func(sessionID int) { defer wg.Done() // Connect to random instance instanceID := instances[sessionID%len(instances)].ID ctx := WithSessionID(context.Background(), fmt.Sprintf("session-%d", sessionID)) err := hub.Server.CallTool(ctx, &mcp.CallToolRequest{ Name: "instances/connect", Arguments: map[string]interface{}{ "instance_id": instanceID, }, }) assert.NoError(t, err) // Call some tools for j := 0; j < 10; j++ { _, err = hub.Server.CallTool(ctx, &mcp.CallToolRequest{ Name: "scripts/list", }) assert.NoError(t, err) } }(i) } wg.Wait() // Check for goroutine leaks initialGoroutines := runtime.NumGoroutine() time.Sleep(1 * time.Second) finalGoroutines := runtime.NumGoroutine() assert.InDelta(t, initialGoroutines, finalGoroutines, 10) } func TestRapidConnectionChurn(t *testing.T) { // Test instances appearing and disappearing rapidly // Verify hub handles it gracefully } ``` ### 6. Performance Benchmarks ```go // test/bench/benchmark_test.go package bench import ( "context" "testing" ) func BenchmarkInstanceList(b *testing.B) { hub := helpers.StartTestHub(b) defer hub.Cancel() // Add 10 instances for i := 0; i < 10; i++ { instance := helpers.StartTestInstance(b, fmt.Sprintf("bench-%d", i)) defer instance.Stop() } ctx := context.Background() b.ResetTimer() for i := 0; i < b.N; i++ { _, err := hub.Server.CallTool(ctx, &mcp.CallToolRequest{ Name: "instances/list", }) if err != nil { b.Fatal(err) } } } func BenchmarkToolProxy(b *testing.B) { // Benchmark tool call throughput } ``` ## Test Scenarios ### 1. Basic Functionality - [ ] Hub starts with no instances - [ ] Instance discovery works - [ ] Connection establishment - [ ] Tool proxying - [ ] Clean disconnect ### 2. Error Handling - [ ] Instance dies during operation - [ ] Network failures - [ ] Invalid tool calls - [ ] Malformed requests - [ ] Resource exhaustion ### 3. Edge Cases - [ ] Instance restart on same port - [ ] Port conflicts - [ ] File permission issues - [ ] Disk full scenarios - [ ] Clock skew ### 4. Performance - [ ] Startup time < 100ms - [ ] Discovery time < 50ms - [ ] Tool response < 200ms overhead - [ ] Handle 100+ instances - [ ] No memory leaks ### 5. Compatibility - [ ] Works with Claude Desktop - [ ] Works with VSCode MCP - [ ] Works with MCP inspector - [ ] Cross-platform (Windows, Mac, Linux) ## CI/CD Integration ```yaml # .github/workflows/test.yml name: Tests on: [push, pull_request] jobs: unit-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-go@v4 with: go-version: '1.21' - name: Run unit tests run: | go test -v -race ./internal/... go test -v -race ./pkg/... integration-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-go@v4 - name: Build run: make build - name: Run integration tests run: | go test -v ./test/integration/... e2e-tests: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3 - uses: actions/setup-go@v4 - name: Run E2E tests run: | make test-e2e ``` ## Monitoring and Debugging ### 1. Debug Logging ```go // Enable debug logging os.Setenv("BRUMMER_MCP_DEBUG", "true") // Add structured logging log.WithFields(log.Fields{ "instance_id": instanceID, "state": state, "action": "state_change", }).Debug("Instance state changed") ``` ### 2. Metrics Collection ```go // Add metrics for monitoring var ( instancesTotal = promauto.NewGauge(prometheus.GaugeOpts{ Name: "brummer_hub_instances_total", Help: "Total number of discovered instances", }) connectionsActive = promauto.NewGauge(prometheus.GaugeOpts{ Name: "brummer_hub_connections_active", Help: "Number of active connections", }) toolCallsTotal = promauto.NewCounter(prometheus.CounterOpts{ Name: "brummer_hub_tool_calls_total", Help: "Total number of tool calls", }) ) ``` ### 3. Debug Tools ```bash #!/bin/bash # debug/debug_hub.sh # Start hub with verbose logging BRUMMER_MCP_DEBUG=true brum --mcp 2>&1 | tee hub_debug.log & # Monitor instance files watch -n 1 'ls -la ~/.local/share/brummer/instances/' # Test with netcat echo '{"jsonrpc":"2.0","id":1,"method":"instances/list"}' | nc -q 1 localhost 7777 # Check goroutines go tool pprof http://localhost:6060/debug/pprof/goroutine ``` ## Success Criteria 1. ✅ All unit tests pass 2. ✅ Integration tests complete successfully 3. ✅ E2E tests work with real MCP clients 4. ✅ Stress tests show no leaks 5. ✅ Performance benchmarks meet targets 6. ✅ Works on all platforms 7. ✅ Documentation complete 8. ✅ CI/CD pipeline green ## Next Steps After verification: 1. Deploy to beta users 2. Gather feedback 3. Fix any issues found 4. Create release notes 5. Tag stable release ## Troubleshooting Guide ### Common Issues 1. **Hub doesn't see instances** - Check file permissions in ~/.local/share/brummer - Verify instances are writing files - Check debug logs for errors 2. **Connection failures** - Verify firewall allows localhost connections - Check instance MCP server is running - Look for port conflicts 3. **Tool calls fail** - Ensure session is connected to instance - Check instance has the tool - Verify request format 4. **Performance issues** - Check number of instances - Monitor goroutine count - Look for blocking operations ## Summary This comprehensive testing plan ensures the MCP hub architecture is: - Functionally correct - Performant and scalable - Reliable and robust - Compatible with MCP ecosystem - Ready for production use

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/standardbeagle/brummer'

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