Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit476fc57

Browse files
committed
Support requesting copilot as a reviewer
1 parent25200cc commit476fc57

File tree

6 files changed

+307
-23
lines changed

6 files changed

+307
-23
lines changed

‎README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -462,8 +462,8 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
462462

463463
-`owner`: Repository owner (string, required)
464464
-`repo`: Repository name (string, required)
465-
-`pull_number`: Pull request number (number, required)
466-
-_Note:As of now, requesting a Copilot review programmatically is not supported by the GitHub API. Thistool willreturn an error until GitHub exposes this functionality._
465+
-`pullNumber`: Pull request number (number, required)
466+
-_Note:Currently, thistool willonly work for github.com
467467

468468
###Repositories
469469

‎e2e/e2e_test.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -772,3 +772,148 @@ func TestDirectoryDeletion(t *testing.T) {
772772
require.Equal(t,"test-dir/test-file.txt",trimmedGetCommitText.Files[0].Filename,"expected filename to match")
773773
require.Equal(t,1,trimmedGetCommitText.Files[0].Deletions,"expected one deletion")
774774
}
775+
776+
funcTestRequestCopilotReview(t*testing.T) {
777+
t.Parallel()
778+
779+
mcpClient:=setupMCPClient(t)
780+
781+
ctx:=context.Background()
782+
783+
// First, who am I
784+
getMeRequest:= mcp.CallToolRequest{}
785+
getMeRequest.Params.Name="get_me"
786+
787+
t.Log("Getting current user...")
788+
resp,err:=mcpClient.CallTool(ctx,getMeRequest)
789+
require.NoError(t,err,"expected to call 'get_me' tool successfully")
790+
require.False(t,resp.IsError,fmt.Sprintf("expected result not to be an error: %+v",resp))
791+
792+
require.False(t,resp.IsError,"expected result not to be an error")
793+
require.Len(t,resp.Content,1,"expected content to have one item")
794+
795+
textContent,ok:=resp.Content[0].(mcp.TextContent)
796+
require.True(t,ok,"expected content to be of type TextContent")
797+
798+
vartrimmedGetMeTextstruct {
799+
Loginstring`json:"login"`
800+
}
801+
err=json.Unmarshal([]byte(textContent.Text),&trimmedGetMeText)
802+
require.NoError(t,err,"expected to unmarshal text content successfully")
803+
804+
currentOwner:=trimmedGetMeText.Login
805+
806+
// Then create a repository with a README (via autoInit)
807+
repoName:=fmt.Sprintf("github-mcp-server-e2e-%s-%d",t.Name(),time.Now().UnixMilli())
808+
createRepoRequest:= mcp.CallToolRequest{}
809+
createRepoRequest.Params.Name="create_repository"
810+
createRepoRequest.Params.Arguments=map[string]any{
811+
"name":repoName,
812+
"private":true,
813+
"autoInit":true,
814+
}
815+
816+
t.Logf("Creating repository %s/%s...",currentOwner,repoName)
817+
_,err=mcpClient.CallTool(ctx,createRepoRequest)
818+
require.NoError(t,err,"expected to call 'create_repository' tool successfully")
819+
require.False(t,resp.IsError,fmt.Sprintf("expected result not to be an error: %+v",resp))
820+
821+
// Cleanup the repository after the test
822+
t.Cleanup(func() {
823+
// MCP Server doesn't support deletions, but we can use the GitHub Client
824+
ghClient:=gogithub.NewClient(nil).WithAuthToken(getE2EToken(t))
825+
t.Logf("Deleting repository %s/%s...",currentOwner,repoName)
826+
_,err:=ghClient.Repositories.Delete(context.Background(),currentOwner,repoName)
827+
require.NoError(t,err,"expected to delete repository successfully")
828+
})
829+
830+
// Create a branch on which to create a new commit
831+
createBranchRequest:= mcp.CallToolRequest{}
832+
createBranchRequest.Params.Name="create_branch"
833+
createBranchRequest.Params.Arguments=map[string]any{
834+
"owner":currentOwner,
835+
"repo":repoName,
836+
"branch":"test-branch",
837+
"from_branch":"main",
838+
}
839+
840+
t.Logf("Creating branch in %s/%s...",currentOwner,repoName)
841+
resp,err=mcpClient.CallTool(ctx,createBranchRequest)
842+
require.NoError(t,err,"expected to call 'create_branch' tool successfully")
843+
require.False(t,resp.IsError,fmt.Sprintf("expected result not to be an error: %+v",resp))
844+
845+
// Create a commit with a new file
846+
commitRequest:= mcp.CallToolRequest{}
847+
commitRequest.Params.Name="create_or_update_file"
848+
commitRequest.Params.Arguments=map[string]any{
849+
"owner":currentOwner,
850+
"repo":repoName,
851+
"path":"test-file.txt",
852+
"content":fmt.Sprintf("Created by e2e test %s",t.Name()),
853+
"message":"Add test file",
854+
"branch":"test-branch",
855+
}
856+
857+
t.Logf("Creating commit with new file in %s/%s...",currentOwner,repoName)
858+
resp,err=mcpClient.CallTool(ctx,commitRequest)
859+
require.NoError(t,err,"expected to call 'create_or_update_file' tool successfully")
860+
require.False(t,resp.IsError,fmt.Sprintf("expected result not to be an error: %+v",resp))
861+
862+
textContent,ok=resp.Content[0].(mcp.TextContent)
863+
require.True(t,ok,"expected content to be of type TextContent")
864+
865+
vartrimmedCommitTextstruct {
866+
SHAstring`json:"sha"`
867+
}
868+
err=json.Unmarshal([]byte(textContent.Text),&trimmedCommitText)
869+
require.NoError(t,err,"expected to unmarshal text content successfully")
870+
commitId:=trimmedCommitText.SHA
871+
872+
// Create a pull request
873+
prRequest:= mcp.CallToolRequest{}
874+
prRequest.Params.Name="create_pull_request"
875+
prRequest.Params.Arguments=map[string]any{
876+
"owner":currentOwner,
877+
"repo":repoName,
878+
"title":"Test PR",
879+
"body":"This is a test PR",
880+
"head":"test-branch",
881+
"base":"main",
882+
"commitId":commitId,
883+
}
884+
885+
t.Logf("Creating pull request in %s/%s...",currentOwner,repoName)
886+
resp,err=mcpClient.CallTool(ctx,prRequest)
887+
require.NoError(t,err,"expected to call 'create_pull_request' tool successfully")
888+
require.False(t,resp.IsError,fmt.Sprintf("expected result not to be an error: %+v",resp))
889+
890+
// Request a copilot review
891+
requestCopilotReviewRequest:= mcp.CallToolRequest{}
892+
requestCopilotReviewRequest.Params.Name="request_copilot_review"
893+
requestCopilotReviewRequest.Params.Arguments=map[string]any{
894+
"owner":currentOwner,
895+
"repo":repoName,
896+
"pullNumber":1,
897+
}
898+
899+
t.Logf("Requesting Copilot review for pull request in %s/%s...",currentOwner,repoName)
900+
resp,err=mcpClient.CallTool(ctx,requestCopilotReviewRequest)
901+
require.NoError(t,err,"expected to call 'request_copilot_review' tool successfully")
902+
require.False(t,resp.IsError,fmt.Sprintf("expected result not to be an error: %+v",resp))
903+
904+
textContent,ok=resp.Content[0].(mcp.TextContent)
905+
require.True(t,ok,"expected content to be of type TextContent")
906+
require.Equal(t,"",textContent.Text,"expected content to be empty")
907+
908+
// Finally, get requested reviews and see copilot is in there
909+
// MCP Server doesn't support requesting reviews yet, but we can use the GitHub Client
910+
ghClient:=gogithub.NewClient(nil).WithAuthToken(getE2EToken(t))
911+
t.Logf("Getting reviews for pull request in %s/%s...",currentOwner,repoName)
912+
reviewRequests,_,err:=ghClient.PullRequests.ListReviewers(context.Background(),currentOwner,repoName,1,nil)
913+
require.NoError(t,err,"expected to get review requests successfully")
914+
915+
// Check that there is one review request from copilot
916+
require.Len(t,reviewRequests.Users,1,"expected to find one review request")
917+
require.Equal(t,"Copilot",*reviewRequests.Users[0].Login,"expected review request to be for Copilot")
918+
require.Equal(t,"Bot",*reviewRequests.Users[0].Type,"expected review request to be for Bot")
919+
}

