github-mcp-server / e2e /e2e_test.go
Gemini
Initial commit
fce10de
//go:build e2e
package e2e_test
import (
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"os/exec"
"slices"
"strings"
"sync"
"testing"
"time"
"github.com/github/github-mcp-server/internal/ghmcp"
"github.com/github/github-mcp-server/pkg/github"
"github.com/github/github-mcp-server/pkg/translations"
gogithub "github.com/google/go-github/v74/github"
mcpClient "github.com/mark3labs/mcp-go/client"
"github.com/mark3labs/mcp-go/mcp"
"github.com/stretchr/testify/require"
)
var (
// Shared variables and sync.Once instances to ensure one-time execution
getTokenOnce sync.Once
token string
getHostOnce sync.Once
host string
buildOnce sync.Once
buildError error
)
// getE2EToken ensures the environment variable is checked only once and returns the token
func getE2EToken(t *testing.T) string {
getTokenOnce.Do(func() {
token = os.Getenv("GITHUB_MCP_SERVER_E2E_TOKEN")
if token == "" {
t.Fatalf("GITHUB_MCP_SERVER_E2E_TOKEN environment variable is not set")
}
})
return token
}
// getE2EHost ensures the environment variable is checked only once and returns the host
func getE2EHost() string {
getHostOnce.Do(func() {
host = os.Getenv("GITHUB_MCP_SERVER_E2E_HOST")
})
return host
}
func getRESTClient(t *testing.T) *gogithub.Client {
// Get token and ensure Docker image is built
token := getE2EToken(t)
// Create a new GitHub client with the token
ghClient := gogithub.NewClient(nil).WithAuthToken(token)
if host := getE2EHost(); host != "" && host != "https://github.com" {
var err error
// Currently this works for GHEC because the API is exposed at the api subdomain and the path prefix
// but it would be preferable to extract the host parsing from the main server logic, and use it here.
ghClient, err = ghClient.WithEnterpriseURLs(host, host)
require.NoError(t, err, "expected to create GitHub client with host")
}
return ghClient
}
// ensureDockerImageBuilt makes sure the Docker image is built only once across all tests
func ensureDockerImageBuilt(t *testing.T) {
buildOnce.Do(func() {
t.Log("Building Docker image for e2e tests...")
cmd := exec.Command("docker", "build", "-t", "github/e2e-github-mcp-server", ".")
cmd.Dir = ".." // Run this in the context of the root, where the Dockerfile is located.
output, err := cmd.CombinedOutput()
buildError = err
if err != nil {
t.Logf("Docker build output: %s", string(output))
}
})
// Check if the build was successful
require.NoError(t, buildError, "expected to build Docker image successfully")
}
// clientOpts holds configuration options for the MCP client setup
type clientOpts struct {
// Toolsets to enable in the MCP server
enabledToolsets []string
}
// clientOption defines a function type for configuring ClientOpts
type clientOption func(*clientOpts)
// withToolsets returns an option that either sets the GITHUB_TOOLSETS envvar when executing in docker,
// or sets the toolsets in the MCP server when running in-process.
func withToolsets(toolsets []string) clientOption {
return func(opts *clientOpts) {
opts.enabledToolsets = toolsets
}
}
func setupMCPClient(t *testing.T, options ...clientOption) *mcpClient.Client {
// Get token and ensure Docker image is built
token := getE2EToken(t)
// Create and configure options
opts := &clientOpts{}
// Apply all options to configure the opts struct
for _, option := range options {
option(opts)
}
// By default, we run the tests including the Docker image, but with DEBUG
// enabled, we run the server in-process, allowing for easier debugging.
var client *mcpClient.Client
if os.Getenv("GITHUB_MCP_SERVER_E2E_DEBUG") == "" {
ensureDockerImageBuilt(t)
// Prepare Docker arguments
args := []string{
"docker",
"run",
"-i",
"--rm",
"-e",
"GITHUB_PERSONAL_ACCESS_TOKEN", // Personal access token is all required
}
host := getE2EHost()
if host != "" {
args = append(args, "-e", "GITHUB_HOST")
}
// Add toolsets environment variable to the Docker arguments
if len(opts.enabledToolsets) > 0 {
args = append(args, "-e", "GITHUB_TOOLSETS")
}
// Add the image name
args = append(args, "github/e2e-github-mcp-server")
// Construct the env vars for the MCP Client to execute docker with
dockerEnvVars := []string{
fmt.Sprintf("GITHUB_PERSONAL_ACCESS_TOKEN=%s", token),
fmt.Sprintf("GITHUB_TOOLSETS=%s", strings.Join(opts.enabledToolsets, ",")),
}
if host != "" {
dockerEnvVars = append(dockerEnvVars, fmt.Sprintf("GITHUB_HOST=%s", host))
}
// Create the client
t.Log("Starting Stdio MCP client...")
var err error
client, err = mcpClient.NewStdioMCPClient(args[0], dockerEnvVars, args[1:]...)
require.NoError(t, err, "expected to create client successfully")
} else {
// We need this because the fully compiled server has a default for the viper config, which is
// not in scope for using the MCP server directly. This probably indicates that we should refactor
// so that there is a shared setup mechanism, but let's wait till we feel more friction.
enabledToolsets := opts.enabledToolsets
if enabledToolsets == nil {
enabledToolsets = github.DefaultTools
}
ghServer, err := ghmcp.NewMCPServer(ghmcp.MCPServerConfig{
Token: token,
EnabledToolsets: enabledToolsets,
Host: getE2EHost(),
Translator: translations.NullTranslationHelper,
})
require.NoError(t, err, "expected to construct MCP server successfully")
t.Log("Starting In Process MCP client...")
client, err = mcpClient.NewInProcessClient(ghServer)
require.NoError(t, err, "expected to create in-process client successfully")
}
t.Cleanup(func() {
require.NoError(t, client.Close(), "expected to close client successfully")
})
// Initialize the client
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
request := mcp.InitializeRequest{}
request.Params.ProtocolVersion = "2025-03-26"
request.Params.ClientInfo = mcp.Implementation{
Name: "e2e-test-client",
Version: "0.0.1",
}
result, err := client.Initialize(ctx, request)
require.NoError(t, err, "failed to initialize client")
require.Equal(t, "github-mcp-server", result.ServerInfo.Name, "unexpected server name")
return client
}
func TestGetMe(t *testing.T) {
t.Parallel()
mcpClient := setupMCPClient(t)
ctx := context.Background()
// When we call the "get_me" tool
request := mcp.CallToolRequest{}
request.Params.Name = "get_me"
response, err := mcpClient.CallTool(ctx, request)
require.NoError(t, err, "expected to call 'get_me' tool successfully")
require.False(t, response.IsError, "expected result not to be an error")
require.Len(t, response.Content, 1, "expected content to have one item")
textContent, ok := response.Content[0].(mcp.TextContent)
require.True(t, ok, "expected content to be of type TextContent")
var trimmedContent struct {
Login string `json:"login"`
}
err = json.Unmarshal([]byte(textContent.Text), &trimmedContent)
require.NoError(t, err, "expected to unmarshal text content successfully")
// Then the login in the response should match the login obtained via the same
// token using the GitHub API.
ghClient := getRESTClient(t)
user, _, err := ghClient.Users.Get(context.Background(), "")
require.NoError(t, err, "expected to get user successfully")
require.Equal(t, trimmedContent.Login, *user.Login, "expected login to match")
}
func TestToolsets(t *testing.T) {
t.Parallel()
mcpClient := setupMCPClient(
t,
withToolsets([]string{"repos", "issues"}),
)
ctx := context.Background()
request := mcp.ListToolsRequest{}
response, err := mcpClient.ListTools(ctx, request)
require.NoError(t, err, "expected to list tools successfully")
// We could enumerate the tools here, but we'll need to expose that information
// declaratively in the MCP server, so for the moment let's just check the existence
// of an issue and repo tool, and the non-existence of a pull_request tool.
var toolsContains = func(expectedName string) bool {
return slices.ContainsFunc(response.Tools, func(tool mcp.Tool) bool {
return tool.Name == expectedName
})
}
require.True(t, toolsContains("get_issue"), "expected to find 'get_issue' tool")
require.True(t, toolsContains("list_branches"), "expected to find 'list_branches' tool")
require.False(t, toolsContains("get_pull_request"), "expected not to find 'get_pull_request' tool")
}
func TestTags(t *testing.T) {
t.Parallel()
mcpClient := setupMCPClient(t)
ctx := context.Background()
// First, who am I
getMeRequest := mcp.CallToolRequest{}
getMeRequest.Params.Name = "get_me"
t.Log("Getting current user...")
resp, err := mcpClient.CallTool(ctx, getMeRequest)
require.NoError(t, err, "expected to call 'get_me' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
require.False(t, resp.IsError, "expected result not to be an error")
require.Len(t, resp.Content, 1, "expected content to have one item")
textContent, ok := resp.Content[0].(mcp.TextContent)
require.True(t, ok, "expected content to be of type TextContent")
var trimmedGetMeText struct {
Login string `json:"login"`
}
err = json.Unmarshal([]byte(textContent.Text), &trimmedGetMeText)
require.NoError(t, err, "expected to unmarshal text content successfully")
currentOwner := trimmedGetMeText.Login
// Then create a repository with a README (via autoInit)
repoName := fmt.Sprintf("github-mcp-server-e2e-%s-%d", t.Name(), time.Now().UnixMilli())
createRepoRequest := mcp.CallToolRequest{}
createRepoRequest.Params.Name = "create_repository"
createRepoRequest.Params.Arguments = map[string]any{
"name": repoName,
"private": true,
"autoInit": true,
}
t.Logf("Creating repository %s/%s...", currentOwner, repoName)
_, err = mcpClient.CallTool(ctx, createRepoRequest)
require.NoError(t, err, "expected to call 'get_me' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
// Cleanup the repository after the test
t.Cleanup(func() {
// MCP Server doesn't support deletions, but we can use the GitHub Client
ghClient := getRESTClient(t)
t.Logf("Deleting repository %s/%s...", currentOwner, repoName)
_, err := ghClient.Repositories.Delete(context.Background(), currentOwner, repoName)
require.NoError(t, err, "expected to delete repository successfully")
})
// Then create a tag
// MCP Server doesn't support tag creation, but we can use the GitHub Client
ghClient := getRESTClient(t)
t.Logf("Creating tag %s/%s:%s...", currentOwner, repoName, "v0.0.1")
ref, _, err := ghClient.Git.GetRef(context.Background(), currentOwner, repoName, "refs/heads/main")
require.NoError(t, err, "expected to get ref successfully")
tagObj, _, err := ghClient.Git.CreateTag(context.Background(), currentOwner, repoName, &gogithub.Tag{
Tag: gogithub.Ptr("v0.0.1"),
Message: gogithub.Ptr("v0.0.1"),
Object: &gogithub.GitObject{
SHA: ref.Object.SHA,
Type: gogithub.Ptr("commit"),
},
})
require.NoError(t, err, "expected to create tag object successfully")
_, _, err = ghClient.Git.CreateRef(context.Background(), currentOwner, repoName, &gogithub.Reference{
Ref: gogithub.Ptr("refs/tags/v0.0.1"),
Object: &gogithub.GitObject{
SHA: tagObj.SHA,
},
})
require.NoError(t, err, "expected to create tag ref successfully")
// List the tags
listTagsRequest := mcp.CallToolRequest{}
listTagsRequest.Params.Name = "list_tags"
listTagsRequest.Params.Arguments = map[string]any{
"owner": currentOwner,
"repo": repoName,
}
t.Logf("Listing tags for %s/%s...", currentOwner, repoName)
resp, err = mcpClient.CallTool(ctx, listTagsRequest)
require.NoError(t, err, "expected to call 'list_tags' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
require.False(t, resp.IsError, "expected result not to be an error")
require.Len(t, resp.Content, 1, "expected content to have one item")
textContent, ok = resp.Content[0].(mcp.TextContent)
require.True(t, ok, "expected content to be of type TextContent")
var trimmedTags []struct {
Name string `json:"name"`
Commit struct {
SHA string `json:"sha"`
} `json:"commit"`
}
err = json.Unmarshal([]byte(textContent.Text), &trimmedTags)
require.NoError(t, err, "expected to unmarshal text content successfully")
require.Len(t, trimmedTags, 1, "expected to find one tag")
require.Equal(t, "v0.0.1", trimmedTags[0].Name, "expected tag name to match")
require.Equal(t, *ref.Object.SHA, trimmedTags[0].Commit.SHA, "expected tag SHA to match")
// And fetch an individual tag
getTagRequest := mcp.CallToolRequest{}
getTagRequest.Params.Name = "get_tag"
getTagRequest.Params.Arguments = map[string]any{
"owner": currentOwner,
"repo": repoName,
"tag": "v0.0.1",
}
t.Logf("Getting tag %s/%s:%s...", currentOwner, repoName, "v0.0.1")
resp, err = mcpClient.CallTool(ctx, getTagRequest)
require.NoError(t, err, "expected to call 'get_tag' tool successfully")
require.False(t, resp.IsError, "expected result not to be an error")
var trimmedTag []struct { // don't understand why this is an array
Name string `json:"name"`
Commit struct {
SHA string `json:"sha"`
} `json:"commit"`
}
err = json.Unmarshal([]byte(textContent.Text), &trimmedTag)
require.NoError(t, err, "expected to unmarshal text content successfully")
require.Len(t, trimmedTag, 1, "expected to find one tag")
require.Equal(t, "v0.0.1", trimmedTag[0].Name, "expected tag name to match")
require.Equal(t, *ref.Object.SHA, trimmedTag[0].Commit.SHA, "expected tag SHA to match")
}
func TestFileDeletion(t *testing.T) {
t.Parallel()
mcpClient := setupMCPClient(t)
ctx := context.Background()
// First, who am I
getMeRequest := mcp.CallToolRequest{}
getMeRequest.Params.Name = "get_me"
t.Log("Getting current user...")
resp, err := mcpClient.CallTool(ctx, getMeRequest)
require.NoError(t, err, "expected to call 'get_me' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
require.False(t, resp.IsError, "expected result not to be an error")
require.Len(t, resp.Content, 1, "expected content to have one item")
textContent, ok := resp.Content[0].(mcp.TextContent)
require.True(t, ok, "expected content to be of type TextContent")
var trimmedGetMeText struct {
Login string `json:"login"`
}
err = json.Unmarshal([]byte(textContent.Text), &trimmedGetMeText)
require.NoError(t, err, "expected to unmarshal text content successfully")
currentOwner := trimmedGetMeText.Login
// Then create a repository with a README (via autoInit)
repoName := fmt.Sprintf("github-mcp-server-e2e-%s-%d", t.Name(), time.Now().UnixMilli())
createRepoRequest := mcp.CallToolRequest{}
createRepoRequest.Params.Name = "create_repository"
createRepoRequest.Params.Arguments = map[string]any{
"name": repoName,
"private": true,
"autoInit": true,
}
t.Logf("Creating repository %s/%s...", currentOwner, repoName)
_, err = mcpClient.CallTool(ctx, createRepoRequest)
require.NoError(t, err, "expected to call 'get_me' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
// Cleanup the repository after the test
t.Cleanup(func() {
// MCP Server doesn't support deletions, but we can use the GitHub Client
ghClient := getRESTClient(t)
t.Logf("Deleting repository %s/%s...", currentOwner, repoName)
_, err := ghClient.Repositories.Delete(context.Background(), currentOwner, repoName)
require.NoError(t, err, "expected to delete repository successfully")
})
// Create a branch on which to create a new commit
createBranchRequest := mcp.CallToolRequest{}
createBranchRequest.Params.Name = "create_branch"
createBranchRequest.Params.Arguments = map[string]any{
"owner": currentOwner,
"repo": repoName,
"branch": "test-branch",
"from_branch": "main",
}
t.Logf("Creating branch in %s/%s...", currentOwner, repoName)
resp, err = mcpClient.CallTool(ctx, createBranchRequest)
require.NoError(t, err, "expected to call 'create_branch' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
// Create a commit with a new file
commitRequest := mcp.CallToolRequest{}
commitRequest.Params.Name = "create_or_update_file"
commitRequest.Params.Arguments = map[string]any{
"owner": currentOwner,
"repo": repoName,
"path": "test-file.txt",
"content": fmt.Sprintf("Created by e2e test %s", t.Name()),
"message": "Add test file",
"branch": "test-branch",
}
t.Logf("Creating commit with new file in %s/%s...", currentOwner, repoName)
resp, err = mcpClient.CallTool(ctx, commitRequest)
require.NoError(t, err, "expected to call 'create_or_update_file' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
// Check the file exists
getFileContentsRequest := mcp.CallToolRequest{}
getFileContentsRequest.Params.Name = "get_file_contents"
getFileContentsRequest.Params.Arguments = map[string]any{
"owner": currentOwner,
"repo": repoName,
"path": "test-file.txt",
"branch": "test-branch",
}
t.Logf("Getting file contents in %s/%s...", currentOwner, repoName)
resp, err = mcpClient.CallTool(ctx, getFileContentsRequest)
require.NoError(t, err, "expected to call 'get_file_contents' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
embeddedResource, ok := resp.Content[1].(mcp.EmbeddedResource)
require.True(t, ok, "expected content to be of type EmbeddedResource")
// raw api
textResource, ok := embeddedResource.Resource.(mcp.TextResourceContents)
require.True(t, ok, "expected embedded resource to be of type TextResourceContents")
require.Equal(t, fmt.Sprintf("Created by e2e test %s", t.Name()), textResource.Text, "expected file content to match")
// Delete the file
deleteFileRequest := mcp.CallToolRequest{}
deleteFileRequest.Params.Name = "delete_file"
deleteFileRequest.Params.Arguments = map[string]any{
"owner": currentOwner,
"repo": repoName,
"path": "test-file.txt",
"message": "Delete test file",
"branch": "test-branch",
}
t.Logf("Deleting file in %s/%s...", currentOwner, repoName)
resp, err = mcpClient.CallTool(ctx, deleteFileRequest)
require.NoError(t, err, "expected to call 'delete_file' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
// See that there is a commit that removes the file
listCommitsRequest := mcp.CallToolRequest{}
listCommitsRequest.Params.Name = "list_commits"
listCommitsRequest.Params.Arguments = map[string]any{
"owner": currentOwner,
"repo": repoName,
"sha": "test-branch", // can be SHA or branch, which is an unfortunate API design
}
t.Logf("Listing commits in %s/%s...", currentOwner, repoName)
resp, err = mcpClient.CallTool(ctx, listCommitsRequest)
require.NoError(t, err, "expected to call 'list_commits' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
textContent, ok = resp.Content[0].(mcp.TextContent)
require.True(t, ok, "expected content to be of type TextContent")
var trimmedListCommitsText []struct {
SHA string `json:"sha"`
Commit struct {
Message string `json:"message"`
}
Files []struct {
Filename string `json:"filename"`
Deletions int `json:"deletions"`
}
}
err = json.Unmarshal([]byte(textContent.Text), &trimmedListCommitsText)
require.NoError(t, err, "expected to unmarshal text content successfully")
require.GreaterOrEqual(t, len(trimmedListCommitsText), 1, "expected to find at least one commit")
deletionCommit := trimmedListCommitsText[0]
require.Equal(t, "Delete test file", deletionCommit.Commit.Message, "expected commit message to match")
// Now get the commit so we can look at the file changes because list_commits doesn't include them
getCommitRequest := mcp.CallToolRequest{}
getCommitRequest.Params.Name = "get_commit"
getCommitRequest.Params.Arguments = map[string]any{
"owner": currentOwner,
"repo": repoName,
"sha": deletionCommit.SHA,
}
t.Logf("Getting commit %s/%s:%s...", currentOwner, repoName, deletionCommit.SHA)
resp, err = mcpClient.CallTool(ctx, getCommitRequest)
require.NoError(t, err, "expected to call 'get_commit' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
textContent, ok = resp.Content[0].(mcp.TextContent)
require.True(t, ok, "expected content to be of type TextContent")
var trimmedGetCommitText struct {
Files []struct {
Filename string `json:"filename"`
Deletions int `json:"deletions"`
}
}
err = json.Unmarshal([]byte(textContent.Text), &trimmedGetCommitText)
require.NoError(t, err, "expected to unmarshal text content successfully")
require.Len(t, trimmedGetCommitText.Files, 1, "expected to find one file change")
require.Equal(t, "test-file.txt", trimmedGetCommitText.Files[0].Filename, "expected filename to match")
require.Equal(t, 1, trimmedGetCommitText.Files[0].Deletions, "expected one deletion")
}
func TestDirectoryDeletion(t *testing.T) {
t.Parallel()
mcpClient := setupMCPClient(t)
ctx := context.Background()
// First, who am I
getMeRequest := mcp.CallToolRequest{}
getMeRequest.Params.Name = "get_me"
t.Log("Getting current user...")
resp, err := mcpClient.CallTool(ctx, getMeRequest)
require.NoError(t, err, "expected to call 'get_me' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
require.False(t, resp.IsError, "expected result not to be an error")
require.Len(t, resp.Content, 1, "expected content to have one item")
textContent, ok := resp.Content[0].(mcp.TextContent)
require.True(t, ok, "expected content to be of type TextContent")
var trimmedGetMeText struct {
Login string `json:"login"`
}
err = json.Unmarshal([]byte(textContent.Text), &trimmedGetMeText)
require.NoError(t, err, "expected to unmarshal text content successfully")
currentOwner := trimmedGetMeText.Login
// Then create a repository with a README (via autoInit)
repoName := fmt.Sprintf("github-mcp-server-e2e-%s-%d", t.Name(), time.Now().UnixMilli())
createRepoRequest := mcp.CallToolRequest{}
createRepoRequest.Params.Name = "create_repository"
createRepoRequest.Params.Arguments = map[string]any{
"name": repoName,
"private": true,
"autoInit": true,
}
t.Logf("Creating repository %s/%s...", currentOwner, repoName)
_, err = mcpClient.CallTool(ctx, createRepoRequest)
require.NoError(t, err, "expected to call 'get_me' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
// Cleanup the repository after the test
t.Cleanup(func() {
// MCP Server doesn't support deletions, but we can use the GitHub Client
ghClient := getRESTClient(t)
t.Logf("Deleting repository %s/%s...", currentOwner, repoName)
_, err := ghClient.Repositories.Delete(context.Background(), currentOwner, repoName)
require.NoError(t, err, "expected to delete repository successfully")
})
// Create a branch on which to create a new commit
createBranchRequest := mcp.CallToolRequest{}
createBranchRequest.Params.Name = "create_branch"
createBranchRequest.Params.Arguments = map[string]any{
"owner": currentOwner,
"repo": repoName,
"branch": "test-branch",
"from_branch": "main",
}
t.Logf("Creating branch in %s/%s...", currentOwner, repoName)
resp, err = mcpClient.CallTool(ctx, createBranchRequest)
require.NoError(t, err, "expected to call 'create_branch' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
// Create a commit with a new file
commitRequest := mcp.CallToolRequest{}
commitRequest.Params.Name = "create_or_update_file"
commitRequest.Params.Arguments = map[string]any{
"owner": currentOwner,
"repo": repoName,
"path": "test-dir/test-file.txt",
"content": fmt.Sprintf("Created by e2e test %s", t.Name()),
"message": "Add test file",
"branch": "test-branch",
}
t.Logf("Creating commit with new file in %s/%s...", currentOwner, repoName)
resp, err = mcpClient.CallTool(ctx, commitRequest)
require.NoError(t, err, "expected to call 'create_or_update_file' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
textContent, ok = resp.Content[0].(mcp.TextContent)
require.True(t, ok, "expected content to be of type TextContent")
// Check the file exists
getFileContentsRequest := mcp.CallToolRequest{}
getFileContentsRequest.Params.Name = "get_file_contents"
getFileContentsRequest.Params.Arguments = map[string]any{
"owner": currentOwner,
"repo": repoName,
"path": "test-dir/test-file.txt",
"branch": "test-branch",
}
t.Logf("Getting file contents in %s/%s...", currentOwner, repoName)
resp, err = mcpClient.CallTool(ctx, getFileContentsRequest)
require.NoError(t, err, "expected to call 'get_file_contents' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
embeddedResource, ok := resp.Content[1].(mcp.EmbeddedResource)
require.True(t, ok, "expected content to be of type EmbeddedResource")
// raw api
textResource, ok := embeddedResource.Resource.(mcp.TextResourceContents)
require.True(t, ok, "expected embedded resource to be of type TextResourceContents")
require.Equal(t, fmt.Sprintf("Created by e2e test %s", t.Name()), textResource.Text, "expected file content to match")
// Delete the directory containing the file
deleteFileRequest := mcp.CallToolRequest{}
deleteFileRequest.Params.Name = "delete_file"
deleteFileRequest.Params.Arguments = map[string]any{
"owner": currentOwner,
"repo": repoName,
"path": "test-dir",
"message": "Delete test directory",
"branch": "test-branch",
}
t.Logf("Deleting directory in %s/%s...", currentOwner, repoName)
resp, err = mcpClient.CallTool(ctx, deleteFileRequest)
require.NoError(t, err, "expected to call 'delete_file' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
// See that there is a commit that removes the directory
listCommitsRequest := mcp.CallToolRequest{}
listCommitsRequest.Params.Name = "list_commits"
listCommitsRequest.Params.Arguments = map[string]any{
"owner": currentOwner,
"repo": repoName,
"sha": "test-branch", // can be SHA or branch, which is an unfortunate API design
}
t.Logf("Listing commits in %s/%s...", currentOwner, repoName)
resp, err = mcpClient.CallTool(ctx, listCommitsRequest)
require.NoError(t, err, "expected to call 'list_commits' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
textContent, ok = resp.Content[0].(mcp.TextContent)
require.True(t, ok, "expected content to be of type TextContent")
var trimmedListCommitsText []struct {
SHA string `json:"sha"`
Commit struct {
Message string `json:"message"`
}
Files []struct {
Filename string `json:"filename"`
Deletions int `json:"deletions"`
} `json:"files"`
}
err = json.Unmarshal([]byte(textContent.Text), &trimmedListCommitsText)
require.NoError(t, err, "expected to unmarshal text content successfully")
require.GreaterOrEqual(t, len(trimmedListCommitsText), 1, "expected to find at least one commit")
deletionCommit := trimmedListCommitsText[0]
require.Equal(t, "Delete test directory", deletionCommit.Commit.Message, "expected commit message to match")
// Now get the commit so we can look at the file changes because list_commits doesn't include them
getCommitRequest := mcp.CallToolRequest{}
getCommitRequest.Params.Name = "get_commit"
getCommitRequest.Params.Arguments = map[string]any{
"owner": currentOwner,
"repo": repoName,
"sha": deletionCommit.SHA,
}
t.Logf("Getting commit %s/%s:%s...", currentOwner, repoName, deletionCommit.SHA)
resp, err = mcpClient.CallTool(ctx, getCommitRequest)
require.NoError(t, err, "expected to call 'get_commit' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
textContent, ok = resp.Content[0].(mcp.TextContent)
require.True(t, ok, "expected content to be of type TextContent")
var trimmedGetCommitText struct {
Files []struct {
Filename string `json:"filename"`
Deletions int `json:"deletions"`
}
}
err = json.Unmarshal([]byte(textContent.Text), &trimmedGetCommitText)
require.NoError(t, err, "expected to unmarshal text content successfully")
require.Len(t, trimmedGetCommitText.Files, 1, "expected to find one file change")
require.Equal(t, "test-dir/test-file.txt", trimmedGetCommitText.Files[0].Filename, "expected filename to match")
require.Equal(t, 1, trimmedGetCommitText.Files[0].Deletions, "expected one deletion")
}
func TestRequestCopilotReview(t *testing.T) {
t.Parallel()
if getE2EHost() != "" && getE2EHost() != "https://github.com" {
t.Skip("Skipping test because the host does not support copilot reviews")
}
mcpClient := setupMCPClient(t)
ctx := context.Background()
// First, who am I
getMeRequest := mcp.CallToolRequest{}
getMeRequest.Params.Name = "get_me"
t.Log("Getting current user...")
resp, err := mcpClient.CallTool(ctx, getMeRequest)
require.NoError(t, err, "expected to call 'get_me' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
require.False(t, resp.IsError, "expected result not to be an error")
require.Len(t, resp.Content, 1, "expected content to have one item")
textContent, ok := resp.Content[0].(mcp.TextContent)
require.True(t, ok, "expected content to be of type TextContent")
var trimmedGetMeText struct {
Login string `json:"login"`
}
err = json.Unmarshal([]byte(textContent.Text), &trimmedGetMeText)
require.NoError(t, err, "expected to unmarshal text content successfully")
currentOwner := trimmedGetMeText.Login
// Then create a repository with a README (via autoInit)
repoName := fmt.Sprintf("github-mcp-server-e2e-%s-%d", t.Name(), time.Now().UnixMilli())
createRepoRequest := mcp.CallToolRequest{}
createRepoRequest.Params.Name = "create_repository"
createRepoRequest.Params.Arguments = map[string]any{
"name": repoName,
"private": true,
"autoInit": true,
}
t.Logf("Creating repository %s/%s...", currentOwner, repoName)
_, err = mcpClient.CallTool(ctx, createRepoRequest)
require.NoError(t, err, "expected to call 'create_repository' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
// Cleanup the repository after the test
t.Cleanup(func() {
// MCP Server doesn't support deletions, but we can use the GitHub Client
ghClient := gogithub.NewClient(nil).WithAuthToken(getE2EToken(t))
t.Logf("Deleting repository %s/%s...", currentOwner, repoName)
_, err := ghClient.Repositories.Delete(context.Background(), currentOwner, repoName)
require.NoError(t, err, "expected to delete repository successfully")
})
// Create a branch on which to create a new commit
createBranchRequest := mcp.CallToolRequest{}
createBranchRequest.Params.Name = "create_branch"
createBranchRequest.Params.Arguments = map[string]any{
"owner": currentOwner,
"repo": repoName,
"branch": "test-branch",
"from_branch": "main",
}
t.Logf("Creating branch in %s/%s...", currentOwner, repoName)
resp, err = mcpClient.CallTool(ctx, createBranchRequest)
require.NoError(t, err, "expected to call 'create_branch' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
// Create a commit with a new file
commitRequest := mcp.CallToolRequest{}
commitRequest.Params.Name = "create_or_update_file"
commitRequest.Params.Arguments = map[string]any{
"owner": currentOwner,
"repo": repoName,
"path": "test-file.txt",
"content": fmt.Sprintf("Created by e2e test %s", t.Name()),
"message": "Add test file",
"branch": "test-branch",
}
t.Logf("Creating commit with new file in %s/%s...", currentOwner, repoName)
resp, err = mcpClient.CallTool(ctx, commitRequest)
require.NoError(t, err, "expected to call 'create_or_update_file' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
textContent, ok = resp.Content[0].(mcp.TextContent)
require.True(t, ok, "expected content to be of type TextContent")
var trimmedCommitText struct {
SHA string `json:"sha"`
}
err = json.Unmarshal([]byte(textContent.Text), &trimmedCommitText)
require.NoError(t, err, "expected to unmarshal text content successfully")
commitId := trimmedCommitText.SHA
// Create a pull request
prRequest := mcp.CallToolRequest{}
prRequest.Params.Name = "create_pull_request"
prRequest.Params.Arguments = map[string]any{
"owner": currentOwner,
"repo": repoName,
"title": "Test PR",
"body": "This is a test PR",
"head": "test-branch",
"base": "main",
"commitId": commitId,
}
t.Logf("Creating pull request in %s/%s...", currentOwner, repoName)
resp, err = mcpClient.CallTool(ctx, prRequest)
require.NoError(t, err, "expected to call 'create_pull_request' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
// Request a copilot review
requestCopilotReviewRequest := mcp.CallToolRequest{}
requestCopilotReviewRequest.Params.Name = "request_copilot_review"
requestCopilotReviewRequest.Params.Arguments = map[string]any{
"owner": currentOwner,
"repo": repoName,
"pullNumber": 1,
}
t.Logf("Requesting Copilot review for pull request in %s/%s...", currentOwner, repoName)
resp, err = mcpClient.CallTool(ctx, requestCopilotReviewRequest)
require.NoError(t, err, "expected to call 'request_copilot_review' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
textContent, ok = resp.Content[0].(mcp.TextContent)
require.True(t, ok, "expected content to be of type TextContent")
require.Equal(t, "", textContent.Text, "expected content to be empty")
// Finally, get requested reviews and see copilot is in there
// MCP Server doesn't support requesting reviews yet, but we can use the GitHub Client
ghClient := gogithub.NewClient(nil).WithAuthToken(getE2EToken(t))
t.Logf("Getting reviews for pull request in %s/%s...", currentOwner, repoName)
reviewRequests, _, err := ghClient.PullRequests.ListReviewers(context.Background(), currentOwner, repoName, 1, nil)
require.NoError(t, err, "expected to get review requests successfully")
// Check that there is one review request from copilot
require.Len(t, reviewRequests.Users, 1, "expected to find one review request")
require.Equal(t, "Copilot", *reviewRequests.Users[0].Login, "expected review request to be for Copilot")
require.Equal(t, "Bot", *reviewRequests.Users[0].Type, "expected review request to be for Bot")
}
func TestAssignCopilotToIssue(t *testing.T) {
t.Parallel()
if getE2EHost() != "" && getE2EHost() != "https://github.com" {
t.Skip("Skipping test because the host does not support copilot being assigned to issues")
}
mcpClient := setupMCPClient(t)
ctx := context.Background()
// First, who am I
getMeRequest := mcp.CallToolRequest{}
getMeRequest.Params.Name = "get_me"
t.Log("Getting current user...")
resp, err := mcpClient.CallTool(ctx, getMeRequest)
require.NoError(t, err, "expected to call 'get_me' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
require.False(t, resp.IsError, "expected result not to be an error")
require.Len(t, resp.Content, 1, "expected content to have one item")
textContent, ok := resp.Content[0].(mcp.TextContent)
require.True(t, ok, "expected content to be of type TextContent")
var trimmedGetMeText struct {
Login string `json:"login"`
}
err = json.Unmarshal([]byte(textContent.Text), &trimmedGetMeText)
require.NoError(t, err, "expected to unmarshal text content successfully")
currentOwner := trimmedGetMeText.Login
// Then create a repository with a README (via autoInit)
repoName := fmt.Sprintf("github-mcp-server-e2e-%s-%d", t.Name(), time.Now().UnixMilli())
createRepoRequest := mcp.CallToolRequest{}
createRepoRequest.Params.Name = "create_repository"
createRepoRequest.Params.Arguments = map[string]any{
"name": repoName,
"private": true,
"autoInit": true,
}
t.Logf("Creating repository %s/%s...", currentOwner, repoName)
_, err = mcpClient.CallTool(ctx, createRepoRequest)
require.NoError(t, err, "expected to call 'create_repository' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
// Cleanup the repository after the test
t.Cleanup(func() {
// MCP Server doesn't support deletions, but we can use the GitHub Client
ghClient := getRESTClient(t)
t.Logf("Deleting repository %s/%s...", currentOwner, repoName)
_, err := ghClient.Repositories.Delete(context.Background(), currentOwner, repoName)
require.NoError(t, err, "expected to delete repository successfully")
})
// Create an issue
createIssueRequest := mcp.CallToolRequest{}
createIssueRequest.Params.Name = "create_issue"
createIssueRequest.Params.Arguments = map[string]any{
"owner": currentOwner,
"repo": repoName,
"title": "Test issue to assign copilot to",
}
t.Logf("Creating issue in %s/%s...", currentOwner, repoName)
resp, err = mcpClient.CallTool(ctx, createIssueRequest)
require.NoError(t, err, "expected to call 'create_issue' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
// Assign copilot to the issue
assignCopilotRequest := mcp.CallToolRequest{}
assignCopilotRequest.Params.Name = "assign_copilot_to_issue"
assignCopilotRequest.Params.Arguments = map[string]any{
"owner": currentOwner,
"repo": repoName,
"issueNumber": 1,
}
t.Logf("Assigning copilot to issue in %s/%s...", currentOwner, repoName)
resp, err = mcpClient.CallTool(ctx, assignCopilotRequest)
require.NoError(t, err, "expected to call 'assign_copilot_to_issue' tool successfully")
textContent, ok = resp.Content[0].(mcp.TextContent)
require.True(t, ok, "expected content to be of type TextContent")
possibleExpectedFailure := "copilot isn't available as an assignee for this issue. Please inform the user to visit https://docs.github.com/en/copilot/using-github-copilot/using-copilot-coding-agent-to-work-on-tasks/about-assigning-tasks-to-copilot for more information."
if resp.IsError && textContent.Text == possibleExpectedFailure {
t.Skip("skipping because copilot wasn't available as an assignee on this issue, it's likely that the owner doesn't have copilot enabled in their settings")
}
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
require.Equal(t, "successfully assigned copilot to issue", textContent.Text)
// Check that copilot is assigned to the issue
// MCP Server doesn't support getting assignees yet
ghClient := getRESTClient(t)
assignees, response, err := ghClient.Issues.Get(context.Background(), currentOwner, repoName, 1)
require.NoError(t, err, "expected to get issue successfully")
require.Equal(t, http.StatusOK, response.StatusCode, "expected to get issue successfully")
require.Len(t, assignees.Assignees, 1, "expected to find one assignee")
require.Equal(t, "Copilot", *assignees.Assignees[0].Login, "expected copilot to be assigned to the issue")
}
func TestPullRequestAtomicCreateAndSubmit(t *testing.T) {
t.Parallel()
mcpClient := setupMCPClient(t)
ctx := context.Background()
// First, who am I
getMeRequest := mcp.CallToolRequest{}
getMeRequest.Params.Name = "get_me"
t.Log("Getting current user...")
resp, err := mcpClient.CallTool(ctx, getMeRequest)
require.NoError(t, err, "expected to call 'get_me' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
require.False(t, resp.IsError, "expected result not to be an error")
require.Len(t, resp.Content, 1, "expected content to have one item")
textContent, ok := resp.Content[0].(mcp.TextContent)
require.True(t, ok, "expected content to be of type TextContent")
var trimmedGetMeText struct {
Login string `json:"login"`
}
err = json.Unmarshal([]byte(textContent.Text), &trimmedGetMeText)
require.NoError(t, err, "expected to unmarshal text content successfully")
currentOwner := trimmedGetMeText.Login
// Then create a repository with a README (via autoInit)
repoName := fmt.Sprintf("github-mcp-server-e2e-%s-%d", t.Name(), time.Now().UnixMilli())
createRepoRequest := mcp.CallToolRequest{}
createRepoRequest.Params.Name = "create_repository"
createRepoRequest.Params.Arguments = map[string]any{
"name": repoName,
"private": true,
"autoInit": true,
}
t.Logf("Creating repository %s/%s...", currentOwner, repoName)
_, err = mcpClient.CallTool(ctx, createRepoRequest)
require.NoError(t, err, "expected to call 'get_me' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
// Cleanup the repository after the test
t.Cleanup(func() {
// MCP Server doesn't support deletions, but we can use the GitHub Client
ghClient := getRESTClient(t)
t.Logf("Deleting repository %s/%s...", currentOwner, repoName)
_, err := ghClient.Repositories.Delete(context.Background(), currentOwner, repoName)
require.NoError(t, err, "expected to delete repository successfully")
})
// Create a branch on which to create a new commit
createBranchRequest := mcp.CallToolRequest{}
createBranchRequest.Params.Name = "create_branch"
createBranchRequest.Params.Arguments = map[string]any{
"owner": currentOwner,
"repo": repoName,
"branch": "test-branch",
"from_branch": "main",
}
t.Logf("Creating branch in %s/%s...", currentOwner, repoName)
resp, err = mcpClient.CallTool(ctx, createBranchRequest)
require.NoError(t, err, "expected to call 'create_branch' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
// Create a commit with a new file
commitRequest := mcp.CallToolRequest{}
commitRequest.Params.Name = "create_or_update_file"
commitRequest.Params.Arguments = map[string]any{
"owner": currentOwner,
"repo": repoName,
"path": "test-file.txt",
"content": fmt.Sprintf("Created by e2e test %s", t.Name()),
"message": "Add test file",
"branch": "test-branch",
}
t.Logf("Creating commit with new file in %s/%s...", currentOwner, repoName)
resp, err = mcpClient.CallTool(ctx, commitRequest)
require.NoError(t, err, "expected to call 'create_or_update_file' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
textContent, ok = resp.Content[0].(mcp.TextContent)
require.True(t, ok, "expected content to be of type TextContent")
var trimmedCommitText struct {
Commit struct {
SHA string `json:"sha"`
} `json:"commit"`
}
err = json.Unmarshal([]byte(textContent.Text), &trimmedCommitText)
require.NoError(t, err, "expected to unmarshal text content successfully")
commitID := trimmedCommitText.Commit.SHA
// Create a pull request
prRequest := mcp.CallToolRequest{}
prRequest.Params.Name = "create_pull_request"
prRequest.Params.Arguments = map[string]any{
"owner": currentOwner,
"repo": repoName,
"title": "Test PR",
"body": "This is a test PR",
"head": "test-branch",
"base": "main",
}
t.Logf("Creating pull request in %s/%s...", currentOwner, repoName)
resp, err = mcpClient.CallTool(ctx, prRequest)
require.NoError(t, err, "expected to call 'create_pull_request' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
// Create and submit a review
createAndSubmitReviewRequest := mcp.CallToolRequest{}
createAndSubmitReviewRequest.Params.Name = "create_and_submit_pull_request_review"
createAndSubmitReviewRequest.Params.Arguments = map[string]any{
"owner": currentOwner,
"repo": repoName,
"pullNumber": 1,
"event": "COMMENT", // the only event we can use as the creator of the PR
"body": "Looks good if you like bad code I guess!",
"commitID": commitID,
}
t.Logf("Creating and submitting review for pull request in %s/%s...", currentOwner, repoName)
resp, err = mcpClient.CallTool(ctx, createAndSubmitReviewRequest)
require.NoError(t, err, "expected to call 'create_and_submit_pull_request_review' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
// Finally, get the list of reviews and see that our review has been submitted
getPullRequestsReview := mcp.CallToolRequest{}
getPullRequestsReview.Params.Name = "get_pull_request_reviews"
getPullRequestsReview.Params.Arguments = map[string]any{
"owner": currentOwner,
"repo": repoName,
"pullNumber": 1,
}
t.Logf("Getting reviews for pull request in %s/%s...", currentOwner, repoName)
resp, err = mcpClient.CallTool(ctx, getPullRequestsReview)
require.NoError(t, err, "expected to call 'get_pull_request_reviews' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
textContent, ok = resp.Content[0].(mcp.TextContent)
require.True(t, ok, "expected content to be of type TextContent")
var reviews []struct {
State string `json:"state"`
}
err = json.Unmarshal([]byte(textContent.Text), &reviews)
require.NoError(t, err, "expected to unmarshal text content successfully")
// Check that there is one review
require.Len(t, reviews, 1, "expected to find one review")
require.Equal(t, "COMMENTED", reviews[0].State, "expected review state to be COMMENTED")
}
func TestPullRequestReviewCommentSubmit(t *testing.T) {
t.Parallel()
mcpClient := setupMCPClient(t)
ctx := context.Background()
// First, who am I
getMeRequest := mcp.CallToolRequest{}
getMeRequest.Params.Name = "get_me"
t.Log("Getting current user...")
resp, err := mcpClient.CallTool(ctx, getMeRequest)
require.NoError(t, err, "expected to call 'get_me' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
require.False(t, resp.IsError, "expected result not to be an error")
require.Len(t, resp.Content, 1, "expected content to have one item")
textContent, ok := resp.Content[0].(mcp.TextContent)
require.True(t, ok, "expected content to be of type TextContent")
var trimmedGetMeText struct {
Login string `json:"login"`
}
err = json.Unmarshal([]byte(textContent.Text), &trimmedGetMeText)
require.NoError(t, err, "expected to unmarshal text content successfully")
currentOwner := trimmedGetMeText.Login
// Then create a repository with a README (via autoInit)
repoName := fmt.Sprintf("github-mcp-server-e2e-%s-%d", t.Name(), time.Now().UnixMilli())
createRepoRequest := mcp.CallToolRequest{}
createRepoRequest.Params.Name = "create_repository"
createRepoRequest.Params.Arguments = map[string]any{
"name": repoName,
"private": true,
"autoInit": true,
}
t.Logf("Creating repository %s/%s...", currentOwner, repoName)
_, err = mcpClient.CallTool(ctx, createRepoRequest)
require.NoError(t, err, "expected to call 'create_repository' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
// Cleanup the repository after the test
t.Cleanup(func() {
// MCP Server doesn't support deletions, but we can use the GitHub Client
ghClient := getRESTClient(t)
t.Logf("Deleting repository %s/%s...", currentOwner, repoName)
_, err := ghClient.Repositories.Delete(context.Background(), currentOwner, repoName)
require.NoError(t, err, "expected to delete repository successfully")
})
// Create a branch on which to create a new commit
createBranchRequest := mcp.CallToolRequest{}
createBranchRequest.Params.Name = "create_branch"
createBranchRequest.Params.Arguments = map[string]any{
"owner": currentOwner,
"repo": repoName,
"branch": "test-branch",
"from_branch": "main",
}
t.Logf("Creating branch in %s/%s...", currentOwner, repoName)
resp, err = mcpClient.CallTool(ctx, createBranchRequest)
require.NoError(t, err, "expected to call 'create_branch' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
// Create a commit with a new file
commitRequest := mcp.CallToolRequest{}
commitRequest.Params.Name = "create_or_update_file"
commitRequest.Params.Arguments = map[string]any{
"owner": currentOwner,
"repo": repoName,
"path": "test-file.txt",
"content": fmt.Sprintf("Created by e2e test %s\nwith multiple lines", t.Name()),
"message": "Add test file",
"branch": "test-branch",
}
t.Logf("Creating commit with new file in %s/%s...", currentOwner, repoName)
resp, err = mcpClient.CallTool(ctx, commitRequest)
require.NoError(t, err, "expected to call 'create_or_update_file' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
textContent, ok = resp.Content[0].(mcp.TextContent)
require.True(t, ok, "expected content to be of type TextContent")
var trimmedCommitText struct {
Commit struct {
SHA string `json:"sha"`
} `json:"commit"`
}
err = json.Unmarshal([]byte(textContent.Text), &trimmedCommitText)
require.NoError(t, err, "expected to unmarshal text content successfully")
commitId := trimmedCommitText.Commit.SHA
// Create a pull request
prRequest := mcp.CallToolRequest{}
prRequest.Params.Name = "create_pull_request"
prRequest.Params.Arguments = map[string]any{
"owner": currentOwner,
"repo": repoName,
"title": "Test PR",
"body": "This is a test PR",
"head": "test-branch",
"base": "main",
}
t.Logf("Creating pull request in %s/%s...", currentOwner, repoName)
resp, err = mcpClient.CallTool(ctx, prRequest)
require.NoError(t, err, "expected to call 'create_pull_request' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
// Create a review for the pull request, but we can't approve it
// because the current owner also owns the PR.
createPendingPullRequestReviewRequest := mcp.CallToolRequest{}
createPendingPullRequestReviewRequest.Params.Name = "create_pending_pull_request_review"
createPendingPullRequestReviewRequest.Params.Arguments = map[string]any{
"owner": currentOwner,
"repo": repoName,
"pullNumber": 1,
}
t.Logf("Creating pending review for pull request in %s/%s...", currentOwner, repoName)
resp, err = mcpClient.CallTool(ctx, createPendingPullRequestReviewRequest)
require.NoError(t, err, "expected to call 'create_pending_pull_request_review' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
textContent, ok = resp.Content[0].(mcp.TextContent)
require.True(t, ok, "expected content to be of type TextContent")
require.Equal(t, "pending pull request created", textContent.Text)
// Add a file review comment
addFileReviewCommentRequest := mcp.CallToolRequest{}
addFileReviewCommentRequest.Params.Name = "add_comment_to_pending_review"
addFileReviewCommentRequest.Params.Arguments = map[string]any{
"owner": currentOwner,
"repo": repoName,
"pullNumber": 1,
"path": "test-file.txt",
"subjectType": "FILE",
"body": "File review comment",
}
t.Logf("Adding file review comment to pull request in %s/%s...", currentOwner, repoName)
resp, err = mcpClient.CallTool(ctx, addFileReviewCommentRequest)
require.NoError(t, err, "expected to call 'add_comment_to_pending_review' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
// Add a single line review comment
addSingleLineReviewCommentRequest := mcp.CallToolRequest{}
addSingleLineReviewCommentRequest.Params.Name = "add_comment_to_pending_review"
addSingleLineReviewCommentRequest.Params.Arguments = map[string]any{
"owner": currentOwner,
"repo": repoName,
"pullNumber": 1,
"path": "test-file.txt",
"subjectType": "LINE",
"body": "Single line review comment",
"line": 1,
"side": "RIGHT",
"commitId": commitId,
}
t.Logf("Adding single line review comment to pull request in %s/%s...", currentOwner, repoName)
resp, err = mcpClient.CallTool(ctx, addSingleLineReviewCommentRequest)
require.NoError(t, err, "expected to call 'add_comment_to_pending_review' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
// Add a multiline review comment
addMultilineReviewCommentRequest := mcp.CallToolRequest{}
addMultilineReviewCommentRequest.Params.Name = "add_comment_to_pending_review"
addMultilineReviewCommentRequest.Params.Arguments = map[string]any{
"owner": currentOwner,
"repo": repoName,
"pullNumber": 1,
"path": "test-file.txt",
"subjectType": "LINE",
"body": "Multiline review comment",
"startLine": 1,
"line": 2,
"startSide": "RIGHT",
"side": "RIGHT",
"commitId": commitId,
}
t.Logf("Adding multi line review comment to pull request in %s/%s...", currentOwner, repoName)
resp, err = mcpClient.CallTool(ctx, addMultilineReviewCommentRequest)
require.NoError(t, err, "expected to call 'add_comment_to_pending_review' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
// Submit the review
submitReviewRequest := mcp.CallToolRequest{}
submitReviewRequest.Params.Name = "submit_pending_pull_request_review"
submitReviewRequest.Params.Arguments = map[string]any{
"owner": currentOwner,
"repo": repoName,
"pullNumber": 1,
"event": "COMMENT", // the only event we can use as the creator of the PR
"body": "Looks good if you like bad code I guess!",
}
t.Logf("Submitting review for pull request in %s/%s...", currentOwner, repoName)
resp, err = mcpClient.CallTool(ctx, submitReviewRequest)
require.NoError(t, err, "expected to call 'submit_pending_pull_request_review' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
// Finally, get the review and see that it has been created
getPullRequestsReview := mcp.CallToolRequest{}
getPullRequestsReview.Params.Name = "get_pull_request_reviews"
getPullRequestsReview.Params.Arguments = map[string]any{
"owner": currentOwner,
"repo": repoName,
"pullNumber": 1,
}
t.Logf("Getting reviews for pull request in %s/%s...", currentOwner, repoName)
resp, err = mcpClient.CallTool(ctx, getPullRequestsReview)
require.NoError(t, err, "expected to call 'get_pull_request_reviews' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
textContent, ok = resp.Content[0].(mcp.TextContent)
require.True(t, ok, "expected content to be of type TextContent")
var reviews []struct {
ID int `json:"id"`
State string `json:"state"`
}
err = json.Unmarshal([]byte(textContent.Text), &reviews)
require.NoError(t, err, "expected to unmarshal text content successfully")
// Check that there is one review
require.Len(t, reviews, 1, "expected to find one review")
require.Equal(t, "COMMENTED", reviews[0].State, "expected review state to be COMMENTED")
// Check that there are three review comments
// MCP Server doesn't support this, but we can use the GitHub Client
ghClient := getRESTClient(t)
comments, _, err := ghClient.PullRequests.ListReviewComments(context.Background(), currentOwner, repoName, 1, int64(reviews[0].ID), nil)
require.NoError(t, err, "expected to list review comments successfully")
require.Equal(t, 3, len(comments), "expected to find three review comments")
}
func TestPullRequestReviewDeletion(t *testing.T) {
t.Parallel()
mcpClient := setupMCPClient(t)
ctx := context.Background()
// First, who am I
getMeRequest := mcp.CallToolRequest{}
getMeRequest.Params.Name = "get_me"
t.Log("Getting current user...")
resp, err := mcpClient.CallTool(ctx, getMeRequest)
require.NoError(t, err, "expected to call 'get_me' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
require.False(t, resp.IsError, "expected result not to be an error")
require.Len(t, resp.Content, 1, "expected content to have one item")
textContent, ok := resp.Content[0].(mcp.TextContent)
require.True(t, ok, "expected content to be of type TextContent")
var trimmedGetMeText struct {
Login string `json:"login"`
}
err = json.Unmarshal([]byte(textContent.Text), &trimmedGetMeText)
require.NoError(t, err, "expected to unmarshal text content successfully")
currentOwner := trimmedGetMeText.Login
// Then create a repository with a README (via autoInit)
repoName := fmt.Sprintf("github-mcp-server-e2e-%s-%d", t.Name(), time.Now().UnixMilli())
createRepoRequest := mcp.CallToolRequest{}
createRepoRequest.Params.Name = "create_repository"
createRepoRequest.Params.Arguments = map[string]any{
"name": repoName,
"private": true,
"autoInit": true,
}
t.Logf("Creating repository %s/%s...", currentOwner, repoName)
_, err = mcpClient.CallTool(ctx, createRepoRequest)
require.NoError(t, err, "expected to call 'get_me' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
// Cleanup the repository after the test
t.Cleanup(func() {
// MCP Server doesn't support deletions, but we can use the GitHub Client
ghClient := getRESTClient(t)
t.Logf("Deleting repository %s/%s...", currentOwner, repoName)
_, err := ghClient.Repositories.Delete(context.Background(), currentOwner, repoName)
require.NoError(t, err, "expected to delete repository successfully")
})
// Create a branch on which to create a new commit
createBranchRequest := mcp.CallToolRequest{}
createBranchRequest.Params.Name = "create_branch"
createBranchRequest.Params.Arguments = map[string]any{
"owner": currentOwner,
"repo": repoName,
"branch": "test-branch",
"from_branch": "main",
}
t.Logf("Creating branch in %s/%s...", currentOwner, repoName)
resp, err = mcpClient.CallTool(ctx, createBranchRequest)
require.NoError(t, err, "expected to call 'create_branch' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
// Create a commit with a new file
commitRequest := mcp.CallToolRequest{}
commitRequest.Params.Name = "create_or_update_file"
commitRequest.Params.Arguments = map[string]any{
"owner": currentOwner,
"repo": repoName,
"path": "test-file.txt",
"content": fmt.Sprintf("Created by e2e test %s", t.Name()),
"message": "Add test file",
"branch": "test-branch",
}
t.Logf("Creating commit with new file in %s/%s...", currentOwner, repoName)
resp, err = mcpClient.CallTool(ctx, commitRequest)
require.NoError(t, err, "expected to call 'create_or_update_file' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
// Create a pull request
prRequest := mcp.CallToolRequest{}
prRequest.Params.Name = "create_pull_request"
prRequest.Params.Arguments = map[string]any{
"owner": currentOwner,
"repo": repoName,
"title": "Test PR",
"body": "This is a test PR",
"head": "test-branch",
"base": "main",
}
t.Logf("Creating pull request in %s/%s...", currentOwner, repoName)
resp, err = mcpClient.CallTool(ctx, prRequest)
require.NoError(t, err, "expected to call 'create_pull_request' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
// Create a review for the pull request, but we can't approve it
// because the current owner also owns the PR.
createPendingPullRequestReviewRequest := mcp.CallToolRequest{}
createPendingPullRequestReviewRequest.Params.Name = "create_pending_pull_request_review"
createPendingPullRequestReviewRequest.Params.Arguments = map[string]any{
"owner": currentOwner,
"repo": repoName,
"pullNumber": 1,
}
t.Logf("Creating pending review for pull request in %s/%s...", currentOwner, repoName)
resp, err = mcpClient.CallTool(ctx, createPendingPullRequestReviewRequest)
require.NoError(t, err, "expected to call 'create_pending_pull_request_review' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
textContent, ok = resp.Content[0].(mcp.TextContent)
require.True(t, ok, "expected content to be of type TextContent")
require.Equal(t, "pending pull request created", textContent.Text)
// See that there is a pending review
getPullRequestsReview := mcp.CallToolRequest{}
getPullRequestsReview.Params.Name = "get_pull_request_reviews"
getPullRequestsReview.Params.Arguments = map[string]any{
"owner": currentOwner,
"repo": repoName,
"pullNumber": 1,
}
t.Logf("Getting reviews for pull request in %s/%s...", currentOwner, repoName)
resp, err = mcpClient.CallTool(ctx, getPullRequestsReview)
require.NoError(t, err, "expected to call 'get_pull_request_reviews' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
textContent, ok = resp.Content[0].(mcp.TextContent)
require.True(t, ok, "expected content to be of type TextContent")
var reviews []struct {
State string `json:"state"`
}
err = json.Unmarshal([]byte(textContent.Text), &reviews)
require.NoError(t, err, "expected to unmarshal text content successfully")
// Check that there is one review
require.Len(t, reviews, 1, "expected to find one review")
require.Equal(t, "PENDING", reviews[0].State, "expected review state to be PENDING")
// Delete the review
deleteReviewRequest := mcp.CallToolRequest{}
deleteReviewRequest.Params.Name = "delete_pending_pull_request_review"
deleteReviewRequest.Params.Arguments = map[string]any{
"owner": currentOwner,
"repo": repoName,
"pullNumber": 1,
}
t.Logf("Deleting review for pull request in %s/%s...", currentOwner, repoName)
resp, err = mcpClient.CallTool(ctx, deleteReviewRequest)
require.NoError(t, err, "expected to call 'delete_pending_pull_request_review' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
// See that there are no reviews
t.Logf("Getting reviews for pull request in %s/%s...", currentOwner, repoName)
resp, err = mcpClient.CallTool(ctx, getPullRequestsReview)
require.NoError(t, err, "expected to call 'get_pull_request_reviews' tool successfully")
require.False(t, resp.IsError, fmt.Sprintf("expected result not to be an error: %+v", resp))
textContent, ok = resp.Content[0].(mcp.TextContent)
require.True(t, ok, "expected content to be of type TextContent")
var noReviews []struct{}
err = json.Unmarshal([]byte(textContent.Text), &noReviews)
require.NoError(t, err, "expected to unmarshal text content successfully")
require.Len(t, noReviews, 0, "expected to find no reviews")
}