@@ -5,8 +5,11 @@ import (
5
5
"fmt"
6
6
"io"
7
7
"log"
8
+ "net/http"
9
+ "net/url"
8
10
"os"
9
11
"os/signal"
12
+ "strings"
10
13
"syscall"
11
14
12
15
"github.com/github/github-mcp-server/pkg/github"
@@ -15,6 +18,7 @@ import (
15
18
gogithub "github.com/google/go-github/v69/github"
16
19
"github.com/mark3labs/mcp-go/mcp"
17
20
"github.com/mark3labs/mcp-go/server"
21
+ "github.com/shurcooL/githubv4"
18
22
"github.com/sirupsen/logrus"
19
23
)
20
24
@@ -44,25 +48,43 @@ type MCPServerConfig struct {
44
48
}
45
49
46
50
func NewMCPServer (cfg MCPServerConfig ) (* server.MCPServer , error ) {
47
- ghClient := gogithub .NewClient (nil ).WithAuthToken (cfg .Token )
48
- ghClient .UserAgent = fmt .Sprintf ("github-mcp-server/%s" , cfg .Version )
49
-
50
- if cfg .Host != "" {
51
- var err error
52
- ghClient , err = ghClient .WithEnterpriseURLs (cfg .Host , cfg .Host )
53
- if err != nil {
54
- return nil , fmt .Errorf ("failed to create GitHub client with host: %w" , err )
55
- }
51
+ apiHost , err := parseAPIHost (cfg .Host )
52
+ if err != nil {
53
+ return nil , fmt .Errorf ("failed to parse API host: %w" , err )
56
54
}
57
55
56
+ // Construct our REST client
57
+ restClient := gogithub .NewClient (nil ).WithAuthToken (cfg .Token )
58
+ restClient .UserAgent = fmt .Sprintf ("github-mcp-server/%s" , cfg .Version )
59
+ restClient .BaseURL = apiHost .baseRESTURL
60
+ restClient .UploadURL = apiHost .uploadURL
61
+
62
+ // Construct our GraphQL client
63
+ // We're using NewEnterpriseClient here unconditionally as opposed to NewClient because we already
64
+ // did the necessary API host parsing so that github.com will return the correct URL anyway.
65
+ gqlHTTPClient := & http.Client {
66
+ Transport : & bearerAuthTransport {
67
+ transport : http .DefaultTransport ,
68
+ token : cfg .Token ,
69
+ },
70
+ } // We're going to wrap the Transport later in beforeInit
71
+ gqlClient := githubv4 .NewEnterpriseClient (apiHost .graphqlURL .String (), gqlHTTPClient )
72
+
58
73
// When a client send an initialize request, update the user agent to include the client info.
59
74
beforeInit := func (_ context.Context , _ any , message * mcp.InitializeRequest ) {
60
- ghClient . UserAgent = fmt .Sprintf (
75
+ userAgent : = fmt .Sprintf (
61
76
"github-mcp-server/%s (%s/%s)" ,
62
77
cfg .Version ,
63
78
message .Params .ClientInfo .Name ,
64
79
message .Params .ClientInfo .Version ,
65
80
)
81
+
82
+ restClient .UserAgent = userAgent
83
+
84
+ gqlHTTPClient .Transport = & userAgentTransport {
85
+ transport : gqlHTTPClient .Transport ,
86
+ agent : userAgent ,
87
+ }
66
88
}
67
89
68
90
hooks := & server.Hooks {
@@ -83,14 +105,19 @@ func NewMCPServer(cfg MCPServerConfig) (*server.MCPServer, error) {
83
105
}
84
106
85
107
getClient := func (_ context.Context ) (* gogithub.Client , error ) {
86
- return ghClient , nil // closing over client
108
+ return restClient , nil // closing over client
109
+ }
110
+
111
+ getGQLClient := func (_ context.Context ) (* githubv4.Client , error ) {
112
+ return gqlClient , nil // closing over client
87
113
}
88
114
89
115
// Create default toolsets
90
116
toolsets , err := github .InitToolsets (
91
117
enabledToolsets ,
92
118
cfg .ReadOnly ,
93
119
getClient ,
120
+ getGQLClient ,
94
121
cfg .Translator ,
95
122
)
96
123
if err != nil {
@@ -213,3 +240,141 @@ func RunStdioServer(cfg StdioServerConfig) error {
213
240
214
241
return nil
215
242
}
243
+
244
+ type apiHost struct {
245
+ baseRESTURL * url.URL
246
+ graphqlURL * url.URL
247
+ uploadURL * url.URL
248
+ }
249
+
250
+ func newDotcomHost () (apiHost , error ) {
251
+ baseRestURL , err := url .Parse ("https://api.github.com/" )
252
+ if err != nil {
253
+ return apiHost {}, fmt .Errorf ("failed to parse dotcom REST URL: %w" , err )
254
+ }
255
+
256
+ gqlURL , err := url .Parse ("https://api.github.com/graphql" )
257
+ if err != nil {
258
+ return apiHost {}, fmt .Errorf ("failed to parse dotcom GraphQL URL: %w" , err )
259
+ }
260
+
261
+ uploadURL , err := url .Parse ("https://uploads.github.com" )
262
+ if err != nil {
263
+ return apiHost {}, fmt .Errorf ("failed to parse dotcom Upload URL: %w" , err )
264
+ }
265
+
266
+ return apiHost {
267
+ baseRESTURL : baseRestURL ,
268
+ graphqlURL : gqlURL ,
269
+ uploadURL : uploadURL ,
270
+ }, nil
271
+ }
272
+
273
+ func newGHECHost (hostname string ) (apiHost , error ) {
274
+ u , err := url .Parse (hostname )
275
+ if err != nil {
276
+ return apiHost {}, fmt .Errorf ("failed to parse GHEC URL: %w" , err )
277
+ }
278
+
279
+ // Unsecured GHEC would be an error
280
+ if u .Scheme == "http" {
281
+ return apiHost {}, fmt .Errorf ("GHEC URL must be HTTPS" )
282
+ }
283
+
284
+ restURL , err := url .Parse (fmt .Sprintf ("https://api.%s/" , u .Hostname ()))
285
+ if err != nil {
286
+ return apiHost {}, fmt .Errorf ("failed to parse GHEC REST URL: %w" , err )
287
+ }
288
+
289
+ gqlURL , err := url .Parse (fmt .Sprintf ("https://api.%s/graphql" , u .Hostname ()))
290
+ if err != nil {
291
+ return apiHost {}, fmt .Errorf ("failed to parse GHEC GraphQL URL: %w" , err )
292
+ }
293
+
294
+ uploadURL , err := url .Parse (fmt .Sprintf ("https://uploads.%s" , u .Hostname ()))
295
+ if err != nil {
296
+ return apiHost {}, fmt .Errorf ("failed to parse GHEC Upload URL: %w" , err )
297
+ }
298
+
299
+ return apiHost {
300
+ baseRESTURL : restURL ,
301
+ graphqlURL : gqlURL ,
302
+ uploadURL : uploadURL ,
303
+ }, nil
304
+ }
305
+
306
+ func newGHESHost (hostname string ) (apiHost , error ) {
307
+ u , err := url .Parse (hostname )
308
+ if err != nil {
309
+ return apiHost {}, fmt .Errorf ("failed to parse GHES URL: %w" , err )
310
+ }
311
+
312
+ restURL , err := url .Parse (fmt .Sprintf ("%s://%s/api/v3/" , u .Scheme , u .Hostname ()))
313
+ if err != nil {
314
+ return apiHost {}, fmt .Errorf ("failed to parse GHES REST URL: %w" , err )
315
+ }
316
+
317
+ gqlURL , err := url .Parse (fmt .Sprintf ("%s://%s/api/graphql" , u .Scheme , u .Hostname ()))
318
+ if err != nil {
319
+ return apiHost {}, fmt .Errorf ("failed to parse GHES GraphQL URL: %w" , err )
320
+ }
321
+
322
+ uploadURL , err := url .Parse (fmt .Sprintf ("%s://%s/api/uploads/" , u .Scheme , u .Hostname ()))
323
+ if err != nil {
324
+ return apiHost {}, fmt .Errorf ("failed to parse GHES Upload URL: %w" , err )
325
+ }
326
+
327
+ return apiHost {
328
+ baseRESTURL : restURL ,
329
+ graphqlURL : gqlURL ,
330
+ uploadURL : uploadURL ,
331
+ }, nil
332
+ }
333
+
334
+ // Note that this does not handle ports yet, so development environments are out.
335
+ func parseAPIHost (s string ) (apiHost , error ) {
336
+ if s == "" {
337
+ return newDotcomHost ()
338
+ }
339
+
340
+ u , err := url .Parse (s )
341
+ if err != nil {
342
+ return apiHost {}, fmt .Errorf ("could not parse host as URL: %s" , s )
343
+ }
344
+
345
+ if u .Scheme == "" {
346
+ return apiHost {}, fmt .Errorf ("host must have a scheme (http or https): %s" , s )
347
+ }
348
+
349
+ if strings .HasSuffix (u .Hostname (), "github.com" ) {
350
+ return newDotcomHost ()
351
+ }
352
+
353
+ if strings .HasSuffix (u .Hostname (), "ghe.com" ) {
354
+ return newGHECHost (s )
355
+ }
356
+
357
+ return newGHESHost (s )
358
+ }
359
+
360
+ type userAgentTransport struct {
361
+ transport http.RoundTripper
362
+ agent string
363
+ }
364
+
365
+ func (t * userAgentTransport ) RoundTrip (req * http.Request ) (* http.Response , error ) {
366
+ req = req .Clone (req .Context ())
367
+ req .Header .Set ("User-Agent" , t .agent )
368
+ return t .transport .RoundTrip (req )
369
+ }
370
+
371
+ type bearerAuthTransport struct {
372
+ transport http.RoundTripper
373
+ token string
374
+ }
375
+
376
+ func (t * bearerAuthTransport ) RoundTrip (req * http.Request ) (* http.Response , error ) {
377
+ req = req .Clone (req .Context ())
378
+ req .Header .Set ("Authorization" , "Bearer " + t .token )
379
+ return t .transport .RoundTrip (req )
380
+ }
0 commit comments