1
1
package main
2
2
3
3
import (
4
- "context "
4
+ "errors "
5
5
"fmt"
6
- "io"
7
- stdlog"log"
8
6
"os"
9
- "os/signal"
10
- "syscall"
11
7
8
+ "github.com/github/github-mcp-server/internal/ghmcp"
12
9
"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"
19
10
"github.com/spf13/cobra"
20
11
"github.com/spf13/viper"
21
12
)
22
13
14
+ // These variables are set by the build process using ldflags.
23
15
var version = "version"
24
16
var commit = "commit"
25
17
var date = "date"
@@ -36,36 +28,34 @@ var (
36
28
Use :"stdio" ,
37
29
Short :"Start stdio server" ,
38
30
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" )
46
35
}
47
36
48
37
// If you're wondering why we're not using viper.GetStringSlice("toolsets"),
49
38
// it's because viper doesn't handle comma-separated values correctly for env
50
39
// vars when using GetStringSlice.
51
40
// https://github.com/spf13/viper/issues/380
52
41
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 )
56
44
}
57
45
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" ),
68
56
}
57
+
58
+ return ghmcp .RunStdioServer (stdioServerConfig )
69
59
},
70
60
}
71
61
)
@@ -103,143 +93,9 @@ func initConfig() {
103
93
viper .AutomaticEnv ()
104
94
}
105
95
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
-
240
96
func main () {
241
97
if err := rootCmd .Execute ();err != nil {
242
- fmt .Println ( err )
98
+ fmt .Fprintf ( os . Stderr , "%v \n " , err )
243
99
os .Exit (1 )
244
100
}
245
101
}