Skip to content

Commit 5dce3e4

Browse files
Copilotpelikhangithub-actions[bot]
authored
[WIP] Refactor duplicate MCP config loading logic (#2829)
* Initial plan * Initial analysis of duplicate code issue Co-authored-by: pelikhan <[email protected]> * Extract shared helper for loading workflow MCP configs - Created loadWorkflowMCPConfigs() helper in mcp_workflow_loader.go - Updated mcp_list.go to use shared helper (removed 14 lines of duplicate code) - Updated mcp_list_tools.go to use shared helper (removed 14 lines of duplicate code) - Added comprehensive tests for the shared helper function - All CLI tests pass - Code formatted and linted successfully Co-authored-by: pelikhan <[email protected]> * Add changeset [skip-ci] --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: pelikhan <[email protected]> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 9de945e commit 5dce3e4

File tree

5 files changed

+221
-29
lines changed

5 files changed

+221
-29
lines changed

.changeset/patch-refactor-mcp-config-loading.md

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/cli/mcp_list.go

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"strings"
88

99
"github.com/githubnext/gh-aw/pkg/console"
10-
"github.com/githubnext/gh-aw/pkg/parser"
1110
"github.com/spf13/cobra"
1211
)
1312

@@ -29,21 +28,10 @@ func ListWorkflowMCP(workflowFile string, verbose bool) error {
2928
return listWorkflowsWithMCPServers(workflowsDir, verbose)
3029
}
3130

32-
// Parse the specific workflow file
33-
content, err := os.ReadFile(workflowPath)
31+
// Parse the specific workflow file and extract MCP configurations
32+
_, mcpConfigs, err := loadWorkflowMCPConfigs(workflowPath, "")
3433
if err != nil {
35-
return fmt.Errorf("failed to read workflow file: %w", err)
36-
}
37-
38-
workflowData, err := parser.ExtractFrontmatterFromContent(string(content))
39-
if err != nil {
40-
return fmt.Errorf("failed to parse workflow file: %w", err)
41-
}
42-
43-
// Extract MCP configurations (no server filter for listing)
44-
mcpConfigs, err := parser.ExtractMCPConfigurations(workflowData.Frontmatter, "")
45-
if err != nil {
46-
return fmt.Errorf("failed to extract MCP configurations: %w", err)
34+
return err
4735
}
4836

4937
if len(mcpConfigs) == 0 {

pkg/cli/mcp_list_tools.go

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -46,21 +46,10 @@ func ListToolsForMCP(workflowFile string, mcpServerName string, verbose bool) er
4646
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Looking for MCP server '%s' in: %s", mcpServerName, workflowPath)))
4747
}
4848

49-
// Parse the workflow file
50-
content, err := os.ReadFile(workflowPath)
49+
// Parse the workflow file and extract MCP configurations
50+
_, mcpConfigs, err := loadWorkflowMCPConfigs(workflowPath, mcpServerName)
5151
if err != nil {
52-
return fmt.Errorf("failed to read workflow file: %w", err)
53-
}
54-
55-
workflowData, err := parser.ExtractFrontmatterFromContent(string(content))
56-
if err != nil {
57-
return fmt.Errorf("failed to parse workflow file: %w", err)
58-
}
59-
60-
// Extract MCP configurations, filtering by server name
61-
mcpConfigs, err := parser.ExtractMCPConfigurations(workflowData.Frontmatter, mcpServerName)
62-
if err != nil {
63-
return fmt.Errorf("failed to extract MCP configurations: %w", err)
52+
return err
6453
}
6554

6655
// Find the specific MCP server

