Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1189,6 +1189,8 @@ The following sets of tools are available:
7. 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.
8. 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.
9. 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.
10. get_reviewers - Get the list of requested reviewers (users and teams) for a pull request who have not yet submitted a review.
11. get_status_checks - Get a unified view of all status checks for a pull request, combining legacy commit statuses and modern check runs.
(string, required)
- `owner`: Repository owner (string, required)
- `page`: Page number for pagination (min 1) (number, optional)
Expand Down
6 changes: 4 additions & 2 deletions pkg/github/__toolsnaps__/pull_request_read.snap
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"type": "string"
},
"method": {
"description": "Action to specify what pull request data needs to be retrieved from GitHub. \nPossible options: \n 1. get - Get details of a specific pull request.\n 2. get_diff - Get the diff of a pull request.\n 3. get_status - Get combined commit status of a head commit in a pull request.\n 4. get_files - Get the list of files changed in a pull request. Use with pagination parameters to control the number of results returned.\n 5. get_commits - Get the list of commits on a pull request. Use with pagination parameters to control the number of results returned.\n 6. get_review_comments - Get review threads on a pull request. Each thread contains logically grouped review comments made on the same code location during pull request reviews. Returns threads with metadata (isResolved, isOutdated, isCollapsed) and their associated comments. Use cursor-based pagination (perPage, after) to control results.\n 7. 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.\n 8. 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.\n 9. 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.\n",
"description": "Action to specify what pull request data needs to be retrieved from GitHub. \nPossible options: \n 1. get - Get details of a specific pull request.\n 2. get_diff - Get the diff of a pull request.\n 3. get_status - Get combined commit status of a head commit in a pull request.\n 4. get_files - Get the list of files changed in a pull request. Use with pagination parameters to control the number of results returned.\n 5. get_commits - Get the list of commits on a pull request. Use with pagination parameters to control the number of results returned.\n 6. get_review_comments - Get review threads on a pull request. Each thread contains logically grouped review comments made on the same code location during pull request reviews. Returns threads with metadata (isResolved, isOutdated, isCollapsed) and their associated comments. Use cursor-based pagination (perPage, after) to control results.\n 7. 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.\n 8. 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.\n 9. 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.\n 10. get_reviewers - Get the list of requested reviewers (users and teams) for a pull request who have not yet submitted a review.\n 11. get_status_checks - Get a unified view of all status checks for a pull request, combining legacy commit statuses and modern check runs.\n",
"enum": [
"get",
"get_diff",
Expand All @@ -22,7 +22,9 @@
"get_review_comments",
"get_reviews",
"get_comments",
"get_check_runs"
"get_check_runs",
"get_reviewers",
"get_status_checks"
],
"type": "string"
},
Expand Down
1 change: 1 addition & 0 deletions pkg/github/helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ const (
PutReposPullsMergeByOwnerByRepoByPullNumber = "PUT /repos/{owner}/{repo}/pulls/{pull_number}/merge"
PutReposPullsUpdateBranchByOwnerByRepoByPullNumber = "PUT /repos/{owner}/{repo}/pulls/{pull_number}/update-branch"
PostReposPullsRequestedReviewersByOwnerByRepoByPullNumber = "POST /repos/{owner}/{repo}/pulls/{pull_number}/requested_reviewers"
GetReposPullsRequestedReviewersByOwnerByRepoByPullNumber = "GET /repos/{owner}/{repo}/pulls/{pull_number}/requested_reviewers"
PostReposPullsCommentsByOwnerByRepoByPullNumber = "POST /repos/{owner}/{repo}/pulls/{pull_number}/comments"
PostReposPullsCommentsReactionsByOwnerByRepoByCommentID = "POST /repos/{owner}/{repo}/pulls/comments/{comment_id}/reactions"

Expand Down
35 changes: 35 additions & 0 deletions pkg/github/minimal_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -1827,6 +1827,41 @@ 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"`
HTMLURL string `json:"html_url,omitempty"`
}

// MinimalPRReviewers contains the list of requested reviewers for a pull request.
type MinimalPRReviewers struct {
Users []MinimalReviewerUser `json:"users"`
Teams []MinimalReviewerTeam `json:"teams"`
}

// MinimalCommitStatus represents a single commit status check.
type MinimalCommitStatus struct {
State string `json:"state"`
Context string `json:"context"`
Description string `json:"description,omitempty"`
TargetURL string `json:"target_url,omitempty"`
}

// MinimalStatusChecks contains a unified view of all status checks for a PR.
type MinimalStatusChecks struct {
CombinedState string `json:"combined_state"`
Statuses []MinimalCommitStatus `json:"statuses"`
CheckRuns []MinimalCheckRun `json:"check_runs"`
TotalCount int `json:"total_count"`
}

