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 7514dfdddff..a3c38f8c672 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 032c1888e5b..7417afee4e7 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 f7af0ba577a..0ca76a52eac 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 3501f594296..8ff0b543c5f 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 699575c4f17..3356171ff5a 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 65c47a46462..d2419183c2c 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.