pkg/cli/mcp_workflow_loader.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"os"
6+
7+
"github.com/githubnext/gh-aw/pkg/parser"
8+
)
9+
10+
// loadWorkflowMCPConfigs loads a workflow file and extracts MCP configurations.
11+
// This is a shared helper used by multiple MCP commands to avoid code duplication.
12+
//
13+
// Parameters:
14+
// - workflowPath: absolute path to the workflow file
15+
// - serverFilter: optional server name to filter by (empty string for no filter)
16+
//
17+
// Returns:
18+
// - *parser.FrontmatterResult: parsed workflow data containing frontmatter and content
19+
// - []parser.MCPServerConfig: list of MCP server configurations
20+
// - error: any error that occurred during loading or parsing
21+
func loadWorkflowMCPConfigs(workflowPath string, serverFilter string) (*parser.FrontmatterResult, []parser.MCPServerConfig, error) {
22+
// Read the workflow file
23+
content, err := os.ReadFile(workflowPath)
24+
if err != nil {
25+
return nil, nil, fmt.Errorf("failed to read workflow file: %w", err)
26+
}
27+
28+
// Parse the frontmatter
29+
workflowData, err := parser.ExtractFrontmatterFromContent(string(content))
30+
if err != nil {
31+
return nil, nil, fmt.Errorf("failed to parse workflow file: %w", err)
32+
}
33+
34+
// Extract MCP configurations
35+
mcpConfigs, err := parser.ExtractMCPConfigurations(workflowData.Frontmatter, serverFilter)
36+
if err != nil {
37+
return nil, nil, fmt.Errorf("failed to extract MCP configurations: %w", err)
38+
}
39+
40+
return workflowData, mcpConfigs, nil
41+
}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
package cli
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"strings"
7+
"testing"
8+
9+
"github.com/githubnext/gh-aw/pkg/constants"
10+
)
11+
12+
func TestLoadWorkflowMCPConfigs(t *testing.T) {
13+
// Create a temporary directory for test workflows
14+
tmpDir := t.TempDir()
15+
workflowsDir := filepath.Join(tmpDir, constants.GetWorkflowDir())
16+
err := os.MkdirAll(workflowsDir, 0755)
17+
if err != nil {
18+
t.Fatalf("Failed to create test directory: %v", err)
19+
}
20+
21+
// Create a test workflow with MCP servers
22+
testWorkflowContent := `---
23+
on:
24+
workflow_dispatch:
25+
26+
permissions: read-all
27+
28+
safe-outputs:
29+
create-issue:
30+
title-prefix: "[Test] "
31+
32+
tools:
33+
github:
34+
mcp:
35+
type: stdio
36+
command: "npx"
37+
args: ["@github/github-mcp-server"]
38+
allowed: ["create_issue"]
39+
40+
mcp-servers:
41+
test-server:
42+
type: stdio
43+
command: "node"
44+
args: ["test-server.js"]
45+
allowed: ["test_tool_1", "test_tool_2"]
46+
47+
---
48+
49+
# Test Workflow
50+
This is a test workflow.`
51+
52+
testWorkflowPath := filepath.Join(workflowsDir, "test-workflow.md")
53+
err = os.WriteFile(testWorkflowPath, []byte(testWorkflowContent), 0644)
54+
if err != nil {
55+
t.Fatalf("Failed to create test workflow file: %v", err)
56+
}
57+
58+
t.Run("load_workflow_with_no_filter", func(t *testing.T) {
59+
workflowData, mcpConfigs, err := loadWorkflowMCPConfigs(testWorkflowPath, "")
60+
if err != nil {
61+
t.Errorf("loadWorkflowMCPConfigs() error = %v", err)
62+
return
63+
}
64+
65+
if workflowData == nil {
66+
t.Error("Expected workflowData to be non-nil")
67+
return
68+
}
69+
70+
if workflowData.Frontmatter == nil {
71+
t.Error("Expected workflowData.Frontmatter to be non-nil")
72+
return
73+
}
74+
75+
// Should find multiple MCP servers (github, test-server, safe-outputs)
76+
if len(mcpConfigs) < 2 {
77+
t.Errorf("Expected at least 2 MCP servers, got %d", len(mcpConfigs))
78+
}
79+
})
80+
81+
t.Run("load_workflow_with_server_filter", func(t *testing.T) {
82+
workflowData, mcpConfigs, err := loadWorkflowMCPConfigs(testWorkflowPath, "github")
83+
if err != nil {
84+
t.Errorf("loadWorkflowMCPConfigs() error = %v", err)
85+
return
86+
}
87+
88+
if workflowData == nil {
89+
t.Error("Expected workflowData to be non-nil")
90+
return
91+
}
92+
93+
// Should find only the github MCP server
94+
found := false
95+
for _, config := range mcpConfigs {
96+
if strings.EqualFold(config.Name, "github") {
97+
found = true
98+
break
99+
}
100+
}
101+
102+
if !found {
103+
t.Error("Expected to find 'github' MCP server with filter")
104+
}
105+
})
106+
107+
t.Run("load_nonexistent_workflow", func(t *testing.T) {
108+
nonexistentPath := filepath.Join(workflowsDir, "nonexistent.md")
109+
_, _, err := loadWorkflowMCPConfigs(nonexistentPath, "")
110+
if err == nil {
111+
t.Error("Expected error for nonexistent workflow, got nil")
112+
}
113+
if !strings.Contains(err.Error(), "failed to read workflow file") {
114+
t.Errorf("Expected 'failed to read workflow file' error, got: %v", err)
115+
}
116+
})
117+
118+
t.Run("load_workflow_with_invalid_frontmatter", func(t *testing.T) {
119+
invalidWorkflowContent := `---
120+
invalid: yaml: content: [
121+
---
122+
# Invalid Workflow`
123+
124+
invalidWorkflowPath := filepath.Join(workflowsDir, "invalid-workflow.md")
125+
err = os.WriteFile(invalidWorkflowPath, []byte(invalidWorkflowContent), 0644)
126+
if err != nil {
127+
t.Fatalf("Failed to create invalid workflow file: %v", err)
128+
}
129+
130+
_, _, err = loadWorkflowMCPConfigs(invalidWorkflowPath, "")
131+
if err == nil {
132+
t.Error("Expected error for invalid frontmatter, got nil")
133+
}
134+
if !strings.Contains(err.Error(), "failed to parse workflow file") {
135+
t.Errorf("Expected 'failed to parse workflow file' error, got: %v", err)
136+
}
137+
})
138+
139+
t.Run("load_workflow_without_mcp_servers", func(t *testing.T) {
140+
noMCPWorkflowContent := `---
141+
on:
142+
push:
143+
permissions: read-all
144+
---
145+
# No MCP Workflow`
146+
147+
noMCPWorkflowPath := filepath.Join(workflowsDir, "no-mcp-workflow.md")
148+
err = os.WriteFile(noMCPWorkflowPath, []byte(noMCPWorkflowContent), 0644)
149+
if err != nil {
150+
t.Fatalf("Failed to create no-MCP workflow file: %v", err)
151+
}
152+
153+
workflowData, mcpConfigs, err := loadWorkflowMCPConfigs(noMCPWorkflowPath, "")
154+
if err != nil {
155+
t.Errorf("loadWorkflowMCPConfigs() error = %v", err)
156+
return
157+
}
158+
159+
if workflowData == nil {
160+
t.Error("Expected workflowData to be non-nil")
161+
return
162+
}
163+
164+
// Should return empty list, not error
165+
if len(mcpConfigs) != 0 {
166+
t.Errorf("Expected 0 MCP servers, got %d", len(mcpConfigs))
167+
}
168+
})
169+
}

0 commit comments

Comments
 (0)