diff --git a/README.md b/README.md index b40974e20..1c79e0672 100644 --- a/README.md +++ b/README.md @@ -679,6 +679,7 @@ The following sets of tools are available (all are on by default): - `maintainer_can_modify`: Allow maintainer edits (boolean, optional) - `owner`: Repository owner (string, required) - `repo`: Repository name (string, required) + - `reviewers`: GitHub usernames to request reviews from (string[], optional) - `title`: PR title (string, required) - **delete_pending_pull_request_review** - Delete the requester's latest pending pull request review diff --git a/pkg/github/__toolsnaps__/create_pull_request.snap b/pkg/github/__toolsnaps__/create_pull_request.snap index 44142a79e..85ba52ea2 100644 --- a/pkg/github/__toolsnaps__/create_pull_request.snap +++ b/pkg/github/__toolsnaps__/create_pull_request.snap @@ -34,6 +34,13 @@ "description": "Repository name", "type": "string" }, + "reviewers": { + "description": "GitHub usernames to request reviews from", + "items": { + "type": "string" + }, + "type": "array" + }, "title": { "description": "PR title", "type": "string" diff --git a/pkg/github/pullrequests.go b/pkg/github/pullrequests.go index f82117cad..5b48f4aca 100644 --- a/pkg/github/pullrequests.go +++ b/pkg/github/pullrequests.go @@ -120,6 +120,12 @@ func CreatePullRequest(getClient GetClientFn, t translations.TranslationHelperFu mcp.WithBoolean("maintainer_can_modify", mcp.Description("Allow maintainer edits"), ), + mcp.WithArray("reviewers", + mcp.Description("GitHub usernames to request reviews from"), + mcp.Items(map[string]interface{}{ + "type": "string", + }), + ), ), func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { owner, err := RequiredParam[string](request, "owner") @@ -158,6 +164,12 @@ func CreatePullRequest(getClient GetClientFn, t translations.TranslationHelperFu return mcp.NewToolResultError(err.Error()), nil } + // Handle reviewers parameter + reviewers, err := OptionalStringArrayParam(request, "reviewers") + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + newPR := &github.NewPullRequest{ Title: github.Ptr(title), Head: github.Ptr(head), @@ -193,6 +205,46 @@ func CreatePullRequest(getClient GetClientFn, t translations.TranslationHelperFu return mcp.NewToolResultError(fmt.Sprintf("failed to create pull request: %s", string(body))), nil } + // Request reviewers if provided + if len(reviewers) > 0 { + reviewersRequest := github.ReviewersRequest{ + Reviewers: reviewers, + } + + _, reviewResp, err := client.PullRequests.RequestReviewers(ctx, owner, repo, *pr.Number, reviewersRequest) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to request reviewers", + reviewResp, + err, + ), nil + } + defer func() { + if reviewResp != nil && reviewResp.Body != nil { + _ = reviewResp.Body.Close() + } + }() + + if reviewResp.StatusCode != http.StatusCreated && reviewResp.StatusCode != http.StatusOK { + body, err := io.ReadAll(reviewResp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + return mcp.NewToolResultError(fmt.Sprintf("failed to request reviewers: %s", string(body))), nil + } + + // Refresh PR data to include reviewers + pr, resp, err = client.PullRequests.Get(ctx, owner, repo, *pr.Number) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, + "failed to get updated pull request", + resp, + err, + ), nil + } + defer func() { _ = resp.Body.Close() }() + } + r, err := json.Marshal(pr) if err != nil { return nil, fmt.Errorf("failed to marshal response: %w", err) diff --git a/pkg/github/pullrequests_test.go b/pkg/github/pullrequests_test.go index 3a99d9f46..ee4b0f478 100644 --- a/pkg/github/pullrequests_test.go +++ b/pkg/github/pullrequests_test.go @@ -1791,6 +1791,7 @@ func Test_CreatePullRequest(t *testing.T) { assert.Contains(t, tool.InputSchema.Properties, "base") assert.Contains(t, tool.InputSchema.Properties, "draft") assert.Contains(t, tool.InputSchema.Properties, "maintainer_can_modify") + assert.Contains(t, tool.InputSchema.Properties, "reviewers") assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "title", "head", "base"}) // Setup mock PR for success case @@ -1885,6 +1886,49 @@ func Test_CreatePullRequest(t *testing.T) { expectError: true, expectedErrMsg: "failed to create pull request", }, + { + name: "successful PR creation with reviewers", + mockedClient: mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.PostReposPullsByOwnerByRepo, + expectRequestBody(t, map[string]interface{}{ + "title": "Test PR with reviewers", + "body": "This PR has reviewers", + "head": "feature-branch", + "base": "main", + "draft": false, + "maintainer_can_modify": true, + }).andThen( + mockResponse(t, http.StatusCreated, mockPR), + ), + ), + mock.WithRequestMatchHandler( + mock.PostReposPullsRequestedReviewersByOwnerByRepoByPullNumber, + expectRequestBody(t, map[string]interface{}{ + "reviewers": []interface{}{"reviewer1", "reviewer2"}, + }).andThen( + mockResponse(t, http.StatusCreated, mockPR), + ), + ), + mock.WithRequestMatch( + mock.GetReposPullsByOwnerByRepoByPullNumber, + mockPR, + ), + ), + requestArgs: map[string]interface{}{ + "owner": "owner", + "repo": "repo", + "title": "Test PR with reviewers", + "body": "This PR has reviewers", + "head": "feature-branch", + "base": "main", + "draft": false, + "maintainer_can_modify": true, + "reviewers": []string{"reviewer1", "reviewer2"}, + }, + expectError: false, + expectedPR: mockPR, + }, } for _, tc := range tests {