diff --git a/cmd/github-mcp-server/main.go b/cmd/github-mcp-server/main.go index 5ca0e21cd..cf459f47f 100644 --- a/cmd/github-mcp-server/main.go +++ b/cmd/github-mcp-server/main.go @@ -45,7 +45,15 @@ var ( stdlog.Fatal("Failed to initialize logger:", err) } - enabledToolsets := viper.GetStringSlice("toolsets") + // If you're wondering why we're not using viper.GetStringSlice("toolsets"), + // it's because viper doesn't handle comma-separated values correctly for env + // vars when using GetStringSlice. + // https://github.com/spf13/viper/issues/380 + var enabledToolsets []string + err = viper.UnmarshalKey("toolsets", &enabledToolsets) + if err != nil { + stdlog.Fatal("Failed to unmarshal toolsets:", err) + } logCommands := viper.GetBool("enable-command-logging") cfg := runConfig{ diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go index 3d8c45dc9..757dd5c2a 100644 --- a/e2e/e2e_test.go +++ b/e2e/e2e_test.go @@ -5,8 +5,11 @@ package e2e_test import ( "context" "encoding/json" + "fmt" "os" "os/exec" + "slices" + "sync" "testing" "time" @@ -16,85 +19,190 @@ import ( "github.com/stretchr/testify/require" ) -func TestE2E(t *testing.T) { - e2eServerToken := os.Getenv("GITHUB_MCP_SERVER_E2E_TOKEN") - if e2eServerToken == "" { - t.Fatalf("GITHUB_MCP_SERVER_E2E_TOKEN environment variable is not set") +var ( + // Shared variables and sync.Once instances to ensure one-time execution + getTokenOnce sync.Once + token 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 +} + +// 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 { + // Environment variables to set before starting the client + EnvVars map[string]string +} + +// ClientOption defines a function type for configuring ClientOpts +type ClientOption func(*ClientOpts) + +// WithEnvVars returns an option that adds environment variables to the client options +func WithEnvVars(envVars map[string]string) ClientOption { + return func(opts *ClientOpts) { + opts.EnvVars = envVars + } +} + +// setupMCPClient sets up the test environment and returns an initialized MCP client +// It handles token retrieval, Docker image building, and applying the provided options +func setupMCPClient(t *testing.T, options ...ClientOption) *mcpClient.Client { + // Get token and ensure Docker image is built + token := getE2EToken(t) + ensureDockerImageBuilt(t) + + // Create and configure options + opts := &ClientOpts{ + EnvVars: make(map[string]string), } - // Build the Docker image for the MCP server. - buildDockerImage(t) + // Apply all options to configure the opts struct + for _, option := range options { + option(opts) + } - t.Setenv("GITHUB_PERSONAL_ACCESS_TOKEN", e2eServerToken) // The MCP Client merges the existing environment. + // Prepare Docker arguments args := []string{ "docker", "run", "-i", "--rm", "-e", - "GITHUB_PERSONAL_ACCESS_TOKEN", - "github/e2e-github-mcp-server", + "GITHUB_PERSONAL_ACCESS_TOKEN", // Personal access token is all required + } + + // Add all environment variables to the Docker arguments + for key := range opts.EnvVars { + args = append(args, "-e", key) + } + + // 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 := make([]string, 0, len(opts.EnvVars)+1) + dockerEnvVars = append(dockerEnvVars, fmt.Sprintf("GITHUB_PERSONAL_ACCESS_TOKEN=%s", token)) + for key, value := range opts.EnvVars { + dockerEnvVars = append(dockerEnvVars, fmt.Sprintf("%s=%s", key, value)) } + + // Create the client t.Log("Starting Stdio MCP client...") - client, err := mcpClient.NewStdioMCPClient(args[0], []string{}, args[1:]...) + client, err := mcpClient.NewStdioMCPClient(args[0], dockerEnvVars, args[1:]...) require.NoError(t, err, "expected to create client successfully") + t.Cleanup(func() { + require.NoError(t, client.Close(), "expected to close client successfully") + }) - t.Run("Initialize", func(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() + // 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", - } + 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, "expected to initialize successfully") + 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") - require.Equal(t, "github-mcp-server", result.ServerInfo.Name) - }) + return client +} - t.Run("CallTool get_me", func(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() +func TestGetMe(t *testing.T) { + t.Parallel() - // When we call the "get_me" tool - request := mcp.CallToolRequest{} - request.Params.Name = "get_me" + mcpClient := setupMCPClient(t) - response, err := client.CallTool(ctx, request) - require.NoError(t, err, "expected to call 'get_me' tool successfully") + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() - require.False(t, response.IsError, "expected result not to be an error") - require.Len(t, response.Content, 1, "expected content to have one item") + // When we call the "get_me" tool + request := mcp.CallToolRequest{} + request.Params.Name = "get_me" - textContent, ok := response.Content[0].(mcp.TextContent) - require.True(t, ok, "expected content to be of type TextContent") + response, err := mcpClient.CallTool(ctx, request) + require.NoError(t, err, "expected to call 'get_me' tool successfully") - 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. - client := github.NewClient(nil).WithAuthToken(e2eServerToken) - user, _, err := client.Users.Get(context.Background(), "") - require.NoError(t, err, "expected to get user successfully") - require.Equal(t, trimmedContent.Login, *user.Login, "expected login to match") - }) + 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 := github.NewClient(nil).WithAuthToken(getE2EToken(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") - require.NoError(t, client.Close(), "expected to close client successfully") } -func buildDockerImage(t *testing.T) { - t.Log("Building Docker image for e2e tests...") +func TestToolsets(t *testing.T) { + t.Parallel() + + mcpClient := setupMCPClient( + t, + WithEnvVars(map[string]string{ + "GITHUB_TOOLSETS": "repos,issues", + }), + ) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + 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 + }) + } - 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() - require.NoError(t, err, "expected to build Docker image successfully, output: %s", string(output)) + 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") } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy