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
8 changes: 5 additions & 3 deletions .github/workflows/agent-performance-analyzer.lock.yml

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

8 changes: 5 additions & 3 deletions .github/workflows/daily-firewall-report.lock.yml

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

8 changes: 5 additions & 3 deletions .github/workflows/dev-hawk.lock.yml

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

8 changes: 5 additions & 3 deletions .github/workflows/example-workflow-analyzer.lock.yml

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

8 changes: 5 additions & 3 deletions .github/workflows/metrics-collector.lock.yml

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

8 changes: 5 additions & 3 deletions .github/workflows/python-data-charts.lock.yml

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

8 changes: 5 additions & 3 deletions .github/workflows/smoke-copilot.lock.yml

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

12 changes: 12 additions & 0 deletions pkg/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,18 @@ const DefaultNodeAlpineLTSImage = "node:lts-alpine"
// Using python:alpine provides the latest stable version with minimal footprint
const DefaultPythonAlpineLTSImage = "python:alpine"

// DefaultAlpineImage is the default minimal Alpine container image for running Go binaries
// Used for MCP servers that run statically-linked Go binaries like gh-aw mcp-server
const DefaultAlpineImage = "alpine:latest"

// DefaultGhAwMount is the mount path for the gh-aw directory in containerized MCP servers
// The gh-aw binary and supporting files are mounted read-only from /opt/gh-aw
const DefaultGhAwMount = "/opt/gh-aw:/opt/gh-aw:ro"

// DefaultTmpGhAwMount is the mount path for temporary gh-aw files in containerized MCP servers
// Used for logs, cache, and other runtime data that needs read-write access
const DefaultTmpGhAwMount = "/tmp/gh-aw:/tmp/gh-aw:rw"

// DefaultPythonVersion is the default version of Python for runtime setup
const DefaultPythonVersion Version = "3.12"

Expand Down
10 changes: 10 additions & 0 deletions pkg/workflow/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ func collectDockerImages(tools map[string]any, workflowData *WorkflowData) []str
}
}

// Check for agentic-workflows tool (uses alpine container for gh-aw mcp-server)
if _, hasAgenticWorkflows := tools["agentic-workflows"]; hasAgenticWorkflows {
image := constants.DefaultAlpineImage
if !imageSet[image] {
images = append(images, image)
imageSet[image] = true
dockerLog.Printf("Added agentic-workflows MCP server container: %s", image)
}
}

