From a5935e2f1cb43f108c20408a3267c6e86900a520 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 04:07:08 +0000 Subject: [PATCH 1/3] Initial plan From c34dc03b065250ee7f379a1f0820c5ecce61485e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 04:12:12 +0000 Subject: [PATCH 2/3] chore: initial planning for MCP config deduplication Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> Agent-Logs-Url: https://github.com/github/gh-aw/sessions/e085685c-e00c-4481-8dca-8f1c8449559e --- .github/workflows/smoke-codex.lock.yml | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/workflows/smoke-codex.lock.yml b/.github/workflows/smoke-codex.lock.yml index 7514dfdddf..a3c38f8c67 100644 --- a/.github/workflows/smoke-codex.lock.yml +++ b/.github/workflows/smoke-codex.lock.yml @@ -450,13 +450,20 @@ jobs: { "description": "Add the 'smoked' label to the current pull request (can only be called once)", "inputSchema": { - "additionalProperties": true, + "additionalProperties": false, "properties": { - "payload": { - "description": "JSON-encoded payload to pass to the action", + "labels": { + "description": "The labels' name to be added. Must be separated with line breaks if there're multiple labels.", + "type": "string" + }, + "number": { + "description": "The number of the issue or pull request.", "type": "string" } }, + "required": [ + "labels" + ], "type": "object" }, "name": "add_smoked_label" @@ -1561,7 +1568,8 @@ jobs: env: GITHUB_TOKEN: ${{ github.token }} with: - payload: ${{ steps.process_safe_outputs.outputs.action_add_smoked_label_payload }} + labels: ${{ fromJSON(steps.process_safe_outputs.outputs.action_add_smoked_label_payload).labels }} + number: ${{ fromJSON(steps.process_safe_outputs.outputs.action_add_smoked_label_payload).number }} - name: Upload safe output items if: always() uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 From bd3cdd4a8d393b71af02d855117ef21b645a7965 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 04:19:22 +0000 Subject: [PATCH 3/3] refactor: extract shared renderStandardJSONMCPConfig helper to reduce duplication (#issue) Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> Agent-Logs-Url: https://github.com/github/gh-aw/sessions/e085685c-e00c-4481-8dca-8f1c8449559e --- pkg/workflow/claude_mcp.go | 14 ++++------- pkg/workflow/codex_mcp.go | 16 ++++--------- pkg/workflow/copilot_mcp.go | 29 ++++++++++------------ pkg/workflow/gemini_mcp.go | 13 ++++------ pkg/workflow/mcp_renderer_helpers.go | 36 ++++++++++++++++++++++++++++ 5 files changed, 60 insertions(+), 48 deletions(-) diff --git a/pkg/workflow/claude_mcp.go b/pkg/workflow/claude_mcp.go index 032c1888e5..7417afee4e 100644 --- a/pkg/workflow/claude_mcp.go +++ b/pkg/workflow/claude_mcp.go @@ -13,17 +13,11 @@ func (e *ClaudeEngine) RenderMCPConfig(yaml *strings.Builder, tools map[string]a claudeMCPLog.Printf("Rendering MCP config for Claude: tool_count=%d, mcp_tool_count=%d", len(tools), len(mcpTools)) // Claude uses JSON format without Copilot-specific fields and multi-line args - createRenderer := buildMCPRendererFactory(workflowData, "json", false, false) - - // Build gateway configuration for MCP config - // Per MCP Gateway Specification v1.0.0 section 4.1.3, the gateway section is required - return RenderJSONMCPConfig(yaml, tools, mcpTools, workflowData, JSONMCPConfigOptions{ - ConfigPath: "/tmp/gh-aw/mcp-config/mcp-servers.json", - GatewayConfig: buildMCPGatewayConfig(workflowData), - Renderers: buildStandardJSONMCPRenderers(workflowData, createRenderer, false, func(yaml *strings.Builder, toolName string, toolConfig map[string]any, isLast bool) error { + return renderStandardJSONMCPConfig(yaml, tools, mcpTools, workflowData, + "/tmp/gh-aw/mcp-config/mcp-servers.json", false, false, false, + func(yaml *strings.Builder, toolName string, toolConfig map[string]any, isLast bool) error { return e.renderClaudeMCPConfigWithContext(yaml, toolName, toolConfig, isLast, workflowData) - }), - }) + }, nil) } // renderClaudeMCPConfigWithContext generates custom MCP server configuration for a single tool in Claude workflow mcp-servers.json diff --git a/pkg/workflow/codex_mcp.go b/pkg/workflow/codex_mcp.go index f7af0ba577..0ca76a52ea 100644 --- a/pkg/workflow/codex_mcp.go +++ b/pkg/workflow/codex_mcp.go @@ -103,20 +103,12 @@ func (e *CodexEngine) RenderMCPConfig(yaml *strings.Builder, tools map[string]an yaml.WriteString(" \n") yaml.WriteString(" # Generate JSON config for MCP gateway\n") - // Build gateway configuration - gatewayConfig := buildMCPGatewayConfig(workflowData) - - // Use shared JSON renderer for gateway input // Gateway uses JSON format without Copilot-specific fields and multi-line args - createJSONRenderer := buildMCPRendererFactory(workflowData, "json", false, false) - - return RenderJSONMCPConfig(yaml, tools, mcpTools, workflowData, JSONMCPConfigOptions{ - ConfigPath: "/tmp/gh-aw/mcp-config/mcp-servers.json", - GatewayConfig: gatewayConfig, - Renderers: buildStandardJSONMCPRenderers(workflowData, createJSONRenderer, false, func(yaml *strings.Builder, toolName string, toolConfig map[string]any, isLast bool) error { + return renderStandardJSONMCPConfig(yaml, tools, mcpTools, workflowData, + "/tmp/gh-aw/mcp-config/mcp-servers.json", false, false, false, + func(yaml *strings.Builder, toolName string, toolConfig map[string]any, isLast bool) error { return e.renderCodexJSONMCPConfigWithContext(yaml, toolName, toolConfig, isLast, workflowData) - }), - }) + }, nil) } // renderCodexMCPConfigWithContext generates custom MCP server configuration for a single tool in codex workflow config.toml diff --git a/pkg/workflow/copilot_mcp.go b/pkg/workflow/copilot_mcp.go index 3501f59429..8ff0b543c5 100644 --- a/pkg/workflow/copilot_mcp.go +++ b/pkg/workflow/copilot_mcp.go @@ -8,6 +8,12 @@ import ( var copilotMCPLog = logger.New("workflow:copilot_mcp") +// copilotMCPToolFilter returns true for MCP tools that should be included in the Copilot MCP config. +// Cache-memory is excluded because it is handled as a simple file share, not an MCP server. +func copilotMCPToolFilter(toolName string) bool { + return toolName != "cache-memory" +} + // RenderMCPConfig generates MCP server configuration for Copilot CLI func (e *CopilotEngine) RenderMCPConfig(yaml *strings.Builder, tools map[string]any, mcpTools []string, workflowData *WorkflowData) error { copilotMCPLog.Printf("Rendering MCP config for Copilot engine: mcpTools=%d", len(mcpTools)) @@ -16,25 +22,14 @@ func (e *CopilotEngine) RenderMCPConfig(yaml *strings.Builder, tools map[string] yaml.WriteString(" mkdir -p /home/runner/.copilot\n") // Copilot uses JSON format with type and tools fields, and inline args - createRenderer := buildMCPRendererFactory(workflowData, "json", true, true) - - // Build gateway configuration for MCP config - // Per MCP Gateway Specification v1.0.0 section 4.1.3, the gateway section is required - options := JSONMCPConfigOptions{ - ConfigPath: "/home/runner/.copilot/mcp-config.json", - GatewayConfig: buildMCPGatewayConfig(workflowData), - // webFetchIncludeTools=true: Copilot requires a tools field in the web-fetch server config - Renderers: buildStandardJSONMCPRenderers(workflowData, createRenderer, true, func(yaml *strings.Builder, toolName string, toolConfig map[string]any, isLast bool) error { + // webFetchIncludeTools=true: Copilot requires a tools field in the web-fetch server config + return renderStandardJSONMCPConfig(yaml, tools, mcpTools, workflowData, + "/home/runner/.copilot/mcp-config.json", true, true, true, + func(yaml *strings.Builder, toolName string, toolConfig map[string]any, isLast bool) error { return e.renderCopilotMCPConfigWithContext(yaml, toolName, toolConfig, isLast, workflowData) - }), - FilterTool: func(toolName string) bool { - // Filter out cache-memory for Copilot - // Cache-memory is handled as a simple file share, not an MCP server - return toolName != "cache-memory" }, - } - - return RenderJSONMCPConfig(yaml, tools, mcpTools, workflowData, options) + copilotMCPToolFilter, + ) } // renderCopilotMCPConfigWithContext generates custom MCP server configuration for Copilot CLI diff --git a/pkg/workflow/gemini_mcp.go b/pkg/workflow/gemini_mcp.go index 699575c4f1..3356171ff5 100644 --- a/pkg/workflow/gemini_mcp.go +++ b/pkg/workflow/gemini_mcp.go @@ -13,14 +13,9 @@ func (e *GeminiEngine) RenderMCPConfig(yaml *strings.Builder, tools map[string]a geminiMCPLog.Printf("Rendering MCP config for Gemini: tool_count=%d, mcp_tool_count=%d", len(tools), len(mcpTools)) // Gemini uses JSON format without Copilot-specific fields and multi-line args - createRenderer := buildMCPRendererFactory(workflowData, "json", false, false) - - // Use shared JSON MCP config renderer - return RenderJSONMCPConfig(yaml, tools, mcpTools, workflowData, JSONMCPConfigOptions{ - ConfigPath: "/tmp/gh-aw/mcp-config/mcp-servers.json", - GatewayConfig: buildMCPGatewayConfig(workflowData), - Renderers: buildStandardJSONMCPRenderers(workflowData, createRenderer, false, func(yaml *strings.Builder, toolName string, toolConfig map[string]any, isLast bool) error { + return renderStandardJSONMCPConfig(yaml, tools, mcpTools, workflowData, + "/tmp/gh-aw/mcp-config/mcp-servers.json", false, false, false, + func(yaml *strings.Builder, toolName string, toolConfig map[string]any, isLast bool) error { return renderCustomMCPConfigWrapperWithContext(yaml, toolName, toolConfig, isLast, workflowData) - }), - }) + }, nil) } diff --git a/pkg/workflow/mcp_renderer_helpers.go b/pkg/workflow/mcp_renderer_helpers.go index 65c47a4646..d2419183c2 100644 --- a/pkg/workflow/mcp_renderer_helpers.go +++ b/pkg/workflow/mcp_renderer_helpers.go @@ -2,6 +2,42 @@ package workflow import "strings" +// renderStandardJSONMCPConfig is a shared helper for JSON MCP config rendering used by +// Claude, Gemini, Copilot, and Codex engines. It consolidates the repeated sequence of: +// buildMCPRendererFactory → buildMCPGatewayConfig → buildStandardJSONMCPRenderers → RenderJSONMCPConfig. +// +// Parameters: +// - yaml: output builder +// - tools: tool configurations from frontmatter +// - mcpTools: list of enabled MCP tool names +// - workflowData: compiled workflow context +// - configPath: engine-specific MCP config file path +// - includeCopilotFields: whether to include "type" and "tools" fields (true for Copilot) +// - inlineArgs: whether to render args inline (true for Copilot) vs multi-line +// - webFetchIncludeTools: whether the web-fetch server includes a tools field (true for Copilot) +// - renderCustom: engine-specific handler for custom MCP tool entries +// - filterTool: optional tool filter function; nil to include all tools +func renderStandardJSONMCPConfig( + yaml *strings.Builder, + tools map[string]any, + mcpTools []string, + workflowData *WorkflowData, + configPath string, + includeCopilotFields bool, + inlineArgs bool, + webFetchIncludeTools bool, + renderCustom RenderCustomMCPToolConfigHandler, + filterTool func(string) bool, +) error { + createRenderer := buildMCPRendererFactory(workflowData, "json", includeCopilotFields, inlineArgs) + return RenderJSONMCPConfig(yaml, tools, mcpTools, workflowData, JSONMCPConfigOptions{ + ConfigPath: configPath, + GatewayConfig: buildMCPGatewayConfig(workflowData), + Renderers: buildStandardJSONMCPRenderers(workflowData, createRenderer, webFetchIncludeTools, renderCustom), + FilterTool: filterTool, + }) +} + // buildMCPRendererFactory creates a factory function for MCPConfigRendererUnified instances. // The returned function accepts isLast as a parameter and creates a renderer with engine-specific // options derived from the provided parameters and workflowData at call time.