Skip to content

Commit d776940

Browse files
committed
Add support for running an Actions workflow
1 parent 62eed34 commit d776940

4 files changed

Lines changed: 218 additions & 0 deletions

File tree

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,16 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
437437
- `state`: Alert state (string, optional)
438438
- `severity`: Alert severity (string, optional)
439439

440+
### Actions
441+
442+
- **run_workflow** - Trigger a workflow run
443+
444+
- `owner`: Repository owner (string, required)
445+
- `repo`: Repository name (string, required)
446+
- `workflowId`: Workflow ID or filename (string, required)
447+
- `ref`: Git reference (branch or tag name) (string, required)
448+
- `inputs`: Workflow inputs (object, optional)
449+
440450
## Resources
441451

442452
### Repository Content

pkg/github/actions.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
8+
"github.com/github/github-mcp-server/pkg/translations"
9+
"github.com/google/go-github/v69/github"
10+
"github.com/mark3labs/mcp-go/mcp"
11+
"github.com/mark3labs/mcp-go/server"
12+
)
13+
14+
// RunWorkflow creates a tool to run an Actions workflow
15+
func RunWorkflow(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
16+
return mcp.NewTool("run_workflow",
17+
mcp.WithDescription(t("TOOL_RUN_WORKFLOW_DESCRIPTION", "Trigger a workflow run")),
18+
mcp.WithString("owner",
19+
mcp.Required(),
20+
mcp.Description("The account owner of the repository. The name is not case sensitive."),
21+
),
22+
mcp.WithString("repo",
23+
mcp.Required(),
24+
mcp.Description("Repository name"),
25+
),
26+
mcp.WithString("workflowId",
27+
mcp.Required(),
28+
mcp.Description("The ID of the workflow. You can also pass the workflow file name as a string."),
29+
),
30+
mcp.WithString("ref",
31+
mcp.Required(),
32+
mcp.Description("Git reference (branch or tag name)"),
33+
),
34+
mcp.WithObject("inputs",
35+
mcp.Description("Input keys and values configured in the workflow file."),
36+
),
37+
),
38+
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
39+
owner, err := requiredParam[string](request, "owner")
40+
if err != nil {
41+
return mcp.NewToolResultError(err.Error()), nil
42+
}
43+
repo, err := requiredParam[string](request, "repo")
44+
if err != nil {
45+
return mcp.NewToolResultError(err.Error()), nil
46+
}
47+
workflowID, err := requiredParam[string](request, "workflowId")
48+
if err != nil {
49+
return mcp.NewToolResultError(err.Error()), nil
50+
}
51+
ref, err := requiredParam[string](request, "ref")
52+
if err != nil {
53+
return mcp.NewToolResultError(err.Error()), nil
54+
}
55+
56+
// Get the optional inputs parameter
57+
var inputs map[string]any
58+
if inputsObj, exists := request.Params.Arguments["inputs"]; exists && inputsObj != nil {
59+
inputs, _ = inputsObj.(map[string]any)
60+
}
61+
62+
// Convert inputs to the format expected by the GitHub API
63+
inputsMap := make(map[string]any)
64+
if inputs != nil {
65+
for k, v := range inputs {
66+
inputsMap[k] = v
67+
}
68+
}
69+
70+
// Create the event to dispatch
71+
event := github.CreateWorkflowDispatchEventRequest{
72+
Ref: ref,
73+
Inputs: inputsMap,
74+
}
75+
76+
client, err := getClient(ctx)
77+
if err != nil {
78+
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
79+
}
80+
81+
resp, err := client.Actions.CreateWorkflowDispatchEventByFileName(ctx, owner, repo, workflowID, event)
82+
if err != nil {
83+
return nil, fmt.Errorf("failed to trigger workflow: %w", err)
84+
}
85+
defer func() { _ = resp.Body.Close() }()
86+
87+
result := map[string]any{
88+
"success": true,
89+
"message": "Workflow triggered successfully",
90+
}
91+
92+
r, err := json.Marshal(result)
93+
if err != nil {
94+
return nil, fmt.Errorf("failed to marshal response: %w", err)
95+
}
96+
97+
return mcp.NewToolResultText(string(r)), nil
98+
}
99+
}

