Skip to content

Commit 3a6c4ec

Browse files
authored
Merge branch 'main' into issue-350
2 parents 0c0ce8f + 2d6e3dd commit 3a6c4ec

17 files changed

Lines changed: 954 additions & 251 deletions

.dockerignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
.github
2+
.vscode
3+
script
4+
third-party
5+
.dockerignore
6+
.gitignore
7+
**/*.yml
8+
**/*.yaml
9+
**/*.md
10+
**/*_test.go
11+
LICENSE

Dockerfile

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,26 @@
1+
FROM golang:1.24.2-alpine AS build
12
ARG VERSION="dev"
23

3-
FROM golang:1.24.2 AS build
4-
# allow this step access to build arg
5-
ARG VERSION
64
# Set the working directory
75
WORKDIR /build
86

9-
RUN go env -w GOMODCACHE=/root/.cache/go-build
7+
# Install git
8+
RUN --mount=type=cache,target=/var/cache/apk \
9+
apk add git
1010

11-
# Install dependencies
12-
COPY go.mod go.sum ./
13-
RUN --mount=type=cache,target=/root/.cache/go-build go mod download
14-
15-
COPY . ./
1611
# Build the server
17-
RUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 go build -ldflags="-s -w -X main.version=${VERSION} -X main.commit=$(git rev-parse HEAD) -X main.date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
18-
-o github-mcp-server cmd/github-mcp-server/main.go
12+
# go build automatically download required module dependencies to /go/pkg/mod
13+
RUN --mount=type=cache,target=/go/pkg/mod \
14+
--mount=type=cache,target=/root/.cache/go-build \
15+
--mount=type=bind,target=. \
16+
CGO_ENABLED=0 go build -ldflags="-s -w -X main.version=${VERSION} -X main.commit=$(git rev-parse HEAD) -X main.date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
17+
-o /bin/github-mcp-server cmd/github-mcp-server/main.go
1918

2019
# Make a stage to run the app
2120
FROM gcr.io/distroless/base-debian12
2221
# Set the working directory
2322
WORKDIR /server
2423
# Copy the binary from the build stage
25-
COPY --from=build /build/github-mcp-server .
24+
COPY --from=build /bin/github-mcp-server .
2625
# Command to run the server
2726
CMD ["./github-mcp-server", "stdio"]

README.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ GITHUB_TOOLSETS="all" ./github-mcp-server
260260

261261
**Note**: This feature is currently in beta and may not be available in all environments. Please test it out and let us know if you encounter any issues.
262262

263-
Instead of starting with all tools enabled, you can turn on dynamic toolset discovery. Dynamic toolsets allow the MCP host to list and enable toolsets in response to a user prompt. This should help to avoid situations where the model gets confused by the shear number of tools available.
263+
Instead of starting with all tools enabled, you can turn on dynamic toolset discovery. Dynamic toolsets allow the MCP host to list and enable toolsets in response to a user prompt. This should help to avoid situations where the model gets confused by the sheer number of tools available.
264264

265265
### Using Dynamic Tool Discovery
266266

@@ -283,6 +283,27 @@ docker run -i --rm \
283283

284284
The flag `--gh-host` and the environment variable `GITHUB_HOST` can be used to set
285285
the GitHub Enterprise Server hostname.
286+
Prefix the hostname with the `https://` URI scheme, as it otherwise defaults to `http://` which GitHub Enterprise Server does not support.
287+
288+
``` json
289+
"github": {
290+
"command": "docker",
291+
"args": [
292+
"run",
293+
"-i",
294+
"--rm",
295+
"-e",
296+
"GITHUB_PERSONAL_ACCESS_TOKEN",
297+
"-e",
298+
"GITHUB_HOST",
299+
"ghcr.io/github/github-mcp-server"
300+
],
301+
"env": {
302+
"GITHUB_PERSONAL_ACCESS_TOKEN": "${input:github_token}",
303+
"GITHUB_HOST": "https://<your GHES domain name>"
304+
}
305+
}
306+
```
286307

287308
## i18n / Overriding Descriptions
288309

cmd/github-mcp-server/main.go

Lines changed: 22 additions & 166 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,17 @@
11
package main
22

33
import (
4-
"context"
4+
"errors"
55
"fmt"
6-
"io"
7-
stdlog "log"
86
"os"
9-
"os/signal"
10-
"syscall"
117

8+
"github.com/github/github-mcp-server/internal/ghmcp"
129
"github.com/github/github-mcp-server/pkg/github"
13-
iolog "github.com/github/github-mcp-server/pkg/log"
14-
"github.com/github/github-mcp-server/pkg/translations"
15-
gogithub "github.com/google/go-github/v69/github"
16-
"github.com/mark3labs/mcp-go/mcp"
17-
"github.com/mark3labs/mcp-go/server"
18-
log "github.com/sirupsen/logrus"
1910
"github.com/spf13/cobra"
2011
"github.com/spf13/viper"
2112
)
2213

