Skip to content

Commit 3d58490

Browse files
authored
Add 'add' tool to mcp-server command (#3447)
1 parent 2b7587a commit 3d58490

4 files changed

Lines changed: 268 additions & 4 deletions

File tree

.github/workflows/go.mod

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
11
module github.com/githubnext/gh-aw-workflows-deps
2-
go 1.21
32

4-
require (
5-
)
3+
go 1.21

pkg/cli/mcp_server.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ The server provides the following tools:
3535
- logs - Download and analyze workflow logs
3636
- audit - Investigate a workflow run and generate a report
3737
- mcp-inspect - Inspect MCP servers in workflows and list available tools
38+
- add - Add workflows from remote repositories to .github/workflows
3839
3940
By default, the server uses stdio transport. Use the --port flag to run
4041
an HTTP server with SSE (Server-Sent Events) transport instead.
@@ -451,6 +452,59 @@ Returns formatted text output showing:
451452
}, nil, nil
452453
})
453454

455+
// Add add tool
456+
type addArgs struct {
457+
Workflows []string `json:"workflows" jsonschema:"Workflows to add (e.g., 'owner/repo/workflow-name' or 'owner/repo/workflow-name@version')"`
458+
Number int `json:"number,omitempty" jsonschema:"Create multiple numbered copies (corresponds to -c flag, default: 1)"`
459+
Name string `json:"name,omitempty" jsonschema:"Specify name for the added workflow - without .md extension (corresponds to -n flag)"`
460+
}
461+
462+
mcp.AddTool(server, &mcp.Tool{
463+
Name: "add",
464+
Description: "Add workflows from remote repositories to .github/workflows",
465+
}, func(ctx context.Context, req *mcp.CallToolRequest, args addArgs) (*mcp.CallToolResult, any, error) {
466+
// Validate required arguments
467+
if len(args.Workflows) == 0 {
468+
return &mcp.CallToolResult{
469+
Content: []mcp.Content{
470+
&mcp.TextContent{Text: "Error: at least one workflow specification is required"},
471+
},
472+
}, nil, nil
473+
}
474+
475+
// Build command arguments
476+
cmdArgs := []string{"add"}
477+
478+
// Add workflows
479+
cmdArgs = append(cmdArgs, args.Workflows...)
480+
481+
// Add optional flags
482+
if args.Number > 0 {
483+
cmdArgs = append(cmdArgs, "-c", strconv.Itoa(args.Number))
484+
}
485+
if args.Name != "" {
486+
cmdArgs = append(cmdArgs, "-n", args.Name)
487+
}
488+
489+
// Execute the CLI command
490+
cmd := execCmd(ctx, cmdArgs...)
491+
output, err := cmd.CombinedOutput()
492+
493+
if err != nil {
494+
return &mcp.CallToolResult{
495+
Content: []mcp.Content{
496+
&mcp.TextContent{Text: fmt.Sprintf("Error: %v\nOutput: %s", err, string(output))},
497+
},
498+
}, nil, nil
499+
}
500+
501+
return &mcp.CallToolResult{
502+
Content: []mcp.Content{
503+
&mcp.TextContent{Text: string(output)},
504+
},
505+
}, nil, nil
506+
})
507+
454508
return server
455509
}
456510

