Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/patch-stop-404-discussion-retries.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ WORKDIR /workspace
ENTRYPOINT ["gh-aw"]

# Default command runs MCP server
CMD ["mcp-server", "--cmd", "gh-aw"]
# Note: Binary path detection is automatic via os.Executable()
CMD ["mcp-server"]
Comment on lines +35 to +36
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is misleading for the same reason as the comment in mcp_config_builtin.go - the binary path is only logged but not actually used. The MCP server still needs the --cmd flag to be explicitly set, or it will fall back to calling "gh aw" which won't work in Docker.

The comment should be updated once the binary path detection is properly implemented to actually use the detected path.

See below for a potential fix:

# Default command runs MCP server. Note: the MCP server still requires an explicit
# --cmd flag to be set; otherwise it falls back to calling "gh aw", which will not
# work correctly inside this Docker image. Update this comment once binary path
# detection is fully wired to use the detected path.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 35b8e5c. The comment is now accurate - the binary path detection is fully wired up to use the detected path via the cmdPath variable.


# Metadata labels
LABEL org.opencontainers.image.source="https://github.com/github/gh-aw"
Expand Down
12 changes: 12 additions & 0 deletions pkg/cli/mcp_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,18 @@ func runMCPServer(port int, cmdPath string) error {
mcpLog.Print("Starting MCP server with stdio transport")
}

