Skip to content

Commit e8c022a

Browse files
Merge branch 'main' into main
2 parents d3aa592 + 29bf8bf commit e8c022a

24 files changed

+1291
-292
lines changed

.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.23.7 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: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ automation and interaction capabilities for developers and tools.
1515
## Prerequisites
1616

1717
1. To run the server in a container, you will need to have [Docker](https://www.docker.com/) installed.
18-
2. Once Docker is installed, you will also need to ensure Docker is running.
18+
2. Once Docker is installed, you will also need to ensure Docker is running. The image is public; if you get errors on pull, you may have an expired token and need to `docker logout ghcr.io`.
1919
3. Lastly you will need to [Create a GitHub Personal Access Token](https://github.com/settings/personal-access-tokens/new).
2020
The MCP server can use many of the GitHub APIs, so enable the permissions that you feel comfortable granting your AI tools (to learn more about access tokens, please check out the [documentation](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens)).
2121

@@ -27,10 +27,6 @@ For quick installation, use one of the one-click install buttons at the top of t
2727

2828
For manual installation, add the following JSON block to your User Settings (JSON) file in VS Code. You can do this by pressing `Ctrl + Shift + P` and typing `Preferences: Open User Settings (JSON)`.
2929

30-
Optionally, you can add it to a file called `.vscode/mcp.json` in your workspace. This will allow you to share the configuration with others.
31-
32-
> Note that the `mcp` key is not needed in the `.vscode/mcp.json` file.
33-
3430
```json
3531
{
3632
"mcp": {
@@ -62,6 +58,39 @@ Optionally, you can add it to a file called `.vscode/mcp.json` in your workspace
6258
}
6359
```
6460

61+
Optionally, you can add a similar example (i.e. without the mcp key) to a file called `.vscode/mcp.json` in your workspace. This will allow you to share the configuration with others.
62+
63+
64+
```json
65+
{
66+
"inputs": [
67+
{
68+
"type": "promptString",
69+
"id": "github_token",
70+
"description": "GitHub Personal Access Token",
71+
"password": true
72+
}
73+
],
74+
"servers": {
75+
"github": {
76+
"command": "docker",
77+
"args": [
78+
"run",
79+
"-i",
80+
"--rm",
81+
"-e",
82+
"GITHUB_PERSONAL_ACCESS_TOKEN",
83+
"ghcr.io/github/github-mcp-server"
84+
],
85+
"env": {
86+
"GITHUB_PERSONAL_ACCESS_TOKEN": "${input:github_token}"
87+
}
88+
}
89+
}
90+
}
91+
92+
```
93+
6594
More about using MCP server tools in VS Code's [agent mode documentation](https://code.visualstudio.com/docs/copilot/chat/mcp-servers).
6695

6796
### Usage with Claude Desktop
@@ -498,7 +527,7 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
498527
- `page`: Page number, for files in the commit (number, optional)
499528
- `perPage`: Results per page, for files in the commit (number, optional)
500529

501-
- **search_code** - Search for code across GitHub repositories
530+
- **search_code** - Search for code across GitHub repositories
502531
- `query`: Search query (string, required)
503532
- `sort`: Sort field (string, optional)
504533
- `order`: Sort order (string, optional)
@@ -508,7 +537,7 @@ export GITHUB_MCP_TOOL_ADD_ISSUE_COMMENT_DESCRIPTION="an alternative description
508537
### Users
509538

510539
- **search_users** - Search for GitHub users
511-
- `query`: Search query (string, required)
540+
- `q`: Search query (string, required)
512541
- `sort`: Sort field (string, optional)
513542
- `order`: Sort order (string, optional)
514543
- `page`: Page number (number, optional)

cmd/github-mcp-server/main.go

Lines changed: 28 additions & 164 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,28 +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

48-
enabledToolsets := viper.GetStringSlice("toolsets")
49-
50-
logCommands := viper.GetBool("enable-command-logging")
51-
cfg := runConfig{
52-
readOnly: readOnly,
53-
logger: logger,
54-
logCommands: logCommands,
55-
exportTranslations: exportTranslations,
56-
enabledToolsets: enabledToolsets,
37+
// If you're wondering why we're not using viper.GetStringSlice("toolsets"),
38+
// it's because viper doesn't handle comma-separated values correctly for env
39+
// vars when using GetStringSlice.
40+
// https://github.com/spf13/viper/issues/380
41+
var enabledToolsets []string
42+
if err := viper.UnmarshalKey("toolsets", &enabledToolsets); err != nil {
43+
return fmt.Errorf("failed to unmarshal toolsets: %w", err)
5744
}
58-
if err := runStdioServer(cfg); err != nil {
59-
stdlog.Fatal("failed to run stdio server:", err)
45+
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"),
6056
}
57+
58+
return ghmcp.RunStdioServer(stdioServerConfig)
6159
},
6260
}
6361
)
@@ -95,143 +93,9 @@ func initConfig() {
9593
viper.AutomaticEnv()
9694
}
9795

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

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)
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