Skip to content

Commit 762e9e1

Browse files
committed
WIP: copilot as reviewer
1 parent 70d47de commit 762e9e1

File tree

3 files changed

+197
-10
lines changed

3 files changed

+197
-10
lines changed

e2e/e2e_test.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -913,3 +913,148 @@ func TestPullRequestReviewDeletion(t *testing.T) {
913913
require.Len(t, noReviews, 0, "expected to find no reviews")
914914

915915
}
916+
917+
func TestRequestCopilotReview(t *testing.T) {
918+
t.Parallel()
919+
920+
mcpClient := setupMCPClient(t)
921+
922+
ctx := context.Background()
923+
924+
// First, who am I
925+
getMeRequest := mcp.CallToolRequest{}
926+
getMeRequest.Params.Name = "get_me"
927+
928+
t.Log("Getting current user...")
929+
resp, err := mcpClient.CallTool(ctx, getMeRequest)
930+
require.NoError(t, err, "expected to call 'get_me' tool successfully")
931+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
932+
933+
require.False(t, resp.IsError, "expected result not to be an error")
934+
require.Len(t, resp.Content, 1, "expected content to have one item")
935+
936+
textContent, ok := resp.Content[0].(mcp.TextContent)
937+
require.True(t, ok, "expected content to be of type TextContent")
938+
939+
var trimmedGetMeText struct {
940+
Login string `json:"login"`
941+
}
942+
err = json.Unmarshal([]byte(textContent.Text), &trimmedGetMeText)
943+
require.NoError(t, err, "expected to unmarshal text content successfully")
944+
945+
currentOwner := trimmedGetMeText.Login
946+
947+
// Then create a repository with a README (via autoInit)
948+
repoName := fmt.Sprintf("github-mcp-server-e2e-%s-%d", t.Name(), time.Now().UnixMilli())
949+
createRepoRequest := mcp.CallToolRequest{}
950+
createRepoRequest.Params.Name = "create_repository"
951+
createRepoRequest.Params.Arguments = map[string]any{
952+
"name": repoName,
953+
"private": true,
954+
"autoInit": true,
955+
}
956+
957+
t.Logf("Creating repository %s/%s...", currentOwner, repoName)
958+
_, err = mcpClient.CallTool(ctx, createRepoRequest)
959+
require.NoError(t, err, "expected to call 'get_me' tool successfully")
960+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
961+
962+
// Cleanup the repository after the test
963+
t.Cleanup(func() {
964+
// MCP Server doesn't support deletions, but we can use the GitHub Client
965+
ghClient := gogithub.NewClient(nil).WithAuthToken(getE2EToken(t))
966+
t.Logf("Deleting repository %s/%s...", currentOwner, repoName)
967+
_, err := ghClient.Repositories.Delete(context.Background(), currentOwner, repoName)
968+
require.NoError(t, err, "expected to delete repository successfully")
969+
})
970+
971+
// Create a branch on which to create a new commit
972+
createBranchRequest := mcp.CallToolRequest{}
973+
createBranchRequest.Params.Name = "create_branch"
974+
createBranchRequest.Params.Arguments = map[string]any{
975+
"owner": currentOwner,
976+
"repo": repoName,
977+
"branch": "test-branch",
978+
"from_branch": "main",
979+
}
980+
981+
t.Logf("Creating branch in %s/%s...", currentOwner, repoName)
982+
resp, err = mcpClient.CallTool(ctx, createBranchRequest)
983+
require.NoError(t, err, "expected to call 'create_branch' tool successfully")
984+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
985+
986+
// Create a commit with a new file
987+
commitRequest := mcp.CallToolRequest{}
988+
commitRequest.Params.Name = "create_or_update_file"
989+
commitRequest.Params.Arguments = map[string]any{
990+
"owner": currentOwner,
991+
"repo": repoName,
992+
"path": "test-file.txt",
993+
"content": fmt.Sprintf("Created by e2e test %s", t.Name()),
994+
"message": "Add test file",
995+
"branch": "test-branch",
996+
}
997+
998+
t.Logf("Creating commit with new file in %s/%s...", currentOwner, repoName)
999+
resp, err = mcpClient.CallTool(ctx, commitRequest)
1000+
require.NoError(t, err, "expected to call 'create_or_update_file' tool successfully")
1001+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
1002+
1003+
textContent, ok = resp.Content[0].(mcp.TextContent)
1004+
require.True(t, ok, "expected content to be of type TextContent")
1005+
1006+
var trimmedCommitText struct {
1007+
SHA string `json:"sha"`
1008+
}
1009+
err = json.Unmarshal([]byte(textContent.Text), &trimmedCommitText)
1010+
require.NoError(t, err, "expected to unmarshal text content successfully")
1011+
commitId := trimmedCommitText.SHA
1012+
1013+
// Create a pull request
1014+
prRequest := mcp.CallToolRequest{}
1015+
prRequest.Params.Name = "create_pull_request"
1016+
prRequest.Params.Arguments = map[string]any{
1017+
"owner": currentOwner,
1018+
"repo": repoName,
1019+
"title": "Test PR",
1020+
"body": "This is a test PR",
1021+
"head": "test-branch",
1022+
"base": "main",
1023+
"commitId": commitId,
1024+
}
1025+
1026+
t.Logf("Creating pull request in %s/%s...", currentOwner, repoName)
1027+
resp, err = mcpClient.CallTool(ctx, prRequest)
1028+
require.NoError(t, err, "expected to call 'create_pull_request' tool successfully")
1029+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
1030+
1031+
// Request a copilot review
1032+
requestCopilotReviewRequest := mcp.CallToolRequest{}
1033+
requestCopilotReviewRequest.Params.Name = "request_copilot_review"
1034+
requestCopilotReviewRequest.Params.Arguments = map[string]any{
1035+
"owner": currentOwner,
1036+
"repo": repoName,
1037+
"pullNumber": 1,
1038+
}
1039+
1040+
t.Logf("Requesting Copilot review for pull request in %s/%s...", currentOwner, repoName)
1041+
resp, err = mcpClient.CallTool(ctx, requestCopilotReviewRequest)
1042+
require.NoError(t, err, "expected to call 'request_copilot_review' tool successfully")
1043+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
1044+
1045+
textContent, ok = resp.Content[0].(mcp.TextContent)
1046+
require.True(t, ok, "expected content to be of type TextContent")
1047+
require.Equal(t, "", textContent.Text, "expected content to be empty")
1048+
1049+
// Finally, get requested reviews and see copilot is in there
1050+
// MCP Server doesn't support requesting reviews yet, but we can use the GitHub Client
1051+
ghClient := gogithub.NewClient(nil).WithAuthToken(getE2EToken(t))
1052+
t.Logf("Getting reviews for pull request in %s/%s...", currentOwner, repoName)
1053+
reviewRequests, _, err := ghClient.PullRequests.ListReviewers(context.Background(), currentOwner, repoName, 1, nil)
1054+
require.NoError(t, err, "expected to get review requests successfully")
1055+
1056+
// Check that there is one review request from copilot
1057+
require.Len(t, reviewRequests.Users, 1, "expected to find one review request")
1058+
require.Equal(t, "Copilot", *reviewRequests.Users[0].Login, "expected review request to be for Copilot")
1059+
require.Equal(t, "Bot", *reviewRequests.Users[0].Type, "expected review request to be for Bot")
1060+
}