// Determine, log, and validate the binary path only if --cmd flag is not provided
// When --cmd is provided, the user explicitly specified the binary path to use
if cmdPath == "" {
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The binary path is determined and logged, but never actually used. When cmdPath is empty, createMCPServer(cmdPath) receives an empty string and falls back to calling "gh aw" via workflow.ExecGHContext(), which won't work in Docker containers where gh-aw isn't installed as a gh extension.

The detected binary path should be assigned to cmdPath when it's empty. Consider modifying the code to:

if cmdPath == "" {
    detectedPath, err := GetBinaryPath()
    if err == nil {
        cmdPath = detectedPath
    }
    // Note: logAndValidateBinaryPath handles all logging internally
    _ = logAndValidateBinaryPath()
}

This would ensure that createMCPServer(cmdPath) receives the actual binary path and uses it in exec.CommandContext(ctx, cmdPath, args...) instead of falling back to "gh aw".

Suggested change
if cmdPath == "" {
if cmdPath == "" {
// Attempt to detect the binary path and assign it to cmdPath
if detectedPath, err := GetBinaryPath(); err == nil && detectedPath != "" {
cmdPath = detectedPath
}

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 35b8e5c. The detected binary path is now assigned to cmdPath when detection succeeds, ensuring createMCPServer receives and uses the actual binary path instead of falling back to "gh aw".

// Attempt to detect the binary path and assign it to cmdPath
// This ensures createMCPServer receives the actual binary path instead of falling back to "gh aw"
detectedPath, err := logAndValidateBinaryPath()
if err == nil && detectedPath != "" {
cmdPath = detectedPath
mcpLog.Printf("Using detected binary path: %s", cmdPath)
}
}

// Log current working directory
if cwd, err := os.Getwd(); err == nil {
mcpLog.Printf("Current working directory: %s", cwd)
Expand Down
66 changes: 66 additions & 0 deletions pkg/cli/mcp_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"

Expand All @@ -19,6 +20,60 @@ import (

var mcpValidationLog = logger.New("cli:mcp_validation")

// GetBinaryPath returns the path to the currently running gh-aw binary.
// This is used by the MCP server to determine where the gh-aw binary is located
// when launching itself with different arguments.
//
// Returns the absolute path to the binary, or an error if the path cannot be determined.
func GetBinaryPath() (string, error) {
// Get the path to the currently running executable
exePath, err := os.Executable()
if err != nil {
return "", fmt.Errorf("failed to get executable path: %w", err)
}

// Resolve any symlinks to get the actual binary path
// This is important because gh extensions are typically symlinked
// Note: EvalSymlinks already returns an absolute path
resolvedPath, err := filepath.EvalSymlinks(exePath)
if err != nil {
// If we can't resolve symlinks, use the original path
mcpValidationLog.Printf("Warning: failed to resolve symlinks for %s: %v", exePath, err)
return exePath, nil
}

return resolvedPath, nil
}

// logAndValidateBinaryPath determines the binary path, logs it, and validates it exists.
// Returns the detected binary path and an error if the path cannot be determined or if the file doesn't exist.
// This is a helper used by both runMCPServer and validateMCPServerConfiguration.
func logAndValidateBinaryPath() (string, error) {
binaryPath, err := GetBinaryPath()
if err != nil {
mcpValidationLog.Printf("Warning: failed to get binary path: %v", err)
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Warning: failed to get binary path: %v", err)))
return "", err
}

// Check if the binary file exists
if _, err := os.Stat(binaryPath); err != nil {
if os.IsNotExist(err) {
mcpValidationLog.Printf("ERROR: binary file does not exist at path: %s", binaryPath)
fmt.Fprintln(os.Stderr, console.FormatErrorMessage(fmt.Sprintf("ERROR: binary file does not exist at path: %s", binaryPath)))
return "", fmt.Errorf("binary file does not exist at path: %s", binaryPath)
}
mcpValidationLog.Printf("Warning: failed to stat binary file at %s: %v", binaryPath, err)
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Warning: failed to stat binary file at %s: %v", binaryPath, err)))
return "", err
}

// Log the binary path for debugging
mcpValidationLog.Printf("gh-aw binary path: %s", binaryPath)
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("gh-aw binary path: %s", binaryPath)))
return binaryPath, nil
}

// validateServerSecrets checks if required environment variables/secrets are available
func validateServerSecrets(config parser.MCPServerConfig, verbose bool, useActionsSecrets bool) error {
mcpValidationLog.Printf("Validating server secrets: server=%s, type=%s, useActionsSecrets=%v", config.Name, config.Type, useActionsSecrets)
Expand Down Expand Up @@ -169,6 +224,17 @@ func validateServerSecrets(config parser.MCPServerConfig, verbose bool, useActio
func validateMCPServerConfiguration(cmdPath string) error {
mcpValidationLog.Printf("Validating MCP server configuration: cmdPath=%s", cmdPath)

// Determine, log, and validate the binary path only if --cmd flag is not provided
// When --cmd is provided, the user explicitly specified the binary path to use
if cmdPath == "" {
// Attempt to detect the binary path and assign it to cmdPath
// This ensures the validation uses the actual binary path instead of falling back to "gh aw"
detectedPath, err := logAndValidateBinaryPath()
if err == nil && detectedPath != "" {
cmdPath = detectedPath
}
}

// Try to run the status command to verify CLI is working
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
Expand Down
61 changes: 61 additions & 0 deletions pkg/cli/mcp_validation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//go:build !integration

package cli

import (
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestGetBinaryPath(t *testing.T) {
t.Run("returns non-empty path", func(t *testing.T) {
path, err := GetBinaryPath()
require.NoError(t, err, "Should get binary path without error")
assert.NotEmpty(t, path, "Binary path should not be empty")
})

t.Run("returns absolute path", func(t *testing.T) {
path, err := GetBinaryPath()
require.NoError(t, err, "Should get binary path without error")
assert.True(t, filepath.IsAbs(path), "Binary path should be absolute")
})

t.Run("returned path exists", func(t *testing.T) {
path, err := GetBinaryPath()
require.NoError(t, err, "Should get binary path without error")

// Check if the file exists
info, err := os.Stat(path)
require.NoError(t, err, "Binary file should exist at the returned path")
assert.False(t, info.IsDir(), "Binary path should not be a directory")
})

t.Run("path ends with executable name", func(t *testing.T) {
path, err := GetBinaryPath()
require.NoError(t, err, "Should get binary path without error")

// The path should end with a reasonable executable name
// During tests, it might be a test binary name
base := filepath.Base(path)
assert.NotEmpty(t, base, "Binary path should have a base name")
// Don't check for specific name as it could be the test binary
})

t.Run("resolves symlinks", func(t *testing.T) {
path, err := GetBinaryPath()
require.NoError(t, err, "Should get binary path without error")

// The path should be the resolved path, not a symlink
// We can verify this by checking that EvalSymlinks returns the same path
resolved, err := filepath.EvalSymlinks(path)
if err == nil {
// If we can resolve symlinks, the path should already be resolved
assert.Equal(t, path, resolved, "Path should already be resolved (no symlinks)")
}
// If EvalSymlinks fails, that's OK - the original path is still valid
})
}
3 changes: 2 additions & 1 deletion pkg/workflow/mcp_config_builtin.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,8 @@ func renderAgenticWorkflowsMCPConfigWithOptions(yaml *strings.Builder, isLast bo

if actionMode.IsDev() {
// Dev mode: Use locally built Docker image which includes gh-aw binary and gh CLI
// The Dockerfile sets ENTRYPOINT ["gh-aw"] and CMD ["mcp-server", "--cmd", "gh-aw"]
// The Dockerfile sets ENTRYPOINT ["gh-aw"] and CMD ["mcp-server"]
// Binary path is automatically detected via os.Executable()
// So we don't need to specify entrypoint or entrypointArgs
Comment on lines +189 to 190
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is misleading because the binary path is currently only logged but not actually used. The MCP server still falls back to calling "gh aw" when cmdPath is empty (see createMCPServer function in mcp_server.go line 159-160), which means the automatic detection doesn't actually work as described.

The comment should be updated to reflect the actual behavior once the binary path detection is properly wired up to be used by the MCP server.

Suggested change
// Binary path is automatically detected via os.Executable()
// So we don't need to specify entrypoint or entrypointArgs
// Note: while the gh-aw binary path may be detected (e.g., via os.Executable()),
// it is currently only logged and not used to override the MCP server command.
// In dev mode we rely on the container's default ENTRYPOINT and CMD instead.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 35b8e5c. The comment is now accurate - the binary path is actually used by the MCP server via the cmdPath variable passed to createMCPServer.

containerImage = constants.DevModeGhAwImage
entrypoint = "" // Use container's default entrypoint
Expand Down