From 252a5dfdf3ea70a912d5f2a8e343c36b33e50dba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 6 Jan 2026 19:41:28 +0000 Subject: [PATCH 1/6] Initial plan From d532a16950918b3a59832dfec0ff25b73d47aa90 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 6 Jan 2026 19:55:47 +0000 Subject: [PATCH 2/6] Remove awmg binary, build scripts, and initial documentation Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/ci.yml | 3 - .gitignore | 6 - DEVGUIDE.md | 7 - Makefile | 13 +- cmd/awmg/main.go | 73 -- docs/awmg.md | 162 ---- docs/mcp-gateway.md | 51 - examples/README.md | 270 ------ examples/mcp-gateway-base.json | 15 - examples/mcp-gateway-config.json | 9 - examples/mcp-gateway-multi-server.json | 22 - examples/mcp-gateway-override.json | 19 - install-awmg.sh | 387 -------- pkg/awmg/gateway.go | 952 ------------------- pkg/awmg/gateway_inspect_integration_test.go | 317 ------ pkg/awmg/gateway_integration_test.go | 136 --- pkg/awmg/gateway_rewrite_test.go | 398 -------- pkg/awmg/gateway_streamable_http_test.go | 708 -------------- pkg/awmg/gateway_test.go | 700 -------------- pkg/workflow/gateway.go | 312 ------ pkg/workflow/gateway_test.go | 878 ----------------- scripts/test-build-release.sh | 23 - specs/mcp-gateway.md | 195 ---- 23 files changed, 1 insertion(+), 5655 deletions(-) delete mode 100644 cmd/awmg/main.go delete mode 100644 docs/awmg.md delete mode 100644 docs/mcp-gateway.md delete mode 100644 examples/mcp-gateway-base.json delete mode 100644 examples/mcp-gateway-config.json delete mode 100644 examples/mcp-gateway-multi-server.json delete mode 100644 examples/mcp-gateway-override.json delete mode 100755 install-awmg.sh delete mode 100644 pkg/awmg/gateway.go delete mode 100644 pkg/awmg/gateway_inspect_integration_test.go delete mode 100644 pkg/awmg/gateway_integration_test.go delete mode 100644 pkg/awmg/gateway_rewrite_test.go delete mode 100644 pkg/awmg/gateway_streamable_http_test.go delete mode 100644 pkg/awmg/gateway_test.go delete mode 100644 pkg/workflow/gateway.go delete mode 100644 pkg/workflow/gateway_test.go delete mode 100644 specs/mcp-gateway.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 567626a048b..9db9d93223f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -166,9 +166,6 @@ jobs: packages: "./pkg/workflow" pattern: "" skip_pattern: "TestCompile|TestWorkflow|TestGenerate|TestParse|TestMCP|TestTool|TestSkill|TestPlaywright|TestFirewall|TestValidat|TestLock|TestError|TestWarning|SafeOutputs|CreatePullRequest|OutputLabel|HasSafeOutputs|GitHub|Git|PushToPullRequest|BuildFromAllowed|Render|Bundle|Script|WritePromptText|^TestCache|TestCacheDependencies|TestCacheKey|TestValidateCache|^TestActionPinSHAsMatchVersionTags|^TestAction[^P]|Container|Dependabot|Security|PII|TestPermissions|TestPackageExtractor|TestCollectPackagesFromWorkflow|TestAgent|TestCopilot|TestCustom|TestEngine|TestModel|TestNetwork|TestOpenAI|TestProvider|String|Sanitize|Normalize|Trim|Clean|Format|Runtime|Setup|Install|Download|Version|Binary" - - name: "AWMG Gateway Tests" # MCP gateway integration tests - packages: "./pkg/awmg" - pattern: "" concurrency: group: ci-${{ github.ref }}-integration-${{ matrix.test-group.name }} cancel-in-progress: true diff --git a/.gitignore b/.gitignore index 34d9612b0db..418c5acdaeb 100644 --- a/.gitignore +++ b/.gitignore @@ -47,12 +47,6 @@ Thumbs.db /gh-aw-darwin-arm64 /gh-aw-linux-amd64 /gh-aw-linux-arm64 -/awmg -/awmg-darwin-amd64 -/awmg-darwin-arm64 -/awmg-linux-amd64 -/awmg-linux-arm64 -/awmg-windows-amd64.exe # credentials .credentials/ diff --git a/DEVGUIDE.md b/DEVGUIDE.md index 8268dab7323..182f708b6fc 100644 --- a/DEVGUIDE.md +++ b/DEVGUIDE.md @@ -38,13 +38,6 @@ make lint # Build and test the binary make build ./gh-aw --help - -# Build the awmg (MCP gateway) standalone binary -make build-awmg -./awmg --help - -# Build both binaries -make all ``` ### 4. Install the Extension Locally for Testing diff --git a/Makefile b/Makefile index 6dfd1d285f0..dcafb4ddaa1 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,6 @@ # Variables BINARY_NAME=gh-aw -AWMG_BINARY_NAME=awmg VERSION ?= $(shell git describe --tags --always --dirty) # Build flags @@ -10,18 +9,13 @@ LDFLAGS=-ldflags "-s -w -X main.version=$(VERSION)" # Default target .PHONY: all -all: build build-awmg +all: build # Build the binary, run make deps before this .PHONY: build build: sync-templates sync-action-pins go build $(LDFLAGS) -o $(BINARY_NAME) ./cmd/gh-aw -# Build the awmg (MCP gateway) binary -.PHONY: build-awmg -build-awmg: - go build $(LDFLAGS) -o $(AWMG_BINARY_NAME) ./cmd/awmg - # Build for all platforms .PHONY: build-all build-all: build-linux build-darwin build-windows @@ -30,20 +24,15 @@ build-all: build-linux build-darwin build-windows build-linux: GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o $(BINARY_NAME)-linux-amd64 ./cmd/gh-aw GOOS=linux GOARCH=arm64 go build $(LDFLAGS) -o $(BINARY_NAME)-linux-arm64 ./cmd/gh-aw - GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o $(AWMG_BINARY_NAME)-linux-amd64 ./cmd/awmg - GOOS=linux GOARCH=arm64 go build $(LDFLAGS) -o $(AWMG_BINARY_NAME)-linux-arm64 ./cmd/awmg .PHONY: build-darwin build-darwin: GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) -o $(BINARY_NAME)-darwin-amd64 ./cmd/gh-aw GOOS=darwin GOARCH=arm64 go build $(LDFLAGS) -o $(BINARY_NAME)-darwin-arm64 ./cmd/gh-aw - GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) -o $(AWMG_BINARY_NAME)-darwin-amd64 ./cmd/awmg - GOOS=darwin GOARCH=arm64 go build $(LDFLAGS) -o $(AWMG_BINARY_NAME)-darwin-arm64 ./cmd/awmg .PHONY: build-windows build-windows: GOOS=windows GOARCH=amd64 go build $(LDFLAGS) -o $(BINARY_NAME)-windows-amd64.exe ./cmd/gh-aw - GOOS=windows GOARCH=amd64 go build $(LDFLAGS) -o $(AWMG_BINARY_NAME)-windows-amd64.exe ./cmd/awmg # Test the code (runs both unit and integration tests) .PHONY: test diff --git a/cmd/awmg/main.go b/cmd/awmg/main.go deleted file mode 100644 index 17dd54741c3..00000000000 --- a/cmd/awmg/main.go +++ /dev/null @@ -1,73 +0,0 @@ -package main - -import ( - "fmt" - "os" - - "github.com/githubnext/gh-aw/pkg/awmg" - "github.com/githubnext/gh-aw/pkg/console" -) - -// Build-time variables. -var ( - version = "dev" -) - -func main() { - // Set version info - awmg.SetVersionInfo(version) - - // Create the mcp-gateway command - cmd := awmg.NewMCPGatewayCommand() - - // Update command usage to reflect standalone binary - cmd.Use = "awmg" - cmd.Short = "MCP Gateway - Aggregate multiple MCP servers into a single HTTP gateway" - cmd.Long = `awmg (Agentic Workflows MCP Gateway) - Aggregate multiple MCP servers into a single HTTP gateway. - -The gateway: -- Integrates by default with the sandbox.mcp extension point -- Imports Claude/Copilot/Codex MCP server JSON configuration -- Starts each MCP server and mounts an MCP client on each -- Mounts an HTTP MCP server that acts as a gateway to the MCP clients -- Supports most MCP gestures through the go-MCP SDK -- Provides extensive logging to file in the MCP log folder - -Configuration can be provided via: -1. --config flag(s) pointing to JSON config file(s) (can be specified multiple times) -2. stdin (reads JSON configuration from standard input) - -Multiple config files are merged in order, with later files overriding earlier ones. - -Configuration format: -{ - "mcpServers": { - "server-name": { - "command": "command", - "args": ["arg1", "arg2"], - "env": {"KEY": "value"} - } - }, - "gateway": { - "port": 8080, - "apiKey": "optional-key" - } -} - -Examples: - awmg --config config.json # From single file - awmg --config base.json --config override.json # From multiple files (merged) - awmg --port 8080 # From stdin - echo '{"mcpServers":{...}}' | awmg # Pipe config - awmg --config config.json --log-dir /tmp/logs # Custom log dir` - - // Add version flag - cmd.Version = version - cmd.SetVersionTemplate("awmg version {{.Version}}\n") - - // Execute command - if err := cmd.Execute(); err != nil { - fmt.Fprintf(os.Stderr, "%s\n", console.FormatErrorMessage(err.Error())) - os.Exit(1) - } -} diff --git a/docs/awmg.md b/docs/awmg.md deleted file mode 100644 index 2d11b50a4c9..00000000000 --- a/docs/awmg.md +++ /dev/null @@ -1,162 +0,0 @@ -# awmg - Agentic Workflows MCP Gateway - -`awmg` is a standalone binary that implements an MCP (Model Context Protocol) gateway for aggregating multiple MCP servers into a single HTTP endpoint. - -## Installation - -### From Source - -```bash -# Clone the repository -git clone https://github.com/githubnext/gh-aw.git -cd gh-aw - -# Build the binary -make build-awmg - -# The binary will be created as ./awmg -``` - -### Pre-built Binaries - -Download the latest release from the [GitHub releases page](https://github.com/githubnext/gh-aw/releases). - -## Usage - -```bash -# Start gateway with config file -awmg --config config.json - -# Start gateway reading from stdin -echo '{"mcpServers":{...}}' | awmg --port 8080 - -# Custom log directory -awmg --config config.json --log-dir /var/log/mcp-gateway -``` - -## Configuration - -The gateway accepts JSON configuration with the following format: - -```json -{ - "mcpServers": { - "server-name": { - "command": "command-to-run", - "args": ["arg1", "arg2"], - "env": { - "ENV_VAR": "value" - } - }, - "another-server": { - "url": "http://localhost:3000" - } - }, - "gateway": { - "port": 8080, - "apiKey": "optional-api-key" - } -} -``` - -### Configuration Fields - -- `mcpServers`: Map of MCP server configurations - - Each server can be configured with: - - `command`: Command to execute (for stdio transport) - - `args`: Command arguments - - `env`: Environment variables - - `url`: HTTP URL (for HTTP transport) -- `gateway`: Gateway-specific settings - - `port`: HTTP port (default: 8080) - - `apiKey`: Optional API key for authentication - -## Endpoints - -Once running, the gateway exposes the following HTTP endpoints: - -- `GET /health` - Health check endpoint -- `GET /servers` - List all configured MCP servers -- `POST /mcp/{server}` - Proxy MCP requests to a specific server - -## Examples - -### Example 1: Single gh-aw MCP Server - -```json -{ - "mcpServers": { - "gh-aw": { - "command": "gh", - "args": ["aw", "mcp-server"] - } - }, - "gateway": { - "port": 8088 - } -} -``` - -### Example 2: Multiple Servers - -```json -{ - "mcpServers": { - "gh-aw": { - "command": "gh", - "args": ["aw", "mcp-server"], - "env": { - "DEBUG": "cli:*" - } - }, - "remote-server": { - "url": "http://localhost:3000" - } - }, - "gateway": { - "port": 8088 - } -} -``` - -## Integration with GitHub Agentic Workflows - -The awmg binary is designed to work seamlessly with GitHub Agentic Workflows. When you configure `sandbox.mcp` in your workflow, the system automatically sets up the MCP gateway: - -```yaml ---- -sandbox: - mcp: - # MCP gateway runs as standalone awmg CLI - port: 8080 ---- -``` - -## Features - -- ✅ **Multiple MCP Servers**: Connect to and manage multiple MCP servers -- ✅ **HTTP Gateway**: Expose all servers through a unified HTTP interface -- ✅ **Protocol Support**: Supports initialize, list_tools, call_tool, list_resources, list_prompts -- ✅ **Comprehensive Logging**: Per-server log files with detailed operation logs -- ✅ **Command Transport**: Subprocess-based MCP servers via stdio -- ✅ **Streamable HTTP Transport**: HTTP transport using go-sdk StreamableClientTransport -- ⏳ **Docker Support**: Container-based MCP servers (planned) - -## Development - -```bash -# Run tests -make test - -# Build for all platforms -make build-all - -# Clean build artifacts -make clean -``` - -## See Also - -- [MCP Gateway Specification](../specs/mcp-gateway.md) -- [MCP Gateway Usage Guide](mcp-gateway.md) -- [GitHub Agentic Workflows Documentation](https://github.com/githubnext/gh-aw) diff --git a/docs/mcp-gateway.md b/docs/mcp-gateway.md deleted file mode 100644 index 5bd79e3d358..00000000000 --- a/docs/mcp-gateway.md +++ /dev/null @@ -1,51 +0,0 @@ -# MCP Gateway Command - -The MCP gateway is implemented as a standalone `awmg` binary that aggregates multiple MCP servers into a single HTTP gateway. - -## Features - -- **Integrates with sandbox.mcp**: Works with the `sandbox.mcp` extension point in workflows -- **Multiple MCP servers**: Supports connecting to multiple MCP servers simultaneously -- **MCP protocol support**: Implements `initialize`, `list_tools`, `call_tool`, `list_resources`, `list_prompts` -- **Transport support**: Currently supports stdio/command transport, HTTP transport planned -- **Comprehensive logging**: Logs to file in MCP log directory (`/tmp/gh-aw/mcp-gateway-logs` by default) -- **API key authentication**: Optional API key for securing gateway endpoints - -## Usage - -### Basic Usage - -```bash -# From stdin (reads JSON config from standard input) -echo '{"mcpServers":{"gh-aw":{"command":"gh","args":["aw","mcp-server"]}}}' | awmg - -# From config file -awmg --config config.json - -# Custom port and log directory -awmg --config config.json --port 8088 --log-dir /custom/logs -``` - -### Configuration Format - -The gateway accepts configuration in JSON format: - -```json -{ - "mcpServers": { - "server-name": { - "command": "command-to-run", - "args": ["arg1", "arg2"], - "env": { - "ENV_VAR": "value" - } - }, - "http-server": { - "url": "http://localhost:3000" - } - }, - "gateway": { - "port": 8080, - "apiKey": "optional-api-key" - } -} diff --git a/examples/README.md b/examples/README.md index 72778a920d7..bd507b2a854 100644 --- a/examples/README.md +++ b/examples/README.md @@ -10,273 +10,3 @@ For examples of network configuration with package registries and CDNs: - [`network-multi-language.md`](./network-multi-language.md) - Multi-language project with multiple registries See the [Network Configuration Guide](../docs/src/content/docs/guides/network-configuration.md) for more information. - -## Model Context Protocol (MCP) Gateway Examples - -This directory also contains MCP Gateway configuration files for the `mcp-gateway` command. - -## What is MCP Gateway? - -The MCP Gateway is a proxy server that connects to multiple Model Context Protocol (MCP) servers and exposes all their tools through a single HTTP endpoint. This allows clients to access tools from multiple MCP servers without managing individual connections. - -## Example Configurations - -### Simple Configuration (`mcp-gateway-config.json`) - -A basic configuration with a single MCP server: - -```json -{ - "mcpServers": { - "gh-aw": { - "command": "gh", - "args": ["aw", "mcp-server"] - } - }, - "port": 8088 -} -```text - -**Note:** The `port` field is optional in the configuration file. If not specified, the gateway will use port 8088 by default, or you can override it with the `--port` flag. - -### Multi-Server Configuration (`mcp-gateway-multi-server.json`) - -A more complex configuration demonstrating all three server types: - -```json -{ - "mcpServers": { - "gh-aw": { - "command": "gh", - "args": ["aw", "mcp-server"], - "env": { - "DEBUG": "cli:*" - } - }, - "remote-server": { - "url": "http://localhost:3000" - }, - "docker-server": { - "container": "mcp-server:latest", - "args": ["--verbose"], - "env": { - "LOG_LEVEL": "debug" - } - } - }, - "port": 8088 -} -```text - -### Multi-Config Example - -Use multiple configuration files that are merged together: - -**Base Configuration (`mcp-gateway-base.json`)** - Common servers: -```json -{ - "mcpServers": { - "gh-aw": { - "command": "gh", - "args": ["aw", "mcp-server"] - }, - "time": { - "command": "npx", - "args": ["-y", "@modelcontextprotocol/server-time"] - } - }, - "gateway": { - "port": 8088 - } -} -```text - -**Override Configuration (`mcp-gateway-override.json`)** - Environment-specific overrides: -```json -{ - "mcpServers": { - "time": { - "command": "npx", - "args": ["-y", "@modelcontextprotocol/server-time"], - "env": { - "DEBUG": "mcp:*" - } - }, - "memory": { - "command": "npx", - "args": ["-y", "@modelcontextprotocol/server-memory"] - } - }, - "gateway": { - "port": 9090, - "apiKey": "optional-api-key" - } -} -```text - -**Usage:** -```bash -awmg --config mcp-gateway-base.json --config mcp-gateway-override.json -```text - -**Result:** The merged configuration will have: -- `gh-aw` server (from base) -- `time` server with debug environment variable (overridden from override) -- `memory` server (added from override) -- Port 9090 and API key (overridden from override) - -## Server Types - -### Stdio Servers - -Use the `command` field to specify a command-line MCP server: - -```json -{ - "command": "node", - "args": ["server.js"], - "env": { - "ENV_VAR": "value" - } -} -```text - -### HTTP Servers - -Use the `url` field to connect to an HTTP MCP server: - -```json -{ - "url": "http://localhost:3000" -} -```text - -### Docker Servers - -Use the `container` field to run an MCP server in a Docker container: - -```json -{ - "container": "my-mcp-server:latest", - "args": ["--option", "value"], - "env": { - "ENV_VAR": "value" - } -} -```text - -## Usage - -### Start the Gateway - -```bash -# From a single config file -awmg --config mcp-gateway-config.json - -# From multiple config files (merged in order) -awmg --config base-config.json --config override-config.json - -# Specify a custom port -awmg --config mcp-gateway-config.json --port 9000 -```text - -### Multiple Configuration Files - -The gateway supports loading multiple configuration files which are merged in order. Later files override settings from earlier files: - -```bash -# Base configuration with common servers -awmg --config common-servers.json --config team-specific.json - -# Add environment-specific overrides -awmg --config base.json --config staging.json -```text - -**Merge Behavior:** -- **MCP Servers**: Later configurations override servers with the same name -- **Gateway Settings**: Later configurations override gateway port and API key (if specified) -- **Example**: If `base.json` defines `server1` and `server2`, and `override.json` redefines `server2` and adds `server3`, the result will have all three servers with `server2` coming from `override.json` - -### Enable API Key Authentication - -```bash -awmg --config mcp-gateway-config.json --api-key secret123 -```text - -When API key authentication is enabled, clients must include the API key in the `Authorization` header: - -```bash -curl -H "Authorization: Bearer secret123" http://localhost:8088/... -```text - -### Write Debug Logs to File - -```bash -awmg --config mcp-gateway-config.json --log-dir /tmp/gateway-logs -```text - -This creates the specified directory and prepares it for logging output. - -### Combined Example - -```bash -awmg \ - --config base-config.json \ - --config override-config.json \ - --port 9000 \ - --api-key mySecretKey \ - --log-dir /var/log/mcp-gateway -```text - -### Enable Verbose Logging - -```bash -DEBUG=* awmg --config mcp-gateway-config.json -```text - -Or for specific modules: - -```bash -DEBUG=cli:mcp_gateway awmg --config mcp-gateway-config.json -```text - -## How It Works - -1. **Startup**: The gateway connects to all configured MCP servers -2. **Tool Discovery**: It lists all available tools from each server -3. **Name Resolution**: If tool names conflict, they're prefixed with the server name (e.g., `server1.tool-name`) -4. **HTTP Server**: An HTTP MCP server starts on the configured port -5. **Proxying**: Tool calls are routed to the appropriate backend server -6. **Response**: Results are returned to the client - -## Use Cases - -- **Unified Interface**: Access tools from multiple MCP servers through a single endpoint -- **Development**: Test multiple MCP servers together -- **Sandboxing**: Act as a gateway for MCP servers with the `sandbox.mcp` configuration -- **Tool Aggregation**: Combine tools from different sources into one interface - -## Troubleshooting - -### Connection Errors - -If a server fails to connect, the gateway will log the error and continue with other servers: - -```text -✗ failed to connect to MCP servers: failed to connect to some servers: [server test: failed to connect: calling "initialize": EOF] -```text - -### Port Already in Use - -If the port is already in use, try a different port: - -```bash -gh aw mcp-gateway --port 8081 mcp-gateway-config.json -```text - -### Tool Name Collisions - -If multiple servers expose tools with the same name, the gateway automatically prefixes them: - -- Original: `status` from `server1` and `server2` -- Result: `status` (first server) and `server2.status` (second server) diff --git a/examples/mcp-gateway-base.json b/examples/mcp-gateway-base.json deleted file mode 100644 index a3f3673dc2f..00000000000 --- a/examples/mcp-gateway-base.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "mcpServers": { - "gh-aw": { - "command": "gh", - "args": ["aw", "mcp-server"] - }, - "time": { - "command": "npx", - "args": ["-y", "@modelcontextprotocol/server-time"] - } - }, - "gateway": { - "port": 8088 - } -} diff --git a/examples/mcp-gateway-config.json b/examples/mcp-gateway-config.json deleted file mode 100644 index 742d8ef5c7f..00000000000 --- a/examples/mcp-gateway-config.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "mcpServers": { - "gh-aw": { - "command": "gh", - "args": ["aw", "mcp-server"] - } - }, - "port": 8088 -} diff --git a/examples/mcp-gateway-multi-server.json b/examples/mcp-gateway-multi-server.json deleted file mode 100644 index 6727fce5606..00000000000 --- a/examples/mcp-gateway-multi-server.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "mcpServers": { - "gh-aw": { - "command": "gh", - "args": ["aw", "mcp-server"], - "env": { - "DEBUG": "cli:*" - } - }, - "remote-server": { - "url": "http://localhost:3000" - }, - "docker-server": { - "container": "mcp-server:latest", - "args": ["--verbose"], - "env": { - "LOG_LEVEL": "debug" - } - } - }, - "port": 8088 -} diff --git a/examples/mcp-gateway-override.json b/examples/mcp-gateway-override.json deleted file mode 100644 index 122ce65e945..00000000000 --- a/examples/mcp-gateway-override.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "mcpServers": { - "time": { - "command": "npx", - "args": ["-y", "@modelcontextprotocol/server-time"], - "env": { - "DEBUG": "mcp:*" - } - }, - "memory": { - "command": "npx", - "args": ["-y", "@modelcontextprotocol/server-memory"] - } - }, - "gateway": { - "port": 9090, - "apiKey": "optional-api-key" - } -} diff --git a/install-awmg.sh b/install-awmg.sh deleted file mode 100755 index 23932728085..00000000000 --- a/install-awmg.sh +++ /dev/null @@ -1,387 +0,0 @@ -#!/bin/bash - -# Script to download and install awmg binary for the current OS and architecture -# Supports: Linux, macOS (Darwin), FreeBSD, Windows (Git Bash/MSYS/Cygwin) -# Usage: ./install-awmg.sh [version] -# If no version is specified, it will fetch and use the latest release -# Note: Checksum validation is currently skipped by default (will be enabled in future releases) -# Example: ./install-awmg.sh v1.0.0 - -set -e # Exit on any error - -# Parse arguments -SKIP_CHECKSUM=true # Default to true until checksums are available in releases -VERSION="" -for arg in "$@"; do - case $arg in - --skip-checksum) - SKIP_CHECKSUM=true - shift - ;; - *) - if [ -z "$VERSION" ]; then - VERSION="$arg" - fi - ;; - esac -done - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -# Function to print colored output -print_info() { - echo -e "${BLUE}[INFO]${NC} $1" -} - -print_success() { - echo -e "${GREEN}[SUCCESS]${NC} $1" -} - -print_warning() { - echo -e "${YELLOW}[WARNING]${NC} $1" -} - -print_error() { - echo -e "${RED}[ERROR]${NC} $1" -} - -# Check if HOME is set -if [ -z "$HOME" ]; then - print_error "HOME environment variable is not set. Cannot determine installation directory." - exit 1 -fi - -# Check if curl is available -if ! command -v curl &> /dev/null; then - print_error "curl is required but not installed. Please install curl first." - exit 1 -fi - -# Check if jq is available (optional, we'll use grep/sed as fallback) -HAS_JQ=false -if command -v jq &> /dev/null; then - HAS_JQ=true -fi - -# Check if sha256sum or shasum is available (for checksum verification) -HAS_CHECKSUM_TOOL=false -CHECKSUM_CMD="" -if command -v sha256sum &> /dev/null; then - HAS_CHECKSUM_TOOL=true - CHECKSUM_CMD="sha256sum" -elif command -v shasum &> /dev/null; then - HAS_CHECKSUM_TOOL=true - CHECKSUM_CMD="shasum -a 256" -fi - -if [ "$SKIP_CHECKSUM" = false ] && [ "$HAS_CHECKSUM_TOOL" = false ]; then - print_warning "Neither sha256sum nor shasum is available. Checksum verification will be skipped." - print_warning "To suppress this warning, use --skip-checksum flag." - SKIP_CHECKSUM=true -fi - -# Determine OS and architecture -OS=$(uname -s) -ARCH=$(uname -m) - -# Normalize OS name -case $OS in - Linux) - OS_NAME="linux" - ;; - Darwin) - OS_NAME="darwin" - ;; - FreeBSD) - OS_NAME="freebsd" - ;; - MINGW*|MSYS*|CYGWIN*) - OS_NAME="windows" - ;; - *) - print_error "Unsupported operating system: $OS" - print_info "Supported operating systems: Linux, macOS (Darwin), FreeBSD, Windows" - exit 1 - ;; -esac - -# Normalize architecture name -case $ARCH in - x86_64|amd64) - ARCH_NAME="amd64" - ;; - aarch64|arm64) - ARCH_NAME="arm64" - ;; - armv7l|armv7) - ARCH_NAME="arm" - ;; - i386|i686) - ARCH_NAME="386" - ;; - *) - print_error "Unsupported architecture: $ARCH" - print_info "Supported architectures: x86_64/amd64, aarch64/arm64, armv7l/arm, i386/i686" - exit 1 - ;; -esac - -# Construct platform string -PLATFORM="${OS_NAME}-${ARCH_NAME}" - -# Add .exe extension for Windows -if [ "$OS_NAME" = "windows" ]; then - BINARY_NAME="awmg.exe" -else - BINARY_NAME="awmg" -fi - -print_info "Detected OS: $OS -> $OS_NAME" -print_info "Detected architecture: $ARCH -> $ARCH_NAME" -print_info "Platform: $PLATFORM" - -# Function to fetch release data with fallback for invalid token and retry logic -fetch_release_data() { - local url=$1 - local max_retries=3 - local retry_delay=2 - local use_auth=false - - # Try with authentication if GH_TOKEN is set - if [ -n "$GH_TOKEN" ]; then - use_auth=true - fi - - # Retry loop - for attempt in $(seq 1 $max_retries); do - local curl_args=("-s" "-f") - - # Add auth header if using authentication - if [ "$use_auth" = true ]; then - curl_args+=("-H" "Authorization: Bearer $GH_TOKEN") - fi - - print_info "Fetching release data (attempt $attempt/$max_retries)..." >&2 - - # Make the API call - local response - response=$(curl "${curl_args[@]}" "$url" 2>/dev/null) - local exit_code=$? - - # Success - if [ $exit_code -eq 0 ] && [ -n "$response" ]; then - echo "$response" - return 0 - fi - - # If this was the first attempt with auth and it failed, try without auth - if [ "$attempt" -eq 1 ] && [ "$use_auth" = true ]; then - print_warning "API call with GH_TOKEN failed. Retrying without authentication..." >&2 - print_warning "Your GH_TOKEN may be incompatible (typically SSO) with this request." >&2 - use_auth=false - # Don't count this as a retry attempt, just switch auth mode - continue - fi - - # If we haven't exhausted retries, wait and try again - if [ "$attempt" -lt "$max_retries" ]; then - print_warning "Fetch attempt $attempt failed (exit code: $exit_code). Retrying in ${retry_delay}s..." >&2 - sleep $retry_delay - retry_delay=$((retry_delay * 2)) - else - print_error "Failed to fetch release data after $max_retries attempts" >&2 - fi - done - - return 1 -} - -# Get version (use provided version or fetch latest) -# VERSION is already set from argument parsing -REPO="githubnext/gh-aw" - -if [ -z "$VERSION" ]; then - print_info "No version specified, fetching latest release information from GitHub..." - - if ! LATEST_RELEASE=$(fetch_release_data "https://api.github.com/repos/$REPO/releases/latest"); then - print_error "Failed to fetch latest release information from GitHub API" - print_info "You can specify a version directly: ./install-awmg.sh v1.0.0" - exit 1 - fi - - if [ "$HAS_JQ" = true ]; then - # Use jq for JSON parsing - VERSION=$(echo "$LATEST_RELEASE" | jq -r '.tag_name') - RELEASE_NAME=$(echo "$LATEST_RELEASE" | jq -r '.name') - else - # Fallback to grep/sed - VERSION=$(echo "$LATEST_RELEASE" | grep '"tag_name"' | sed -E 's/.*"tag_name": *"([^"]+)".*/\1/') - RELEASE_NAME=$(echo "$LATEST_RELEASE" | grep '"name"' | sed -E 's/.*"name": *"([^"]+)".*/\1/') - fi - - if [ -z "$VERSION" ] || [ "$VERSION" = "null" ]; then - print_error "Failed to parse latest release information" - exit 1 - fi - - print_info "Latest release: $RELEASE_NAME ($VERSION)" -else - print_info "Using specified version: $VERSION" -fi - -# Construct download URL and paths -DOWNLOAD_URL="https://github.com/$REPO/releases/download/$VERSION/awmg-$PLATFORM" -CHECKSUMS_URL="https://github.com/$REPO/releases/download/$VERSION/checksums.txt" -if [ "$OS_NAME" = "windows" ]; then - DOWNLOAD_URL="${DOWNLOAD_URL}.exe" -fi -INSTALL_DIR="$HOME/.local/bin" -BINARY_PATH="$INSTALL_DIR/$BINARY_NAME" -CHECKSUMS_PATH="$INSTALL_DIR/checksums.txt" - -print_info "Download URL: $DOWNLOAD_URL" -print_info "Installation directory: $INSTALL_DIR" - -# Create the installation directory if it doesn't exist -if [ ! -d "$INSTALL_DIR" ]; then - print_info "Creating installation directory..." - mkdir -p "$INSTALL_DIR" -fi - -# Check if binary already exists -if [ -f "$BINARY_PATH" ]; then - print_warning "Binary '$BINARY_PATH' already exists. It will be overwritten." -fi - -# Download the binary with retry logic -print_info "Downloading awmg binary..." -MAX_RETRIES=3 -RETRY_DELAY=2 - -for attempt in $(seq 1 $MAX_RETRIES); do - if curl -L -f -o "$BINARY_PATH" "$DOWNLOAD_URL"; then - print_success "Binary downloaded successfully" - break - else - if [ "$attempt" -eq "$MAX_RETRIES" ]; then - print_error "Failed to download binary from $DOWNLOAD_URL after $MAX_RETRIES attempts" - print_info "Please check if the version and platform combination exists in the releases." - exit 1 - else - print_warning "Download attempt $attempt failed. Retrying in ${RETRY_DELAY}s..." - sleep $RETRY_DELAY - RETRY_DELAY=$((RETRY_DELAY * 2)) - fi - fi -done - -# Download and verify checksums if not skipped -if [ "$SKIP_CHECKSUM" = false ]; then - print_info "Downloading checksums file..." - CHECKSUMS_DOWNLOADED=false - - for attempt in $(seq 1 $MAX_RETRIES); do - if curl -L -f -o "$CHECKSUMS_PATH" "$CHECKSUMS_URL" 2>/dev/null; then - CHECKSUMS_DOWNLOADED=true - print_success "Checksums file downloaded successfully" - break - else - if [ "$attempt" -eq "$MAX_RETRIES" ]; then - print_warning "Failed to download checksums file after $MAX_RETRIES attempts" - print_warning "Checksum verification will be skipped for this version." - print_info "This may occur for older releases that don't include checksums." - break - else - print_warning "Checksum download attempt $attempt failed. Retrying in 2s..." - sleep 2 - fi - fi - done - - # Verify checksum if we downloaded it successfully - if [ "$CHECKSUMS_DOWNLOADED" = true ]; then - print_info "Verifying binary checksum..." - - # Determine the expected filename in the checksums file - EXPECTED_FILENAME="awmg-$PLATFORM" - if [ "$OS_NAME" = "windows" ]; then - EXPECTED_FILENAME="awmg-${PLATFORM}.exe" - fi - - # Extract the expected checksum from the checksums file - EXPECTED_CHECKSUM=$(grep "$EXPECTED_FILENAME" "$CHECKSUMS_PATH" | awk '{print $1}') - - if [ -z "$EXPECTED_CHECKSUM" ]; then - print_warning "Checksum for $EXPECTED_FILENAME not found in checksums file" - print_warning "Checksum verification will be skipped." - else - # Compute the actual checksum of the downloaded binary - ACTUAL_CHECKSUM=$($CHECKSUM_CMD "$BINARY_PATH" | awk '{print $1}') - - if [ "$ACTUAL_CHECKSUM" = "$EXPECTED_CHECKSUM" ]; then - print_success "Checksum verification passed!" - print_info "Expected: $EXPECTED_CHECKSUM" - print_info "Actual: $ACTUAL_CHECKSUM" - else - print_error "Checksum verification failed!" - print_error "Expected: $EXPECTED_CHECKSUM" - print_error "Actual: $ACTUAL_CHECKSUM" - print_error "The downloaded binary may be corrupted or tampered with." - print_info "To skip checksum verification, use: ./install-awmg.sh $VERSION --skip-checksum" - rm -f "$BINARY_PATH" - exit 1 - fi - fi - - # Clean up checksums file - rm -f "$CHECKSUMS_PATH" - fi -else - print_warning "Checksum verification skipped (--skip-checksum flag used)" -fi - -# Make it executable -print_info "Making binary executable..." -chmod +x "$BINARY_PATH" - -# Verify the binary -print_info "Verifying binary..." -if "$BINARY_PATH" --help > /dev/null 2>&1; then - print_success "Binary is working correctly!" -else - print_error "Binary verification failed. The downloaded file may be corrupted or incompatible." - exit 1 -fi - -# Show file info -FILE_SIZE=$(ls -lh "$BINARY_PATH" | awk '{print $5}') -print_success "Installation complete!" -print_info "Binary location: $BINARY_PATH" -print_info "Binary size: $FILE_SIZE" -print_info "Version: $VERSION" - -# Check if install dir is in PATH -if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then - print_warning "" - print_warning "The installation directory is not in your PATH." - print_warning "Add it to your PATH by adding this line to your shell profile:" - print_warning " export PATH=\"\$HOME/.local/bin:\$PATH\"" - print_warning "" -fi - -# Show usage info -print_info "" -print_info "You can now use awmg from the command line:" -print_info " awmg --help" -print_info " awmg --version" -print_info " awmg --config config.json" - -# Show version -print_info "" -print_info "Running awmg version check..." -"$BINARY_PATH" --version diff --git a/pkg/awmg/gateway.go b/pkg/awmg/gateway.go deleted file mode 100644 index 692fb742f9a..00000000000 --- a/pkg/awmg/gateway.go +++ /dev/null @@ -1,952 +0,0 @@ -package awmg - -import ( - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "os" - "os/exec" - "path/filepath" - "strings" - "sync" - "time" - - "github.com/githubnext/gh-aw/pkg/console" - "github.com/githubnext/gh-aw/pkg/logger" - "github.com/githubnext/gh-aw/pkg/parser" - "github.com/modelcontextprotocol/go-sdk/mcp" - "github.com/spf13/cobra" -) - -var gatewayLog = logger.New("awmg:gateway") - -// version is set by the main package. -var version = "dev" - -// SetVersionInfo sets the version information for the awmg package. -func SetVersionInfo(v string) { - version = v -} - -// GetVersion returns the current version. -func GetVersion() string { - return version -} - -// MCPGatewayServiceConfig represents the configuration for the MCP gateway service. -type MCPGatewayServiceConfig struct { - MCPServers map[string]parser.MCPServerConfig `json:"mcpServers"` - Gateway GatewaySettings `json:"gateway,omitempty"` -} - -// GatewaySettings represents gateway-specific settings. -type GatewaySettings struct { - Port int `json:"port,omitempty"` - APIKey string `json:"apiKey,omitempty"` - Domain string `json:"domain,omitempty"` // Domain for gateway URL (localhost or host.docker.internal) -} - -// MCPGatewayServer manages multiple MCP sessions and exposes them via HTTP -type MCPGatewayServer struct { - config *MCPGatewayServiceConfig - sessions map[string]*mcp.ClientSession - servers map[string]*mcp.Server // Proxy servers for each session - mu sync.RWMutex - logDir string -} - -// NewMCPGatewayCommand creates the mcp-gateway command -func NewMCPGatewayCommand() *cobra.Command { - var configFiles []string - var port int - var logDir string - - cmd := &cobra.Command{ - Use: "mcp-gateway", - Short: "Run an MCP gateway proxy that aggregates multiple MCP servers", - Long: `Run an MCP gateway that acts as a proxy to multiple MCP servers. - -The gateway: -- Integrates by default with the sandbox.mcp extension point -- Imports Claude/Copilot/Codex MCP server JSON configuration -- Starts each MCP server and mounts an MCP client on each -- Mounts an HTTP MCP server that acts as a gateway to the MCP clients -- Supports most MCP gestures through the go-MCP SDK -- Provides extensive logging to file in the MCP log folder - -Configuration can be provided via: -1. --config flag(s) pointing to JSON config file(s) (can be specified multiple times) -2. stdin (reads JSON configuration from standard input) - -Multiple config files are merged in order, with later files overriding earlier ones. - -Configuration format: -{ - "mcpServers": { - "server-name": { - "command": "command", - "args": ["arg1", "arg2"], - "env": {"KEY": "value"} - } - }, - "gateway": { - "port": 8080, - "apiKey": "optional-key" - } -} - -Examples: - awmg --config config.json # From single file - awmg --config base.json --config override.json # From multiple files (merged) - awmg --port 8080 # From stdin - echo '{"mcpServers":{...}}' | awmg # Pipe config - awmg --config config.json --log-dir /tmp/logs # Custom log dir`, - RunE: func(cmd *cobra.Command, args []string) error { - return runMCPGateway(configFiles, port, logDir) - }, - } - - cmd.Flags().StringArrayVarP(&configFiles, "config", "c", []string{}, "Path to MCP gateway configuration JSON file (can be specified multiple times)") - cmd.Flags().IntVarP(&port, "port", "p", 8080, "Port to run HTTP gateway on") - cmd.Flags().StringVar(&logDir, "log-dir", "/tmp/gh-aw/mcp-logs", "Directory for MCP gateway logs") - - return cmd -} - -// runMCPGateway starts the MCP gateway server -func runMCPGateway(configFiles []string, port int, logDir string) error { - fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Starting MCP gateway (port: %d, logDir: %s, configFiles: %v)", port, logDir, configFiles))) - gatewayLog.Printf("Starting MCP gateway on port %d", port) - - // Read configuration - config, originalConfigPath, err := readGatewayConfig(configFiles) - if err != nil { - fmt.Fprintln(os.Stderr, console.FormatErrorMessage(fmt.Sprintf("Failed to read configuration: %v", err))) - return fmt.Errorf("failed to read gateway configuration: %w", err) - } - - // Override port if specified in command line - if port > 0 { - config.Gateway.Port = port - } else if config.Gateway.Port == 0 { - config.Gateway.Port = 8080 // Default port - } - - // Create log directory - if err := os.MkdirAll(logDir, 0755); err != nil { - return fmt.Errorf("failed to create log directory: %w", err) - } - - // Create gateway server - gateway := &MCPGatewayServer{ - config: config, - sessions: make(map[string]*mcp.ClientSession), - servers: make(map[string]*mcp.Server), - logDir: logDir, - } - - // Initialize MCP sessions for each server - if err := gateway.initializeSessions(); err != nil { - return fmt.Errorf("failed to initialize MCP sessions: %w", err) - } - - // Rewrite the MCP config file to point servers to the gateway - if originalConfigPath != "" { - if err := rewriteMCPConfigForGateway(originalConfigPath, config); err != nil { - gatewayLog.Printf("Warning: Failed to rewrite MCP config: %v", err) - fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Warning: Failed to rewrite MCP config: %v", err))) - // Don't fail - gateway can still run - } - } else { - gatewayLog.Print("Skipping config rewrite (config was read from stdin)") - fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Skipping config rewrite (config was read from stdin)")) - } - - // Start HTTP server - return gateway.startHTTPServer() -} - -// readGatewayConfig reads the gateway configuration from files or stdin -// Returns the config, the path to the first config file (for rewriting), and any error -func readGatewayConfig(configFiles []string) (*MCPGatewayServiceConfig, string, error) { - var configs []*MCPGatewayServiceConfig - var originalConfigPath string - - if len(configFiles) > 0 { - // Read from file(s) - for i, configFile := range configFiles { - gatewayLog.Printf("Reading configuration from file: %s", configFile) - fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Reading configuration from file: %s", configFile))) - - // Store the first config file path for rewriting - if i == 0 { - originalConfigPath = configFile - } - - // Check if file exists - if _, err := os.Stat(configFile); os.IsNotExist(err) { - fmt.Fprintln(os.Stderr, console.FormatErrorMessage(fmt.Sprintf("Configuration file not found: %s", configFile))) - gatewayLog.Printf("Configuration file not found: %s", configFile) - return nil, "", fmt.Errorf("configuration file not found: %s", configFile) - } - - data, err := os.ReadFile(configFile) - if err != nil { - fmt.Fprintln(os.Stderr, console.FormatErrorMessage(fmt.Sprintf("Failed to read config file: %v", err))) - return nil, "", fmt.Errorf("failed to read config file: %w", err) - } - fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Read %d bytes from file", len(data)))) - gatewayLog.Printf("Read %d bytes from file", len(data)) - - // Validate we have data - if len(data) == 0 { - fmt.Fprintln(os.Stderr, console.FormatErrorMessage("ERROR: Configuration data is empty")) - gatewayLog.Print("Configuration data is empty") - return nil, "", fmt.Errorf("configuration data is empty") - } - - config, err := parseGatewayConfig(data) - if err != nil { - return nil, "", err - } - - configs = append(configs, config) - } - } else { - // Read from stdin - gatewayLog.Print("Reading configuration from stdin") - fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Reading configuration from stdin...")) - data, err := io.ReadAll(os.Stdin) - if err != nil { - fmt.Fprintln(os.Stderr, console.FormatErrorMessage(fmt.Sprintf("Failed to read from stdin: %v", err))) - return nil, "", fmt.Errorf("failed to read from stdin: %w", err) - } - fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Read %d bytes from stdin", len(data)))) - gatewayLog.Printf("Read %d bytes from stdin", len(data)) - - if len(data) == 0 { - fmt.Fprintln(os.Stderr, console.FormatErrorMessage("ERROR: No configuration data received from stdin")) - fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Please provide configuration via --config flag or pipe JSON to stdin")) - gatewayLog.Print("No data received from stdin") - return nil, "", fmt.Errorf("no configuration data received from stdin") - } - - // Validate we have data - if len(data) == 0 { - fmt.Fprintln(os.Stderr, console.FormatErrorMessage("ERROR: Configuration data is empty")) - gatewayLog.Print("Configuration data is empty") - return nil, "", fmt.Errorf("configuration data is empty") - } - - config, err := parseGatewayConfig(data) - if err != nil { - return nil, "", err - } - - configs = append(configs, config) - // No config file path when reading from stdin - originalConfigPath = "" - } - - // Merge all configs - if len(configs) == 0 { - return nil, "", fmt.Errorf("no configuration loaded") - } - - mergedConfig := configs[0] - for i := 1; i < len(configs); i++ { - gatewayLog.Printf("Merging configuration %d of %d", i+1, len(configs)) - fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Merging configuration %d of %d", i+1, len(configs)))) - mergedConfig = mergeConfigs(mergedConfig, configs[i]) - } - - gatewayLog.Printf("Successfully merged %d configuration(s)", len(configs)) - if len(configs) > 1 { - fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Successfully merged %d configurations", len(configs)))) - } - - gatewayLog.Printf("Loaded configuration with %d MCP servers", len(mergedConfig.MCPServers)) - fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Successfully loaded configuration with %d MCP servers", len(mergedConfig.MCPServers)))) - - // Validate we have at least one server configured - if len(mergedConfig.MCPServers) == 0 { - fmt.Fprintln(os.Stderr, console.FormatErrorMessage("ERROR: No MCP servers configured in configuration")) - fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Configuration must include at least one MCP server in 'mcpServers' section")) - gatewayLog.Print("No MCP servers configured") - return nil, "", fmt.Errorf("no MCP servers configured in configuration") - } - - // Log server names for debugging - serverNames := make([]string, 0, len(mergedConfig.MCPServers)) - for name := range mergedConfig.MCPServers { - serverNames = append(serverNames, name) - } - fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("MCP servers configured: %v", serverNames))) - gatewayLog.Printf("MCP servers configured: %v", serverNames) - - return mergedConfig, originalConfigPath, nil -} - -// parseGatewayConfig parses raw JSON data into a gateway config -func parseGatewayConfig(data []byte) (*MCPGatewayServiceConfig, error) { - gatewayLog.Printf("Parsing %d bytes of configuration data", len(data)) - fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Parsing %d bytes of configuration data", len(data)))) - - var config MCPGatewayServiceConfig - if err := json.Unmarshal(data, &config); err != nil { - fmt.Fprintln(os.Stderr, console.FormatErrorMessage(fmt.Sprintf("Failed to parse JSON: %v", err))) - fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Data received (first 500 chars): %s", string(data[:min(500, len(data))])))) - gatewayLog.Printf("Failed to parse JSON: %v", err) - return nil, fmt.Errorf("failed to parse configuration JSON: %w", err) - } - - gatewayLog.Printf("Successfully parsed JSON configuration") - - // Apply environment variable expansion to all server configurations - // This supports ${VAR} or $VAR patterns in URLs, headers, and env values - expandedServers := make(map[string]parser.MCPServerConfig) - for name, serverConfig := range config.MCPServers { - // Expand URL field - if serverConfig.URL != "" { - serverConfig.URL = os.ExpandEnv(serverConfig.URL) - gatewayLog.Printf("Expanded URL for server %s: %s", name, serverConfig.URL) - } - - // Expand headers - if len(serverConfig.Headers) > 0 { - expandedHeaders := make(map[string]string) - for key, value := range serverConfig.Headers { - expandedHeaders[key] = os.ExpandEnv(value) - } - serverConfig.Headers = expandedHeaders - gatewayLog.Printf("Expanded %d headers for server %s", len(expandedHeaders), name) - } - - // Expand environment variables - if len(serverConfig.Env) > 0 { - expandedEnv := make(map[string]string) - for key, value := range serverConfig.Env { - expandedEnv[key] = os.ExpandEnv(value) - } - serverConfig.Env = expandedEnv - gatewayLog.Printf("Expanded %d env vars for server %s", len(expandedEnv), name) - } - - expandedServers[name] = serverConfig - } - config.MCPServers = expandedServers - - return &config, nil -} - -// mergeConfigs merges two gateway configurations, with the second overriding the first -func mergeConfigs(base, override *MCPGatewayServiceConfig) *MCPGatewayServiceConfig { - result := &MCPGatewayServiceConfig{ - MCPServers: make(map[string]parser.MCPServerConfig), - Gateway: base.Gateway, - } - - // Copy all servers from base - for name, config := range base.MCPServers { - result.MCPServers[name] = config - } - - // Override/add servers from override config - for name, config := range override.MCPServers { - gatewayLog.Printf("Merging server config for: %s", name) - result.MCPServers[name] = config - } - - // Override gateway settings if provided - if override.Gateway.Port != 0 { - result.Gateway.Port = override.Gateway.Port - gatewayLog.Printf("Override gateway port: %d", override.Gateway.Port) - } - if override.Gateway.APIKey != "" { - result.Gateway.APIKey = override.Gateway.APIKey - gatewayLog.Printf("Override gateway API key (length: %d)", len(override.Gateway.APIKey)) - } - - return result -} - -// rewriteMCPConfigForGateway rewrites the MCP config file to point all servers to the gateway -func rewriteMCPConfigForGateway(configPath string, config *MCPGatewayServiceConfig) error { - // Sanitize the path to prevent path traversal attacks - cleanPath := filepath.Clean(configPath) - if !filepath.IsAbs(cleanPath) { - gatewayLog.Printf("Invalid config file path (not absolute): %s", configPath) - return fmt.Errorf("config path must be absolute: %s", configPath) - } - - gatewayLog.Printf("Rewriting MCP config file: %s", cleanPath) - fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Rewriting MCP config file: %s", cleanPath))) - - // Read the original config file to preserve non-proxied servers - gatewayLog.Printf("Reading original config from %s", cleanPath) - // #nosec G304 - cleanPath is validated: sanitized with filepath.Clean() and verified to be absolute path (lines 377-381) - originalConfigData, err := os.ReadFile(cleanPath) - if err != nil { - gatewayLog.Printf("Failed to read original config: %v", err) - fmt.Fprintln(os.Stderr, console.FormatErrorMessage(fmt.Sprintf("Failed to read original config: %v", err))) - return fmt.Errorf("failed to read original config: %w", err) - } - - var originalConfig map[string]any - if err := json.Unmarshal(originalConfigData, &originalConfig); err != nil { - gatewayLog.Printf("Failed to parse original config: %v", err) - fmt.Fprintln(os.Stderr, console.FormatErrorMessage(fmt.Sprintf("Failed to parse original config: %v", err))) - return fmt.Errorf("failed to parse original config: %w", err) - } - - port := config.Gateway.Port - if port == 0 { - port = 8080 - } - - // Determine the domain for the gateway URL - // Use the configured domain, or default to localhost - domain := config.Gateway.Domain - if domain == "" { - domain = "localhost" - gatewayLog.Print("No domain configured, defaulting to localhost") - } - - // Use configured domain since the rewritten config is consumed by Copilot CLI - // Domain is either localhost (firewall disabled) or host.docker.internal (firewall enabled) - gatewayURL := fmt.Sprintf("http://%s:%d", domain, port) - - gatewayLog.Printf("Gateway URL: %s (domain: %s)", gatewayURL, domain) - fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Gateway URL: %s", gatewayURL))) - - // Get original mcpServers to preserve non-proxied servers - var originalMCPServers map[string]any - if servers, ok := originalConfig["mcpServers"].(map[string]any); ok { - originalMCPServers = servers - gatewayLog.Printf("Found %d servers in original config", len(originalMCPServers)) - fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Found %d servers in original config", len(originalMCPServers)))) - } else { - originalMCPServers = make(map[string]any) - gatewayLog.Print("No mcpServers found in original config, starting with empty map") - fmt.Fprintln(os.Stderr, console.FormatInfoMessage("No mcpServers found in original config")) - } - - // Create merged config with rewritten proxied servers and preserved non-proxied servers - rewrittenConfig := make(map[string]any) - mcpServers := make(map[string]any) - - // Track which servers are rewritten vs ignored for summary logging - var rewrittenServers []string - var ignoredServers []string - - // First, copy all servers from original (preserves non-proxied servers like safeinputs/safeoutputs) - gatewayLog.Printf("Copying %d servers from original config to preserve non-proxied servers", len(originalMCPServers)) - for serverName, serverConfig := range originalMCPServers { - mcpServers[serverName] = serverConfig - gatewayLog.Printf(" Preserved server: %s", serverName) - - // Track if this server will be ignored (not rewritten) - if _, willBeRewritten := config.MCPServers[serverName]; !willBeRewritten { - ignoredServers = append(ignoredServers, serverName) - } - } - - gatewayLog.Printf("Transforming %d proxied servers to point to gateway", len(config.MCPServers)) - fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Transforming %d proxied servers to point to gateway", len(config.MCPServers)))) - - // Then, overwrite with gateway URLs for proxied servers only - for serverName := range config.MCPServers { - serverURL := fmt.Sprintf("%s/mcp/%s", gatewayURL, serverName) - - gatewayLog.Printf("Rewriting server '%s' to use gateway URL: %s", serverName, serverURL) - fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf(" %s -> %s", serverName, serverURL))) - - serverConfig := map[string]any{ - "type": "http", - "url": serverURL, - "tools": []string{"*"}, - } - - // Add authentication header if API key is configured - if config.Gateway.APIKey != "" { - gatewayLog.Printf("Adding authorization header for server '%s'", serverName) - serverConfig["headers"] = map[string]any{ - "Authorization": fmt.Sprintf("Bearer %s", config.Gateway.APIKey), - } - } - - mcpServers[serverName] = serverConfig - rewrittenServers = append(rewrittenServers, serverName) - } - - rewrittenConfig["mcpServers"] = mcpServers - - // Do NOT include gateway section in rewritten config (per requirement) - gatewayLog.Print("Gateway section removed from rewritten config") - - // Log summary of servers rewritten vs ignored - gatewayLog.Printf("Server summary: %d rewritten, %d ignored, %d total", len(rewrittenServers), len(ignoredServers), len(mcpServers)) - fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Server summary: %d rewritten, %d ignored", len(rewrittenServers), len(ignoredServers)))) - - if len(rewrittenServers) > 0 { - gatewayLog.Printf("Servers rewritten (proxied through gateway):") - fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Servers rewritten (proxied through gateway):")) - for _, serverName := range rewrittenServers { - gatewayLog.Printf(" - %s", serverName) - fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf(" - %s", serverName))) - } - } - - if len(ignoredServers) > 0 { - gatewayLog.Printf("Servers ignored (preserved as-is):") - fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Servers ignored (preserved as-is):")) - for _, serverName := range ignoredServers { - gatewayLog.Printf(" - %s", serverName) - fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf(" - %s", serverName))) - } - } - - // Marshal to JSON with indentation - data, err := json.MarshalIndent(rewrittenConfig, "", " ") - if err != nil { - gatewayLog.Printf("Failed to marshal rewritten config: %v", err) - fmt.Fprintln(os.Stderr, console.FormatErrorMessage(fmt.Sprintf("Failed to marshal rewritten config: %v", err))) - return fmt.Errorf("failed to marshal rewritten config: %w", err) - } - - gatewayLog.Printf("Marshaled config to JSON: %d bytes", len(data)) - gatewayLog.Printf("Writing to file: %s", cleanPath) - fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Writing %d bytes to config file: %s", len(data), cleanPath))) - - // Log a preview of the config being written (first 500 chars, redacting sensitive data) - preview := string(data) - if len(preview) > 500 { - preview = preview[:500] + "..." - } - // Redact any Bearer tokens in the preview - preview = strings.ReplaceAll(preview, config.Gateway.APIKey, "******") - gatewayLog.Printf("Config preview (redacted): %s", preview) - - // Write back to file with restricted permissions (0600) since it contains sensitive API keys - gatewayLog.Printf("Writing file with permissions 0600 (owner read/write only)") - // #nosec G304 - cleanPath is validated: sanitized with filepath.Clean() and verified to be absolute path (lines 377-381) - if err := os.WriteFile(cleanPath, data, 0600); err != nil { - gatewayLog.Printf("Failed to write rewritten config to %s: %v", cleanPath, err) - fmt.Fprintln(os.Stderr, console.FormatErrorMessage(fmt.Sprintf("Failed to write rewritten config: %v", err))) - return fmt.Errorf("failed to write rewritten config: %w", err) - } - - gatewayLog.Printf("Successfully wrote config file: %s", cleanPath) - - // Self-check: Read back the file and verify it was written correctly - gatewayLog.Print("Performing self-check: verifying config was written correctly") - // #nosec G304 - cleanPath is validated: sanitized with filepath.Clean() and verified to be absolute path (lines 377-381) - verifyData, err := os.ReadFile(cleanPath) - if err != nil { - gatewayLog.Printf("Self-check failed: could not read back config file: %v", err) - fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Warning: Could not verify config was written: %v", err))) - } else { - var verifyConfig map[string]any - if err := json.Unmarshal(verifyData, &verifyConfig); err != nil { - gatewayLog.Printf("Self-check failed: could not parse config: %v", err) - fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Warning: Could not parse rewritten config: %v", err))) - } else { - // Verify mcpServers section exists - verifyServers, ok := verifyConfig["mcpServers"].(map[string]any) - if !ok { - gatewayLog.Print("Self-check failed: mcpServers section missing or invalid") - fmt.Fprintln(os.Stderr, console.FormatErrorMessage("ERROR: Self-check failed - mcpServers section missing")) - return fmt.Errorf("self-check failed: mcpServers section missing after rewrite") - } - - // Verify all proxied servers were rewritten correctly - verificationErrors := []string{} - for serverName := range config.MCPServers { - serverConfig, ok := verifyServers[serverName].(map[string]any) - if !ok { - verificationErrors = append(verificationErrors, fmt.Sprintf("Server '%s' missing from rewritten config", serverName)) - continue - } - - // Check that server has correct type and URL - serverType, hasType := serverConfig["type"].(string) - serverURL, hasURL := serverConfig["url"].(string) - - if !hasType || serverType != "http" { - verificationErrors = append(verificationErrors, fmt.Sprintf("Server '%s' missing 'type: http' field", serverName)) - } - - if !hasURL || !strings.Contains(serverURL, gatewayURL) { - verificationErrors = append(verificationErrors, fmt.Sprintf("Server '%s' URL does not point to gateway", serverName)) - } - } - - if len(verificationErrors) > 0 { - gatewayLog.Printf("Self-check found %d verification errors", len(verificationErrors)) - fmt.Fprintln(os.Stderr, console.FormatErrorMessage(fmt.Sprintf("ERROR: Self-check found %d verification errors:", len(verificationErrors)))) - for _, errMsg := range verificationErrors { - gatewayLog.Printf(" - %s", errMsg) - fmt.Fprintln(os.Stderr, console.FormatErrorMessage(fmt.Sprintf(" - %s", errMsg))) - } - return fmt.Errorf("self-check failed: config rewrite verification errors") - } - - gatewayLog.Printf("Self-check passed: all %d proxied servers correctly rewritten", len(config.MCPServers)) - fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("✓ Self-check passed: all %d proxied servers correctly rewritten", len(config.MCPServers)))) - } - } - - gatewayLog.Printf("Successfully rewrote MCP config file") - fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Successfully rewrote MCP config: %s", configPath))) - fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf(" %d proxied servers now point to gateway at %s", len(config.MCPServers), gatewayURL))) - fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf(" %d total servers in config", len(mcpServers)))) - - return nil -} - -// initializeSessions creates MCP sessions for all configured servers -func (g *MCPGatewayServer) initializeSessions() error { - fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Initializing %d MCP sessions", len(g.config.MCPServers)))) - gatewayLog.Printf("Initializing %d MCP sessions", len(g.config.MCPServers)) - - // This should never happen as we validate in readGatewayConfig, but double-check - if len(g.config.MCPServers) == 0 { - fmt.Fprintln(os.Stderr, console.FormatErrorMessage("ERROR: No MCP servers to initialize")) - gatewayLog.Print("No MCP servers to initialize") - return fmt.Errorf("no MCP servers configured") - } - - successCount := 0 - for serverName, serverConfig := range g.config.MCPServers { - gatewayLog.Printf("Initializing session for server: %s", serverName) - fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Initializing session for server: %s (command: %s, args: %v)", serverName, serverConfig.Command, serverConfig.Args))) - - session, err := g.createMCPSession(serverName, serverConfig) - if err != nil { - gatewayLog.Printf("Failed to initialize session for %s: %v", serverName, err) - fmt.Fprintln(os.Stderr, console.FormatErrorMessage(fmt.Sprintf("Failed to initialize session for %s: %v", serverName, err))) - return fmt.Errorf("failed to create session for server %s: %w", serverName, err) - } - - g.mu.Lock() - g.sessions[serverName] = session - g.mu.Unlock() - - // Create a proxy MCP server that forwards calls to this session - proxyServer := g.createProxyServer(serverName, session) - g.mu.Lock() - g.servers[serverName] = proxyServer - g.mu.Unlock() - - successCount++ - gatewayLog.Printf("Successfully initialized session for %s (%d/%d)", serverName, successCount, len(g.config.MCPServers)) - fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Successfully initialized session for %s (%d/%d)", serverName, successCount, len(g.config.MCPServers)))) - } - - fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("All %d MCP sessions initialized successfully", len(g.config.MCPServers)))) - gatewayLog.Printf("All %d MCP sessions initialized successfully", len(g.config.MCPServers)) - return nil -} - -// createMCPSession creates an MCP session for a single server configuration -func (g *MCPGatewayServer) createMCPSession(serverName string, config parser.MCPServerConfig) (*mcp.ClientSession, error) { - // Create log file for this server (flat directory structure) - logFile := filepath.Join(g.logDir, fmt.Sprintf("%s.log", serverName)) - gatewayLog.Printf("Creating log file for %s: %s", serverName, logFile) - fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Creating log file for %s: %s", serverName, logFile))) - - logFd, err := os.Create(logFile) - if err != nil { - gatewayLog.Printf("Failed to create log file for %s: %v", serverName, err) - return nil, fmt.Errorf("failed to create log file: %w", err) - } - defer logFd.Close() - - gatewayLog.Printf("Log file created successfully for %s", serverName) - - // Handle different server types - if config.URL != "" { - // Streamable HTTP transport using the go-sdk StreamableClientTransport - gatewayLog.Printf("Creating streamable HTTP client for %s at %s", serverName, config.URL) - fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Using streamable HTTP transport: %s", config.URL))) - - // Create streamable client transport - transport := &mcp.StreamableClientTransport{ - Endpoint: config.URL, - } - - gatewayLog.Printf("Creating MCP client for %s", serverName) - client := mcp.NewClient(&mcp.Implementation{ - Name: fmt.Sprintf("gateway-client-%s", serverName), - Version: GetVersion(), - }, nil) - - gatewayLog.Printf("Connecting to MCP server %s with 30s timeout", serverName) - fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Connecting to %s...", serverName))) - - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - session, err := client.Connect(ctx, transport, nil) - if err != nil { - gatewayLog.Printf("Failed to connect to HTTP server %s: %v", serverName, err) - fmt.Fprintln(os.Stderr, console.FormatErrorMessage(fmt.Sprintf("Connection failed for %s: %v", serverName, err))) - return nil, fmt.Errorf("failed to connect to HTTP server: %w", err) - } - - gatewayLog.Printf("Successfully connected to MCP server %s via streamable HTTP", serverName) - fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Connected to %s successfully via streamable HTTP", serverName))) - return session, nil - } else if config.Command != "" { - // Command transport (subprocess with stdio) - gatewayLog.Printf("Creating command client for %s with command: %s %v", serverName, config.Command, config.Args) - fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Using command transport: %s %v", config.Command, config.Args))) - - // Create command with environment variables - cmd := exec.Command(config.Command, config.Args...) - if len(config.Env) > 0 { - gatewayLog.Printf("Setting %d environment variables for %s", len(config.Env), serverName) - cmd.Env = os.Environ() - for k, v := range config.Env { - cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, v)) - gatewayLog.Printf("Env var for %s: %s=%s", serverName, k, v) - } - } - - // Create command transport - gatewayLog.Printf("Creating CommandTransport for %s", serverName) - transport := &mcp.CommandTransport{ - Command: cmd, - } - - gatewayLog.Printf("Creating MCP client for %s", serverName) - client := mcp.NewClient(&mcp.Implementation{ - Name: fmt.Sprintf("gateway-client-%s", serverName), - Version: GetVersion(), - }, nil) - - gatewayLog.Printf("Connecting to MCP server %s with 30s timeout", serverName) - fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Connecting to %s...", serverName))) - - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - session, err := client.Connect(ctx, transport, nil) - if err != nil { - gatewayLog.Printf("Failed to connect to command server %s: %v", serverName, err) - fmt.Fprintln(os.Stderr, console.FormatErrorMessage(fmt.Sprintf("Connection failed for %s: %v", serverName, err))) - return nil, fmt.Errorf("failed to connect to command server: %w", err) - } - - gatewayLog.Printf("Successfully connected to MCP server %s", serverName) - fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Connected to %s successfully", serverName))) - return session, nil - } else if config.Container != "" { - // Docker container (not yet implemented) - gatewayLog.Printf("Docker container requested for %s but not yet implemented", serverName) - fmt.Fprintln(os.Stderr, console.FormatErrorMessage(fmt.Sprintf("Docker container support not available for %s", serverName))) - return nil, fmt.Errorf("docker container support not yet implemented") - } - - gatewayLog.Printf("Invalid server configuration for %s: no command, url, or container specified", serverName) - fmt.Fprintln(os.Stderr, console.FormatErrorMessage(fmt.Sprintf("Invalid configuration for %s: must specify command, url, or container", serverName))) - return nil, fmt.Errorf("invalid server configuration: must specify command, url, or container") -} - -// createProxyServer creates a proxy MCP server that forwards all calls to the backend session -func (g *MCPGatewayServer) createProxyServer(serverName string, session *mcp.ClientSession) *mcp.Server { - gatewayLog.Printf("Creating proxy MCP server for %s", serverName) - - // Create a server that will proxy requests to the backend session - server := mcp.NewServer(&mcp.Implementation{ - Name: fmt.Sprintf("gateway-proxy-%s", serverName), - Version: GetVersion(), - }, &mcp.ServerOptions{ - Capabilities: &mcp.ServerCapabilities{ - Tools: &mcp.ToolCapabilities{ - ListChanged: false, - }, - Resources: &mcp.ResourceCapabilities{ - Subscribe: false, - ListChanged: false, - }, - Prompts: &mcp.PromptCapabilities{ - ListChanged: false, - }, - }, - Logger: logger.NewSlogLoggerWithHandler(gatewayLog), - }) - - // Query backend for its tools and register them on the proxy server - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - // List tools from backend - toolsResult, err := session.ListTools(ctx, &mcp.ListToolsParams{}) - if err != nil { - gatewayLog.Printf("Warning: Failed to list tools from backend %s: %v", serverName, err) - } else { - // Register each tool on the proxy server - for _, tool := range toolsResult.Tools { - toolCopy := tool // Capture for closure - gatewayLog.Printf("Registering tool %s from backend %s", tool.Name, serverName) - - server.AddTool(toolCopy, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { - gatewayLog.Printf("Proxy %s: Calling tool %s on backend", serverName, req.Params.Name) - return session.CallTool(ctx, &mcp.CallToolParams{ - Name: req.Params.Name, - Arguments: req.Params.Arguments, - }) - }) - } - gatewayLog.Printf("Registered %d tools from backend %s", len(toolsResult.Tools), serverName) - } - - // List resources from backend - resourcesResult, err := session.ListResources(ctx, &mcp.ListResourcesParams{}) - if err != nil { - gatewayLog.Printf("Warning: Failed to list resources from backend %s: %v", serverName, err) - } else { - // Register each resource on the proxy server - for _, resource := range resourcesResult.Resources { - resourceCopy := resource // Capture for closure - gatewayLog.Printf("Registering resource %s from backend %s", resource.URI, serverName) - - server.AddResource(resourceCopy, func(ctx context.Context, req *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { - gatewayLog.Printf("Proxy %s: Reading resource %s from backend", serverName, req.Params.URI) - return session.ReadResource(ctx, &mcp.ReadResourceParams{ - URI: req.Params.URI, - }) - }) - } - gatewayLog.Printf("Registered %d resources from backend %s", len(resourcesResult.Resources), serverName) - } - - // List prompts from backend - promptsResult, err := session.ListPrompts(ctx, &mcp.ListPromptsParams{}) - if err != nil { - gatewayLog.Printf("Warning: Failed to list prompts from backend %s: %v", serverName, err) - } else { - // Register each prompt on the proxy server - for _, prompt := range promptsResult.Prompts { - promptCopy := prompt // Capture for closure - gatewayLog.Printf("Registering prompt %s from backend %s", prompt.Name, serverName) - - server.AddPrompt(promptCopy, func(ctx context.Context, req *mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { - gatewayLog.Printf("Proxy %s: Getting prompt %s from backend", serverName, req.Params.Name) - return session.GetPrompt(ctx, &mcp.GetPromptParams{ - Name: req.Params.Name, - Arguments: req.Params.Arguments, - }) - }) - } - gatewayLog.Printf("Registered %d prompts from backend %s", len(promptsResult.Prompts), serverName) - } - - gatewayLog.Printf("Proxy MCP server created for %s", serverName) - return server -} - -// startHTTPServer starts the HTTP server for the gateway -func (g *MCPGatewayServer) startHTTPServer() error { - port := g.config.Gateway.Port - gatewayLog.Printf("Starting HTTP server on port %d", port) - - mux := http.NewServeMux() - - // Health check endpoint - mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - fmt.Fprintf(w, "OK") - }) - - // List servers endpoint - mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { - g.handleListServers(w, r) - }) - - // Create StreamableHTTPHandler for each MCP server - for serverName := range g.config.MCPServers { - serverNameCopy := serverName // Capture for closure - path := fmt.Sprintf("/mcp/%s", serverName) - gatewayLog.Printf("Registering StreamableHTTPHandler endpoint: %s", path) - fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Registering StreamableHTTPHandler endpoint: %s", path))) - - // Create streamable HTTP handler for this server - handler := mcp.NewStreamableHTTPHandler(func(req *http.Request) *mcp.Server { - // Get the proxy server for this backend - g.mu.RLock() - defer g.mu.RUnlock() - server, exists := g.servers[serverNameCopy] - if !exists { - gatewayLog.Printf("Server not found in handler: %s", serverNameCopy) - return nil - } - gatewayLog.Printf("Returning proxy server for: %s", serverNameCopy) - return server - }, &mcp.StreamableHTTPOptions{ - SessionTimeout: 2 * time.Hour, // Close idle sessions after 2 hours - Logger: logger.NewSlogLoggerWithHandler(gatewayLog), - }) - - // Add authentication middleware if API key is configured - if g.config.Gateway.APIKey != "" { - wrappedHandler := g.withAuth(handler, serverNameCopy) - mux.Handle(path, wrappedHandler) - } else { - mux.Handle(path, handler) - } - } - - httpServer := &http.Server{ - Addr: fmt.Sprintf(":%d", port), - Handler: mux, - ReadHeaderTimeout: 30 * time.Second, - ReadTimeout: 30 * time.Second, - WriteTimeout: 30 * time.Second, - } - - fmt.Fprintf(os.Stderr, "%s\n", console.FormatSuccessMessage(fmt.Sprintf("MCP gateway listening on http://localhost:%d", port))) - fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Using StreamableHTTPHandler for MCP protocol")) - gatewayLog.Printf("HTTP server ready on port %d with StreamableHTTPHandler", port) - - return httpServer.ListenAndServe() -} - -// withAuth wraps an HTTP handler with authentication if API key is configured -func (g *MCPGatewayServer) withAuth(handler http.Handler, serverName string) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - authHeader := r.Header.Get("Authorization") - expectedAuth := fmt.Sprintf("Bearer %s", g.config.Gateway.APIKey) - if authHeader != expectedAuth { - gatewayLog.Printf("Unauthorized request for %s", serverName) - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return - } - handler.ServeHTTP(w, r) - }) -} - -// handleListServers handles the /servers endpoint -func (g *MCPGatewayServer) handleListServers(w http.ResponseWriter, r *http.Request) { - gatewayLog.Print("Handling list servers request") - - g.mu.RLock() - servers := make([]string, 0, len(g.sessions)) - for name := range g.sessions { - servers = append(servers, name) - } - g.mu.RUnlock() - - response := map[string]any{ - "servers": servers, - "count": len(servers), - } - - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(response); err != nil { - gatewayLog.Printf("Failed to encode JSON response: %v", err) - } -} diff --git a/pkg/awmg/gateway_inspect_integration_test.go b/pkg/awmg/gateway_inspect_integration_test.go deleted file mode 100644 index 8b7e3333d50..00000000000 --- a/pkg/awmg/gateway_inspect_integration_test.go +++ /dev/null @@ -1,317 +0,0 @@ -//go:build integration - -package awmg - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "os" - "os/exec" - "path/filepath" - "strings" - "testing" - "time" - - "github.com/githubnext/gh-aw/pkg/parser" - "github.com/githubnext/gh-aw/pkg/types" -) - -// TestMCPGateway_InspectWithPlaywright tests the MCP gateway by: -// 1. Starting the gateway with a test configuration -// 2. Using mcp inspect to verify the gateway configuration -// 3. Checking the tool list is accessible -func TestMCPGateway_InspectWithPlaywright(t *testing.T) { - // Get absolute path to binary - binaryPath, err := filepath.Abs(filepath.Join("..", "..", "gh-aw")) - if err != nil { - t.Fatalf("Failed to get absolute path: %v", err) - } - - if _, err := os.Stat(binaryPath); os.IsNotExist(err) { - t.Skipf("Skipping test: gh-aw binary not found at %s. Run 'make build' first.", binaryPath) - } - - // Create temporary directory structure - tmpDir := t.TempDir() - workflowsDir := filepath.Join(tmpDir, ".github", "workflows") - if err := os.MkdirAll(workflowsDir, 0755); err != nil { - t.Fatalf("Failed to create workflows directory: %v", err) - } - - // Create a test workflow that uses the MCP gateway - workflowContent := `--- -on: workflow_dispatch -permissions: - contents: read -engine: copilot -sandbox: - mcp: - port: 8089 -tools: - playwright: - allowed_domains: - - "localhost" - - "example.com" ---- - -# Test MCP Gateway with mcp-inspect - -This workflow tests the MCP gateway configuration and tool list. -` - - workflowFile := filepath.Join(workflowsDir, "test-mcp-gateway.md") - if err := os.WriteFile(workflowFile, []byte(workflowContent), 0644); err != nil { - t.Fatalf("Failed to create test workflow file: %v", err) - } - - // Create MCP gateway configuration with gh-aw MCP server - configFile := filepath.Join(tmpDir, "gateway-config.json") - config := MCPGatewayServiceConfig{ - MCPServers: map[string]parser.MCPServerConfig{ - "gh-aw": { - BaseMCPServerConfig: types.BaseMCPServerConfig{ - Command: binaryPath, - Args: []string{"mcp-server"}, - }, - }, - }, - Gateway: GatewaySettings{ - Port: 8089, - }, - } - - configJSON, err := json.Marshal(config) - if err != nil { - t.Fatalf("Failed to marshal gateway config: %v", err) - } - - if err := os.WriteFile(configFile, configJSON, 0644); err != nil { - t.Fatalf("Failed to write gateway config file: %v", err) - } - - // Start the MCP gateway in background - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - gatewayErrChan := make(chan error, 1) - go func() { - // Use context for gateway lifecycle - _ = ctx // Mark as used - gatewayErrChan <- runMCPGateway([]string{configFile}, 8089, tmpDir) - }() - - // Wait for gateway to start - t.Log("Waiting for MCP gateway to start...") - time.Sleep(3 * time.Second) - - // Verify gateway health endpoint - healthResp, err := http.Get("http://localhost:8089/health") - if err != nil { - cancel() - t.Fatalf("Failed to connect to gateway health endpoint: %v", err) - } - healthResp.Body.Close() - - if healthResp.StatusCode != http.StatusOK { - cancel() - t.Fatalf("Gateway health check failed: status=%d", healthResp.StatusCode) - } - t.Log("✓ Gateway health check passed") - - // Test 1: Verify gateway servers endpoint - serversResp, err := http.Get("http://localhost:8089/servers") - if err != nil { - cancel() - t.Fatalf("Failed to get servers list from gateway: %v", err) - } - defer serversResp.Body.Close() - - var serversData map[string]any - if err := json.NewDecoder(serversResp.Body).Decode(&serversData); err != nil { - t.Fatalf("Failed to decode servers response: %v", err) - } - - servers, ok := serversData["servers"].([]any) - if !ok || len(servers) == 0 { - t.Fatalf("Expected servers list, got: %v", serversData) - } - t.Logf("✓ Gateway has %d server(s)", len(servers)) - - // Test 2: Use mcp inspect to check the workflow configuration - t.Log("Running mcp inspect on test workflow...") - inspectCmd := exec.Command(binaryPath, "mcp", "inspect", "test-mcp-gateway", "--verbose") - inspectCmd.Dir = tmpDir - inspectCmd.Env = append(os.Environ(), - fmt.Sprintf("HOME=%s", tmpDir), - ) - - output, err := inspectCmd.CombinedOutput() - outputStr := string(output) - - if err != nil { - t.Logf("mcp inspect output:\n%s", outputStr) - t.Fatalf("mcp inspect failed: %v", err) - } - - t.Logf("mcp inspect output:\n%s", outputStr) - - // Verify the output contains expected information - if !strings.Contains(outputStr, "playwright") { - t.Errorf("Expected 'playwright' in mcp inspect output") - } - - // Test 3: Use mcp inspect with --server flag to check specific server - t.Log("Running mcp inspect with --server playwright...") - inspectServerCmd := exec.Command(binaryPath, "mcp", "inspect", "test-mcp-gateway", "--server", "playwright", "--verbose") - inspectServerCmd.Dir = tmpDir - inspectServerCmd.Env = append(os.Environ(), - fmt.Sprintf("HOME=%s", tmpDir), - ) - - serverOutput, err := inspectServerCmd.CombinedOutput() - serverOutputStr := string(serverOutput) - - if err != nil { - t.Logf("mcp inspect --server output:\n%s", serverOutputStr) - // This might fail if playwright server isn't available, which is okay - t.Logf("Warning: mcp inspect --server failed (expected if playwright not configured): %v", err) - } else { - t.Logf("mcp inspect --server output:\n%s", serverOutputStr) - } - - // Test 4: Verify tool list can be accessed via mcp list command - t.Log("Running mcp list to check available tools...") - listCmd := exec.Command(binaryPath, "mcp", "list", "test-mcp-gateway") - listCmd.Dir = tmpDir - listCmd.Env = append(os.Environ(), - fmt.Sprintf("HOME=%s", tmpDir), - ) - - listOutput, err := listCmd.CombinedOutput() - listOutputStr := string(listOutput) - - if err != nil { - t.Logf("mcp list output:\n%s", listOutputStr) - t.Fatalf("mcp list failed: %v", err) - } - - t.Logf("mcp list output:\n%s", listOutputStr) - - // Verify the list output contains MCP server information - if !strings.Contains(listOutputStr, "MCP") { - t.Errorf("Expected 'MCP' in mcp list output") - } - - // Test 5: Check tool list using mcp list-tools command - t.Log("Running mcp list-tools to enumerate available tools...") - listToolsCmd := exec.Command(binaryPath, "mcp", "list-tools", "test-mcp-gateway") - listToolsCmd.Dir = tmpDir - listToolsCmd.Env = append(os.Environ(), - fmt.Sprintf("HOME=%s", tmpDir), - ) - - toolsOutput, err := listToolsCmd.CombinedOutput() - toolsOutputStr := string(toolsOutput) - - if err != nil { - t.Logf("mcp list-tools output:\n%s", toolsOutputStr) - // This might fail depending on MCP server configuration - t.Logf("Warning: mcp list-tools failed: %v", err) - } else { - t.Logf("mcp list-tools output:\n%s", toolsOutputStr) - - // If successful, verify we have tool information - if strings.Contains(toolsOutputStr, "No tools") { - t.Log("Note: No tools found in MCP servers (this may be expected)") - } - } - - t.Log("✓ All mcp inspect tests completed successfully") - - // Clean up: cancel context to stop the gateway - cancel() - - // Wait for gateway to stop - select { - case err := <-gatewayErrChan: - if err != nil && err != http.ErrServerClosed && !strings.Contains(err.Error(), "context canceled") { - t.Logf("Gateway stopped with error: %v", err) - } - case <-time.After(3 * time.Second): - t.Log("Gateway shutdown timed out") - } -} - -// TestMCPGateway_InspectToolList specifically tests tool list inspection -func TestMCPGateway_InspectToolList(t *testing.T) { - // Get absolute path to binary - binaryPath, err := filepath.Abs(filepath.Join("..", "..", "gh-aw")) - if err != nil { - t.Fatalf("Failed to get absolute path: %v", err) - } - - if _, err := os.Stat(binaryPath); os.IsNotExist(err) { - t.Skipf("Skipping test: gh-aw binary not found at %s. Run 'make build' first.", binaryPath) - } - - // Create temporary directory - tmpDir := t.TempDir() - workflowsDir := filepath.Join(tmpDir, ".github", "workflows") - if err := os.MkdirAll(workflowsDir, 0755); err != nil { - t.Fatalf("Failed to create workflows directory: %v", err) - } - - // Create a minimal workflow for tool list testing - workflowContent := `--- -on: workflow_dispatch -permissions: - contents: read -engine: copilot -tools: - github: - mode: remote - toolsets: [default] ---- - -# Test Tool List Inspection - -Test workflow for verifying tool list via mcp inspect. -` - - workflowFile := filepath.Join(workflowsDir, "test-tools.md") - if err := os.WriteFile(workflowFile, []byte(workflowContent), 0644); err != nil { - t.Fatalf("Failed to create test workflow file: %v", err) - } - - // Run mcp inspect to check tool list - t.Log("Running mcp inspect to check tool list...") - inspectCmd := exec.Command(binaryPath, "mcp", "inspect", "test-tools", "--server", "github", "--verbose") - inspectCmd.Dir = tmpDir - inspectCmd.Env = append(os.Environ(), - fmt.Sprintf("HOME=%s", tmpDir), - "GH_TOKEN=placeholder_token_for_testing", // Provide placeholder token for GitHub MCP - ) - - output, err := inspectCmd.CombinedOutput() - outputStr := string(output) - - t.Logf("mcp inspect output:\n%s", outputStr) - - // Check if inspection was successful or at least attempted - if err != nil { - // It's okay if it fails due to auth issues, we're testing the workflow parsing - if !strings.Contains(outputStr, "github") && !strings.Contains(outputStr, "Secret validation") { - t.Fatalf("mcp inspect failed unexpectedly: %v", err) - } - t.Log("Note: Inspection failed as expected due to auth/connection issues") - } - - // Verify the workflow was parsed and github server was detected - if strings.Contains(outputStr, "github") || strings.Contains(outputStr, "GitHub MCP") { - t.Log("✓ GitHub MCP server detected in workflow") - } - - t.Log("✓ Tool list inspection test completed") -} diff --git a/pkg/awmg/gateway_integration_test.go b/pkg/awmg/gateway_integration_test.go deleted file mode 100644 index f0f46079d96..00000000000 --- a/pkg/awmg/gateway_integration_test.go +++ /dev/null @@ -1,136 +0,0 @@ -//go:build integration - -package awmg - -import ( - "context" - "encoding/json" - "net/http" - "os" - "path/filepath" - "testing" - "time" - - "github.com/githubnext/gh-aw/pkg/parser" - "github.com/githubnext/gh-aw/pkg/types" -) - -func TestMCPGateway_BasicStartup(t *testing.T) { - // Skip if the binary doesn't exist - binaryPath := "../../gh-aw" - if _, err := os.Stat(binaryPath); os.IsNotExist(err) { - t.Skip("Skipping test: gh-aw binary not found. Run 'make build' first.") - } - - // Create temporary config - tmpDir := t.TempDir() - configFile := filepath.Join(tmpDir, "gateway-config.json") - - config := MCPGatewayServiceConfig{ - MCPServers: map[string]parser.MCPServerConfig{ - "gh-aw": { - BaseMCPServerConfig: types.BaseMCPServerConfig{ - Command: binaryPath, - Args: []string{"mcp-server"}, - }, - }, - }, - Gateway: GatewaySettings{ - Port: 8088, - }, - } - - configJSON, err := json.Marshal(config) - if err != nil { - t.Fatalf("Failed to marshal config: %v", err) - } - - if err := os.WriteFile(configFile, configJSON, 0644); err != nil { - t.Fatalf("Failed to write config file: %v", err) - } - - // Start gateway in background - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - // Use the runMCPGateway function directly in a goroutine - errChan := make(chan error, 1) - go func() { - errChan <- runMCPGateway([]string{configFile}, 8088, tmpDir) - }() - - // Wait for server to start - select { - case <-ctx.Done(): - t.Fatal("Context canceled before server could start") - case <-time.After(2 * time.Second): - // Server should be ready - } - - // Test health endpoint - resp, err := http.Get("http://localhost:8088/health") - if err != nil { - cancel() - t.Fatalf("Failed to connect to gateway: %v", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - t.Errorf("Expected status 200, got %d", resp.StatusCode) - } - - // Test servers list endpoint - resp, err = http.Get("http://localhost:8088/servers") - if err != nil { - cancel() - t.Fatalf("Failed to get servers list: %v", err) - } - defer resp.Body.Close() - - var serversResp map[string]any - if err := json.NewDecoder(resp.Body).Decode(&serversResp); err != nil { - t.Fatalf("Failed to decode servers response: %v", err) - } - - servers, ok := serversResp["servers"].([]any) - if !ok { - t.Fatal("Expected servers array in response") - } - - if len(servers) != 1 { - t.Errorf("Expected 1 server, got %d", len(servers)) - } - - // Check if gh-aw server is present - foundGhAw := false - for _, server := range servers { - if serverName, ok := server.(string); ok && serverName == "gh-aw" { - foundGhAw = true - break - } - } - - if !foundGhAw { - t.Error("Expected gh-aw server in servers list") - } - - // Cancel context to stop the server - cancel() - - // Wait for server to stop or timeout - select { - case err := <-errChan: - // Server stopped, check if it was a clean shutdown - if err != nil && err != http.ErrServerClosed && err.Error() != "context canceled" { - t.Logf("Server stopped with error: %v", err) - } - case <-time.After(2 * time.Second): - t.Log("Server shutdown timed out") - } -} - -func TestMCPGateway_ConfigFromStdin(t *testing.T) { - // This test would require piping config to stdin - // which is more complex in Go tests, so we'll skip for now - t.Skip("Stdin config test requires more complex setup") -} diff --git a/pkg/awmg/gateway_rewrite_test.go b/pkg/awmg/gateway_rewrite_test.go deleted file mode 100644 index 63af411347d..00000000000 --- a/pkg/awmg/gateway_rewrite_test.go +++ /dev/null @@ -1,398 +0,0 @@ -package awmg - -import ( - "encoding/json" - "os" - "path/filepath" - "testing" - - "github.com/githubnext/gh-aw/pkg/types" - - "github.com/githubnext/gh-aw/pkg/parser" -) - -// TestRewriteMCPConfigForGateway_ProxiesSafeInputsAndSafeOutputs tests that -// safeinputs and safeoutputs servers ARE proxied through the gateway (rewritten) -func TestRewriteMCPConfigForGateway_ProxiesSafeInputsAndSafeOutputs(t *testing.T) { - // Create a temporary config file - tmpDir := t.TempDir() - configFile := filepath.Join(tmpDir, "test-config.json") - - // Initial config with both proxied and non-proxied servers - initialConfig := map[string]any{ - "mcpServers": map[string]any{ - "safeinputs": map[string]any{ - "command": "gh", - "args": []string{"aw", "mcp-server", "--mode", "safe-inputs"}, - }, - "safeoutputs": map[string]any{ - "command": "gh", - "args": []string{"aw", "mcp-server", "--mode", "safe-outputs"}, - }, - "github": map[string]any{ - "command": "docker", - "args": []string{"run", "-i", "--rm", "ghcr.io/github-mcp-server"}, - }, - }, - "gateway": map[string]any{ - "port": 8080, - }, - } - - initialJSON, _ := json.Marshal(initialConfig) - if err := os.WriteFile(configFile, initialJSON, 0644); err != nil { - t.Fatalf("Failed to write config file: %v", err) - } - - // Gateway config includes ALL servers (including safeinputs/safeoutputs) - gatewayConfig := &MCPGatewayServiceConfig{ - MCPServers: map[string]parser.MCPServerConfig{ - "safeinputs": { - BaseMCPServerConfig: types.BaseMCPServerConfig{ - Command: "gh", - Args: []string{"aw", "mcp-server", "--mode", "safe-inputs"}, - }, - }, - "safeoutputs": { - BaseMCPServerConfig: types.BaseMCPServerConfig{ - Command: "gh", - Args: []string{"aw", "mcp-server", "--mode", "safe-outputs"}, - }, - }, - "github": { - BaseMCPServerConfig: types.BaseMCPServerConfig{ - Command: "docker", - Args: []string{"run", "-i", "--rm", "ghcr.io/github-mcp-server"}, - }, - }, - }, - Gateway: GatewaySettings{ - Port: 8080, - }, - } - - // Rewrite the config - if err := rewriteMCPConfigForGateway(configFile, gatewayConfig); err != nil { - t.Fatalf("rewriteMCPConfigForGateway failed: %v", err) - } - - // Read back the rewritten config - rewrittenData, err := os.ReadFile(configFile) - if err != nil { - t.Fatalf("Failed to read rewritten config: %v", err) - } - - var rewrittenConfig map[string]any - if err := json.Unmarshal(rewrittenData, &rewrittenConfig); err != nil { - t.Fatalf("Failed to parse rewritten config: %v", err) - } - - // Verify structure - mcpServers, ok := rewrittenConfig["mcpServers"].(map[string]any) - if !ok { - t.Fatal("mcpServers not found or wrong type") - } - - // Should have all 3 servers, all rewritten - if len(mcpServers) != 3 { - t.Errorf("Expected 3 servers in rewritten config, got %d", len(mcpServers)) - } - - // Verify safeinputs points to gateway (rewritten) - safeinputs, ok := mcpServers["safeinputs"].(map[string]any) - if !ok { - t.Fatal("safeinputs server not found") - } - - safeinputsURL, ok := safeinputs["url"].(string) - if !ok { - t.Fatal("safeinputs server should have url (rewritten)") - } - - expectedURL := "http://localhost:8080/mcp/safeinputs" - if safeinputsURL != expectedURL { - t.Errorf("Expected safeinputs URL %s, got %s", expectedURL, safeinputsURL) - } - - safeinputsType, ok := safeinputs["type"].(string) - if !ok || safeinputsType != "http" { - t.Errorf("Expected safeinputs to have type 'http', got %v", safeinputsType) - } - - // Verify safeinputs does NOT have command/args (was rewritten) - if _, hasCommand := safeinputs["command"]; hasCommand { - t.Error("Rewritten safeinputs server should not have 'command' field") - } - - // Verify safeoutputs points to gateway (rewritten) - safeoutputs, ok := mcpServers["safeoutputs"].(map[string]any) - if !ok { - t.Fatal("safeoutputs server not found") - } - - safeoutputsURL, ok := safeoutputs["url"].(string) - if !ok { - t.Fatal("safeoutputs server should have url (rewritten)") - } - - expectedURL = "http://localhost:8080/mcp/safeoutputs" - if safeoutputsURL != expectedURL { - t.Errorf("Expected safeoutputs URL %s, got %s", expectedURL, safeoutputsURL) - } - - safeoutputsType, ok := safeoutputs["type"].(string) - if !ok || safeoutputsType != "http" { - t.Errorf("Expected safeoutputs to have type 'http', got %v", safeoutputsType) - } - - // Verify safeoutputs does NOT have command/args (was rewritten) - if _, hasCommand := safeoutputs["command"]; hasCommand { - t.Error("Rewritten safeoutputs server should not have 'command' field") - } - - // Verify github server points to gateway (was rewritten) - github, ok := mcpServers["github"].(map[string]any) - if !ok { - t.Fatal("github server not found") - } - - githubURL, ok := github["url"].(string) - if !ok { - t.Fatal("github server should have url (rewritten)") - } - - expectedURL = "http://localhost:8080/mcp/github" - if githubURL != expectedURL { - t.Errorf("Expected github URL %s, got %s", expectedURL, githubURL) - } - - // Verify github server has type: http - githubType, ok := github["type"].(string) - if !ok || githubType != "http" { - t.Errorf("Expected github server to have type 'http', got %v", githubType) - } - - // Verify github server has tools: ["*"] - githubTools, ok := github["tools"].([]any) - if !ok { - t.Fatal("github server should have tools array") - } - if len(githubTools) != 1 || githubTools[0].(string) != "*" { - t.Errorf("Expected github server to have tools ['*'], got %v", githubTools) - } - - // Verify github server does NOT have command/args (was rewritten) - if _, hasCommand := github["command"]; hasCommand { - t.Error("Rewritten github server should not have 'command' field") - } - - // Verify gateway settings are NOT included in rewritten config - _, hasGateway := rewrittenConfig["gateway"] - if hasGateway { - t.Error("Gateway section should not be included in rewritten config") - } -} - -// TestRewriteMCPConfigForGateway_NoGatewaySection tests that gateway section is removed -func TestRewriteMCPConfigForGateway_NoGatewaySection(t *testing.T) { - // Create a temporary config file - tmpDir := t.TempDir() - configFile := filepath.Join(tmpDir, "test-config.json") - - initialConfig := map[string]any{ - "mcpServers": map[string]any{ - "github": map[string]any{ - "command": "gh", - "args": []string{"aw", "mcp-server"}, - }, - }, - "gateway": map[string]any{ - "port": 8080, - "apiKey": "test-key", - }, - } - - initialJSON, _ := json.Marshal(initialConfig) - if err := os.WriteFile(configFile, initialJSON, 0644); err != nil { - t.Fatalf("Failed to write config file: %v", err) - } - - gatewayConfig := &MCPGatewayServiceConfig{ - MCPServers: map[string]parser.MCPServerConfig{ - "github": { - BaseMCPServerConfig: types.BaseMCPServerConfig{ - Command: "gh", - Args: []string{"aw", "mcp-server"}, - }, - }, - }, - Gateway: GatewaySettings{ - Port: 8080, - APIKey: "test-key", - }, - } - - // Rewrite the config - if err := rewriteMCPConfigForGateway(configFile, gatewayConfig); err != nil { - t.Fatalf("rewriteMCPConfigForGateway failed: %v", err) - } - - // Read back the rewritten config - rewrittenData, err := os.ReadFile(configFile) - if err != nil { - t.Fatalf("Failed to read rewritten config: %v", err) - } - - var rewrittenConfig map[string]any - if err := json.Unmarshal(rewrittenData, &rewrittenConfig); err != nil { - t.Fatalf("Failed to parse rewritten config: %v", err) - } - - // Verify gateway settings are NOT included in rewritten config - _, hasGateway := rewrittenConfig["gateway"] - if hasGateway { - t.Error("Gateway section should not be included in rewritten config") - } - - // Verify mcpServers still exists - _, hasMCPServers := rewrittenConfig["mcpServers"] - if !hasMCPServers { - t.Error("mcpServers section should be present in rewritten config") - } - - // Verify the rewritten server has type and tools - mcpServers, ok := rewrittenConfig["mcpServers"].(map[string]any) - if !ok { - t.Fatal("mcpServers not found or wrong type") - } - - github, ok := mcpServers["github"].(map[string]any) - if !ok { - t.Fatal("github server not found") - } - - // Check type field - githubType, ok := github["type"].(string) - if !ok || githubType != "http" { - t.Errorf("Expected github server to have type 'http', got %v", githubType) - } - - // Check tools field - githubTools, ok := github["tools"].([]any) - if !ok { - t.Fatal("github server should have tools array") - } - if len(githubTools) != 1 || githubTools[0].(string) != "*" { - t.Errorf("Expected github server to have tools ['*'], got %v", githubTools) - } - - // Check headers field (API key was configured) - githubHeaders, ok := github["headers"].(map[string]any) - if !ok { - t.Fatal("github server should have headers (API key configured)") - } - - authHeader, ok := githubHeaders["Authorization"].(string) - if !ok || authHeader != "Bearer test-key" { - t.Errorf("Expected Authorization header 'Bearer test-key', got %v", authHeader) - } -} - -// TestRewriteMCPConfigForGateway_UsesDomainFromConfig tests that the domain -// field from the gateway config is used when rewriting server URLs -func TestRewriteMCPConfigForGateway_UsesDomainFromConfig(t *testing.T) { - tests := []struct { - name string - domain string - expectedURL string - }{ - { - name: "host.docker.internal domain", - domain: "host.docker.internal", - expectedURL: "http://host.docker.internal:8080/mcp/github", - }, - { - name: "localhost domain", - domain: "localhost", - expectedURL: "http://localhost:8080/mcp/github", - }, - { - name: "empty domain defaults to localhost", - domain: "", - expectedURL: "http://localhost:8080/mcp/github", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tmpDir := t.TempDir() - configFile := filepath.Join(tmpDir, "test-config.json") - - initialConfig := map[string]any{ - "mcpServers": map[string]any{ - "github": map[string]any{ - "command": "gh", - "args": []string{"aw", "mcp-server"}, - }, - }, - } - - initialJSON, _ := json.Marshal(initialConfig) - if err := os.WriteFile(configFile, initialJSON, 0644); err != nil { - t.Fatalf("Failed to write config file: %v", err) - } - - gatewayConfig := &MCPGatewayServiceConfig{ - MCPServers: map[string]parser.MCPServerConfig{ - "github": { - BaseMCPServerConfig: types.BaseMCPServerConfig{ - Command: "gh", - Args: []string{"aw", "mcp-server"}, - }, - }, - }, - Gateway: GatewaySettings{ - Port: 8080, - Domain: tt.domain, - }, - } - - // Rewrite the config - if err := rewriteMCPConfigForGateway(configFile, gatewayConfig); err != nil { - t.Fatalf("rewriteMCPConfigForGateway failed: %v", err) - } - - // Read back the rewritten config - rewrittenData, err := os.ReadFile(configFile) - if err != nil { - t.Fatalf("Failed to read rewritten config: %v", err) - } - - var rewrittenConfig map[string]any - if err := json.Unmarshal(rewrittenData, &rewrittenConfig); err != nil { - t.Fatalf("Failed to parse rewritten config: %v", err) - } - - // Verify mcpServers exists - mcpServers, ok := rewrittenConfig["mcpServers"].(map[string]any) - if !ok { - t.Fatal("mcpServers not found or wrong type") - } - - // Check the github server URL - github, ok := mcpServers["github"].(map[string]any) - if !ok { - t.Fatal("github server not found") - } - - githubURL, ok := github["url"].(string) - if !ok { - t.Fatal("github server URL not found") - } - - if githubURL != tt.expectedURL { - t.Errorf("Expected URL %s, got %s", tt.expectedURL, githubURL) - } - }) - } -} diff --git a/pkg/awmg/gateway_streamable_http_test.go b/pkg/awmg/gateway_streamable_http_test.go deleted file mode 100644 index 9e488ff41ad..00000000000 --- a/pkg/awmg/gateway_streamable_http_test.go +++ /dev/null @@ -1,708 +0,0 @@ -//go:build integration - -package awmg - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "os" - "os/exec" - "path/filepath" - "strings" - "testing" - "time" - - "github.com/githubnext/gh-aw/pkg/parser" - "github.com/githubnext/gh-aw/pkg/types" - - "github.com/modelcontextprotocol/go-sdk/mcp" -) - -// TestStreamableHTTPTransport_GatewayConnection tests the streamable HTTP transport -// by starting the gateway with a command-based MCP server, then verifying we can -// connect via the gateway's HTTP endpoint using the go-sdk StreamableClientTransport. -func TestStreamableHTTPTransport_GatewayConnection(t *testing.T) { - // Get absolute path to binary - binaryPath, err := filepath.Abs(filepath.Join("..", "..", "gh-aw")) - if err != nil { - t.Fatalf("Failed to get absolute path: %v", err) - } - - if _, err := os.Stat(binaryPath); os.IsNotExist(err) { - t.Skipf("Skipping test: gh-aw binary not found at %s. Run 'make build' first.", binaryPath) - } - - // Create temporary directory for config - tmpDir := t.TempDir() - configFile := filepath.Join(tmpDir, "gateway-config.json") - - // Create gateway config with the gh-aw MCP server - config := MCPGatewayServiceConfig{ - MCPServers: map[string]parser.MCPServerConfig{ - "gh-aw": { - BaseMCPServerConfig: types.BaseMCPServerConfig{ - Command: binaryPath, - Args: []string{"mcp-server"}, - }, - }, - }, - Gateway: GatewaySettings{ - Port: 8091, // Use a different port to avoid conflicts - }, - } - - configJSON, err := json.Marshal(config) - if err != nil { - t.Fatalf("Failed to marshal config: %v", err) - } - - if err := os.WriteFile(configFile, configJSON, 0644); err != nil { - t.Fatalf("Failed to write config file: %v", err) - } - - // Start the gateway in background - _, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - gatewayErrChan := make(chan error, 1) - go func() { - gatewayErrChan <- runMCPGateway([]string{configFile}, 8091, tmpDir) - }() - - // Wait for gateway to start - t.Log("Waiting for MCP gateway to start...") - time.Sleep(3 * time.Second) - - // Verify gateway health - healthResp, err := http.Get("http://localhost:8091/health") - if err != nil { - cancel() - t.Fatalf("Failed to connect to gateway health endpoint: %v", err) - } - healthResp.Body.Close() - - if healthResp.StatusCode != http.StatusOK { - cancel() - t.Fatalf("Gateway health check failed: status=%d", healthResp.StatusCode) - } - t.Log("✓ Gateway health check passed") - - // Test 1: Verify the gateway servers list - serversResp, err := http.Get("http://localhost:8091/servers") - if err != nil { - cancel() - t.Fatalf("Failed to get servers list: %v", err) - } - defer serversResp.Body.Close() - - var serversData map[string]any - if err := json.NewDecoder(serversResp.Body).Decode(&serversData); err != nil { - t.Fatalf("Failed to decode servers response: %v", err) - } - - servers, ok := serversData["servers"].([]any) - if !ok || len(servers) == 0 { - t.Fatalf("Expected servers list, got: %v", serversData) - } - t.Logf("✓ Gateway has %d server(s): %v", len(servers), servers) - - // Test 2: Connect to the MCP endpoint using StreamableClientTransport - mcpURL := "http://localhost:8091/mcp/gh-aw" - t.Logf("Testing MCP endpoint with StreamableClientTransport: %s", mcpURL) - - // Create streamable client transport - transport := &mcp.StreamableClientTransport{ - Endpoint: mcpURL, - } - - // Create MCP client - client := mcp.NewClient(&mcp.Implementation{ - Name: "test-client", - Version: "1.0.0", - }, nil) - - // Connect to the gateway - connectCtx, connectCancel := context.WithTimeout(context.Background(), 10*time.Second) - defer connectCancel() - - session, err := client.Connect(connectCtx, transport, nil) - if err != nil { - cancel() - t.Fatalf("Failed to connect via StreamableClientTransport: %v", err) - } - defer session.Close() - - t.Log("✓ Successfully connected via StreamableClientTransport") - - // Test listing tools - toolsCtx, toolsCancel := context.WithTimeout(context.Background(), 10*time.Second) - defer toolsCancel() - - toolsResult, err := session.ListTools(toolsCtx, &mcp.ListToolsParams{}) - if err != nil { - t.Fatalf("Failed to list tools: %v", err) - } - - if len(toolsResult.Tools) == 0 { - t.Error("Expected at least one tool from backend") - } - - t.Logf("✓ Found %d tools from backend via gateway", len(toolsResult.Tools)) - - // Test listing resources - resourcesCtx, resourcesCancel := context.WithTimeout(context.Background(), 10*time.Second) - defer resourcesCancel() - - resourcesResult, err := session.ListResources(resourcesCtx, &mcp.ListResourcesParams{}) - if err != nil { - t.Fatalf("Failed to list resources: %v", err) - } - - t.Logf("✓ Found %d resources from backend via gateway", len(resourcesResult.Resources)) - - // If there are resources, test reading one - if len(resourcesResult.Resources) > 0 { - firstResource := resourcesResult.Resources[0] - t.Logf("Testing read resource: %s", firstResource.URI) - - readCtx, readCancel := context.WithTimeout(context.Background(), 10*time.Second) - defer readCancel() - - readResult, err := session.ReadResource(readCtx, &mcp.ReadResourceParams{ - URI: firstResource.URI, - }) - if err != nil { - t.Logf("Note: Failed to read resource (may not be readable in test environment): %v", err) - } else { - t.Logf("✓ Successfully read resource via gateway") - if len(readResult.Contents) > 0 { - t.Logf(" Resource returned %d content items", len(readResult.Contents)) - } - } - } - - t.Log("✓ All streamable HTTP transport tests completed successfully") - - // Clean up - cancel() - - // Wait for gateway to stop - select { - case err := <-gatewayErrChan: - if err != nil && err != http.ErrServerClosed && !strings.Contains(err.Error(), "context canceled") { - t.Logf("Gateway stopped with error: %v", err) - } - case <-time.After(3 * time.Second): - t.Log("Gateway shutdown timed out") - } -} - -// TestStreamableHTTPTransport_GoSDKClient tests using the go-sdk StreamableClientTransport -// to connect to a mock MCP server that implements the streamable HTTP protocol. -func TestStreamableHTTPTransport_GoSDKClient(t *testing.T) { - // Create a mock MCP server that implements the streamable HTTP protocol - mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Only accept POST requests - if r.Method != http.MethodPost { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - // Parse the JSON-RPC request - var request map[string]any - if err := json.NewDecoder(r.Body).Decode(&request); err != nil { - http.Error(w, "Invalid JSON", http.StatusBadRequest) - return - } - - method, _ := request["method"].(string) - id := request["id"] - - // Build JSON-RPC response - var result any - - switch method { - case "initialize": - result = map[string]any{ - "protocolVersion": "2024-11-05", - "capabilities": map[string]any{ - "tools": map[string]any{}, - "resources": map[string]any{}, - }, - "serverInfo": map[string]any{ - "name": "test-server", - "version": "1.0.0", - }, - } - case "notifications/initialized": - // No response needed for notification - w.WriteHeader(http.StatusAccepted) - return - case "tools/list": - result = map[string]any{ - "tools": []map[string]any{ - { - "name": "test_tool", - "description": "A test tool", - "inputSchema": map[string]any{ - "type": "object", - "properties": map[string]any{}, - }, - }, - }, - } - case "resources/list": - result = map[string]any{ - "resources": []map[string]any{ - { - "uri": "file:///test/resource.txt", - "name": "test_resource", - "description": "A test resource", - "mimeType": "text/plain", - }, - }, - } - case "resources/read": - params, _ := request["params"].(map[string]any) - uri, _ := params["uri"].(string) - result = map[string]any{ - "contents": []map[string]any{ - { - "uri": uri, - "mimeType": "text/plain", - "text": "This is test resource content", - }, - }, - } - default: - http.Error(w, fmt.Sprintf("Unknown method: %s", method), http.StatusBadRequest) - return - } - - response := map[string]any{ - "jsonrpc": "2.0", - "id": id, - "result": result, - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(response) - })) - defer mockServer.Close() - - t.Logf("Mock MCP server running at: %s", mockServer.URL) - - // Create the streamable client transport - transport := &mcp.StreamableClientTransport{ - Endpoint: mockServer.URL, - } - - // Create MCP client - client := mcp.NewClient(&mcp.Implementation{ - Name: "test-client", - Version: "1.0.0", - }, nil) - - // Connect to the server - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - session, err := client.Connect(ctx, transport, nil) - if err != nil { - t.Fatalf("Failed to connect to mock MCP server: %v", err) - } - defer session.Close() - - t.Log("✓ Successfully connected to mock MCP server via StreamableClientTransport") - - // Test listing tools - toolsResult, err := session.ListTools(ctx, &mcp.ListToolsParams{}) - if err != nil { - t.Fatalf("Failed to list tools: %v", err) - } - - if len(toolsResult.Tools) != 1 { - t.Errorf("Expected 1 tool, got %d", len(toolsResult.Tools)) - } - - if toolsResult.Tools[0].Name != "test_tool" { - t.Errorf("Expected tool name 'test_tool', got '%s'", toolsResult.Tools[0].Name) - } - - t.Logf("✓ Successfully listed tools: %v", toolsResult.Tools) - - // Test listing resources - resourcesResult, err := session.ListResources(ctx, &mcp.ListResourcesParams{}) - if err != nil { - t.Fatalf("Failed to list resources: %v", err) - } - - if len(resourcesResult.Resources) != 1 { - t.Errorf("Expected 1 resource, got %d", len(resourcesResult.Resources)) - } - - if resourcesResult.Resources[0].Name != "test_resource" { - t.Errorf("Expected resource name 'test_resource', got '%s'", resourcesResult.Resources[0].Name) - } - - t.Logf("✓ Successfully listed resources: %v", resourcesResult.Resources) - - // Test reading a resource - readResult, err := session.ReadResource(ctx, &mcp.ReadResourceParams{ - URI: "file:///test/resource.txt", - }) - if err != nil { - t.Fatalf("Failed to read resource: %v", err) - } - - if len(readResult.Contents) != 1 { - t.Errorf("Expected 1 content item, got %d", len(readResult.Contents)) - } - - if readResult.Contents[0].Text != "This is test resource content" { - t.Errorf("Expected content 'This is test resource content', got '%s'", readResult.Contents[0].Text) - } - - t.Logf("✓ Successfully read resource content") - - t.Log("✓ StreamableClientTransport go-sdk test completed successfully") -} - -// TestStreamableHTTPTransport_URLConfigured tests that a URL-configured server -// uses the StreamableClientTransport when connecting. -func TestStreamableHTTPTransport_URLConfigured(t *testing.T) { - // Create a mock server that tracks connection attempts - connectionAttempted := false - mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - connectionAttempted = true - - if r.Method != http.MethodPost { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - var request map[string]any - if err := json.NewDecoder(r.Body).Decode(&request); err != nil { - http.Error(w, "Invalid JSON", http.StatusBadRequest) - return - } - - method, _ := request["method"].(string) - id := request["id"] - - var result any - switch method { - case "initialize": - result = map[string]any{ - "protocolVersion": "2024-11-05", - "capabilities": map[string]any{}, - "serverInfo": map[string]any{ - "name": "url-test-server", - "version": "1.0.0", - }, - } - case "notifications/initialized": - w.WriteHeader(http.StatusAccepted) - return - default: - result = map[string]any{} - } - - response := map[string]any{ - "jsonrpc": "2.0", - "id": id, - "result": result, - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(response) - })) - defer mockServer.Close() - - t.Logf("Mock URL-based MCP server at: %s", mockServer.URL) - - // Test that createMCPSession uses StreamableClientTransport for URL config - gateway := &MCPGatewayServer{ - config: &MCPGatewayServiceConfig{}, - sessions: make(map[string]*mcp.ClientSession), - logDir: t.TempDir(), - } - - // Create a session with URL configuration - serverConfig := parser.MCPServerConfig{BaseMCPServerConfig: types.BaseMCPServerConfig{URL: mockServer.URL}} - - session, err := gateway.createMCPSession("test-url-server", serverConfig) - if err != nil { - t.Fatalf("Failed to create session for URL-configured server: %v", err) - } - defer session.Close() - - if !connectionAttempted { - t.Error("Expected connection to be attempted via streamable HTTP") - } - - t.Log("✓ URL-configured server successfully connected via StreamableClientTransport") -} - -// TestStreamableHTTPTransport_MCPInspect tests using the mcp inspect command -// to verify the streamable HTTP configuration works end-to-end. -func TestStreamableHTTPTransport_MCPInspect(t *testing.T) { - // Get absolute path to binary - binaryPath, err := filepath.Abs(filepath.Join("..", "..", "gh-aw")) - if err != nil { - t.Fatalf("Failed to get absolute path: %v", err) - } - - if _, err := os.Stat(binaryPath); os.IsNotExist(err) { - t.Skipf("Skipping test: gh-aw binary not found at %s. Run 'make build' first.", binaryPath) - } - - // Create temporary directory - tmpDir := t.TempDir() - workflowsDir := filepath.Join(tmpDir, ".github", "workflows") - if err := os.MkdirAll(workflowsDir, 0755); err != nil { - t.Fatalf("Failed to create workflows directory: %v", err) - } - - // Create a test workflow with HTTP-based MCP server configuration - workflowContent := `--- -on: workflow_dispatch -permissions: - contents: read -engine: copilot -tools: - github: - mode: remote - toolsets: [default] ---- - -# Test Streamable HTTP Transport - -This workflow tests the streamable HTTP transport via mcp inspect. -` - - workflowFile := filepath.Join(workflowsDir, "test-streamable.md") - if err := os.WriteFile(workflowFile, []byte(workflowContent), 0644); err != nil { - t.Fatalf("Failed to create test workflow file: %v", err) - } - - // Run mcp inspect to verify the workflow can be parsed - t.Log("Running mcp inspect to verify streamable HTTP configuration...") - inspectCmd := exec.Command(binaryPath, "mcp", "inspect", "test-streamable", "--verbose") - inspectCmd.Dir = tmpDir - inspectCmd.Env = append(os.Environ(), - fmt.Sprintf("HOME=%s", tmpDir), - ) - - output, err := inspectCmd.CombinedOutput() - outputStr := string(output) - - t.Logf("mcp inspect output:\n%s", outputStr) - - // Check if the workflow was parsed successfully - if err != nil { - // It might fail due to auth, but we're testing the parsing - if !strings.Contains(outputStr, "github") { - t.Fatalf("mcp inspect failed to parse workflow: %v", err) - } - t.Log("Note: Inspection failed due to auth (expected), but workflow was parsed correctly") - } - - // Verify the github server was detected - if strings.Contains(outputStr, "github") || strings.Contains(outputStr, "GitHub") { - t.Log("✓ GitHub server detected in workflow (uses HTTP transport)") - } - - t.Log("✓ MCP inspect test for streamable HTTP completed successfully") -} - -// TestStreamableHTTPTransport_GatewayWithSDKClient tests that the gateway properly -// exposes backend servers via StreamableHTTPHandler and that we can connect to them -// using the go-sdk StreamableClientTransport. -func TestStreamableHTTPTransport_GatewayWithSDKClient(t *testing.T) { - // Get absolute path to binary - binaryPath, err := filepath.Abs(filepath.Join("..", "..", "gh-aw")) - if err != nil { - t.Fatalf("Failed to get absolute path: %v", err) - } - - if _, err := os.Stat(binaryPath); os.IsNotExist(err) { - t.Skipf("Skipping test: gh-aw binary not found at %s. Run 'make build' first.", binaryPath) - } - - // Create temporary directory for config - tmpDir := t.TempDir() - configFile := filepath.Join(tmpDir, "gateway-config.json") - - // Create gateway config with the gh-aw MCP server - config := MCPGatewayServiceConfig{ - MCPServers: map[string]parser.MCPServerConfig{ - "gh-aw": { - BaseMCPServerConfig: types.BaseMCPServerConfig{ - Command: binaryPath, - Args: []string{"mcp-server"}, - }, - }, - }, - Gateway: GatewaySettings{ - Port: 8092, // Use a different port to avoid conflicts - }, - } - - configJSON, err := json.Marshal(config) - if err != nil { - t.Fatalf("Failed to marshal config: %v", err) - } - - if err := os.WriteFile(configFile, configJSON, 0644); err != nil { - t.Fatalf("Failed to write config file: %v", err) - } - - // Start the gateway in background - _, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - gatewayErrChan := make(chan error, 1) - go func() { - gatewayErrChan <- runMCPGateway([]string{configFile}, 8092, tmpDir) - }() - - // Wait for gateway to start - t.Log("Waiting for MCP gateway to start...") - time.Sleep(3 * time.Second) - - // Verify gateway health - healthResp, err := http.Get("http://localhost:8092/health") - if err != nil { - cancel() - t.Fatalf("Failed to connect to gateway health endpoint: %v", err) - } - healthResp.Body.Close() - - if healthResp.StatusCode != http.StatusOK { - cancel() - t.Fatalf("Gateway health check failed: status=%d", healthResp.StatusCode) - } - t.Log("✓ Gateway health check passed") - - // Now test connecting to the gateway using StreamableClientTransport - gatewayURL := "http://localhost:8092/mcp/gh-aw" - t.Logf("Connecting to gateway via StreamableClientTransport: %s", gatewayURL) - - // Create streamable client transport - transport := &mcp.StreamableClientTransport{ - Endpoint: gatewayURL, - } - - // Create MCP client - client := mcp.NewClient(&mcp.Implementation{ - Name: "test-client", - Version: "1.0.0", - }, nil) - - // Connect to the gateway - connectCtx, connectCancel := context.WithTimeout(context.Background(), 10*time.Second) - defer connectCancel() - - session, err := client.Connect(connectCtx, transport, nil) - if err != nil { - cancel() - t.Fatalf("Failed to connect to gateway via StreamableClientTransport: %v", err) - } - defer session.Close() - - t.Log("✓ Successfully connected to gateway via StreamableClientTransport") - - // Test listing tools - toolsCtx, toolsCancel := context.WithTimeout(context.Background(), 10*time.Second) - defer toolsCancel() - - toolsResult, err := session.ListTools(toolsCtx, &mcp.ListToolsParams{}) - if err != nil { - t.Fatalf("Failed to list tools: %v", err) - } - - if len(toolsResult.Tools) == 0 { - t.Error("Expected at least one tool from gh-aw MCP server") - } - - t.Logf("✓ Successfully listed %d tools from backend via gateway", len(toolsResult.Tools)) - for i, tool := range toolsResult.Tools { - if i < 3 { // Log first 3 tools - t.Logf(" - %s: %s", tool.Name, tool.Description) - } - } - - // Test calling a tool (status tool should be available) - callCtx, callCancel := context.WithTimeout(context.Background(), 30*time.Second) - defer callCancel() - - // Create a simple test by calling the status tool - callResult, err := session.CallTool(callCtx, &mcp.CallToolParams{ - Name: "status", - Arguments: map[string]any{}, - }) - if err != nil { - t.Logf("Note: Failed to call status tool (may not be in test environment): %v", err) - } else { - t.Logf("✓ Successfully called status tool via gateway") - if len(callResult.Content) > 0 { - t.Logf(" Tool returned %d content items", len(callResult.Content)) - } - } - - // Test listing resources - resourcesCtx, resourcesCancel := context.WithTimeout(context.Background(), 10*time.Second) - defer resourcesCancel() - - resourcesResult, err := session.ListResources(resourcesCtx, &mcp.ListResourcesParams{}) - if err != nil { - t.Fatalf("Failed to list resources: %v", err) - } - - t.Logf("✓ Successfully listed %d resources from backend via gateway", len(resourcesResult.Resources)) - for i, resource := range resourcesResult.Resources { - if i < 3 { // Log first 3 resources - t.Logf(" - %s: %s", resource.Name, resource.Description) - } - } - - // If there are resources, test reading one - if len(resourcesResult.Resources) > 0 { - firstResource := resourcesResult.Resources[0] - t.Logf("Testing read resource: %s", firstResource.URI) - - readCtx, readCancel := context.WithTimeout(context.Background(), 10*time.Second) - defer readCancel() - - readResult, err := session.ReadResource(readCtx, &mcp.ReadResourceParams{ - URI: firstResource.URI, - }) - if err != nil { - t.Logf("Note: Failed to read resource (may not be readable in test environment): %v", err) - } else { - t.Logf("✓ Successfully read resource via gateway") - if len(readResult.Contents) > 0 { - t.Logf(" Resource returned %d content items", len(readResult.Contents)) - } - } - } - - t.Log("✓ All StreamableHTTPHandler gateway tests completed successfully") - - // Clean up - cancel() - - // Wait for gateway to stop - select { - case err := <-gatewayErrChan: - if err != nil && err != http.ErrServerClosed && !strings.Contains(err.Error(), "context canceled") { - t.Logf("Gateway stopped with error: %v", err) - } - case <-time.After(3 * time.Second): - t.Log("Gateway shutdown timed out") - } -} diff --git a/pkg/awmg/gateway_test.go b/pkg/awmg/gateway_test.go deleted file mode 100644 index 022511efa39..00000000000 --- a/pkg/awmg/gateway_test.go +++ /dev/null @@ -1,700 +0,0 @@ -package awmg - -import ( - "encoding/json" - "os" - "path/filepath" - "testing" - - "github.com/githubnext/gh-aw/pkg/types" - - "github.com/githubnext/gh-aw/pkg/parser" -) - -func TestReadGatewayConfig_FromFile(t *testing.T) { - // Create a temporary config file - tmpDir := t.TempDir() - configFile := filepath.Join(tmpDir, "gateway-config.json") - - config := MCPGatewayServiceConfig{ - MCPServers: map[string]parser.MCPServerConfig{ - "test-server": { - BaseMCPServerConfig: types.BaseMCPServerConfig{ - Command: "test-command", - Args: []string{"arg1", "arg2"}, - Env: map[string]string{ - "KEY": "value", - }, - }, - }, - }, - Gateway: GatewaySettings{ - Port: 8080, - }, - } - - configJSON, err := json.Marshal(config) - if err != nil { - t.Fatalf("Failed to marshal config: %v", err) - } - - if err := os.WriteFile(configFile, configJSON, 0644); err != nil { - t.Fatalf("Failed to write config file: %v", err) - } - - // Read config - result, _, err := readGatewayConfig([]string{configFile}) - if err != nil { - t.Fatalf("Failed to read config: %v", err) - } - - // Verify config - if len(result.MCPServers) != 1 { - t.Errorf("Expected 1 server, got %d", len(result.MCPServers)) - } - - testServer, exists := result.MCPServers["test-server"] - if !exists { - t.Fatal("test-server not found in config") - } - - if testServer.Command != "test-command" { - t.Errorf("Expected command 'test-command', got '%s'", testServer.Command) - } - - if len(testServer.Args) != 2 { - t.Errorf("Expected 2 args, got %d", len(testServer.Args)) - } - - if result.Gateway.Port != 8080 { - t.Errorf("Expected port 8080, got %d", result.Gateway.Port) - } -} - -func TestReadGatewayConfig_InvalidJSON(t *testing.T) { - // Create a temporary config file with invalid JSON - tmpDir := t.TempDir() - configFile := filepath.Join(tmpDir, "invalid-config.json") - - if err := os.WriteFile(configFile, []byte("not valid json"), 0644); err != nil { - t.Fatalf("Failed to write config file: %v", err) - } - - // Read config - should fail - _, _, err := readGatewayConfig([]string{configFile}) - if err == nil { - t.Error("Expected error for invalid JSON, got nil") - } -} - -func TestMCPGatewayConfig_EmptyServers(t *testing.T) { - config := &MCPGatewayServiceConfig{ - MCPServers: make(map[string]parser.MCPServerConfig), - Gateway: GatewaySettings{ - Port: 8080, - }, - } - - if len(config.MCPServers) != 0 { - t.Errorf("Expected 0 servers, got %d", len(config.MCPServers)) - } -} - -func TestMCPServerConfig_CommandType(t *testing.T) { - config := parser.MCPServerConfig{BaseMCPServerConfig: types.BaseMCPServerConfig{Command: "gh", - Args: []string{"aw", "mcp-server"}, - Env: map[string]string{ - "DEBUG": "cli:*", - }}, - } - - if config.Command != "gh" { - t.Errorf("Expected command 'gh', got '%s'", config.Command) - } - - if config.URL != "" { - t.Error("Expected empty URL for command-based server") - } - - if config.Container != "" { - t.Error("Expected empty container for command-based server") - } -} - -func TestMCPServerConfig_URLType(t *testing.T) { - config := parser.MCPServerConfig{BaseMCPServerConfig: types.BaseMCPServerConfig{URL: "http://localhost:3000"}} - - if config.URL != "http://localhost:3000" { - t.Errorf("Expected URL 'http://localhost:3000', got '%s'", config.URL) - } - - if config.Command != "" { - t.Error("Expected empty command for URL-based server") - } -} - -func TestMCPServerConfig_ContainerType(t *testing.T) { - config := parser.MCPServerConfig{BaseMCPServerConfig: types.BaseMCPServerConfig{Container: "mcp-server:latest", - Args: []string{"--verbose"}, - Env: map[string]string{ - "LOG_LEVEL": "debug", - }}, - } - - if config.Container != "mcp-server:latest" { - t.Errorf("Expected container 'mcp-server:latest', got '%s'", config.Container) - } - - if config.Command != "" { - t.Error("Expected empty command for container-based server") - } - - if config.URL != "" { - t.Error("Expected empty URL for container-based server") - } -} - -func TestGatewaySettings_DefaultPort(t *testing.T) { - settings := GatewaySettings{} - - if settings.Port != 0 { - t.Errorf("Expected default port 0, got %d", settings.Port) - } -} - -func TestGatewaySettings_WithAPIKey(t *testing.T) { - settings := GatewaySettings{ - Port: 8080, - APIKey: "test-api-key", - } - - if settings.APIKey != "test-api-key" { - t.Errorf("Expected API key 'test-api-key', got '%s'", settings.APIKey) - } -} - -func TestReadGatewayConfig_FileNotFound(t *testing.T) { - // Try to read a non-existent file - _, _, err := readGatewayConfig([]string{"/tmp/nonexistent-gateway-config-12345.json"}) - if err == nil { - t.Error("Expected error for non-existent file, got nil") - } - if err != nil && err.Error() != "configuration file not found: /tmp/nonexistent-gateway-config-12345.json" { - t.Errorf("Expected specific error message, got: %v", err) - } -} - -func TestReadGatewayConfig_EmptyServers(t *testing.T) { - // Create a config file with no servers - tmpDir := t.TempDir() - configFile := filepath.Join(tmpDir, "empty-servers.json") - - config := MCPGatewayServiceConfig{ - MCPServers: map[string]parser.MCPServerConfig{}, - Gateway: GatewaySettings{ - Port: 8080, - }, - } - - configJSON, err := json.Marshal(config) - if err != nil { - t.Fatalf("Failed to marshal config: %v", err) - } - - if err := os.WriteFile(configFile, configJSON, 0644); err != nil { - t.Fatalf("Failed to write config file: %v", err) - } - - // Try to read config - should fail with no servers - _, _, err = readGatewayConfig([]string{configFile}) - if err == nil { - t.Error("Expected error for config with no servers, got nil") - } - if err != nil && err.Error() != "no MCP servers configured in configuration" { - t.Errorf("Expected 'no MCP servers configured' error, got: %v", err) - } -} - -func TestReadGatewayConfig_EmptyData(t *testing.T) { - // Create an empty config file - tmpDir := t.TempDir() - configFile := filepath.Join(tmpDir, "empty.json") - - if err := os.WriteFile(configFile, []byte(""), 0644); err != nil { - t.Fatalf("Failed to write empty config file: %v", err) - } - - // Try to read config - should fail with empty data - _, _, err := readGatewayConfig([]string{configFile}) - if err == nil { - t.Error("Expected error for empty config file, got nil") - } - if err != nil && err.Error() != "configuration data is empty" { - t.Errorf("Expected 'configuration data is empty' error, got: %v", err) - } -} - -func TestReadGatewayConfig_MultipleFiles(t *testing.T) { - // Create base config file - tmpDir := t.TempDir() - baseConfig := filepath.Join(tmpDir, "base-config.json") - baseConfigData := MCPGatewayServiceConfig{ - MCPServers: map[string]parser.MCPServerConfig{ - "server1": { - BaseMCPServerConfig: types.BaseMCPServerConfig{ - Command: "command1", - Args: []string{"arg1"}, - }, - }, - "server2": { - BaseMCPServerConfig: types.BaseMCPServerConfig{ - Command: "command2", - Args: []string{"arg2"}, - }, - }, - }, - Gateway: GatewaySettings{ - Port: 8080, - }, - } - - baseJSON, err := json.Marshal(baseConfigData) - if err != nil { - t.Fatalf("Failed to marshal base config: %v", err) - } - if err := os.WriteFile(baseConfig, baseJSON, 0644); err != nil { - t.Fatalf("Failed to write base config: %v", err) - } - - // Create override config file - overrideConfig := filepath.Join(tmpDir, "override-config.json") - overrideConfigData := MCPGatewayServiceConfig{ - MCPServers: map[string]parser.MCPServerConfig{ - "server2": { - BaseMCPServerConfig: types.BaseMCPServerConfig{ - Command: "override-command2", - Args: []string{"override-arg2"}, - }, - }, - "server3": { - BaseMCPServerConfig: types.BaseMCPServerConfig{ - Command: "command3", - Args: []string{"arg3"}, - }, - }, - }, - Gateway: GatewaySettings{ - Port: 9090, - APIKey: "test-key", - }, - } - - overrideJSON, err := json.Marshal(overrideConfigData) - if err != nil { - t.Fatalf("Failed to marshal override config: %v", err) - } - if err := os.WriteFile(overrideConfig, overrideJSON, 0644); err != nil { - t.Fatalf("Failed to write override config: %v", err) - } - - // Read and merge configs - result, _, err := readGatewayConfig([]string{baseConfig, overrideConfig}) - if err != nil { - t.Fatalf("Failed to read configs: %v", err) - } - - // Verify merged config - if len(result.MCPServers) != 3 { - t.Errorf("Expected 3 servers, got %d", len(result.MCPServers)) - } - - // server1 should remain from base - server1, exists := result.MCPServers["server1"] - if !exists { - t.Fatal("server1 not found in merged config") - } - if server1.Command != "command1" { - t.Errorf("Expected server1 command 'command1', got '%s'", server1.Command) - } - - // server2 should be overridden - server2, exists := result.MCPServers["server2"] - if !exists { - t.Fatal("server2 not found in merged config") - } - if server2.Command != "override-command2" { - t.Errorf("Expected server2 command 'override-command2', got '%s'", server2.Command) - } - - // server3 should be added from override - server3, exists := result.MCPServers["server3"] - if !exists { - t.Fatal("server3 not found in merged config") - } - if server3.Command != "command3" { - t.Errorf("Expected server3 command 'command3', got '%s'", server3.Command) - } - - // Gateway settings should be overridden - if result.Gateway.Port != 9090 { - t.Errorf("Expected port 9090, got %d", result.Gateway.Port) - } - if result.Gateway.APIKey != "test-key" { - t.Errorf("Expected API key 'test-key', got '%s'", result.Gateway.APIKey) - } -} - -func TestMergeConfigs(t *testing.T) { - base := &MCPGatewayServiceConfig{ - MCPServers: map[string]parser.MCPServerConfig{ - "server1": {BaseMCPServerConfig: types.BaseMCPServerConfig{Command: "cmd1"}}, - "server2": {BaseMCPServerConfig: types.BaseMCPServerConfig{Command: "cmd2"}}, - }, - Gateway: GatewaySettings{ - Port: 8080, - APIKey: "base-key", - }, - } - - override := &MCPGatewayServiceConfig{ - MCPServers: map[string]parser.MCPServerConfig{ - "server2": {BaseMCPServerConfig: types.BaseMCPServerConfig{Command: "override-cmd2"}}, - "server3": {BaseMCPServerConfig: types.BaseMCPServerConfig{Command: "cmd3"}}, - }, - Gateway: GatewaySettings{ - Port: 9090, - // APIKey not set, should keep base - }, - } - - merged := mergeConfigs(base, override) - - // Check servers - if len(merged.MCPServers) != 3 { - t.Errorf("Expected 3 servers, got %d", len(merged.MCPServers)) - } - - if merged.MCPServers["server1"].Command != "cmd1" { - t.Error("server1 should remain from base") - } - - if merged.MCPServers["server2"].Command != "override-cmd2" { - t.Error("server2 should be overridden") - } - - if merged.MCPServers["server3"].Command != "cmd3" { - t.Error("server3 should be added from override") - } - - // Check gateway settings - if merged.Gateway.Port != 9090 { - t.Error("Port should be overridden") - } - - if merged.Gateway.APIKey != "base-key" { - t.Error("APIKey should be kept from base when not set in override") - } -} - -func TestMergeConfigs_EmptyOverride(t *testing.T) { - base := &MCPGatewayServiceConfig{ - MCPServers: map[string]parser.MCPServerConfig{ - "server1": {BaseMCPServerConfig: types.BaseMCPServerConfig{Command: "cmd1"}}, - }, - Gateway: GatewaySettings{ - Port: 8080, - }, - } - - override := &MCPGatewayServiceConfig{ - MCPServers: map[string]parser.MCPServerConfig{}, - Gateway: GatewaySettings{}, - } - - merged := mergeConfigs(base, override) - - // Should keep base config - if len(merged.MCPServers) != 1 { - t.Errorf("Expected 1 server, got %d", len(merged.MCPServers)) - } - - if merged.Gateway.Port != 8080 { - t.Error("Port should be kept from base") - } -} - -func TestParseGatewayConfig_IncludesSafeInputsAndSafeOutputs(t *testing.T) { - // Create a config with safeinputs, safeoutputs, and other servers - configJSON := `{ - "mcpServers": { - "safeinputs": { - "command": "node", - "args": ["/tmp/gh-aw/safeinputs/mcp-server.cjs"] - }, - "safeoutputs": { - "command": "node", - "args": ["/tmp/gh-aw/safeoutputs/mcp-server.cjs"] - }, - "github": { - "command": "gh", - "args": ["aw", "mcp-server", "--toolsets", "default"] - }, - "custom-server": { - "command": "custom-command", - "args": ["arg1"] - } - }, - "gateway": { - "port": 8080 - } - }` - - config, err := parseGatewayConfig([]byte(configJSON)) - if err != nil { - t.Fatalf("Failed to parse config: %v", err) - } - - // Verify that safeinputs and safeoutputs are included (not filtered) - if _, exists := config.MCPServers["safeinputs"]; !exists { - t.Error("safeinputs should be included") - } - - if _, exists := config.MCPServers["safeoutputs"]; !exists { - t.Error("safeoutputs should be included") - } - - // Verify that other servers are kept - if _, exists := config.MCPServers["github"]; !exists { - t.Error("github server should be kept") - } - - if _, exists := config.MCPServers["custom-server"]; !exists { - t.Error("custom-server should be kept") - } - - // Verify server count - all 4 servers should be present - if len(config.MCPServers) != 4 { - t.Errorf("Expected 4 servers, got %d", len(config.MCPServers)) - } -} - -func TestParseGatewayConfig_TemplateSubstitution(t *testing.T) { - // Set environment variables for testing - t.Setenv("TEST_PORT", "3000") - t.Setenv("TEST_API_KEY", "test-secret-key") - t.Setenv("TEST_ENV_VALUE", "test-value") - - configJSON := `{ - "mcpServers": { - "safeinputs": { - "type": "http", - "url": "http://localhost:${TEST_PORT}", - "headers": { - "Authorization": "Bearer ${TEST_API_KEY}" - }, - "env": { - "CUSTOM_VAR": "${TEST_ENV_VALUE}" - } - } - } - }` - - config, err := parseGatewayConfig([]byte(configJSON)) - if err != nil { - t.Fatalf("Failed to parse config: %v", err) - } - - // Verify URL expansion - safeinputs := config.MCPServers["safeinputs"] - expectedURL := "http://localhost:3000" - if safeinputs.URL != expectedURL { - t.Errorf("Expected URL %s, got %s", expectedURL, safeinputs.URL) - } - - // Verify headers expansion - expectedAuth := "Bearer test-secret-key" - if safeinputs.Headers["Authorization"] != expectedAuth { - t.Errorf("Expected Authorization header %s, got %s", expectedAuth, safeinputs.Headers["Authorization"]) - } - - // Verify env expansion - expectedEnvValue := "test-value" - if safeinputs.Env["CUSTOM_VAR"] != expectedEnvValue { - t.Errorf("Expected env CUSTOM_VAR=%s, got %s", expectedEnvValue, safeinputs.Env["CUSTOM_VAR"]) - } -} - -func TestRewriteMCPConfigForGateway(t *testing.T) { - // Create a temporary config file - tmpDir := t.TempDir() - configFile := filepath.Join(tmpDir, "test-config.json") - - // Initial config with multiple servers - initialConfig := map[string]any{ - "mcpServers": map[string]any{ - "github": map[string]any{ - "command": "gh", - "args": []string{"aw", "mcp-server"}, - }, - "custom": map[string]any{ - "command": "node", - "args": []string{"server.js"}, - }, - }, - "gateway": map[string]any{ - "port": 8080, - }, - } - - initialJSON, _ := json.Marshal(initialConfig) - if err := os.WriteFile(configFile, initialJSON, 0644); err != nil { - t.Fatalf("Failed to write config file: %v", err) - } - - // Create a gateway config (after filtering) - gatewayConfig := &MCPGatewayServiceConfig{ - MCPServers: map[string]parser.MCPServerConfig{ - "github": {BaseMCPServerConfig: types.BaseMCPServerConfig{Command: "gh", - Args: []string{"aw", "mcp-server"}}, - }, - "custom": {BaseMCPServerConfig: types.BaseMCPServerConfig{Command: "node", - Args: []string{"server.js"}}, - }, - }, - Gateway: GatewaySettings{ - Port: 8080, - }, - } - - // Rewrite the config - if err := rewriteMCPConfigForGateway(configFile, gatewayConfig); err != nil { - t.Fatalf("rewriteMCPConfigForGateway failed: %v", err) - } - - // Read back the rewritten config - rewrittenData, err := os.ReadFile(configFile) - if err != nil { - t.Fatalf("Failed to read rewritten config: %v", err) - } - - var rewrittenConfig map[string]any - if err := json.Unmarshal(rewrittenData, &rewrittenConfig); err != nil { - t.Fatalf("Failed to parse rewritten config: %v", err) - } - - // Verify structure - mcpServers, ok := rewrittenConfig["mcpServers"].(map[string]any) - if !ok { - t.Fatal("mcpServers not found or wrong type") - } - - if len(mcpServers) != 2 { - t.Errorf("Expected 2 servers in rewritten config, got %d", len(mcpServers)) - } - - // Verify github server points to gateway - github, ok := mcpServers["github"].(map[string]any) - if !ok { - t.Fatal("github server not found") - } - - githubURL, ok := github["url"].(string) - if !ok { - t.Fatal("github server missing url") - } - - expectedURL := "http://localhost:8080/mcp/github" - if githubURL != expectedURL { - t.Errorf("Expected github URL %s, got %s", expectedURL, githubURL) - } - - // Verify custom server points to gateway - custom, ok := mcpServers["custom"].(map[string]any) - if !ok { - t.Fatal("custom server not found") - } - - customURL, ok := custom["url"].(string) - if !ok { - t.Fatal("custom server missing url") - } - - expectedCustomURL := "http://localhost:8080/mcp/custom" - if customURL != expectedCustomURL { - t.Errorf("Expected custom URL %s, got %s", expectedCustomURL, customURL) - } - - // Verify gateway settings are NOT included in rewritten config - _, hasGateway := rewrittenConfig["gateway"] - if hasGateway { - t.Error("Gateway section should not be included in rewritten config") - } -} - -func TestRewriteMCPConfigForGateway_WithAPIKey(t *testing.T) { - // Create a temporary config file - tmpDir := t.TempDir() - configFile := filepath.Join(tmpDir, "test-config.json") - - initialConfig := map[string]any{ - "mcpServers": map[string]any{ - "github": map[string]any{ - "command": "gh", - "args": []string{"aw", "mcp-server"}, - }, - }, - } - - initialJSON, _ := json.Marshal(initialConfig) - if err := os.WriteFile(configFile, initialJSON, 0644); err != nil { - t.Fatalf("Failed to write config file: %v", err) - } - - // Create a gateway config with API key - gatewayConfig := &MCPGatewayServiceConfig{ - MCPServers: map[string]parser.MCPServerConfig{ - "github": {BaseMCPServerConfig: types.BaseMCPServerConfig{Command: "gh", - Args: []string{"aw", "mcp-server"}}, - }, - }, - Gateway: GatewaySettings{ - Port: 8080, - APIKey: "test-api-key", - }, - } - - // Rewrite the config - if err := rewriteMCPConfigForGateway(configFile, gatewayConfig); err != nil { - t.Fatalf("rewriteMCPConfigForGateway failed: %v", err) - } - - // Read back the rewritten config - rewrittenData, err := os.ReadFile(configFile) - if err != nil { - t.Fatalf("Failed to read rewritten config: %v", err) - } - - var rewrittenConfig map[string]any - if err := json.Unmarshal(rewrittenData, &rewrittenConfig); err != nil { - t.Fatalf("Failed to parse rewritten config: %v", err) - } - - // Verify server has authorization header - mcpServers := rewrittenConfig["mcpServers"].(map[string]any) - github := mcpServers["github"].(map[string]any) - - headers, ok := github["headers"].(map[string]any) - if !ok { - t.Fatal("Expected headers in server config") - } - - auth, ok := headers["Authorization"].(string) - if !ok { - t.Fatal("Expected Authorization header") - } - - expectedAuth := "Bearer test-api-key" - if auth != expectedAuth { - t.Errorf("Expected auth '%s', got '%s'", expectedAuth, auth) - } -} diff --git a/pkg/workflow/gateway.go b/pkg/workflow/gateway.go deleted file mode 100644 index ad327411240..00000000000 --- a/pkg/workflow/gateway.go +++ /dev/null @@ -1,312 +0,0 @@ -package workflow - -import ( - "fmt" - "sort" - "strings" - - "github.com/githubnext/gh-aw/pkg/logger" -) - -var gatewayLog = logger.New("workflow:gateway") - -const ( - // DefaultMCPGatewayPort is the default port for the MCP gateway - DefaultMCPGatewayPort = 8080 - // MCPGatewayLogsFolder is the folder where MCP gateway logs are stored - MCPGatewayLogsFolder = "/tmp/gh-aw/mcp-gateway-logs" -) - -// isMCPGatewayEnabled checks if the MCP gateway feature is enabled for the workflow -func isMCPGatewayEnabled(workflowData *WorkflowData) bool { - if workflowData == nil { - return false - } - - // Check if sandbox.mcp is configured - if workflowData.SandboxConfig == nil { - return false - } - if workflowData.SandboxConfig.MCP == nil { - return false - } - - // MCP gateway is enabled by default when sandbox.mcp is configured - return true -} - -// getMCPGatewayConfig extracts the MCPGatewayRuntimeConfig from sandbox configuration -func getMCPGatewayConfig(workflowData *WorkflowData) *MCPGatewayRuntimeConfig { - if workflowData == nil || workflowData.SandboxConfig == nil { - return nil - } - - return workflowData.SandboxConfig.MCP -} - -// generateMCPGatewaySteps generates the steps to start and verify the MCP gateway -func generateMCPGatewaySteps(workflowData *WorkflowData, mcpEnvVars map[string]string) []GitHubActionStep { - if !isMCPGatewayEnabled(workflowData) { - return nil - } - - config := getMCPGatewayConfig(workflowData) - if config == nil { - return nil - } - - gatewayLog.Printf("Generating MCP gateway steps: port=%d, container=%s, command=%s, env_vars=%d", - config.Port, config.Container, config.Command, len(mcpEnvVars)) - - var steps []GitHubActionStep - - // Step 1: Start MCP Gateway (background process) - startStep := generateMCPGatewayStartStep(config, mcpEnvVars) - steps = append(steps, startStep) - - // Step 2: Health check to verify gateway is running - healthCheckStep := generateMCPGatewayHealthCheckStep(config) - steps = append(steps, healthCheckStep) - - return steps -} - -// generateMCPGatewayDownloadStep generates the step that downloads the awmg binary - -// generateMCPGatewayStartStep generates the step that starts the MCP gateway -func generateMCPGatewayStartStep(config *MCPGatewayRuntimeConfig, mcpEnvVars map[string]string) GitHubActionStep { - gatewayLog.Print("Generating MCP gateway start step") - - port, err := validateAndNormalizePort(config.Port) - if err != nil { - // In case of validation error, log and use default port - // This shouldn't happen in practice as validation should catch it earlier - gatewayLog.Printf("Warning: %v, using default port %d", err, DefaultMCPGatewayPort) - port = DefaultMCPGatewayPort - } - - // MCP config file path (created by RenderMCPConfig) - mcpConfigPath := "/home/runner/.copilot/mcp-config.json" - - stepLines := []string{ - " - name: Start MCP Gateway", - } - - // Add env block if there are environment variables to pass through - if len(mcpEnvVars) > 0 { - stepLines = append(stepLines, " env:") - - // Sort environment variable names for consistent output - envVarNames := make([]string, 0, len(mcpEnvVars)) - for envVarName := range mcpEnvVars { - envVarNames = append(envVarNames, envVarName) - } - sort.Strings(envVarNames) - - // Write environment variables in sorted order - for _, envVarName := range envVarNames { - envVarValue := mcpEnvVars[envVarName] - stepLines = append(stepLines, fmt.Sprintf(" %s: %s", envVarName, envVarValue)) - } - } - - stepLines = append(stepLines, - " run: |", - " mkdir -p "+MCPGatewayLogsFolder, - " echo 'Starting MCP Gateway...'", - " ", - ) - - // Check which mode to use: container or command (both are required) - if config.Container != "" { - // Container mode - gatewayLog.Printf("Using container mode: %s", config.Container) - stepLines = append(stepLines, generateContainerStartCommands(config, mcpConfigPath, port)...) - } else if config.Command != "" { - // Custom command mode - gatewayLog.Printf("Using custom command mode: %s", config.Command) - stepLines = append(stepLines, generateCommandStartCommands(config, mcpConfigPath, port)...) - } else { - // Error: neither container nor command specified - gatewayLog.Print("ERROR: Neither container nor command specified for MCP gateway") - stepLines = append(stepLines, - " echo 'ERROR: sandbox.mcp must specify either container or command'", - " echo 'Example container mode: sandbox.mcp.container: \"ghcr.io/githubnext/gh-aw-mcpg:latest\"'", - " echo 'Example command mode: sandbox.mcp.command: \"./custom-gateway\"'", - " exit 1", - ) - } - - return GitHubActionStep(stepLines) -} - -// generateContainerStartCommands generates shell commands to start the MCP gateway using a Docker container -func generateContainerStartCommands(config *MCPGatewayRuntimeConfig, mcpConfigPath string, port int) []string { - var lines []string - - // Build environment variables - var envFlags []string - if len(config.Env) > 0 { - for key, value := range config.Env { - envFlags = append(envFlags, fmt.Sprintf("-e %s=\"%s\"", key, value)) - } - } - envFlagsStr := strings.Join(envFlags, " ") - - // Build docker run command with args - dockerCmd := "docker run" - - // Add args (e.g., --rm, -i, -v, -p) - if len(config.Args) > 0 { - for _, arg := range config.Args { - dockerCmd += " " + arg - } - } - - // Add environment variables - if envFlagsStr != "" { - dockerCmd += " " + envFlagsStr - } - - // Add container image - containerImage := config.Container - if config.Version != "" { - containerImage += ":" + config.Version - } - dockerCmd += " " + containerImage - - // Add entrypoint args - if len(config.EntrypointArgs) > 0 { - for _, arg := range config.EntrypointArgs { - dockerCmd += " " + arg - } - } - - lines = append(lines, - " # Start MCP gateway using Docker container", - fmt.Sprintf(" echo 'Starting MCP Gateway container: %s'", config.Container), - " ", - " # Pipe MCP config to container via stdin", - fmt.Sprintf(" cat %s | %s > %s/gateway.log 2>&1 &", mcpConfigPath, dockerCmd, MCPGatewayLogsFolder), - " GATEWAY_PID=$!", - " echo \"MCP Gateway container started with PID $GATEWAY_PID\"", - " ", - " # Give the gateway a moment to start", - " sleep 2", - ) - - return lines -} - -// generateCommandStartCommands generates shell commands to start the MCP gateway using a custom command -func generateCommandStartCommands(config *MCPGatewayRuntimeConfig, mcpConfigPath string, port int) []string { - var lines []string - - // Build the command with args - command := config.Command - if len(config.Args) > 0 { - command += " " + strings.Join(config.Args, " ") - } - - // Build environment variables - var envVars []string - if len(config.Env) > 0 { - for key, value := range config.Env { - envVars = append(envVars, fmt.Sprintf("export %s=\"%s\"", key, value)) - } - } - - lines = append(lines, - " # Start MCP gateway using custom command", - fmt.Sprintf(" echo 'Starting MCP Gateway with command: %s'", config.Command), - " ", - ) - - // Add environment variables if any - if len(envVars) > 0 { - lines = append(lines, " # Set environment variables") - for _, envVar := range envVars { - lines = append(lines, " "+envVar) - } - lines = append(lines, " ") - } - - lines = append(lines, - " # Start the command in background", - fmt.Sprintf(" cat %s | %s > %s/gateway.log 2>&1 &", mcpConfigPath, command, MCPGatewayLogsFolder), - " GATEWAY_PID=$!", - " echo \"MCP Gateway started with PID $GATEWAY_PID\"", - " ", - " # Give the gateway a moment to start", - " sleep 2", - ) - - return lines -} - -// generateMCPGatewayHealthCheckStep generates the step that pings the gateway to verify it's running -func generateMCPGatewayHealthCheckStep(config *MCPGatewayRuntimeConfig) GitHubActionStep { - gatewayLog.Print("Generating MCP gateway health check step") - - port, err := validateAndNormalizePort(config.Port) - if err != nil { - // In case of validation error, log and use default port - // This shouldn't happen in practice as validation should catch it earlier - gatewayLog.Printf("Warning: %v, using default port %d", err, DefaultMCPGatewayPort) - port = DefaultMCPGatewayPort - } - - gatewayURL := fmt.Sprintf("http://localhost:%d", port) - - // MCP config file path (created by RenderMCPConfig) - mcpConfigPath := "/home/runner/.copilot/mcp-config.json" - - // Call the bundled shell script to verify gateway health - stepLines := []string{ - " - name: Verify MCP Gateway Health", - fmt.Sprintf(" run: bash /tmp/gh-aw/actions/verify_mcp_gateway_health.sh \"%s\" \"%s\" \"%s\"", gatewayURL, mcpConfigPath, MCPGatewayLogsFolder), - } - - return GitHubActionStep(stepLines) -} - -// getMCPGatewayURL returns the HTTP URL for the MCP gateway -func getMCPGatewayURL(config *MCPGatewayRuntimeConfig) string { - port, err := validateAndNormalizePort(config.Port) - if err != nil { - // In case of validation error, log and use default port - // This shouldn't happen in practice as validation should catch it earlier - gatewayLog.Printf("Warning: %v, using default port %d", err, DefaultMCPGatewayPort) - port = DefaultMCPGatewayPort - } - return fmt.Sprintf("http://localhost:%d", port) -} - -// transformMCPConfigForGateway transforms the MCP server configuration to use the gateway URL -// instead of individual server configurations -func transformMCPConfigForGateway(mcpServers map[string]any, gatewayConfig *MCPGatewayRuntimeConfig) map[string]any { - if gatewayConfig == nil { - return mcpServers - } - - gatewayLog.Print("Transforming MCP config for gateway") - - gatewayURL := getMCPGatewayURL(gatewayConfig) - - // Create a new config that points all servers to the gateway - transformed := make(map[string]any) - for serverName := range mcpServers { - transformed[serverName] = map[string]any{ - "type": "http", - "url": fmt.Sprintf("%s/mcp/%s", gatewayURL, serverName), - } - // Add API key header if configured - if gatewayConfig.APIKey != "" { - transformed[serverName].(map[string]any)["headers"] = map[string]any{ - "Authorization": "Bearer ${MCP_GATEWAY_API_KEY}", - } - } - } - - return transformed -} diff --git a/pkg/workflow/gateway_test.go b/pkg/workflow/gateway_test.go deleted file mode 100644 index 7a9ad29e8f0..00000000000 --- a/pkg/workflow/gateway_test.go +++ /dev/null @@ -1,878 +0,0 @@ -package workflow - -import ( - "fmt" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestParseMCPGatewayTool(t *testing.T) { - tests := []struct { - name string - input any - expected *MCPGatewayRuntimeConfig - }{ - { - name: "nil input returns nil", - input: nil, - expected: nil, - }, - { - name: "non-map input returns nil", - input: "not a map", - expected: nil, - }, - { - name: "minimal config with port only", - input: map[string]any{ - "port": 8080, - }, - expected: &MCPGatewayRuntimeConfig{ - Port: 8080, - }, - }, - { - name: "full config", - input: map[string]any{ - "port": 8888, - "api-key": "${{ secrets.API_KEY }}", - "args": []any{"-v", "--debug"}, - "entrypointArgs": []any{"--config", "/config.json"}, - "env": map[string]any{ - "DEBUG": "true", - }, - }, - expected: &MCPGatewayRuntimeConfig{ - Port: 8888, - APIKey: "${{ secrets.API_KEY }}", - Args: []string{"-v", "--debug"}, - EntrypointArgs: []string{"--config", "/config.json"}, - Env: map[string]string{"DEBUG": "true"}, - }, - }, - { - name: "empty config", - input: map[string]any{}, - expected: &MCPGatewayRuntimeConfig{ - Port: DefaultMCPGatewayPort, - }, - }, - { - name: "float port", - input: map[string]any{ - "port": 8888.0, - }, - expected: &MCPGatewayRuntimeConfig{ - Port: 8888, - }, - }, - { - name: "uint64 port (YAML parser default)", - input: map[string]any{ - "port": uint64(8000), - }, - expected: &MCPGatewayRuntimeConfig{ - Port: 8000, - }, - }, - { - name: "int64 port", - input: map[string]any{ - "port": int64(9000), - }, - expected: &MCPGatewayRuntimeConfig{ - Port: 9000, - }, - }, - { - name: "container mode with full configuration", - input: map[string]any{ - "container": "ghcr.io/githubnext/gh-aw-mcpg:latest", - "args": []any{ - "--rm", - "-i", - "-v", - "/var/run/docker.sock:/var/run/docker.sock", - "-p", - "8000:8000", - }, - "entrypointArgs": []any{ - "--routed", - "--listen", - "0.0.0.0:8000", - "--config-stdin", - }, - "port": uint64(8000), - "env": map[string]any{ - "DOCKER_API_VERSION": "1.44", - "GITHUB_TOKEN": "${{ secrets.GITHUB_TOKEN }}", - }, - }, - expected: &MCPGatewayRuntimeConfig{ - Container: "ghcr.io/githubnext/gh-aw-mcpg:latest", - Args: []string{ - "--rm", - "-i", - "-v", - "/var/run/docker.sock:/var/run/docker.sock", - "-p", - "8000:8000", - }, - EntrypointArgs: []string{ - "--routed", - "--listen", - "0.0.0.0:8000", - "--config-stdin", - }, - Port: 8000, - Env: map[string]string{ - "DOCKER_API_VERSION": "1.44", - "GITHUB_TOKEN": "${{ secrets.GITHUB_TOKEN }}", - }, - }, - }, - { - name: "command mode with full configuration", - input: map[string]any{ - "command": "./custom-gateway", - "args": []any{ - "--port", - "9000", - }, - "port": uint64(9000), - "env": map[string]any{ - "LOG_LEVEL": "debug", - }, - }, - expected: &MCPGatewayRuntimeConfig{ - Command: "./custom-gateway", - Args: []string{ - "--port", - "9000", - }, - Port: 9000, - Env: map[string]string{ - "LOG_LEVEL": "debug", - }, - }, - }, - { - name: "config with domain", - input: map[string]any{ - "port": 8080, - "domain": "host.docker.internal", - }, - expected: &MCPGatewayRuntimeConfig{ - Port: 8080, - Domain: "host.docker.internal", - }, - }, - { - name: "config with localhost domain", - input: map[string]any{ - "port": 8080, - "domain": "localhost", - }, - expected: &MCPGatewayRuntimeConfig{ - Port: 8080, - Domain: "localhost", - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := parseMCPGatewayTool(tt.input) - if tt.expected == nil { - assert.Nil(t, result) - } else { - require.NotNil(t, result) - assert.Equal(t, tt.expected.Container, result.Container) - assert.Equal(t, tt.expected.Version, result.Version) - assert.Equal(t, tt.expected.Port, result.Port) - assert.Equal(t, tt.expected.APIKey, result.APIKey) - assert.Equal(t, tt.expected.Domain, result.Domain) - assert.Equal(t, tt.expected.Args, result.Args) - assert.Equal(t, tt.expected.EntrypointArgs, result.EntrypointArgs) - assert.Equal(t, tt.expected.Env, result.Env) - } - }) - } -} - -func TestIsMCPGatewayEnabled(t *testing.T) { - tests := []struct { - name string - data *WorkflowData - expected bool - }{ - { - name: "nil workflow data", - data: nil, - expected: false, - }, - { - name: "nil sandbox config", - data: &WorkflowData{ - SandboxConfig: nil, - }, - expected: false, - }, - { - name: "no mcp in sandbox", - data: &WorkflowData{ - SandboxConfig: &SandboxConfig{ - Agent: &AgentSandboxConfig{Type: SandboxTypeAWF}, - }, - }, - expected: false, - }, - { - name: "sandbox.mcp configured", - data: &WorkflowData{ - SandboxConfig: &SandboxConfig{ - MCP: &MCPGatewayRuntimeConfig{ - Port: 8080, - }, - }, - }, - expected: true, - }, - { - name: "sandbox.mcp with empty config", - data: &WorkflowData{ - SandboxConfig: &SandboxConfig{ - MCP: &MCPGatewayRuntimeConfig{}, - }, - }, - expected: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := isMCPGatewayEnabled(tt.data) - assert.Equal(t, tt.expected, result) - }) - } -} - -func TestGetMCPGatewayConfig(t *testing.T) { - tests := []struct { - name string - data *WorkflowData - hasConfig bool - }{ - { - name: "nil workflow data", - data: nil, - hasConfig: false, - }, - { - name: "no mcp in sandbox", - data: &WorkflowData{ - SandboxConfig: &SandboxConfig{ - Agent: &AgentSandboxConfig{Type: SandboxTypeAWF}, - }, - }, - hasConfig: false, - }, - { - name: "valid sandbox.mcp config", - data: &WorkflowData{ - SandboxConfig: &SandboxConfig{ - MCP: &MCPGatewayRuntimeConfig{ - Port: 9090, - }, - }, - }, - hasConfig: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := getMCPGatewayConfig(tt.data) - if tt.hasConfig { - require.NotNil(t, result) - assert.Equal(t, 9090, result.Port) - } else { - assert.Nil(t, result) - } - }) - } -} - -func TestGenerateMCPGatewaySteps(t *testing.T) { - tests := []struct { - name string - data *WorkflowData - mcpEnvVars map[string]string - expectSteps int - }{ - { - name: "gateway disabled returns no steps", - data: &WorkflowData{}, - mcpEnvVars: map[string]string{}, - expectSteps: 0, - }, - { - name: "gateway enabled returns two steps", - data: &WorkflowData{ - SandboxConfig: &SandboxConfig{ - MCP: &MCPGatewayRuntimeConfig{ - Container: "ghcr.io/githubnext/gh-aw-mcpg:latest", - Port: 8080, - }, - }, - Features: map[string]any{ - "mcp-gateway": true, - }, - }, - mcpEnvVars: map[string]string{}, - expectSteps: 2, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - steps := generateMCPGatewaySteps(tt.data, tt.mcpEnvVars) - assert.Len(t, steps, tt.expectSteps) - }) - } -} - -func TestGenerateMCPGatewayHealthCheckStep(t *testing.T) { - config := &MCPGatewayRuntimeConfig{ - Port: 8080, - } - - step := generateMCPGatewayHealthCheckStep(config) - stepStr := strings.Join(step, "\n") - - assert.Contains(t, stepStr, "Verify MCP Gateway Health") - assert.Contains(t, stepStr, "bash /tmp/gh-aw/actions/verify_mcp_gateway_health.sh") - assert.Contains(t, stepStr, "http://localhost:8080") - assert.Contains(t, stepStr, "/home/runner/.copilot/mcp-config.json") - assert.Contains(t, stepStr, MCPGatewayLogsFolder) -} - -func TestGenerateMCPGatewayHealthCheckStep_UsesCorrectPort(t *testing.T) { - config := &MCPGatewayRuntimeConfig{ - Port: 8080, - } - - // Test that the health check uses the configured port - step := generateMCPGatewayHealthCheckStep(config) - stepStr := strings.Join(step, "\n") - - // Should include health check with correct port - assert.Contains(t, stepStr, "Verify MCP Gateway Health") - assert.Contains(t, stepStr, "http://localhost:8080") - assert.Contains(t, stepStr, "bash /tmp/gh-aw/actions/verify_mcp_gateway_health.sh") -} - -func TestGenerateMCPGatewayHealthCheckStep_IncludesMCPConfig(t *testing.T) { - config := &MCPGatewayRuntimeConfig{ - Port: 8080, - } - - // Test that health check includes MCP config path - step := generateMCPGatewayHealthCheckStep(config) - stepStr := strings.Join(step, "\n") - - // Should include MCP config path - assert.Contains(t, stepStr, "/home/runner/.copilot/mcp-config.json") - assert.Contains(t, stepStr, MCPGatewayLogsFolder) - - // Should still have basic health check - assert.Contains(t, stepStr, "Verify MCP Gateway Health") - assert.Contains(t, stepStr, "bash /tmp/gh-aw/actions/verify_mcp_gateway_health.sh") -} - -func TestGenerateMCPGatewayHealthCheckStep_GeneratesValidStep(t *testing.T) { - config := &MCPGatewayRuntimeConfig{ - Port: 8080, - } - - // Test that a valid step is generated - step := generateMCPGatewayHealthCheckStep(config) - stepStr := strings.Join(step, "\n") - - // Should generate a valid GitHub Actions step - assert.Contains(t, stepStr, "- name: Verify MCP Gateway Health") - assert.Contains(t, stepStr, "run: bash /tmp/gh-aw/actions/verify_mcp_gateway_health.sh") - assert.Contains(t, stepStr, "http://localhost:8080") -} - -func TestGetMCPGatewayURL(t *testing.T) { - tests := []struct { - name string - config *MCPGatewayRuntimeConfig - expected string - }{ - { - name: "default port", - config: &MCPGatewayRuntimeConfig{}, - expected: "http://localhost:8080", - }, - { - name: "custom port", - config: &MCPGatewayRuntimeConfig{ - Port: 9090, - }, - expected: "http://localhost:9090", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := getMCPGatewayURL(tt.config) - assert.Equal(t, tt.expected, result) - }) - } -} - -func TestTransformMCPConfigForGateway(t *testing.T) { - tests := []struct { - name string - mcpServers map[string]any - config *MCPGatewayRuntimeConfig - expected map[string]any - }{ - { - name: "nil config returns original", - mcpServers: map[string]any{ - "github": map[string]any{"type": "local"}, - }, - config: nil, - expected: map[string]any{ - "github": map[string]any{"type": "local"}, - }, - }, - { - name: "transforms servers to gateway URLs", - mcpServers: map[string]any{ - "github": map[string]any{}, - "playwright": map[string]any{}, - }, - config: &MCPGatewayRuntimeConfig{ - Port: 8080, - }, - expected: map[string]any{ - "github": map[string]any{ - "type": "http", - "url": "http://localhost:8080/mcp/github", - }, - "playwright": map[string]any{ - "type": "http", - "url": "http://localhost:8080/mcp/playwright", - }, - }, - }, - { - name: "adds auth header when api-key present", - mcpServers: map[string]any{ - "github": map[string]any{}, - }, - config: &MCPGatewayRuntimeConfig{ - Port: 8080, - APIKey: "secret", - }, - expected: map[string]any{ - "github": map[string]any{ - "type": "http", - "url": "http://localhost:8080/mcp/github", - "headers": map[string]any{ - "Authorization": "Bearer ${MCP_GATEWAY_API_KEY}", - }, - }, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := transformMCPConfigForGateway(tt.mcpServers, tt.config) - assert.Equal(t, tt.expected, result) - }) - } -} - -func TestSandboxConfigWithMCP(t *testing.T) { - sandboxConfig := &SandboxConfig{ - Agent: &AgentSandboxConfig{ - Type: SandboxTypeAWF, - }, - MCP: &MCPGatewayRuntimeConfig{ - Container: "test-image", - Port: 9000, - }, - } - - require.NotNil(t, sandboxConfig.MCP) - assert.Equal(t, "test-image", sandboxConfig.MCP.Container) - assert.Equal(t, 9000, sandboxConfig.MCP.Port) - - require.NotNil(t, sandboxConfig.Agent) - assert.Equal(t, SandboxTypeAWF, sandboxConfig.Agent.Type) -} - -func TestGenerateContainerStartCommands(t *testing.T) { - config := &MCPGatewayRuntimeConfig{ - Container: "ghcr.io/githubnext/gh-aw-mcpg:latest", - Args: []string{"--rm", "-i", "-v", "/var/run/docker.sock:/var/run/docker.sock", "-p", "8000:8000", "--entrypoint", "/app/flowguard-go"}, - EntrypointArgs: []string{"--routed", "--listen", "0.0.0.0:8000", "--config-stdin"}, - Port: 8000, - Env: map[string]string{ - "DOCKER_API_VERSION": "1.44", - }, - } - - mcpConfigPath := "/home/runner/.copilot/mcp-config.json" - lines := generateContainerStartCommands(config, mcpConfigPath, 8000) - output := strings.Join(lines, "\n") - - // Verify container mode is indicated - assert.Contains(t, output, "Start MCP gateway using Docker container") - assert.Contains(t, output, "ghcr.io/githubnext/gh-aw-mcpg:latest") - - // Verify docker run command is constructed correctly - assert.Contains(t, output, "docker run") - assert.Contains(t, output, "--rm") - assert.Contains(t, output, "-i") - assert.Contains(t, output, "-v") - assert.Contains(t, output, "/var/run/docker.sock:/var/run/docker.sock") - assert.Contains(t, output, "-p") - assert.Contains(t, output, "8000:8000") - assert.Contains(t, output, "--entrypoint") - assert.Contains(t, output, "/app/flowguard-go") - - // Verify environment variables are set - assert.Contains(t, output, "-e DOCKER_API_VERSION=\"1.44\"") - - // Verify entrypoint args - assert.Contains(t, output, "--routed") - assert.Contains(t, output, "--listen") - assert.Contains(t, output, "0.0.0.0:8000") - assert.Contains(t, output, "--config-stdin") - - // Verify config is piped via stdin - assert.Contains(t, output, "cat /home/runner/.copilot/mcp-config.json |") - assert.Contains(t, output, MCPGatewayLogsFolder) -} - -func TestGenerateCommandStartCommands(t *testing.T) { - config := &MCPGatewayRuntimeConfig{ - Command: "/usr/local/bin/mcp-gateway", - Args: []string{"--port", "8080", "--verbose"}, - Port: 8080, - Env: map[string]string{ - "LOG_LEVEL": "debug", - "API_KEY": "test-key", - }, - } - - mcpConfigPath := "/home/runner/.copilot/mcp-config.json" - lines := generateCommandStartCommands(config, mcpConfigPath, 8080) - output := strings.Join(lines, "\n") - - // Verify command mode is indicated - assert.Contains(t, output, "Start MCP gateway using custom command") - assert.Contains(t, output, "/usr/local/bin/mcp-gateway") - - // Verify command with args - assert.Contains(t, output, "/usr/local/bin/mcp-gateway --port 8080 --verbose") - - // Verify environment variables are exported - assert.Contains(t, output, "export LOG_LEVEL=\"debug\"") - assert.Contains(t, output, "export API_KEY=\"test-key\"") - - // Verify config is piped via stdin - assert.Contains(t, output, "cat /home/runner/.copilot/mcp-config.json |") - assert.Contains(t, output, MCPGatewayLogsFolder) -} - -func TestGenerateMCPGatewayStartStep_ContainerMode(t *testing.T) { - config := &MCPGatewayRuntimeConfig{ - Container: "ghcr.io/githubnext/gh-aw-mcpg:latest", - Args: []string{"--rm", "-i"}, - EntrypointArgs: []string{"--config-stdin"}, - Port: 8000, - } - mcpEnvVars := map[string]string{} - - step := generateMCPGatewayStartStep(config, mcpEnvVars) - stepStr := strings.Join(step, "\n") - - // Should use container mode - assert.Contains(t, stepStr, "Start MCP Gateway") - assert.Contains(t, stepStr, "docker run") - assert.Contains(t, stepStr, "ghcr.io/githubnext/gh-aw-mcpg:latest") - assert.NotContains(t, stepStr, "awmg") // Should not use awmg -} - -func TestGenerateMCPGatewayStartStep_CommandMode(t *testing.T) { - config := &MCPGatewayRuntimeConfig{ - Command: "/usr/local/bin/custom-gateway", - Args: []string{"--debug"}, - Port: 9000, - } - mcpEnvVars := map[string]string{} - - step := generateMCPGatewayStartStep(config, mcpEnvVars) - stepStr := strings.Join(step, "\n") - - // Should use command mode - assert.Contains(t, stepStr, "Start MCP Gateway") - assert.Contains(t, stepStr, "/usr/local/bin/custom-gateway --debug") - assert.NotContains(t, stepStr, "docker run") // Should not use docker - assert.NotContains(t, stepStr, "awmg") // Should not use awmg -} - -func TestGenerateMCPGatewayStartStep_NoContainerOrCommand(t *testing.T) { - config := &MCPGatewayRuntimeConfig{ - Port: 8080, - } - mcpEnvVars := map[string]string{} - - step := generateMCPGatewayStartStep(config, mcpEnvVars) - stepStr := strings.Join(step, "\n") - - // Should error when neither container nor command is specified - assert.Contains(t, stepStr, "Start MCP Gateway") - assert.Contains(t, stepStr, "ERROR: sandbox.mcp must specify either container or command") - assert.NotContains(t, stepStr, "docker run") // Should not use docker - assert.NotContains(t, stepStr, "/usr/local/bin/custom-gateway") // Should not use custom command -} - -func TestValidateAndNormalizePort(t *testing.T) { - tests := []struct { - name string - port int - expected int - expectError bool - }{ - { - name: "port 0 uses default", - port: 0, - expected: DefaultMCPGatewayPort, - expectError: false, - }, - { - name: "valid port 1", - port: 1, - expected: 1, - expectError: false, - }, - { - name: "valid port 8080", - port: 8080, - expected: 8080, - expectError: false, - }, - { - name: "valid port 65535", - port: 65535, - expected: 65535, - expectError: false, - }, - { - name: "negative port returns error", - port: -1, - expected: 0, - expectError: true, - }, - { - name: "port above 65535 returns error", - port: 65536, - expected: 0, - expectError: true, - }, - { - name: "large negative port returns error", - port: -9999, - expected: 0, - expectError: true, - }, - { - name: "port well above max returns error", - port: 100000, - expected: 0, - expectError: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result, err := validateAndNormalizePort(tt.port) - if tt.expectError { - require.Error(t, err) - assert.Contains(t, err.Error(), "port must be between 1 and 65535") - assert.Contains(t, err.Error(), fmt.Sprintf("%d", tt.port)) - } else { - require.NoError(t, err) - assert.Equal(t, tt.expected, result) - } - }) - } -} - -func TestGenerateMCPGatewayStartStepWithInvalidPort(t *testing.T) { - tests := []struct { - name string - port int - expectsInLog bool - }{ - { - name: "negative port falls back to default", - port: -1, - expectsInLog: true, - }, - { - name: "port above 65535 falls back to default", - port: 70000, - expectsInLog: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - config := &MCPGatewayRuntimeConfig{ - Container: "ghcr.io/githubnext/gh-aw-mcpg:latest", - Port: tt.port, - } - mcpEnvVars := map[string]string{} - - step := generateMCPGatewayStartStep(config, mcpEnvVars) - stepStr := strings.Join(step, "\n") - - // Should still generate valid step with default port - assert.Contains(t, stepStr, "Start MCP Gateway") - assert.Contains(t, stepStr, "docker run") - }) - } -} - -func TestGenerateMCPGatewayHealthCheckStepWithInvalidPort(t *testing.T) { - tests := []struct { - name string - port int - expectsInLog bool - }{ - { - name: "negative port falls back to default", - port: -1, - expectsInLog: true, - }, - { - name: "port above 65535 falls back to default", - port: 70000, - expectsInLog: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - config := &MCPGatewayRuntimeConfig{ - Port: tt.port, - } - - step := generateMCPGatewayHealthCheckStep(config) - stepStr := strings.Join(step, "\n") - - // Should still generate valid step with default port - assert.Contains(t, stepStr, "Verify MCP Gateway Health") - assert.Contains(t, stepStr, fmt.Sprintf("http://localhost:%d", DefaultMCPGatewayPort)) - }) - } -} - -func TestGetMCPGatewayURLWithInvalidPort(t *testing.T) { - tests := []struct { - name string - port int - expected string - }{ - { - name: "negative port falls back to default", - port: -1, - expected: fmt.Sprintf("http://localhost:%d", DefaultMCPGatewayPort), - }, - { - name: "port above 65535 falls back to default", - port: 70000, - expected: fmt.Sprintf("http://localhost:%d", DefaultMCPGatewayPort), - }, - { - name: "port 0 uses default", - port: 0, - expected: fmt.Sprintf("http://localhost:%d", DefaultMCPGatewayPort), - }, - { - name: "valid port 9090", - port: 9090, - expected: "http://localhost:9090", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - config := &MCPGatewayRuntimeConfig{ - Port: tt.port, - } - - result := getMCPGatewayURL(config) - assert.Equal(t, tt.expected, result) - }) - } -} - -func TestGenerateMCPGatewayStartStep_WithEnvVars(t *testing.T) { - config := &MCPGatewayRuntimeConfig{ - Container: "ghcr.io/githubnext/gh-aw-mcpg:latest", - Port: 8080, - } - mcpEnvVars := map[string]string{ - "GITHUB_MCP_SERVER_TOKEN": "${{ secrets.GITHUB_TOKEN }}", - "GH_AW_SAFE_OUTPUTS": "${{ env.GH_AW_SAFE_OUTPUTS }}", - "GITHUB_TOKEN": "${{ secrets.GITHUB_TOKEN }}", - } - - step := generateMCPGatewayStartStep(config, mcpEnvVars) - stepStr := strings.Join(step, "\n") - - // Should include env block - assert.Contains(t, stepStr, "env:") - // Should include environment variables in alphabetical order - assert.Contains(t, stepStr, "GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }}") - assert.Contains(t, stepStr, "GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GITHUB_TOKEN }}") - assert.Contains(t, stepStr, "GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}") - - // Verify alphabetical ordering (GH_AW_SAFE_OUTPUTS should come before GITHUB_*) - ghAwPos := strings.Index(stepStr, "GH_AW_SAFE_OUTPUTS") - githubMcpPos := strings.Index(stepStr, "GITHUB_MCP_SERVER_TOKEN") - githubTokenPos := strings.Index(stepStr, "GITHUB_TOKEN") - assert.Less(t, ghAwPos, githubMcpPos, "GH_AW_SAFE_OUTPUTS should come before GITHUB_MCP_SERVER_TOKEN") - assert.Less(t, githubMcpPos, githubTokenPos, "GITHUB_MCP_SERVER_TOKEN should come before GITHUB_TOKEN") -} - -func TestGenerateMCPGatewayStartStep_WithoutEnvVars(t *testing.T) { - config := &MCPGatewayRuntimeConfig{ - Container: "ghcr.io/githubnext/gh-aw-mcpg:latest", - Port: 8080, - } - mcpEnvVars := map[string]string{} - - step := generateMCPGatewayStartStep(config, mcpEnvVars) - stepStr := strings.Join(step, "\n") - - // Should NOT include env block when no environment variables - assert.NotContains(t, stepStr, "env:") - // Should still have the run block - assert.Contains(t, stepStr, "run: |") - assert.Contains(t, stepStr, "Start MCP Gateway") -} diff --git a/scripts/test-build-release.sh b/scripts/test-build-release.sh index 7d34c8798d2..ac1ce97489f 100755 --- a/scripts/test-build-release.sh +++ b/scripts/test-build-release.sh @@ -73,13 +73,6 @@ for p in "${platforms[@]}"; do -ldflags="-s -w -X main.version=${VERSION} -X main.isRelease=true" \ -o "dist/${p}${ext}" \ ./cmd/gh-aw - - echo "Building awmg for $p..." - GOOS="$goos" GOARCH="$goarch" go build \ - -trimpath \ - -ldflags="-s -w -X main.version=${VERSION}" \ - -o "dist/awmg-${p}${ext}" \ - ./cmd/awmg done echo "Build complete." @@ -97,12 +90,6 @@ if [ ! -f "dist/linux-amd64" ]; then exit 1 fi -# Check that awmg binary was created -if [ ! -f "dist/awmg-linux-amd64" ]; then - echo "FAIL: awmg binary was not created" - exit 1 -fi - # Check that version is embedded in gh-aw binary BINARY_VERSION=$(./dist/linux-amd64 version 2>&1 | grep -o "v[0-9]\+\.[0-9]\+\.[0-9]\+-test" || echo "") if [ "$BINARY_VERSION" != "$TEST_VERSION" ]; then @@ -113,16 +100,6 @@ fi echo "PASS: gh-aw binary built with correct version: $BINARY_VERSION" -# Check that version is embedded in awmg binary -AWMG_VERSION=$(./dist/awmg-linux-amd64 --version 2>&1 | grep -o "v[0-9]\+\.[0-9]\+\.[0-9]\+-test" || echo "") -if [ "$AWMG_VERSION" != "$TEST_VERSION" ]; then - echo "FAIL: awmg binary version is '$AWMG_VERSION', expected '$TEST_VERSION'" - ./dist/awmg-linux-amd64 --version - exit 1 -fi - -echo "PASS: awmg binary built with correct version: $AWMG_VERSION" - # Test 3: Verify version is not "dev" echo "" echo "Test 3: Verify version is not 'dev'" diff --git a/specs/mcp-gateway.md b/specs/mcp-gateway.md deleted file mode 100644 index 049707c0e86..00000000000 --- a/specs/mcp-gateway.md +++ /dev/null @@ -1,195 +0,0 @@ -# MCP Gateway Implementation Summary - -This document summarizes the implementation of the `awmg` command as requested in the problem statement. - -## Problem Statement Requirements - -The problem statement requested: -1. ✅ Add a mcp-gateway command that implements a minimal MCP proxy application -2. ✅ Integrates by default with the sandbox.mcp extension point -3. ✅ Imports the Claude/Copilot/Codex MCP server JSON configuration file -4. ✅ Starts each MCP servers and mounts an MCP client on each -5. ✅ Mounts an HTTP MCP server that acts as a gateway to the MCP clients -6. ✅ Supports most MCP gestures through the go-MCP SDK -7. ✅ Extensive logging to file (MCP log file folder) -8. ✅ Add step in agent job to download gh-aw CLI if released CLI version or install local build -9. ✅ Enable in smoke-copilot - -## Implementation Details - -### 1. Command Structure (`pkg/cli/mcp_gateway_command.go`) - -**Core Components**: -- `MCPGatewayServiceConfig`: Configuration structure matching Claude/Copilot/Codex format -- `MCPServerConfig`: Individual server configuration (command, args, env, url, container) -- `GatewaySettings`: Gateway-specific settings (port, API key) -- `MCPGatewayServer`: Main server managing multiple MCP sessions - -**Key Functions**: -- `NewMCPGatewayCommand()`: Cobra command definition -- `runMCPGateway()`: Main gateway orchestration -- `readGatewayConfig()`: Reads config from file or stdin -- `initializeSessions()`: Creates MCP sessions for all configured servers -- `createMCPSession()`: Creates individual MCP session with command transport -- `startHTTPServer()`: Starts HTTP server with endpoints - -### 2. HTTP Endpoints - -| Endpoint | Method | Description | -|----------|--------|-------------| -| `/health` | GET | Health check (returns 200 OK) | -| `/servers` | GET | List all configured servers | -| `/mcp/{server}` | POST | Proxy MCP requests to specific server | - -### 3. MCP Protocol Support - -Implemented MCP methods: -- ✅ `initialize` - Server initialization and capabilities exchange -- ✅ `tools/list` - List available tools from server -- ✅ `tools/call` - Call a tool with arguments -- ✅ `resources/list` - List available resources -- ✅ `prompts/list` - List available prompts - -### 4. Transport Support - -| Transport | Status | Description | -|-----------|--------|-------------| -| Command/Stdio | ✅ Implemented | Subprocess with stdin/stdout communication | -| Streamable HTTP | ✅ Implemented | HTTP transport with SSE using go-sdk StreamableClientTransport | -| Docker | ⏳ Planned | Container-based MCP servers | - -### 5. Integration Points - -**Existing Integration** (`pkg/workflow/gateway.go`): -- The workflow compiler already has full support for `sandbox.mcp` configuration -- Generates Docker container steps to run MCP gateway in workflows -- Feature flag: `mcp-gateway` (already implemented) -- The CLI command provides an **alternative** for local development/testing - -**Agent Job Integration**: -- gh-aw CLI installation already handled by `pkg/workflow/mcp_servers.go` -- Detects released vs local builds automatically -- Installs via `gh extension install githubnext/gh-aw` -- Upgrades if already installed - -### 6. Configuration Format - -The gateway accepts configuration matching Claude/Copilot format: - -```json -{ - "mcpServers": { - "gh-aw": { - "command": "gh", - "args": ["aw", "mcp-server"], - "env": { - "DEBUG": "cli:*" - } - }, - "remote-server": { - "url": "http://localhost:3000" - } - }, - "gateway": { - "port": 8080, - "apiKey": "optional-api-key" - } -} -```text - -### 7. Logging - -**Log Structure**: -- Default location: `/tmp/gh-aw/mcp-gateway-logs/` -- One log file per MCP server: `{server-name}.log` -- Main gateway logs via `logger` package with category `cli:mcp_gateway` -- Configurable via `--log-dir` flag - -**Log Contents**: -- Server initialization and connection events -- MCP protocol method calls and responses -- Error messages and stack traces -- Performance metrics (connection times, request durations) - -### 8. Testing - -**Unit Tests** (`pkg/cli/mcp_gateway_command_test.go`): -- ✅ Configuration parsing (from file) -- ✅ Invalid JSON handling -- ✅ Empty servers configuration -- ✅ Different server types (command, url, container) -- ✅ Gateway settings (port, API key) - -**Integration Tests** (`pkg/cli/mcp_gateway_integration_test.go`): -- ✅ Basic gateway startup -- ✅ Health endpoint verification -- ✅ Servers list endpoint -- ✅ Multiple MCP server connections - -### 9. Example Usage - -**From file**: -```bash -awmg --config examples/mcp-gateway-config.json -```text - -**From stdin**: -```bash -echo '{"mcpServers":{"gh-aw":{"command":"gh","args":["aw","mcp-server"]}}}' | awmg -```text - -**Custom port and logs**: -```bash -awmg --config config.json --port 8088 --log-dir /custom/logs -```text - -### 10. Smoke Testing - -The mcp-gateway can be tested in smoke-copilot or any workflow by: - -1. **Using sandbox.mcp** (existing integration): -```yaml -sandbox: - mcp: - # MCP gateway runs as standalone awmg CLI - port: 8080 -features: - - mcp-gateway -```text - -2. **Using CLI command directly**: -```yaml -steps: - - name: Start MCP Gateway - run: | - echo '{"mcpServers":{...}}' | awmg --port 8080 & - sleep 2 -```text - -## Files Changed - -| File | Lines | Purpose | -|------|-------|---------| -| `pkg/cli/mcp_gateway_command.go` | 466 | Main implementation | -| `pkg/cli/mcp_gateway_command_test.go` | 168 | Unit tests | -| `pkg/cli/mcp_gateway_integration_test.go` | 128 | Integration test | -| `cmd/gh-aw/main.go` | 6 | Register command | -| `docs/mcp-gateway.md` | 50 | Documentation | - -**Total**: ~818 lines of code (including tests and docs) - -## Future Enhancements - -Potential improvements for future versions: -- [x] Streamable HTTP transport support (implemented using go-sdk StreamableClientTransport) -- [ ] Docker container transport -- [ ] WebSocket transport -- [ ] Gateway metrics and monitoring endpoints -- [ ] Configuration hot-reload -- [ ] Rate limiting and request queuing -- [ ] Multi-region gateway support -- [ ] Gateway clustering for high availability - -## Conclusion - -The mcp-gateway command is **fully implemented and tested**, meeting all requirements from the problem statement. It provides a robust MCP proxy that can aggregate multiple MCP servers, with comprehensive logging, flexible configuration, and seamless integration with existing workflow infrastructure. From 69907e275f673682adf5cb9651d0189cc16f6bf3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 6 Jan 2026 20:06:28 +0000 Subject: [PATCH 3/6] Remove MCP gateway feature and fix compilation errors Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- docs/src/content/docs/reference/sandbox.md | 20 +-- pkg/workflow/copilot_mcp.go | 26 --- .../frontmatter_extraction_security.go | 13 +- pkg/workflow/gateway_domain_test.go | 151 ---------------- pkg/workflow/gateway_validation.go | 24 --- pkg/workflow/mcp_gateway_constants.go | 8 + pkg/workflow/mcp_servers.go | 8 - pkg/workflow/sandbox.go | 5 +- pkg/workflow/sandbox_mcp_integration_test.go | 169 ------------------ pkg/workflow/sandbox_test.go | 97 ---------- pkg/workflow/sandbox_validation.go | 15 -- specs/gosec.md | 1 - 12 files changed, 20 insertions(+), 517 deletions(-) delete mode 100644 pkg/workflow/gateway_domain_test.go delete mode 100644 pkg/workflow/gateway_validation.go create mode 100644 pkg/workflow/mcp_gateway_constants.go delete mode 100644 pkg/workflow/sandbox_mcp_integration_test.go diff --git a/docs/src/content/docs/reference/sandbox.md b/docs/src/content/docs/reference/sandbox.md index e4fbe65205d..72d9868dc2d 100644 --- a/docs/src/content/docs/reference/sandbox.md +++ b/docs/src/content/docs/reference/sandbox.md @@ -235,35 +235,23 @@ The MCP Gateway routes all MCP server calls through a unified HTTP gateway, enab | `env` | `object` | No | Environment variables for the gateway | :::note[Execution Modes] -The MCP gateway supports three execution modes: +The MCP gateway supports two execution modes: 1. **Custom command** - Use `command` field to specify a custom binary or script 2. **Container** - Use `container` field for Docker-based execution -3. **Default** - If neither `command` nor `container` is specified, uses the standalone `awmg` binary The `command` and `container` fields are mutually exclusive - only one can be specified. +You must specify either `command` or `container` to use the MCP gateway feature. ::: ### How It Works -When MCP gateway is enabled: +When MCP gateway is configured: -1. The gateway starts using one of three execution modes (command, container, or default awmg binary) +1. The gateway starts using the specified execution mode (command or container) 2. A health check verifies the gateway is ready 3. All MCP server configurations are transformed to route through the gateway 4. The gateway receives server configs via a configuration file -### Example: Default Mode (awmg binary) - -```yaml wrap -features: - mcp-gateway: true - -sandbox: - mcp: - port: 8080 - api-key: "${{ secrets.MCP_GATEWAY_API_KEY }}" -``` - ### Example: Custom Command Mode ```yaml wrap diff --git a/pkg/workflow/copilot_mcp.go b/pkg/workflow/copilot_mcp.go index 9fc95629dd8..6592607071d 100644 --- a/pkg/workflow/copilot_mcp.go +++ b/pkg/workflow/copilot_mcp.go @@ -77,32 +77,6 @@ func (e *CopilotEngine) RenderMCPConfig(yaml *strings.Builder, tools map[string] }, } - // Add gateway configuration if MCP gateway is enabled - if workflowData != nil && workflowData.SandboxConfig != nil && workflowData.SandboxConfig.MCP != nil { - copilotMCPLog.Print("MCP gateway is enabled, adding gateway config to MCP config") - - // Copy the gateway config to avoid modifying the original - gatewayConfig := *workflowData.SandboxConfig.MCP - - // Set the domain based on whether sandbox.agent is enabled - // If no domain is explicitly configured, determine it based on firewall status - if gatewayConfig.Domain == "" { - // Check if sandbox.agent is enabled (firewall running) - // When firewall is running, awmg runs in a container and needs host.docker.internal - // When firewall is disabled, awmg runs on host and uses localhost - isFirewallEnabled := !isFirewallDisabledBySandboxAgent(workflowData) - if isFirewallEnabled { - gatewayConfig.Domain = "host.docker.internal" - copilotMCPLog.Print("Firewall enabled: using host.docker.internal for gateway domain") - } else { - gatewayConfig.Domain = "localhost" - copilotMCPLog.Print("Firewall disabled: using localhost for gateway domain") - } - } - - options.GatewayConfig = &gatewayConfig - } - RenderJSONMCPConfig(yaml, tools, mcpTools, workflowData, options) //GITHUB_COPILOT_CLI_MODE yaml.WriteString(" echo \"HOME: $HOME\"\n") diff --git a/pkg/workflow/frontmatter_extraction_security.go b/pkg/workflow/frontmatter_extraction_security.go index e3e3a69b7ee..c23c7cb6d65 100644 --- a/pkg/workflow/frontmatter_extraction_security.go +++ b/pkg/workflow/frontmatter_extraction_security.go @@ -161,15 +161,14 @@ func (c *Compiler) extractSandboxConfig(frontmatter map[string]any) *SandboxConf } if mcpVal, hasMCP := sandboxObj["mcp"]; hasMCP { - frontmatterExtractionSecurityLog.Print("Extracting MCP sandbox configuration") - if mcpObj, ok := mcpVal.(map[string]any); ok { - config.MCP = parseMCPGatewayTool(mcpObj) - } + frontmatterExtractionSecurityLog.Print("Unsupported MCP gateway configuration (removed)") + // MCP gateway (awmg) has been removed - this configuration is no longer supported + _ = mcpVal // Silence unused variable warning } - // If we found agent or mcp fields, return the new format config - if config.Agent != nil || config.MCP != nil { - frontmatterExtractionSecurityLog.Print("Sandbox configured with new format (agent/mcp)") + // If we found agent field, return the new format config + if config.Agent != nil { + frontmatterExtractionSecurityLog.Print("Sandbox configured with new format (agent)") return config } diff --git a/pkg/workflow/gateway_domain_test.go b/pkg/workflow/gateway_domain_test.go deleted file mode 100644 index 4293564add9..00000000000 --- a/pkg/workflow/gateway_domain_test.go +++ /dev/null @@ -1,151 +0,0 @@ -package workflow - -import ( - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestGatewayDomainConfiguration(t *testing.T) { - tests := []struct { - name string - workflowData *WorkflowData - expectedDomain string - }{ - { - name: "firewall enabled - should use host.docker.internal", - workflowData: &WorkflowData{ - SandboxConfig: &SandboxConfig{ - Agent: &AgentSandboxConfig{ - Disabled: false, - }, - MCP: &MCPGatewayRuntimeConfig{ - Port: 8080, - }, - }, - }, - expectedDomain: "host.docker.internal", - }, - { - name: "firewall disabled - should use localhost", - workflowData: &WorkflowData{ - SandboxConfig: &SandboxConfig{ - Agent: &AgentSandboxConfig{ - Disabled: true, - }, - MCP: &MCPGatewayRuntimeConfig{ - Port: 8080, - }, - }, - }, - expectedDomain: "localhost", - }, - { - name: "no agent config - should use host.docker.internal (default enabled)", - workflowData: &WorkflowData{ - SandboxConfig: &SandboxConfig{ - MCP: &MCPGatewayRuntimeConfig{ - Port: 8080, - }, - }, - }, - expectedDomain: "host.docker.internal", - }, - { - name: "explicit domain overrides auto-detection", - workflowData: &WorkflowData{ - SandboxConfig: &SandboxConfig{ - Agent: &AgentSandboxConfig{ - Disabled: false, - }, - MCP: &MCPGatewayRuntimeConfig{ - Port: 8080, - Domain: "custom.domain.com", - }, - }, - }, - expectedDomain: "custom.domain.com", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a copilot engine - engine := &CopilotEngine{} - - // Prepare test data - tools := map[string]any{} - mcpTools := []string{} - - // Render MCP config - var yaml strings.Builder - engine.RenderMCPConfig(&yaml, tools, mcpTools, tt.workflowData) - - // Check that the domain is in the rendered config - output := yaml.String() - if tt.expectedDomain != "" { - assert.Contains(t, output, "\"domain\": \""+tt.expectedDomain+"\"", - "Expected domain %s to be in rendered config", tt.expectedDomain) - } - }) - } -} - -func TestGatewayDomainInRenderedJSON(t *testing.T) { - workflowData := &WorkflowData{ - SandboxConfig: &SandboxConfig{ - Agent: &AgentSandboxConfig{ - Disabled: false, // Firewall enabled - }, - MCP: &MCPGatewayRuntimeConfig{ - Port: 8080, - APIKey: "test-key", - }, - }, - } - - engine := &CopilotEngine{} - tools := map[string]any{} - mcpTools := []string{} - - var yaml strings.Builder - engine.RenderMCPConfig(&yaml, tools, mcpTools, workflowData) - - output := yaml.String() - - // Verify the gateway section is present - require.Contains(t, output, "\"gateway\":", "Gateway section should be present") - require.Contains(t, output, "\"port\": 8080", "Port should be present") - require.Contains(t, output, "\"apiKey\": \"test-key\"", "API key should be present") - require.Contains(t, output, "\"domain\": \"host.docker.internal\"", - "Domain should be set to host.docker.internal when firewall is enabled") -} - -func TestGatewayDomainLocalhostWhenFirewallDisabled(t *testing.T) { - workflowData := &WorkflowData{ - SandboxConfig: &SandboxConfig{ - Agent: &AgentSandboxConfig{ - Disabled: true, // Firewall disabled - }, - MCP: &MCPGatewayRuntimeConfig{ - Port: 8080, - }, - }, - } - - engine := &CopilotEngine{} - tools := map[string]any{} - mcpTools := []string{} - - var yaml strings.Builder - engine.RenderMCPConfig(&yaml, tools, mcpTools, workflowData) - - output := yaml.String() - - // Verify the domain is set to localhost - require.Contains(t, output, "\"gateway\":", "Gateway section should be present") - require.Contains(t, output, "\"domain\": \"localhost\"", - "Domain should be set to localhost when firewall is disabled") -} diff --git a/pkg/workflow/gateway_validation.go b/pkg/workflow/gateway_validation.go deleted file mode 100644 index 4de098f630a..00000000000 --- a/pkg/workflow/gateway_validation.go +++ /dev/null @@ -1,24 +0,0 @@ -// Package workflow provides gateway validation functions for agentic workflow compilation. -// -// This file contains domain-specific validation functions for MCP gateway configuration: -// - validateAndNormalizePort() - Validates and normalizes gateway port values -// -// These validation functions are organized in a dedicated file following the validation -// architecture pattern where domain-specific validation belongs in domain validation files. -// See validation.go for the complete validation architecture documentation. -package workflow - -// validateAndNormalizePort validates the port value and returns the normalized port or an error -func validateAndNormalizePort(port int) (int, error) { - // If port is 0, use the default - if port == 0 { - return DefaultMCPGatewayPort, nil - } - - // Validate port is in valid range (1-65535) - if err := validateIntRange(port, 1, 65535, "port"); err != nil { - return 0, err - } - - return port, nil -} diff --git a/pkg/workflow/mcp_gateway_constants.go b/pkg/workflow/mcp_gateway_constants.go new file mode 100644 index 00000000000..57a69cc4185 --- /dev/null +++ b/pkg/workflow/mcp_gateway_constants.go @@ -0,0 +1,8 @@ +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 +) diff --git a/pkg/workflow/mcp_servers.go b/pkg/workflow/mcp_servers.go index 7b94a583138..57ee0a0c271 100644 --- a/pkg/workflow/mcp_servers.go +++ b/pkg/workflow/mcp_servers.go @@ -462,14 +462,6 @@ func (c *Compiler) generateMCPSetup(yaml *strings.Builder, tools map[string]any, yaml.WriteString(" run: |\n") yaml.WriteString(" mkdir -p /tmp/gh-aw/mcp-config\n") engine.RenderMCPConfig(yaml, tools, mcpTools, workflowData) - - // Generate MCP gateway steps if configured (after Setup MCPs completes) - gatewaySteps := generateMCPGatewaySteps(workflowData, mcpEnvVars) - for _, step := range gatewaySteps { - for _, line := range step { - yaml.WriteString(line + "\n") - } - } } func getGitHubDockerImageVersion(githubTool any) string { diff --git a/pkg/workflow/sandbox.go b/pkg/workflow/sandbox.go index e5560512b7b..3e1e5fbade1 100644 --- a/pkg/workflow/sandbox.go +++ b/pkg/workflow/sandbox.go @@ -32,12 +32,11 @@ const ( ) // SandboxConfig represents the top-level sandbox configuration from front matter -// New format: { agent: "awf"|"srt"|{type, config}, mcp: {...} } +// New format: { agent: "awf"|"srt"|{type, config} } // Legacy format: "default"|"sandbox-runtime" or { type, config } type SandboxConfig struct { // New fields - Agent *AgentSandboxConfig `yaml:"agent,omitempty"` // Agent sandbox configuration - MCP *MCPGatewayRuntimeConfig `yaml:"mcp,omitempty"` // MCP gateway configuration + Agent *AgentSandboxConfig `yaml:"agent,omitempty"` // Agent sandbox configuration // Legacy fields (for backward compatibility) Type SandboxType `yaml:"type,omitempty"` // Sandbox type: "default" or "sandbox-runtime" diff --git a/pkg/workflow/sandbox_mcp_integration_test.go b/pkg/workflow/sandbox_mcp_integration_test.go deleted file mode 100644 index 69b220b70ca..00000000000 --- a/pkg/workflow/sandbox_mcp_integration_test.go +++ /dev/null @@ -1,169 +0,0 @@ -package workflow - -import ( - "os" - "path/filepath" - "strings" - "testing" - - "github.com/githubnext/gh-aw/pkg/testutil" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestSandboxMCPContainerConfiguration(t *testing.T) { - // Test workflow with exact configuration from problem statement - workflow := `--- -name: Test Sandbox MCP Container -engine: copilot -on: workflow_dispatch -sandbox: - agent: awf - mcp: - container: "ghcr.io/githubnext/gh-aw-mcpg:latest" - args: - - "--rm" - - "-i" - - "-v" - - "/var/run/docker.sock:/var/run/docker.sock" - - "-p" - - "8000:8000" - - "--entrypoint" - - "/app/flowguard-go" - entrypointArgs: - - "--routed" - - "--listen" - - "0.0.0.0:8000" - - "--config-stdin" - port: 8000 - env: - DOCKER_API_VERSION: "1.44" - GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" -tools: - github: - mode: remote - toolsets: [default] -permissions: - issues: read - pull-requests: read ---- - -Test workflow for sandbox MCP container configuration. -` - - tmpDir := testutil.TempDir(t, "sandbox-mcp-container-test") - testFile := filepath.Join(tmpDir, "test-workflow.md") - err := os.WriteFile(testFile, []byte(workflow), 0644) - require.NoError(t, err) - - // Compile the workflow - compiler := NewCompiler(false, "", "test") - compiler.SetStrictMode(false) - err = compiler.CompileWorkflow(testFile) - require.NoError(t, err) - - // Read the compiled lock file - lockFile := filepath.Join(tmpDir, "test-workflow.lock.yml") - lockFileContent, err := os.ReadFile(lockFile) - require.NoError(t, err) - require.NotEmpty(t, lockFileContent) - - // Verify the compiled workflow contains the correct container configuration - lockFileStr := string(lockFileContent) - - // Check container start command - assert.Contains(t, lockFileStr, "Start MCP Gateway") - assert.Contains(t, lockFileStr, "docker run --rm -i -v /var/run/docker.sock:/var/run/docker.sock -p 8000:8000 --entrypoint /app/flowguard-go") - assert.Contains(t, lockFileStr, "ghcr.io/githubnext/gh-aw-mcpg:latest") - assert.Contains(t, lockFileStr, "--routed --listen 0.0.0.0:8000 --config-stdin") - - // Check environment variables - assert.Contains(t, lockFileStr, `DOCKER_API_VERSION="1.44"`) - assert.Contains(t, lockFileStr, `GITHUB_TOKEN="${{ secrets.GITHUB_TOKEN }}"`) - - // Check health check uses correct port - assert.Contains(t, lockFileStr, "Verify MCP Gateway Health") - assert.Contains(t, lockFileStr, "http://localhost:8000") - - // Ensure we're NOT using the default port - healthCheckLine := "" - for _, line := range strings.Split(lockFileStr, "\n") { - if strings.Contains(line, "Verify MCP Gateway Health") { - // Find the next line with the health check URL - idx := strings.Index(lockFileStr, line) - remaining := lockFileStr[idx:] - lines := strings.Split(remaining, "\n") - for _, l := range lines[1:] { - if strings.Contains(l, "http://localhost:") { - healthCheckLine = l - break - } - } - break - } - } - require.NotEmpty(t, healthCheckLine, "Health check line not found") - assert.Contains(t, healthCheckLine, "http://localhost:8000", "Health check should use configured port 8000, not default 8080") - assert.NotContains(t, healthCheckLine, "http://localhost:8080", "Health check should not use default port 8080") -} - -func TestSandboxMCPCommandConfiguration(t *testing.T) { - // Test workflow with command mode (not container mode) - workflow := `--- -name: Test Sandbox MCP Command -engine: copilot -on: workflow_dispatch -sandbox: - agent: awf - mcp: - command: "./custom-gateway" - args: - - "--port" - - "9000" - port: 9000 - env: - LOG_LEVEL: "debug" -tools: - github: - mode: remote - toolsets: [default] -permissions: - issues: read - pull-requests: read ---- - -Test workflow for sandbox MCP command configuration. -` - - tmpDir := testutil.TempDir(t, "sandbox-mcp-command-test") - testFile := filepath.Join(tmpDir, "test-workflow.md") - err := os.WriteFile(testFile, []byte(workflow), 0644) - require.NoError(t, err) - - // Compile the workflow - compiler := NewCompiler(false, "", "test") - compiler.SetStrictMode(false) - err = compiler.CompileWorkflow(testFile) - require.NoError(t, err) - - // Read the compiled lock file - lockFile := filepath.Join(tmpDir, "test-workflow.lock.yml") - lockFileContent, err := os.ReadFile(lockFile) - require.NoError(t, err) - require.NotEmpty(t, lockFileContent) - - // Verify the compiled workflow contains the correct command configuration - lockFileStr := string(lockFileContent) - - // Check command start - assert.Contains(t, lockFileStr, "Start MCP Gateway") - assert.Contains(t, lockFileStr, "./custom-gateway --port 9000") - - // Check environment variables - assert.Contains(t, lockFileStr, `LOG_LEVEL="debug"`) - - // Check health check uses correct port - assert.Contains(t, lockFileStr, "Verify MCP Gateway Health") - assert.Contains(t, lockFileStr, "http://localhost:9000") - assert.NotContains(t, lockFileStr, "http://localhost:8080", "Health check should not use default port 8080") -} diff --git a/pkg/workflow/sandbox_test.go b/pkg/workflow/sandbox_test.go index b8fa61ba30e..99eab5c35a6 100644 --- a/pkg/workflow/sandbox_test.go +++ b/pkg/workflow/sandbox_test.go @@ -292,67 +292,6 @@ func TestValidateSandboxConfig(t *testing.T) { expectError: true, errorMsg: "sandbox-runtime and AWF firewall cannot be used together", }, - { - name: "MCP gateway with both command and container fails", - data: &WorkflowData{ - SandboxConfig: &SandboxConfig{ - MCP: &MCPGatewayRuntimeConfig{ - Command: "/usr/bin/gateway", - Container: "ghcr.io/gateway:latest", - }, - }, - }, - expectError: true, - errorMsg: "cannot specify both 'command' and 'container'", - }, - { - name: "MCP gateway with entrypointArgs without container fails", - data: &WorkflowData{ - SandboxConfig: &SandboxConfig{ - MCP: &MCPGatewayRuntimeConfig{ - Command: "/usr/bin/gateway", - EntrypointArgs: []string{"--config-stdin"}, - }, - }, - }, - expectError: true, - errorMsg: "'entrypointArgs' can only be used with 'container'", - }, - { - name: "MCP gateway with container only is valid", - data: &WorkflowData{ - SandboxConfig: &SandboxConfig{ - MCP: &MCPGatewayRuntimeConfig{ - Container: "ghcr.io/gateway:latest", - EntrypointArgs: []string{"--config-stdin"}, - }, - }, - }, - expectError: false, - }, - { - name: "MCP gateway with command only is valid", - data: &WorkflowData{ - SandboxConfig: &SandboxConfig{ - MCP: &MCPGatewayRuntimeConfig{ - Command: "/usr/bin/gateway", - Args: []string{"--port", "8080"}, - }, - }, - }, - expectError: false, - }, - { - name: "MCP gateway with neither command nor container is valid", - data: &WorkflowData{ - SandboxConfig: &SandboxConfig{ - MCP: &MCPGatewayRuntimeConfig{ - Port: 8080, - }, - }, - }, - expectError: false, - }, } for _, tt := range tests { @@ -450,39 +389,3 @@ permissions: _, err = os.Stat(lockFile) require.NoError(t, err, "Lock file should be created") } - -func TestSandboxConfigWithMCPGateway(t *testing.T) { - content := `--- -on: workflow_dispatch -engine: copilot -sandbox: - agent: awf - mcp: - container: "ghcr.io/githubnext/mcp-gateway" - port: 9090 - api-key: "${{ secrets.MCP_API_KEY }}" -features: - mcp-gateway: true -permissions: - contents: read ---- - -# Test Workflow with MCP Gateway -` - - tmpDir := testutil.TempDir(t, "sandbox-mcp-gateway-test") - - testFile := filepath.Join(tmpDir, "test-workflow.md") - err := os.WriteFile(testFile, []byte(content), 0644) - require.NoError(t, err) - - compiler := NewCompiler(false, "", "test") - compiler.SetStrictMode(false) - err = compiler.CompileWorkflow(testFile) - require.NoError(t, err) - - // Verify the lock file was created - lockFile := filepath.Join(tmpDir, "test-workflow.lock.yml") - _, err = os.Stat(lockFile) - require.NoError(t, err, "Lock file should be created") -} diff --git a/pkg/workflow/sandbox_validation.go b/pkg/workflow/sandbox_validation.go index fdce53024e7..c84f8fb82c9 100644 --- a/pkg/workflow/sandbox_validation.go +++ b/pkg/workflow/sandbox_validation.go @@ -99,20 +99,5 @@ func validateSandboxConfig(workflowData *WorkflowData) error { } } - // Validate MCP gateway configuration - if sandboxConfig.MCP != nil { - mcpConfig := sandboxConfig.MCP - - // Validate mutual exclusivity of command and container - if mcpConfig.Command != "" && mcpConfig.Container != "" { - return fmt.Errorf("sandbox.mcp: cannot specify both 'command' and 'container', use one or the other") - } - - // Validate entrypointArgs is only used with container - if len(mcpConfig.EntrypointArgs) > 0 && mcpConfig.Container == "" { - return fmt.Errorf("sandbox.mcp: 'entrypointArgs' can only be used with 'container'") - } - } - return nil } diff --git a/specs/gosec.md b/specs/gosec.md index 83a7d1276f6..0da9f2907e7 100644 --- a/specs/gosec.md +++ b/specs/gosec.md @@ -66,7 +66,6 @@ The following files have specific gosec rule exclusions with documented rational ### G204: Subprocess Execution with Variable Arguments - **CWE**: CWE-78 (OS Command Injection) - **Files**: - - `pkg/awmg/gateway.go` - MCP gateway server commands - `pkg/cli/actionlint.go` - Docker commands for actionlint - `pkg/parser/remote_fetch.go` - Git commands for remote workflow fetching - `pkg/cli/download_workflow.go` - Git operations for workflow downloads From a7f01ac469c914b304f6171446bb87152f61cce7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 6 Jan 2026 20:13:31 +0000 Subject: [PATCH 4/6] Remove unused parseMCPGatewayTool function Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/tools_parser.go | 69 ------------------------------------ 1 file changed, 69 deletions(-) diff --git a/pkg/workflow/tools_parser.go b/pkg/workflow/tools_parser.go index 11fc1be1555..c054efa2e2a 100644 --- a/pkg/workflow/tools_parser.go +++ b/pkg/workflow/tools_parser.go @@ -416,75 +416,6 @@ func parseRepoMemoryTool(val any) *RepoMemoryToolConfig { return &RepoMemoryToolConfig{Raw: val} } -// parseMCPGatewayTool converts raw mcp-gateway tool configuration -func parseMCPGatewayTool(val any) *MCPGatewayRuntimeConfig { - if val == nil { - return nil - } - - configMap, ok := val.(map[string]any) - if !ok { - return nil - } - - config := &MCPGatewayRuntimeConfig{ - Port: DefaultMCPGatewayPort, - } - - if command, ok := configMap["command"].(string); ok { - config.Command = command - } - if container, ok := configMap["container"].(string); ok { - config.Container = container - } - if version, ok := configMap["version"].(string); ok { - config.Version = version - } else if versionNum, ok := configMap["version"].(float64); ok { - config.Version = fmt.Sprintf("%.0f", versionNum) - } - if args, ok := configMap["args"].([]any); ok { - config.Args = make([]string, 0, len(args)) - for _, arg := range args { - if str, ok := arg.(string); ok { - config.Args = append(config.Args, str) - } - } - } - if entrypointArgs, ok := configMap["entrypointArgs"].([]any); ok { - config.EntrypointArgs = make([]string, 0, len(entrypointArgs)) - for _, arg := range entrypointArgs { - if str, ok := arg.(string); ok { - config.EntrypointArgs = append(config.EntrypointArgs, str) - } - } - } - if env, ok := configMap["env"].(map[string]any); ok { - config.Env = make(map[string]string) - for k, v := range env { - if str, ok := v.(string); ok { - config.Env[k] = str - } - } - } - if port, ok := configMap["port"].(int); ok { - config.Port = port - } else if portFloat, ok := configMap["port"].(float64); ok { - config.Port = int(portFloat) - } else if portUint, ok := configMap["port"].(uint64); ok { - config.Port = int(portUint) - } else if portInt64, ok := configMap["port"].(int64); ok { - config.Port = int(portInt64) - } - if apiKey, ok := configMap["api-key"].(string); ok { - config.APIKey = apiKey - } - if domain, ok := configMap["domain"].(string); ok { - config.Domain = domain - } - - return config -} - // parseSafetyPromptTool converts raw safety-prompt tool configuration func parseSafetyPromptTool(val any) *bool { if boolVal, ok := val.(bool); ok { From 5bbb77507a92eed740e76f281ac746e4642f4f7f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 6 Jan 2026 20:16:23 +0000 Subject: [PATCH 5/6] Clean up remaining awmg references in config and test files Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .golangci.yml | 4 ---- scripts/test-build-release.sh | 6 +----- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 25a454181e1..f044f40f5f2 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -88,10 +88,6 @@ issues: - gosec text: "^G104:" # Ignore "errors unhandled" - intentional in tests path: _test\.go - - linters: - - gosec - text: "^G204:" # Allow exec.Command in controlled contexts - path: pkg/awmg/gateway\.go - linters: - gosec text: "^G204:" # Allow docker commands in actionlint diff --git a/scripts/test-build-release.sh b/scripts/test-build-release.sh index ac1ce97489f..5a791e40934 100755 --- a/scripts/test-build-release.sh +++ b/scripts/test-build-release.sh @@ -107,11 +107,7 @@ if echo "$BINARY_VERSION" | grep -q "dev"; then echo "FAIL: gh-aw binary version should not contain 'dev'" exit 1 fi -if echo "$AWMG_VERSION" | grep -q "dev"; then - echo "FAIL: awmg binary version should not contain 'dev'" - exit 1 -fi -echo "PASS: Binary versions do not contain 'dev'" +echo "PASS: Binary version does not contain 'dev'" # Clean up dist directory rm -rf dist From 0f553abbb7a493be4cc211cbab06926806282d81 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 6 Jan 2026 20:39:46 +0000 Subject: [PATCH 6/6] Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../smoke-copilot-no-firewall.lock.yml | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/.github/workflows/smoke-copilot-no-firewall.lock.yml b/.github/workflows/smoke-copilot-no-firewall.lock.yml index 5be8448c1db..ab131b1c25f 100644 --- a/.github/workflows/smoke-copilot-no-firewall.lock.yml +++ b/.github/workflows/smoke-copilot-no-firewall.lock.yml @@ -543,11 +543,6 @@ jobs: "tools": ["*"] } } - , - "gateway": { - "port": 8080, - "domain": "localhost" - } } EOF echo "-------START MCP CONFIG-----------" @@ -557,25 +552,6 @@ jobs: find /home/runner/.copilot echo "HOME: $HOME" echo "GITHUB_COPILOT_CLI_MODE: $GITHUB_COPILOT_CLI_MODE" - - name: Start MCP Gateway - env: - GH_AW_GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_AW_SAFE_INPUTS_API_KEY: ${{ steps.safe-inputs-start.outputs.api_key }} - GH_AW_SAFE_INPUTS_PORT: ${{ steps.safe-inputs-start.outputs.port }} - GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }} - GH_DEBUG: 1 - GITHUB_MCP_LOCKDOWN: ${{ steps.determine-automatic-lockdown.outputs.lockdown == 'true' && '1' || '0' }} - GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - run: | - mkdir -p /tmp/gh-aw/mcp-gateway-logs - echo 'Starting MCP Gateway...' - - echo 'ERROR: sandbox.mcp must specify either container or command' - echo 'Example container mode: sandbox.mcp.container: "ghcr.io/githubnext/gh-aw-mcpg:latest"' - echo 'Example command mode: sandbox.mcp.command: "./custom-gateway"' - exit 1 - - name: Verify MCP Gateway Health - run: bash /tmp/gh-aw/actions/verify_mcp_gateway_health.sh "http://localhost:8080" "/home/runner/.copilot/mcp-config.json" "/tmp/gh-aw/mcp-gateway-logs" - name: Generate agentic run info id: generate_aw_info uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0