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-extract-github-mcp-remote-config.md

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

32 changes: 9 additions & 23 deletions pkg/workflow/claude_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -653,32 +653,18 @@ func (e *ClaudeEngine) renderGitHubClaudeMCPConfig(yaml *strings.Builder, github

// Check if remote mode is enabled (type: remote)
if githubType == "remote" {
// Remote mode - use hosted GitHub MCP server
yaml.WriteString(" \"type\": \"http\",\n")
yaml.WriteString(" \"url\": \"https://api.githubcopilot.com/mcp/\",\n")
yaml.WriteString(" \"headers\": {\n")

// Use effective token with precedence: custom > top-level > default
effectiveToken := getEffectiveGitHubToken(customGitHubToken, workflowData.GitHubToken)

// Collect headers in a map
headers := make(map[string]string)
headers["Authorization"] = fmt.Sprintf("Bearer %s", effectiveToken)

// Add X-MCP-Readonly header if read-only mode is enabled
if readOnly {
headers["X-MCP-Readonly"] = "true"
}

// Add X-MCP-Toolsets header if toolsets are configured
if toolsets != "" {
headers["X-MCP-Toolsets"] = toolsets
}

// Write headers using helper
writeHeadersToYAML(yaml, headers, " ")

yaml.WriteString(" }\n")
// Render remote configuration using shared helper
RenderGitHubMCPRemoteConfig(yaml, GitHubMCPRemoteOptions{
ReadOnly: readOnly,
Toolsets: toolsets,
AuthorizationValue: fmt.Sprintf("Bearer %s", effectiveToken),
IncludeToolsField: false, // Claude doesn't use tools field
AllowedTools: nil,
IncludeEnvSection: false, // Claude doesn't use env section
})
} else {
// Local mode - use Docker-based GitHub MCP server (default)
githubDockerImageVersion := getGitHubDockerImageVersion(githubTool)
Expand Down
52 changes: 9 additions & 43 deletions pkg/workflow/copilot_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -399,49 +399,15 @@ func (e *CopilotEngine) renderGitHubCopilotMCPConfig(yaml *strings.Builder, gith

// Check if remote mode is enabled (type: remote)
if githubType == "remote" {
// Remote mode - use hosted GitHub MCP server
yaml.WriteString(" \"type\": \"http\",\n")
yaml.WriteString(" \"url\": \"https://api.githubcopilot.com/mcp/\",\n")
yaml.WriteString(" \"headers\": {\n")

// Collect headers in a map
headers := make(map[string]string)
headers["Authorization"] = "Bearer \\${GITHUB_PERSONAL_ACCESS_TOKEN}"

// Add X-MCP-Readonly header if read-only mode is enabled
if readOnly {
headers["X-MCP-Readonly"] = "true"
}

// Add X-MCP-Toolsets header if toolsets are configured
if toolsets != "" {
headers["X-MCP-Toolsets"] = toolsets
}

// Write headers using helper
writeHeadersToYAML(yaml, headers, " ")

yaml.WriteString(" },\n")

// Populate tools field with allowed tools or "*" if none specified
if len(allowedTools) > 0 {
yaml.WriteString(" \"tools\": [\n")
for i, tool := range allowedTools {
comma := ","
if i == len(allowedTools)-1 {
comma = ""
}
fmt.Fprintf(yaml, " \"%s\"%s\n", tool, comma)
}
yaml.WriteString(" ],\n")
} else {
yaml.WriteString(" \"tools\": [\"*\"],\n")
}

// Add env section for passthrough
yaml.WriteString(" \"env\": {\n")
yaml.WriteString(" \"GITHUB_PERSONAL_ACCESS_TOKEN\": \"\\${GITHUB_PERSONAL_ACCESS_TOKEN}\"\n")
yaml.WriteString(" }\n")
// Render remote configuration using shared helper
RenderGitHubMCPRemoteConfig(yaml, GitHubMCPRemoteOptions{
ReadOnly: readOnly,
Toolsets: toolsets,
AuthorizationValue: "Bearer \\${GITHUB_PERSONAL_ACCESS_TOKEN}",
IncludeToolsField: true, // Copilot uses tools field
AllowedTools: allowedTools,
IncludeEnvSection: true, // Copilot uses env section for passthrough
})
} else {
// Local mode - use Docker-based GitHub MCP server (default)
githubDockerImageVersion := getGitHubDockerImageVersion(githubTool)
Expand Down
79 changes: 79 additions & 0 deletions pkg/workflow/engine_shared_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,85 @@ func RenderGitHubMCPDockerConfig(yaml *strings.Builder, options GitHubMCPDockerO
yaml.WriteString(" }\n")
}

// GitHubMCPRemoteOptions defines configuration for GitHub MCP remote mode rendering
type GitHubMCPRemoteOptions struct {
// ReadOnly enables read-only mode for GitHub API operations
ReadOnly bool
// Toolsets specifies the GitHub toolsets to enable
Toolsets string
// AuthorizationValue is the value for the Authorization header
// For Claude: "Bearer {effectiveToken}"
// For Copilot: "Bearer \\${GITHUB_PERSONAL_ACCESS_TOKEN}"
AuthorizationValue string
// IncludeToolsField indicates whether to include the "tools" field (Copilot needs it, Claude doesn't)
IncludeToolsField bool
// AllowedTools specifies the list of allowed tools (Copilot uses this, Claude doesn't)
AllowedTools []string
// IncludeEnvSection indicates whether to include the env section (Copilot needs it, Claude doesn't)
IncludeEnvSection bool
}

// RenderGitHubMCPRemoteConfig renders the GitHub MCP server configuration for remote (hosted) mode.
// This shared function extracts the duplicate pattern from Claude and Copilot engines.
//
// Parameters:
// - yaml: The string builder for YAML output
// - options: GitHub MCP remote rendering options
func RenderGitHubMCPRemoteConfig(yaml *strings.Builder, options GitHubMCPRemoteOptions) {
// Remote mode - use hosted GitHub MCP server
yaml.WriteString(" \"type\": \"http\",\n")
yaml.WriteString(" \"url\": \"https://api.githubcopilot.com/mcp/\",\n")
yaml.WriteString(" \"headers\": {\n")

// Collect headers in a map
headers := make(map[string]string)
headers["Authorization"] = options.AuthorizationValue

// Add X-MCP-Readonly header if read-only mode is enabled
if options.ReadOnly {
headers["X-MCP-Readonly"] = "true"
}

// Add X-MCP-Toolsets header if toolsets are configured
if options.Toolsets != "" {
headers["X-MCP-Toolsets"] = options.Toolsets
}

// Write headers using helper
writeHeadersToYAML(yaml, headers, " ")

// Close headers section
if options.IncludeToolsField || options.IncludeEnvSection {
yaml.WriteString(" },\n")
} else {
yaml.WriteString(" }\n")
}

// Add tools field if needed (Copilot uses this, Claude doesn't)
if options.IncludeToolsField {
if len(options.AllowedTools) > 0 {
yaml.WriteString(" \"tools\": [\n")
for i, tool := range options.AllowedTools {
comma := ","
if i == len(options.AllowedTools)-1 {
comma = ""
}
fmt.Fprintf(yaml, " \"%s\"%s\n", tool, comma)
}
yaml.WriteString(" ],\n")
} else {
yaml.WriteString(" \"tools\": [\"*\"],\n")
}
}

// Add env section if needed (Copilot uses this, Claude doesn't)
if options.IncludeEnvSection {
yaml.WriteString(" \"env\": {\n")
yaml.WriteString(" \"GITHUB_PERSONAL_ACCESS_TOKEN\": \"\\${GITHUB_PERSONAL_ACCESS_TOKEN}\"\n")
yaml.WriteString(" }\n")
}
}

// RenderJSONMCPConfig renders MCP configuration in JSON format with the common mcpServers structure.
// This shared function extracts the duplicate pattern from Claude, Copilot, and Custom engines.
//
Expand Down
Loading
Loading