Skip to content

Commit f0b8835

Browse files
committed
Add implementations for consolidated Actions resource read tool
1 parent dc6f457 commit f0b8835

File tree

3 files changed

+601
-41
lines changed

3 files changed

+601
-41
lines changed

pkg/github/actions.go

Lines changed: 272 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -26,36 +26,40 @@ type actionsResource int
2626

2727
const (
2828
actionsResourceUnknown actionsResource = iota
29-
actionsResourceWorkflow
30-
actionsResourceWorkflowRun
31-
actionsResourceWorkflowJob
29+
actionsResourceGetWorkflow
30+
actionsResourceGetWorkflowRun
31+
actionsResourceGetWorkflowRuns
32+
actionsResourceGetWorkflowJob
33+
actionsResourceGetWorkflowJobs
34+
actionsResourceDownloadWorkflowArtifact
35+
actionsResourceGetWorkflowArtifacts
3236
)
3337

38+
var actionsResourceTypes = map[actionsResource]string{
39+
actionsResourceGetWorkflow: "workflow",
40+
actionsResourceGetWorkflowRun: "workflow_run",
41+
actionsResourceGetWorkflowRuns: "workflow_runs",
42+
actionsResourceGetWorkflowJob: "workflow_job",
43+
actionsResourceGetWorkflowJobs: "workflow_jobs",
44+
actionsResourceDownloadWorkflowArtifact: "workflow_artifact",
45+
actionsResourceGetWorkflowArtifacts: "workflow_artifacts",
46+
}
47+
3448
func (r actionsResource) String() string {
35-
switch r {
36-
case actionsResourceUnknown:
37-
return "unknown"
38-
case actionsResourceWorkflow:
39-
return "workflow"
40-
case actionsResourceWorkflowRun:
41-
return "workflow_run"
42-
case actionsResourceWorkflowJob:
43-
return "workflow_job"
49+
if str, ok := actionsResourceTypes[r]; ok {
50+
return str
4451
}
52+
4553
return "unknown"
4654
}
4755

4856
func ActionsResourceFromString(s string) actionsResource {
49-
switch strings.ToLower(s) {
50-
case "workflow":
51-
return actionsResourceWorkflow
52-
case "workflow_run":
53-
return actionsResourceWorkflowRun
54-
case "workflow_job":
55-
return actionsResourceWorkflowJob
56-
default:
57-
return actionsResourceUnknown
57+
for r, str := range actionsResourceTypes {
58+
if str == strings.ToLower(s) {
59+
return r
60+
}
5861
}
62+
return actionsResourceUnknown
5963
}
6064

6165
func ActionsRead(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
@@ -69,9 +73,13 @@ func ActionsRead(getClient GetClientFn, t translations.TranslationHelperFunc) (t
6973
mcp.Required(),
7074
mcp.Description("The type of Actions resource to read"),
7175
mcp.Enum(
72-
actionsResourceWorkflow.String(),
73-
actionsResourceWorkflowRun.String(),
74-
actionsResourceWorkflowJob.String(),
76+
actionsResourceGetWorkflow.String(),
77+
actionsResourceGetWorkflowRun.String(),
78+
actionsResourceGetWorkflowRuns.String(),
79+
actionsResourceGetWorkflowJob.String(),
80+
actionsResourceGetWorkflowJobs.String(),
81+
actionsResourceDownloadWorkflowArtifact.String(),
82+
actionsResourceGetWorkflowArtifacts.String(),
7583
),
7684
),
7785
mcp.WithString("owner",
@@ -84,8 +92,79 @@ func ActionsRead(getClient GetClientFn, t translations.TranslationHelperFunc) (t
8492
),
8593
mcp.WithNumber("resource_id",
8694
mcp.Required(),
87-
mcp.Description("The unique identifier of the resource"),
95+
mcp.Description(`The unique identifier of the resource. This will vary based on the "resource" provided, so ensure you provide the correct ID:
96+
- Provide a workflow ID for 'workflow' and 'workflow_runs' resources.
97+
- Provide a workflow run ID for 'workflow_run', 'workflow_jobs', 'workflow_artifact' and 'workflow_artifacts'.
98+
- Provide a job ID for 'workflow_job' resource.
99+
`),
100+
),
101+
mcp.WithObject("workflow_runs_filter",
102+
mcp.Description("Filters for workflow runs. **ONLY** used when resource is 'workflow_runs'"),
103+
mcp.Properties(map[string]any{
104+
"actor": map[string]any{
105+
"type": "string",
106+
"description": "Returns someone's workflow runs. Use the login for the user who created the workflow run.",
107+
},
108+
"branch": map[string]any{
109+
"type": "string",
110+
"description": "Returns workflow runs associated with a branch. Use the name of the branch.",
111+
},
112+
"event": map[string]any{
113+
"type": "string",
114+
"description": "Returns workflow runs for a specific event type",
115+
"enum": []string{
116+
"branch_protection_rule",
117+
"check_run",
118+
"check_suite",
119+
"create",
120+
"delete",
121+
"deployment",
122+
"deployment_status",
123+
"discussion",
124+
"discussion_comment",
125+
"fork",
126+
"gollum",
127+
"issue_comment",
128+
"issues",
129+
"label",
130+
"merge_group",
131+
"milestone",
132+
"page_build",
133+
"public",
134+
"pull_request",
135+
"pull_request_review",
136+
"pull_request_review_comment",
137+
"pull_request_target",
138+
"push",
139+
"registry_package",
140+
"release",
141+
"repository_dispatch",
142+
"schedule",
143+
"status",
144+
"watch",
145+
"workflow_call",
146+
"workflow_dispatch",
147+
"workflow_run",
148+
},
149+
},
150+
"status": map[string]any{
151+
"type": "string",
152+
"description": "Returns workflow runs with the check run status",
153+
"enum": []string{"queued", "in_progress", "completed", "requested", "waiting"},
154+
},
155+
}),
156+
),
157+
mcp.WithObject("workflow_jobs_filter",
158+
mcp.Description("Filters for workflow jobs. **ONLY** used when resource is 'workflow_jobs'"),
159+
mcp.Properties(map[string]any{
160+
"filter": map[string]any{
161+
"type": "string",
162+
"description": "Filters jobs by their completed_at timestamp",
163+
"enum": []string{"latest", "all"},
164+
},
165+
}),
88166
),
167+
WithPagination(),
89168
),
90169
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
91170
owner, err := RequiredParam[string](request, "owner")
@@ -100,25 +179,42 @@ func ActionsRead(getClient GetClientFn, t translations.TranslationHelperFunc) (t
100179
if err != nil {
101180
return mcp.NewToolResultError(err.Error()), nil
102181
}
182+
103183
resourceType := ActionsResourceFromString(resourceTypeStr)
184+
if resourceType == actionsResourceUnknown {
185+
return mcp.NewToolResultError(fmt.Sprintf("unknown resource type: %s", resourceTypeStr)), nil
186+
}
104187

105188
resourceIDInt, err := RequiredInt(request, "resource_id")
106189
if err != nil {
107190
return mcp.NewToolResultError(err.Error()), nil
108191
}
109192

193+
pagination, err := OptionalPaginationParams(request)
194+
if err != nil {
195+
return mcp.NewToolResultError(err.Error()), nil
196+
}
197+
110198
client, err := getClient(ctx)
111199
if err != nil {
112200
return nil, fmt.Errorf("failed to get GitHub client: %w", err)
113201
}
114202

115203
switch resourceType {
116-
case actionsResourceWorkflow:
117-
return getActionsResourceWorkflow(ctx, client, owner, repo, int64(resourceIDInt))
118-
case actionsResourceWorkflowRun:
119-
return nil, fmt.Errorf("get workflow run by ID not implemented yet")
120-
case actionsResourceWorkflowJob:
121-
return nil, fmt.Errorf("get workflow job by ID not implemented yet")
204+
case actionsResourceGetWorkflow:
205+
return getActionsResourceWorkflow(ctx, client, request, owner, repo, int64(resourceIDInt))
206+
case actionsResourceGetWorkflowRun:
207+
return getActionsResourceWorkflowRun(ctx, client, request, owner, repo, int64(resourceIDInt))
208+
case actionsResourceGetWorkflowRuns:
209+
return getActionsResourceWorkflowRuns(ctx, client, request, owner, repo, int64(resourceIDInt), pagination)
210+
case actionsResourceGetWorkflowJob:
211+
return getActionsResourceWorkflowJob(ctx, client, request, owner, repo, int64(resourceIDInt))
212+
case actionsResourceGetWorkflowJobs:
213+
return getActionsResourceWorkflowJobs(ctx, client, request, owner, repo, int64(resourceIDInt), pagination)
214+
case actionsResourceDownloadWorkflowArtifact:
215+
return getActionsResourceDownloadWorkflowArtifact(ctx, client, request, owner, repo, int64(resourceIDInt))
216+
case actionsResourceGetWorkflowArtifacts:
217+
return getActionsResourceWorkflowArtifacts(ctx, client, request, owner, repo, int64(resourceIDInt), pagination)
122218
case actionsResourceUnknown:
123219
return mcp.NewToolResultError(fmt.Sprintf("unknown resource type: %s", resourceTypeStr)), nil
124220
default:
@@ -128,7 +224,7 @@ func ActionsRead(getClient GetClientFn, t translations.TranslationHelperFunc) (t
128224
}
129225
}
130226

131-
func getActionsResourceWorkflow(ctx context.Context, client *github.Client, owner, repo string, resourceID int64) (*mcp.CallToolResult, error) {
227+
func getActionsResourceWorkflow(ctx context.Context, client *github.Client, _ mcp.CallToolRequest, owner, repo string, resourceID int64) (*mcp.CallToolResult, error) {
132228
workflow, resp, err := client.Actions.GetWorkflowByID(ctx, owner, repo, resourceID)
133229
if err != nil {
134230
return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get workflow", resp, err), nil
@@ -143,6 +239,150 @@ func getActionsResourceWorkflow(ctx context.Context, client *github.Client, owne
143239
return mcp.NewToolResultText(string(r)), nil
144240
}
145241

242+
func getActionsResourceWorkflowRun(ctx context.Context, client *github.Client, _ mcp.CallToolRequest, owner, repo string, resourceID int64) (*mcp.CallToolResult, error) {
243+
workflowRun, resp, err := client.Actions.GetWorkflowRunByID(ctx, owner, repo, resourceID)
244+
if err != nil {
245+
return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get workflow run", resp, err), nil
246+
}
247+
defer func() { _ = resp.Body.Close() }()
248+
r, err := json.Marshal(workflowRun)
249+
if err != nil {
250+
return nil, fmt.Errorf("failed to marshal workflow run: %w", err)
251+
}
252+
return mcp.NewToolResultText(string(r)), nil
253+
}
254+
255+
func getActionsResourceWorkflowRuns(ctx context.Context, client *github.Client, request mcp.CallToolRequest, owner, repo string, resourceID int64, pagination PaginationParams) (*mcp.CallToolResult, error) {
256+
filterArgs, err := OptionalParam[map[string]any](request, "workflow_runs_filter")
257+
if err != nil {
258+
return mcp.NewToolResultError(err.Error()), nil
259+
}
260+
261+
filterArgsTyped := make(map[string]string)
262+
for k, v := range filterArgs {
263+
if strVal, ok := v.(string); ok {
264+
filterArgsTyped[k] = strVal
265+
} else {
266+
filterArgsTyped[k] = ""
267+
}
268+
}
269+
270+
workflowRuns, resp, err := client.Actions.ListWorkflowRunsByID(ctx, owner, repo, resourceID, &github.ListWorkflowRunsOptions{
271+
Actor: filterArgsTyped["actor"],
272+
Branch: filterArgsTyped["branch"],
273+
Event: filterArgsTyped["event"],
274+
Status: filterArgsTyped["status"],
275+
ListOptions: github.ListOptions{
276+
Page: pagination.Page,
277+
PerPage: pagination.PerPage,
278+
},
279+
})
280+
if err != nil {
281+
return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to list workflow runs", resp, err), nil
282+
}
283+
284+
defer func() { _ = resp.Body.Close() }()
285+
r, err := json.Marshal(workflowRuns)
286+
if err != nil {
287+
return nil, fmt.Errorf("failed to marshal workflow runs: %w", err)
288+
}
289+
290+
return mcp.NewToolResultText(string(r)), nil
291+
}
292+
293+
func getActionsResourceWorkflowJob(ctx context.Context, client *github.Client, _ mcp.CallToolRequest, owner, repo string, resourceID int64) (*mcp.CallToolResult, error) {
294+
workflowJob, resp, err := client.Actions.GetWorkflowJobByID(ctx, owner, repo, resourceID)
295+
if err != nil {
296+
return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get workflow job", resp, err), nil
297+
}
298+
defer func() { _ = resp.Body.Close() }()
299+
r, err := json.Marshal(workflowJob)
300+
if err != nil {
301+
return nil, fmt.Errorf("failed to marshal workflow job: %w", err)
302+
}
303+
return mcp.NewToolResultText(string(r)), nil
304+
}
305+
306+
func getActionsResourceWorkflowJobs(ctx context.Context, client *github.Client, request mcp.CallToolRequest, owner, repo string, resourceID int64, pagination PaginationParams) (*mcp.CallToolResult, error) {
307+
filterArgs, err := OptionalParam[map[string]any](request, "workflow_jobs_filter")
308+
if err != nil {
309+
return mcp.NewToolResultError(err.Error()), nil
310+
}
311+
312+
filterArgsTyped := make(map[string]string)
313+
for k, v := range filterArgs {
314+
if strVal, ok := v.(string); ok {
315+
filterArgsTyped[k] = strVal
316+
} else {
317+
filterArgsTyped[k] = ""
318+
}
319+
}
320+
321+
workflowJobs, resp, err := client.Actions.ListWorkflowJobs(ctx, owner, repo, resourceID, &github.ListWorkflowJobsOptions{
322+
Filter: filterArgsTyped["filter"],
323+
ListOptions: github.ListOptions{
324+
Page: pagination.Page,
325+
PerPage: pagination.PerPage,
326+
},
327+
})
328+
if err != nil {
329+
return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to list workflow jobs", resp, err), nil
330+
}
331+
332+
defer func() { _ = resp.Body.Close() }()
333+
r, err := json.Marshal(workflowJobs)
334+
if err != nil {
335+
return nil, fmt.Errorf("failed to marshal workflow jobs: %w", err)
336+
}
337+
338+
return mcp.NewToolResultText(string(r)), nil
339+
}
340+
341+
func getActionsResourceDownloadWorkflowArtifact(ctx context.Context, client *github.Client, _ mcp.CallToolRequest, owner, repo string, resourceID int64) (*mcp.CallToolResult, error) {
342+
// Get the download URL for the artifact
343+
url, resp, err := client.Actions.DownloadArtifact(ctx, owner, repo, resourceID, 1)
344+
if err != nil {
345+
return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get artifact download URL", resp, err), nil
346+
}
347+
defer func() { _ = resp.Body.Close() }()
348+
349+
// Create response with the download URL and information
350+
result := map[string]any{
351+
"download_url": url.String(),
352+
"message": "Artifact is available for download",
353+
"note": "The download_url provides a download link for the artifact as a ZIP archive. The link is temporary and expires after a short time.",
354+
"artifact_id": resourceID,
355+
}
356+
357+
r, err := json.Marshal(result)
358+
if err != nil {
359+
return nil, fmt.Errorf("failed to marshal response: %w", err)
360+
}
361+
362+
return mcp.NewToolResultText(string(r)), nil
363+
}
364+
365+
func getActionsResourceWorkflowArtifacts(ctx context.Context, client *github.Client, _ mcp.CallToolRequest, owner, repo string, resourceID int64, pagination PaginationParams) (*mcp.CallToolResult, error) {
366+
// Set up list options
367+
opts := &github.ListOptions{
368+
PerPage: pagination.PerPage,
369+
Page: pagination.Page,
370+
}
371+
372+
artifacts, resp, err := client.Actions.ListWorkflowRunArtifacts(ctx, owner, repo, resourceID, opts)
373+
if err != nil {
374+
return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to list workflow run artifacts", resp, err), nil
375+
}
376+
defer func() { _ = resp.Body.Close() }()
377+
378+
r, err := json.Marshal(artifacts)
379+
if err != nil {
380+
return nil, fmt.Errorf("failed to marshal response: %w", err)
381+
}
382+
383+
return mcp.NewToolResultText(string(r)), nil
384+
}
385+
146386
// ListWorkflows creates a tool to list workflows in a repository
147387
func ListWorkflows(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
148388
return mcp.NewTool("list_workflows",

0 commit comments

Comments
 (0)