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
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1052,8 +1052,18 @@ The following sets of tools are available:
- `repo`: Repository name (string, required)
- `title`: PR title (string, required)

- **get_prs_reviewed_by** - Get PRs reviewed by user
- **Required OAuth Scopes**: `repo`
- `owner`: Repository owner (string, required)
- `page`: Page number for pagination (min 1) (number, optional)
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
- `repo`: Repository name (string, required)
- `reviewer`: GitHub username of the reviewer (string, required)
- `state`: PR state filter: open, closed, or all (string, optional)

- **list_pull_requests** - List pull requests
- **Required OAuth Scopes**: `repo`
- `author`: Filter by PR author username (client-side filter) (string, optional)
- `base`: Filter by base branch (string, optional)
- `direction`: Sort direction (string, optional)
- `head`: Filter by head user/org and branch (string, optional)
Expand All @@ -1075,8 +1085,8 @@ The following sets of tools are available:

- **pull_request_read** - Get details for a single pull request
- **Required OAuth Scopes**: `repo`
- `method`: Action to specify what pull request data needs to be retrieved from GitHub.
Possible options:
- `method`: Action to specify what pull request data needs to be retrieved from GitHub.
Possible options:
1. get - Get details of a specific pull request.
2. get_diff - Get the diff of a pull request.
3. get_status - Get status of a head commit in a pull request. This reflects status of builds and checks.
Expand Down
177 changes: 173 additions & 4 deletions pkg/github/pullrequests.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"io"
"net/http"
"strings"

"github.com/go-viper/mapstructure/v2"
"github.com/google/go-github/v79/github"
Expand All @@ -29,8 +30,8 @@ func PullRequestRead(t translations.TranslationHelperFunc) inventory.ServerTool
Properties: map[string]*jsonschema.Schema{
"method": {
Type: "string",
Description: `Action to specify what pull request data needs to be retrieved from GitHub.
Possible options:
Description: `Action to specify what pull request data needs to be retrieved from GitHub.
Possible options:
1. get - Get details of a specific pull request.
2. get_diff - Get the diff of a pull request.
3. get_status - Get status of a head commit in a pull request. This reflects status of builds and checks.
Expand Down Expand Up @@ -1046,6 +1047,10 @@ func ListPullRequests(t translations.TranslationHelperFunc) inventory.ServerTool
Description: "Sort direction",
Enum: []any{"asc", "desc"},
},
"author": {
Type: "string",
Description: "Filter by PR author username (client-side filter)",
},
},
Required: []string{"owner", "repo"},
}
Expand All @@ -1055,7 +1060,7 @@ func ListPullRequests(t translations.TranslationHelperFunc) inventory.ServerTool
ToolsetMetadataPullRequests,
mcp.Tool{
Name: "list_pull_requests",
Description: t("TOOL_LIST_PULL_REQUESTS_DESCRIPTION", "List pull requests in a GitHub repository. If the user specifies an author, then DO NOT use this tool and use the search_pull_requests tool instead."),
Description: t("TOOL_LIST_PULL_REQUESTS_DESCRIPTION", "List pull requests in a GitHub repository. If the user specifies an author, then DO NOT use this tool and use the search_pull_requests tool instead. If you receive a 422 error from search_pull_requests, then use the get_prs_reviewed_by tool (to list by reviewer), or this tool with the author parameter (for filtering by author) depending on what you need."),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description for TOOL_LIST_PULL_REQUESTS_DESCRIPTION is getting a bit long and complex. Consider rephrasing for clarity or splitting it into multiple sentences.

Annotations: &mcp.ToolAnnotations{
Title: t("TOOL_LIST_PULL_REQUESTS_USER_TITLE", "List pull requests"),
ReadOnlyHint: true,
Expand Down Expand Up @@ -1092,6 +1097,10 @@ func ListPullRequests(t translations.TranslationHelperFunc) inventory.ServerTool
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}
author, err := OptionalParam[string](args, "author")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}
pagination, err := OptionalPaginationParams(args)
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
Expand Down Expand Up @@ -1131,6 +1140,18 @@ func ListPullRequests(t translations.TranslationHelperFunc) inventory.ServerTool
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to list pull requests", resp, bodyBytes), nil, nil
}

// Filter by author if specified (client-side filtering)
if author != "" {
filtered := make([]*github.PullRequest, 0)
for _, pr := range prs {
if pr != nil && pr.User != nil && pr.User.Login != nil &&
strings.EqualFold(*pr.User.Login, author) {
filtered = append(filtered, pr)
}
}
prs = filtered
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add some comments for the new function GetPRsReviewedBy to explain the high-level logic.


// sanitize title/body on each PR
for _, pr := range prs {
if pr == nil {
Expand All @@ -1153,6 +1174,154 @@ func ListPullRequests(t translations.TranslationHelperFunc) inventory.ServerTool
})
}

