Spaces:
Build error
Build error
| //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") | |
| } | |