From 265440c742ef4638f8b667ef404daa199d4a1209 Mon Sep 17 00:00:00 2001 From: salakonrad Date: Mon, 8 Jun 2026 08:33:55 +0200 Subject: [PATCH] feat: add get_reviewers and get_status_checks methods to pull_request_read MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends the pull_request_read tool with two new methods: - get_reviewers: returns pending reviewer requests (both individual users and teams) using the PullRequests.ListReviewers API. This fills a gap in the existing toolset — requested team reviewers were previously not exposed anywhere in the MCP server. - get_status_checks: returns a unified view of all status checks for a PR's head commit by combining legacy commit statuses (e.g. Atlantis plan/policy results) and modern GitHub Actions check runs into a single response with a top-level combined_state. New output types added to minimal_types.go: - MinimalReviewerUser, MinimalReviewerTeam, MinimalPRReviewers - MinimalCommitStatus, MinimalStatusChecks Co-Authored-By: Claude Sonnet 4.6 --- pkg/github/minimal_types.go | 38 ++++++++++++ pkg/github/pullrequests.go | 118 +++++++++++++++++++++++++++++++++++- 2 files changed, 155 insertions(+), 1 deletion(-) diff --git a/pkg/github/minimal_types.go b/pkg/github/minimal_types.go index 5200be297f..641ffdbd48 100644 --- a/pkg/github/minimal_types.go +++ b/pkg/github/minimal_types.go @@ -1667,6 +1667,44 @@ type MinimalCheckRunsResult struct { CheckRuns []MinimalCheckRun `json:"check_runs"` } +// MinimalReviewerUser is a user requested to review a pull request. +type MinimalReviewerUser struct { + Login string `json:"login"` + HTMLURL string `json:"html_url,omitempty"` +} + +// MinimalReviewerTeam is a team requested to review a pull request. +type MinimalReviewerTeam struct { + Slug string `json:"slug"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + HTMLURL string `json:"html_url,omitempty"` +} + +// MinimalPRReviewers is the output type for PR requested reviewers. +type MinimalPRReviewers struct { + Users []MinimalReviewerUser `json:"users,omitempty"` + Teams []MinimalReviewerTeam `json:"teams,omitempty"` +} + +// MinimalCommitStatus is a single commit status entry. +type MinimalCommitStatus struct { + State string `json:"state"` + Context string `json:"context"` + Description string `json:"description,omitempty"` + TargetURL string `json:"target_url,omitempty"` + CreatedAt string `json:"created_at,omitempty"` + UpdatedAt string `json:"updated_at,omitempty"` +} + +// MinimalStatusChecks combines legacy commit statuses and modern check runs. +type MinimalStatusChecks struct { + CombinedState string `json:"combined_state"` + Statuses []MinimalCommitStatus `json:"statuses,omitempty"` + CheckRuns []MinimalCheckRun `json:"check_runs,omitempty"` + TotalCount int `json:"total_count"` +} + // convertToMinimalCheckRun converts a GitHub API CheckRun to MinimalCheckRun func convertToMinimalCheckRun(checkRun *github.CheckRun) MinimalCheckRun { minimalCheckRun := MinimalCheckRun{ diff --git a/pkg/github/pullrequests.go b/pkg/github/pullrequests.go index 05028850d7..2f7a087ac7 100644 --- a/pkg/github/pullrequests.go +++ b/pkg/github/pullrequests.go @@ -39,8 +39,10 @@ Possible options: 6. get_reviews - Get the reviews on a pull request. When asked for review comments, use get_review_comments method. Use with pagination parameters to control the number of results returned. 7. get_comments - Get comments on a pull request. Use this if user doesn't specifically want review comments. Use with pagination parameters to control the number of results returned. 8. get_check_runs - Get check runs for the head commit of a pull request. Check runs are the individual CI/CD jobs and checks that run on the PR. + 9. get_reviewers - Get the list of requested reviewers (users and teams) for a pull request who have not yet submitted a review. + 10. get_status_checks - Get a unified view of all status checks for a pull request, combining legacy commit statuses and modern check runs. `, - Enum: []any{"get", "get_diff", "get_status", "get_files", "get_review_comments", "get_reviews", "get_comments", "get_check_runs"}, + Enum: []any{"get", "get_diff", "get_status", "get_files", "get_review_comments", "get_reviews", "get_comments", "get_check_runs", "get_reviewers", "get_status_checks"}, }, "owner": { Type: "string", @@ -139,6 +141,12 @@ Possible options: case "get_check_runs": result, err := GetPullRequestCheckRuns(ctx, client, owner, repo, pullNumber, pagination) return result, nil, err + case "get_reviewers": + result, err := GetPullRequestReviewers(ctx, client, owner, repo, pullNumber) + return result, nil, err + case "get_status_checks": + result, err := GetPullRequestStatusChecks(ctx, client, owner, repo, pullNumber) + return result, nil, err default: return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", method)), nil, nil } @@ -343,6 +351,114 @@ func GetPullRequestCheckRuns(ctx context.Context, client *github.Client, owner, return utils.NewToolResultText(string(r)), nil } +func GetPullRequestReviewers(ctx context.Context, client *github.Client, owner, repo string, pullNumber int) (*mcp.CallToolResult, error) { + reviewers, resp, err := client.PullRequests.ListReviewers(ctx, owner, repo, pullNumber) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get pull request reviewers", resp, err), nil + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get pull request reviewers", resp, body), nil + } + + result := MinimalPRReviewers{} + for _, u := range reviewers.Users { + result.Users = append(result.Users, MinimalReviewerUser{ + Login: u.GetLogin(), + HTMLURL: u.GetHTMLURL(), + }) + } + for _, t := range reviewers.Teams { + result.Teams = append(result.Teams, MinimalReviewerTeam{ + Slug: t.GetSlug(), + Name: t.GetName(), + Description: t.GetDescription(), + HTMLURL: t.GetHTMLURL(), + }) + } + + return MarshalledTextResult(result), nil +} + +func GetPullRequestStatusChecks(ctx context.Context, client *github.Client, owner, repo string, pullNumber int) (*mcp.CallToolResult, error) { + pr, resp, err := client.PullRequests.Get(ctx, owner, repo, pullNumber) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get pull request", resp, err), nil + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get pull request", resp, body), nil + } + + sha := pr.GetHead().GetSHA() + + combinedStatus, resp, err := client.Repositories.GetCombinedStatus(ctx, owner, repo, sha, nil) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get combined status", resp, err), nil + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get combined status", resp, body), nil + } + + checkRuns, resp, err := client.Checks.ListCheckRunsForRef(ctx, owner, repo, sha, nil) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get check runs", resp, err), nil + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get check runs", resp, body), nil + } + + result := MinimalStatusChecks{ + CombinedState: combinedStatus.GetState(), + } + + for _, s := range combinedStatus.Statuses { + ms := MinimalCommitStatus{ + State: s.GetState(), + Context: s.GetContext(), + Description: s.GetDescription(), + TargetURL: s.GetTargetURL(), + } + if s.CreatedAt != nil { + ms.CreatedAt = s.CreatedAt.Format("2006-01-02T15:04:05Z") + } + if s.UpdatedAt != nil { + ms.UpdatedAt = s.UpdatedAt.Format("2006-01-02T15:04:05Z") + } + result.Statuses = append(result.Statuses, ms) + } + + for _, cr := range checkRuns.CheckRuns { + result.CheckRuns = append(result.CheckRuns, convertToMinimalCheckRun(cr)) + } + + result.TotalCount = len(result.Statuses) + len(result.CheckRuns) + + return MarshalledTextResult(result), nil +} + func GetPullRequestFiles(ctx context.Context, client *github.Client, owner, repo string, pullNumber int, pagination PaginationParams) (*mcp.CallToolResult, error) { opts := &github.ListOptions{ PerPage: pagination.PerPage,