Skip to content

Commit 8ef3228

Browse files
committed
Split PR review creation, commenting and submission
1 parent 0ca07aa commit 8ef3228

File tree

7 files changed

+684
-1
lines changed

7 files changed

+684
-1
lines changed

e2e/e2e_test.go

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,3 +369,196 @@ 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 TestPullRequestReview(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+
// Create a review for the pull request, but we can't approve it
488+
// because the current owner also owns the PR.
489+
createPendingPullRequestReviewRequest := mcp.CallToolRequest{}
490+
createPendingPullRequestReviewRequest.Params.Name = "mvp_create_pending_pull_request_review"
491+
createPendingPullRequestReviewRequest.Params.Arguments = map[string]any{
492+
"owner": currentOwner,
493+
"repo": repoName,
494+
"pullNumber": 1,
495+
}
496+
497+
t.Logf("Creating pending review for pull request in %s/%s...", currentOwner, repoName)
498+
resp, err = mcpClient.CallTool(ctx, createPendingPullRequestReviewRequest)
499+
require.NoError(t, err, "expected to call 'mvp_create_pending_pull_request_review' tool successfully")
500+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
501+
502+
textContent, ok = resp.Content[0].(mcp.TextContent)
503+
require.True(t, ok, "expected content to be of type TextContent")
504+
require.Equal(t, "", textContent.Text, "expected content to be empty")
505+
506+
// Add a review comment
507+
addReviewCommentRequest := mcp.CallToolRequest{}
508+
addReviewCommentRequest.Params.Name = "mvp_add_pull_request_review_comment_to_pending_review"
509+
addReviewCommentRequest.Params.Arguments = map[string]any{
510+
"owner": currentOwner,
511+
"repo": repoName,
512+
"pullNumber": 1,
513+
"path": "test-file.txt",
514+
"body": "Very nice!",
515+
"line": 1,
516+
}
517+
518+
t.Logf("Adding review comment to pull request in %s/%s...", currentOwner, repoName)
519+
resp, err = mcpClient.CallTool(ctx, addReviewCommentRequest)
520+
require.NoError(t, err, "expected to call 'mvp_add_pull_request_review_comment_to_pending_review' tool successfully")
521+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
522+
523+
// Submit the review
524+
submitReviewRequest := mcp.CallToolRequest{}
525+
submitReviewRequest.Params.Name = "mvp_submit_pull_request_review"
526+
submitReviewRequest.Params.Arguments = map[string]any{
527+
"owner": currentOwner,
528+
"repo": repoName,
529+
"pullNumber": 1,
530+
"event": "COMMENT", // the only event we can use as the creator of the PR
531+
"body": "Looks good if you like bad code I guess!",
532+
}
533+
534+
t.Logf("Submitting review for pull request in %s/%s...", currentOwner, repoName)
535+
resp, err = mcpClient.CallTool(ctx, submitReviewRequest)
536+
require.NoError(t, err, "expected to call 'mvp_submit_pull_request_review' tool successfully")
537+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
538+
539+
// Finally, get the review and see that it has been created
540+
getPullRequestsReview := mcp.CallToolRequest{}
541+
getPullRequestsReview.Params.Name = "get_pull_request_reviews"
542+
getPullRequestsReview.Params.Arguments = map[string]any{
543+
"owner": currentOwner,
544+
"repo": repoName,
545+
"pullNumber": 1,
546+
}
547+
548+
t.Logf("Getting reviews for pull request in %s/%s...", currentOwner, repoName)
549+
resp, err = mcpClient.CallTool(ctx, getPullRequestsReview)
550+
require.NoError(t, err, "expected to call 'get_pull_request_reviews' tool successfully")
551+
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
552+
553+
textContent, ok = resp.Content[0].(mcp.TextContent)
554+
require.True(t, ok, "expected content to be of type TextContent")
555+
556+
var reviews []struct {
557+
NodeID string `json:"node_id"`
558+
}
559+
err = json.Unmarshal([]byte(textContent.Text), &reviews)
560+
require.NoError(t, err, "expected to unmarshal text content successfully")
561+
562+
// Check that there is one review
563+
require.Len(t, reviews, 1, "expected to find one review")
564+
}

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,16 @@ require (
2525
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
2626
github.com/rogpeppe/go-internal v1.13.1 // indirect
2727
github.com/sagikazarmark/locafero v0.9.0 // indirect
28+
github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7
29+
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 // indirect
2830
github.com/sourcegraph/conc v0.3.0 // indirect
2931
github.com/spf13/afero v1.14.0 // indirect
3032
github.com/spf13/cast v1.7.1 // indirect
3133
github.com/spf13/pflag v1.0.6 // indirect
3234
github.com/subosito/gotenv v1.6.0 // indirect
3335
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
3436
go.uber.org/multierr v1.11.0 // indirect
37+
golang.org/x/oauth2 v0.29.0
3538
golang.org/x/sys v0.31.0 // indirect
3639
golang.org/x/text v0.23.0 // indirect
3740
golang.org/x/time v0.5.0 // indirect

go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWN
4545
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
4646
github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
4747
github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
48+
github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7 h1:cYCy18SHPKRkvclm+pWm1Lk4YrREb4IOIb/YdFO0p2M=
49+
github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7/go.mod h1:zqMwyHmnN/eDOZOdiTohqIUKUrTFX62PNlu7IJdu0q8=
50+
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 h1:17JxqqJY66GmZVHkmAsGEkcIu0oCe3AM420QDgGwZx0=
51+
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE=
4852
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
4953
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
5054
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
@@ -69,6 +73,8 @@ github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zI
6973
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
7074
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
7175
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
76+
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
77+
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
7278
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
7379
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
7480
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=

internal/ghmcp/server.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import (
1414
"github.com/github/github-mcp-server/pkg/translations"
1515
gogithub "github.com/google/go-github/v69/github"
1616
"github.com/mark3labs/mcp-go/mcp"
17+
"github.com/shurcooL/githubv4"
18+
"golang.org/x/oauth2"
1719

1820
"github.com/mark3labs/mcp-go/server"
1921
"github.com/sirupsen/logrus"
@@ -87,11 +89,21 @@ func NewMCPServer(cfg MCPServerConfig) (*server.MCPServer, error) {
8789
return ghClient, nil // closing over client
8890
}
8991

92+
getGQLClient := func(_ context.Context) (*githubv4.Client, error) {
93+
// TODO: Enterprise support
94+
src := oauth2.StaticTokenSource(
95+
&oauth2.Token{AccessToken: cfg.Token},
96+
)
97+
httpClient := oauth2.NewClient(context.Background(), src)
98+
return githubv4.NewClient(httpClient), nil
99+
}
100+
90101
// Create default toolsets
91102
toolsets, err := github.InitToolsets(
92103
enabledToolsets,
93104
cfg.ReadOnly,
94105
getClient,
106+
getGQLClient,
95107
cfg.Translator,
96108
)
97109
if err != nil {

0 commit comments

Comments
 (0)