pkg/cli/mcp_server_add_test.go

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
//go:build integration
2+
3+
package cli
4+
5+
import (
6+
"context"
7+
"os"
8+
"os/exec"
9+
"path/filepath"
10+
"strings"
11+
"testing"
12+
"time"
13+
14+
"github.com/modelcontextprotocol/go-sdk/mcp"
15+
)
16+
17+
// TestMCPServer_AddTool tests that the add tool is exposed and functional
18+
func TestMCPServer_AddTool(t *testing.T) {
19+
// Skip if the binary doesn't exist
20+
binaryPath := "../../gh-aw"
21+
if _, err := os.Stat(binaryPath); os.IsNotExist(err) {
22+
t.Skip("Skipping test: gh-aw binary not found. Run 'make build' first.")
23+
}
24+
25+
// Create MCP client
26+
client := mcp.NewClient(&mcp.Implementation{
27+
Name: "test-client",
28+
Version: "1.0.0",
29+
}, nil)
30+
31+
// Start the MCP server as a subprocess with custom command path
32+
serverCmd := exec.Command(binaryPath, "mcp-server", "--cmd", binaryPath)
33+
transport := &mcp.CommandTransport{Command: serverCmd}
34+
35+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
36+
defer cancel()
37+
38+
session, err := client.Connect(ctx, transport, nil)
39+
if err != nil {
40+
t.Fatalf("Failed to connect to MCP server: %v", err)
41+
}
42+
defer session.Close()
43+
44+
// List tools to verify add is present
45+
result, err := session.ListTools(ctx, &mcp.ListToolsParams{})
46+
if err != nil {
47+
t.Fatalf("Failed to list tools: %v", err)
48+
}
49+
50+
// Verify add tool exists
51+
var addTool *mcp.Tool
52+
for i := range result.Tools {
53+
if result.Tools[i].Name == "add" {
54+
addTool = result.Tools[i]
55+
break
56+
}
57+
}
58+
59+
if addTool == nil {
60+
t.Fatal("add tool not found in MCP server tools")
61+
}
62+
63+
// Verify the tool has proper description
64+
if addTool.Description == "" {
65+
t.Error("add tool has empty description")
66+
}
67+
68+
// Verify the description mentions key functionality
69+
if len(addTool.Description) < 50 {
70+
t.Errorf("add tool description seems too short: %s", addTool.Description)
71+
}
72+
73+
// Verify description contains key phrases
74+
if !strings.Contains(addTool.Description, "workflows") {
75+
t.Error("add tool description should mention 'workflows'")
76+
}
77+
}
78+
79+
// TestMCPServer_AddToolInvocation tests calling the add tool
80+
func TestMCPServer_AddToolInvocation(t *testing.T) {
81+
// Skip if the binary doesn't exist
82+
binaryPath := "../../gh-aw"
83+
if _, err := os.Stat(binaryPath); os.IsNotExist(err) {
84+
t.Skip("Skipping test: gh-aw binary not found. Run 'make build' first.")
85+
}
86+
87+
// Get absolute path to binary
88+
absBinaryPath, err := filepath.Abs(binaryPath)
89+
if err != nil {
90+
t.Fatalf("Failed to get absolute path to binary: %v", err)
91+
}
92+
93+
// Create a temporary directory
94+
tmpDir := t.TempDir()
95+
workflowsDir := filepath.Join(tmpDir, ".github", "workflows")
96+
if err := os.MkdirAll(workflowsDir, 0755); err != nil {
97+
t.Fatalf("Failed to create workflows directory: %v", err)
98+
}
99+
100+
// Initialize git repository in the temp directory
101+
gitCmd := exec.Command("git", "init")
102+
gitCmd.Dir = tmpDir
103+
if err := gitCmd.Run(); err != nil {
104+
t.Fatalf("Failed to initialize git repository: %v", err)
105+
}
106+
107+
// Configure git user (required for commits)
108+
configCmds := [][]string{
109+
{"git", "config", "user.email", "test@example.com"},
110+
{"git", "config", "user.name", "Test User"},
111+
}
112+
for _, cmdArgs := range configCmds {
113+
cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...)
114+
cmd.Dir = tmpDir
115+
if err := cmd.Run(); err != nil {
116+
t.Fatalf("Failed to configure git: %v", err)
117+
}
118+
}
119+
120+
// Change to the temporary directory
121+
originalDir, _ := os.Getwd()
122+
defer os.Chdir(originalDir)
123+
os.Chdir(tmpDir)
124+
125+
// Create MCP client
126+
client := mcp.NewClient(&mcp.Implementation{
127+
Name: "test-client",
128+
Version: "1.0.0",
129+
}, nil)
130+
131+
// Start the MCP server as a subprocess with custom command path (absolute path)
132+
serverCmd := exec.Command(absBinaryPath, "mcp-server", "--cmd", absBinaryPath)
133+
serverCmd.Dir = tmpDir
134+
transport := &mcp.CommandTransport{Command: serverCmd}
135+
136+
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
137+
defer cancel()
138+
139+
session, err := client.Connect(ctx, transport, nil)
140+
if err != nil {
141+
t.Fatalf("Failed to connect to MCP server: %v", err)
142+
}
143+
defer session.Close()
144+
145+
// Test 1: Call with just repository (should list workflows)
146+
t.Run("ListWorkflows", func(t *testing.T) {
147+
callResult, err := session.CallTool(ctx, &mcp.CallToolParams{
148+
Name: "add",
149+
Arguments: map[string]any{
150+
"workflows": []any{"githubnext/agentics"},
151+
},
152+
})
153+
154+
if err != nil {
155+
t.Fatalf("Failed to call add tool: %v", err)
156+
}
157+
158+
// Verify we got some output
159+
if len(callResult.Content) == 0 {
160+
t.Fatal("add tool returned no content")
161+
}
162+
163+
// Extract text content
164+
var outputText string
165+
for _, content := range callResult.Content {
166+
if textContent, ok := content.(*mcp.TextContent); ok {
167+
outputText += textContent.Text
168+
}
169+
}
170+
171+
if outputText == "" {
172+
t.Fatal("add tool returned empty text content")
173+
}
174+
175+
t.Logf("add tool output (list workflows):\n%s", outputText)
176+
177+
// Output should mention available workflows or indicate repository was processed
178+
if !strings.Contains(outputText, "workflow") && !strings.Contains(outputText, "Workflow") {
179+
t.Logf("Warning: Output doesn't mention 'workflow': %s", outputText)
180+
}
181+
})
182+
183+
// Test 2: Call with missing workflows parameter (should fail)
184+
t.Run("MissingWorkflows", func(t *testing.T) {
185+
callResult, err := session.CallTool(ctx, &mcp.CallToolParams{
186+
Name: "add",
187+
Arguments: map[string]any{},
188+
})
189+
190+
if err != nil {
191+
t.Fatalf("Failed to call add tool: %v", err)
192+
}
193+
194+
// Verify we got error output
195+
if len(callResult.Content) == 0 {
196+
t.Fatal("add tool returned no content")
197+
}
198+
199+
// Extract text content
200+
var outputText string
201+
for _, content := range callResult.Content {
202+
if textContent, ok := content.(*mcp.TextContent); ok {
203+
outputText += textContent.Text
204+
}
205+
}
206+
207+
// Should contain error message
208+
if !strings.Contains(outputText, "Error") && !strings.Contains(outputText, "error") && !strings.Contains(outputText, "required") {
209+
t.Errorf("Expected error message for missing workflows, got: %s", outputText)
210+
}
211+
})
212+
}

pkg/cli/mcp_server_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ func TestMCPServer_ListTools(t *testing.T) {
4747
}
4848

4949
// Verify expected tools are present
50-
expectedTools := []string{"status", "compile", "logs", "audit", "mcp-inspect"}
50+
expectedTools := []string{"status", "compile", "logs", "audit", "mcp-inspect", "add"}
5151
toolNames := make(map[string]bool)
5252
for _, tool := range result.Tools {
5353
toolNames[tool.Name] = true

0 commit comments

Comments
 (0)