14+
// These variables are set by the build process using ldflags.
2315
var version = "version"
2416
var commit = "commit"
2517
var date = "date"
@@ -36,36 +28,34 @@ var (
3628
Use: "stdio",
3729
Short: "Start stdio server",
3830
Long: `Start a server that communicates via standard input/output streams using JSON-RPC messages.`,
39-
Run: func(_ *cobra.Command, _ []string) {
40-
logFile := viper.GetString("log-file")
41-
readOnly := viper.GetBool("read-only")
42-
exportTranslations := viper.GetBool("export-translations")
43-
logger, err := initLogger(logFile)
44-
if err != nil {
45-
stdlog.Fatal("Failed to initialize logger:", err)
31+
RunE: func(_ *cobra.Command, _ []string) error {
32+
token := viper.GetString("personal_access_token")
33+
if token == "" {
34+
return errors.New("GITHUB_PERSONAL_ACCESS_TOKEN not set")
4635
}
4736

4837
// If you're wondering why we're not using viper.GetStringSlice("toolsets"),
4938
// it's because viper doesn't handle comma-separated values correctly for env
5039
// vars when using GetStringSlice.
5140
// https://github.com/spf13/viper/issues/380
5241
var enabledToolsets []string
53-
err = viper.UnmarshalKey("toolsets", &enabledToolsets)
54-
if err != nil {
55-
stdlog.Fatal("Failed to unmarshal toolsets:", err)
42+
if err := viper.UnmarshalKey("toolsets", &enabledToolsets); err != nil {
43+
return fmt.Errorf("failed to unmarshal toolsets: %w", err)
5644
}
5745

58-
logCommands := viper.GetBool("enable-command-logging")
59-
cfg := runConfig{
60-
readOnly: readOnly,
61-
logger: logger,
62-
logCommands: logCommands,
63-
exportTranslations: exportTranslations,
64-
enabledToolsets: enabledToolsets,
65-
}
66-
if err := runStdioServer(cfg); err != nil {
67-
stdlog.Fatal("failed to run stdio server:", err)
46+
stdioServerConfig := ghmcp.StdioServerConfig{
47+
Version: version,
48+
Host: viper.GetString("host"),
49+
Token: token,
50+
EnabledToolsets: enabledToolsets,
51+
DynamicToolsets: viper.GetBool("dynamic_toolsets"),
52+
ReadOnly: viper.GetBool("read-only"),
53+
ExportTranslations: viper.GetBool("export-translations"),
54+
EnableCommandLogging: viper.GetBool("enable-command-logging"),
55+
LogFilePath: viper.GetString("log-file"),
6856
}
57+
58+
return ghmcp.RunStdioServer(stdioServerConfig)
6959
},
7060
}
7161
)
@@ -103,143 +93,9 @@ func initConfig() {
10393
viper.AutomaticEnv()
10494
}
10595

106-
func initLogger(outPath string) (*log.Logger, error) {
107-
if outPath == "" {
108-
return log.New(), nil
109-
}
110-
111-
file, err := os.OpenFile(outPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
112-
if err != nil {
113-
return nil, fmt.Errorf("failed to open log file: %w", err)
114-
}
115-
116-
logger := log.New()
117-
logger.SetLevel(log.DebugLevel)
118-
logger.SetOutput(file)
119-
120-
return logger, nil
121-
}
122-
123-
type runConfig struct {
124-
readOnly bool
125-
logger *log.Logger
126-
logCommands bool
127-
exportTranslations bool
128-
enabledToolsets []string
129-
}
130-
131-
func runStdioServer(cfg runConfig) error {
132-
// Create app context
133-
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
134-
defer stop()
135-
136-
// Create GH client
137-
token := viper.GetString("personal_access_token")
138-
if token == "" {
139-
cfg.logger.Fatal("GITHUB_PERSONAL_ACCESS_TOKEN not set")
140-
}
141-
ghClient := gogithub.NewClient(nil).WithAuthToken(token)
142-
ghClient.UserAgent = fmt.Sprintf("github-mcp-server/%s", version)
143-
144-
host := viper.GetString("host")
145-
146-
if host != "" {
147-
var err error
148-
ghClient, err = ghClient.WithEnterpriseURLs(host, host)
149-
if err != nil {
150-
return fmt.Errorf("failed to create GitHub client with host: %w", err)
151-
}
152-
}
153-
154-
t, dumpTranslations := translations.TranslationHelper()
155-
156-
beforeInit := func(_ context.Context, _ any, message *mcp.InitializeRequest) {
157-
ghClient.UserAgent = fmt.Sprintf("github-mcp-server/%s (%s/%s)", version, message.Params.ClientInfo.Name, message.Params.ClientInfo.Version)
158-
}
159-
160-
getClient := func(_ context.Context) (*gogithub.Client, error) {
161-
return ghClient, nil // closing over client
162-
}
163-
164-
hooks := &server.Hooks{
165-
OnBeforeInitialize: []server.OnBeforeInitializeFunc{beforeInit},
166-
}
167-
// Create server
168-
ghServer := github.NewServer(version, server.WithHooks(hooks))
169-
170-
enabled := cfg.enabledToolsets
171-
dynamic := viper.GetBool("dynamic_toolsets")
172-
if dynamic {
173-
// filter "all" from the enabled toolsets
174-
enabled = make([]string, 0, len(cfg.enabledToolsets))
175-
for _, toolset := range cfg.enabledToolsets {
176-
if toolset != "all" {
177-
enabled = append(enabled, toolset)
178-
}
179-
}
180-
}
181-
182-
// Create default toolsets
183-
toolsets, err := github.InitToolsets(enabled, cfg.readOnly, getClient, t)
184-
context := github.InitContextToolset(getClient, t)
185-
186-
if err != nil {
187-
stdlog.Fatal("Failed to initialize toolsets:", err)
188-
}
189-
190-
// Register resources with the server
191-
github.RegisterResources(ghServer, getClient, t)
192-
// Register the tools with the server
193-
toolsets.RegisterTools(ghServer)
194-
context.RegisterTools(ghServer)
195-
196-
if dynamic {
197-
dynamic := github.InitDynamicToolset(ghServer, toolsets, t)
198-
dynamic.RegisterTools(ghServer)
199-
}
200-
201-
stdioServer := server.NewStdioServer(ghServer)
202-
203-
stdLogger := stdlog.New(cfg.logger.Writer(), "stdioserver", 0)
204-
stdioServer.SetErrorLogger(stdLogger)
205-
206-
if cfg.exportTranslations {
207-
// Once server is initialized, all translations are loaded
208-
dumpTranslations()
209-
}
210-
211-
// Start listening for messages
212-
errC := make(chan error, 1)
213-
go func() {
214-
in, out := io.Reader(os.Stdin), io.Writer(os.Stdout)
215-
216-
if cfg.logCommands {
217-
loggedIO := iolog.NewIOLogger(in, out, cfg.logger)
218-
in, out = loggedIO, loggedIO
219-
}
220-
221-
errC <- stdioServer.Listen(ctx, in, out)
222-
}()
223-
224-
// Output github-mcp-server string
225-
_, _ = fmt.Fprintf(os.Stderr, "GitHub MCP Server running on stdio\n")
226-
227-
// Wait for shutdown signal
228-
select {
229-
case <-ctx.Done():
230-
cfg.logger.Infof("shutting down server...")
231-
case err := <-errC:
232-
if err != nil {
233-
return fmt.Errorf("error running server: %w", err)
234-
}
235-
}
236-
237-
return nil
238-
}
239-
24096
func main() {
24197
if err := rootCmd.Execute(); err != nil {
242-
fmt.Println(err)
98+
fmt.Fprintf(os.Stderr, "%v\n", err)
24399
os.Exit(1)
244100
}
245101
}

cmd/mcpcurl/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ be executed against the configured MCP server.
1717

1818
## Usage
1919

20-
```bash
20+
```console
2121
mcpcurl --stdio-server-cmd="<command to start MCP server>" <command> [flags]
2222
```
2323

@@ -33,7 +33,7 @@ The `--stdio-server-cmd` flag is required for all commands and specifies the com
3333

3434
List available tools in Anthropic's MCP server:
3535

36-
```bash
36+
```console
3737
% ./mcpcurl --stdio-server-cmd "docker run -i --rm -e GITHUB_PERSONAL_ACCESS_TOKEN mcp/github" tools --help
3838
Contains all dynamically generated tool commands from the schema
3939

@@ -72,7 +72,7 @@ Use "mcpcurl tools [command] --help" for more information about a command.
7272

7373
Get help for a specific tool:
7474

75-
```bash
75+
```console
7676
% ./mcpcurl --stdio-server-cmd "docker run -i --rm -e GITHUB_PERSONAL_ACCESS_TOKEN mcp/github" tools get_issue --help
7777
Get details of a specific issue in a GitHub repository
7878

@@ -93,7 +93,7 @@ Global Flags:
9393

9494
Use one of the tools:
9595

96-
```bash
96+
```console
9797
% ./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
9898
{
9999
"active_lock_reason": null,

e2e/README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,16 @@ 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!
8389

84-
Currently, visibility into failures is not particularly good.
90+
The tests are quite repetitive and verbose. This is intentional as we want to see them develop more before committing to abstractions.
91+
92+
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.

0 commit comments

Comments
 (0)