Skip to content

Commit 240d41a

Browse files
committed
Merge branch 'main' into docker
2 parents 4ebc718 + 0ca07aa commit 240d41a

File tree

13 files changed

+906
-234
lines changed

13 files changed

+906
-234
lines changed

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
}

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