// GetPRsReviewedBy creates a tool for finding PRs reviewed by a specific user
func GetPRsReviewedBy(t translations.TranslationHelperFunc) inventory.ServerTool {
schema := &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"owner": {
Type: "string",
Description: "Repository owner",
},
"repo": {
Type: "string",
Description: "Repository name",
},
"reviewer": {
Type: "string",
Description: "GitHub username of the reviewer",
},
"state": {
Type: "string",
Description: "PR state filter: open, closed, or all",
Enum: []any{"open", "closed", "all"},
},
},
Required: []string{"owner", "repo", "reviewer"},
}
WithPagination(schema)

return NewTool(
ToolsetMetadataPullRequests,
mcp.Tool{
Name: "get_prs_reviewed_by",
Description: t("TOOL_GET_PRS_REVIEWED_BY_DESCRIPTION",
"Find PRs reviewed by a user. Use this tool if you receive a 422 error when using the search_pull_requests tool."),
Annotations: &mcp.ToolAnnotations{
Title: t("TOOL_GET_PRS_REVIEWED_BY_TITLE", "Get PRs reviewed by user"),
ReadOnlyHint: true,
},
InputSchema: schema,
},
[]scopes.Scope{scopes.Repo},
func(ctx context.Context, deps ToolDependencies, _ *mcp.CallToolRequest, args map[string]any) (*mcp.CallToolResult, any, error) {
owner, err := RequiredParam[string](args, "owner")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}
repo, err := RequiredParam[string](args, "repo")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}
reviewer, err := RequiredParam[string](args, "reviewer")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}
state, err := OptionalParam[string](args, "state")
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}
if state == "" {
state = "all"
}

opts := &github.PullRequestListOptions{
State: state,
ListOptions: github.ListOptions{
PerPage: 100,
},
}

pagination, err := OptionalPaginationParams(args)
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}
if pagination.Page > 0 {
opts.ListOptions.Page = pagination.Page
}
if pagination.PerPage > 0 {
opts.ListOptions.PerPage = pagination.PerPage
}

client, err := deps.GetClient(ctx)
if err != nil {
return utils.NewToolResultErrorFromErr("failed to get GitHub client", err), nil, nil
}

// List all PRs
prs, resp, err := client.PullRequests.List(ctx, owner, repo, opts)
if err != nil {
return ghErrors.NewGitHubAPIErrorResponse(ctx,
"failed to list pull requests",
resp,
err,
), nil, nil
}
defer func() { _ = resp.Body.Close() }()

// Filter PRs by reviewer
var reviewedPRs []*github.PullRequest
for _, pr := range prs {
if pr.Number == nil {
continue
}
reviews, _, err := client.PullRequests.ListReviews(ctx, owner, repo, *pr.Number, nil)
if err != nil {
continue // Skip PRs we can't get reviews for
}
for _, review := range reviews {
if review.User != nil && review.User.Login != nil &&
strings.EqualFold(*review.User.Login, reviewer) {
reviewedPRs = append(reviewedPRs, pr)
break
}
}
}

// Sanitize the results
sanitized := make([]map[string]any, 0, len(reviewedPRs))
for _, pr := range reviewedPRs {
if pr.Title != nil {
pr.Title = github.Ptr(sanitize.Sanitize(*pr.Title))
}
if pr.Body != nil {
pr.Body = github.Ptr(sanitize.Sanitize(*pr.Body))
}
sanitized = append(sanitized, map[string]any{
"number": pr.GetNumber(),
"title": pr.GetTitle(),
"state": pr.GetState(),
"html_url": pr.GetHTMLURL(),
"user": pr.GetUser().GetLogin(),
"draft": pr.GetDraft(),
})
}

result := map[string]any{
"pull_requests": sanitized,
"total_count": len(sanitized),
}

r, err := json.Marshal(result)
if err != nil {
return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil
}

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

// MergePullRequest creates a tool to merge a pull request.
func MergePullRequest(t translations.TranslationHelperFunc) inventory.ServerTool {
schema := &jsonschema.Schema{
Expand Down Expand Up @@ -1310,7 +1479,7 @@ func SearchPullRequests(t translations.TranslationHelperFunc) inventory.ServerTo
ToolsetMetadataPullRequests,
mcp.Tool{
Name: "search_pull_requests",
Description: t("TOOL_SEARCH_PULL_REQUESTS_DESCRIPTION", "Search for pull requests in GitHub repositories using issues search syntax already scoped to is:pr"),
Description: t("TOOL_SEARCH_PULL_REQUESTS_DESCRIPTION", "Search for pull requests in GitHub repositories using issues search syntax already scoped to is:pr. If you receive a 422 error, then use the get_prs_reviewed_by tool instead."),
Annotations: &mcp.ToolAnnotations{
Title: t("TOOL_SEARCH_PULL_REQUESTS_USER_TITLE", "Search pull requests"),
ReadOnlyHint: true,
Expand Down
Loading