Skip to content

Commit ee07006

Browse files
committed
WIP: copilot as reviewer
1 parent eb2766a commit ee07006

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
@@ -369,3 +369,148 @@ func TestTags(t *testing.T) {
369369
require.Equal(t, "v0.0.1", trimmedTag[0].Name, "expected tag name to match")
370370
require.Equal(t, *ref.Object.SHA, trimmedTag[0].Commit.SHA, "expected tag SHA to match")
371371
}
372+
373+
func TestRequestCopilotReview(t *testing.T) {
374+
t.Parallel()
375+
376+
mcpClient := setupMCPClient(t)
377+
378+
ctx := context.Background()
379+
380+
// First, who am I
381+
getMeRequest := mcp.CallToolRequest{}
382+
getMeRequest.Params.Name = "get_me"
383+
384+
t.Log("Getting current user...")
385+
resp, err := mcpClient.CallTool(ctx, getMeRequest)
386+
require.NoError(t, err, "expected to call 'get_me' tool successfully")
387+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
388+
389+
require.False(t, resp.IsError, "expected result not to be an error")
390+
require.Len(t, resp.Content, 1, "expected content to have one item")
391+
392+
textContent, ok := resp.Content[0].(mcp.TextContent)
393+
require.True(t, ok, "expected content to be of type TextContent")
394+
395+
var trimmedGetMeText struct {
396+
Login string `json:"login"`
397+
}
398+
err = json.Unmarshal([]byte(textContent.Text), &trimmedGetMeText)
399+
require.NoError(t, err, "expected to unmarshal text content successfully")
400+
401+
currentOwner := trimmedGetMeText.Login
402+
403+
// Then create a repository with a README (via autoInit)
404+
repoName := fmt.Sprintf("github-mcp-server-e2e-%s-%d", t.Name(), time.Now().UnixMilli())
405+
createRepoRequest := mcp.CallToolRequest{}
406+
createRepoRequest.Params.Name = "create_repository"
407+
createRepoRequest.Params.Arguments = map[string]any{
408+
"name": repoName,
409+
"private": true,
410+
"autoInit": true,
411+
}
412+
413+
t.Logf("Creating repository %s/%s...", currentOwner, repoName)
414+
_, err = mcpClient.CallTool(ctx, createRepoRequest)
415+
require.NoError(t, err, "expected to call 'get_me' tool successfully")
416+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
417+
418+
// Cleanup the repository after the test
419+
t.Cleanup(func() {
420+
// MCP Server doesn't support deletions, but we can use the GitHub Client
421+
ghClient := gogithub.NewClient(nil).WithAuthToken(getE2EToken(t))
422+
t.Logf("Deleting repository %s/%s...", currentOwner, repoName)
423+
_, err := ghClient.Repositories.Delete(context.Background(), currentOwner, repoName)
424+
require.NoError(t, err, "expected to delete repository successfully")
425+
})
426+
427+
// Create a branch on which to create a new commit
428+
createBranchRequest := mcp.CallToolRequest{}
429+
createBranchRequest.Params.Name = "create_branch"
430+
createBranchRequest.Params.Arguments = map[string]any{
431+
"owner": currentOwner,
432+
"repo": repoName,
433+
"branch": "test-branch",
434+
"from_branch": "main",
435+
}
436+
437+
t.Logf("Creating branch in %s/%s...", currentOwner, repoName)
438+
resp, err = mcpClient.CallTool(ctx, createBranchRequest)
439+
require.NoError(t, err, "expected to call 'create_branch' tool successfully")
440+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
441+
442+
// Create a commit with a new file
443+
commitRequest := mcp.CallToolRequest{}
444+
commitRequest.Params.Name = "create_or_update_file"
445+
commitRequest.Params.Arguments = map[string]any{
446+
"owner": currentOwner,
447+
"repo": repoName,
448+
"path": "test-file.txt",
449+
"content": fmt.Sprintf("Created by e2e test %s", t.Name()),
450+
"message": "Add test file",
451+
"branch": "test-branch",
452+
}
453+
454+
t.Logf("Creating commit with new file in %s/%s...", currentOwner, repoName)
455+
resp, err = mcpClient.CallTool(ctx, commitRequest)
456+
require.NoError(t, err, "expected to call 'create_or_update_file' tool successfully")
457+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
458+
459+
textContent, ok = resp.Content[0].(mcp.TextContent)
460+
require.True(t, ok, "expected content to be of type TextContent")
461+
462+
var trimmedCommitText struct {
463+
SHA string `json:"sha"`
464+
}
465+
err = json.Unmarshal([]byte(textContent.Text), &trimmedCommitText)
466+
require.NoError(t, err, "expected to unmarshal text content successfully")
467+
commitId := trimmedCommitText.SHA
468+
469+
// Create a pull request
470+
prRequest := mcp.CallToolRequest{}
471+
prRequest.Params.Name = "create_pull_request"
472+
prRequest.Params.Arguments = map[string]any{
473+
"owner": currentOwner,
474+
"repo": repoName,
475+
"title": "Test PR",
476+
"body": "This is a test PR",
477+
"head": "test-branch",
478+
"base": "main",
479+
"commitId": commitId,
480+
}
481+
482+
t.Logf("Creating pull request in %s/%s...", currentOwner, repoName)
483+
resp, err = mcpClient.CallTool(ctx, prRequest)
484+
require.NoError(t, err, "expected to call 'create_pull_request' tool successfully")
485+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
486+
487+
// Request a copilot review
488+
requestCopilotReviewRequest := mcp.CallToolRequest{}
489+
requestCopilotReviewRequest.Params.Name = "request_copilot_review"
490+
requestCopilotReviewRequest.Params.Arguments = map[string]any{
491+
"owner": currentOwner,
492+
"repo": repoName,
493+
"pullNumber": 1,
494+
}
495+
496+
t.Logf("Requesting Copilot review for pull request in %s/%s...", currentOwner, repoName)
497+
resp, err = mcpClient.CallTool(ctx, requestCopilotReviewRequest)
498+
require.NoError(t, err, "expected to call 'request_copilot_review' tool successfully")
499+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
500+
501+
textContent, ok = resp.Content[0].(mcp.TextContent)
502+
require.True(t, ok, "expected content to be of type TextContent")
503+
require.Equal(t, "", textContent.Text, "expected content to be empty")
504+
505+
// Finally, get requested reviews and see copilot is in there
506+
// MCP Server doesn't support requesting reviews yet, but we can use the GitHub Client
507+
ghClient := gogithub.NewClient(nil).WithAuthToken(getE2EToken(t))
508+
t.Logf("Getting reviews for pull request in %s/%s...", currentOwner, repoName)
509+
reviewRequests, _, err := ghClient.PullRequests.ListReviewers(context.Background(), currentOwner, repoName, 1, nil)
510+
require.NoError(t, err, "expected to get review requests successfully")
511+
512+
// Check that there is one review request from copilot
513+
require.Len(t, reviewRequests.Users, 1, "expected to find one review request")
514+
require.Equal(t, "Copilot", *reviewRequests.Users[0].Login, "expected review request to be for Copilot")
515+
require.Equal(t, "Bot", *reviewRequests.Users[0].Type, "expected review request to be for Bot")
516+
}

