Skip to content

Commit 635b831

Browse files
committed
Support breakpoint debugging e2e tests
1 parent db8a6c4 commit 635b831

File tree

2 files changed

+88
-56
lines changed

2 files changed

+88
-56
lines changed

e2e/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,12 @@ FAIL github.com/github/github-mcp-server/e2e 1.433s
7777
FAIL
7878
```
7979

80+
## Debugging the Tests
81+
82+
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.
83+
84+
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.
85+
8086
## Limitations
8187

8288
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!

e2e/e2e_test.go

Lines changed: 82 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,15 @@ import (
99
"os"
1010
"os/exec"
1111
"slices"
12+
"strings"
1213
"sync"
1314
"testing"
1415
"time"
1516

16-
"github.com/google/go-github/v69/github"
17+
"github.com/github/github-mcp-server/internal/ghmcp"
18+
"github.com/github/github-mcp-server/pkg/github"
19+
"github.com/github/github-mcp-server/pkg/translations"
20+
gogithub "github.com/google/go-github/v69/github"
1721
mcpClient "github.com/mark3labs/mcp-go/client"
1822
"github.com/mark3labs/mcp-go/mcp"
1923
"github.com/stretchr/testify/require"
@@ -56,68 +60,90 @@ func ensureDockerImageBuilt(t *testing.T) {
5660
require.NoError(t, buildError, "expected to build Docker image successfully")
5761
}
5862

59-
// ClientOpts holds configuration options for the MCP client setup
60-
type ClientOpts struct {
61-
// Environment variables to set before starting the client
62-
EnvVars map[string]string
63+
// clientOpts holds configuration options for the MCP client setup
64+
type clientOpts struct {
65+
// Toolsets to enable in the MCP server
66+
enabledToolsets []string
6367
}
6468

65-
// ClientOption defines a function type for configuring ClientOpts
66-
type ClientOption func(*ClientOpts)
69+
// clientOption defines a function type for configuring ClientOpts
70+
type clientOption func(*clientOpts)
6771

68-
// WithEnvVars returns an option that adds environment variables to the client options
69-
func WithEnvVars(envVars map[string]string) ClientOption {
70-
return func(opts *ClientOpts) {
71-
opts.EnvVars = envVars
72+
// withToolsets returns an option that either sets an Env Var when executing in docker,
73+
// or sets the toolsets in the MCP server when running in-process.
74+
func withToolsets(toolsets []string) clientOption {
75+
return func(opts *clientOpts) {
76+
opts.enabledToolsets = toolsets
7277
}
7378
}
7479

75-
// setupMCPClient sets up the test environment and returns an initialized MCP client
76-
// It handles token retrieval, Docker image building, and applying the provided options
77-
func setupMCPClient(t *testing.T, options ...ClientOption) *mcpClient.Client {
80+
func setupMCPClient(t *testing.T, options ...clientOption) *mcpClient.Client {
7881
// Get token and ensure Docker image is built
7982
token := getE2EToken(t)
80-
ensureDockerImageBuilt(t)
8183

8284
// Create and configure options
83-
opts := &ClientOpts{
84-
EnvVars: make(map[string]string),
85-
}
85+
opts := &clientOpts{}
8686

8787
// Apply all options to configure the opts struct
8888
for _, option := range options {
8989
option(opts)
9090
}
9191

92-
// Prepare Docker arguments
93-
args := []string{
94-
"docker",
95-
"run",
96-
"-i",
97-
"--rm",
98-
"-e",
99-
"GITHUB_PERSONAL_ACCESS_TOKEN", // Personal access token is all required
100-
}
92+
// By default, we run the tests including the Docker image, but with DEBUG
93+
// enabled, we run the server in-process, allowing for easier debugging.
94+
var client *mcpClient.Client
95+
if os.Getenv("GITHUB_MCP_SERVER_E2E_DEBUG") == "" {
96+
ensureDockerImageBuilt(t)
97+
98+
// Prepare Docker arguments
99+
args := []string{
100+
"docker",
101+
"run",
102+
"-i",
103+
"--rm",
104+
"-e",
105+
"GITHUB_PERSONAL_ACCESS_TOKEN", // Personal access token is all required
106+
}
101107

102-
// Add all environment variables to the Docker arguments
103-
for key := range opts.EnvVars {
104-
args = append(args, "-e", key)
105-
}
108+
// Add toolsets environment variable to the Docker arguments
109+
if len(opts.enabledToolsets) > 0 {
110+
args = append(args, "-e", "GITHUB_TOOLSETS")
111+
}
106112

107-
// Add the image name
108-
args = append(args, "github/e2e-github-mcp-server")
113+
// Add the image name
114+
args = append(args, "github/e2e-github-mcp-server")
115+
116+
// Construct the env vars for the MCP Client to execute docker with
117+
dockerEnvVars := make([]string, 0, len(opts.enabledToolsets)+1)
118+
dockerEnvVars = append(dockerEnvVars, fmt.Sprintf("GITHUB_PERSONAL_ACCESS_TOKEN=%s", token))
119+
dockerEnvVars = append(dockerEnvVars, fmt.Sprintf("GITHUB_TOOLSETS=%s", strings.Join(opts.enabledToolsets, ",")))
120+
121+
// Create the client
122+
t.Log("Starting Stdio MCP client...")
123+
var err error
124+
client, err = mcpClient.NewStdioMCPClient(args[0], dockerEnvVars, args[1:]...)
125+
require.NoError(t, err, "expected to create client successfully")
126+
} else {
127+
// We need this because the fully compiled server has a default for the viper config, which is
128+
// not in scope for using the MCP server directly. This probably indicates that we should refactor
129+
// so that there is a shared setup mechanism, but let's wait till we feel more friction.
130+
enabledToolsets := opts.enabledToolsets
131+
if enabledToolsets == nil {
132+
enabledToolsets = github.DefaultTools
133+
}
134+
135+
ghServer, err := ghmcp.NewMCPServer(ghmcp.MCPServerConfig{
136+
Token: token,
137+
EnabledToolsets: enabledToolsets,
138+
Translator: translations.NullTranslationHelper,
139+
})
140+
require.NoError(t, err, "expected to construct MCP server successfully")
109141

110-
// Construct the env vars for the MCP Client to execute docker with
111-
dockerEnvVars := make([]string, 0, len(opts.EnvVars)+1)
112-
dockerEnvVars = append(dockerEnvVars, fmt.Sprintf("GITHUB_PERSONAL_ACCESS_TOKEN=%s", token))
113-
for key, value := range opts.EnvVars {
114-
dockerEnvVars = append(dockerEnvVars, fmt.Sprintf("%s=%s", key, value))
142+
t.Log("Starting In Process MCP client...")
143+
client, err = mcpClient.NewInProcessClient(ghServer)
144+
require.NoError(t, err, "expected to create in-process client successfully")
115145
}
116146

117-
// Create the client
118-
t.Log("Starting Stdio MCP client...")
119-
client, err := mcpClient.NewStdioMCPClient(args[0], dockerEnvVars, args[1:]...)
120-
require.NoError(t, err, "expected to create client successfully")
121147
t.Cleanup(func() {
122148
require.NoError(t, client.Close(), "expected to close client successfully")
123149
})
@@ -169,7 +195,7 @@ func TestGetMe(t *testing.T) {
169195

170196
// Then the login in the response should match the login obtained via the same
171197
// token using the GitHub API.
172-
ghClient := github.NewClient(nil).WithAuthToken(getE2EToken(t))
198+
ghClient := gogithub.NewClient(nil).WithAuthToken(getE2EToken(t))
173199
user, _, err := ghClient.Users.Get(context.Background(), "")
174200
require.NoError(t, err, "expected to get user successfully")
175201
require.Equal(t, trimmedContent.Login, *user.Login, "expected login to match")
@@ -181,9 +207,7 @@ func TestToolsets(t *testing.T) {
181207

182208
mcpClient := setupMCPClient(
183209
t,
184-
WithEnvVars(map[string]string{
185-
"GITHUB_TOOLSETS": "repos,issues",
186-
}),
210+
withToolsets([]string{"repos", "issues"}),
187211
)
188212

189213
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
@@ -208,6 +232,8 @@ func TestToolsets(t *testing.T) {
208232
}
209233

210234
func TestTags(t *testing.T) {
235+
t.Parallel()
236+
211237
mcpClient := setupMCPClient(t)
212238

213239
ctx := context.Background()
@@ -253,32 +279,32 @@ func TestTags(t *testing.T) {
253279
// Cleanup the repository after the test
254280
t.Cleanup(func() {
255281
// MCP Server doesn't support deletions, but we can use the GitHub Client
256-
ghClient := github.NewClient(nil).WithAuthToken(getE2EToken(t))
282+
ghClient := gogithub.NewClient(nil).WithAuthToken(getE2EToken(t))
257283
t.Logf("Deleting repository %s/%s...", currentOwner, repoName)
258284
_, err := ghClient.Repositories.Delete(context.Background(), currentOwner, repoName)
259285
require.NoError(t, err, "expected to delete repository successfully")
260286
})
261287

262288
// Then create a tag
263289
// MCP Server doesn't support tag creation, but we can use the GitHub Client
264-
ghClient := github.NewClient(nil).WithAuthToken(getE2EToken(t))
290+
ghClient := gogithub.NewClient(nil).WithAuthToken(getE2EToken(t))
265291
t.Logf("Creating tag %s/%s:%s...", currentOwner, repoName, "v0.0.1")
266292
ref, _, err := ghClient.Git.GetRef(context.Background(), currentOwner, repoName, "refs/heads/main")
267293
require.NoError(t, err, "expected to get ref successfully")
268294

269-
tagObj, _, err := ghClient.Git.CreateTag(context.Background(), currentOwner, repoName, &github.Tag{
270-
Tag: github.Ptr("v0.0.1"),
271-
Message: github.Ptr("v0.0.1"),
272-
Object: &github.GitObject{
295+
tagObj, _, err := ghClient.Git.CreateTag(context.Background(), currentOwner, repoName, &gogithub.Tag{
296+
Tag: gogithub.Ptr("v0.0.1"),
297+
Message: gogithub.Ptr("v0.0.1"),
298+
Object: &gogithub.GitObject{
273299
SHA: ref.Object.SHA,
274-
Type: github.Ptr("commit"),
300+
Type: gogithub.Ptr("commit"),
275301
},
276302
})
277303
require.NoError(t, err, "expected to create tag object successfully")
278304

279-
_, _, err = ghClient.Git.CreateRef(context.Background(), currentOwner, repoName, &github.Reference{
280-
Ref: github.Ptr("refs/tags/v0.0.1"),
281-
Object: &github.GitObject{
305+
_, _, err = ghClient.Git.CreateRef(context.Background(), currentOwner, repoName, &gogithub.Reference{
306+
Ref: gogithub.Ptr("refs/tags/v0.0.1"),
307+
Object: &gogithub.GitObject{
282308
SHA: tagObj.SHA,
283309
},
284310
})

0 commit comments

Comments
 (0)