pkg/github/pullrequests.go

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1730,8 +1730,40 @@ func newGQLStringlike[T ~string](s string) *T {
17301730
return &stringlike
17311731
}
17321732

1733+
type requestCopilotReviewArgs struct {
1734+
Owner string
1735+
Repo string
1736+
PullNumber int32
1737+
}
1738+
1739+
// TODO: This, and all the param parsing absolutely does not need the MCP request, it just needs the
1740+
// Argument map. Ideally we would just get the byte array and unmarshal it into the struct but mcp-go
1741+
// doesn't expose that.
1742+
func parseRequestCopilotReviewArgs(request mcp.CallToolRequest) (requestCopilotReviewArgs, error) {
1743+
owner, err := requiredParam[string](request, "owner")
1744+
if err != nil {
1745+
return requestCopilotReviewArgs{}, err
1746+
}
1747+
1748+
repo, err := requiredParam[string](request, "repo")
1749+
if err != nil {
1750+
return requestCopilotReviewArgs{}, err
1751+
}
1752+
1753+
pullNumber, err := requiredParam[constrainableInt32](request, "pullNumber")
1754+
if err != nil {
1755+
return requestCopilotReviewArgs{}, err
1756+
}
1757+
1758+
return requestCopilotReviewArgs{
1759+
Owner: owner,
1760+
Repo: repo,
1761+
PullNumber: int32(pullNumber),
1762+
}, nil
1763+
}
1764+
17331765
// RequestCopilotReview creates a tool to request a Copilot review for a pull request.
1734-
func RequestCopilotReview(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
1766+
func RequestCopilotReview(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) {
17351767
return mcp.NewTool("request_copilot_review",
17361768
mcp.WithDescription(t("TOOL_REQUEST_COPILOT_REVIEW_DESCRIPTION", "Request a GitHub Copilot review for a pull request. Note: This feature depends on GitHub API support and may not be available for all users.")),
17371769
mcp.WithString("owner",
@@ -1742,27 +1774,35 @@ func RequestCopilotReview(getClient GetClientFn, t translations.TranslationHelpe
17421774
mcp.Required(),
17431775
mcp.Description("Repository name"),
17441776
),
1745-
mcp.WithNumber("pull_number",
1777+
mcp.WithNumber("pullNumber",
17461778
mcp.Required(),
17471779
mcp.Description("Pull request number"),
17481780
),
17491781
),
17501782
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
1751-
owner, err := requiredParam[string](request, "owner")
1783+
args, err := parseRequestCopilotReviewArgs(request)
17521784
if err != nil {
1753-
return mcp.NewToolResultError(err.Error()), nil
1785+
return nil, err
17541786
}
1755-
repo, err := requiredParam[string](request, "repo")
1787+
1788+
client, err := getClient(ctx)
17561789
if err != nil {
17571790
return mcp.NewToolResultError(err.Error()), nil
17581791
}
1759-
pullNumber, err := RequiredInt(request, "pull_number")
1760-
if err != nil {
1792+
1793+
if _, _, err := client.PullRequests.RequestReviewers(
1794+
ctx,
1795+
args.Owner,
1796+
args.Repo,
1797+
int(args.PullNumber),
1798+
github.ReviewersRequest{
1799+
Reviewers: []string{"copilot-pull-request-reviewer[bot]"}, // The login name of the copilot bot.
1800+
},
1801+
); err != nil {
17611802
return mcp.NewToolResultError(err.Error()), nil
17621803
}
17631804

1764-
// As of now, GitHub API does not support Copilot as a reviewer programmatically.
1765-
// This is a placeholder for future support.
1766-
return mcp.NewToolResultError(fmt.Sprintf("Requesting a Copilot review for PR #%d in %s/%s is not currently supported by the GitHub API. Please request a Copilot review via the GitHub UI.", pullNumber, owner, repo)), nil
1805+
// Return nothing, just indicate success for the time being.
1806+
return mcp.NewToolResultText(""), nil
17671807
}
17681808
}

pkg/github/tools.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ func InitToolsets(passedToolsets []string, readOnly bool, getClient GetClientFn,
7777
toolsets.NewServerTool(AddPullRequestReviewCommentToPendingReview(getGQLClient, t)),
7878
toolsets.NewServerTool(SubmitPendingPullRequestReview(getGQLClient, t)),
7979
toolsets.NewServerTool(DeletePendingPullRequestReview(getGQLClient, t)),
80+
81+
toolsets.NewServerTool(RequestCopilotReview(getClient, t)),
8082
)
8183
codeSecurity := toolsets.NewToolset("code_security", "Code security related tools, such as GitHub Code Scanning").
8284
AddReadTools(

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy