From 676353fdf07f8173d2742807b015b1b1ae146c7c Mon Sep 17 00:00:00 2001 From: "Jiaxiao (mossaka) Zhou" Date: Tue, 6 Jan 2026 21:49:29 +0000 Subject: [PATCH 1/2] Replace awmg with gh-aw-mcpg as MCP gateway MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change modifies the gh-aw compiler to use gh-aw-mcpg (MCP gateway) instead of the deprecated awmg binary. gh-aw-mcpg runs as a Docker container on the Actions runner host, and AWF containers connect to it via host.docker.internal. Key changes: - Add gateway constants and types for gh-aw-mcpg - Add gateway.go with Docker command generation and helper functions - Add MCP field to SandboxConfig for gateway configuration - Add --enable-host-access AWF flag when MCP gateway is enabled - Update health check script for gh-aw-mcpg - Add host.docker.internal to CodexDefaultDomains - Remove awmg references from Makefile 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- Makefile | 4 +- actions/setup/sh/verify_mcp_gateway_health.sh | 118 +++++---- pkg/workflow/claude_engine.go | 10 + pkg/workflow/codex_engine.go | 10 + pkg/workflow/copilot_engine_execution.go | 10 + pkg/workflow/domains.go | 1 + .../frontmatter_extraction_security.go | 80 +++++- pkg/workflow/gateway.go | 238 ++++++++++++++++++ pkg/workflow/mcp_gateway_constants.go | 27 +- pkg/workflow/mcp_renderer.go | 4 +- pkg/workflow/sandbox.go | 5 +- pkg/workflow/tools_types.go | 7 +- 12 files changed, 456 insertions(+), 58 deletions(-) create mode 100644 pkg/workflow/gateway.go diff --git a/Makefile b/Makefile index dcafb4ddaa..0266efbeb3 100644 --- a/Makefile +++ b/Makefile @@ -193,7 +193,6 @@ clean: @echo "Cleaning build artifacts..." @# Remove main binary and platform-specific binaries rm -f $(BINARY_NAME) $(BINARY_NAME)-* - rm -f $(AWMG_BINARY_NAME) $(AWMG_BINARY_NAME)-* @# Remove bundle-js binary rm -f bundle-js @# Remove coverage files @@ -563,8 +562,7 @@ agent-finish: deps-dev fmt lint build test-all fix recompile dependabot generate help: @echo "Available targets:" @echo " build - Build the binary for current platform" - @echo " build-awmg - Build the awmg (MCP gateway) binary for current platform" - @echo " build-all - Build binaries for all platforms (gh-aw and awmg)" + @echo " build-all - Build binaries for all platforms" @echo " test - Run Go tests (unit + integration)" @echo " test-unit - Run Go unit tests only (faster)" @echo " test-security - Run security regression tests" diff --git a/actions/setup/sh/verify_mcp_gateway_health.sh b/actions/setup/sh/verify_mcp_gateway_health.sh index c72458e334..086b06af95 100755 --- a/actions/setup/sh/verify_mcp_gateway_health.sh +++ b/actions/setup/sh/verify_mcp_gateway_health.sh @@ -1,30 +1,32 @@ #!/usr/bin/env bash -# Verify MCP Gateway Health -# This script verifies that the MCP gateway is running and healthy +# Verify MCP Gateway (gh-aw-mcpg) Health +# This script verifies that the gh-aw-mcpg gateway is running and healthy set -e -# Usage: verify_mcp_gateway_health.sh GATEWAY_URL MCP_CONFIG_PATH LOGS_FOLDER +# Usage: verify_mcp_gateway_health.sh GATEWAY_URL MCP_CONFIG_PATH LOGS_FOLDER [SESSION_TOKEN] # # Arguments: -# GATEWAY_URL : The HTTP URL of the MCP gateway (e.g., http://localhost:8080) +# GATEWAY_URL : The HTTP URL of the MCP gateway (e.g., http://localhost:80) # MCP_CONFIG_PATH : Path to the MCP configuration file # LOGS_FOLDER : Path to the gateway logs folder +# SESSION_TOKEN : Bearer token for MCP client auth (optional, defaults to awf-session) # # Exit codes: # 0 - Gateway is healthy and ready # 1 - Gateway failed to start or configuration is invalid -if [ "$#" -ne 3 ]; then - echo "Usage: $0 GATEWAY_URL MCP_CONFIG_PATH LOGS_FOLDER" >&2 +if [ "$#" -lt 3 ]; then + echo "Usage: $0 GATEWAY_URL MCP_CONFIG_PATH LOGS_FOLDER [SESSION_TOKEN]" >&2 exit 1 fi gateway_url="$1" mcp_config_path="$2" logs_folder="$3" +session_token="${4:-awf-session}" -echo 'Waiting for MCP Gateway to be ready...' +echo 'Verifying gh-aw-mcpg (MCP Gateway) health...' echo '' echo '=== File Locations ===' echo "Gateway URL: $gateway_url" @@ -37,7 +39,7 @@ echo '' echo '=== Gateway Logs Check ===' if [ -f "${logs_folder}/gateway.log" ]; then echo "✓ Gateway log file exists at: ${logs_folder}/gateway.log" - echo "Log file size: $(stat -f%z "${logs_folder}/gateway.log" 2>/dev/null || stat -c%s "${logs_folder}/gateway.log" 2>/dev/null || echo 'unknown') bytes" + echo "Log file size: $(stat -c%s "${logs_folder}/gateway.log" 2>/dev/null || stat -f%z "${logs_folder}/gateway.log" 2>/dev/null || echo 'unknown') bytes" echo "Last few lines of gateway log:" tail -10 "${logs_folder}/gateway.log" 2>/dev/null || echo "Could not read log tail" else @@ -45,6 +47,18 @@ else fi echo '' +# Check if gh-aw-mcpg container is running +echo '=== Docker Container Check ===' +if docker ps | grep -q gh-aw-mcpg; then + echo "✓ gh-aw-mcpg container is running" + docker ps --filter "name=gh-aw-mcpg" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" +else + echo "⚠ gh-aw-mcpg container not found in running containers" + echo "Available containers:" + docker ps --format "table {{.Names}}\t{{.Status}}" +fi +echo '' + # Wait for gateway to be ready FIRST before checking config echo '=== Testing Gateway Health ===' max_retries=30 @@ -66,7 +80,10 @@ if [ "$gateway_ready" = false ]; then echo "✗ Error: MCP Gateway failed to start after $max_retries attempts" echo '' echo '=== Gateway Logs (Full) ===' - cat "${logs_folder}/gateway.log" || echo 'No gateway logs found' + cat "${logs_folder}/gateway.log" 2>/dev/null || echo 'No gateway logs found' + echo '' + echo '=== Docker Logs ===' + docker logs gh-aw-mcpg 2>&1 || echo 'Could not get docker logs' exit 1 fi echo '' @@ -75,8 +92,8 @@ echo '' echo '=== MCP Configuration File ===' if [ -f "$mcp_config_path" ]; then echo "✓ Config file exists at: $mcp_config_path" - echo "File size: $(stat -f%z "$mcp_config_path" 2>/dev/null || stat -c%s "$mcp_config_path" 2>/dev/null || echo 'unknown') bytes" - echo "Last modified: $(stat -f%Sm "$mcp_config_path" 2>/dev/null || stat -c%y "$mcp_config_path" 2>/dev/null || echo 'unknown')" + echo "File size: $(stat -c%s "$mcp_config_path" 2>/dev/null || stat -f%z "$mcp_config_path" 2>/dev/null || echo 'unknown') bytes" + echo "Last modified: $(stat -c%y "$mcp_config_path" 2>/dev/null || stat -f%Sm "$mcp_config_path" 2>/dev/null || echo 'unknown')" else echo "✗ Config file NOT found at: $mcp_config_path" exit 1 @@ -88,64 +105,79 @@ echo '=== MCP Configuration Content ===' cat "$mcp_config_path" || { echo 'ERROR: Failed to read MCP config file'; exit 1; } echo '' -# Verify safeinputs and safeoutputs are present in config +# Verify required servers are present in config (if applicable) echo '=== Verifying Required Servers ===' -if ! grep -q '"safeinputs"' "$mcp_config_path"; then - echo '✗ ERROR: safeinputs server not found in MCP configuration' - exit 1 +# Check for safeinputs and safeoutputs if they're expected +if grep -q '"safeinputs"' "$mcp_config_path" 2>/dev/null; then + echo '✓ safeinputs server found in configuration' +else + echo '⚠ safeinputs server not found (may not be required)' fi -echo '✓ safeinputs server found in configuration' -if ! grep -q '"safeoutputs"' "$mcp_config_path"; then - echo '✗ ERROR: safeoutputs server not found in MCP configuration' - exit 1 +if grep -q '"safeoutputs"' "$mcp_config_path" 2>/dev/null; then + echo '✓ safeoutputs server found in configuration' +else + echo '⚠ safeoutputs server not found (may not be required)' fi -echo '✓ safeoutputs server found in configuration' echo '' -# Fetch and display gateway servers list -echo '=== Gateway Servers List ===' -echo "Fetching servers from: ${gateway_url}/servers" -curl -s "${gateway_url}/servers" || echo "✗ Could not fetch servers list" +# List registered MCP servers via sys endpoint +echo '=== Gateway MCP System Info ===' +echo "Querying: ${gateway_url}/mcp/sys" +sys_response=$(curl -sf "${gateway_url}/mcp/sys" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${session_token}" \ + -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list_servers","arguments":{}}}' 2>/dev/null) || true + +if [ -n "$sys_response" ]; then + echo "Response:" + echo "$sys_response" | jq -r '.result.content[0].text' 2>/dev/null || echo "$sys_response" +else + echo "⚠ Could not query sys endpoint (this may be expected if sys server is not configured)" +fi echo '' # Test MCP server connectivity through gateway echo '=== Testing MCP Server Connectivity ===' -# Extract first external MCP server name from config (excluding safeinputs/safeoutputs) -mcp_server=$(jq -r '.mcpServers | to_entries[] | select(.key != "safeinputs" and .key != "safeoutputs") | .key' "$mcp_config_path" | head -n 1) +# Extract first MCP server name from config (excluding safeinputs/safeoutputs) +mcp_server=$(jq -r '.mcpServers | to_entries[] | select(.key != "safeinputs" and .key != "safeoutputs") | .key' "$mcp_config_path" 2>/dev/null | head -n 1) || true + if [ -n "$mcp_server" ]; then echo "Testing connectivity to MCP server: $mcp_server" mcp_url="${gateway_url}/mcp/${mcp_server}" echo "MCP URL: $mcp_url" echo '' - - # Check if server was rewritten in config - echo "Checking if '$mcp_server' was rewritten to use gateway..." - server_config=$(jq -r ".mcpServers.\"$mcp_server\"" "$mcp_config_path") - echo "Server config for '$mcp_server':" - echo "$server_config" | jq '.' 2>/dev/null || echo "$server_config" - - if echo "$server_config" | grep -q "gateway"; then - echo "✓ Server appears to be configured for gateway" - else - echo "⚠ Server may not be configured for gateway (no 'gateway' field found)" + + # Check if server config uses HTTP transport (gateway-proxied) + echo "Checking '$mcp_server' configuration..." + server_config=$(jq -r ".mcpServers.\"$mcp_server\"" "$mcp_config_path" 2>/dev/null) || true + if [ -n "$server_config" ]; then + echo "Server config:" + echo "$server_config" | jq '.' 2>/dev/null || echo "$server_config" + + if echo "$server_config" | grep -q '"type": "http"' 2>/dev/null; then + echo "✓ Server is configured for HTTP transport (gateway-proxied)" + else + echo "⚠ Server may use local transport (not gateway-proxied)" + fi fi echo '' - + # Test with MCP initialize call echo "Sending MCP initialize request..." response=$(curl -s -w "\n%{http_code}" -X POST "$mcp_url" \ -H "Content-Type: application/json" \ - -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0.0"}}}') - + -H "Authorization: Bearer ${session_token}" \ + -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0.0"}}}' 2>/dev/null) || true + http_code=$(echo "$response" | tail -n 1) body=$(echo "$response" | head -n -1) - + echo "HTTP Status: $http_code" echo "Response: $body" echo '' - + if [ "$http_code" = "200" ]; then echo "✓ MCP server connectivity test passed" else @@ -158,4 +190,6 @@ else echo "No external MCP servers configured for testing" fi +echo '' +echo '=== Health Check Complete ===' exit 0 diff --git a/pkg/workflow/claude_engine.go b/pkg/workflow/claude_engine.go index 9b6869dd61..7b7b043a41 100644 --- a/pkg/workflow/claude_engine.go +++ b/pkg/workflow/claude_engine.go @@ -235,6 +235,9 @@ func (e *ClaudeEngine) GetExecutionSteps(workflowData *WorkflowData, logFile str // Check if safe-inputs is enabled to include host.docker.internal in allowed domains hasSafeInputs := IsSafeInputsEnabled(workflowData.SafeInputs, workflowData) + // Check if MCP gateway is enabled + hasMCPGateway := IsMCPGatewayEnabled(workflowData) + // Get allowed domains (Claude defaults + network permissions + host.docker.internal if safe-inputs enabled) allowedDomains := GetClaudeAllowedDomainsWithSafeInputs(workflowData.NetworkPermissions, hasSafeInputs) @@ -242,6 +245,13 @@ func (e *ClaudeEngine) GetExecutionSteps(workflowData *WorkflowData, logFile str var awfArgs []string awfArgs = append(awfArgs, "--env-all") + // Add --enable-host-access when MCP gateway is enabled + // This allows AWF containers to reach gh-aw-mcpg running on the host + if hasMCPGateway { + awfArgs = append(awfArgs, "--enable-host-access") + claudeLog.Print("Added --enable-host-access for MCP gateway access") + } + // TTY is required for Claude Code CLI awfArgs = append(awfArgs, "--tty") diff --git a/pkg/workflow/codex_engine.go b/pkg/workflow/codex_engine.go index f635872c5a..bb03513c66 100644 --- a/pkg/workflow/codex_engine.go +++ b/pkg/workflow/codex_engine.go @@ -156,10 +156,20 @@ func (e *CodexEngine) GetExecutionSteps(workflowData *WorkflowData, logFile stri // Get allowed domains (Codex defaults + network permissions) allowedDomains := GetCodexAllowedDomains(workflowData.NetworkPermissions) + // Check if MCP gateway is enabled + hasMCPGateway := IsMCPGatewayEnabled(workflowData) + // Build AWF arguments: mount points + standard flags + custom args from config var awfArgs []string awfArgs = append(awfArgs, "--env-all") + // Add --enable-host-access when MCP gateway is enabled + // This allows AWF containers to reach gh-aw-mcpg running on the host + if hasMCPGateway { + awfArgs = append(awfArgs, "--enable-host-access") + codexEngineLog.Print("Added --enable-host-access for MCP gateway access") + } + // Set container working directory to match GITHUB_WORKSPACE awfArgs = append(awfArgs, "--container-workdir", "\"${GITHUB_WORKSPACE}\"") codexEngineLog.Print("Set container working directory to GITHUB_WORKSPACE") diff --git a/pkg/workflow/copilot_engine_execution.go b/pkg/workflow/copilot_engine_execution.go index 3017129df6..a33e74b4e2 100644 --- a/pkg/workflow/copilot_engine_execution.go +++ b/pkg/workflow/copilot_engine_execution.go @@ -225,6 +225,9 @@ func (e *CopilotEngine) GetExecutionSteps(workflowData *WorkflowData, logFile st // Check if safe-inputs is enabled to include host.docker.internal in allowed domains hasSafeInputs := IsSafeInputsEnabled(workflowData.SafeInputs, workflowData) + // Check if MCP gateway is enabled + hasMCPGateway := IsMCPGatewayEnabled(workflowData) + // Get allowed domains (copilot defaults + network permissions + host.docker.internal if safe-inputs enabled) allowedDomains := GetCopilotAllowedDomainsWithSafeInputs(workflowData.NetworkPermissions, hasSafeInputs) @@ -232,6 +235,13 @@ func (e *CopilotEngine) GetExecutionSteps(workflowData *WorkflowData, logFile st var awfArgs []string awfArgs = append(awfArgs, "--env-all") + // Add --enable-host-access when MCP gateway is enabled + // This allows AWF containers to reach gh-aw-mcpg running on the host + if hasMCPGateway { + awfArgs = append(awfArgs, "--enable-host-access") + copilotExecLog.Print("Added --enable-host-access for MCP gateway access") + } + // Set container working directory to match GITHUB_WORKSPACE // This ensures pwd inside the container matches what the prompt tells the AI awfArgs = append(awfArgs, "--container-workdir", "\"${GITHUB_WORKSPACE}\"") diff --git a/pkg/workflow/domains.go b/pkg/workflow/domains.go index a65b29aac5..a46fdc41c7 100644 --- a/pkg/workflow/domains.go +++ b/pkg/workflow/domains.go @@ -33,6 +33,7 @@ var CopilotDefaultDomains = []string{ // CodexDefaultDomains are the minimal default domains required for Codex CLI operation var CodexDefaultDomains = []string{ "api.openai.com", + "host.docker.internal", "openai.com", } diff --git a/pkg/workflow/frontmatter_extraction_security.go b/pkg/workflow/frontmatter_extraction_security.go index c23c7cb6d6..a3210fcca3 100644 --- a/pkg/workflow/frontmatter_extraction_security.go +++ b/pkg/workflow/frontmatter_extraction_security.go @@ -161,9 +161,8 @@ func (c *Compiler) extractSandboxConfig(frontmatter map[string]any) *SandboxConf } if mcpVal, hasMCP := sandboxObj["mcp"]; hasMCP { - frontmatterExtractionSecurityLog.Print("Unsupported MCP gateway configuration (removed)") - // MCP gateway (awmg) has been removed - this configuration is no longer supported - _ = mcpVal // Silence unused variable warning + frontmatterExtractionSecurityLog.Print("Extracting MCP gateway configuration (gh-aw-mcpg)") + config.MCP = c.extractMCPGatewayConfig(mcpVal) } // If we found agent field, return the new format config @@ -283,6 +282,81 @@ func (c *Compiler) extractAgentSandboxConfig(agentVal any) *AgentSandboxConfig { return agentConfig } +// extractMCPGatewayConfig extracts MCP gateway configuration from sandbox.mcp +func (c *Compiler) extractMCPGatewayConfig(mcpVal any) *MCPGatewayRuntimeConfig { + mcpObj, ok := mcpVal.(map[string]any) + if !ok { + // If mcp is just "true" or some non-object, use defaults + return &MCPGatewayRuntimeConfig{} + } + + config := &MCPGatewayRuntimeConfig{} + + // Extract version (MUST be pinned, not "latest") + if version, ok := mcpObj["version"].(string); ok { + if version == "latest" { + frontmatterExtractionSecurityLog.Print("Warning: MCP gateway version 'latest' is not allowed, using default pinned version") + config.Version = DefaultMCPGatewayVersion + } else { + config.Version = version + } + } + + // Extract port + if port, ok := mcpObj["port"].(int); ok { + config.Port = port + } else if portFloat, ok := mcpObj["port"].(float64); ok { + config.Port = int(portFloat) + } + + // Extract session-token + if sessionToken, ok := mcpObj["session-token"].(string); ok { + config.SessionToken = sessionToken + } + + // Extract api-key (for backward compatibility) + if apiKey, ok := mcpObj["api-key"].(string); ok { + config.APIKey = apiKey + } + + // Extract domain + if domain, ok := mcpObj["domain"].(string); ok { + config.Domain = domain + } + + // Extract custom container image + if container, ok := mcpObj["container"].(string); ok { + config.Container = container + } + + // Extract command (for custom gateway binary) + if command, ok := mcpObj["command"].(string); ok { + config.Command = command + } + + // Extract args + if args, ok := mcpObj["args"].([]any); ok { + for _, arg := range args { + if argStr, ok := arg.(string); ok { + config.Args = append(config.Args, argStr) + } + } + } + + // Extract env + if env, ok := mcpObj["env"].(map[string]any); ok { + config.Env = make(map[string]string) + for key, val := range env { + if valStr, ok := val.(string); ok { + config.Env[key] = valStr + } + } + } + + frontmatterExtractionSecurityLog.Printf("Extracted MCP gateway config: version=%s, port=%d", config.Version, config.Port) + return config +} + // extractSRTConfig extracts Sandbox Runtime configuration from a map func (c *Compiler) extractSRTConfig(configVal any) *SandboxRuntimeConfig { configObj, ok := configVal.(map[string]any) diff --git a/pkg/workflow/gateway.go b/pkg/workflow/gateway.go new file mode 100644 index 0000000000..1cb26eabfc --- /dev/null +++ b/pkg/workflow/gateway.go @@ -0,0 +1,238 @@ +package workflow + +import ( + "fmt" + "strings" + + "github.com/githubnext/gh-aw/pkg/logger" +) + +var gatewayLog = logger.New("workflow:gateway") + +// ValidateGatewayVersion validates that a gateway version is properly pinned +// Returns an error if the version is "latest" or empty +func ValidateGatewayVersion(version string) error { + if version == "latest" { + return fmt.Errorf("gh-aw-mcpg version must be pinned (e.g., v0.1.0), 'latest' is not allowed for reproducibility") + } + if version == "" { + return fmt.Errorf("gh-aw-mcpg version must be specified") + } + if !strings.HasPrefix(version, "v") { + return fmt.Errorf("gh-aw-mcpg version must start with 'v' (e.g., v0.1.0), got: %s", version) + } + return nil +} + +// GetGatewayVersion returns the gateway version to use, defaulting to DefaultMCPGatewayVersion +func GetGatewayVersion(config *MCPGatewayRuntimeConfig) string { + if config != nil && config.Version != "" { + return config.Version + } + return DefaultMCPGatewayVersion +} + +// GetGatewayPort returns the gateway port to use, defaulting to DefaultMCPGatewayPort +func GetGatewayPort(config *MCPGatewayRuntimeConfig) int { + if config != nil && config.Port > 0 { + return config.Port + } + return DefaultMCPGatewayPort +} + +// GetGatewaySessionToken returns the session token to use, defaulting to DefaultGatewaySessionToken +func GetGatewaySessionToken(config *MCPGatewayRuntimeConfig) string { + if config != nil && config.SessionToken != "" { + return config.SessionToken + } + return DefaultGatewaySessionToken +} + +// GenerateMCPGatewayDockerCommands generates the commands to start gh-aw-mcpg as a Docker container +// The gateway runs on the host and AWF containers connect to it via host.docker.internal +func GenerateMCPGatewayDockerCommands(config *MCPGatewayRuntimeConfig, mcpConfigPath string) []string { + version := GetGatewayVersion(config) + port := GetGatewayPort(config) + + // Validate version at generation time + if err := ValidateGatewayVersion(version); err != nil { + gatewayLog.Printf("Warning: %v, using default version %s", err, DefaultMCPGatewayVersion) + version = DefaultMCPGatewayVersion + } + + image := fmt.Sprintf("%s:%s", DefaultMCPGatewayImage, version) + + gatewayLog.Printf("Generating gh-aw-mcpg Docker commands: image=%s, port=%d", image, port) + + var commands []string + + // Create logs directory + commands = append(commands, fmt.Sprintf("mkdir -p %s", MCPGatewayLogsFolder)) + + // Build the Docker run command + // The config is piped via stdin to avoid file system issues + dockerCmd := fmt.Sprintf(`cat %s | docker run \ + --rm -i \ + --name %s \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -p %d:%d \ + --add-host host.docker.internal:host-gateway \ + -e GITHUB_PERSONAL_ACCESS_TOKEN \ + %s \ + --routed --listen 0.0.0.0:%d --config-stdin \ + > %s/gateway.log 2>&1 &`, + mcpConfigPath, + MCPGatewayContainerName, + port, + MCPGatewayContainerPort, + image, + MCPGatewayContainerPort, + MCPGatewayLogsFolder, + ) + + commands = append(commands, dockerCmd) + + // Wait for gateway to be healthy + commands = append(commands, fmt.Sprintf(`echo "Waiting for gh-aw-mcpg to be ready..." +for i in $(seq 1 30); do + if curl -sf http://localhost:%d/health > /dev/null 2>&1; then + echo "gh-aw-mcpg is ready" + break + fi + if [ $i -eq 30 ]; then + echo "ERROR: gh-aw-mcpg failed to start" + cat %s/gateway.log + exit 1 + fi + sleep 1 +done`, port, MCPGatewayLogsFolder)) + + return commands +} + +// GenerateMCPGatewayStartStep generates the GitHub Actions step to start the MCP gateway +// This step starts gh-aw-mcpg as a Docker container +func GenerateMCPGatewayStartStep(config *MCPGatewayRuntimeConfig, mcpEnvVars map[string]string) GitHubActionStep { + gatewayLog.Print("Generating MCP gateway start step") + + var stepLines []string + + stepLines = append(stepLines, " - name: Start MCP Gateway") + stepLines = append(stepLines, " id: mcp-gateway-start") + + // Add environment variables + if len(mcpEnvVars) > 0 { + stepLines = append(stepLines, " env:") + for key, value := range mcpEnvVars { + stepLines = append(stepLines, fmt.Sprintf(" %s: %s", key, value)) + } + } + + stepLines = append(stepLines, " run: |") + + // Generate the Docker commands + mcpConfigPath := "/tmp/gh-aw/mcpg-config.json" + commands := GenerateMCPGatewayDockerCommands(config, mcpConfigPath) + for _, cmd := range commands { + // Indent each line of multi-line commands + for _, line := range strings.Split(cmd, "\n") { + stepLines = append(stepLines, fmt.Sprintf(" %s", line)) + } + } + + return GitHubActionStep(stepLines) +} + +// TransformMCPConfigForGatewayClient transforms MCP server configs for the agent client +// Each server is converted to use HTTP transport via the gateway +func TransformMCPConfigForGatewayClient(mcpServers map[string]any, config *MCPGatewayRuntimeConfig) map[string]any { + sessionToken := GetGatewaySessionToken(config) + port := GetGatewayPort(config) + + transformed := make(map[string]any) + for serverName := range mcpServers { + transformed[serverName] = map[string]any{ + "type": "http", + "url": fmt.Sprintf("http://host.docker.internal:%d/mcp/%s", port, serverName), + "headers": map[string]any{ + "Authorization": fmt.Sprintf("Bearer %s", sessionToken), + }, + "tools": []string{"*"}, + } + } + + gatewayLog.Printf("Transformed %d MCP servers for gateway client access", len(transformed)) + return transformed +} + +// IsMCPGatewayEnabled checks if MCP gateway is enabled in the workflow configuration +func IsMCPGatewayEnabled(workflowData *WorkflowData) bool { + if workflowData == nil || workflowData.SandboxConfig == nil { + return false + } + // Gateway is enabled when sandbox.mcp is configured + return workflowData.SandboxConfig.MCP != nil +} + +// GetMCPGatewayConfig returns the MCP gateway configuration from workflow data +func GetMCPGatewayConfig(workflowData *WorkflowData) *MCPGatewayRuntimeConfig { + if workflowData == nil || workflowData.SandboxConfig == nil { + return nil + } + return workflowData.SandboxConfig.MCP +} + +// GenerateGatewayConfigPath returns the path to the gateway config file +func GenerateGatewayConfigPath() string { + return "/tmp/gh-aw/mcpg-config.json" +} + +// GenerateClientMCPConfigForGateway generates the MCP client config that routes +// all MCP server requests through the gateway +func GenerateClientMCPConfigForGateway(yaml *strings.Builder, serverNames []string, config *MCPGatewayRuntimeConfig, format string) { + sessionToken := GetGatewaySessionToken(config) + port := GetGatewayPort(config) + + gatewayLog.Printf("Generating client MCP config for %d servers via gateway", len(serverNames)) + + if format == "json" { + // JSON format for Copilot/Claude + for i, serverName := range serverNames { + isLast := i == len(serverNames)-1 + yaml.WriteString(fmt.Sprintf(" \"%s\": {\n", serverName)) + yaml.WriteString(" \"type\": \"http\",\n") + yaml.WriteString(fmt.Sprintf(" \"url\": \"http://host.docker.internal:%d/mcp/%s\",\n", port, serverName)) + yaml.WriteString(" \"headers\": {\n") + yaml.WriteString(fmt.Sprintf(" \"Authorization\": \"Bearer %s\"\n", sessionToken)) + yaml.WriteString(" },\n") + yaml.WriteString(" \"tools\": [\"*\"]\n") + if isLast { + yaml.WriteString(" }\n") + } else { + yaml.WriteString(" },\n") + } + } + } else if format == "toml" { + // TOML format for Codex + for _, serverName := range serverNames { + yaml.WriteString("\n") + yaml.WriteString(fmt.Sprintf(" [mcp_servers.%s]\n", serverName)) + yaml.WriteString(" type = \"http\"\n") + yaml.WriteString(fmt.Sprintf(" url = \"http://host.docker.internal:%d/mcp/%s\"\n", port, serverName)) + yaml.WriteString(fmt.Sprintf(" headers = { Authorization = \"Bearer %s\" }\n", sessionToken)) + } + } +} + +// ShouldUseGatewayForMCP determines if the MCP gateway should be used +// Returns true if: +// - sandbox.mcp is configured +// - AWF (firewall) is enabled +func ShouldUseGatewayForMCP(workflowData *WorkflowData) bool { + if !IsMCPGatewayEnabled(workflowData) { + return false + } + // Gateway only makes sense when firewall is enabled + // because the gateway runs on the host and containers connect to it + return isFirewallEnabled(workflowData) +} diff --git a/pkg/workflow/mcp_gateway_constants.go b/pkg/workflow/mcp_gateway_constants.go index 57a69cc418..1220f7234b 100644 --- a/pkg/workflow/mcp_gateway_constants.go +++ b/pkg/workflow/mcp_gateway_constants.go @@ -1,8 +1,27 @@ package workflow const ( - // DefaultMCPGatewayPort is the default port for the MCP gateway - // This constant is kept for backwards compatibility with existing configurations - // even though the awmg gateway binary has been removed. - DefaultMCPGatewayPort = 8080 + // DefaultMCPGatewayPort is the default port for the MCP gateway (gh-aw-mcpg) + // Port 80 is used for host.docker.internal access from AWF containers + DefaultMCPGatewayPort = 80 + + // DefaultMCPGatewayImage is the Docker image for gh-aw-mcpg + // IMPORTANT: Always use a pinned version, NEVER use "latest" + DefaultMCPGatewayImage = "ghcr.io/githubnext/gh-aw-mcpg" + + // DefaultMCPGatewayVersion is the pinned version of gh-aw-mcpg + // MUST be updated when releasing new gh-aw-mcpg versions + DefaultMCPGatewayVersion = "v0.1.0" + + // DefaultGatewaySessionToken is the default Bearer token for MCP client authentication + DefaultGatewaySessionToken = "awf-session" + + // MCPGatewayLogsFolder is the folder where gateway logs are stored + MCPGatewayLogsFolder = "/tmp/gh-aw/mcp-gateway-logs" + + // MCPGatewayContainerName is the name of the gh-aw-mcpg Docker container + MCPGatewayContainerName = "gh-aw-mcpg" + + // MCPGatewayContainerPort is the internal port in the gh-aw-mcpg container + MCPGatewayContainerPort = 8000 ) diff --git a/pkg/workflow/mcp_renderer.go b/pkg/workflow/mcp_renderer.go index 0d2287f3a7..81daeb5876 100644 --- a/pkg/workflow/mcp_renderer.go +++ b/pkg/workflow/mcp_renderer.go @@ -434,7 +434,7 @@ type JSONMCPConfigOptions struct { // PostEOFCommands is an optional function to add commands after the EOF (e.g., debug output) PostEOFCommands func(yaml *strings.Builder) // GatewayConfig is an optional gateway configuration to include in the MCP config - // When set, adds a "gateway" section with port and apiKey for awmg to use + // When set, adds a "gateway" section with port and apiKey for gh-aw-mcpg to use GatewayConfig *MCPGatewayRuntimeConfig } @@ -708,7 +708,7 @@ func RenderJSONMCPConfig( // Write config file footer yaml.WriteString(" }\n") - // Add gateway section if configured (needed for awmg to rewrite config) + // Add gateway section if configured (needed for gh-aw-mcpg to rewrite config) if options.GatewayConfig != nil { yaml.WriteString(" ,\n") yaml.WriteString(" \"gateway\": {\n") diff --git a/pkg/workflow/sandbox.go b/pkg/workflow/sandbox.go index 3e1e5fbade..38b3d39bea 100644 --- a/pkg/workflow/sandbox.go +++ b/pkg/workflow/sandbox.go @@ -32,11 +32,12 @@ const ( ) // SandboxConfig represents the top-level sandbox configuration from front matter -// New format: { agent: "awf"|"srt"|{type, config} } +// New format: { agent: "awf"|"srt"|{type, config}, mcp: {version, ...} } // Legacy format: "default"|"sandbox-runtime" or { type, config } type SandboxConfig struct { // New fields - Agent *AgentSandboxConfig `yaml:"agent,omitempty"` // Agent sandbox configuration + Agent *AgentSandboxConfig `yaml:"agent,omitempty"` // Agent sandbox configuration + MCP *MCPGatewayRuntimeConfig `yaml:"mcp,omitempty"` // MCP gateway configuration (gh-aw-mcpg) // Legacy fields (for backward compatibility) Type SandboxType `yaml:"type,omitempty"` // Sandbox type: "default" or "sandbox-runtime" diff --git a/pkg/workflow/tools_types.go b/pkg/workflow/tools_types.go index 929dc0260f..616fd12aa0 100644 --- a/pkg/workflow/tools_types.go +++ b/pkg/workflow/tools_types.go @@ -302,16 +302,19 @@ type MCPServerConfig struct { // MCPGatewayRuntimeConfig represents the configuration for the MCP gateway runtime execution // The gateway routes MCP server calls through a unified HTTP endpoint +// When using gh-aw-mcpg, the gateway runs as a Docker container and AWF containers +// connect to it via host.docker.internal type MCPGatewayRuntimeConfig struct { Command string `yaml:"command,omitempty"` // Custom command to execute (mutually exclusive with Container) Container string `yaml:"container,omitempty"` // Container image for the gateway (mutually exclusive with Command) - Version string `yaml:"version,omitempty"` // Optional version/tag for the container + Version string `yaml:"version,omitempty"` // Version/tag for the container (MUST be pinned, not "latest") Args []string `yaml:"args,omitempty"` // Arguments for command or docker run EntrypointArgs []string `yaml:"entrypointArgs,omitempty"` // Arguments passed to container entrypoint (container only) Env map[string]string `yaml:"env,omitempty"` // Environment variables for the gateway - Port int `yaml:"port,omitempty"` // Port for the gateway HTTP server (default: 8080) + Port int `yaml:"port,omitempty"` // Port for the gateway HTTP server (default: 80) APIKey string `yaml:"api-key,omitempty"` // API key for gateway authentication Domain string `yaml:"domain,omitempty"` // Domain for gateway URL (localhost or host.docker.internal) + SessionToken string `yaml:"session-token,omitempty"` // Bearer token for MCP client auth (default: awf-session) } // HasTool checks if a tool is present in the configuration From 416c6b0fa1ccd983d4402071ab7ec0edbad89850 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 6 Jan 2026 21:52:27 +0000 Subject: [PATCH 2/2] Add changeset [skip-ci] --- .changeset/patch-replace-awmg-with-gh-aw-mcpg.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .changeset/patch-replace-awmg-with-gh-aw-mcpg.md diff --git a/.changeset/patch-replace-awmg-with-gh-aw-mcpg.md b/.changeset/patch-replace-awmg-with-gh-aw-mcpg.md new file mode 100644 index 0000000000..cd97ff6937 --- /dev/null +++ b/.changeset/patch-replace-awmg-with-gh-aw-mcpg.md @@ -0,0 +1,14 @@ +--- +"gh-aw": patch +--- + +Replace the `awmg` MCP gateway container with `gh-aw-mcpg` and add supporting +configuration. + +- Replaces the `awmg` gateway container with `gh-aw-mcpg`. +- Adds gateway constants, types, and configuration extraction. +- Adds `--enable-host-access` AWF flag to allow gateway access when needed. +- Updates the health check script to target `gh-aw-mcpg`. + +This is an internal/tooling change and does not change the public CLI API. +