diff --git a/test/integration/auth_config_test.go b/test/integration/auth_config_test.go index 05673ab2..78447a0f 100644 --- a/test/integration/auth_config_test.go +++ b/test/integration/auth_config_test.go @@ -28,14 +28,18 @@ func TestOutputConfigWithAuthHeaders(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() + // Use an in-process mock backend to avoid Docker dependency + mockBackend := createMinimalMockMCPBackend(t) + defer mockBackend.Close() + // Prepare config JSON for stdin with API key apiKey := "test-secret-key-12345" port := 13010 configJSON := map[string]interface{}{ "mcpServers": map[string]interface{}{ "echoserver": map[string]interface{}{ - "type": "local", - "container": "echo", + "type": "http", + "url": mockBackend.URL + "/mcp", }, }, "gateway": map[string]interface{}{ @@ -232,18 +236,24 @@ func TestOutputConfigUnifiedMode(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() + // Use in-process mock backends to avoid Docker dependency + mockBackend1 := createMinimalMockMCPBackend(t) + defer mockBackend1.Close() + mockBackend2 := createMinimalMockMCPBackend(t) + defer mockBackend2.Close() + // Prepare config JSON for stdin with API key apiKey := "unified-test-key" port := 13011 configJSON := map[string]interface{}{ "mcpServers": map[string]interface{}{ "server1": map[string]interface{}{ - "type": "local", - "container": "echo", + "type": "http", + "url": mockBackend1.URL + "/mcp", }, "server2": map[string]interface{}{ - "type": "local", - "container": "echo", + "type": "http", + "url": mockBackend2.URL + "/mcp", }, }, "gateway": map[string]interface{}{ diff --git a/test/integration/binary_test.go b/test/integration/binary_test.go index d5163c4f..48586642 100644 --- a/test/integration/binary_test.go +++ b/test/integration/binary_test.go @@ -7,14 +7,16 @@ import ( "fmt" "io" "net/http" + "net/http/httptest" "os" "os/exec" "path/filepath" "strings" "testing" + "time" + sdk "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/stretchr/testify/require" - "time" ) // TestBinaryInvocation_RoutedMode tests the awmg binary in routed mode @@ -220,12 +222,16 @@ func TestBinaryInvocation_ConfigStdin(t *testing.T) { "--routed", ) + // Use an in-process mock backend to avoid Docker dependency + mockBackend := createMinimalMockMCPBackend(t) + defer mockBackend.Close() + // Prepare config JSON for stdin configJSON := map[string]interface{}{ "mcpServers": map[string]interface{}{ "testserver": map[string]interface{}{ - "type": "local", - "container": "echo", + "type": "http", + "url": mockBackend.URL + "/mcp", }, }, "gateway": map[string]interface{}{ @@ -385,10 +391,14 @@ func TestBinaryInvocation_PipeInputOutput(t *testing.T) { t.Skip("Skipping binary integration test in short mode") } + // Start a mock HTTP MCP backend so the gateway can connect without Docker + mockBackend := createMinimalMockMCPBackend(t) + defer mockBackend.Close() + binaryPath := findBinary(t) t.Logf("Using binary: %s", binaryPath) - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() port := "13005" @@ -398,12 +408,12 @@ func TestBinaryInvocation_PipeInputOutput(t *testing.T) { "--unified", ) - // Prepare config JSON for stdin pipe + // Prepare config JSON for stdin pipe using the mock HTTP backend configJSON := map[string]interface{}{ "mcpServers": map[string]interface{}{ "pipetest": map[string]interface{}{ - "type": "local", - "container": "echo", + "type": "http", + "url": mockBackend.URL + "/mcp", }, }, "gateway": map[string]interface{}{ @@ -433,7 +443,7 @@ func TestBinaryInvocation_PipeInputOutput(t *testing.T) { // Wait for server to start serverURL := "http://127.0.0.1:" + port - if !waitForServer(t, serverURL+"/health", 5*time.Second) { + if !waitForServer(t, serverURL+"/health", 15*time.Second) { t.Logf("STDOUT: %s", stdout.String()) t.Logf("STDERR: %s", stderr.String()) t.Fatal("Server did not start in time") @@ -538,6 +548,33 @@ func TestBinaryInvocation_Version(t *testing.T) { // Helper functions +// createMinimalMockMCPBackend creates a minimal MCP HTTP server suitable for use as a +// gateway backend in tests that don't need a real Docker container. It responds correctly +// to initialize and tools/list so the gateway can register it and start the HTTP server. +func createMinimalMockMCPBackend(t *testing.T) *httptest.Server { + t.Helper() + impl := &sdk.Implementation{Name: "mock-backend", Version: "1.0.0"} + mcpServer := sdk.NewServer(impl, nil) + mcpServer.AddTool(&sdk.Tool{ + Name: "mock_tool", + Description: "A mock tool for testing", + InputSchema: map[string]interface{}{"type": "object"}, + }, func(_ context.Context, _ *sdk.CallToolRequest) (*sdk.CallToolResult, error) { + return &sdk.CallToolResult{ + Content: []sdk.Content{&sdk.TextContent{Text: "mock response"}}, + }, nil + }) + handler := sdk.NewStreamableHTTPHandler(func(_ *http.Request) *sdk.Server { + return mcpServer + }, &sdk.StreamableHTTPOptions{ + Stateless: false, + }) + mux := http.NewServeMux() + mux.Handle("/mcp", handler) + mux.Handle("/mcp/", handler) + return httptest.NewServer(mux) +} + // findBinary locates the awmg binary func findBinary(t *testing.T) string { t.Helper() diff --git a/test/integration/github_test.go b/test/integration/github_test.go index 508cde3d..30d35123 100644 --- a/test/integration/github_test.go +++ b/test/integration/github_test.go @@ -345,6 +345,7 @@ func TestGitHubMCPRealBackend(t *testing.T) { // Test 2: Initialize connection var sessionID string + var mcpSessionID string // Mcp-Session-Id from SDK, needed for stateful session reuse t.Run("InitializeConnection", func(t *testing.T) { initReq := map[string]interface{}{ "jsonrpc": "2.0", @@ -374,6 +375,11 @@ func TestGitHubMCPRealBackend(t *testing.T) { body, _ := io.ReadAll(resp.Body) t.Logf("Initialize response: %s", string(body)) + // Capture Mcp-Session-Id for stateful session reuse in subsequent requests + mcpSessionID = resp.Header.Get("Mcp-Session-Id") + t.Logf("Captured Mcp-Session-Id: %q", mcpSessionID) + require.NotEmpty(t, mcpSessionID, "Mcp-Session-Id header must be present for stateful session reuse; ensure the gateway/SDK returns this header on initialize") + require.Equal(t, http.StatusOK, resp.StatusCode, "Initialize failed with status %d: %s", resp.StatusCode, string(body)) // Check if response uses SSE-formatted streaming @@ -406,6 +412,9 @@ func TestGitHubMCPRealBackend(t *testing.T) { req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json, text/event-stream") req.Header.Set("Authorization", "test-github-key") + if mcpSessionID != "" { + req.Header.Set("Mcp-Session-Id", mcpSessionID) + } resp, err = client.Do(req) if err == nil { resp.Body.Close() @@ -434,6 +443,9 @@ func TestGitHubMCPRealBackend(t *testing.T) { req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json, text/event-stream") req.Header.Set("Authorization", sessionID) + if mcpSessionID != "" { + req.Header.Set("Mcp-Session-Id", mcpSessionID) + } client := &http.Client{Timeout: 30 * time.Second} resp, err := client.Do(req) @@ -442,11 +454,19 @@ func TestGitHubMCPRealBackend(t *testing.T) { body, _ := io.ReadAll(resp.Body) t.Logf("Tools list response length: %d bytes", len(body)) + bodyStr := string(body) + const maxLoggedBodyLen = 1024 + if len(bodyStr) > maxLoggedBodyLen { + t.Logf("Tools list response body (truncated to %d bytes): %q", maxLoggedBodyLen, bodyStr[:maxLoggedBodyLen]) + } else { + t.Logf("Tools list response body: %q", bodyStr) + } - require.Equal(t, http.StatusOK, resp.StatusCode, "Tools list failed with status %d: %s", resp.StatusCode, string(body)) + require.Equal(t, http.StatusOK, resp.StatusCode, "Tools list failed with status %d: %s", resp.StatusCode, bodyStr) // Check if response uses SSE-formatted streaming contentType := resp.Header.Get("Content-Type") + t.Logf("Tools list content-type: %q", contentType) var result map[string]interface{} if contentType == "text/event-stream" { // Parse SSE-formatted response @@ -583,6 +603,9 @@ func TestGitHubMCPRealBackend(t *testing.T) { req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json, text/event-stream") req.Header.Set("Authorization", sessionID) + if mcpSessionID != "" { + req.Header.Set("Mcp-Session-Id", mcpSessionID) + } client := &http.Client{Timeout: 30 * time.Second} resp, err := client.Do(req) diff --git a/test/integration/pipe_launch_test.go b/test/integration/pipe_launch_test.go index 5ec06a52..224688d7 100644 --- a/test/integration/pipe_launch_test.go +++ b/test/integration/pipe_launch_test.go @@ -12,9 +12,9 @@ import ( "strconv" "strings" "testing" + "time" "github.com/stretchr/testify/require" - "time" ) // TestPipeBasedLaunch tests launching the gateway using pipes via shell script. @@ -45,6 +45,12 @@ func TestPipeBasedLaunch(t *testing.T) { t.Skip("Skipping pipe-based launch integration test in short mode") } + // Start a mock HTTP MCP backend so gateways don't need Docker to register tools. + // This is shared across all subtests; each subtest connects to it via HTTP. + mockBackend := createMinimalMockMCPBackend(t) + defer mockBackend.Close() + t.Logf("Mock MCP backend started at %s", mockBackend.URL) + // Find the binary binaryPath := findBinary(t) t.Logf("Using binary: %s", binaryPath) @@ -99,6 +105,7 @@ func TestPipeBasedLaunch(t *testing.T) { "PIPE_TYPE="+tt.pipeType, "TIMEOUT=30", "NO_CLEANUP=1", // Don't cleanup gateway so tests can interact with it + "MOCK_BACKEND_URL="+mockBackend.URL+"/mcp", // Use mock HTTP backend instead of Docker container ) // Create context with timeout diff --git a/test/integration/playwright_test.go b/test/integration/playwright_test.go index 04586379..a1976ff7 100644 --- a/test/integration/playwright_test.go +++ b/test/integration/playwright_test.go @@ -411,7 +411,7 @@ CMD ["node", "mock-mcp-server.js"] }, }, "gateway": map[string]interface{}{ - "port": 13101, + "port": 13109, "domain": "localhost", "apiKey": "test-mock-key", }, @@ -424,7 +424,7 @@ CMD ["node", "mock-mcp-server.js"] ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() - port := "13101" + port := "13109" // Kill any stale processes on this port from previous test runs killProcessOnPort(t, port) @@ -432,6 +432,7 @@ CMD ["node", "mock-mcp-server.js"] cmd := exec.CommandContext(ctx, binaryPath, "--config-stdin", "--listen", "127.0.0.1:"+port, + "--unified", ) cmd.Stdin = bytes.NewReader(configJSON) @@ -487,7 +488,7 @@ CMD ["node", "mock-mcp-server.js"] }, } - result := sendMCPRequest(t, serverURL+"/mcp/mock-playwright", "test-mock-key", initReq) + result := sendMCPRequest(t, serverURL+"/mcp", "test-mock-key", initReq) // Verify initialize succeeded if _, ok := result["error"]; ok { @@ -502,7 +503,7 @@ CMD ["node", "mock-mcp-server.js"] "params": map[string]interface{}{}, } - result = sendMCPRequest(t, serverURL+"/mcp/mock-playwright", "test-mock-key", listReq) + result = sendMCPRequest(t, serverURL+"/mcp", "test-mock-key", listReq) // The key test is that we didn't panic, not whether tools/list works perfectly // Check if we got any tools registered (may be zero if backend connection failed) diff --git a/test/integration/safeinputs_http_test.go b/test/integration/safeinputs_http_test.go index bd372000..29b3ac07 100644 --- a/test/integration/safeinputs_http_test.go +++ b/test/integration/safeinputs_http_test.go @@ -167,6 +167,7 @@ func TestSafeinputsHTTPBackend(t *testing.T) { cmd := exec.CommandContext(ctx, binaryPath, "--config-stdin", + "--listen", "127.0.0.1:3001", "--routed", ) @@ -185,8 +186,14 @@ func TestSafeinputsHTTPBackend(t *testing.T) { cmd.Wait() }() - // Wait for gateway to start and read the configuration output - time.Sleep(2 * time.Second) + // Wait for the gateway HTTP server to be ready + if !waitForServer(t, "http://127.0.0.1:3001/health", 20*time.Second) { + t.Logf("STDOUT: %s", stdout.String()) + t.Logf("STDERR: %s", stderr.String()) + t.Fatal("Gateway did not start in time") + } + // Small delay to ensure stdout JSON is written + time.Sleep(200 * time.Millisecond) // Parse the gateway output to get the actual port var gatewayConfig struct { diff --git a/test/integration/start_gateway_with_pipe.sh b/test/integration/start_gateway_with_pipe.sh index 0309fe6a..78d591ea 100755 --- a/test/integration/start_gateway_with_pipe.sh +++ b/test/integration/start_gateway_with_pipe.sh @@ -84,7 +84,27 @@ fi log_info "Using binary: $BINARY" # Prepare configuration JSON -CONFIG_JSON=$(cat < /dev/null 2>&1; then + while true; do + if curl -s -f --max-time 2 "$url" > /dev/null 2>&1; then log_info "Gateway is ready!" return 0 fi + # Check elapsed time after the curl attempt + local elapsed=$(( $(date +%s) - start_time )) + if [ "$elapsed" -ge "$max_wait" ]; then + break + fi + # Check if process is still running if ! kill -0 "$GATEWAY_PID" 2>/dev/null; then log_error "Gateway process died unexpectedly" @@ -168,7 +196,6 @@ wait_for_gateway() { fi sleep 0.5 - waited=$((waited + 1)) done log_error "Gateway did not become ready within ${max_wait}s"