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)