pkg/github/pullrequests.go

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1247,8 +1247,40 @@ func CreatePullRequest(getClient GetClientFn, t translations.TranslationHelperFu
12471247
}
12481248
}
12491249

1250+
type requestCopilotReviewArgs struct {
1251+
Owner string
1252+
Repo string
1253+
PullNumber int
1254+
}
1255+
1256+
// TODO: This, and all the param parsing absolutely does not need the MCP request, it just needs the
1257+
// Argument map. Ideally we would just get the byte array and unmarshal it into the struct but mcp-go
1258+
// doesn't expose that.
1259+
func parseRequestCopilotReviewArgs(request mcp.CallToolRequest) (requestCopilotReviewArgs, error) {
1260+
owner, err := requiredParam[string](request, "owner")
1261+
if err != nil {
1262+
return requestCopilotReviewArgs{}, err
1263+
}
1264+
1265+
repo, err := requiredParam[string](request, "repo")
1266+
if err != nil {
1267+
return requestCopilotReviewArgs{}, err
1268+
}
1269+
1270+
pullNumber, err := RequiredInt(request, "pullNumber")
1271+
if err != nil {
1272+
return requestCopilotReviewArgs{}, err
1273+
}
1274+
1275+
return requestCopilotReviewArgs{
1276+
Owner: owner,
1277+
Repo: repo,
1278+
PullNumber: pullNumber,
1279+
}, nil
1280+
}
1281+
12501282
// RequestCopilotReview creates a tool to request a Copilot review for a pull request.
1251-
func RequestCopilotReview(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mcp.Tool, handler server.ToolHandlerFunc) {
1283+
func RequestCopilotReview(getClient GetClientFn, t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) {
12521284
return mcp.NewTool("request_copilot_review",
12531285
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.")),
12541286
mcp.WithString("owner",
@@ -1259,27 +1291,35 @@ func RequestCopilotReview(getClient GetClientFn, t translations.TranslationHelpe
12591291
mcp.Required(),
12601292
mcp.Description("Repository name"),
12611293
),
1262-
mcp.WithNumber("pull_number",
1294+
mcp.WithNumber("pullNumber",
12631295
mcp.Required(),
12641296
mcp.Description("Pull request number"),
12651297
),
12661298
),
12671299
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
1268-
owner, err := requiredParam[string](request, "owner")
1300+
args, err := parseRequestCopilotReviewArgs(request)
12691301
if err != nil {
1270-
return mcp.NewToolResultError(err.Error()), nil
1302+
return nil, err
12711303
}
1272-
repo, err := requiredParam[string](request, "repo")
1304+
1305+
client, err := getClient(ctx)
12731306
if err != nil {
12741307
return mcp.NewToolResultError(err.Error()), nil
12751308
}
1276-
pullNumber, err := RequiredInt(request, "pull_number")
1277-
if err != nil {
1309+
1310+
if _, _, err := client.PullRequests.RequestReviewers(
1311+
ctx,
1312+
args.Owner,
1313+
args.Repo,
1314+
args.PullNumber,
1315+
github.ReviewersRequest{
1316+
Reviewers: []string{"copilot-pull-request-reviewer[bot]"}, // The login name of the copilot bot.
1317+
},
1318+
); err != nil {
12781319
return mcp.NewToolResultError(err.Error()), nil
12791320
}
12801321

1281-
// As of now, GitHub API does not support Copilot as a reviewer programmatically.
1282-
// This is a placeholder for future support.
1283-
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
1322+
// Return nothing, just indicate success for the time being.
1323+
return mcp.NewToolResultText(""), nil
12841324
}
12851325
}

pkg/github/tools.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ func InitToolsets(passedToolsets []string, readOnly bool, getClient GetClientFn,
6969
toolsets.NewServerTool(CreatePullRequest(getClient, t)),
7070
toolsets.NewServerTool(UpdatePullRequest(getClient, t)),
7171
toolsets.NewServerTool(AddPullRequestReviewComment(getClient, t)),
72+
73+
toolsets.NewServerTool(RequestCopilotReview(getClient, t)),
7274
)
7375
codeSecurity := toolsets.NewToolset("code_security", "Code security related tools, such as GitHub Code Scanning").
7476
AddReadTools(

0 commit comments

Comments
 (0)