// Collect sandbox.mcp container (MCP gateway)
// Skip if sandbox is disabled (sandbox: false)
if workflowData != nil && workflowData.SandboxConfig != nil {
Expand Down
15 changes: 8 additions & 7 deletions pkg/workflow/importable_tools_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,14 +226,14 @@ Uses imported agentic-workflows tool.

workflowData := string(lockFileContent)

// Verify gh aw mcp-server command is present
if !strings.Contains(workflowData, `"aw", "mcp-server"`) {
t.Error("Expected compiled workflow to contain 'aw', 'mcp-server' command")
// Verify containerized agentic_workflows server is present (per MCP Gateway Specification v1.0.0)
if !strings.Contains(workflowData, `"entrypointArgs": ["mcp-server"]`) {
t.Error("Expected compiled workflow to contain 'mcp-server' entrypointArgs")
}

// Verify gh CLI is used
if !strings.Contains(workflowData, `"command": "gh"`) {
t.Error("Expected compiled workflow to contain gh CLI command for agentic-workflows")
// Verify container format is used (not command format)
if !strings.Contains(workflowData, `"container": "alpine:latest"`) {
t.Error("Expected compiled workflow to contain alpine container for agentic-workflows")
}
}

Expand Down Expand Up @@ -310,7 +310,8 @@ Uses all imported tools.
if !strings.Contains(workflowData, `"serena"`) {
t.Error("Expected compiled workflow to contain serena tool")
}
if !strings.Contains(workflowData, `"aw", "mcp-server"`) {
// Per MCP Gateway Specification v1.0.0, agentic-workflows uses containerized format
if !strings.Contains(workflowData, `"agentic_workflows"`) {
t.Error("Expected compiled workflow to contain agentic-workflows tool")
}

Expand Down
134 changes: 52 additions & 82 deletions pkg/workflow/mcp-config.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,72 +226,6 @@ func renderSerenaMCPConfigWithOptions(yaml *strings.Builder, serenaTool any, isL
}
}

// BuiltinMCPServerOptions contains the options for rendering a built-in MCP server block
type BuiltinMCPServerOptions struct {
Yaml *strings.Builder
ServerID string
Command string
Args []string
EnvVars []string
IsLast bool
IncludeCopilotFields bool
}

// renderBuiltinMCPServerBlock is a shared helper function that renders MCP server configuration blocks
// for built-in servers (Safe Outputs and Agentic Workflows) with consistent formatting.
// This eliminates code duplication between renderSafeOutputsMCPConfigWithOptions and
// renderAgenticWorkflowsMCPConfigWithOptions by extracting the common YAML generation pattern.
func renderBuiltinMCPServerBlock(opts BuiltinMCPServerOptions) {
opts.Yaml.WriteString(" \"" + opts.ServerID + "\": {\n")

// Add type field for Copilot (per MCP Gateway Specification v1.0.0, use "stdio" for containerized servers)
if opts.IncludeCopilotFields {
opts.Yaml.WriteString(" \"type\": \"stdio\",\n")
}

opts.Yaml.WriteString(" \"command\": \"" + opts.Command + "\",\n")

// Write args array
opts.Yaml.WriteString(" \"args\": [")
for i, arg := range opts.Args {
if i > 0 {
opts.Yaml.WriteString(", ")
}
opts.Yaml.WriteString("\"" + arg + "\"")
}
opts.Yaml.WriteString("],\n")

// Note: tools field is NOT included here - the converter script adds it back
// for Copilot. This keeps the gateway config compatible with the schema.

opts.Yaml.WriteString(" \"env\": {\n")

// Write environment variables with appropriate escaping
for i, envVar := range opts.EnvVars {
isLastEnvVar := i == len(opts.EnvVars)-1
comma := ""
if !isLastEnvVar {
comma = ","
}

if opts.IncludeCopilotFields {
// Copilot format: backslash-escaped shell variable reference
opts.Yaml.WriteString(" \"" + envVar + "\": \"\\${" + envVar + "}\"" + comma + "\n")
} else {
// Claude/Custom format: direct shell variable reference
opts.Yaml.WriteString(" \"" + envVar + "\": \"$" + envVar + "\"" + comma + "\n")
}
}

opts.Yaml.WriteString(" }\n")

if opts.IsLast {
opts.Yaml.WriteString(" }\n")
} else {
opts.Yaml.WriteString(" },\n")
}
}

// renderSafeOutputsMCPConfig generates the Safe Outputs MCP server configuration
// This is a shared function used by both Claude and Custom engines
func renderSafeOutputsMCPConfig(yaml *strings.Builder, isLast bool) {
Expand Down Expand Up @@ -331,7 +265,7 @@ func renderSafeOutputsMCPConfigWithOptions(yaml *strings.Builder, isLast bool, i
yaml.WriteString(" \"container\": \"" + constants.DefaultNodeAlpineLTSImage + "\",\n")
yaml.WriteString(" \"entrypoint\": \"node\",\n")
yaml.WriteString(" \"entrypointArgs\": [\"/opt/gh-aw/safeoutputs/mcp-server.cjs\"],\n")
yaml.WriteString(" \"mounts\": [\"/opt/gh-aw:/opt/gh-aw:ro\", \"/tmp/gh-aw:/tmp/gh-aw:rw\"],\n")
yaml.WriteString(" \"mounts\": [\"" + constants.DefaultGhAwMount + "\", \"" + constants.DefaultTmpGhAwMount + "\"],\n")

// Note: tools field is NOT included here - the converter script adds it back
// for Copilot. This keeps the gateway config compatible with the schema.
Expand Down Expand Up @@ -363,20 +297,55 @@ func renderSafeOutputsMCPConfigWithOptions(yaml *strings.Builder, isLast bool, i
}

// renderAgenticWorkflowsMCPConfigWithOptions generates the Agentic Workflows MCP server configuration with engine-specific options
// Per MCP Gateway Specification v1.0.0 section 3.2.1, stdio-based MCP servers MUST be containerized.
// Uses MCP Gateway spec format: container, entrypoint, entrypointArgs, and mounts fields.
func renderAgenticWorkflowsMCPConfigWithOptions(yaml *strings.Builder, isLast bool, includeCopilotFields bool) {
envVars := []string{
"GITHUB_TOKEN",
}

renderBuiltinMCPServerBlock(BuiltinMCPServerOptions{
Yaml: yaml,
ServerID: "agentic_workflows",
Command: "gh",
Args: []string{"aw", "mcp-server"},
EnvVars: envVars,
IsLast: isLast,
IncludeCopilotFields: includeCopilotFields,
})
// Use MCP Gateway spec format with container, entrypoint, entrypointArgs, and mounts
// The gh-aw binary is mounted from /opt/gh-aw and executed directly inside a minimal Alpine container
yaml.WriteString(" \"agentic_workflows\": {\n")

// Add type field for Copilot (per MCP Gateway Specification v1.0.0, use "stdio" for containerized servers)
if includeCopilotFields {
yaml.WriteString(" \"type\": \"stdio\",\n")
}

// MCP Gateway spec fields for containerized stdio servers
yaml.WriteString(" \"container\": \"" + constants.DefaultAlpineImage + "\",\n")
yaml.WriteString(" \"entrypoint\": \"/opt/gh-aw/gh-aw\",\n")
yaml.WriteString(" \"entrypointArgs\": [\"mcp-server\"],\n")
yaml.WriteString(" \"mounts\": [\"" + constants.DefaultGhAwMount + "\"],\n")

// Note: tools field is NOT included here - the converter script adds it back
// for Copilot. This keeps the gateway config compatible with the schema.

// Write environment variables
yaml.WriteString(" \"env\": {\n")
for i, envVar := range envVars {
isLastEnvVar := i == len(envVars)-1
comma := ""
if !isLastEnvVar {
comma = ","
}

if includeCopilotFields {
// Copilot format: backslash-escaped shell variable reference
yaml.WriteString(" \"" + envVar + "\": \"\\${" + envVar + "}\"" + comma + "\n")
} else {
// Claude/Custom format: direct shell variable reference
yaml.WriteString(" \"" + envVar + "\": \"$" + envVar + "\"" + comma + "\n")
}
}
yaml.WriteString(" }\n")

if isLast {
yaml.WriteString(" }\n")
} else {
yaml.WriteString(" },\n")
}
}

// renderPlaywrightMCPConfigTOML generates the Playwright MCP server configuration in TOML format for Codex
Expand Down Expand Up @@ -432,20 +401,21 @@ func renderSafeOutputsMCPConfigTOML(yaml *strings.Builder) {
yaml.WriteString(" container = \"" + constants.DefaultNodeAlpineLTSImage + "\"\n")
yaml.WriteString(" entrypoint = \"node\"\n")
yaml.WriteString(" entrypointArgs = [\"/opt/gh-aw/safeoutputs/mcp-server.cjs\"]\n")
yaml.WriteString(" mounts = [\"/opt/gh-aw:/opt/gh-aw:ro\", \"/tmp/gh-aw:/tmp/gh-aw:rw\"]\n")
yaml.WriteString(" mounts = [\"" + constants.DefaultGhAwMount + "\", \"" + constants.DefaultTmpGhAwMount + "\"]\n")
// Use env_vars array to reference environment variables instead of embedding GitHub Actions expressions
yaml.WriteString(" env_vars = [\"GH_AW_SAFE_OUTPUTS\", \"GH_AW_ASSETS_BRANCH\", \"GH_AW_ASSETS_MAX_SIZE_KB\", \"GH_AW_ASSETS_ALLOWED_EXTS\", \"GITHUB_REPOSITORY\", \"GITHUB_SERVER_URL\", \"GITHUB_SHA\", \"GITHUB_WORKSPACE\", \"DEFAULT_BRANCH\"]\n")
}

// renderAgenticWorkflowsMCPConfigTOML generates the Agentic Workflows MCP server configuration in TOML format for Codex
// Per MCP Gateway Specification v1.0.0 section 3.2.1, stdio-based MCP servers MUST be containerized.
// Uses MCP Gateway spec format: container, entrypoint, entrypointArgs, and mounts fields.
func renderAgenticWorkflowsMCPConfigTOML(yaml *strings.Builder) {
yaml.WriteString(" \n")
yaml.WriteString(" [mcp_servers.agentic_workflows]\n")
yaml.WriteString(" command = \"gh\"\n")
yaml.WriteString(" args = [\n")
yaml.WriteString(" \"aw\",\n")
yaml.WriteString(" \"mcp-server\",\n")
yaml.WriteString(" ]\n")
yaml.WriteString(" container = \"" + constants.DefaultAlpineImage + "\"\n")
yaml.WriteString(" entrypoint = \"/opt/gh-aw/gh-aw\"\n")
yaml.WriteString(" entrypointArgs = [\"mcp-server\"]\n")
yaml.WriteString(" mounts = [\"" + constants.DefaultGhAwMount + "\"]\n")
// Use env_vars array to reference environment variables instead of embedding secrets
yaml.WriteString(" env_vars = [\"GITHUB_TOKEN\"]\n")
}
Expand Down
Loading