pkg/github/actions_test.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"net/http"
7+
"testing"
8+
9+
"github.com/github/github-mcp-server/pkg/translations"
10+
"github.com/google/go-github/v69/github"
11+
"github.com/migueleliasweb/go-github-mock/src/mock"
12+
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/require"
14+
)
15+
16+
func Test_RunWorkflow(t *testing.T) {
17+
// Verify tool definition once
18+
mockClient := github.NewClient(nil)
19+
tool, _ := RunWorkflow(stubGetClientFn(mockClient), translations.NullTranslationHelper)
20+
21+
assert.Equal(t, "run_workflow", tool.Name)
22+
assert.NotEmpty(t, tool.Description)
23+
assert.Contains(t, tool.InputSchema.Properties, "owner")
24+
assert.Contains(t, tool.InputSchema.Properties, "repo")
25+
assert.Contains(t, tool.InputSchema.Properties, "workflowId")
26+
assert.Contains(t, tool.InputSchema.Properties, "ref")
27+
assert.Contains(t, tool.InputSchema.Properties, "inputs")
28+
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "workflowId", "ref"})
29+
30+
tests := []struct {
31+
name string
32+
mockedClient *http.Client
33+
requestArgs map[string]any
34+
expectError bool
35+
expectedErrMsg string
36+
}{
37+
{
38+
name: "successful workflow trigger",
39+
mockedClient: mock.NewMockedHTTPClient(
40+
mock.WithRequestMatchHandler(
41+
mock.PostReposActionsWorkflowsDispatchesByOwnerByRepoByWorkflowId,
42+
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
43+
w.WriteHeader(http.StatusNoContent)
44+
}),
45+
),
46+
),
47+
requestArgs: map[string]any{
48+
"owner": "owner",
49+
"repo": "repo",
50+
"workflowId": "workflow_id",
51+
"ref": "main",
52+
"inputs": map[string]any{
53+
"input1": "value1",
54+
"input2": "value2",
55+
},
56+
},
57+
expectError: false,
58+
},
59+
{
60+
name: "missing required parameter",
61+
mockedClient: mock.NewMockedHTTPClient(),
62+
requestArgs: map[string]any{
63+
"owner": "owner",
64+
"repo": "repo",
65+
"workflowId": "main.yaml",
66+
// missing ref
67+
},
68+
expectError: true,
69+
expectedErrMsg: "missing required parameter: ref",
70+
},
71+
}
72+
73+
for _, tc := range tests {
74+
t.Run(tc.name, func(t *testing.T) {
75+
// Setup client with mock
76+
client := github.NewClient(tc.mockedClient)
77+
_, handler := RunWorkflow(stubGetClientFn(client), translations.NullTranslationHelper)
78+
79+
// Create call request
80+
request := createMCPRequest(tc.requestArgs)
81+
82+
// Call handler
83+
result, err := handler(context.Background(), request)
84+
85+
require.NoError(t, err)
86+
require.Equal(t, tc.expectError, result.IsError)
87+
88+
// Parse the result and get the text content if no error
89+
textContent := getTextResult(t, result)
90+
91+
if tc.expectedErrMsg != "" {
92+
assert.Equal(t, tc.expectedErrMsg, textContent.Text)
93+
return
94+
}
95+
96+
// Unmarshal and verify the result
97+
var response map[string]any
98+
err = json.Unmarshal([]byte(textContent.Text), &response)
99+
require.NoError(t, err)
100+
assert.Equal(t, true, response["success"])
101+
assert.Equal(t, "Workflow triggered successfully", response["message"])
102+
})
103+
}
104+
}

pkg/github/server.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ func NewServer(getClient GetClientFn, version string, readOnly bool, t translati
8080
s.AddTool(PushFiles(getClient, t))
8181
}
8282

83+
// Add GitHub tools - Actions
84+
if !readOnly {
85+
s.AddTool(RunWorkflow(getClient, t))
86+
}
87+
8388
// Add GitHub tools - Search
8489
s.AddTool(SearchCode(getClient, t))
8590
s.AddTool(SearchUsers(getClient, t))

0 commit comments

Comments
 (0)