8000 Extract ghmcp internal package · codingoutloud/github-mcp-server@afc7a93 · GitHub
[go: up one dir, main page]

Skip to content

Commit afc7a93

Browse files
committed
Extract ghmcp internal package
This commit cleanly separates config parsing, stdio server execution and mcp server construction. Aside from significant clarity improvements, it allows for direct construction of the mcp server in e2e tests to allow for breakpoint debugging.
1 parent e56c096 commit afc7a93

File tree

2 files changed

+238
-166
lines changed

2 files changed

+238
-166
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
628C
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
}

0 commit comments

Comments
 (0)
0