‎pkg/github/helper_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,23 @@ import (
1010
"github.com/stretchr/testify/require"
1111
)
1212

13+
typeexpectationsstruct {
14+
pathstring
15+
queryParamsmap[string]string
16+
requestBodyany
17+
}
18+
19+
// expect is a helper function to create a partial mock that expects various
20+
// request behaviors, such as path, query parameters, and request body.
21+
funcexpect(t*testing.T,eexpectations)*partialMock {
22+
return&partialMock{
23+
t:t,
24+
expectedPath:e.path,
25+
expectedQueryParams:e.queryParams,
26+
expectedRequestBody:e.requestBody,
27+
}
28+
}
29+
1330
// expectPath is a helper function to create a partial mock that expects a
1431
// request with the given path, with the ability to chain a response handler.
1532
funcexpectPath(t*testing.T,expectedPathstring)*partialMock {

‎pkg/github/pullrequests.go

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1248,9 +1248,15 @@ func CreatePullRequest(getClient GetClientFn, t translations.TranslationHelperFu
12481248
}
12491249

12501250
// RequestCopilotReview creates a tool to request a Copilot review for a pull request.
1251-
funcRequestCopilotReview(getClientGetClientFn,t translations.TranslationHelperFunc) (tool mcp.Tool,handler server.ToolHandlerFunc) {
1251+
// Note that this tool will not work on GHES where this feature is unsupported. In future, we should not expose this
1252+
// tool if the configured host does not support it.
1253+
funcRequestCopilotReview(getClientGetClientFn,t translations.TranslationHelperFunc) (mcp.Tool, server.ToolHandlerFunc) {
12521254
returnmcp.NewTool("request_copilot_review",
12531255
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.")),
1256+
mcp.WithToolAnnotation(mcp.ToolAnnotation{
1257+
Title:t("TOOL_REQUEST_COPILOT_REVIEW_USER_TITLE","Request Copilot review"),
1258+
ReadOnlyHint:toBoolPtr(false),
1259+
}),
12541260
mcp.WithString("owner",
12551261
mcp.Required(),
12561262
mcp.Description("Repository owner"),
@@ -1259,7 +1265,7 @@ func RequestCopilotReview(getClient GetClientFn, t translations.TranslationHelpe
12591265
mcp.Required(),
12601266
mcp.Description("Repository name"),
12611267
),
1262-
mcp.WithNumber("pull_number",
1268+
mcp.WithNumber("pullNumber",
12631269
mcp.Required(),
12641270
mcp.Description("Pull request number"),
12651271
),
@@ -1269,17 +1275,46 @@ func RequestCopilotReview(getClient GetClientFn, t translations.TranslationHelpe
12691275
iferr!=nil {
12701276
returnmcp.NewToolResultError(err.Error()),nil
12711277
}
1278+
12721279
repo,err:=requiredParam[string](request,"repo")
12731280
iferr!=nil {
12741281
returnmcp.NewToolResultError(err.Error()),nil
12751282
}
1276-
pullNumber,err:=RequiredInt(request,"pull_number")
1283+
1284+
pullNumber,err:=RequiredInt(request,"pullNumber")
1285+
iferr!=nil {
1286+
returnmcp.NewToolResultError(err.Error()),nil
1287+
}
1288+
1289+
client,err:=getClient(ctx)
12771290
iferr!=nil {
12781291
returnmcp.NewToolResultError(err.Error()),nil
12791292
}
12801293

1281-
// As of now, GitHub API does not support Copilot as a reviewer programmatically.
1282-
// This is a placeholder for future support.
1283-
returnmcp.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
1294+
_,resp,err:=client.PullRequests.RequestReviewers(
1295+
ctx,
1296+
owner,
1297+
repo,
1298+
pullNumber,
1299+
github.ReviewersRequest{
1300+
// The login name of the copilot reviewer bot
1301+
Reviewers: []string{"copilot-pull-request-reviewer[bot]"},
1302+
},
1303+
)
1304+
iferr!=nil {
1305+
returnnil,fmt.Errorf("failed to request copilot review: %w",err)
1306+
}
1307+
deferfunc() {_=resp.Body.Close() }()
1308+
1309+
ifresp.StatusCode!=http.StatusCreated {
1310+
body,err:=io.ReadAll(resp.Body)
1311+
iferr!=nil {
1312+
returnnil,fmt.Errorf("failed to read response body: %w",err)
1313+
}
1314+
returnmcp.NewToolResultError(fmt.Sprintf("failed to request copilot review: %s",string(body))),nil
1315+
}
1316+
1317+
// Return nothing on success, as there's not much value in returning the Pull Request itself
1318+
returnmcp.NewToolResultText(""),nil
12841319
}
12851320
}

‎pkg/github/pullrequests_test.go

Lines changed: 100 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1918,25 +1918,110 @@ func Test_AddPullRequestReviewComment(t *testing.T) {
19181918
}
19191919

19201920
funcTest_RequestCopilotReview(t*testing.T) {
1921+
t.Parallel()
1922+
19211923
mockClient:=github.NewClient(nil)
1922-
tool,handler:=RequestCopilotReview(stubGetClientFn(mockClient),translations.NullTranslationHelper)
1924+
tool,_:=RequestCopilotReview(stubGetClientFn(mockClient),translations.NullTranslationHelper)
19231925

19241926
assert.Equal(t,"request_copilot_review",tool.Name)
19251927
assert.NotEmpty(t,tool.Description)
19261928
assert.Contains(t,tool.InputSchema.Properties,"owner")
19271929
assert.Contains(t,tool.InputSchema.Properties,"repo")
1928-
assert.Contains(t,tool.InputSchema.Properties,"pull_number")
1929-
assert.ElementsMatch(t,tool.InputSchema.Required, []string{"owner","repo","pull_number"})
1930-
1931-
request:=createMCPRequest(map[string]interface{}{
1932-
"owner":"owner",
1933-
"repo":"repo",
1934-
"pull_number":float64(42),
1935-
})
1936-
1937-
result,err:=handler(context.Background(),request)
1938-
assert.NoError(t,err)
1939-
assert.NotNil(t,result)
1940-
textContent:=getTextResult(t,result)
1941-
assert.Contains(t,textContent.Text,"not currently supported by the GitHub API")
1930+
assert.Contains(t,tool.InputSchema.Properties,"pullNumber")
1931+
assert.ElementsMatch(t,tool.InputSchema.Required, []string{"owner","repo","pullNumber"})
1932+
1933+
// Setup mock PR for success case
1934+
mockPR:=&github.PullRequest{
1935+
Number:github.Ptr(42),
1936+
Title:github.Ptr("Test PR"),
1937+
State:github.Ptr("open"),
1938+
HTMLURL:github.Ptr("https://github.com/owner/repo/pull/42"),
1939+
Head:&github.PullRequestBranch{
1940+
SHA:github.Ptr("abcd1234"),
1941+
Ref:github.Ptr("feature-branch"),
1942+
},
1943+
Base:&github.PullRequestBranch{
1944+
Ref:github.Ptr("main"),
1945+
},
1946+
Body:github.Ptr("This is a test PR"),
1947+
User:&github.User{
1948+
Login:github.Ptr("testuser"),
1949+
},
1950+
}
1951+
1952+
tests:= []struct {
1953+
namestring
1954+
mockedClient*http.Client
1955+
requestArgsmap[string]any
1956+
expectErrorbool
1957+
expectedErrMsgstring
1958+
}{
1959+
{
1960+
name:"successful request",
1961+
mockedClient:mock.NewMockedHTTPClient(
1962+
mock.WithRequestMatchHandler(
1963+
mock.PostReposPullsRequestedReviewersByOwnerByRepoByPullNumber,
1964+
expect(t,expectations{
1965+
path:"/repos/owner/repo/pulls/1/requested_reviewers",
1966+
requestBody:map[string]any{
1967+
"reviewers": []any{"copilot-pull-request-reviewer[bot]"},
1968+
},
1969+
}).andThen(
1970+
mockResponse(t,http.StatusCreated,mockPR),
1971+
),
1972+
),
1973+
),
1974+
requestArgs:map[string]any{
1975+
"owner":"owner",
1976+
"repo":"repo",
1977+
"pullNumber":float64(1),
1978+
},
1979+
expectError:false,
1980+
},
1981+
{
1982+
name:"request fails",
1983+
mockedClient:mock.NewMockedHTTPClient(
1984+
mock.WithRequestMatchHandler(
1985+
mock.PostReposPullsRequestedReviewersByOwnerByRepoByPullNumber,
1986+
http.HandlerFunc(func(w http.ResponseWriter,_*http.Request) {
1987+
w.WriteHeader(http.StatusNotFound)
1988+
_,_=w.Write([]byte(`{"message": "Not Found"}`))
1989+
}),
1990+
),
1991+
),
1992+
requestArgs:map[string]any{
1993+
"owner":"owner",
1994+
"repo":"repo",
1995+
"pullNumber":float64(999),
1996+
},
1997+
expectError:true,
1998+
expectedErrMsg:"failed to request copilot review",
1999+
},
2000+
}
2001+
2002+
for_,tc:=rangetests {
2003+
t.Run(tc.name,func(t*testing.T) {
2004+
t.Parallel()
2005+
2006+
client:=github.NewClient(tc.mockedClient)
2007+
_,handler:=RequestCopilotReview(stubGetClientFn(client),translations.NullTranslationHelper)
2008+
2009+
request:=createMCPRequest(tc.requestArgs)
2010+
2011+
result,err:=handler(context.Background(),request)
2012+
2013+
iftc.expectError {
2014+
require.Error(t,err)
2015+
assert.Contains(t,err.Error(),tc.expectedErrMsg)
2016+
return
2017+
}
2018+
2019+
require.NoError(t,err)
2020+
assert.NotNil(t,result)
2021+
assert.Len(t,result.Content,1)
2022+
2023+
textContent:=getTextResult(t,result)
2024+
require.Equal(t,"",textContent.Text)
2025+
})
2026+
}
19422027
}

‎pkg/github/tools.go

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

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp