diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000..8ff0ef282 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,76 @@ +## Objectif + +Fournir à un agent IA les informations essentielles pour être immédiatement productif dans ce dépôt : architecture, points d'entrée, workflows de build/test/exécution, conventions propres au projet et exemples concrets tirés du code. + +## Big picture (où regarder en premier) + +- Commandes / points d'entrée : `cmd/github-mcp-server/main.go` (binaire principal) et `cmd/mcpcurl/` (outil utilitaire). +- Serveur MCP : `internal/ghmcp/server.go` — assemble les clients REST/GraphQL, crée les toolsets et démarre le serveur stdio. +- Logic métier et toolsets : `pkg/github/` — définit les outils, helper patterns, pagination et generation d'instructions. +- Clients bas niveau : `pkg/raw/`, `pkg/translations/`, `pkg/log/` — utilitaires pour accès raw, i18n et logging. + +Lire ces fichiers dans l'ordre ci‑dessous pour comprendre le flux : `main.go` → `internal/ghmcp/server.go` → `pkg/github/*.go`. + +## Commandes essentielles (exemples) + +- Build local : + +```bash +cd cmd/github-mcp-server +go build -o github-mcp-server +``` + +- Démarrer en mode stdio (nécessite `GITHUB_PERSONAL_ACCESS_TOKEN`): + +```bash +GITHUB_PERSONAL_ACCESS_TOKEN= ./github-mcp-server stdio +``` + +- Docker (image publique) : + +```bash +docker run -i --rm -e GITHUB_PERSONAL_ACCESS_TOKEN= ghcr.io/github/github-mcp-server +``` + +- Tests E2E (nécessitent jeton et Docker) : + +```bash +GITHUB_MCP_SERVER_E2E_TOKEN= go test -v --tags e2e ./e2e +``` + +## Flags et variables d'environnement importantes + +- `GITHUB_PERSONAL_ACCESS_TOKEN` : token utilisé pour les appels GitHub (voir `main.go` / viper). +- Flags globaux exposés par le binaire (`main.go`): `--toolsets`, `--dynamic-toolsets`, `--read-only`, `--log-file`, `--enable-command-logging`, `--export-translations`, `--content-window-size`. +- `GITHUB_TOOLSETS` peut aussi être utilisé pour définir les toolsets via variable d'env. + +## Conventions et patterns spécifiques + +- Configuration : utilisation de `spf13/cobra` + `spf13/viper`. Les noms de flags utilisent `-` (normalisation dans `main.go`). +- Auth et clients : REST via `github.com/google/go-github`, GraphQL via `github.com/shurcooL/githubv4`. Voir `internal/ghmcp.NewMCPServer`. +- Tool definitions : utilisez `pkg/github` helpers (ex. `WithPagination`, `WithUnifiedPagination`, `OptionalParam`, `RequiredParam`) pour définir paramètres et pagination uniformes. +- Erreurs GitHub : le contexte transporte des erreurs spécifiques (voir `pkg/errors` et `isAcceptedError` dans `pkg/github`). +- Traductions : i18n est exposé via `pkg/translations` et peut être exporté avec `--export-translations`. + +## Intégrations externes et points de vigilance + +- Dépendances clés : `mark3labs/mcp-go` (MCP primitives), `google/go-github` (REST), `shurcooL/githubv4` (GraphQL). Modifier la façon dont clients sont construits peut impacter tous les toolsets. +- API host parsing : `internal/ghmcp/parseAPIHost` gère `github.com`, GHEC et GHES. Pour tests locaux attention aux ports et schemes. +- Dynamic toolsets : si activé (`--dynamic-toolsets`) le code filtre `all` et enregistre dynamiquement des outils (voir `pkg/github` flows dans `internal/ghmcp`). + +## Exemples concrets tirés du code + +- Récupérer le token et démarrer le serveur stdio (de `main.go`): le serveur réclame `GITHUB_PERSONAL_ACCESS_TOKEN` et construit `StdioServerConfig`. +- Pagination uniforme : `pkg/github.WithUnifiedPagination()` ajoute `page`, `perPage` et `after` — GraphQL convertira ces paramètres en curseurs. +- Helpers paramètres : `RequiredParam[T]`, `OptionalParam[T]` standardisent la validation des arguments d'outils. + +## Où poser des modifications sûres + +- Ajouter ou modifier un toolset : travailler dans `pkg/github/` et exécuter les tests unitaires. Les changements d'API publique nécessitent mise à jour des instructions auto-générées (`github.GenerateInstructions`). +- Changer la manière dont les clients sont construits : `internal/ghmcp.NewMCPServer` — impact global, testez manuellement avec `stdio` et via E2E. + +## Raccourci pour l'agent + +- Pour explorer le comportement d'un outil : lire le fichier `pkg/github/_*.go` correspondant, repérer les `mcp.With...` paramètres et tester l'appel via un client stdio (initialisation + CallTool JSON-RPC). + +Si tu veux, j'intègre des extraits d'exemples JSON-RPC d'initialisation / d'appel d'outils et j'ajoute une section « checklist de PR » pour agents (ex. build, lint, tests unitaires, e2e flag). Veux-tu que je l'ajoute ? diff --git a/.vscode/launch.json b/.vscode/launch.json index cea7fd917..3164bb5e6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,6 +4,23 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ + + /** + * No code selection was provided. + * Please paste the code (or selection) you want documented and include the language if relevant. + * I will generate a documentation comment for that selection only. + */ + + { + "name": "Attach to Chrome", + "port": 9222, + "request": "attach", + "type": "chrome", + "webRoot": "${workspaceFolder}" + }, + + + { "name": "Launch stdio server", "type": "go", diff --git a/README.md b/README.md index f95810c65..a0e5899b6 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,7 @@ Alternatively, to manually configure VS Code, choose the appropriate JSON block ### Install in other MCP hosts + - **[GitHub Copilot in other IDEs](/docs/installation-guides/install-other-copilot-ides.md)** - Installation for JetBrains, Visual Studio, Eclipse, and Xcode with GitHub Copilot - **[Claude Applications](/docs/installation-guides/install-claude.md)** - Installation guide for Claude Web, Claude Desktop and Claude Code CLI - **[Cursor](/docs/installation-guides/install-cursor.md)** - Installation guide for Cursor IDE diff --git a/cmd/mcpcurl/README.md b/cmd/mcpcurl/README.md deleted file mode 100644 index 717ea207f..000000000 --- a/cmd/mcpcurl/README.md +++ /dev/null @@ -1,150 +0,0 @@ -# mcpcurl - -A CLI tool that dynamically builds commands based on schemas retrieved from MCP servers that can -be executed against the configured MCP server. - -## Overview - -`mcpcurl` is a command-line interface that: - -1. Connects to an MCP server via stdio -2. Dynamically retrieves the available tools schema -3. Generates CLI commands corresponding to each tool -4. Handles parameter validation based on the schema -5. Executes commands and displays responses - -## Installation - -### Prerequisites -- Go 1.21 or later -- Access to the GitHub MCP Server from either Docker or local build - -### Build from Source -```bash -cd cmd/mcpcurl -go build -o mcpcurl -``` - -### Using Go Install -```bash -go install github.com/github/github-mcp-server/cmd/mcpcurl@latest -``` - -### Verify Installation -```bash -./mcpcurl --help -``` - -## Usage - -```console -mcpcurl --stdio-server-cmd="" [flags] -``` - -The `--stdio-server-cmd` flag is required for all commands and specifies the command to run the MCP server. - -### Available Commands - -- `tools`: Contains all dynamically generated tool commands from the schema -- `schema`: Fetches and displays the raw schema from the MCP server -- `help`: Shows help for any command - -### Examples - -List available tools in Github's MCP server: - -```console -% ./mcpcurl --stdio-server-cmd "docker run -i --rm -e GITHUB_PERSONAL_ACCESS_TOKEN mcp/github" tools --help -Contains all dynamically generated tool commands from the schema - -Usage: - mcpcurl tools [command] - -Available Commands: - add_issue_comment Add a comment to an existing issue - create_branch Create a new branch in a GitHub repository - create_issue Create a new issue in a GitHub repository - create_or_update_file Create or update a single file in a GitHub repository - create_pull_request Create a new pull request in a GitHub repository - create_repository Create a new GitHub repository in your account - fork_repository Fork a GitHub repository to your account or specified organization - get_file_contents Get the contents of a file or directory from a GitHub repository - get_issue Get details of a specific issue in a GitHub repository - get_issue_comments Get comments for a GitHub issue - list_commits Get list of commits of a branch in a GitHub repository - list_issues List issues in a GitHub repository with filtering options - push_files Push multiple files to a GitHub repository in a single commit - search_code Search for code across GitHub repositories - search_issues Search for issues and pull requests across GitHub repositories - search_repositories Search for GitHub repositories - search_users Search for users on GitHub - update_issue Update an existing issue in a GitHub repository - -Flags: - -h, --help help for tools - -Global Flags: - --pretty Pretty print MCP response (only for JSON responses) (default true) - --stdio-server-cmd string Shell command to invoke MCP server via stdio (required) - -Use "mcpcurl tools [command] --help" for more information about a command. -``` - -Get help for a specific tool: - -```console - % ./mcpcurl --stdio-server-cmd "docker run -i --rm -e GITHUB_PERSONAL_ACCESS_TOKEN mcp/github" tools get_issue --help -Get details of a specific issue in a GitHub repository - -Usage: - mcpcurl tools get_issue [flags] - -Flags: - -h, --help help for get_issue - --issue_number float - --owner string - --repo string - -Global Flags: - --pretty Pretty print MCP response (only for JSON responses) (default true) - --stdio-server-cmd string Shell command to invoke MCP server via stdio (required) - -``` - -Use one of the tools: - -```console - % ./mcpcurl --stdio-server-cmd "docker run -i --rm -e GITHUB_PERSONAL_ACCESS_TOKEN mcp/github" tools get_issue --owner golang --repo go --issue_number 1 -{ - "active_lock_reason": null, - "assignee": null, - "assignees": [], - "author_association": "CONTRIBUTOR", - "body": "by **rsc+personal@swtch.com**:\n\n\u003cpre\u003eWhat steps will reproduce the problem?\n1. Run build on Ubuntu 9.10, which uses gcc 4.4.1\n\nWhat is the expected output? What do you see instead?\n\nCgo fails with the following error:\n\n{{{\ngo/misc/cgo/stdio$ make\ncgo file.go\ncould not determine kind of name for C.CString\ncould not determine kind of name for C.puts\ncould not determine kind of name for C.fflushstdout\ncould not determine kind of name for C.free\nthrow: sys·mapaccess1: key not in map\n\npanic PC=0x2b01c2b96a08\nthrow+0x33 /media/scratch/workspace/go/src/pkg/runtime/runtime.c:71\n throw(0x4d2daf, 0x0)\nsys·mapaccess1+0x74 \n/media/scratch/workspace/go/src/pkg/runtime/hashmap.c:769\n sys·mapaccess1(0xc2b51930, 0x2b01)\nmain·*Prog·loadDebugInfo+0xa67 \n/media/scratch/workspace/go/src/cmd/cgo/gcc.go:164\n main·*Prog·loadDebugInfo(0xc2bc0000, 0x2b01)\nmain·main+0x352 \n/media/scratch/workspace/go/src/cmd/cgo/main.go:68\n main·main()\nmainstart+0xf \n/media/scratch/workspace/go/src/pkg/runtime/amd64/asm.s:55\n mainstart()\ngoexit /media/scratch/workspace/go/src/pkg/runtime/proc.c:133\n goexit()\nmake: *** [file.cgo1.go] Error 2\n}}}\n\nPlease use labels and text to provide additional information.\u003c/pre\u003e\n", - "closed_at": "2014-12-08T10:02:16Z", - "closed_by": null, - "comments": 12, - "comments_url": "https://api.github.com/repos/golang/go/issues/1/comments", - "created_at": "2009-10-22T06:07:26Z", - "events_url": "https://api.github.com/repos/golang/go/issues/1/events", - [...] -} -``` - -## Dynamic Commands - -All tools provided by the MCP server are automatically available as subcommands under the `tools` command. Each generated command has: - -- Appropriate flags matching the tool's input schema -- Validation for required parameters -- Type validation -- Enum validation (for string parameters with allowable values) -- Help text generated from the tool's description - -## How It Works - -1. `mcpcurl` makes a JSON-RPC request to the server using the `tools/list` method -2. The server responds with a schema describing all available tools -3. `mcpcurl` dynamically builds a command structure based on this schema -4. When a command is executed, arguments are converted to a JSON-RPC request -5. The request is sent to the server via stdin, and the response is printed to stdout diff --git a/cmd/mcpcurl/mcpcurl b/cmd/mcpcurl/mcpcurl new file mode 100755 index 000000000..5bb105f2f Binary files /dev/null and b/cmd/mcpcurl/mcpcurl differ diff --git a/e2e/README.md b/e2e/README.md deleted file mode 100644 index 62730431a..000000000 --- a/e2e/README.md +++ /dev/null @@ -1,96 +0,0 @@ -# End To End (e2e) Tests - -The purpose of the E2E tests is to have a simple (currently) test that gives maintainers some confidence in the black box behavior of our artifacts. It does this by: - * Building the `github-mcp-server` docker image - * Running the image - * Interacting with the server via stdio - * Issuing requests that interact with the live GitHub API - -## Running the Tests - -A service must be running that supports image building and container creation via the `docker` CLI. - -Since these tests require a token to interact with real resources on the GitHub API, it is gated behind the `e2e` build flag. - -``` -GITHUB_MCP_SERVER_E2E_TOKEN= go test -v --tags e2e ./e2e -``` - -The `GITHUB_MCP_SERVER_E2E_TOKEN` environment variable is mapped to `GITHUB_PERSONAL_ACCESS_TOKEN` internally, but separated to avoid accidental reuse of credentials. - -## Example - -The following diff adjusts the `get_me` tool to return `foobar` as the user login. - -```diff -diff --git a/pkg/github/context_tools.go b/pkg/github/context_tools.go -index 1c91d70..ac4ef2b 100644 ---- a/pkg/github/context_tools.go -+++ b/pkg/github/context_tools.go -@@ -39,6 +39,8 @@ func GetMe(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mc - return mcp.NewToolResultError(fmt.Sprintf("failed to get user: %s", string(body))), nil - } - -+ user.Login = sPtr("foobar") -+ - r, err := json.Marshal(user) - if err != nil { - return nil, fmt.Errorf("failed to marshal user: %w", err) -@@ -47,3 +49,7 @@ func GetMe(getClient GetClientFn, t translations.TranslationHelperFunc) (tool mc - return mcp.NewToolResultText(string(r)), nil - } - } -+ -+func sPtr(s string) *string { -+ return &s -+} -``` - -Running the tests: - -``` -➜ GITHUB_MCP_SERVER_E2E_TOKEN=$(gh auth token) go test -v --tags e2e ./e2e -=== RUN TestE2E - e2e_test.go:92: Building Docker image for e2e tests... - e2e_test.go:36: Starting Stdio MCP client... -=== RUN TestE2E/Initialize -=== RUN TestE2E/CallTool_get_me - e2e_test.go:85: - Error Trace: /Users/williammartin/workspace/github-mcp-server/e2e/e2e_test.go:85 - Error: Not equal: - expected: "foobar" - actual : "williammartin" - - Diff: - --- Expected - +++ Actual - @@ -1 +1 @@ - -foobar - +williammartin - Test: TestE2E/CallTool_get_me - Messages: expected login to match ---- FAIL: TestE2E (1.05s) - --- PASS: TestE2E/Initialize (0.09s) - --- FAIL: TestE2E/CallTool_get_me (0.46s) -FAIL -FAIL github.com/github/github-mcp-server/e2e 1.433s -FAIL -``` - -## Debugging the Tests - -It is possible to provide `GITHUB_MCP_SERVER_E2E_DEBUG=true` to run the e2e tests with an in-process version of the MCP server. This has slightly reduced coverage as it doesn't integrate with Docker, or make use of the cobra/viper configuration parsing. However, it allows for placing breakpoints in the MCP Server internals, supporting much better debugging flows than the fully black-box tests. - -One might argue that the lack of visibility into failures for the black box tests also indicates a product need, but this solves for the immediate pain point felt as a maintainer. - -## Limitations - -The current test suite is intentionally very limited in scope. This is because the maintenance costs on e2e tests tend to increase significantly over time. To read about some challenges with GitHub integration tests, see [go-github integration tests README](https://github.com/google/go-github/blob/5b75aa86dba5cf4af2923afa0938774f37fa0a67/test/README.md). We will expand this suite circumspectly! - -The tests are quite repetitive and verbose. This is intentional as we want to see them develop more before committing to abstractions. - -Currently, visibility into failures is not particularly good. We're hoping that we can pull apart the mcp-go client and have it hook into streams representing stdio without requiring an exec. This way we can get breakpoints in the debugger easily. - -### Global State Mutation Tests - -Some tools (such as those that mark all notifications as read) would change the global state for the tester, and are also not idempotent, so they offer little value for end to end tests and instead should rely on unit testing and manual verifications. diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go deleted file mode 100644 index 24cfc7096..000000000 --- a/e2e/e2e_test.go +++ /dev/null @@ -1,1626 +0,0 @@ -//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") -}