// convertToMinimalCheckRun converts a GitHub API CheckRun to MinimalCheckRun
func convertToMinimalCheckRun(checkRun *github.CheckRun) MinimalCheckRun {
minimalCheckRun := MinimalCheckRun{
Expand Down
2 changes: 1 addition & 1 deletion pkg/github/projects_resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,7 @@ func Test_ResolveFieldNamesToIDs_Success(t *testing.T) {
assert.Equal(t, []int64{100, 200}, ids)
}

// Field and single-select option name matching is case-insensitive so agents passing lowercase
// Field and single-select option name matching is case-insensitive so agents passing lowercase
// names like "status" or "in progress" resolve to "Status" and "In Progress" respectively.
func Test_ResolveProjectFieldByName_CaseInsensitive(t *testing.T) {
mocked := githubv4mock.NewMockedHTTPClient(
Expand Down
137 changes: 136 additions & 1 deletion pkg/github/pullrequests.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@ Possible options:
7. 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.
8. 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.
9. 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.
10. get_reviewers - Get the list of requested reviewers (users and teams) for a pull request who have not yet submitted a review.
11. 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_commits", "get_review_comments", "get_reviews", "get_comments", "get_check_runs"},
Enum: []any{"get", "get_diff", "get_status", "get_files", "get_commits", "get_review_comments", "get_reviews", "get_comments", "get_check_runs", "get_reviewers", "get_status_checks"},
},
"owner": {
Type: "string",
Expand Down Expand Up @@ -154,6 +156,12 @@ Possible options:
case "get_check_runs":
result, err := GetPullRequestCheckRuns(ctx, client, owner, repo, pullNumber, pagination)
return attachIFC(result), nil, err
case "get_reviewers":
result, err := GetPullRequestReviewers(ctx, client, owner, repo, pullNumber)
return attachIFC(result), nil, err
case "get_status_checks":
result, err := GetPullRequestStatusChecks(ctx, client, owner, repo, pullNumber)
return attachIFC(result), nil, err
default:
return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", method)), nil, nil
}
Expand Down Expand Up @@ -380,6 +388,133 @@ func GetPullRequestCheckRuns(ctx context.Context, client *github.Client, owner,
return utils.NewToolResultText(string(r)), nil
}

// closeStatusResponse closes the response body and, for any non-200 status,
// returns a tool error result. It returns (nil, nil) when the response is OK so
// the caller can proceed. Closing the body synchronously here (rather than via a
// deferred closure over a reused resp variable) avoids leaking earlier response
// bodies when several API calls are made in sequence.
func closeStatusResponse(ctx context.Context, resp *github.Response, message string) (*mcp.CallToolResult, error) {
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, message, resp, body), nil
}

return nil, 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
}
if errResult, err := closeStatusResponse(ctx, resp, "failed to get pull request reviewers"); err != nil || errResult != nil {
return errResult, err
}

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(),
HTMLURL: t.GetHTMLURL(),
})
}

r, err := json.Marshal(result)
if err != nil {
return nil, fmt.Errorf("failed to marshal response: %w", err)
}

return utils.NewToolResultText(string(r)), 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
}
if errResult, err := closeStatusResponse(ctx, resp, "failed to get pull request"); err != nil || errResult != nil {
return errResult, err
}

sha := pr.GetHead().GetSHA()

result := MinimalStatusChecks{}

// Page through the combined commit statuses so the unified view is complete
// rather than limited to the first page.
statusOpts := &github.ListOptions{PerPage: 100}
for {
combinedStatus, resp, err := client.Repositories.GetCombinedStatus(ctx, owner, repo, sha, statusOpts)
if err != nil {
return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get combined status", resp, err), nil
}
if errResult, err := closeStatusResponse(ctx, resp, "failed to get combined status"); err != nil || errResult != nil {
return errResult, err
}

// The combined state is identical across pages; capture it once.
if result.CombinedState == "" {
result.CombinedState = combinedStatus.GetState()
}

for _, s := range combinedStatus.Statuses {
result.Statuses = append(result.Statuses, MinimalCommitStatus{
State: s.GetState(),
Context: s.GetContext(),
Description: s.GetDescription(),
TargetURL: s.GetTargetURL(),
})
}

if resp.NextPage == 0 {
break
}
statusOpts.Page = resp.NextPage
}

// Page through the check runs for the same reason.
checkOpts := &github.ListCheckRunsOptions{ListOptions: github.ListOptions{PerPage: 100}}
for {
checkRuns, resp, err := client.Checks.ListCheckRunsForRef(ctx, owner, repo, sha, checkOpts)
if err != nil {
return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get check runs", resp, err), nil
}
if errResult, err := closeStatusResponse(ctx, resp, "failed to get check runs"); err != nil || errResult != nil {
return errResult, err
}

for _, cr := range checkRuns.CheckRuns {
result.CheckRuns = append(result.CheckRuns, convertToMinimalCheckRun(cr))
}

if resp.NextPage == 0 {
break
}
checkOpts.Page = resp.NextPage
}

result.TotalCount = len(result.Statuses) + len(result.CheckRuns)

r, err := json.Marshal(result)
if err != nil {
return nil, fmt.Errorf("failed to marshal response: %w", err)
}

return utils.NewToolResultText(string(r)), nil
}

func GetPullRequestFiles(ctx context.Context, client *github.Client, deps ToolDependencies, owner, repo string, pullNumber int, pagination PaginationParams) (*mcp.CallToolResult, error) {
if restricted, err := enforcePullRequestLockdown(ctx, client, deps, owner, repo, pullNumber); restricted != nil || err != nil {
return restricted, err
Expand Down
Loading