11package main
22
33import (
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.
2315var version = "version"
2416var commit = "commit"
2517var date = "date"
@@ -36,36 +28,34 @@ var (
3628Use :"stdio" ,
3729Short :"Start stdio server" ,
3830Long :`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
5241var 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() {
10393viper .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-
24096func main () {
24197if err := rootCmd .Execute ();err != nil {
242- fmt .Println ( err )
98+ fmt .Fprintf ( os . Stderr , "%v \n " , err )
24399os .Exit (1 )
244100}
245101}